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
@@ -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` is in §5 of the guide; depth law 4 gives each layer's job. In dark mode:
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
 
@@ -30,12 +30,7 @@ backdrop-filter: blur(20px) saturate(1.8);
30
30
 
31
31
  ## 3. The 97% panel
32
32
 
33
- `glass-panel` pairs a 24px blur with **97% opacity** because its menus' wrappers *scale open*, and a transform on an ancestor weakens `backdrop-filter` beneath it (depth law 9): for the whole animation the tint does the work. Where you can, the animating wrapper and the glass surface are different elements:
34
-
35
- ```tsx
36
- <DropMenu open={open}> {/* animates scale + y */}
37
- <ul className="glass-panel …"> {/* carries the blur, never transformed */}
38
- ```
33
+ `glass-panel` pairs a 24px blur with **97% opacity** because its menus' wrappers *scale open*, and a transform on an ancestor weakens `backdrop-filter` beneath it (depth law 9): for the whole animation the tint does the work. Where you can, the animating wrapper and the glass surface are different elements: `DropMenu` scales and moves, the `glass-panel` inside it carries the blur and is never transformed (`glass/motion/assets/DropMenu.tsx`).
39
34
 
40
35
  ## 4. Glass over glass
41
36
 
@@ -53,30 +48,7 @@ Depth law 3's translucent edge is the alpha ramp (`glass/color/guide.md`): `gray
53
48
 
54
49
  ### The shadow stack
55
50
 
56
- Four layers, one light source above (depth law 4), in **one class**, not inline:
57
-
58
- ```css
59
- .dock-shadow {
60
- box-shadow:
61
- inset 0 1px 0 rgba(255, 255, 255, 0.6), /* top-edge catch light */
62
- 0 1px 1px rgba(0, 0, 0, 0.02), /* contact */
63
- 0 8px 16px -4px rgba(0, 0, 0, 0.04), /* near ambient */
64
- 0 24px 32px -8px rgba(0, 0, 0, 0.06); /* far ambient */
65
- }
66
- :root[data-theme="dark"] .dock-shadow {
67
- box-shadow:
68
- inset 0 1px 0 rgba(255, 255, 255, 0.1),
69
- 0 1px 1px rgba(0, 0, 0, 0.3),
70
- 0 8px 16px -4px rgba(0, 0, 0, 0.5),
71
- 0 24px 32px -8px rgba(0, 0, 0, 0.6);
72
- }
73
- ```
74
-
75
- Opacities are 2–6% in light. Dark is a different shadow, not a re-tint (depth law 5): shadows ~10× stronger (4% black on `#0b0b0b` is invisible), highlight ~6× weaker (0.6 white on dark is a glowing edge, not a catch light).
76
-
77
- **Extracted the moment it appeared twice.** This ~400-character string was once inline in five places here, each with a dark twin. Not being a colour, it *can't* be a design token (colour law 10), so a class is its only home.
78
-
79
- → Every elevation tier, the pocket's edge, and stacking inside a glass surface: `glass/depth/depth-cues.md`
51
+ `.dock-shadow`: four layers, one light (depth law 4), 2–6% black in light and a different shadow in dark (depth law 5); one class with its dark twin, not a token or a JS constant. → `glass/depth/assets/glass.css`; tiers, the pocket's edge, stacking: `glass/depth/depth-cues.md`
80
52
 
81
53
  ## 6. A gradient edge, without a wrapper or a mask
82
54
 
@@ -141,54 +113,13 @@ The class drops on `animationend`; under reduced motion, plain accent colour.
141
113
 
142
114
  ## 8. Fallbacks
143
115
 
144
- Depth law 15, at glass's tokens.
145
-
146
- ### `prefers-reduced-transparency`
147
-
148
- ```css
149
- /* AFTER both theme blocks — it re-declares the same properties. */
150
- @media (prefers-reduced-transparency: reduce) {
151
- :root, :root[data-theme="dark"] {
152
- --glass-nav-bg: var(--color-gray-100);
153
- --glass-panel-bg: var(--color-gray-100);
154
- --folder-glass-bg: var(--color-background-100);
155
- }
156
- .glass-nav, .glass-panel, .folder-glass {
157
- backdrop-filter: none;
158
- }
159
- }
160
- ```
161
-
162
- ### `prefers-contrast: more`
163
-
164
- ```css
165
- @media (prefers-contrast: more) {
166
- .glass-nav, .glass-panel, .folder-glass { border-color: var(--color-gray-1000); }
167
- }
168
- ```
169
-
170
- ### `@supports` — the gap in the reference implementation
171
-
172
- The reference implementation has **no `@supports` guard**, so without `backdrop-filter` it gets a bare 72% tint: legible, but weak. `glass/depth/assets/glass.css` ships with the guard:
173
-
174
- ```css
175
- .glass-nav { background: var(--color-gray-100); } /* opaque baseline */
176
-
177
- @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
178
- .glass-nav {
179
- background: var(--glass-nav-bg);
180
- backdrop-filter: blur(20px) saturate(1.8);
181
- }
182
- }
183
- ```
184
-
185
- **Don't hand-write `-webkit-backdrop-filter`.** Tailwind v4's Lightning CSS adds it and keeps only the last of a hand-written pair, so the site ships the prefix alone and Firefox loses every blur (`glass/depth/performance-and-fallbacks.md`).
116
+ Depth law 15, at glass's tokens, in `glass/depth/assets/glass.css`: reduced transparency swaps the tints to `gray-100` / `background-100` and drops the filter, after both theme blocks; `prefers-contrast: more` turns the edge `gray-1000`; an `@supports` guard over a `gray-100` baseline, which the site lacks. **Never hand-write `-webkit-backdrop-filter`**: Lightning CSS keeps only the last of a pair, and Firefox loses every blur (§9).
186
117
 
187
118
  ## 9. Performance
188
119
 
189
120
  Depth law 16, within glass's budget: three or four surfaces at most, each small — a dock a few hundred pixels wide, not a full-viewport overlay — and closed menus unmount.
190
121
 
191
- → What keeps it affordable here, and the prefix bug in detail: `glass/depth/performance-and-fallbacks.md`
122
+ → What keeps it affordable, and the prefix bug: `glass/depth/performance-and-fallbacks.md`
192
123
 
193
124
  ## 10. Confirmations
194
125
 
@@ -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