@fluid-theme/core 0.4.3

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 (48) hide show
  1. package/AGENTS.md +32 -0
  2. package/CHANGELOG.md +159 -0
  3. package/CLAUDE.md +104 -0
  4. package/GEMINI.md +48 -0
  5. package/LICENSE +21 -0
  6. package/README.md +253 -0
  7. package/dist/fluid.min.css +2 -0
  8. package/fluid-agent-kit/README.md +57 -0
  9. package/fluid-agent-kit/skills/accessibility.md +41 -0
  10. package/fluid-agent-kit/skills/css-conventions.md +51 -0
  11. package/fluid-agent-kit/skills/image-optimization.md +115 -0
  12. package/fluid-agent-kit/skills/ux-rules.md +43 -0
  13. package/package.json +68 -0
  14. package/src/animations/scroll-reveal.css +224 -0
  15. package/src/components/accordion.css +250 -0
  16. package/src/components/alert.css +453 -0
  17. package/src/components/avatar.css +192 -0
  18. package/src/components/badge.css +182 -0
  19. package/src/components/button.css +387 -0
  20. package/src/components/card.css +650 -0
  21. package/src/components/chip.css +143 -0
  22. package/src/components/dropdown.css +335 -0
  23. package/src/components/empty-state.css +105 -0
  24. package/src/components/form.css +508 -0
  25. package/src/components/hero.css +293 -0
  26. package/src/components/indicator.css +455 -0
  27. package/src/components/loading.css +274 -0
  28. package/src/components/modal.css +364 -0
  29. package/src/components/nav.css +726 -0
  30. package/src/components/scroll-progress.css +104 -0
  31. package/src/components/search-input.css +125 -0
  32. package/src/components/settings-panel.css +603 -0
  33. package/src/components/step-indicator.css +188 -0
  34. package/src/components/table.css +350 -0
  35. package/src/components/themed-images.css +229 -0
  36. package/src/components/tooltip.css +247 -0
  37. package/src/components/version-badge.css +70 -0
  38. package/src/core/base.css +884 -0
  39. package/src/core/reset.css +159 -0
  40. package/src/core/tokens.css +2416 -0
  41. package/src/fluid.css +242 -0
  42. package/src/layouts/positioning.css +626 -0
  43. package/src/sections/cta.css +210 -0
  44. package/src/sections/features.css +356 -0
  45. package/src/sections/footer.css +283 -0
  46. package/src/sections/stats.css +234 -0
  47. package/src/sections/team.css +181 -0
  48. package/src/sections/testimonials.css +251 -0
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Fluid Framework - Status Indicator System
3
+ * @layer components
4
+ *
5
+ * A semantic status system for scores, progress, and feedback.
6
+ * Uses data-status to drive ALL visual styling - prevents color drift.
7
+ *
8
+ * Philosophy:
9
+ * - Status is the single source of truth
10
+ * - Score → Status → Theme Token → Visuals
11
+ * - No component may mix status color families
12
+ *
13
+ * Status Tiers (default thresholds):
14
+ * - good: ≥80% (success family)
15
+ * - moderate: 60-79% (warning family)
16
+ * - severe: <60% (error family)
17
+ *
18
+ * Usage:
19
+ * <div data-status="good">88% - Meeting goals</div>
20
+ * <progress data-status="moderate" value="65" max="100"></progress>
21
+ * <span class="badge" data-status="severe">Needs attention</span>
22
+ */
23
+
24
+ @layer components {
25
+
26
+ /* ========================================
27
+ STATUS COLOR SYSTEM
28
+ All colors derive from status token
29
+ ======================================== */
30
+
31
+ /* Good (Success family) - ≥80% */
32
+ [data-status="good"] {
33
+ --status-color: var(--color-success);
34
+ --status-bg: var(--color-success-subtle);
35
+ --status-text: var(--color-success);
36
+ --status-border: var(--color-success);
37
+ }
38
+
39
+ /* Moderate (Warning family) - 60-79% */
40
+ [data-status="moderate"] {
41
+ --status-color: var(--color-warning);
42
+ --status-bg: var(--color-warning-subtle);
43
+ --status-text: var(--color-warning);
44
+ --status-text: oklch(from var(--color-warning) calc(l - 0.2) c h);
45
+ --status-border: var(--color-warning);
46
+ }
47
+
48
+ /* Severe (Error family) - <60% */
49
+ [data-status="severe"] {
50
+ --status-color: var(--color-error);
51
+ --status-bg: var(--color-error-subtle);
52
+ --status-text: var(--color-error);
53
+ --status-border: var(--color-error);
54
+ }
55
+
56
+ /* Neutral (no score/status) */
57
+ [data-status="neutral"] {
58
+ --status-color: var(--color-text-muted);
59
+ --status-bg: var(--color-surface);
60
+ --status-text: var(--color-text-muted);
61
+ --status-border: var(--color-border);
62
+ }
63
+
64
+
65
+ /* ========================================
66
+ ENHANCED PROGRESS BAR
67
+ Animated, labeled, status-aware
68
+ ======================================== */
69
+
70
+ .progress {
71
+ --_progress-height: var(--space-3);
72
+ --_progress-radius: var(--radius-full);
73
+ --_progress-track: var(--color-border);
74
+ --_progress-fill: var(--status-color, var(--accent));
75
+
76
+ position: relative;
77
+ display: flex;
78
+ flex-direction: column;
79
+ gap: var(--space-2);
80
+ }
81
+
82
+ /* Progress label row */
83
+ .progress__label {
84
+ display: flex;
85
+ justify-content: space-between;
86
+ align-items: baseline;
87
+ font-size: var(--font-size-sm);
88
+ }
89
+
90
+ .progress__title {
91
+ font-weight: var(--weight-medium);
92
+ color: var(--color-text);
93
+ }
94
+
95
+ .progress__value {
96
+ font-weight: var(--weight-semibold);
97
+ font-variant-numeric: tabular-nums;
98
+ color: var(--status-text, var(--color-text));
99
+ }
100
+
101
+ /* Progress track (background) - shows the "empty" portion */
102
+ .progress__track {
103
+ position: relative;
104
+ block-size: var(--_progress-height);
105
+ background-color: var(--_progress-track);
106
+ border-radius: var(--_progress-radius);
107
+ 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);
125
+ }
126
+
127
+ /* Progress fill bar */
128
+ .progress__fill {
129
+ position: absolute;
130
+ inset-block: 0;
131
+ inset-inline-start: 0;
132
+ inline-size: var(--progress-percent, 0%);
133
+ background-color: var(--_progress-fill);
134
+ border-radius: var(--_progress-radius);
135
+ transition: inline-size var(--duration-slow) var(--ease-fluid);
136
+ }
137
+
138
+ /* Animated progress - starts at 0, fills when visible */
139
+ .progress[data-animate] .progress__fill {
140
+ inline-size: 0;
141
+ }
142
+
143
+ .progress[data-animate][data-visible] .progress__fill {
144
+ inline-size: var(--progress-percent, 0%);
145
+ }
146
+
147
+
148
+ /* ========================================
149
+ PROGRESS SIZE VARIANTS
150
+ ======================================== */
151
+
152
+ .progress[data-size="sm"] {
153
+ --_progress-height: var(--space-2);
154
+ }
155
+
156
+ .progress[data-size="lg"] {
157
+ --_progress-height: var(--space-4);
158
+ }
159
+
160
+ .progress[data-size="xl"] {
161
+ --_progress-height: var(--space-6);
162
+ }
163
+
164
+
165
+ /* ========================================
166
+ PROGRESS WITH THRESHOLD MARKERS
167
+ Visual indicators for score bands
168
+ ======================================== */
169
+
170
+ .progress__markers {
171
+ position: absolute;
172
+ inset: 0;
173
+ pointer-events: none;
174
+ }
175
+
176
+ .progress__marker {
177
+ position: absolute;
178
+ inset-block: calc(var(--_progress-height) * -0.25);
179
+ inline-size: 2px;
180
+ background-color: var(--color-text-subtle);
181
+ opacity: 0.5;
182
+ }
183
+
184
+ /* Position markers at thresholds */
185
+ .progress__marker[data-at="60"] {
186
+ inset-inline-start: 60%;
187
+ }
188
+
189
+ .progress__marker[data-at="80"] {
190
+ inset-inline-start: 80%;
191
+ }
192
+
193
+
194
+ /* ========================================
195
+ STATUS BADGE / PILL
196
+ Compact status indicator
197
+ ======================================== */
198
+
199
+ .badge {
200
+ display: inline-flex;
201
+ align-items: center;
202
+ gap: var(--space-1);
203
+ padding-block: var(--space-1);
204
+ padding-inline: var(--space-2);
205
+ font-size: var(--font-size-xs);
206
+ font-weight: var(--weight-medium);
207
+ line-height: var(--leading-none);
208
+ color: var(--status-text, var(--color-text));
209
+ background-color: var(--status-bg, var(--color-surface));
210
+ border-radius: var(--radius-full);
211
+ white-space: nowrap;
212
+ }
213
+
214
+ /* Filled variant - stronger emphasis */
215
+ .badge[data-variant="filled"] {
216
+ color: var(--white);
217
+ background-color: var(--status-color, var(--accent));
218
+ }
219
+
220
+ /* Outline variant */
221
+ .badge[data-variant="outline"] {
222
+ background-color: transparent;
223
+ border: 1px solid var(--status-border, var(--color-border));
224
+ }
225
+
226
+ /* Badge sizes */
227
+ .badge[data-size="sm"] {
228
+ padding-block: var(--space-0-5);
229
+ padding-inline: var(--space-1-5);
230
+ font-size: var(--font-size-xs);
231
+ }
232
+
233
+ .badge[data-size="lg"] {
234
+ padding-block: var(--space-2);
235
+ padding-inline: var(--space-3);
236
+ font-size: var(--font-size-sm);
237
+ }
238
+
239
+ /* Badge with dot indicator */
240
+ .badge[data-dot]::before {
241
+ content: "";
242
+ inline-size: 0.5em;
243
+ block-size: 0.5em;
244
+ border-radius: var(--radius-full);
245
+ background-color: var(--status-color, var(--accent));
246
+ }
247
+
248
+
249
+ /* ========================================
250
+ STATUS CONTAINER / CARD
251
+ Wrap content in status context
252
+ ======================================== */
253
+
254
+ .status-card {
255
+ padding: var(--space-4);
256
+ 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);
259
+ border-inline-start-width: 4px;
260
+ }
261
+
262
+ .status-card__header {
263
+ display: flex;
264
+ justify-content: space-between;
265
+ align-items: center;
266
+ margin-block-end: var(--space-3);
267
+ }
268
+
269
+ .status-card__title {
270
+ font-weight: var(--weight-semibold);
271
+ color: var(--color-text);
272
+ }
273
+
274
+ .status-card__score {
275
+ font-size: var(--font-size-2xl);
276
+ font-weight: var(--weight-bold);
277
+ font-variant-numeric: tabular-nums;
278
+ color: var(--status-text, var(--color-text));
279
+ }
280
+
281
+
282
+ /* ========================================
283
+ SCORE LEGEND / KEY
284
+ Display threshold explanations
285
+ ======================================== */
286
+
287
+ .score-legend {
288
+ display: flex;
289
+ flex-wrap: wrap;
290
+ gap: var(--space-4);
291
+ padding: var(--space-4);
292
+ background-color: var(--color-surface);
293
+ border-radius: var(--radius-lg);
294
+ font-size: var(--font-size-sm);
295
+ }
296
+
297
+ .score-legend__item {
298
+ display: flex;
299
+ align-items: center;
300
+ gap: var(--space-2);
301
+ }
302
+
303
+ .score-legend__color {
304
+ inline-size: var(--space-4);
305
+ block-size: var(--space-4);
306
+ border-radius: var(--radius-sm);
307
+ }
308
+
309
+ .score-legend__item[data-status="good"] .score-legend__color {
310
+ background-color: var(--color-success);
311
+ }
312
+
313
+ .score-legend__item[data-status="moderate"] .score-legend__color {
314
+ background-color: var(--color-warning);
315
+ }
316
+
317
+ .score-legend__item[data-status="severe"] .score-legend__color {
318
+ background-color: var(--color-error);
319
+ }
320
+
321
+ .score-legend__range {
322
+ color: var(--color-text-muted);
323
+ }
324
+
325
+ .score-legend__label {
326
+ font-weight: var(--weight-medium);
327
+ }
328
+
329
+
330
+ /* ========================================
331
+ FRACTION DISPLAY
332
+ Show "23 / 26" style values
333
+ ======================================== */
334
+
335
+ .fraction {
336
+ display: inline-flex;
337
+ align-items: baseline;
338
+ gap: var(--space-1);
339
+ font-variant-numeric: tabular-nums;
340
+ }
341
+
342
+ .fraction__numerator {
343
+ font-weight: var(--weight-bold);
344
+ color: var(--status-text, var(--color-text));
345
+ }
346
+
347
+ .fraction__separator {
348
+ color: var(--color-text-subtle);
349
+ }
350
+
351
+ .fraction__denominator {
352
+ color: var(--color-text-muted);
353
+ }
354
+
355
+
356
+ /* ========================================
357
+ MOTION-AWARE ANIMATIONS
358
+ Each theme has its own progress animation style
359
+ ======================================== */
360
+
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 {
378
+ transition: inline-size var(--duration-slow) var(--ease-fluid);
379
+ }
380
+
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
+
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
+ }
455
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Fluid Framework - Loading & Skeleton Component
3
+ * @layer components
4
+ *
5
+ * Loading spinners and skeleton screens for loading states.
6
+ * Respects prefers-reduced-motion.
7
+ *
8
+ * Spinner Usage:
9
+ * <div class="spinner" aria-label="Loading"></div>
10
+ * <div class="spinner" data-size="sm"></div>
11
+ * <div class="spinner" data-size="lg"></div>
12
+ *
13
+ * Skeleton Usage:
14
+ * <div class="skeleton" style="--skeleton-h: 1rem; --skeleton-w: 60%;"></div>
15
+ * <div class="skeleton" data-variant="circle"></div>
16
+ * <div class="skeleton" data-variant="card"></div>
17
+ *
18
+ * Skeleton Card (preset layout):
19
+ * <div class="skeleton-card">
20
+ * <div class="skeleton" data-variant="media"></div>
21
+ * <div class="skeleton" data-variant="title"></div>
22
+ * <div class="skeleton" data-variant="text"></div>
23
+ * <div class="skeleton" data-variant="text" style="--skeleton-w: 75%;"></div>
24
+ * </div>
25
+ */
26
+
27
+ @layer components {
28
+ /* ========================================
29
+ SPINNER
30
+ ======================================== */
31
+
32
+ .spinner {
33
+ --_spinner-size: 2rem;
34
+ --_spinner-width: 3px;
35
+ --_spinner-color: var(--accent);
36
+
37
+ display: inline-block;
38
+ inline-size: var(--_spinner-size);
39
+ block-size: var(--_spinner-size);
40
+ border: var(--_spinner-width) solid color-mix(in oklch, var(--_spinner-color) 20%, transparent);
41
+ border-block-start-color: var(--_spinner-color);
42
+ border-radius: var(--radius-full);
43
+ animation: spinner-rotate var(--duration-slow, 640ms) linear infinite;
44
+ }
45
+
46
+ @keyframes spinner-rotate {
47
+ to { transform: rotate(360deg); }
48
+ }
49
+
50
+ /* Sizes */
51
+ .spinner[data-size="xs"] {
52
+ --_spinner-size: 1rem;
53
+ --_spinner-width: 2px;
54
+ }
55
+
56
+ .spinner[data-size="sm"] {
57
+ --_spinner-size: 1.5rem;
58
+ --_spinner-width: 2px;
59
+ }
60
+
61
+ .spinner[data-size="lg"] {
62
+ --_spinner-size: 3rem;
63
+ --_spinner-width: 4px;
64
+ }
65
+
66
+ .spinner[data-size="xl"] {
67
+ --_spinner-size: 4rem;
68
+ --_spinner-width: 4px;
69
+ }
70
+
71
+ /* Reduced motion: pulse instead of spin */
72
+ @media (prefers-reduced-motion: reduce) {
73
+ .spinner {
74
+ animation: spinner-pulse 1.5s ease-in-out infinite;
75
+ border-color: color-mix(in oklch, var(--_spinner-color) 30%, transparent);
76
+ border-block-start-color: color-mix(in oklch, var(--_spinner-color) 30%, transparent);
77
+ }
78
+
79
+ @keyframes spinner-pulse {
80
+ 0%, 100% { opacity: 0.4; }
81
+ 50% { opacity: 1; }
82
+ }
83
+ }
84
+
85
+
86
+ /* ========================================
87
+ DOTS SPINNER (alternative)
88
+ ======================================== */
89
+
90
+ .spinner-dots {
91
+ display: inline-flex;
92
+ gap: var(--space-1-5);
93
+ align-items: center;
94
+ }
95
+
96
+ .spinner-dots::before,
97
+ .spinner-dots::after,
98
+ .spinner-dots span {
99
+ content: "";
100
+ display: block;
101
+ inline-size: 0.5rem;
102
+ block-size: 0.5rem;
103
+ border-radius: var(--radius-full);
104
+ background: var(--accent);
105
+ animation: spinner-dot-bounce var(--duration-slow, 640ms) ease-in-out infinite;
106
+ }
107
+
108
+ .spinner-dots::after {
109
+ animation-delay: calc(var(--duration-slow, 640ms) * 0.16);
110
+ }
111
+
112
+ .spinner-dots span {
113
+ animation-delay: calc(var(--duration-slow, 640ms) * 0.32);
114
+ }
115
+
116
+ @keyframes spinner-dot-bounce {
117
+ 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
118
+ 40% { transform: scale(1); opacity: 1; }
119
+ }
120
+
121
+ @media (prefers-reduced-motion: reduce) {
122
+ .spinner-dots::before,
123
+ .spinner-dots::after,
124
+ .spinner-dots span {
125
+ animation: none;
126
+ opacity: 0.6;
127
+ }
128
+ }
129
+
130
+
131
+ /* ========================================
132
+ LOADING OVERLAY
133
+ Full-area loading state
134
+ ======================================== */
135
+
136
+ .loading-overlay {
137
+ position: absolute;
138
+ inset: 0;
139
+ display: flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ flex-direction: column;
143
+ gap: var(--space-3);
144
+ background: color-mix(in oklch, var(--color-background) 80%, transparent);
145
+ z-index: var(--z-overlay);
146
+ border-radius: inherit;
147
+ }
148
+
149
+ .loading-overlay__text {
150
+ font-size: var(--font-size-sm);
151
+ color: var(--color-text-muted);
152
+ }
153
+
154
+
155
+ /* ========================================
156
+ SKELETON SCREENS
157
+ ======================================== */
158
+
159
+ .skeleton {
160
+ --skeleton-h: 1rem;
161
+ --skeleton-w: 100%;
162
+ --_skeleton-radius: var(--radius-md);
163
+
164
+ display: block;
165
+ inline-size: var(--skeleton-w);
166
+ block-size: var(--skeleton-h);
167
+ border-radius: var(--_skeleton-radius);
168
+ background: linear-gradient(
169
+ 90deg,
170
+ var(--color-surface) 0%,
171
+ var(--color-surface-raised) 40%,
172
+ var(--color-surface) 80%
173
+ );
174
+ background-size: 200% 100%;
175
+ animation: skeleton-shimmer 1.5s ease-in-out infinite;
176
+ }
177
+
178
+ @keyframes skeleton-shimmer {
179
+ 0% { background-position: 200% 0; }
180
+ 100% { background-position: -200% 0; }
181
+ }
182
+
183
+ @media (prefers-reduced-motion: reduce) {
184
+ .skeleton {
185
+ animation: none;
186
+ background: var(--color-surface);
187
+ }
188
+ }
189
+
190
+
191
+ /* ========================================
192
+ SKELETON VARIANTS
193
+ ======================================== */
194
+
195
+ /* Circle: avatar placeholder */
196
+ .skeleton[data-variant="circle"] {
197
+ --skeleton-w: 2.5rem;
198
+ --skeleton-h: 2.5rem;
199
+ --_skeleton-radius: var(--radius-full);
200
+ }
201
+
202
+ /* Title: heading placeholder */
203
+ .skeleton[data-variant="title"] {
204
+ --skeleton-h: 1.5rem;
205
+ --skeleton-w: 60%;
206
+ }
207
+
208
+ /* Text: body line placeholder */
209
+ .skeleton[data-variant="text"] {
210
+ --skeleton-h: 0.875rem;
211
+ --skeleton-w: var(--skeleton-w, 100%);
212
+ }
213
+
214
+ /* Media: image/video placeholder */
215
+ .skeleton[data-variant="media"] {
216
+ --skeleton-h: 12rem;
217
+ --_skeleton-radius: var(--radius-lg);
218
+ }
219
+
220
+ /* Button placeholder */
221
+ .skeleton[data-variant="button"] {
222
+ --skeleton-h: 2.75rem;
223
+ --skeleton-w: 8rem;
224
+ --_skeleton-radius: var(--radius-button, var(--radius-md));
225
+ }
226
+
227
+
228
+ /* ========================================
229
+ SKELETON CARD
230
+ Pre-built card loading state
231
+ ======================================== */
232
+
233
+ .skeleton-card {
234
+ display: flex;
235
+ flex-direction: column;
236
+ gap: var(--space-3);
237
+ padding: var(--space-5);
238
+ background: var(--color-surface-raised);
239
+ border: var(--border-width) solid var(--color-border-subtle);
240
+ border-radius: var(--radius-xl);
241
+ }
242
+
243
+ .skeleton-card .skeleton[data-variant="media"] {
244
+ margin: calc(var(--space-5) * -1);
245
+ margin-block-end: var(--space-3);
246
+ inline-size: calc(100% + var(--space-5) * 2);
247
+ border-radius: var(--radius-xl) var(--radius-xl) 0 0;
248
+ }
249
+
250
+
251
+ /* ========================================
252
+ THEME-SPECIFIC STYLES
253
+ ======================================== */
254
+
255
+ /* Still: no animation */
256
+ [data-motion="still"] .spinner {
257
+ animation-duration: 800ms;
258
+ }
259
+
260
+ [data-motion="still"] .skeleton {
261
+ animation: none;
262
+ background: var(--color-surface);
263
+ }
264
+
265
+ /* Tsunami: asymmetric skeleton cards */
266
+ [data-motion="tsunami"] .skeleton-card {
267
+ border-radius: var(--radius-asymmetric, 0 24px 0 0);
268
+ }
269
+
270
+ /* Rapids: faster spinner */
271
+ [data-motion="rapids"] .spinner {
272
+ animation-duration: 500ms;
273
+ }
274
+ }