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,85 +1,20 @@
1
1
  /**
2
- * The glass surface, as a component — every depth cue in one place.
3
- *
4
- * The point of this file is the wrapper/surface split and the depth cues in
5
- * one place. (The elevation stack itself is better as the `.dock-shadow`
6
- * class in glass/depth/assets/glass.css — see the note on the SHADOW constant below.)
7
- * Glass is four properties plus three depth cues, and the cues are the part
8
- * people skip: a blurred tinted rectangle with no hairline and no shadow is a
9
- * patch, not a floating panel.
10
- *
11
- * Pairs with glass/depth/assets/glass.css (the materials) and
2
+ * The glass surface, as a component: the wrapper/surface split and the three
3
+ * depth cues — material, hairline, shadow — in one place. Pairs with
4
+ * glass/depth/assets/glass.css (the materials) and
12
5
  * glass/color/assets/color-tokens.css (the tints and the ramps).
13
6
  */
14
7
 
15
8
  import type { ReactNode } from "react";
16
9
 
17
- /* ── Elevation ────────────────────────────────────────────────────────────
18
- Four layers modelling one light source above the element:
19
-
20
- inset 1px white the specular catch where a raised surface faces the
21
- light. Contributes more to the floating impression
22
- than any of the shadows, and is the one most often
23
- omitted — remove it and the surface looks printed on.
24
- 1px/1px @ 2% contact shadow: resting on something, not pasted over.
25
- 8/16 @ 4% near ambient.
26
- 24/32 @ 6% far ambient. Two layers rather than one because a
27
- single large shadow reads as fog; two fall off like
28
- real penumbra.
29
-
30
- Negative spread (-4px, -8px) contracts the shadow before blurring, keeping
31
- it tucked under the element instead of haloing past its edges. This is the
32
- detail most hand-written shadows miss, and why stock box-shadow presets
33
- often look like a glow.
34
-
35
- Opacities are 2-6%. Elevation should be felt, not seen.
36
-
37
- Dark mode is a DIFFERENT shadow, not the same one re-tinted:
38
- · shadows go up ~10x — 4% black on #0b0b0b is mathematically
39
- invisible; dark needs 30-60% to register at all
40
- · highlight drops ~6x — 0.6 white on a dark surface reads as a glowing
41
- edge rather than a catch light
42
-
43
- Because the STRUCTURE changes and not just the values, elevation cannot be
44
- a single token — it needs a per-theme pair, and this is the one place raw
45
- rgba() outside the token file is acceptable.
46
-
47
- PREFER THE CLASS. `.dock-shadow` in glass/depth/assets/glass.css is the same four
48
- layers in a form you can actually read and diff against its dark twin.
49
- The reference implementation started with the constant below and moved
50
- to the class, for three reasons:
51
-
52
- · it's ~400 characters either way, and four CSS lines beat one string
53
- · a constant still gets imported and interpolated at every call site,
54
- so a grep for the shadow finds five hits rather than one definition
55
- · the arbitrary-value escaping is a live hazard — underscores for
56
- spaces, no spaces inside the parens, and a typo produces a class the
57
- build silently drops: no shadow, no error
58
-
59
- The constant is kept here for codebases that can't add a stylesheet. */
10
+ /* The elevation stack as a utility string, for codebases that can't add a
11
+ stylesheet. Prefer the `.dock-shadow` class in glass.css; the layers and
12
+ why the class won are in glass/depth/depth-cues.md §2. */
60
13
  export const SHADOW =
61
14
  "shadow-[inset_0_1px_0_rgba(255,255,255,0.6),0_1px_1px_rgba(0,0,0,0.02),0_8px_16px_-4px_rgba(0,0,0,0.04),0_24px_32px_-8px_rgba(0,0,0,0.06)] " +
62
15
  "dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.10),0_1px_1px_rgba(0,0,0,0.3),0_8px_16px_-4px_rgba(0,0,0,0.5),0_24px_32px_-8px_rgba(0,0,0,0.6)]";
63
16
 
64
- /** Hover cards and other briefly-floating panels. One ambient layer, no
65
- contact shadow — it's hovering, not resting. */
66
- export const SHADOW_PANEL =
67
- "shadow-[0_8px_24px_-8px_rgba(0,0,0,0.12)] dark:shadow-[0_8px_24px_-8px_rgba(0,0,0,0.6)]";
68
-
69
- /** Tooltips. 30% black in BOTH themes — it sits on bg-gray-1000, which is
70
- near-black in light and near-white in dark, so the SURFACE sets the
71
- requirement rather than the page. Elevation is relative to what's
72
- immediately behind, not to the theme. */
73
- export const SHADOW_TIP = "shadow-[0_4px_10px_-4px_rgba(0,0,0,0.3)]";
74
-
75
- /* ── The hairline ─────────────────────────────────────────────────────────
76
- From the TRANSLUCENT ramp, not a solid grey. A solid border doesn't
77
- participate in what's behind it, so over a photograph it reads as a
78
- pasted-on frame. And because the alpha ramp inverts to white-based in dark
79
- mode, this is automatically lighter than its surface there and darker here.
80
-
81
- 0.5px renders as a true sub-pixel hairline on retina and rounds UP to 1px
82
- elsewhere — it degrades to the next weight rather than disappearing. */
17
+ /* The translucent-ramp hairline (glass/depth/depth-cues.md §1). */
83
18
  const HAIRLINE = "border-[0.5px] border-gray-alpha-400";
84
19
 
85
20
  export type GlassTier = "nav" | "panel";
@@ -87,18 +22,9 @@ export type GlassTier = "nav" | "panel";
87
22
  /**
88
23
  * A glass surface with all three depth cues attached.
89
24
  *
90
- * `tier` picks the material — and the choice is about how little you know
91
- * about the backdrop, not about size or importance:
92
- *
93
- * nav 20px blur, 72% tint. Fixed chrome that arbitrary content scrolls
94
- * under.
95
- * panel 24px blur, 97% tint. A floating menu that must stay legible over
96
- * anything — including over other glass.
97
- *
98
- * `isolate` is on by default because these surfaces usually contain a sliding
99
- * highlight at -z-10 (`glass/motion/guide.md`). Without it, -z-10
100
- * would drop the highlight behind the container's own tint instead of just
101
- * behind the icons.
25
+ * `tier` picks the material (nav 20px / 72%, panel 24px / 97%:
26
+ * glass/depth/guide.md §1). `isolate` is on by default so a sliding highlight
27
+ * at -z-10 stays in front of the tint (glass/depth/depth-cues.md §4).
102
28
  */
103
29
  export function GlassSurface({
104
30
  tier = "panel",
@@ -126,46 +52,13 @@ export function GlassSurface({
126
52
  );
127
53
  }
128
54
 
129
- /* ─────────────────────────────────────────────────────────────────────────
130
- THE WRAPPER / SURFACE SPLIT — the one structural rule to internalise.
131
-
132
- A transform on an ancestor weakens or breaks backdrop-filter in its
133
- subtree: the blur samples the wrong region, or nothing. So an animating
134
- menu must be TWO elements — the wrapper animates, the surface blurs, and
135
- they are never the same node:
55
+ /* The wrapper / surface split: the wrapper animates, the surface blurs, and
56
+ they are never the same node (glass/depth/guide.md §3); closed menus
57
+ unmount (glass/depth/performance-and-fallbacks.md).
136
58
 
137
59
  <DropMenu open={open}> // animates scale + y. No filter.
138
60
  <GlassSurface tier="panel"> // filters. Never transformed.
139
61
  …
140
62
  </GlassSurface>
141
63
  </DropMenu>
142
-
143
- Where you can't split them (a surface that must animate itself), fall back
144
- to glass-panel's 97% tint and treat the blur as decoration.
145
-
146
- This is also a performance win, not just a correctness one: the wrapper
147
- animates on the compositor (transform and opacity only), and the glass
148
- child's filter isn't being recomputed against a moving containing block
149
- every frame.
150
-
151
- Same root cause, second symptom: a transform also makes an element the
152
- containing block for position: fixed descendants. In the reference
153
- implementation the fixed nav strip deliberately carries NO transform, so
154
- the mobile sheet's w-screen full-bleed overlay can span the viewport
155
- instead of being trapped inside the strip's bounds.
156
-
157
- When something filtered or positioned behaves inexplicably, check every
158
- ancestor for: transform, filter, perspective, contain, will-change.
159
-
160
- ── And close menus by UNMOUNTING them ─────────────────────────────────
161
- A backdrop-filter element still pays its full filter pass when it's
162
- transparent — visibility is decided at composite time, after the filter has
163
- already run. opacity: 0 and visibility: hidden do NOT save you; unmounting
164
- (or display: none) is the only thing that does.
165
-
166
- <AnimatePresence>
167
- {open && <m.div …>{children}</m.div>}
168
- </AnimatePresence>
169
-
170
- Which also keeps a closed menu out of the tab order.
171
- ───────────────────────────────────────────────────────────────────────── */
64
+ */
@@ -1,31 +1,14 @@
1
1
  /* ─────────────────────────────────────────────────────────────────────────
2
- Glass materials — three tiers, with every fallback.
2
+ Glass materials — three tiers, with every fallback (glass/depth/guide.md).
3
3
 
4
- PAIRS WITH glass/color/assets/color-tokens.css, which declares the
5
- --glass-*-bg tints and their dark-theme values. This file holds the
6
- material classes; that file holds the tints. Both are needed.
7
-
8
- Why the tints live there and not here: they change per theme AND get
9
- replaced wholesale by the reduced-transparency fallback, so they have to be
10
- runtime :root custom properties rather than build-time tokens. (One of them
11
- is also a gradient, which no colour-token system accepts.)
12
-
13
- Three tiers, not four. The reference implementation also defines a
14
- `glass-menu` (16px / 1.8 / 72%) for "a menu over a known surface" — and
15
- nothing uses it. Every menu turned out to need glass-panel's defensive
16
- tint, either because its wrapper animates or because it opens over other
17
- glass. Dead CSS in a design system is worse than absent CSS: the next
18
- person assumes it's load-bearing.
4
+ Pairs with glass/color/assets/color-tokens.css, which declares the
5
+ --glass-*-bg tints, their dark values and the reduced-transparency tint
6
+ swap. Both files are needed.
19
7
  ───────────────────────────────────────────────────────────────────────── */
20
8
 
21
9
  @layer components {
22
- /* ── Opaque baselines ──────────────────────────────────────────────────
23
- What a browser without backdrop-filter gets. Progressive enhancement in
24
- the correct direction: the opaque version is the DEFAULT and the glass is
25
- layered on top. Invert it and the non-supporting path never gets tested.
26
-
27
- Support is good (Firefox unflagged since 103), so this is insurance
28
- rather than a live concern — but it's cheap insurance. */
10
+ /* ── Opaque baselines: what a browser without backdrop-filter gets. The
11
+ glass is layered on top (depth law 15). */
29
12
  .glass-nav {
30
13
  background: var(--color-gray-100);
31
14
  }
@@ -36,85 +19,33 @@
36
19
  background: var(--color-background-100);
37
20
  }
38
21
 
39
- /* ── The materials ─────────────────────────────────────────────────────
40
- Test BOTH conditions — Safari shipped only the prefixed version for
41
- years, and Safari is where glass matters most.
42
-
43
- Only the standard `backdrop-filter` is written below, on purpose. Let the
44
- build add `-webkit-backdrop-filter` (Lightning CSS in Tailwind v4, or
45
- Autoprefixer). Lightning CSS keeps only the LAST of a hand-written pair,
46
- so writing both, standard first, ships the prefixed one alone — and
47
- Firefox, which only reads the standard property, loses every blur. With
48
- no build step, write the prefixed line first and the standard one last. */
22
+ /* ── The materials. Only the standard `backdrop-filter` is written: let the
23
+ build add the -webkit- line (glass/depth/performance-and-fallbacks.md). */
49
24
  @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
50
- /* Fixed chrome that arbitrary page content scrolls under — a paragraph, a
51
- photograph, a full-bleed image wall. 20px because the less you know
52
- about the backdrop, the more of it has to be suppressed so the
53
- surface's own icons can be read.
54
-
55
- saturate() is NOT decoration. Blur is a convolution, averaging colours
56
- moves them toward grey, so a blurred backdrop is ALWAYS desaturated —
57
- more so at larger radii. Without the boost you get the cloudy
58
- "glassmorphism" wash. This one line is the difference between
59
- translucent and dirty, and it costs nothing: same filter pass. */
25
+ /* Fixed chrome over arbitrary content; saturate() restores what the blur
26
+ greys out (depth law 8). */
60
27
  .glass-nav {
61
28
  background: var(--glass-nav-bg);
62
29
  backdrop-filter: blur(20px) saturate(1.8);
63
30
  }
64
31
 
65
- /* Near-opaque (97%) on purpose, and for three reasons at once:
66
-
67
- 1. Its wrapper ANIMATES. A transform on an ancestor weakens
68
- backdrop-filter in its subtree — the blur samples the wrong region,
69
- or nothing, for the duration. So the TINT carries the legibility and
70
- the blur is a bonus once it settles. General rule: when you can't
71
- rely on the blur, thicken the tint.
72
-
73
- 2. It opens over OTHER GLASS (the nav dock). Two surfaces at the same
74
- tint read as one object — the panel would look like the dock had
75
- grown taller. The heavy tint buys tier separation.
76
-
77
- 3. At 97% you can actually COMPUTE text contrast against the tint. At
78
- 72% the effective background is whatever happens to be behind it,
79
- and the ratio is not computable.
80
-
81
- Where you have the choice, fix it structurally instead: animate the
82
- wrapper, put the glass on a static child. */
32
+ /* 97%: its wrapper animates and it opens over other glass (§3–4 of the
33
+ guide). Animate the wrapper, put the glass on a static child. */
83
34
  .glass-panel {
84
35
  background: var(--glass-panel-bg);
85
36
  backdrop-filter: blur(24px) saturate(1.8);
86
37
  }
87
38
 
88
- /* A frosted pocket with cards peeking out behind it. 8px because here you
89
- WANT the backdrop recognised — blur is softening, not obscuring. And
90
- 1.4 rather than 1.8 because those cards are the subject; pushing their
91
- saturation would make the pocket louder than its own contents.
92
-
93
- Its tint is a gradient (brighter at the top, where the light hits)
94
- rather than a flat fill — the same idea as a 1px inset catch light, at
95
- a scale where a 1px line would be lost. */
39
+ /* A frosted pocket: 8px softens rather than obscures, 1.4 keeps it
40
+ quieter than the cards behind it. The tint is a gradient. */
96
41
  .folder-glass {
97
42
  background: var(--folder-glass-bg);
98
43
  backdrop-filter: blur(8px) saturate(1.4);
99
44
  }
100
45
  }
101
46
 
102
- /* ── prefers-reduced-transparency ──────────────────────────────────────
103
- Two reasons users set this: blur is expensive on low-powered devices,
104
- and translucent surfaces are harder to read. One response serves both —
105
- replace the material with an opaque one.
106
-
107
- BOTH halves are mandatory and each is useless alone:
108
- · filter only → a 72% tint over live content with no blur to suppress
109
- it. WORSE than the glass was; text overlaps text.
110
- · tint only → you still pay the whole cost of the blur, which was
111
- half the reason for the preference.
112
-
113
- The tint half lives in color-tokens.css (it re-declares the :root
114
- variables and must come after both theme blocks). This is the other half.
115
-
116
- Useful side effect: with a solid tint, text contrast becomes real and
117
- computable — for exactly the users most likely to need it. */
47
+ /* ── prefers-reduced-transparency: the filter half. The tint half is in
48
+ color-tokens.css; each is useless alone (depth law 15). */
118
49
  @media (prefers-reduced-transparency: reduce) {
119
50
  .glass-nav,
120
51
  .glass-panel,
@@ -123,15 +54,7 @@
123
54
  }
124
55
  }
125
56
 
126
- /* ── prefers-contrast: more ────────────────────────────────────────────
127
- A translucent surface leans on its hairline to define its edge, and an
128
- 8%-alpha hairline is the first thing to disappear under a contrast
129
- preference. Promote it to primary text colour.
130
-
131
- Note what is NOT changed: the surface itself. High contrast is about
132
- edges and text, not about removing the material. Composes correctly with
133
- reduced-transparency if the user has both — solid surface, strong
134
- border. */
57
+ /* ── prefers-contrast: more — the edge goes to primary; the surface stays. */
135
58
  @media (prefers-contrast: more) {
136
59
  .glass-nav,
137
60
  .glass-panel,
@@ -142,70 +65,13 @@
142
65
  }
143
66
 
144
67
  /* ─────────────────────────────────────────────────────────────────────────
145
- REFERENCE — glass is only half of it. A blurred, tinted rectangle is a
146
- PATCH. Three non-transparency cues turn it into an object, and every glass
147
- surface needs all three. See glass/depth/depth-cues.md.
148
-
149
- 1 · A HAIRLINE, from the TRANSLUCENT ramp:
150
-
151
- border-[0.5px] border-gray-alpha-400
152
-
153
- Not solid grey — a solid border doesn't participate in what's behind
154
- it, so over a photograph it reads as a pasted-on frame. And because
155
- the alpha ramp inverts to white-based in dark mode, the border is
156
- automatically lighter than its surface there and darker here.
157
-
158
- 0.5px renders as a true sub-pixel hairline on retina and rounds UP to
159
- 1px elsewhere — it degrades rather than disappears.
160
-
161
- 2 · A LAYERED SHADOW. Four layers, one light source above:
162
-
163
- inset 0 1px 0 rgba(255,255,255,0.6) top-edge catch light
164
- 0 1px 1px rgba(0,0,0,0.02) contact
165
- 0 8px 16px -4px rgba(0,0,0,0.04) near ambient
166
- 0 24px 32px -8px rgba(0,0,0,0.06) far ambient
167
-
168
- The inset highlight contributes more than any of the shadows and is
169
- the one most often omitted — remove it and the surface looks printed
170
- on rather than raised. Negative spread contracts before blurring, so
171
- the shadow stays tucked under instead of haloing out. Opacities are
172
- 2–6%: if you can identify the shadow as a shadow, it's too strong.
173
-
174
- Dark mode is a DIFFERENT shadow, not a re-tint — shadows ~10x stronger
175
- (4% black on #0b0b0b is mathematically invisible), inset highlight ~6x
176
- weaker (0.6 white on dark reads as a glowing edge). Because the
177
- structure changes, this can't be one token — it's a per-theme pair,
178
- and it belongs in the .dock-shadow class at the foot of this file
179
- rather than repeated inline at every use.
180
-
181
- 3 · CONTINUOUS CORNERS:
182
-
183
- *, *::before, *::after { corner-shape: superellipse(1.5); }
184
-
185
- And nest radii by: INNER = OUTER − the padding between them.
186
- Dock rounded-2xl (16) p-1 (4) → button rounded-[12px] → avatar 10px
187
-
188
- ── Performance, briefly ──────────────────────────────────────────────
189
- Cost scales with AREA and re-runs whenever anything behind it changes —
190
- including every frame of every scroll. Keep surfaces small, keep them few,
191
- UNMOUNT closed menus (a hidden backdrop-filter element still pays its
192
- filter pass; opacity: 0 does not save you), never animate blur(), and
193
- never put glass over video or a flat background.
194
- ───────────────────────────────────────────────────────────────────────── */
195
-
196
- /* ─────────────────────────────────────────────────────────────────────────
197
- The elevation stack, as a class.
198
-
199
- Every floating surface takes this alongside its material class:
68
+ The elevation stack, as a class, beside each material class:
200
69
 
201
- <div class="glass-nav dock-shadow rounded-2xl border-[0.5px] …">
70
+ <div class="glass-nav dock-shadow rounded-2xl border-[0.5px] border-gray-alpha-400 …">
202
71
 
203
- It is NOT a design token, because it isn't a colour — no token system
204
- takes a four-layer box-shadow (see §6 of the Glass colour guide (`glass/color/guide.md`)).
205
- And it can't be one value, because dark mode changes the structure and
206
- not just the numbers. A class is the only place it can live, and putting
207
- it in one is what stops a ~400-character string being copied into five
208
- components with a dark twin each.
72
+ A class, not a token: dark mode changes its structure, not just its
73
+ numbers. The hairline, corners, tiers and why each layer is there:
74
+ glass/depth/depth-cues.md.
209
75
  ───────────────────────────────────────────────────────────────────────── */
210
76
 
211
77
  @layer components {
@@ -228,27 +94,10 @@
228
94
  }
229
95
 
230
96
  /* ─────────────────────────────────────────────────────────────────────────
231
- A gradient 1px stroke — no wrapper element, no mask.
232
-
233
- Two background layers on one box: the fill clipped to the padding edge,
234
- the gradient clipped to the border edge, with a transparent border to
235
- show it through.
236
-
237
- background-origin: border-box IS THE LOAD-BEARING LINE. Without it the
238
- gradient is SIZED to the padding box while being clipped to the border
239
- box, so the stroke gets the wrong end of the ramp on all four sides — a
240
- one-word omission that produces a result which looks almost right.
241
-
242
- AND THE STOPS ARE IN PIXELS, NOT PERCENTAGES. Percentages are relative to
243
- the box, so the ramp rescales whenever the box resizes: a 46px closed bar
244
- ran the entire sweep inside its own height, came out louder than the
245
- 250px panel it opened into, and shimmered through the open animation as
246
- the ramp re-fitted frame by frame. Anchored in pixels the top of the
247
- stroke holds still and the colour simply grows downward.
248
-
249
- Colours come from the Glass colour guide's --sv-* family, which is
250
- that system's one chromatic exception. Delete this block if you don't
251
- have such an element.
97
+ A gradient 1px stroke — no wrapper element, no mask (§6 of the guide).
98
+ background-origin: border-box is load-bearing, and the stops are in
99
+ pixels so the ramp holds still as the panel grows. Colours are the --sv-*
100
+ family in color-tokens.css. Delete this block if you have no such element.
252
101
  ───────────────────────────────────────────────────────────────────────── */
253
102
 
254
103
  @layer components {
@@ -265,16 +114,12 @@
265
114
  background-origin: border-box;
266
115
  background-clip: padding-box, border-box;
267
116
 
268
- /* Four shadows, not one. A single centred blur reads as a drop shadow
269
- that happens to be blue; the two HORIZONTAL casts are what make it
270
- read as light coming off the panel. Same negative spread as the
271
- elevation stack, so the glow stays tucked rather than haloing. */
117
+ /* Two horizontal casts make it read as light, not a blue drop shadow. */
272
118
  box-shadow: var(--sv-glow);
273
119
  transition: box-shadow 220ms var(--ease-geist);
274
120
  }
275
121
 
276
- /* Lifts on hover and STAYS lifted while open, so the colour reports the
277
- same state the chevron does. */
122
+ /* Lifts on hover and stays lifted while open. */
278
123
  .sv-panel:hover,
279
124
  .sv-panel[data-open="true"] {
280
125
  box-shadow: var(--sv-glow-lift);
@@ -286,11 +131,7 @@
286
131
  }
287
132
  }
288
133
 
289
- /* One pass of light across the label as it opens — never a loop. A
290
- repeating shimmer claims the thing is still working, which it isn't.
291
- Drop the class on animationend so the label goes back to painted text:
292
- background-clip: text with color: transparent is a text-selection and
293
- forced-colours hazard to leave in place. */
134
+ /* One pass of light, never a loop; drop the class on animationend. */
294
135
  .sv-sheen {
295
136
  background-image: linear-gradient(
296
137
  100deg,
@@ -20,22 +20,7 @@ Vertical dividers inside the dock are layout's (`glass/layout/radius-and-elevati
20
20
 
21
21
  ## 2. The shadow stack
22
22
 
23
- `.dock-shadow` lives in `glass/depth/assets/glass.css`, both themes side by side; depth law 4 gives each layer's job. Light to dark:
24
-
25
- ```
26
- inset 0 1px 0 rgba(255,255,255,0.10), /* highlight: 0.6 → 0.10 */
27
- 0 1px 1px rgba(0,0,0,0.3), /* shadows: 0.02 → 0.3 */
28
- 0 8px 16px -4px rgba(0,0,0,0.5), /* 0.04 → 0.5 */
29
- 0 24px 32px -8px rgba(0,0,0,0.6) /* 0.06 → 0.6 */
30
- ```
31
-
32
- A 4%-black shadow on a `#0b0b0b` surface is invisible; dark mode needs 30–60% black before a shadow registers. It's also why the dark page is `#0b0b0b`, not `#000000` (depth law 5).
33
-
34
- Because the structure changes and not just the values, elevation **cannot be a single token**. It needs a per-theme pair — the one place raw `rgba()` outside the token file is acceptable — and it belongs in a **CSS class**, not a JS constant:
35
-
36
- ```tsx
37
- <div className="glass-nav dock-shadow rounded-2xl border-[0.5px] border-gray-alpha-400 p-1">
38
- ```
23
+ `.dock-shadow` lives in `glass/depth/assets/glass.css`, both themes side by side; depth law 4 gives each layer's job. Light is 2–6% black with a 0.6 catch light; dark is 30–60% black with the catch light at 0.10, because a 4%-black shadow on `#0b0b0b` is invisible. It's also why the dark page is `#0b0b0b`, not `#000000` (depth law 5). Because the structure changes and not just the values, elevation **cannot be a single token**: it's a per-theme pair — the one place raw `rgba()` outside the token file is acceptable — in a **CSS class**, not a JS constant.
39
24
 
40
25
  The reference implementation started with an exported constant and moved to the class:
41
26
 
@@ -1,6 +1,6 @@
1
1
  # Performance and fallbacks
2
2
 
3
- Glass's budget and fallbacks, for depth laws 15 and 16. The cost model, what to avoid, how to measure, containment and the fallback order in general are in `domains/depth/fallbacks-and-performance.md`; glass's fallback code is in §8 of the guide.
3
+ Glass's budget and fallbacks, for depth laws 15 and 16. The cost model, what to avoid, how to measure, containment and the fallback order in general are in `domains/depth/fallbacks-and-performance.md`.
4
4
 
5
5
  ## What makes it affordable here
6
6
 
@@ -20,17 +20,9 @@ The reference implementation runs three or four glass surfaces and stays smooth
20
20
 
21
21
  **The wrapper animates, the surface filters.** The `DropMenu` split in §3 of the guide is a double win: the wrapper animates on the compositor, and the child's filter isn't recomputed against a moving containing block every frame.
22
22
 
23
- ## Reduced transparency, per material
23
+ ## The fallbacks
24
24
 
25
- The substitutions are per material, not blanket-white: chrome goes to `gray-100`, the raised-surface token, so the dock still reads as raised; the decorative pocket goes to `background-100`, fully opaque, since its whole purpose was translucency. The block names both `:root` and `:root[data-theme="dark"]`, or it wins in one theme only.
26
-
27
- ## `prefers-contrast: more`
28
-
29
- An 8%-alpha hairline is the first thing to disappear under a contrast preference, so the edge goes to `gray-1000`, primary text colour, and the surface stays.
30
-
31
- ## `@supports`
32
-
33
- Without the guard, the reference implementation's surfaces get a bare tint where `backdrop-filter` is missing: legible but weak, marginal at 72% over a photograph. Support is very good (Firefox shipped it unflagged in 103, mid-2022), so it's a small risk, not a live bug. `glass/depth/assets/glass.css` in this guide ships with the guard in place.
25
+ The code is in `glass/depth/assets/glass.css` (baselines, `@supports`, the filter half of reduced transparency, `prefers-contrast`) and `glass/color/assets/color-tokens.css` (the tint half). Two decisions it encodes: the substitutions are per material, not blanket-white — chrome goes to `gray-100` so the dock still reads as raised, the decorative pocket to opaque `background-100` — and the `@supports` guard is one the live site lacks, a small risk given support (Firefox unflagged since 103) but cheap to close.
34
26
 
35
27
  ## The `-webkit-` prefix on the site
36
28
 
@@ -59,65 +59,13 @@ The desktop panel needs no equivalent: it *mounts fresh* on open and reads the v
59
59
 
60
60
  ## Case 2: the table of contents
61
61
 
62
- Same tree, two containers, shared row rendering.
63
-
64
- ```tsx
65
- /** Shared — used by both presentations. */
66
- function Rows({ nodes, active, parents, onJump }) { … }
67
-
68
- /** The sticky rail, in the left margin on wide screens. */
69
- export function SectionNav({ nodes, active, onJump }) {
70
- return (
71
- <nav aria-label="On this page">
72
- <ul className="flex flex-col">
73
- <Rows … />
74
- </ul>
75
- </nav>
76
- );
77
- }
78
-
79
- /** The narrow-screen disclosure — one line above the article. */
80
- export function SectionNavCompact({ nodes, active, onJump }) {
81
- const [open, setOpen] = useState(false);
82
- const jump = (id: string) => { setOpen(false); onJump(id); };
83
- return (
84
- <details open={open} onToggle={(e) => setOpen(e.currentTarget.open)}
85
- className="group rounded-xl border border-gray-alpha-300">
86
- <summary className="… [&::-webkit-details-marker]:hidden">
87
- On this page
88
- <span className="… group-open:rotate-180 motion-reduce:transition-none">
89
- <ChevronDownIcon />
90
- </span>
91
- </summary>
92
- <ul className="… border-t border-gray-alpha-300">
93
- <Rows … onJump={jump} />
94
- </ul>
95
- </details>
96
- );
97
- }
98
- ```
99
-
100
- Both are rendered; CSS picks one:
101
-
102
- ```tsx
103
- <div className="absolute inset-y-0 left-8 hidden w-[196px] xl:block">
104
- <div className="sticky top-28 max-h-[calc(100dvh-11rem)] overflow-y-auto">
105
- <SectionNav {...nav} />
106
- </div>
107
- </div>
108
-
109
- <div className="reveal mt-8 xl:hidden">
110
- <SectionNavCompact {...nav} />
111
- </div>
112
- ```
113
-
114
- The props are memoised so both get a stable object:
62
+ Same tree, two containers, shared row rendering: `glass/layout/assets/SectionNav.tsx`. Both are rendered and CSS picks one — the rail in the 1280px frame from `xl` (§2 of the guide), the compact `<details>` above the article below that (`reveal mt-8 xl:hidden`). The props are memoised so both get one stable object:
115
63
 
116
64
  ```tsx
117
65
  const nav = useMemo(() => ({ nodes: toc, active, onJump: jump }), [active, jump]);
118
66
  ```
119
67
 
120
- The compact version is a `<details>` (layout law 11); `open` is mirrored into React state only so it can close itself after a jump. `[&::-webkit-details-marker]:hidden` plus `list-none` removes the default triangle so the chevron can be styled, and `group-open:rotate-180` reads the parent's `open` attribute, so the rotation needs no state. It costs **one line** above the article, which is what makes a nav in the reading flow acceptable.
68
+ The compact version is a `<details>` (layout law 11), and costs **one line** above the article, which is what makes a nav in the reading flow acceptable.
121
69
 
122
70
  ## Case 3: two views of one collection, chosen by the reader
123
71
 
@@ -125,24 +73,7 @@ The first two cases fork on a *device* capability; the gallery's **wall** and **
125
73
 
126
74
  ### The state can't live in either component
127
75
 
128
- The control and what it controls are siblings, so a third party owns the value:
129
-
130
- ```ts
131
- export type GalleryLayout = "grid" | "roll";
132
- let current: GalleryLayout = "grid";
133
-
134
- const listeners = new Set<(layout: GalleryLayout) => void>();
135
- export function onGalleryLayoutChange(fn: (l: GalleryLayout) => void) {
136
- listeners.add(fn);
137
- return () => { listeners.delete(fn); };
138
- }
139
- export function setGalleryLayout(layout: GalleryLayout) {
140
- current = layout;
141
- for (const fn of listeners) fn(layout);
142
- }
143
- ```
144
-
145
- The shape of a theme or sound store, for the same reason: neither consumer can be the other's parent without distorting the layout.
76
+ The control and what it controls are siblings, so a module store owns the value (its code is §6 of the guide), mirrored in a hook:
146
77
 
147
78
  ```ts
148
79
  // Mirror it in a hook. Read in an EFFECT, not during render: on a
@@ -175,10 +106,6 @@ Find the item nearest the viewport's centre before the outgoing layout unmounts
175
106
 
176
107
  Nothing is cropped, so `object-cover` has nothing to do; nothing shifts on load; and one image travels between layouts undistorted, because both ends compute to the same shape.
177
108
 
178
- ## Hover in Tailwind
179
-
180
- Tailwind v4's `hover:` variant already compiles to a `(hover: hover)` guard, so utility hover states are safe by default. Imperative JavaScript needs the explicit check.
181
-
182
109
  ## The touch-versus-hover audit
183
110
 
184
111
  Glass's answers to the audit in `domains/layout/forks-gating-and-disclosures.md`:
@@ -213,19 +140,6 @@ The last is a hover card *below* its row on narrow screens and *beside* it at `x
213
140
  | `sm` | 640px | Mobile dock ↔ desktop dock. Page padding. Masonry columns. Stack columns. |
214
141
  | `xl` | 1280px | The TOC rail appears. The hover card moves beside its row. |
215
142
 
216
- Two for the whole site, both derived from content. Both are viewport breakpoints only because here the column *is* the page. `sm` coincides with the reading measure: below it the column is padding-limited, the natural place for the layout to change character. `xl` is where a 640px column finally leaves margin for a 196px rail. Type size is *not* on the list: a 30px article title once stepped to 26px below `sm`, until an 18px type ceiling removed the need (`glass/layout/measure-and-rhythm.md`; the Glass type guide, `glass/type/guide.md`).
217
-
218
- ## Breakpoints on the container
219
-
220
- Where a pane shares the screen with something that opens and closes, it breaks on its own width (layout law 8). In Tailwind:
221
-
222
- ```tsx
223
- <section className="@container"> {/* the pane, not the page */}
224
- <div className="grid @2xl:grid-cols-[minmax(240px,1fr)_2fr]">
225
- <List />
226
- <Detail className="@container" /> {/* sizes its own insides */}
227
- </div>
228
- </section>
229
- ```
143
+ Two for the whole site, both derived from content. Both are viewport breakpoints only because here the column *is* the page. `sm` coincides with the reading measure: below it the column is padding-limited, the natural place for the layout to change character. `xl` is where a 640px column finally leaves margin for a 196px rail. Type size is *not* on the list (`glass/type/scale.md`).
230
144
 
231
- Tailwind v4 ships `@container` and the `@sm:`…`@7xl:` variants; plain CSS is `container-type: inline-size` and `@container (min-width: …)`. The list is clamped with `minmax` so it narrows before the detail does, and the nested `@container` lets the detail size its own gutters, rail and labels.
145
+ Glass uses no container breakpoints; where a pane shares the screen with something that opens and closes, see `domains/layout/forks-gating-and-disclosures.md`.