trykeyline 0.1.1 → 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 (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/bin/keyline.mjs +2 -1
  3. package/package.json +1 -1
  4. package/plugins/keyline/.claude-plugin/plugin.json +1 -1
  5. package/plugins/keyline/plugin.json +1 -1
  6. package/plugins/keyline/skills/keyline/domains/motion/guide.md +1 -0
  7. package/plugins/keyline/skills/keyline/domains/patterns/guide.md +1 -1
  8. package/plugins/keyline/skills/keyline/domains/type/guide.md +1 -1
  9. package/plugins/keyline/skills/keyline/domains/type/ladders.md +1 -1
  10. package/plugins/keyline/skills/keyline/glass/color/assets/color-tokens.css +34 -145
  11. package/plugins/keyline/skills/keyline/glass/color/assets/theme.ts +10 -70
  12. package/plugins/keyline/skills/keyline/glass/color/guide.md +1 -1
  13. package/plugins/keyline/skills/keyline/glass/color/intent-ramp.md +2 -34
  14. package/plugins/keyline/skills/keyline/glass/color/theming.md +4 -65
  15. package/plugins/keyline/skills/keyline/glass/depth/assets/GlassPanel.tsx +14 -121
  16. package/plugins/keyline/skills/keyline/glass/depth/assets/glass.css +29 -188
  17. package/plugins/keyline/skills/keyline/glass/depth/depth-cues.md +1 -16
  18. package/plugins/keyline/skills/keyline/glass/depth/performance-and-fallbacks.md +3 -11
  19. package/plugins/keyline/skills/keyline/glass/layout/adaptive-presentations.md +5 -91
  20. package/plugins/keyline/skills/keyline/glass/layout/assets/SectionNav.tsx +3 -6
  21. package/plugins/keyline/skills/keyline/glass/layout/assets/layout-tokens.css +11 -140
  22. package/plugins/keyline/skills/keyline/glass/layout/guide.md +2 -8
  23. package/plugins/keyline/skills/keyline/glass/layout/measure-and-rhythm.md +0 -2
  24. package/plugins/keyline/skills/keyline/glass/layout/radius-and-elevation.md +1 -1
  25. package/plugins/keyline/skills/keyline/glass/motion/assets/DropMenu.tsx +4 -6
  26. package/plugins/keyline/skills/keyline/glass/motion/assets/motion-presets.ts +10 -184
  27. package/plugins/keyline/skills/keyline/glass/motion/assets/motion-tokens.css +21 -105
  28. package/plugins/keyline/skills/keyline/glass/motion/easing-and-duration.md +2 -31
  29. package/plugins/keyline/skills/keyline/glass/motion/guide.md +3 -3
  30. package/plugins/keyline/skills/keyline/glass/motion/reduced-motion.md +1 -7
  31. package/plugins/keyline/skills/keyline/glass/motion/scroll-performance.md +3 -32
  32. package/plugins/keyline/skills/keyline/glass/motion/sound.md +10 -83
  33. package/plugins/keyline/skills/keyline/glass/type/assets/prose.tsx +8 -43
  34. package/plugins/keyline/skills/keyline/glass/type/assets/type-tokens.css +46 -85
  35. package/plugins/keyline/skills/keyline/glass/type/guide.md +3 -3
  36. package/plugins/keyline/skills/keyline/glass/type/long-form-prose.md +9 -74
  37. package/plugins/keyline/skills/keyline/glass/type/scale.md +2 -6
  38. package/plugins/keyline/skills/keyline/glass/type/weight-and-tracking.md +4 -20
  39. package/plugins/keyline/skills/keyline/scripts/check/css.mjs +5 -14
  40. package/plugins/keyline/skills/keyline/scripts/check/design.mjs +3 -1
  41. package/plugins/keyline/skills/keyline/scripts/check/files.mjs +70 -26
  42. package/plugins/keyline/skills/keyline/scripts/check/markup.mjs +3 -3
  43. package/plugins/keyline/skills/keyline/scripts/check/rules.mjs +9 -1
  44. package/plugins/keyline/skills/keyline/scripts/check.mjs +2 -2
  45. package/plugins/keyline/skills/keyline/scripts/contrast.mjs +2 -0
  46. package/plugins/keyline/skills/keyline/scripts/scan.mjs +29 -45
  47. package/plugins/keyline/skills/keyline/system/guide.md +1 -1
@@ -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,24 +334,24 @@
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;
@@ -398,17 +359,17 @@
398
359
 
399
360
  /* Imported Markdown can't take the authored pull quote's 3px rounded bar
400
361
  (glass/type/assets/prose.tsx), so it keeps a plain 2px rule. */
401
- .prose-imported blockquote {
362
+ .skill-prose blockquote {
402
363
  padding-left: 15px;
403
364
  border-left: 2px solid var(--color-gray-alpha-300);
404
365
  color: var(--color-faint);
405
366
  }
406
- .prose-imported hr {
367
+ .skill-prose hr {
407
368
  margin: 32px 0;
408
369
  border: 0;
409
370
  border-top: 1px solid var(--color-gray-alpha-300);
410
371
  }
411
- .prose-imported img {
372
+ .skill-prose img {
412
373
  max-width: 100%;
413
374
  height: auto;
414
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
 
@@ -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
 
@@ -17,7 +17,7 @@ A modular scale gives too few sizes in the 11–16px band, where Glass needs 11,
17
17
  - 12.5px code. Monospace runs wide, so it steps down to occupy the same measure as the prose around it — the one size that compensates for a *family* rather than a role.
18
18
  - 12px. Tags, prev/next labels, hover-card titles; small enough to read as annotation.
19
19
  - 11px. Tooltips and avatar initials. The floor for anything a user is expected to read.
20
- - 8.5px lettermarks, 7.5px card lettering. Below the floor on purpose — recognised rather than read, which is also why 7.5px is the one place a 600 weight is allowed.
20
+ - 8.5px lettermarks, 7.5px card lettering. Below the floor on purpose — recognised rather than read (the 600 exception: §2 of the guide).
21
21
 
22
22
  **What was deleted**
23
23
 
@@ -44,11 +44,7 @@ In the 11–16px band the steps are 1px, and that is enough (`domains/type/ladde
44
44
 
45
45
  ## Fixed sizes, not `rem`, and why that's a choice
46
46
 
47
- The reference implementation uses `px` throughout (`text-[14px]`, not `text-sm` or `0.875rem`).
48
-
49
- The trade: `rem` respects a user's browser font-size setting and `px` doesn't, a real accessibility cost. But this hierarchy depends on precise 1px relationships in the 11–16 band, a root scaled to 20px turns them into 15px steps that break the ladder, and browser *zoom* — which everyone uses, and which scales `px` fine — already covers the need.
50
-
51
- If your audience skews toward users who set a root font size — documentation, government, healthcare — use `rem` and accept a looser ladder. Convert at a fixed 16px root: 18px → 1.125rem, 14px → 0.875rem, 13px → 0.8125rem. Never mix the two (the domain's unit dial).
47
+ The reference implementation uses `px` throughout (`text-[14px]`, not `text-sm` or `0.875rem`), to keep the 1px steps of the 11–16 band exact under a scaled root; the trade is the domain's unit dial. Moving to `rem`, convert at a fixed 16px root: 18px → 1.125rem, 14px → 0.875rem, 13px → 0.8125rem.
52
48
 
53
49
  ## Keeping every element on it
54
50
 
@@ -16,13 +16,11 @@ All body copy. All prose. Deks. Meta lines. Tags. Tooltips. Timestamps. Quiet na
16
16
  **500 — structure and state.**
17
17
  Headings h1–h4. `<strong>`. The active row in a table of contents. Names and wordmarks. Captions that label an image rather than describe it. Lettermarks. Company/role titles in a list, where the role is structural and the date supporting.
18
18
 
19
- ### Quiet and lifted, by scenario
19
+ The lists are defaults; the choice is made per spot (domain law 5): the same 13px line is 400 as a date under a title and 500 as the name heading a group.
20
20
 
21
- The lists above are defaults; the choice is made per spot, by the rules in domain law 5. The same 13px line is 400 as a date under a title and 500 as the name heading a group, because the job changed, not the importance. With two weights, a step is the whole axis, so when a screen feels heavy the fix is moving lines back to 400, one context at a time; there's never a lighter scale to reach for.
21
+ ### 300
22
22
 
23
- ### When 300 is legitimate
24
-
25
- Rarely, and never for text read at length: at a very large display size (48px+), where 400 starts to look heavy, or a decorative numeral. If your interface tops out at 30px — most do — you don't need it. The reference implementation loaded 300 for a while and never used it; dropping it from the font request was a straight performance win.
23
+ Not loaded. The reference implementation loaded it for a while and never used it; dropping it from the font request was a straight performance win.
26
24
 
27
25
  ### Weight as an accessibility signal
28
26
 
@@ -32,23 +30,9 @@ The active table-of-contents row takes the primary text colour *and* weight 500
32
30
  className={`… ${tone} ${state === "active" ? "font-medium" : ""}`}
33
31
  ```
34
32
 
35
- Weight is cheaper here than a background pill, border or icon, each of which adds visual weight to a list meant to be quiet.
36
-
37
33
  ## Tracking
38
34
 
39
- ### The ladder
40
-
41
- | Size | Tracking | Reasoning |
42
- |---|---|---|
43
- | 7.5px | −0.01em | Card lettering, set solid. The exception below the floor — see the note. |
44
- | 8.5–11px | +0.02em | Tiny glyphs and near-caps need air or the counters close up. |
45
- | 12–14px | 0 | Default metrics are designed for this range. Leave them. |
46
- | 16px | −0.015em | Enough to look set; small enough to be invisible as a decision. |
47
- | 18px | −0.02em | A page title is a tighter, more deliberate object than a section heading two pixels below. |
48
-
49
- Only two rows are the negative-tracking-at-size rule, because the ladder tops out at 18px. An earlier table ran to 30px at −0.025em, with separate values for a 19px heading and a 19px quote; all three sizes left the ladder and took their tracking with them.
50
-
51
- The 7.5px row doesn't contradict the 8.5–11px one: that lettering is set solid at near-caps size on a card, so the tightening compensates for the *setting*, not the size. It's also the one place a 600 weight is allowed. Two exceptions in one place hint you're outside the text system — which you are, since nobody reads it.
35
+ The values are §4 of `glass/type/guide.md`. Only 16px and 18px are the negative-tracking-at-size rule, because the ladder tops out at 18px. The 7.5px card lettering's −0.01em doesn't contradict the lettermarks' +0.02em: it is set solid at near-caps size, so the tightening compensates for the *setting*, not the size.
52
36
 
53
37
  ### The 8.5px lettermark
54
38
 
@@ -13,12 +13,6 @@ export function blankComments(text) {
13
13
  return text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
14
14
  }
15
15
 
16
- export function lineAt(text, offset) {
17
- let line = 1;
18
- for (let i = 0; i < offset && i < text.length; i++) if (text.charCodeAt(i) === 10) line++;
19
- return line;
20
- }
21
-
22
16
  /** Lines start offsets, for fast offset → line lookups on big files. */
23
17
  export function lineIndex(text) {
24
18
  const starts = [0];
@@ -44,8 +38,7 @@ export function parseCss(source, baseLine = 1) {
44
38
  const at = lineIndex(text);
45
39
  const decls = [];
46
40
  const atRules = [];
47
- const blocks = []; // { selector, atRules, decls }
48
- const stack = []; // open blocks: { kind: 'rule'|'at', prelude, block }
41
+ const stack = []; // open blocks: { kind: 'rule'|'at', prelude, block: { decls } }
49
42
  let start = 0;
50
43
  let quote = null;
51
44
  let paren = 0;
@@ -65,7 +58,7 @@ export function parseCss(source, baseLine = 1) {
65
58
  const value = raw.slice(colon + 1).trim();
66
59
  const lead = from + raw.search(/\S/);
67
60
  const ctx = context();
68
- const d = { prop, value, line: at(lead) + baseLine - 1, offset: lead, selector: ctx.selector, atRules: ctx.atRules, block: ctx.block };
61
+ const d = { prop, value, line: at(lead) + baseLine - 1, selector: ctx.selector, atRules: ctx.atRules, block: ctx.block };
69
62
  decls.push(d);
70
63
  ctx.block?.decls.push(d);
71
64
  };
@@ -85,10 +78,8 @@ export function parseCss(source, baseLine = 1) {
85
78
  const prelude = text.slice(start, i).trim().replace(/\s+/g, " ");
86
79
  const kind = prelude.startsWith("@") ? "at" : "rule";
87
80
  const lead = start + text.slice(start, i).search(/\S/);
88
- const block = { selector: kind === "rule" ? prelude : context().selector, atRules: [...context().atRules, ...(kind === "at" ? [prelude] : [])], decls: [], line: at(Math.max(lead, 0)) + baseLine - 1 };
89
- if (kind === "at") atRules.push({ prelude, line: block.line });
90
- blocks.push(block);
91
- stack.push({ kind, prelude, block });
81
+ if (kind === "at") atRules.push({ prelude, line: at(Math.max(lead, 0)) + baseLine - 1 });
82
+ stack.push({ kind, prelude, block: { decls: [] } });
92
83
  start = i + 1;
93
84
  } else if (c === ";") {
94
85
  if (stack.length) pushDecl(start, i);
@@ -103,7 +94,7 @@ export function parseCss(source, baseLine = 1) {
103
94
  start = i + 1;
104
95
  }
105
96
  }
106
- return { decls, atRules, blocks };
97
+ return { decls, atRules };
107
98
  }
108
99
 
109
100
  /** Parse a `style="..."` attribute's body. */
@@ -50,7 +50,9 @@ export function readDesign(root) {
50
50
  // or mentions one — their values are the ladder.
51
51
  const layout = section(md, "Layout");
52
52
  const zLadder = new Set();
53
- for (const row of layout.split("\n").filter((l) => /^\|\s*`?z\./.test(l) || (l.startsWith("|") && /`-?z-\[?-?\d|`z\./.test(l)))) {
53
+ // A project rung (`--z-header`, `$z-header`, a `z-header` utility) counts when it opens the token cell, the row's second.
54
+ const zCell = (c) => /^`(--|\$)?z-[\w-]+`/.test(c);
55
+ for (const row of layout.split("\n").filter((l) => /^\|\s*`?z\./.test(l) || (l.startsWith("|") && (/`-?z-\[?-?\d|`z\./.test(l) || zCell((l.split("|")[2] ?? "").trim()))))) {
54
56
  const cells = row.split("|").slice(1, -1).map((c) => c.trim());
55
57
  for (const c of cells.slice(1)) for (const m of c.matchAll(/(?<![\w.#-])(-?\d+)(?![\w.%])/g)) zLadder.add(Number(m[1]));
56
58
  }