@vegastack/design-tokens 0.1.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.
@@ -0,0 +1,712 @@
1
+ /**
2
+ * @vegastack/design-tokens/utilities.css
3
+ * ---------------------------------------------------------------------------
4
+ * Tailwind v4 `@utility` helpers shared by both the docs app (imported in
5
+ * apps/docs/app/global.css) and real npm consumers (imported in
6
+ * @vegastack/design/preset.css). Ported VERBATIM from shadcn@4.12.0's
7
+ * `tailwind.css` (the `scroll-fade` + `shimmer` sections) so behaviour matches
8
+ * upstream 1:1; both are pure CSS (no JS), dark-mode + RTL aware, and degrade
9
+ * gracefully without scroll-driven animations / honour prefers-reduced-motion.
10
+ *
11
+ * - scroll-fade : mask-image edge fades that track scroll position.
12
+ * - shimmer : animated text-highlight sweep (streaming/loading copy).
13
+ * - scrollbar-* : minimal native scrollbar helpers (NOT in upstream's
14
+ * tailwind.css) used by message-scroller's viewport.
15
+ *
16
+ * The `#000` literals below are alpha-mask stops (opaque vs transparent), not
17
+ * brand colours — masks have no themeable colour. CSS files are exempt from the
18
+ * semantic-token lint (design-lint); the only CSS rule it enforces is the
19
+ * priority-flag ban, which this file does not trip.
20
+ */
21
+
22
+ /* scroll-fade */
23
+ @property --scroll-fade-t {
24
+ syntax: "<length-percentage>";
25
+ inherits: false;
26
+ initial-value: 0px;
27
+ }
28
+ @property --scroll-fade-b {
29
+ syntax: "<length-percentage>";
30
+ inherits: false;
31
+ initial-value: 0px;
32
+ }
33
+ @property --scroll-fade-s {
34
+ syntax: "<length-percentage>";
35
+ inherits: false;
36
+ initial-value: 0px;
37
+ }
38
+ @property --scroll-fade-e {
39
+ syntax: "<length-percentage>";
40
+ inherits: false;
41
+ initial-value: 0px;
42
+ }
43
+ @property --scroll-fade-mask {
44
+ syntax: "*";
45
+ inherits: false;
46
+ }
47
+
48
+ @theme inline {
49
+ @keyframes scroll-fade-reveal-t {
50
+ from {
51
+ --scroll-fade-t: 0px;
52
+ }
53
+ to {
54
+ --scroll-fade-t: var(--_scroll-fade-size-t, var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))));
55
+ }
56
+ }
57
+ @keyframes scroll-fade-reveal-b {
58
+ from {
59
+ --scroll-fade-b: var(--_scroll-fade-size-b, var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))));
60
+ }
61
+ to {
62
+ --scroll-fade-b: 0px;
63
+ }
64
+ }
65
+ @keyframes scroll-fade-reveal-s {
66
+ from {
67
+ --scroll-fade-s: 0px;
68
+ }
69
+ to {
70
+ --scroll-fade-s: var(--_scroll-fade-size-s, var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))));
71
+ }
72
+ }
73
+ @keyframes scroll-fade-reveal-e {
74
+ from {
75
+ --scroll-fade-e: var(--_scroll-fade-size-e, var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10))));
76
+ }
77
+ to {
78
+ --scroll-fade-e: 0px;
79
+ }
80
+ }
81
+ }
82
+
83
+ @utility scroll-fade {
84
+ --_scroll-fade-size-t: var(
85
+ --scroll-fade-t-size,
86
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
87
+ );
88
+ --_scroll-fade-size-b: var(
89
+ --scroll-fade-b-size,
90
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
91
+ );
92
+ --scroll-fade-block: linear-gradient(
93
+ to bottom,
94
+ transparent 0,
95
+ #000 var(--scroll-fade-t, 0px),
96
+ #000 calc(100% - var(--scroll-fade-b, 0px)),
97
+ transparent 100%
98
+ );
99
+ -webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));
100
+ mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));
101
+ -webkit-mask-composite: source-in;
102
+ mask-composite: intersect;
103
+ -webkit-mask-repeat: no-repeat;
104
+ mask-repeat: no-repeat;
105
+
106
+ @supports (animation-timeline: scroll()) {
107
+ animation:
108
+ scroll-fade-reveal-t 1ms ease-in-out,
109
+ scroll-fade-reveal-b 1ms ease-in-out;
110
+ animation-timeline: scroll(self y), scroll(self y);
111
+ animation-range:
112
+ 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)),
113
+ calc(100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))) 100%;
114
+ animation-fill-mode: both;
115
+ }
116
+
117
+ @supports not (animation-timeline: scroll()) {
118
+ --scroll-fade-t: var(--_scroll-fade-size-t);
119
+ --scroll-fade-b: var(--_scroll-fade-size-b);
120
+ }
121
+ }
122
+
123
+ @utility scroll-fade-y {
124
+ --_scroll-fade-size-t: var(
125
+ --scroll-fade-t-size,
126
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
127
+ );
128
+ --_scroll-fade-size-b: var(
129
+ --scroll-fade-b-size,
130
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
131
+ );
132
+ --scroll-fade-block: linear-gradient(
133
+ to bottom,
134
+ transparent 0,
135
+ #000 var(--scroll-fade-t, 0px),
136
+ #000 calc(100% - var(--scroll-fade-b, 0px)),
137
+ transparent 100%
138
+ );
139
+ -webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));
140
+ mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));
141
+ -webkit-mask-composite: source-in;
142
+ mask-composite: intersect;
143
+ -webkit-mask-repeat: no-repeat;
144
+ mask-repeat: no-repeat;
145
+
146
+ @supports (animation-timeline: scroll()) {
147
+ animation:
148
+ scroll-fade-reveal-t 1ms ease-in-out,
149
+ scroll-fade-reveal-b 1ms ease-in-out;
150
+ animation-timeline: scroll(self y), scroll(self y);
151
+ animation-range:
152
+ 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)),
153
+ calc(100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))) 100%;
154
+ animation-fill-mode: both;
155
+ }
156
+
157
+ @supports not (animation-timeline: scroll()) {
158
+ --scroll-fade-t: var(--_scroll-fade-size-t);
159
+ --scroll-fade-b: var(--_scroll-fade-size-b);
160
+ }
161
+ }
162
+
163
+ @utility scroll-fade-x {
164
+ --_scroll-fade-size-s: var(
165
+ --scroll-fade-s-size,
166
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
167
+ );
168
+ --_scroll-fade-size-e: var(
169
+ --scroll-fade-e-size,
170
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
171
+ );
172
+ --scroll-fade-inline: linear-gradient(
173
+ to right,
174
+ transparent 0,
175
+ #000 var(--scroll-fade-s, 0px),
176
+ #000 calc(100% - var(--scroll-fade-e, 0px)),
177
+ transparent 100%
178
+ );
179
+ &:where([dir="rtl"], [dir="rtl"] *) {
180
+ --scroll-fade-inline: linear-gradient(
181
+ to left,
182
+ transparent 0,
183
+ #000 var(--scroll-fade-s, 0px),
184
+ #000 calc(100% - var(--scroll-fade-e, 0px)),
185
+ transparent 100%
186
+ );
187
+ }
188
+ -webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline));
189
+ mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline));
190
+ -webkit-mask-composite: source-in;
191
+ mask-composite: intersect;
192
+ -webkit-mask-repeat: no-repeat;
193
+ mask-repeat: no-repeat;
194
+
195
+ @supports (animation-timeline: scroll()) {
196
+ animation:
197
+ scroll-fade-reveal-s 1ms ease-in-out,
198
+ scroll-fade-reveal-e 1ms ease-in-out;
199
+ animation-timeline: scroll(self inline), scroll(self inline);
200
+ animation-range:
201
+ 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)),
202
+ calc(100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))) 100%;
203
+ animation-fill-mode: both;
204
+ }
205
+
206
+ @supports not (animation-timeline: scroll()) {
207
+ --scroll-fade-s: var(--_scroll-fade-size-s);
208
+ --scroll-fade-e: var(--_scroll-fade-size-e);
209
+ }
210
+ }
211
+
212
+ @utility scroll-fade-t {
213
+ --_scroll-fade-size-t: var(
214
+ --scroll-fade-t-size,
215
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
216
+ );
217
+ --scroll-fade-mask: linear-gradient(
218
+ to bottom,
219
+ transparent 0,
220
+ #000 var(--scroll-fade-t, 0px),
221
+ #000 100%
222
+ );
223
+ -webkit-mask-image: var(--scroll-fade-mask);
224
+ mask-image: var(--scroll-fade-mask);
225
+ -webkit-mask-composite: source-in;
226
+ mask-composite: intersect;
227
+ -webkit-mask-repeat: no-repeat;
228
+ mask-repeat: no-repeat;
229
+
230
+ @supports (animation-timeline: scroll()) {
231
+ animation: scroll-fade-reveal-t 1ms ease-in-out;
232
+ animation-timeline: scroll(self y);
233
+ animation-range: 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24));
234
+ animation-fill-mode: both;
235
+ }
236
+
237
+ @supports not (animation-timeline: scroll()) {
238
+ --scroll-fade-t: var(--_scroll-fade-size-t);
239
+ }
240
+ }
241
+
242
+ @utility scroll-fade-b {
243
+ --_scroll-fade-size-b: var(
244
+ --scroll-fade-b-size,
245
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
246
+ );
247
+ --scroll-fade-mask: linear-gradient(
248
+ to bottom,
249
+ #000 0,
250
+ #000 calc(100% - var(--scroll-fade-b, 0px)),
251
+ transparent 100%
252
+ );
253
+ -webkit-mask-image: var(--scroll-fade-mask);
254
+ mask-image: var(--scroll-fade-mask);
255
+ -webkit-mask-composite: source-in;
256
+ mask-composite: intersect;
257
+ -webkit-mask-repeat: no-repeat;
258
+ mask-repeat: no-repeat;
259
+
260
+ @supports (animation-timeline: scroll()) {
261
+ animation: scroll-fade-reveal-b 1ms ease-in-out;
262
+ animation-timeline: scroll(self y);
263
+ animation-range: calc(
264
+ 100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))
265
+ )
266
+ 100%;
267
+ animation-fill-mode: both;
268
+ }
269
+
270
+ @supports not (animation-timeline: scroll()) {
271
+ --scroll-fade-b: var(--_scroll-fade-size-b);
272
+ }
273
+ }
274
+
275
+ @utility scroll-fade-l {
276
+ --_scroll-fade-size-s: var(
277
+ --scroll-fade-s-size,
278
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
279
+ );
280
+ --scroll-fade-mask: linear-gradient(
281
+ to right,
282
+ transparent 0,
283
+ #000 var(--scroll-fade-s, 0px),
284
+ #000 100%
285
+ );
286
+ -webkit-mask-image: var(--scroll-fade-mask);
287
+ mask-image: var(--scroll-fade-mask);
288
+ -webkit-mask-composite: source-in;
289
+ mask-composite: intersect;
290
+ -webkit-mask-repeat: no-repeat;
291
+ mask-repeat: no-repeat;
292
+
293
+ @supports (animation-timeline: scroll()) {
294
+ animation: scroll-fade-reveal-s 1ms ease-in-out;
295
+ animation-timeline: scroll(self x);
296
+ animation-range: 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24));
297
+ animation-fill-mode: both;
298
+ }
299
+
300
+ @supports not (animation-timeline: scroll()) {
301
+ --scroll-fade-s: var(--_scroll-fade-size-s);
302
+ }
303
+ }
304
+
305
+ @utility scroll-fade-r {
306
+ --_scroll-fade-size-e: var(
307
+ --scroll-fade-e-size,
308
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
309
+ );
310
+ --scroll-fade-mask: linear-gradient(
311
+ to right,
312
+ #000 0,
313
+ #000 calc(100% - var(--scroll-fade-e, 0px)),
314
+ transparent 100%
315
+ );
316
+ -webkit-mask-image: var(--scroll-fade-mask);
317
+ mask-image: var(--scroll-fade-mask);
318
+ -webkit-mask-composite: source-in;
319
+ mask-composite: intersect;
320
+ -webkit-mask-repeat: no-repeat;
321
+ mask-repeat: no-repeat;
322
+
323
+ @supports (animation-timeline: scroll()) {
324
+ animation: scroll-fade-reveal-e 1ms ease-in-out;
325
+ animation-timeline: scroll(self x);
326
+ animation-range: calc(
327
+ 100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))
328
+ )
329
+ 100%;
330
+ animation-fill-mode: both;
331
+ }
332
+
333
+ @supports not (animation-timeline: scroll()) {
334
+ --scroll-fade-e: var(--_scroll-fade-size-e);
335
+ }
336
+ }
337
+
338
+ @utility scroll-fade-s {
339
+ --_scroll-fade-size-s: var(
340
+ --scroll-fade-s-size,
341
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
342
+ );
343
+ --scroll-fade-mask: linear-gradient(
344
+ to right,
345
+ transparent 0,
346
+ #000 var(--scroll-fade-s, 0px),
347
+ #000 100%
348
+ );
349
+ &:where([dir="rtl"], [dir="rtl"] *) {
350
+ --scroll-fade-mask: linear-gradient(
351
+ to left,
352
+ transparent 0,
353
+ #000 var(--scroll-fade-s, 0px),
354
+ #000 100%
355
+ );
356
+ }
357
+ -webkit-mask-image: var(--scroll-fade-mask);
358
+ mask-image: var(--scroll-fade-mask);
359
+ -webkit-mask-composite: source-in;
360
+ mask-composite: intersect;
361
+ -webkit-mask-repeat: no-repeat;
362
+ mask-repeat: no-repeat;
363
+
364
+ @supports (animation-timeline: scroll()) {
365
+ animation: scroll-fade-reveal-s 1ms ease-in-out;
366
+ animation-timeline: scroll(self inline);
367
+ animation-range: 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24));
368
+ animation-fill-mode: both;
369
+ }
370
+
371
+ @supports not (animation-timeline: scroll()) {
372
+ --scroll-fade-s: var(--_scroll-fade-size-s);
373
+ }
374
+ }
375
+
376
+ @utility scroll-fade-e {
377
+ --_scroll-fade-size-e: var(
378
+ --scroll-fade-e-size,
379
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
380
+ );
381
+ --scroll-fade-mask: linear-gradient(
382
+ to right,
383
+ #000 0,
384
+ #000 calc(100% - var(--scroll-fade-e, 0px)),
385
+ transparent 100%
386
+ );
387
+ &:where([dir="rtl"], [dir="rtl"] *) {
388
+ --scroll-fade-mask: linear-gradient(
389
+ to left,
390
+ #000 0,
391
+ #000 calc(100% - var(--scroll-fade-e, 0px)),
392
+ transparent 100%
393
+ );
394
+ }
395
+ -webkit-mask-image: var(--scroll-fade-mask);
396
+ mask-image: var(--scroll-fade-mask);
397
+ -webkit-mask-composite: source-in;
398
+ mask-composite: intersect;
399
+ -webkit-mask-repeat: no-repeat;
400
+ mask-repeat: no-repeat;
401
+
402
+ @supports (animation-timeline: scroll()) {
403
+ animation: scroll-fade-reveal-e 1ms ease-in-out;
404
+ animation-timeline: scroll(self inline);
405
+ animation-range: calc(
406
+ 100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))
407
+ )
408
+ 100%;
409
+ animation-fill-mode: both;
410
+ }
411
+
412
+ @supports not (animation-timeline: scroll()) {
413
+ --scroll-fade-e: var(--_scroll-fade-size-e);
414
+ }
415
+ }
416
+
417
+ @utility scroll-fade-* {
418
+ --scroll-fade-size: calc(var(--spacing) * --value(integer));
419
+ --scroll-fade-size: --value([length], [percentage]);
420
+ }
421
+
422
+ @utility scroll-fade-t-* {
423
+ --scroll-fade-t-size: calc(var(--spacing) * --value(integer));
424
+ --scroll-fade-t-size: --value([length], [percentage]);
425
+ }
426
+
427
+ @utility scroll-fade-b-* {
428
+ --scroll-fade-b-size: calc(var(--spacing) * --value(integer));
429
+ --scroll-fade-b-size: --value([length], [percentage]);
430
+ }
431
+
432
+ @utility scroll-fade-s-* {
433
+ --scroll-fade-s-size: calc(var(--spacing) * --value(integer));
434
+ --scroll-fade-s-size: --value([length], [percentage]);
435
+ }
436
+
437
+ @utility scroll-fade-e-* {
438
+ --scroll-fade-e-size: calc(var(--spacing) * --value(integer));
439
+ --scroll-fade-e-size: --value([length], [percentage]);
440
+ }
441
+
442
+ @utility scroll-fade-none {
443
+ --scroll-fade-mask: none;
444
+ }
445
+
446
+ /* shimmer */
447
+ @property --shimmer-angle {
448
+ syntax: "<angle>";
449
+ inherits: true;
450
+ initial-value: 20deg;
451
+ }
452
+ @property --shimmer-image {
453
+ syntax: "*";
454
+ inherits: false;
455
+ }
456
+ @property --shimmer-text-fill {
457
+ syntax: "*";
458
+ inherits: false;
459
+ }
460
+
461
+ @theme inline {
462
+ @keyframes tw-shimmer {
463
+ from {
464
+ background-position: 100% 0;
465
+ }
466
+ to {
467
+ background-position: 0 0;
468
+ }
469
+ }
470
+ }
471
+
472
+ @utility shimmer {
473
+ --_spread: var(--shimmer-spread, calc(3ch + 40px));
474
+ --_base: currentColor;
475
+ --_highlight: var(
476
+ --shimmer-color,
477
+ oklch(from currentColor l c h / calc(alpha* 0.2))
478
+ );
479
+
480
+ background-image: var(
481
+ --shimmer-image,
482
+ linear-gradient(
483
+ calc(90deg + var(--shimmer-angle)),
484
+ var(--_base) calc(50% - var(--_spread)),
485
+ color-mix(in oklch, var(--_highlight), var(--_base) 50%)
486
+ calc(50% - var(--_spread) * 0.5),
487
+ var(--_highlight) 50%,
488
+ color-mix(in oklch, var(--_highlight), var(--_base) 50%)
489
+ calc(50% + var(--_spread) * 0.5),
490
+ var(--_base) calc(50% + var(--_spread))
491
+ )
492
+ );
493
+ background-repeat: no-repeat;
494
+ background-size: calc(200% + var(--_spread) * 2) 100%;
495
+ background-position: 0 0;
496
+ background-clip: text;
497
+ -webkit-background-clip: text;
498
+ -webkit-text-fill-color: var(--shimmer-text-fill, transparent);
499
+ animation: tw-shimmer var(--shimmer-duration, 2s) linear infinite;
500
+
501
+ @variant dark {
502
+ --_highlight: var(
503
+ --shimmer-color,
504
+ oklch(from currentColor max(0.8, calc(l + 0.4)) c h / calc(alpha + 0.4))
505
+ );
506
+ }
507
+
508
+ &:where([dir="rtl"], [dir="rtl"] *) {
509
+ animation-direction: reverse;
510
+ }
511
+ }
512
+
513
+ @utility shimmer-once {
514
+ animation-iteration-count: 1;
515
+ }
516
+
517
+ @utility shimmer-reverse {
518
+ animation-direction: reverse;
519
+ }
520
+
521
+ @utility shimmer-none {
522
+ --shimmer-image: none;
523
+ --shimmer-text-fill: currentColor;
524
+ }
525
+
526
+ @utility shimmer-color-* {
527
+ --shimmer-color: --value(--color, [color]);
528
+ --shimmer-color: color-mix(
529
+ in oklch,
530
+ --value(--color, [color]) calc(--modifier(integer) * 1%),
531
+ transparent
532
+ );
533
+ }
534
+
535
+ @utility shimmer-duration-* {
536
+ --shimmer-duration: calc(--value(integer) * 1ms);
537
+ }
538
+
539
+ @utility shimmer-spread-* {
540
+ --shimmer-spread: calc(var(--spacing) * --value(integer));
541
+ --shimmer-spread: --value([length], [percentage]);
542
+ }
543
+
544
+ @utility shimmer-angle-* {
545
+ --shimmer-angle: calc(--value(integer) * 1deg);
546
+ }
547
+
548
+ @media (prefers-reduced-motion: reduce) {
549
+ .shimmer {
550
+ animation: none;
551
+ background-image: none;
552
+ -webkit-text-fill-color: currentColor;
553
+ }
554
+ }
555
+
556
+
557
+ /* ---------------------------------------------------------------------------
558
+ * scrollbar-* — minimal native scrollbar helpers (VegaStack addition; not in
559
+ * upstream shadcn tailwind.css). Used by message-scroller's viewport.
560
+ * ------------------------------------------------------------------------- */
561
+ @utility scrollbar-none {
562
+ scrollbar-width: none;
563
+ &::-webkit-scrollbar {
564
+ display: none;
565
+ }
566
+ }
567
+
568
+ @utility scrollbar-thin {
569
+ scrollbar-width: thin;
570
+ }
571
+
572
+ @utility scrollbar-gutter-stable {
573
+ scrollbar-gutter: stable;
574
+ }
575
+
576
+
577
+ /* ---------------------------------------------------------------------------
578
+ * motion-* — the state-feedback animation vocabulary (VegaStack addition,
579
+ * Phase M). Mount-triggered CSS animations for the "keyed presence" mechanism:
580
+ * remount an element (change its React `key`) and the animation plays once.
581
+ * Every value routes through the motion tokens; the global
582
+ * prefers-reduced-motion reset in base.css collapses all of these to their
583
+ * settled end state (each keyframe's `to` equals the natural resting style).
584
+ *
585
+ * - motion-pop-in : scale+fade arrival for icon/text swaps and badges
586
+ * (the "success bounce" — spring ease, tiny overshoot).
587
+ * - motion-enter-up : fade+rise arrival for content (chat messages,
588
+ * skeleton→content reveal).
589
+ * - motion-shake : horizontal error jiggle. Replayable: re-trigger by
590
+ * remounting (key bump) or toggling the class after
591
+ * `animationend`.
592
+ * ------------------------------------------------------------------------- */
593
+ @keyframes vs-pop-in {
594
+ from {
595
+ opacity: 0;
596
+ scale: 0.9;
597
+ }
598
+ to {
599
+ opacity: 1;
600
+ scale: 1;
601
+ }
602
+ }
603
+
604
+ @utility motion-pop-in {
605
+ animation: vs-pop-in var(--duration-fast) var(--motion-ease-spring);
606
+ }
607
+
608
+ @keyframes vs-enter-up {
609
+ from {
610
+ opacity: 0;
611
+ translate: 0 calc(var(--spacing) * 1);
612
+ }
613
+ to {
614
+ opacity: 1;
615
+ translate: 0 0;
616
+ }
617
+ }
618
+
619
+ @utility motion-enter-up {
620
+ animation: vs-enter-up var(--duration-fast) var(--motion-ease-standard);
621
+ }
622
+
623
+ @keyframes vs-shake {
624
+ /* Small ±4px jiggle, decaying — subtle by design (dense dev-tool aesthetic). */
625
+ 15% {
626
+ translate: calc(var(--spacing) * -1) 0;
627
+ }
628
+ 40% {
629
+ translate: calc(var(--spacing) * 1) 0;
630
+ }
631
+ 65% {
632
+ translate: calc(var(--spacing) * -0.5) 0;
633
+ }
634
+ 90% {
635
+ translate: calc(var(--spacing) * 0.5) 0;
636
+ }
637
+ to {
638
+ translate: 0 0;
639
+ }
640
+ }
641
+
642
+ @utility motion-shake {
643
+ animation: vs-shake var(--duration-slow) var(--motion-ease-standard);
644
+ }
645
+
646
+ /* ---------------------------------------------------------------------------
647
+ * .vs-marketing — the marketing dark-ground SCOPE MECHANISM (Phase B, audit
648
+ * 17-brand-direction §Color & surface, CX-10). Marketing surfaces (hero,
649
+ * section bands, the Terminal primitive) render the warm ramp's DARK end
650
+ * regardless of the page/product theme — `.dark` alone can't express "product
651
+ * light + marketing dark on the SAME page," which is exactly the co-existing
652
+ * case this class exists for (docs-home is light by default but its hero is
653
+ * dark-first). Any subtree wrapped in `.vs-marketing` (see the
654
+ * `MarketingSurface` primitive, and `Terminal`, which self-scopes) gets the
655
+ * same semantic custom properties `.dark` sets, scoped LOCALLY instead of
656
+ * gated on the page-root class — every `bg-background` / `text-foreground` /
657
+ * `border-border` / `bg-brand` utility inside it resolves through these, with
658
+ * NO component code changes (any existing token-built component composes
659
+ * correctly, since only the custom-property VALUES change, never the
660
+ * utility-to-var wiring).
661
+ *
662
+ * Values are a RATIONED, MANUALLY-SYNCED duplicate of the resolved oklch()
663
+ * output of `packages/design-tokens/tokens/semantic.dark.tokens.json` (the same
664
+ * values `.dark` sets) — the same "one sanctioned literal duplication"
665
+ * precedent as the `overlay` scrim token in that file. The principled fix is
666
+ * a THIRD generated Style Dictionary platform run in `build-tokens.mjs`
667
+ * (source `semantic.dark.tokens.json`, selector `.vs-marketing` instead of
668
+ * `.dark`) so this can never drift silently — that's a tokens-BUILD-PIPELINE
669
+ * change outside this component-level addition; flagged for whoever owns
670
+ * `packages/design-tokens/build-tokens.mjs` next. If `semantic.dark.tokens.json`
671
+ * changes, update this block to match (or land the generated version).
672
+ *
673
+ * LIMITATION (documented, not fixed here): Base UI floating surfaces portal
674
+ * to `<body>`, OUTSIDE this scoped subtree — a popover/tooltip/menu/dialog
675
+ * opened from inside a `.vs-marketing` surface renders with the PAGE theme,
676
+ * not the marketing dark ground. Marketing surfaces rarely need portals (a
677
+ * hero/section band is not app chrome); the docs-home dogfood doesn't use one.
678
+ * ------------------------------------------------------------------------- */
679
+ .vs-marketing {
680
+ color-scheme: dark;
681
+
682
+ --background: oklch(0.175 0.003 75);
683
+ --foreground: oklch(0.922 0.003 75);
684
+ --card: oklch(0.205 0.003 75);
685
+ --card-foreground: oklch(0.922 0.003 75);
686
+ --popover: oklch(0.205 0.003 75);
687
+ --popover-foreground: oklch(0.922 0.003 75);
688
+
689
+ --primary: oklch(0.922 0.003 75);
690
+ --primary-foreground: oklch(0.145 0.003 75);
691
+ --primary-hover: oklch(0.97 0.003 75);
692
+ --primary-active: oklch(0.994 0.002 75);
693
+
694
+ --secondary: oklch(0.269 0.003 75);
695
+ --secondary-foreground: oklch(0.922 0.003 75);
696
+ --muted: oklch(0.269 0.003 75);
697
+ --muted-foreground: oklch(0.66 0.003 75);
698
+ --muted-foreground-faint: oklch(0.559 0.003 75);
699
+ --accent: oklch(0.269 0.003 75);
700
+ --accent-foreground: oklch(0.967 0.003 75);
701
+
702
+ --border: oklch(0.269 0.003 75);
703
+ --input: var(--border);
704
+ --ring: var(--primary);
705
+ --overlay: oklch(0 0 0 / 0.55);
706
+
707
+ /* Brand phosphor — DARK half (13.3:1 on this ground, MK's pick), independent
708
+ of whether `.dark` is present anywhere in the ancestor chain. Marker roles
709
+ only (live-state dot, sparkline endpoint, eyebrow highlight, terminal
710
+ prompt glyph) — see design.md §Brand & Marketing. */
711
+ --brand: oklch(0.86 0.21 148);
712
+ }