@fluid-theme/core 0.4.3 → 2.0.0

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 (80) hide show
  1. package/AGENTS.md +6 -29
  2. package/CHANGELOG.md +543 -2
  3. package/CLAUDE.md +193 -14
  4. package/GEMINI.md +6 -45
  5. package/README.md +17 -8
  6. package/assets/js/compare.js +77 -0
  7. package/assets/js/fit-text.js +103 -0
  8. package/assets/js/lightbox.js +153 -0
  9. package/assets/js/scroll-reveal.js +72 -0
  10. package/assets/js/toc-build.mjs +86 -0
  11. package/assets/js/toc.js +384 -0
  12. package/dist/fluid-1x.min.css +4 -0
  13. package/dist/fluid-fonts.min.css +2 -0
  14. package/dist/fluid-legacy-1x.css +3 -0
  15. package/dist/fluid-nav.min.css +3 -0
  16. package/dist/fluid-toc.min.css +3 -0
  17. package/dist/fluid-zero.min.css +3 -0
  18. package/dist/fluid.min.css +3 -2
  19. package/dist/fonts/OFL-newsreader.txt +93 -0
  20. package/dist/fonts/OFL-schibsted-grotesk.txt +93 -0
  21. package/dist/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
  22. package/dist/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  23. package/dist/fonts/schibsted-grotesk-latin-wght-italic.woff2 +0 -0
  24. package/dist/fonts/schibsted-grotesk-latin-wght-normal.woff2 +0 -0
  25. package/fluid-agent-kit/README.md +1 -0
  26. package/fluid-agent-kit/skills/accessibility.md +3 -1
  27. package/fluid-agent-kit/skills/css-conventions.md +22 -5
  28. package/fluid-agent-kit/skills/image-optimization.md +2 -0
  29. package/fluid-agent-kit/skills/preset-composition.md +108 -0
  30. package/package.json +29 -3
  31. package/src/animations/scroll-reveal.css +75 -193
  32. package/src/components/accordion.css +6 -47
  33. package/src/components/alert.css +47 -88
  34. package/src/components/avatar.css +4 -20
  35. package/src/components/badge.css +25 -48
  36. package/src/components/button.css +69 -175
  37. package/src/components/card.css +146 -481
  38. package/src/components/chip.css +46 -12
  39. package/src/components/compare.css +234 -0
  40. package/src/components/dropdown.css +10 -67
  41. package/src/components/empty-state.css +4 -0
  42. package/src/components/form.css +113 -81
  43. package/src/components/gallery.css +252 -0
  44. package/src/components/hero.css +203 -88
  45. package/src/components/indicator.css +23 -117
  46. package/src/components/loading.css +7 -29
  47. package/src/components/modal.css +12 -65
  48. package/src/components/nav-modes.css +261 -0
  49. package/src/components/nav.css +324 -392
  50. package/src/components/search-input.css +4 -2
  51. package/src/components/step-indicator.css +26 -27
  52. package/src/components/table.css +64 -171
  53. package/src/components/toc.css +254 -0
  54. package/src/components/tooltip.css +5 -46
  55. package/src/core/base.css +393 -547
  56. package/src/core/reset.css +17 -5
  57. package/src/core/tokens.css +426 -2276
  58. package/src/fluid.css +258 -17
  59. package/src/fonts/OFL-newsreader.txt +93 -0
  60. package/src/fonts/OFL-schibsted-grotesk.txt +93 -0
  61. package/src/fonts/fluid-fonts.css +55 -0
  62. package/src/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
  63. package/src/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  64. package/src/fonts/schibsted-grotesk-latin-wght-italic.woff2 +0 -0
  65. package/src/fonts/schibsted-grotesk-latin-wght-normal.woff2 +0 -0
  66. package/src/layouts/positioning.css +6 -3
  67. package/src/legacy-1x.css +61 -0
  68. package/src/sections/cta.css +33 -101
  69. package/src/sections/features.css +42 -219
  70. package/src/sections/footer.css +53 -134
  71. package/src/sections/stats.css +40 -153
  72. package/src/sections/team.css +24 -113
  73. package/src/sections/testimonials.css +34 -148
  74. package/src/zero/base.css +166 -0
  75. package/src/zero/tokens.css +64 -0
  76. package/src/zero.css +20 -0
  77. package/src/components/scroll-progress.css +0 -104
  78. package/src/components/settings-panel.css +0 -603
  79. package/src/components/themed-images.css +0 -229
  80. package/src/components/version-badge.css +0 -70
@@ -1,21 +1,25 @@
1
1
  /**
2
- * Fluid Framework - Hero Component
2
+ * Fluid Theme - Hero Component
3
3
  * @layer components
4
4
  *
5
- * Full-width hero sections for landing pages and page headers.
6
- * Uses the positioning system for layout - just add data attributes.
7
- *
8
- * Philosophy:
9
- * - The hero is a canvas, not a constraint
10
- * - Backgrounds, overlays, and content are separate concerns
11
- * - Serene presence, not aggressive attention-seeking
5
+ * Plain background, start-aligned, content-tall. Order: eyebrow → display
6
+ * headline → lede → actions; the default media is the plate, a full-bleed
7
+ * figure under the text at its natural ratio (data-aspect crops it).
8
+ * The headline is the base .display rule: it fills the hero's column, and
9
+ * hand breaks (<br>) hold as the screen grows (src/core/base.css).
12
10
  *
13
11
  * Usage:
14
- * <section class="hero" data-layout="center" data-padding="xl">
15
- * <h1>Headline</h1>
16
- * <p>Subheadline</p>
17
- * <button>Call to Action</button>
12
+ * <section class="hero">
13
+ * <p class="eyebrow">Studio</p>
14
+ * <h1 class="hero__headline" data-fit>Make the<br>quiet thing<br>loud.</h1>
15
+ * <p class="lede">One or two sentences.</p>
16
+ * <div class="hero__actions"><a class="button" href="#">Start</a></div>
17
+ * <figure class="hero__plate"><img src="..." alt="..." width="1600" height="900"></figure>
18
18
  * </section>
19
+ *
20
+ * Themes: data-theme="dark" (a dark band: the role scope, tokens.css),
21
+ * "accent" (solid accent, shared with data-bg="accent-strong"), "gradient",
22
+ * "subtle"; data-overlay over a .hero__background image.
19
23
  */
20
24
 
21
25
  @layer components {
@@ -25,9 +29,11 @@
25
29
  A full-width section ready for content
26
30
  ======================================== */
27
31
 
32
+ /* The inline padding puts the content on the .container edges while the
33
+ background runs full width. */
28
34
  .hero {
29
- --hero-min-height: 60vh;
30
- --hero-background: var(--color-surface);
35
+ --hero-min-height: auto;
36
+ --hero-background: var(--color-background);
31
37
  --hero-text-color: var(--color-text);
32
38
 
33
39
  position: relative;
@@ -36,9 +42,19 @@
36
42
  color: var(--hero-text-color);
37
43
  overflow: hidden;
38
44
  container-type: inline-size;
45
+ padding-inline: max(var(--page-edge), (100% - var(--container-content)) / 2);
46
+ }
47
+
48
+ /* An inner .container owns the edge, as in every .section; padding here
49
+ too would inset the content twice. */
50
+ .hero:has(> .container) { padding-inline: 0; }
39
51
 
40
- /* Inline padding to prevent text touching edges */
41
- padding-inline: var(--space-5);
52
+ /* Generous space goes between sections, not above the first line: at
53
+ 1440 × 900 the old 160px top pushed the actions onto the fold. It
54
+ follows --space-section, so a tight look keeps a tight hero. The
55
+ data-padding scale (layouts layer) wins whenever the author sets it. */
56
+ .hero:not([data-padding]) {
57
+ padding-block: min(var(--space-xl), var(--space-section));
42
58
  }
43
59
 
44
60
  /* Ensure content stays above backgrounds/overlays */
@@ -58,12 +74,10 @@
58
74
  }
59
75
 
60
76
  .hero[data-size="full"] {
61
- --hero-min-height: 100vh;
77
+ --hero-min-height: 100svh;
62
78
  }
63
79
 
64
- .hero[data-size="auto"] {
65
- --hero-min-height: auto;
66
- }
80
+ /* data-size="auto" is the default: no rule. */
67
81
 
68
82
 
69
83
  /* ========================================
@@ -75,33 +89,77 @@
75
89
  max-inline-size: var(--content-width, var(--container-md));
76
90
  }
77
91
 
92
+ /* The headline's face, size, leading and balance are the base .display
93
+ rule; here only its overflow safety (it is not always a heading; a long
94
+ word breaks, never with an automatic hyphen) and the space to the lede.
95
+ The space grows with the headline (0.18em), so a display face with a
96
+ long descender (a Didone at 190px) never inks into the lede. */
78
97
  .hero__headline {
79
- /* Fluid scaling: smooth transition across all viewport sizes */
80
- font-size: clamp(2rem, 1rem + 5vw, 3.5rem);
81
- font-weight: var(--weight-bold);
82
- letter-spacing: -0.03em;
83
- line-height: var(--leading-tight);
84
- margin-block-end: var(--space-4);
85
- text-wrap: balance; /* Prevent orphans in headlines */
98
+ margin-block: 0 max(var(--space-m), 0.18em);
99
+ overflow-wrap: break-word;
100
+ hyphens: manual;
86
101
  }
87
102
 
88
- .hero__subheadline {
89
- /* Fluid scaling for subheadlines */
90
- font-size: clamp(1.125rem, 0.875rem + 1.5vw, 1.5rem);
91
- color: var(--hero-text-muted, var(--color-text-muted));
92
- line-height: var(--leading-relaxed);
93
- margin-block-end: var(--space-6);
94
- text-wrap: pretty; /* Better line breaks */
95
- max-inline-size: 65ch; /* Optimal reading width */
103
+ .hero .eyebrow { margin-block-end: var(--space-m); }
104
+
105
+ /* The label over a display headline: the trimmed cap height sat 24px
106
+ under it at 160px type, glued on. The gap grows with the headline
107
+ (about 48px at 160px, 20px on a phone); margins collapse with the
108
+ eyebrow's own. */
109
+ .eyebrow + :is(.hero__headline, .display) { margin-block-start: max(var(--space-m), 0.3em); }
110
+
111
+ /* In a gap layout (the stack's <div class="container stack"> inside the
112
+ hero) the gap is the rhythm; margins would add to it. */
113
+ :is(.stack, .cluster, .grid) > :is(.hero__headline, .hero__subheadline, .lede, .eyebrow) {
114
+ margin-block: 0;
115
+ }
116
+
117
+ :is(.hero__subheadline, .hero .lede) {
118
+ font-family: var(--font-heading);
119
+ font-weight: calc(var(--weight-light) + var(--_dark-weight));
120
+ font-size: var(--step-1);
121
+ line-height: 1.4;
122
+ letter-spacing: -0.005em;
123
+ color: var(--hero-text-muted, var(--hero-text-color));
124
+ max-inline-size: 30em;
125
+ margin-block: 0 var(--space-l);
96
126
  }
97
127
 
98
128
  .hero__actions {
99
129
  display: flex;
100
130
  flex-wrap: wrap;
101
- gap: var(--space-4);
131
+ align-items: center;
132
+ gap: var(--space-s) var(--space-m);
102
133
  justify-content: inherit;
103
134
  }
104
135
 
136
+ /* The plate: a full-bleed figure under the text, natural ratio. Its
137
+ negative margin is the hero's inline padding measured on the viewport;
138
+ a scrollbar's few extra pixels are clipped by the hero. */
139
+ .hero__plate {
140
+ margin-block: var(--space-xl) calc(-1 * var(--space-xl));
141
+ margin-inline: calc(-1 * max(var(--page-edge), (100vi - var(--container-content)) / 2));
142
+ }
143
+
144
+ /* An uncaptioned plate runs to the hero's bottom edge; a caption keeps
145
+ --space-m above that edge, so its text never sits flush on a dark or
146
+ tinted hero band (visual-scan SEAM, 2026-10-29). */
147
+ .hero__plate:has(> figcaption) {
148
+ margin-block-end: calc(var(--space-m) - var(--space-xl));
149
+ }
150
+
151
+ .hero__plate :is(img, video) {
152
+ inline-size: 100%;
153
+ border-radius: 0;
154
+ }
155
+
156
+ .hero__plate :is(img, video)[data-aspect] { max-block-size: 88svh; }
157
+
158
+ /* The caption returns to the content edge. */
159
+ .hero__plate figcaption {
160
+ margin-inline: max(var(--page-edge), (100vi - var(--container-content)) / 2);
161
+ }
162
+
105
163
 
106
164
  /* ========================================
107
165
  HERO BACKGROUNDS
@@ -113,6 +171,9 @@
113
171
  inset: 0;
114
172
  z-index: 0;
115
173
  overflow: hidden;
174
+ /* data-layout="left" (and the other layouts) cap each child's width; the
175
+ background is not content and always fills the hero (#72). */
176
+ max-inline-size: none;
116
177
  }
117
178
 
118
179
  .hero__background img,
@@ -122,13 +183,7 @@
122
183
  object-fit: cover;
123
184
  }
124
185
 
125
- /* Focal point control for hero backgrounds */
126
- .hero__background[data-focus="top"] img { object-position: center top; }
127
- .hero__background[data-focus="bottom"] img { object-position: center bottom; }
128
- .hero__background[data-focus="left"] img { object-position: left center; }
129
- .hero__background[data-focus="right"] img { object-position: right center; }
130
- .hero__background[data-focus="center"] img { object-position: center center; }
131
-
186
+ /* data-focus on the wrapper sets --focus (base.css), which the img reads. */
132
187
  /* Custom focal point via CSS custom properties */
133
188
  .hero__background[style*="--focus"] img {
134
189
  object-position: var(--focus-x, 50%) var(--focus-y, 50%);
@@ -149,7 +204,7 @@
149
204
  pointer-events: none;
150
205
  }
151
206
 
152
- /* Overlay intensity presets */
207
+ /* Overlay intensity */
153
208
  .hero[data-overlay="light"]::before {
154
209
  --hero-overlay: oklch(0% 0 0 / 0.2);
155
210
  }
@@ -178,34 +233,48 @@
178
233
  Pre-designed color schemes
179
234
  ======================================== */
180
235
 
181
- /* Dark hero with light text — derives from page's dark mode colors */
236
+ /* Dark hero: a dark band. Its roles re-resolve dark (tokens.css role scope). */
182
237
  .hero[data-theme="dark"] {
183
- --hero-background: color-mix(in oklch, var(--accent) 10%, oklch(12% 0.02 230));
184
- --hero-text-color: oklch(94% 0.01 220);
185
- --hero-text-muted: oklch(70% 0.01 220);
238
+ --hero-background: var(--band-inverse);
239
+ --hero-text-color: var(--color-text);
240
+ --hero-text-muted: var(--color-text-muted);
186
241
  }
187
242
 
188
- /* Accent color hero */
243
+ /* Accent hero: the solid accent band, shared with data-bg="accent-strong"
244
+ (src/fluid.css re-points the roles and button hooks). */
189
245
  .hero[data-theme="accent"] {
190
246
  --hero-background: var(--accent);
191
- --hero-text-color: var(--white);
192
- --hero-text-muted: oklch(100% 0 0 / 0.85);
247
+ --hero-text-color: var(--on-accent);
248
+ --hero-text-muted: var(--on-accent);
193
249
  }
194
250
 
195
- /* Subtle/muted hero */
196
- .hero[data-theme="subtle"] {
251
+ /* Headings in a themed hero take the hero's text colour, not the page's. */
252
+ .hero[data-theme] :is(h1, h2, h3, h4, h5, h6, .hero__headline) {
253
+ color: var(--hero-text-color);
254
+ }
255
+
256
+ /* Section bands (data-bg, src/fluid.css) on a hero */
257
+ .hero:is([data-bg="tinted"], [data-bg="alt"]) {
197
258
  --hero-background: var(--color-surface);
198
- --hero-text-color: var(--color-text);
199
- --hero-text-muted: var(--color-text-muted);
200
259
  }
201
260
 
202
- /* Gradient hero - derives from accent color */
261
+ .hero[data-bg="accent"] {
262
+ --hero-background: var(--accent-subtle);
263
+ }
264
+
265
+ /* Subtle: the default hero with a muted lede. */
266
+ .hero[data-theme="subtle"] { --hero-text-muted: var(--color-text-muted); }
267
+
268
+ /* Gradient hero: derives from the accent. Not a role band, so its ink
269
+ and focus are lifted here. */
203
270
  .hero[data-theme="gradient"] {
271
+ --accent-ink: oklch(from var(--accent) max(l, 0.76) c h);
272
+ --color-focus: var(--accent-ink);
204
273
  --hero-background: linear-gradient(
205
274
  135deg,
206
- color-mix(in oklch, var(--accent) 30%, oklch(15% 0.02 230)) 0%,
207
- color-mix(in oklch, var(--accent) 20%, oklch(25% 0.02 230)) 50%,
208
- color-mix(in oklch, var(--accent) 15%, oklch(35% 0.02 230)) 100%
275
+ color-mix(in oklch, var(--accent) 30%, oklch(from var(--neutral-tint) 0.15 calc(c * 3) h)) 0%,
276
+ color-mix(in oklch, var(--accent) 20%, oklch(from var(--neutral-tint) 0.25 calc(c * 3) h)) 50%,
277
+ color-mix(in oklch, var(--accent) 15%, oklch(from var(--neutral-tint) 0.35 calc(c * 3) h)) 100%
209
278
  );
210
279
  --hero-text-color: oklch(95% 0.01 0);
211
280
  --hero-text-muted: oklch(80% 0.01 0);
@@ -213,6 +282,26 @@
213
282
  }
214
283
 
215
284
 
285
+ /* Legacy floor (see tokens.css): without relative colour the roles are
286
+ plain light values, so the dark hero goes light like every other dark
287
+ band (its text is the floor's dark text). The gradient keeps fixed
288
+ light text, so its paint is computed here from the default tint. */
289
+ @supports not (color: oklch(from red min(l, 0.5) c h)) {
290
+ .hero[data-theme="dark"] {
291
+ --hero-background: var(--color-background);
292
+ }
293
+
294
+ .hero[data-theme="gradient"] {
295
+ --hero-background: linear-gradient(
296
+ 135deg,
297
+ color-mix(in oklch, var(--accent) 30%, oklch(15% 0.03 75)) 0%,
298
+ color-mix(in oklch, var(--accent) 20%, oklch(25% 0.03 75)) 50%,
299
+ color-mix(in oklch, var(--accent) 15%, oklch(35% 0.03 75)) 100%
300
+ );
301
+ }
302
+ }
303
+
304
+
216
305
  /* ========================================
217
306
  SPLIT HERO
218
307
  Content + media side by side
@@ -229,7 +318,52 @@
229
318
  inline-size: 100%;
230
319
  block-size: 100%;
231
320
  object-fit: cover;
232
- border-radius: var(--radius-lg);
321
+ border-radius: var(--radius-card);
322
+ }
323
+
324
+
325
+ /* ========================================
326
+ DATA-ARRANGE="auto": the centered stack
327
+
328
+ Lays out flat hero markup as a centered grid: headline,
329
+ subheadline, actions, media. (The per-preset arrangements it
330
+ used to choose between went with the presets.)
331
+
332
+ Requires the flat hero markup (hero__headline, hero__subheadline,
333
+ hero__actions, optional hero__media as direct children). Markup
334
+ using a hero__content wrapper keeps its data-layout behavior —
335
+ the :has() guard leaves it untouched. data-arrange wins over
336
+ data-layout because components layer beats layouts layer.
337
+ ======================================== */
338
+
339
+ .hero[data-arrange="auto"]:not(:has(> .hero__content)) {
340
+ display: grid;
341
+ grid-template-columns: minmax(0, 1fr);
342
+ grid-template-areas:
343
+ "headline"
344
+ "subheadline"
345
+ "actions"
346
+ "media";
347
+ align-content: center;
348
+ justify-items: start;
349
+ column-gap: var(--space-10);
350
+ }
351
+
352
+ .hero[data-arrange="auto"] > .hero__headline { grid-area: headline; }
353
+ .hero[data-arrange="auto"] > .hero__subheadline { grid-area: subheadline; }
354
+ .hero[data-arrange="auto"] > .hero__actions { grid-area: actions; }
355
+ .hero[data-arrange="auto"] > .hero__media { grid-area: media; }
356
+
357
+ /* On narrow viewports the media moves above the actions. */
358
+ @media (max-width: 48rem) {
359
+ .hero[data-arrange="auto"]:not(:has(> .hero__content)) {
360
+ grid-template-columns: minmax(0, 1fr);
361
+ grid-template-areas:
362
+ "headline"
363
+ "subheadline"
364
+ "media"
365
+ "actions";
366
+ }
233
367
  }
234
368
 
235
369
 
@@ -253,41 +387,22 @@
253
387
 
254
388
  /* ========================================
255
389
  HERO BUTTON CONTRAST
256
- Ensure buttons are visible on themed backgrounds
390
+ Dark and accent heroes re-point the roles, so their buttons follow on
391
+ their own. The gradient hero is not a role band: its outline buttons
392
+ go white. `.button` too, so a link matches a <button> (#72).
257
393
  ======================================== */
258
394
 
259
- /* Dark theme: use light outline buttons for secondary */
260
- .hero[data-theme="dark"] button[data-variant="outline"],
261
- .hero[data-theme="gradient"] button[data-variant="outline"] {
395
+ .hero[data-theme="gradient"] :is(button, .button)[data-variant="outline"] {
262
396
  --_btn-color: var(--white);
263
397
  --_btn-border: var(--white);
264
- --_btn-bg-hover: oklch(100% 0 0 / 0.15);
265
- }
266
-
267
- /* Accent theme: invert button colors for contrast */
268
- .hero[data-theme="accent"] button:not([data-variant]) {
269
- --_btn-color: var(--accent);
270
- --_btn-bg: var(--white);
271
- --_btn-bg-hover: var(--gray-95);
398
+ --_btn-bg-hover: var(--white);
399
+ --_btn-color-hover: var(--black);
272
400
  }
273
401
 
274
- .hero[data-theme="accent"] button[data-variant="outline"] {
275
- --_btn-color: var(--white);
276
- --_btn-border: var(--white);
277
- --_btn-bg-hover: oklch(100% 0 0 / 0.15);
402
+ .hero:is([data-theme="gradient"], [data-overlay]) :is(button, .button)[data-variant="ghost"] {
403
+ --_btn-color: var(--hero-text-color);
278
404
  }
279
405
 
280
406
 
281
- /* ========================================
282
- REDUCED MOTION
283
- Respect user preferences
284
- ======================================== */
285
-
286
- @media (prefers-reduced-motion: reduce) {
287
- .hero,
288
- .hero * {
289
- animation: none !important;
290
- transition: none !important;
291
- }
292
- }
407
+ /* Reduced motion: the reset shortens every transition and animation. */
293
408
  }
@@ -31,6 +31,8 @@
31
31
  /* Good (Success family) - ≥80% */
32
32
  [data-status="good"] {
33
33
  --status-color: var(--color-success);
34
+ --status-fill: var(--color-success-fill);
35
+ --status-on: var(--color-success-on);
34
36
  --status-bg: var(--color-success-subtle);
35
37
  --status-text: var(--color-success);
36
38
  --status-border: var(--color-success);
@@ -39,15 +41,18 @@
39
41
  /* Moderate (Warning family) - 60-79% */
40
42
  [data-status="moderate"] {
41
43
  --status-color: var(--color-warning);
44
+ --status-fill: var(--color-warning-fill);
45
+ --status-on: var(--color-warning-on);
42
46
  --status-bg: var(--color-warning-subtle);
43
47
  --status-text: var(--color-warning);
44
- --status-text: oklch(from var(--color-warning) calc(l - 0.2) c h);
45
48
  --status-border: var(--color-warning);
46
49
  }
47
50
 
48
51
  /* Severe (Error family) - <60% */
49
52
  [data-status="severe"] {
50
53
  --status-color: var(--color-error);
54
+ --status-fill: var(--color-error-fill);
55
+ --status-on: var(--color-error-on);
51
56
  --status-bg: var(--color-error-subtle);
52
57
  --status-text: var(--color-error);
53
58
  --status-border: var(--color-error);
@@ -105,23 +110,12 @@
105
110
  background-color: var(--_progress-track);
106
111
  border-radius: var(--_progress-radius);
107
112
  overflow: hidden;
108
- /* Subtle inner shadow to define the track boundary */
109
- box-shadow: inset 0 1px 2px oklch(0% 0 0 / 0.1);
110
- /* Light mode: use status-aware tinted track */
111
- --_progress-track: color-mix(in oklch, var(--status-color, var(--accent)) 15%, var(--color-surface-raised));
112
- }
113
-
114
- /* Dark mode: make track more visible with stronger tint */
115
- @media (prefers-color-scheme: dark) {
116
- .progress__track {
117
- --_progress-track: color-mix(in oklch, var(--status-color, var(--accent)) 20%, var(--color-surface-raised));
118
- box-shadow: inset 0 1px 3px oklch(0% 0 0 / 0.3);
119
- }
120
- }
121
-
122
- [data-theme="dark"] .progress__track {
123
- --_progress-track: color-mix(in oklch, var(--status-color, var(--accent)) 20%, var(--color-surface-raised));
124
- box-shadow: inset 0 1px 3px oklch(0% 0 0 / 0.3);
113
+ /* Subtle inner shadow to define the track boundary; a status-tinted
114
+ track, both a little stronger in dark mode */
115
+ --_progress-track: light-dark(
116
+ color-mix(in oklab, var(--status-color, var(--accent)) 15%, var(--color-surface-raised)),
117
+ color-mix(in oklab, var(--status-color, var(--accent)) 20%, var(--color-surface-raised))
118
+ );
125
119
  }
126
120
 
127
121
  /* Progress fill bar */
@@ -207,20 +201,20 @@
207
201
  line-height: var(--leading-none);
208
202
  color: var(--status-text, var(--color-text));
209
203
  background-color: var(--status-bg, var(--color-surface));
210
- border-radius: var(--radius-full);
204
+ border-radius: var(--radius-pill);
211
205
  white-space: nowrap;
212
206
  }
213
207
 
214
208
  /* Filled variant - stronger emphasis */
215
209
  .badge[data-variant="filled"] {
216
- color: var(--white);
217
- background-color: var(--status-color, var(--accent));
210
+ color: var(--status-on, var(--on-accent));
211
+ background-color: var(--status-fill, var(--accent));
218
212
  }
219
213
 
220
214
  /* Outline variant */
221
215
  .badge[data-variant="outline"] {
222
216
  background-color: transparent;
223
- border: 1px solid var(--status-border, var(--color-border));
217
+ border: var(--border-width) solid var(--status-border, var(--color-border));
224
218
  }
225
219
 
226
220
  /* Badge sizes */
@@ -254,8 +248,8 @@
254
248
  .status-card {
255
249
  padding: var(--space-4);
256
250
  background-color: var(--status-bg, var(--color-surface));
257
- border: 1px solid var(--status-border, var(--color-border-subtle));
258
- border-radius: var(--radius-lg);
251
+ border: var(--border-width) solid var(--status-border, var(--color-border-subtle));
252
+ border-radius: var(--radius-card);
259
253
  border-inline-start-width: 4px;
260
254
  }
261
255
 
@@ -290,7 +284,7 @@
290
284
  gap: var(--space-4);
291
285
  padding: var(--space-4);
292
286
  background-color: var(--color-surface);
293
- border-radius: var(--radius-lg);
287
+ border-radius: var(--radius-card);
294
288
  font-size: var(--font-size-sm);
295
289
  }
296
290
 
@@ -354,102 +348,14 @@
354
348
 
355
349
 
356
350
  /* ========================================
357
- MOTION-AWARE ANIMATIONS
358
- Each theme has its own progress animation style
351
+ MOTION
359
352
  ======================================== */
360
353
 
361
- /* Still: instant fill - no animation */
362
- [data-motion="still"] .progress__fill {
363
- transition: none;
364
- }
365
-
366
- /* Serene: very slow, gentle fill */
367
- [data-motion="serene"] .progress__fill {
368
- transition: inline-size var(--duration-slowest) var(--ease-fluid);
369
- }
370
-
371
- /* Trickling: subtle, quick fill */
372
- [data-motion="trickling"] .progress__fill {
373
- transition: inline-size var(--duration-slow) var(--ease-fluid);
374
- }
375
-
376
- /* Stream: smooth horizontal flow */
377
- [data-motion="stream"] .progress__fill {
354
+ /* Balanced, organic fill */
355
+ .progress__fill {
378
356
  transition: inline-size var(--duration-slow) var(--ease-fluid);
379
357
  }
380
358
 
381
- /* Flowing: balanced, organic fill (default) */
382
- [data-motion="flowing"] .progress__fill,
383
- :root:not([data-motion]) .progress__fill {
384
- transition: inline-size var(--duration-slow) var(--ease-fluid);
385
- }
386
359
 
387
- /* Cascade: bouncy waterfall effect */
388
- [data-motion="cascade"] .progress__fill {
389
- transition: inline-size var(--duration-normal) var(--ease-settle);
390
- }
391
-
392
- /* Rapids: fast, energetic with overshoot */
393
- [data-motion="rapids"] .progress__fill {
394
- transition: inline-size var(--duration-normal) var(--ease-settle);
395
- }
396
-
397
- /* Tsunami: wave animation - rise, crest, crash, bounce, settle */
398
- @keyframes tsunami-progress-fill {
399
- 0% {
400
- inline-size: 0;
401
- transform: scaleY(1);
402
- }
403
- 25% {
404
- /* Rising - wave builds up, overshoots target */
405
- inline-size: calc(var(--progress-percent, 0%) * 1.15);
406
- transform: scaleY(1.3);
407
- }
408
- 40% {
409
- /* Cresting - peak of the wave */
410
- inline-size: calc(var(--progress-percent, 0%) * 1.1);
411
- transform: scaleY(1.4);
412
- }
413
- 60% {
414
- /* Crashing down - undershoots */
415
- inline-size: calc(var(--progress-percent, 0%) * 0.92);
416
- transform: scaleY(0.8);
417
- }
418
- 75% {
419
- /* Bounce back up */
420
- inline-size: calc(var(--progress-percent, 0%) * 1.03);
421
- transform: scaleY(1.1);
422
- }
423
- 90% {
424
- /* Settling */
425
- inline-size: calc(var(--progress-percent, 0%) * 0.99);
426
- transform: scaleY(0.95);
427
- }
428
- 100% {
429
- /* Final position */
430
- inline-size: var(--progress-percent, 0%);
431
- transform: scaleY(1);
432
- }
433
- }
434
-
435
- [data-motion="tsunami"] .progress__fill {
436
- transition: none;
437
- transform-origin: left center;
438
- }
439
-
440
- [data-motion="tsunami"] .progress[data-animate][data-visible] .progress__fill {
441
- animation: tsunami-progress-fill var(--duration-slower) var(--ease-settle) forwards;
442
- }
443
-
444
- /* Non-animated tsunami progress - just use transition */
445
- [data-motion="tsunami"] .progress:not([data-animate]) .progress__fill {
446
- transition: inline-size var(--duration-slower) var(--ease-settle);
447
- }
448
-
449
- /* Reduced motion: respect preference */
450
- @media (prefers-reduced-motion: reduce) {
451
- .progress__fill {
452
- transition: none;
453
- }
454
- }
360
+ /* Reduced motion: the durations are 0 and the reset shortens the rest. */
455
361
  }