trykeyline 0.1.0 → 0.1.2

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 (73) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/LICENSE +0 -16
  3. package/bin/keyline.mjs +2 -1
  4. package/package.json +1 -1
  5. package/plugins/keyline/.claude-plugin/plugin.json +1 -1
  6. package/plugins/keyline/plugin.json +22 -0
  7. package/plugins/keyline/skills/keyline/SKILL.md +22 -32
  8. package/plugins/keyline/skills/keyline/commands/audit.md +8 -3
  9. package/plugins/keyline/skills/keyline/commands/critique.md +3 -26
  10. package/plugins/keyline/skills/keyline/commands/init.md +5 -4
  11. package/plugins/keyline/skills/keyline/commands/polish.md +2 -2
  12. package/plugins/keyline/skills/keyline/commands/refine.md +6 -16
  13. package/plugins/keyline/skills/keyline/commands/sync.md +1 -3
  14. package/plugins/keyline/skills/keyline/commands/ux-laws.md +26 -0
  15. package/plugins/keyline/skills/keyline/domains/color/deriving-a-ramp.md +1 -1
  16. package/plugins/keyline/skills/keyline/domains/color/guide.md +10 -46
  17. package/plugins/keyline/skills/keyline/domains/color/status-and-exceptions.md +2 -2
  18. package/plugins/keyline/skills/keyline/domains/depth/elevation-recipes.md +2 -8
  19. package/plugins/keyline/skills/keyline/domains/depth/guide.md +12 -41
  20. package/plugins/keyline/skills/keyline/domains/layout/guide.md +31 -56
  21. package/plugins/keyline/skills/keyline/domains/layout/measure-and-rhythm.md +16 -16
  22. package/plugins/keyline/skills/keyline/domains/layout/radius-lines-and-layers.md +2 -2
  23. package/plugins/keyline/skills/keyline/domains/motion/guide.md +12 -47
  24. package/plugins/keyline/skills/keyline/domains/motion/patterns.md +1 -1
  25. package/plugins/keyline/skills/keyline/domains/motion/performance.md +1 -1
  26. package/plugins/keyline/skills/keyline/domains/patterns/guide.md +20 -62
  27. package/plugins/keyline/skills/keyline/domains/patterns/iteration.md +1 -9
  28. package/plugins/keyline/skills/keyline/domains/type/guide.md +13 -37
  29. package/plugins/keyline/skills/keyline/domains/type/ladders.md +1 -1
  30. package/plugins/keyline/skills/keyline/domains/type/type-audit.md +5 -8
  31. package/plugins/keyline/skills/keyline/glass/color/assets/color-tokens.css +34 -165
  32. package/plugins/keyline/skills/keyline/glass/color/assets/theme.ts +10 -70
  33. package/plugins/keyline/skills/keyline/glass/color/guide.md +3 -30
  34. package/plugins/keyline/skills/keyline/glass/color/intent-ramp.md +2 -34
  35. package/plugins/keyline/skills/keyline/glass/color/theming.md +5 -72
  36. package/plugins/keyline/skills/keyline/glass/depth/assets/GlassPanel.tsx +14 -121
  37. package/plugins/keyline/skills/keyline/glass/depth/assets/glass.css +29 -188
  38. package/plugins/keyline/skills/keyline/glass/depth/depth-cues.md +1 -16
  39. package/plugins/keyline/skills/keyline/glass/depth/guide.md +4 -73
  40. package/plugins/keyline/skills/keyline/glass/depth/performance-and-fallbacks.md +3 -11
  41. package/plugins/keyline/skills/keyline/glass/layout/adaptive-presentations.md +5 -91
  42. package/plugins/keyline/skills/keyline/glass/layout/assets/SectionNav.tsx +3 -6
  43. package/plugins/keyline/skills/keyline/glass/layout/assets/layout-tokens.css +11 -138
  44. package/plugins/keyline/skills/keyline/glass/layout/guide.md +10 -15
  45. package/plugins/keyline/skills/keyline/glass/layout/measure-and-rhythm.md +4 -6
  46. package/plugins/keyline/skills/keyline/glass/layout/radius-and-elevation.md +3 -2
  47. package/plugins/keyline/skills/keyline/glass/motion/assets/DropMenu.tsx +4 -6
  48. package/plugins/keyline/skills/keyline/glass/motion/assets/motion-presets.ts +10 -184
  49. package/plugins/keyline/skills/keyline/glass/motion/assets/motion-tokens.css +21 -105
  50. package/plugins/keyline/skills/keyline/glass/motion/easing-and-duration.md +2 -31
  51. package/plugins/keyline/skills/keyline/glass/motion/guide.md +10 -41
  52. package/plugins/keyline/skills/keyline/glass/motion/reduced-motion.md +1 -7
  53. package/plugins/keyline/skills/keyline/glass/motion/scroll-performance.md +3 -32
  54. package/plugins/keyline/skills/keyline/glass/motion/shared-element.md +1 -1
  55. package/plugins/keyline/skills/keyline/glass/motion/sound.md +10 -80
  56. package/plugins/keyline/skills/keyline/glass/type/assets/prose.tsx +8 -43
  57. package/plugins/keyline/skills/keyline/glass/type/assets/type-tokens.css +49 -86
  58. package/plugins/keyline/skills/keyline/glass/type/guide.md +5 -7
  59. package/plugins/keyline/skills/keyline/glass/type/long-form-prose.md +9 -74
  60. package/plugins/keyline/skills/keyline/glass/type/scale.md +2 -6
  61. package/plugins/keyline/skills/keyline/glass/type/weight-and-tracking.md +4 -20
  62. package/plugins/keyline/skills/keyline/scripts/check/css.mjs +5 -14
  63. package/plugins/keyline/skills/keyline/scripts/check/design.mjs +3 -1
  64. package/plugins/keyline/skills/keyline/scripts/check/files.mjs +70 -26
  65. package/plugins/keyline/skills/keyline/scripts/check/markup.mjs +3 -3
  66. package/plugins/keyline/skills/keyline/scripts/check/rules.mjs +9 -1
  67. package/plugins/keyline/skills/keyline/scripts/check.mjs +6 -3
  68. package/plugins/keyline/skills/keyline/scripts/contrast.mjs +2 -0
  69. package/plugins/keyline/skills/keyline/scripts/scan.mjs +29 -45
  70. package/plugins/keyline/skills/keyline/system/design-md.md +4 -4
  71. package/plugins/keyline/skills/keyline/system/guide.md +4 -8
  72. package/plugins/keyline/skills/keyline/system/presets.md +16 -16
  73. package/plugins/keyline/skills/keyline/system/roles.md +8 -8
@@ -74,73 +74,20 @@ export const playClick = () => tok(900, 0.14, 0.13);
74
74
 
75
75
  Hover is higher, shorter and **2.6× quieter**: you cross dozens of links moving around a page, and hover at click volume is intolerable within seconds. Click, a completed action, is louder and lower, since lower pitch reads as more consequential. Roughly a fourth apart, they sound pleasant when a click follows a hover.
76
76
 
77
- ## The autoplay policy
77
+ ## Wiring
78
78
 
79
- An `AudioContext` starts `suspended` and resumes only from a user gesture, so hover sounds stay silent until the reader's first real interaction unlocks it, permanently.
79
+ The rules — arm on every activation gesture, mouse-only and deduplicated hover, a dense-surface opt-out, a persisted mute checked at playback — are `domains/motion/patterns.md`, sound. Glass's values for them:
80
80
 
81
81
  ```ts
82
- export function unlockAudio(): void {
83
- const c = getCtx();
84
- if (c && c.state === "suspended") void c.resume();
85
- }
82
+ const UNLOCK_EVENTS = ["pointerdown", "keydown", "touchstart"] as const; // resume() is idempotent
83
+ const SELECTOR = "a, button, [role='button'], summary"; // one delegated listener set
84
+ if (el && !el.closest("[data-quiet-hover]")) playHover(); // image wall, filmstrip
86
85
  ```
87
86
 
88
- Every playback path checks state rather than assuming:
89
-
90
- ```ts
91
- if (!c || c.state !== "running") return;
92
- ```
93
-
94
- That's a feature, not a workaround: a page you've merely scrolled past makes no sound. Fail silently; never surface an error for this.
95
-
96
- ### Arm it on every activation gesture
97
-
98
- ```ts
99
- const UNLOCK_EVENTS = ["pointerdown", "keydown", "touchstart"] as const;
100
-
101
- /* Idempotent — resume() on a running context is a no-op, which is cheaper
102
- than the bookkeeping to detach these after the first one lands. */
103
- const onUnlock = () => unlockAudio();
104
- for (const type of UNLOCK_EVENTS) {
105
- document.addEventListener(type, onUnlock, { passive: true });
106
- }
107
- ```
108
-
109
- The reference implementation shipped `pointerdown` alone for months. A reader who only moved the mouse across the dock heard nothing — *the hover was the interaction* — which hid because testers click something within seconds; and keyboard-only readers never heard the site. `keydown` is the fix that matters most: the affected group is the one least able to work around it.
110
-
111
- ## Delegation, not per-element wiring
112
-
113
- One set of document listeners covers every link and button on the site.
114
-
115
- ```ts
116
- const SELECTOR = "a, button, [role='button'], summary";
117
-
118
- const onOver = (e: PointerEvent) => {
119
- // Mouse only — touch has no hover, and we don't want a tick on tap.
120
- if (e.pointerType && e.pointerType !== "mouse") return;
121
- const el = (e.target as Element | null)?.closest?.(SELECTOR) ?? null;
122
- if (el === lastHover) return; // don't re-fire while moving within one element
123
- lastHover = el;
124
- if (el) playHover();
125
- };
126
-
127
- document.addEventListener("pointerover", onOver);
128
- document.addEventListener("click", onClick);
129
- ```
130
-
131
- - **`pointerType !== "mouse"` bails out**, or every tap plays both sounds 15ms apart, like a stutter.
132
- - **`lastHover` deduplication.** `pointerover` fires for descendants too, so an icon *inside* a button would re-trigger without the identity check.
133
- - **`closest(SELECTOR)`** covers anything interactive, including elements added later, with no per-component wiring. `summary` is in it because a `<details>` disclosure is a real control that should feel like one.
134
-
135
- ### Dense surfaces opt out of the hover tick
136
-
137
- ```ts
138
- // Surfaces of a hundred small targets (an image wall, a filmstrip) opt out with
139
- // data-quiet-hover: a tick per tile is a machine gun. Clicks still tick.
140
- if (el && !el.closest("[data-quiet-hover]")) playHover();
141
- ```
142
-
143
- One attribute on the container quiets the subtree on hover while clicks stay audible. It's a rate limit, not "this surface is unimportant".
87
+ - Every playback path checks `c.state !== "running"` and fails silently, so a page merely scrolled past makes no sound.
88
+ - The site shipped `pointerdown` alone for months; mouse-only readers and keyboard readers heard nothing, and testers, who click within seconds, never noticed.
89
+ - `summary` is in the selector because a `<details>` disclosure is a real control.
90
+ - Mute persists as `localStorage["ui-sound"] = "off" | "on"`, from an `aria-pressed` control in the nav's settings box beside the theme toggle.
144
91
 
145
92
  ## Sound as a phrase
146
93
 
@@ -166,27 +113,10 @@ export const playLanding = () => tok(587, 0.26, 0.16);
166
113
 
167
114
  It's still a set piece, not a texture — once a session. See §14 of `glass/motion/guide.md`.
168
115
 
169
- ## Mute, and persistence
170
-
171
- ```ts
172
- let muted = localStorage.getItem("ui-sound") === "off";
173
-
174
- export function setMuted(value: boolean): void {
175
- muted = value;
176
- try { localStorage.setItem("ui-sound", value ? "off" : "on"); } catch {}
177
- }
178
- ```
179
-
180
- The mute control sits in the nav's settings box beside the theme toggle, with an `aria-pressed` state and a label.
181
-
182
116
  ## Should the default be on?
183
117
 
184
118
  Arguable. On a personal site where sound is part of the character, on is defensible — the only way most people will discover it, and it's quiet, short and muteable. On anything the user didn't choose to visit (a tool, an app, a client site), default to off. If it's on, the mute control must be *findable within seconds*; that's the price.
185
119
 
186
120
  ## What's deliberately absent
187
121
 
188
- **Haptics.** `navigator.vibrate()` isn't implemented: Safari on iOS lacks it, so it would fire only on Android, and a feedback channel on one platform is worse than none. If you add it, keep it under 10ms and tie it to the same mute preference.
189
-
190
- **Sounds for anything but hover, click and the one greeting phrase** — no navigation, reveal or error sounds. The riffle earns its place by playing once per tab.
191
-
192
- **Pitch variation** by element type or position. It turns an ambient texture into something the user starts trying to interpret.
122
+ **Haptics**: `navigator.vibrate()` would fire only on Android, and a feedback channel on one platform is worse than none. **Pitch variation** by element type or position, which turns an ambient texture into something the user tries to interpret. And no sound beyond hover, click and the one greeting phrase.
@@ -1,33 +1,12 @@
1
1
  /**
2
2
  * Long-form prose renderer — the reading half of the type system.
3
3
  *
4
- * The reading column is TWO SIZES: 16px for headings, 14px for everything a
5
- * reader reads. Which means size does no work below the headings, and the
6
- * blocks sharing 14px have to separate on other axes — each on a different
7
- * one, or they collide:
4
+ * Two sizes: 16px headings, 14px for everything read, so the 14px blocks
5
+ * each separate on a different axis (the table in glass/type/long-form-prose.md).
8
6
  *
9
- * lead leading, 1.9 against the body's 1.75
10
- * keyline weight 500, and 36px of air on both sides
11
- * body 1.75 — the baseline the others are read against
12
- * quote a 3px rounded bar and a 16px indent
13
- * note 13px, the faint tier, and a 1px rule
14
- *
15
- * The rule that produced this: A TIER MUST NEVER OUTRANK THE HEADING IT SITS
16
- * UNDER. An earlier pass ran the keyline at 17px, above the 16px heading it
17
- * belonged to, and the page read as though the paragraphs were shouting over
18
- * the titles. Pulling everything to one size removes any chance of that
19
- * returning.
20
- *
21
- * Spacing is deliberately asymmetric around headings: 40px above, 12px below,
22
- * so a heading belongs to the paragraph that follows it rather than floating
23
- * between two sections. The keyline is the one symmetric block, because it
24
- * belongs to neither neighbour.
25
- *
26
- * Colour comes from the Glass colour guide (`glass/color/guide.md`): `gray-1000`/`gray-900` for
27
- * the two upper text tiers, and the `muted`/`quiet`/`faint` tokens below them
28
- * (which are deliberately NOT ramp steps — 500–800 are border values and fail
29
- * as text in a light theme). `gray-500` stays on decorative marks, which carry
30
- * no text. `reveal` comes from glass/motion.
7
+ * Spacing: 40px above a heading, 12px below; the keyline is the one
8
+ * symmetric block (glass/type/long-form-prose.md). Text tiers come from
9
+ * glass/color/guide.md; `reveal` from glass/motion.
31
10
  */
32
11
 
33
12
  import { Fragment, type ReactNode } from "react";
@@ -188,23 +167,9 @@ export function Prose({ blocks }: { blocks: Block[] }) {
188
167
  }
189
168
 
190
169
  /**
191
- * Inline markdown for **strong**, *em* and [links](/there), mapped onto the
192
- * system's two axes instead of bold and italic:
193
- *
194
- * **strong** → weight 500 + primary colour (advances)
195
- * *em* → muted colour, no slant (recedes)
196
- *
197
- * This gives genuinely two-directional emphasis, which a bold-only system
198
- * can't express. The semantic tags stay — screen readers still announce them.
199
- *
200
- * One caution specific to a two-size reading column: <strong> and the keyline
201
- * are now the same weight at the same size, so a paragraph with a long bolded
202
- * clause reads as a keyline that failed to get its own line. Keep strong
203
- * emphasis to a few words; anything longer wants to BE a keyline.
204
- *
205
- * Links are here because the strongest sentences often live in data rather
206
- * than JSX, and a claim that can't carry a link to its own evidence is just an
207
- * assertion. One link treatment everywhere prose carries one.
170
+ * Inline markdown for **strong** (500 + primary), *em* (muted, no slant) and
171
+ * [links](/there) — §5 of glass/type/guide.md. Keep strong to a few words:
172
+ * at this size it is the keyline's weight.
208
173
  */
209
174
  const TOKENS = /(\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)\s]+\))/g;
210
175
  const LINK = /^\[([^\]]+)\]\(([^)\s]+)\)$/;
@@ -1,24 +1,16 @@
1
1
  /* ─────────────────────────────────────────────────────────────────────────
2
2
  Typographic system — tokens
3
- Drop-in for Tailwind v4 (@theme) or any CSS setup (the :root block).
4
- Swap "Archivo" for your family; keep the ladder.
5
-
6
- The ladder tops out at 18px on purpose. See glass/type/scale.md — a low
7
- ceiling is what forces every template onto one scale, and it's the single
8
- most consequential choice in this system.
3
+ Tailwind v4 @theme tokens; outside Tailwind, put the same custom
4
+ properties on :root. Swap "Archivo" for your family; keep the ladder,
5
+ which tops out at 18px on purpose (glass/type/scale.md).
9
6
  ───────────────────────────────────────────────────────────────────────── */
10
7
 
11
8
  @theme {
12
- /* Self-hosted, with a metric-matched fallback generated by the loader
13
- (next/font, fontaine, or a hand-tuned @font-face with size-adjust). The
14
- system faces after it are belt and braces for a request that never lands.
15
- A bare `display: swap` without metric matching is a reflow under the
16
- reader's eye — see §7 of glass/type/guide.md. */
9
+ /* Self-hosted with a loader-generated metric-matched fallback (§7 of
10
+ glass/type/guide.md). */
17
11
  --font-sans: var(--font-archivo), system-ui, -apple-system, "Helvetica Neue", sans-serif;
18
12
 
19
- /* Code, commands and paths. System stack on purpose — a second webfont for
20
- a handful of monospace runs isn't worth the request. A second family
21
- earns a download only if it's carrying reading copy. */
13
+ /* Code, commands and paths: a system stack on purpose. */
22
14
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
23
15
 
24
16
  /* ── Size ladder ──────────────────────────────────────────────────────
@@ -33,14 +25,10 @@
33
25
  --text-micro: 12px; /* tags · micro-labels */
34
26
  --text-tip: 11px; /* tooltips · initials */
35
27
  --text-mark: 8.5px; /* lettermarks — recognised, not read */
36
- --text-card: 7.5px; /* card lettering — the one place 600 is allowed */
37
-
38
- /* ── Leading: three curves, one per job ───────────────────────────────
39
- Display tightens as size grows. Reading opens as passages lengthen.
40
- UI collapses to 1 so fixed-height boxes centre exactly.
28
+ --text-card: 7.5px; /* card lettering */
41
29
 
42
- With the size ladder this short, leading is a ROLE SIGNAL, not a
43
- refinement: lead / body / dek are the same size and differ only here. */
30
+ /* ── Leading: three curves, one per job (§3 of the guide). With a ladder
31
+ this short, leading is a role signal: lead / body / dek differ only here. */
44
32
  --leading-page: 1.25;
45
33
  --leading-title: 1.3; /* a detail-page title, which wraps to two lines */
46
34
  --leading-heading: 1.2; /* a section heading, which doesn't */
@@ -52,9 +40,7 @@
52
40
  --leading-tight: 1.4;
53
41
  --leading-none: 1;
54
42
 
55
- /* ── Tracking: a large-size correction ────────────────────────────────
56
- Two values, 2px apart, because there are only two sizes above 14px.
57
- A system tracking six steps has a scale doing too much work. */
43
+ /* ── Tracking (§4 of the guide) ───────────────────────────────────── */
58
44
  --tracking-page: -0.02em; /* 18px */
59
45
  --tracking-heading: -0.015em; /* 16px */
60
46
  --tracking-card: -0.01em; /* 7.5px, set solid */
@@ -63,24 +49,11 @@
63
49
  /* ── Weight: two, and only two ─────────────────────────────────────── */
64
50
  --font-weight-normal: 400; /* all body, all prose, all quiet UI */
65
51
  --font-weight-medium: 500; /* headings, <strong>, active state, marks */
66
- /* The one exception, and it lives below the readable floor: at 7.5px a 500
67
- doesn't survive rasterisation, so the choice is 600 or nothing. One
68
- instance of this in a codebase is the right number. */
52
+ /* The one exception: §2 of the guide. */
69
53
  --font-weight-card: 600;
70
54
  }
71
55
 
72
- /* Text colour tiers live in the Glass colour guide (`glass/color/guide.md`). The roles below
73
- reference them by name:
74
- primary → --color-gray-1000 17.93:1
75
- secondary → --color-gray-900 8.45:1
76
- muted → --color-muted 6.48:1
77
- quiet → --color-quiet 5.49:1
78
- faint → --color-faint 4.61:1
79
-
80
- Only the top two are ramp steps. muted/quiet/faint are their OWN tokens,
81
- because a border needs 3:1 against its surface and text needs 4.5:1 — one
82
- step can't serve both. Reusing mid-ramp greys (500-800) for text fails in
83
- a light theme while looking perfectly fine in dark. */
56
+ /* Text tiers (primary → faint) and their contrast: glass/color/guide.md. */
84
57
 
85
58
  @layer base {
86
59
  body {
@@ -89,18 +62,14 @@
89
62
  font-size: var(--text-copy);
90
63
  line-height: var(--leading-body);
91
64
 
92
- /* Tabular by default. In a text-heavy interface most numbers sit in a
93
- column that should align — dates, timestamps, read times, clocks.
94
- A live-updating number is unreadable without this. */
65
+ /* Tabular by default (§6 of the guide). */
95
66
  font-variant-numeric: tabular-nums;
96
67
 
97
68
  -webkit-font-smoothing: antialiased;
98
69
  text-rendering: optimizeLegibility;
99
70
  }
100
71
 
101
- /* Emphasis is re-mapped onto weight and colour. A separate italic file
102
- fights geometric letterforms, and a 700 bold blots open prose. Keep the
103
- semantic tags — only the visual mapping changed. */
72
+ /* Emphasis maps onto weight and colour (§5 of the guide). */
104
73
  em,
105
74
  i {
106
75
  font-style: normal;
@@ -115,11 +84,7 @@
115
84
  /* ─────────────────────────────────────────────────────────────────────────
116
85
  Role classes — optional. Use these, or apply the tokens inline through
117
86
  Tailwind arbitrary values (text-[14px] leading-[1.65]) as the reference
118
- implementation does. Either way the numbers come from one place.
119
-
120
- Note how many of these share --text-copy. That's the system working: once
121
- the ladder is short, a role is a (size, leading, weight, rule) tuple and
122
- size is usually the part that doesn't vary.
87
+ implementation does.
123
88
  ───────────────────────────────────────────────────────────────────────── */
124
89
 
125
90
  @layer components {
@@ -260,27 +225,26 @@
260
225
  }
261
226
 
262
227
  /* ─────────────────────────────────────────────────────────────────────────
263
- Imported prose — a rendered Markdown document, a CMS field, a README.
264
- It arrives as an HTML string, so it can't carry utility classes. Style it
265
- by element inside one scoped class, and map it onto the roles above rather
266
- than letting it invent a second ladder.
228
+ Imported prose — rendered Markdown that can't carry utility classes,
229
+ styled by element in one scoped class on the ladder above (§8 of the
230
+ guide). `.skill-prose` is the class the reference site uses.
267
231
  ───────────────────────────────────────────────────────────────────────── */
268
232
 
269
233
  @layer components {
270
- .prose-imported {
234
+ .skill-prose {
271
235
  font-size: var(--text-copy);
272
236
  line-height: var(--leading-read);
273
237
  color: var(--color-muted);
274
238
  }
275
239
  /* The document starts with whatever it starts with; a leading <h2> would
276
240
  otherwise push 44px of nothing above itself. */
277
- .prose-imported > :first-child {
241
+ .skill-prose > :first-child {
278
242
  margin-top: 0;
279
243
  }
280
244
 
281
245
  /* Three levels, not two — an imported document nests deeper than authored
282
246
  prose, so h4 gets a real role instead of a browser default. */
283
- .prose-imported h2 {
247
+ .skill-prose h2 {
284
248
  margin: 44px 0 12px;
285
249
  font-size: var(--text-section);
286
250
  font-weight: var(--font-weight-medium);
@@ -288,14 +252,14 @@
288
252
  letter-spacing: var(--tracking-heading);
289
253
  color: var(--color-gray-1000);
290
254
  }
291
- .prose-imported h3 {
255
+ .skill-prose h3 {
292
256
  margin: 32px 0 10px;
293
257
  font-size: var(--text-copy);
294
258
  font-weight: var(--font-weight-medium);
295
259
  line-height: var(--leading-tight);
296
260
  color: var(--color-gray-1000);
297
261
  }
298
- .prose-imported h4 {
262
+ .skill-prose h4 {
299
263
  margin: 24px 0 8px;
300
264
  font-size: var(--text-dense);
301
265
  font-weight: var(--font-weight-medium);
@@ -303,23 +267,22 @@
303
267
  color: var(--color-gray-900);
304
268
  }
305
269
 
306
- .prose-imported p,
307
- .prose-imported ul,
308
- .prose-imported ol,
309
- .prose-imported blockquote {
270
+ .skill-prose p,
271
+ .skill-prose ul,
272
+ .skill-prose ol,
273
+ .skill-prose blockquote {
310
274
  margin: 12px 0;
311
275
  }
312
276
 
313
- /* A UA disc at 14px is far too heavy for this system. Same 3px dot as the
314
- hand-rolled bullets everywhere else. */
315
- .prose-imported ul {
277
+ /* The 3px dot of the hand-rolled bullets (glass/type/long-form-prose.md). */
278
+ .skill-prose ul {
316
279
  list-style: none;
317
280
  padding-left: 18px;
318
281
  }
319
- .prose-imported ul > li {
282
+ .skill-prose ul > li {
320
283
  position: relative;
321
284
  }
322
- .prose-imported ul > li::before {
285
+ .skill-prose ul > li::before {
323
286
  content: "";
324
287
  position: absolute;
325
288
  left: -14px;
@@ -329,22 +292,22 @@
329
292
  border-radius: 999px;
330
293
  background: var(--color-gray-500);
331
294
  }
332
- .prose-imported ol {
295
+ .skill-prose ol {
333
296
  list-style: decimal;
334
297
  padding-left: 18px;
335
298
  }
336
299
  /* Tabular, so a run past 9 doesn't shift its text. */
337
- .prose-imported li::marker {
300
+ .skill-prose li::marker {
338
301
  color: var(--color-faint);
339
302
  font-variant-numeric: tabular-nums;
340
303
  }
341
304
 
342
- .prose-imported strong {
305
+ .skill-prose strong {
343
306
  font-weight: var(--font-weight-medium);
344
307
  color: var(--color-gray-1000);
345
308
  }
346
309
 
347
- .prose-imported code {
310
+ .skill-prose code {
348
311
  font-family: var(--font-mono);
349
312
  font-size: 0.87em;
350
313
  background: var(--color-gray-100);
@@ -353,10 +316,8 @@
353
316
  color: var(--color-gray-1000);
354
317
  }
355
318
 
356
- /* Code blocks and tables are the two things a Markdown document has that a
357
- designed page doesn't, and both are unbounded. Each scrolls inside its
358
- own box — the page body must never scroll sideways. */
359
- .prose-imported pre {
319
+ /* Code blocks and tables scroll inside their own box, never the page. */
320
+ .skill-prose pre {
360
321
  margin: 16px 0;
361
322
  padding: 14px 16px;
362
323
  background: var(--color-background-200);
@@ -364,7 +325,7 @@
364
325
  border-radius: 12px;
365
326
  overflow-x: auto;
366
327
  }
367
- .prose-imported pre code {
328
+ .skill-prose pre code {
368
329
  background: none;
369
330
  padding: 0;
370
331
  font-size: var(--text-code);
@@ -373,40 +334,42 @@
373
334
  }
374
335
 
375
336
  /* Wrap each <table> in a div with this class at render time. */
376
- .prose-imported .table-scroll {
337
+ .skill-prose .table-scroll {
377
338
  margin: 16px 0;
378
339
  overflow-x: auto;
379
340
  }
380
- .prose-imported table {
341
+ .skill-prose table {
381
342
  width: 100%;
382
343
  border-collapse: collapse;
383
344
  font-size: var(--text-dense);
384
345
  line-height: 1.5;
385
346
  }
386
- .prose-imported th,
387
- .prose-imported td {
347
+ .skill-prose th,
348
+ .skill-prose td {
388
349
  padding: 8px 12px 8px 0;
389
350
  text-align: left;
390
351
  vertical-align: top;
391
352
  border-bottom: 1px solid var(--color-gray-alpha-300);
392
353
  }
393
- .prose-imported th {
354
+ .skill-prose th {
394
355
  font-weight: var(--font-weight-medium);
395
356
  color: var(--color-gray-1000);
396
357
  white-space: nowrap;
397
358
  }
398
359
 
399
- .prose-imported blockquote {
400
- padding-left: 14px;
360
+ /* Imported Markdown can't take the authored pull quote's 3px rounded bar
361
+ (glass/type/assets/prose.tsx), so it keeps a plain 2px rule. */
362
+ .skill-prose blockquote {
363
+ padding-left: 15px;
401
364
  border-left: 2px solid var(--color-gray-alpha-300);
402
365
  color: var(--color-faint);
403
366
  }
404
- .prose-imported hr {
367
+ .skill-prose hr {
405
368
  margin: 32px 0;
406
369
  border: 0;
407
370
  border-top: 1px solid var(--color-gray-alpha-300);
408
371
  }
409
- .prose-imported img {
372
+ .skill-prose img {
410
373
  max-width: 100%;
411
374
  height: auto;
412
375
  border-radius: 12px;
@@ -33,7 +33,7 @@ Each role gets a size in steps of 1–2px, as densely as the roles demand (domai
33
33
 
34
34
  Text tiers (`primary` → `quiet`) come from the Glass colour guide (`glass/color/guide.md`). Only the top two are grey-ramp steps (`gray-1000`, `gray-900`); `muted` / `quiet` / `faint` are separate tokens picked for contrast (colour law 2).
35
35
 
36
- **Eighteen is the ceiling, and it's lower than you think.** An earlier ladder topped out at 30px, with a 19px prose heading, a 17px dek and a 16px prose body. Case studies and articles opened at 30px and every other page at 18px, so a case study felt like a different site (domain law 2). The rest of the site ran on **two sizes**, 16 for headings and 14 for everything read; the detail templates came down to those, and the four sizes were deleted, not replaced. One element likewise takes one role on every page, and sibling controls match (domain law 2).
36
+ **Eighteen is the ceiling, and it's lower than you think.** The site runs on **two sizes**, 16 for headings and 14 for everything read; the detail templates that once opened two steps above every other page came down to those (domain law 2; the deleted sizes are in `glass/type/scale.md`). One element likewise takes one role on every page, and sibling controls match (domain law 2).
37
37
 
38
38
  ### When size stops doing the work
39
39
 
@@ -50,7 +50,7 @@ At one size, the 17px keyline that once shouted over a 16px heading can't return
50
50
 
51
51
  The cost is a flatter page: titles and section headings are both 16px, separated by position and spacing. It's the right flat, because the home page already worked that way and the detail pages were the outlier.
52
52
 
53
- **Nothing in this ladder resizes.** The old step, a 30px title dropping to 26px below `sm`, went with the size that needed it (the domain's responsive-sizes dial).
53
+ **Nothing in this ladder resizes** (the domain's responsive-sizes dial).
54
54
 
55
55
  → Full derivation, including how to add a role without bloating the ladder: `glass/type/scale.md`
56
56
 
@@ -67,7 +67,7 @@ Quiet or lifted is set by what the line is doing there, one lift per group, one
67
67
 
68
68
  A contents rail's active row takes `font-medium` *and* primary colour, so state is never on colour alone; no state greys a title (domain laws 3 and 5).
69
69
 
70
- → Weight and tracking in depth, including when 300 is legitimate: `glass/type/weight-and-tracking.md`
70
+ → Weight and tracking in depth: `glass/type/weight-and-tracking.md`
71
71
 
72
72
  ## 3. Leading runs inverse to size
73
73
 
@@ -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
 
@@ -10,7 +10,7 @@ How Glass sets the reading half: looser leading and spacing than anything else o
10
10
 
11
11
  Prose body is 14/1.75: a UI paragraph's size, noticeably more open (1.75 vs 1.55).
12
12
 
13
- Why 1.75: a 576px text column at 14px holds roughly 85–95 characters a line, past comfortable, and long lines need more leading for the eye to find the next line's start. A shorter measure could pull it back toward 1.6.
13
+ Why 1.75: the ~90-character measure (`glass/layout/measure-and-rhythm.md`). A shorter measure could pull it back toward 1.6.
14
14
 
15
15
  ## The reading column is two sizes
16
16
 
@@ -24,25 +24,9 @@ Why 1.75: a 576px text column at 14px holds roughly 85–95 characters a line, p
24
24
  | Pull quote | 14 | a 3px rounded bar and a 16px indent |
25
25
  | Note | 13 | a 1px rule, the faint tier, and a step down in size |
26
26
 
27
- ## Keyline and note
27
+ ## The blocks, in code
28
28
 
29
- Both came from one diagnosis: a body of writing that was 114 paragraphs and almost nothing else. What each is for, and the build check that keeps them honest: `domains/type/prose-and-emphasis.md`, **Two block types worth having**.
30
-
31
- **`keyline`** — body size, weight 500, `my-9`.
32
-
33
- ```tsx
34
- case "keyline":
35
- return <p className="reveal my-9 text-[14px] font-medium leading-[1.6] text-gray-1000">…</p>;
36
- ```
37
-
38
- **`note`** — 13px, faint tier, a 1px left rule and 16px of indent.
39
-
40
- ```tsx
41
- case "note":
42
- return <p className="reveal my-7 border-l border-gray-alpha-300 pl-4 text-[13px] leading-[1.6] text-faint">…</p>;
43
- ```
44
-
45
- The note's rule is 1px and the quote's 3px, with the type inside four steps apart, so the two indented rules can't be mistaken for each other.
29
+ Every block's classes — lead, body, keyline, note, h2, quote, figure, lists — are in `glass/type/assets/prose.tsx`, with the reason for each beside it. Keyline and note both came from one diagnosis, a body of writing that was 114 paragraphs and almost nothing else; what each is for, and the build check that keeps them honest: `domains/type/prose-and-emphasis.md`, **Two block types worth having**.
46
30
 
47
31
  ## Block spacing
48
32
 
@@ -59,61 +43,12 @@ The note's rule is 1px and the quote's 3px, with the type inside four steps apar
59
43
 
60
44
  The heading's 40 above and 12 below, ~3.3:1, is the domain's asymmetric heading space; the keyline is its symmetric exception.
61
45
 
62
- ## Headings inside prose
63
-
64
- ```tsx
65
- <h2 className="mt-10 mb-3 text-[16px] font-medium leading-[1.2] tracking-[-0.015em]">
66
- ```
67
-
68
- 16px against 14px body is one step; the weight (500), the tight leading (1.2) and the 40px above do the real work.
69
-
70
- It matches a page-level section heading and the detail page's own `<h1>`: three levels at one size, separated by position and spacing. Flatter than most article templates, and deliberate — the alternative was an in-body heading larger than the page title.
71
-
72
- ## Quotes
73
-
74
- ```tsx
75
- <blockquote className="my-9 flex gap-4">
76
- <span aria-hidden className="w-[3px] shrink-0 rounded-full bg-gray-500" />
77
- <div className="text-[14px] leading-[1.75] text-gray-1000">…</div>
78
- </blockquote>
79
- ```
80
-
81
- The type matches the body around it, so the bar is the entire signal: the 2px `gray-alpha-300` border it replaced is domain law 4's example of auditing what's left after a signal goes. Why a flex child with no vertical margin: `domains/type/prose-and-emphasis.md`, **Quotes**.
82
-
83
- - **`bg-gray-500`, from the grey ramp, not the alpha ramp.** It's a decorative mark on an opaque surface — see the Glass colour guide (`glass/color/guide.md`) on why decorative marks stay on the solid ramp.
84
-
85
- Still absent: italic (globally disabled), quotation marks, background, colour step. Stepping the quote *down* to secondary, as earlier versions did, at body size just makes a de-emphasised paragraph.
86
-
87
- The citation goes quieter and explicitly kills the browser's italic (domain law 9):
88
-
89
- ```tsx
90
- <cite className="mt-2 block text-[13px] not-italic text-quiet">— {cite}</cite>
91
- ```
92
-
93
- ## Figures
94
-
95
- ```tsx
96
- <figure className="reveal my-9">
97
- <div className="overflow-hidden rounded-[10px] bg-gray-100 ring-1 ring-gray-alpha-300">
98
- <div style={{ aspectRatio: b.aspect ?? "16 / 9" }}>
99
- <img src={b.src} alt={b.alt} loading="lazy" decoding="async" className="h-full w-full object-cover" />
100
- </div>
101
- </div>
102
- <figcaption className="mt-2.5 text-[13px] leading-[1.55] text-faint">{caption}</figcaption>
103
- </figure>
104
- ```
105
-
106
- The aspect ratio sits on a wrapper so the box is reserved before the image loads. `ring-1` rather than a border, so the outline stays out of the box model and the corner radius stays exact.
107
-
108
- **The caption is the point of the figure, not a label on it**: what the thing was for, and which constraint shaped it. It sits at the note/meta tier (13px, faint).
109
-
110
- ## Lists
111
-
112
- ```tsx
113
- <ul className="mb-6 list-disc space-y-2 pl-5 marker:text-gray-500">
114
- ```
46
+ ## What the code doesn't say
115
47
 
116
- The `marker:` variant puts the domain's quiet marker (`domains/type/prose-and-emphasis.md`, **Lists**) on `gray-500`; the same goes for `list-decimal`.
48
+ - **Prose headings** match a page-level section heading and the detail page's own `<h1>`: three levels at 16px, separated by position and spacing. The alternative was an in-body heading larger than the page title.
49
+ - **The pull quote's bar is the entire signal**, since its type matches the body: the 2px `gray-alpha-300` border it replaced is domain law 4's example of auditing what's left after a signal goes. `bg-gray-500` is from the solid ramp, a decorative mark on an opaque surface (`glass/color/guide.md`). Still absent: italic, quotation marks, background, a colour step.
50
+ - **A figure's caption is the point of the figure**, not a label on it: what the thing was for, and which constraint shaped it.
51
+ - **Authored lists keep the UA marker** (`list-disc` / `list-decimal`) on `gray-500`, the domain's quiet marker; imported Markdown uses the 3px dot instead (§8 of `glass/type/guide.md`). Both are what the site ships.
117
52
 
118
53
  ## Hand-rolled bullets in UI context
119
54
 
@@ -144,7 +79,7 @@ Every block carries `reveal`, so paragraphs fade up one by one as they enter the
144
79
  <p className="mt-2 text-[14px] leading-[1.55] text-muted">{dek}</p>
145
80
  ```
146
81
 
147
- Body size, muted, tighter leading. It was 17px, the ladder's one odd number, sitting between a 30px title and 16px prose; with both gone it separates on colour and leading alone: 1.55, against the body's 1.75 and the lead's 1.9. Dek and lead are the domain's tightest pairing; check them first.
82
+ Body size, muted, tighter leading: it separates on colour and leading alone, 1.55 against the body's 1.75 and the lead's 1.9. Dek and lead are the domain's tightest pairing; check them first.
148
83
 
149
84
  ## Cover art and the measure
150
85