fancy-ui-react 0.2.1 → 0.4.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 (93) hide show
  1. package/README.md +56 -20
  2. package/dist/components/animated-beam/AnimatedBeam.d.ts +47 -11
  3. package/dist/components/animated-beam/AnimatedBeam.js +293 -118
  4. package/dist/components/animated-tooltip/AnimatedTooltip.d.ts +10 -1
  5. package/dist/components/animated-tooltip/AnimatedTooltip.js +170 -101
  6. package/dist/components/bento-grid/BentoFrame.d.ts +16 -0
  7. package/dist/components/bento-grid/BentoFrame.js +28 -0
  8. package/dist/components/bento-grid/BentoGrid.d.ts +5 -1
  9. package/dist/components/bento-grid/BentoGrid.js +71 -2
  10. package/dist/components/bento-grid/BentoGridCard.js +34 -26
  11. package/dist/components/bento-grid/BentoGridItem.js +10 -10
  12. package/dist/components/border-beam/BorderBeam.d.ts +55 -10
  13. package/dist/components/border-beam/BorderBeam.js +93 -16
  14. package/dist/components/button/Button.js +5 -1
  15. package/dist/components/compare/Compare.d.ts +32 -8
  16. package/dist/components/compare/Compare.js +213 -95
  17. package/dist/components/container-scroll/ContainerScroll.d.ts +26 -4
  18. package/dist/components/container-scroll/ContainerScroll.js +88 -49
  19. package/dist/components/datamosh-transition/DatamoshTransition.d.ts +76 -0
  20. package/dist/components/datamosh-transition/DatamoshTransition.js +368 -0
  21. package/dist/components/datamosh-transition/datamosh-transition-core.d.ts +180 -0
  22. package/dist/components/datamosh-transition/datamosh-transition-core.js +370 -0
  23. package/dist/components/datamosh-transition/index.d.ts +3 -0
  24. package/dist/components/dock/Dock.d.ts +10 -7
  25. package/dist/components/dock/Dock.js +76 -31
  26. package/dist/components/dock/DockIcon.d.ts +14 -3
  27. package/dist/components/dock/DockIcon.js +23 -19
  28. package/dist/components/dock/DockSeparator.d.ts +4 -1
  29. package/dist/components/dock/DockSeparator.js +5 -2
  30. package/dist/components/dock/types.d.ts +13 -6
  31. package/dist/components/dropdown-menu/menu-shared.d.ts +11 -8
  32. package/dist/components/editorial-engine/engine.js +1 -14
  33. package/dist/components/fireworks-hdr/webgl2-renderer.js +0 -19
  34. package/dist/components/fireworks-hdr/webgpu-renderer.js +3 -22
  35. package/dist/components/flip-card/FlipCard.d.ts +42 -1
  36. package/dist/components/flip-card/FlipCard.js +178 -25
  37. package/dist/components/flip-card/index.d.ts +1 -1
  38. package/dist/components/fluid-cursor/FluidCursor.js +5 -4
  39. package/dist/components/fluid-cursor/dev.d.ts +1 -0
  40. package/dist/components/fluid-cursor/dev.js +8 -0
  41. package/dist/components/fluid-cursor/fluid-shared.d.ts +0 -2
  42. package/dist/components/fluid-cursor/fluid-shared.js +0 -2
  43. package/dist/components/fluid-cursor/webgpu-engine.js +10 -24
  44. package/dist/components/glow-border/GlowBorder.d.ts +41 -1
  45. package/dist/components/glow-border/GlowBorder.js +96 -16
  46. package/dist/components/glow-border/index.d.ts +2 -2
  47. package/dist/components/gradient-button/GradientButton.d.ts +10 -5
  48. package/dist/components/gradient-button/GradientButton.js +25 -18
  49. package/dist/components/image-trail-cursor/ImageTrailCursor.js +1 -0
  50. package/dist/components/image-trail-cursor/trail-variants.d.ts +37 -1
  51. package/dist/components/image-trail-cursor/trail-variants.js +331 -8
  52. package/dist/components/interactive-hover-button/InteractiveHoverButton.js +30 -35
  53. package/dist/components/marquee/Marquee.d.ts +7 -3
  54. package/dist/components/marquee/Marquee.js +134 -2
  55. package/dist/components/marquee/ReviewCard.d.ts +2 -1
  56. package/dist/components/marquee/ReviewCard.js +20 -9
  57. package/dist/components/meteors/Meteors.d.ts +32 -10
  58. package/dist/components/meteors/Meteors.js +53 -20
  59. package/dist/components/mosaic-glow/mosaic-glow-core.d.ts +98 -10
  60. package/dist/components/neon-border/NeonBorder.d.ts +24 -5
  61. package/dist/components/neon-border/NeonBorder.js +37 -7
  62. package/dist/components/pagination/Pagination.js +220 -82
  63. package/dist/components/rainbow-button/RainbowButton.js +13 -5
  64. package/dist/components/ripple-button/RippleButton.d.ts +20 -1
  65. package/dist/components/ripple-button/RippleButton.js +44 -21
  66. package/dist/components/shimmer-button/ShimmerButton.d.ts +5 -5
  67. package/dist/components/shimmer-button/ShimmerButton.js +17 -19
  68. package/dist/components/smooth-cursor/SmoothCursor.d.ts +13 -9
  69. package/dist/components/smooth-cursor/SmoothCursor.js +114 -163
  70. package/dist/components/smooth-cursor/smooth-cursor-core.d.ts +85 -0
  71. package/dist/components/smooth-cursor/smooth-cursor-core.js +327 -0
  72. package/dist/components/stepper/Step.js +32 -19
  73. package/dist/components/stepper/Stepper.js +18 -2
  74. package/dist/components/stepper/types.d.ts +13 -0
  75. package/dist/components/timeline/Timeline.d.ts +8 -1
  76. package/dist/components/timeline/Timeline.js +165 -76
  77. package/dist/index.d.ts +1 -0
  78. package/dist/index.js +6 -1
  79. package/dist/internals/markdown.d.ts +5 -0
  80. package/dist/internals/motion/haptics.d.ts +0 -4
  81. package/dist/internals/motion/presence-core.d.ts +59 -0
  82. package/dist/internals/motion/presence-core.js +162 -0
  83. package/dist/internals/motion/presence.d.ts +2 -4
  84. package/dist/internals/motion/presence.js +1 -156
  85. package/dist/internals/motion/raf.d.ts +1 -1
  86. package/dist/internals/motion/stagger.d.ts +3 -4
  87. package/dist/internals/motion/tokens.js +4 -0
  88. package/dist/internals/motion/transitions.d.ts +17 -17
  89. package/dist/internals/motion/types.d.ts +6 -7
  90. package/dist/internals/relative-time.d.ts +1 -1
  91. package/dist/styles.css +3545 -204
  92. package/package.json +18 -2
  93. package/dist/components/fireworks-hdr/webgpu-types.d.ts +0 -159
package/dist/styles.css CHANGED
@@ -313,7 +313,81 @@
313
313
  * scales the panel about its own centre without touching the centring.
314
314
  */
315
315
  .ft-btn {
316
- --ft-btn-accent: var(--ft-accent, light-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75)));
316
+ --ft-btn-accent: var(
317
+ --ft-accent,
318
+ light-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75))
319
+ );
320
+ /* 150ms = tokens.DURATIONS.fast, cubic-bezier(0.4, 0, 0.2, 1) =
321
+ tokens.EASINGS.inout — the reversible-state pair. It runs the colour
322
+ channel, and the release half of the press: letting go settles, it does
323
+ not snap back. */
324
+ --ft-btn-motion: var(--ft-duration-fast, 150ms) var(--ft-ease-inout, cubic-bezier(0.4, 0, 0.2, 1));
325
+ /* The down-stroke only. 150ms = tokens.DURATIONS.fast,
326
+ cubic-bezier(0.16, 1, 0.3, 1) = tokens.EASINGS.out — the arrival curve,
327
+ which spends most of the scale in the first frames, so the press bites
328
+ instead of easing in. Pressing is an arrival; releasing is what resolves
329
+ either way. */
330
+ --ft-btn-press: var(--ft-duration-fast, 150ms) var(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
331
+ /*
332
+ * Replaces the `transition-colors` utility removed from the class string,
333
+ * at exactly the values that utility already resolved to. Colour is a state
334
+ * change, not motion, so it stays OUTSIDE the reduced-motion query — gating
335
+ * it would only make a theme flip look broken for the people who asked for
336
+ * less movement.
337
+ *
338
+ * `box-shadow` is deliberately absent, and must stay absent: this button's
339
+ * focus ring is `focus-visible:ring-*`, which compiles to a `box-shadow`,
340
+ * and a focus ring must never animate.
341
+ */
342
+ transition:
343
+ color var(--ft-btn-motion),
344
+ background-color var(--ft-btn-motion),
345
+ border-color var(--ft-btn-motion);
346
+ /* Kills the ~300ms tap delay without blocking scroll. A press that answers
347
+ a third of a second late is not press feedback. */
348
+ touch-action: manipulation;
349
+ }
350
+
351
+ /*
352
+ * The press. Pressable's contract inlined as a plain rule on the native
353
+ * control rather than wrapping a `<button>` in a `<Pressable>` div — same
354
+ * `0.97`, same clock, one element instead of two.
355
+ *
356
+ * Only the property list is re-declared in here: `transform` joins the colour
357
+ * channel under `no-preference` and nowhere else, so with motion reduced the
358
+ * colours still cross and the button simply does not move. The resting state
359
+ * (no `transform` at all) is the ungated fallback.
360
+ *
361
+ * The two strokes are timed apart. The rule below carries the release curve,
362
+ * because that is the transition the button runs on its way back to rest;
363
+ * `:active` overrides `transform` with the arrival curve for the way down.
364
+ */
365
+ @media (prefers-reduced-motion: no-preference) {
366
+ .ft-btn {
367
+ transition:
368
+ color var(--ft-btn-motion),
369
+ background-color var(--ft-btn-motion),
370
+ border-color var(--ft-btn-motion),
371
+ transform var(--ft-btn-motion);
372
+ }
373
+
374
+ /* Both inert states are excluded so a press that does nothing does not
375
+ pretend to. `[data-disabled]` is the anchor branch, which has no native
376
+ `:disabled` to hang this off; `[aria-busy]` is the loading one, on both
377
+ branches. A native disabled `<button>` never matches `:active` anyway —
378
+ the attribute selectors are what make the anchor behave like it. */
379
+ .ft-btn:not([data-disabled="true"]):not([aria-busy="true"]):active {
380
+ transform: scale(0.97);
381
+ /* The whole list is re-declared rather than just
382
+ `transition-timing-function`, which has no per-property form here: a
383
+ single value would retime the colour channel too. Only `transform`
384
+ differs from the rule above. */
385
+ transition:
386
+ color var(--ft-btn-motion),
387
+ background-color var(--ft-btn-motion),
388
+ border-color var(--ft-btn-motion),
389
+ transform var(--ft-btn-press);
390
+ }
317
391
  }
318
392
 
319
393
  .ft-btn--accent {
@@ -355,6 +429,422 @@
355
429
  transform: rotate(360deg);
356
430
  }
357
431
  }
432
+ /*
433
+ * `.animated-beam` is a port-added anchor: the source has no root class and
434
+ * relied on compiler scoping, while plain CSS is global. Every rule below is
435
+ * anchored on it; class names, custom properties and keyframe names are
436
+ * identical to the source.
437
+ */
438
+
439
+ /* Theme-aware glass. Public vars first, house defaults as fallbacks. */
440
+ .animated-beam .fibre-sheath {
441
+ stroke: var(--beam-fibre-rim, rgba(0, 0, 0, 0.07));
442
+ }
443
+ .animated-beam .fibre-core {
444
+ stroke: var(--beam-fibre, rgba(0, 0, 0, 0.18));
445
+ }
446
+ .animated-beam .fibre-glint {
447
+ stroke: var(--beam-fibre-glint, rgba(255, 255, 255, 0.75));
448
+ }
449
+ .dark .animated-beam .fibre-sheath {
450
+ stroke: var(--beam-fibre-rim, rgba(255, 255, 255, 0.06));
451
+ }
452
+ .dark .animated-beam .fibre-core {
453
+ stroke: var(--beam-fibre, rgba(255, 255, 255, 0.14));
454
+ }
455
+ .dark .animated-beam .fibre-glint {
456
+ stroke: var(--beam-fibre-glint, rgba(255, 255, 255, 0.16));
457
+ }
458
+
459
+ /* On a light surface a white core disappears; keep more colour there. */
460
+ .animated-beam .packet-core {
461
+ stroke: color-mix(in oklab, var(--_core) 72%, white);
462
+ }
463
+ .dark .animated-beam .packet-core {
464
+ stroke: color-mix(in oklab, var(--_core) 45%, white);
465
+ }
466
+
467
+ .animated-beam .packet-layer {
468
+ stroke-dasharray: var(--_len) 3000;
469
+ stroke-dashoffset: var(--_o0);
470
+ animation: beam-packet var(--_dur, 5s) linear var(--_delay, 0s) infinite backwards;
471
+ }
472
+
473
+ /* Glide out, settle into the node at ARRIVAL (78%), then drain the tail. */
474
+ @keyframes beam-packet {
475
+ 0% {
476
+ stroke-dashoffset: var(--_o0);
477
+ animation-timing-function: cubic-bezier(0.45, 0, 0.2, 1);
478
+ }
479
+ 78% {
480
+ stroke-dashoffset: var(--_o1);
481
+ animation-timing-function: cubic-bezier(0.5, 0, 1, 1);
482
+ }
483
+ 100% {
484
+ stroke-dashoffset: var(--_o2);
485
+ }
486
+ }
487
+
488
+ .animated-beam .bloom-disc,
489
+ .animated-beam .bloom-ring {
490
+ transform-box: fill-box;
491
+ transform-origin: center;
492
+ opacity: 0;
493
+ animation: beam-bloom var(--_dur, 5s) linear var(--_delay, 0s) infinite backwards;
494
+ }
495
+ .animated-beam .bloom-ring {
496
+ animation-name: beam-ring;
497
+ }
498
+
499
+ /* The head lands at 78%: the halo swells behind the node, then lets go. */
500
+ @keyframes beam-bloom {
501
+ 0%,
502
+ 74% {
503
+ opacity: 0;
504
+ transform: scale(0.6);
505
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
506
+ }
507
+ 84% {
508
+ opacity: 0.85;
509
+ transform: scale(1);
510
+ animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
511
+ }
512
+ 100% {
513
+ opacity: 0;
514
+ transform: scale(1.15);
515
+ }
516
+ }
517
+
518
+ /* A single ripple leaves the node on arrival. */
519
+ @keyframes beam-ring {
520
+ 0%,
521
+ 77% {
522
+ opacity: 0;
523
+ transform: scale(0.96);
524
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
525
+ }
526
+ 81% {
527
+ opacity: 0.7;
528
+ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
529
+ }
530
+ 100% {
531
+ opacity: 0;
532
+ transform: scale(1.5);
533
+ }
534
+ }
535
+
536
+ /* Before hydration can ask the browser, keep reduced-motion users still. */
537
+ @media (prefers-reduced-motion: reduce) {
538
+ .animated-beam .packet {
539
+ display: none;
540
+ }
541
+ }
542
+ /*
543
+ * Every rule below is anchored on `.at-root`, the class the source's own root
544
+ * element carries: the source's compiler scoped these selectors, plain CSS does
545
+ * not, so the anchor keeps the `at-*` hooks from leaking into a consumer page.
546
+ * Class names, custom properties, `@property` and keyframe names are identical
547
+ * to the source's; they are the visual contract.
548
+ */
549
+
550
+ @property --at-angle {
551
+ syntax: "<angle>";
552
+ inherits: false;
553
+ initial-value: 0deg;
554
+ }
555
+
556
+ .at-root {
557
+ /* Public, class-themeable knobs with private fallbacks. */
558
+ --_at-a: var(--at-accent, oklch(0.62 0.13 295));
559
+ --_at-b: var(--at-accent-2, oklch(0.7 0.1 220));
560
+ --_at-core: color-mix(in oklab, var(--_at-a) 45%, white);
561
+ --_at-sep: var(--at-separator, var(--background, #ffffff));
562
+ --_at-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
563
+ --_at-ease-rise: cubic-bezier(0.34, 1.32, 0.64, 1);
564
+ --_at-ease-inout: cubic-bezier(0.4, 0, 0.2, 1);
565
+ --_at-card-bg: rgba(255, 255, 255, 0.85);
566
+ --_at-card-line: rgba(0, 0, 0, 0.08);
567
+ --_at-card-hi: rgba(255, 255, 255, 0.9);
568
+ --_at-card-shadow: 0 10px 28px -12px rgba(0, 0, 0, 0.28), 0 2px 6px -2px rgba(0, 0, 0, 0.1);
569
+ --_at-name: #111113;
570
+ --_at-role: rgba(0, 0, 0, 0.55);
571
+ }
572
+
573
+ .dark .at-root {
574
+ --_at-a: var(--at-accent, oklch(0.78 0.1 295));
575
+ --_at-b: var(--at-accent-2, oklch(0.83 0.08 210));
576
+ --_at-card-bg: rgba(20, 20, 22, 0.85);
577
+ --_at-card-line: rgba(255, 255, 255, 0.1);
578
+ --_at-card-hi: rgba(255, 255, 255, 0.06);
579
+ --_at-card-shadow: 0 14px 32px -12px rgba(0, 0, 0, 0.8), 0 2px 6px -2px rgba(0, 0, 0, 0.5);
580
+ --_at-name: #f4f4f5;
581
+ --_at-role: rgba(255, 255, 255, 0.55);
582
+ }
583
+
584
+ /* ---------- stack ---------- */
585
+
586
+ .at-root .at-item {
587
+ outline: none;
588
+ transform: translateX(var(--_at-shift, 0px));
589
+ }
590
+
591
+ .at-root .at-item:not(:last-child) {
592
+ margin-inline-end: calc(var(--_at-size) * -0.285);
593
+ }
594
+
595
+ .at-root .at-item.at-active {
596
+ z-index: 30;
597
+ }
598
+
599
+ .at-root .at-avatar {
600
+ position: relative;
601
+ width: var(--_at-size);
602
+ height: var(--_at-size);
603
+ border-radius: 9999px;
604
+ isolation: isolate;
605
+ transform-origin: 50% 60%;
606
+ }
607
+
608
+ /* The hairline that separates stacked avatars, in the page colour. */
609
+ .at-root .at-disc {
610
+ position: absolute;
611
+ inset: 0;
612
+ z-index: 1;
613
+ border-radius: 9999px;
614
+ background: var(--_at-sep);
615
+ box-shadow: 0 0 0 2px var(--_at-sep);
616
+ }
617
+
618
+ .at-root .at-avatar.at-lifted {
619
+ transform: translateY(-4px) scale(1.08);
620
+ }
621
+
622
+ .at-root .at-img {
623
+ position: absolute;
624
+ inset: 3px;
625
+ width: calc(100% - 6px);
626
+ height: calc(100% - 6px);
627
+ z-index: 3;
628
+ }
629
+
630
+ /* The lit ring is the focus indicator; forced-colors mode strips it, so
631
+ fall back to a system-coloured outline there. */
632
+ @media (forced-colors: active) {
633
+ .at-root .at-item:focus-visible .at-avatar {
634
+ outline: 2px solid Highlight;
635
+ outline-offset: 2px;
636
+ }
637
+ }
638
+
639
+ /* ---------- presence ring ---------- */
640
+
641
+ .at-root .at-ring,
642
+ .at-root .at-glow {
643
+ position: absolute;
644
+ border-radius: 9999px;
645
+ background: conic-gradient(
646
+ from var(--at-angle),
647
+ var(--_at-a) 0deg,
648
+ var(--_at-b) 150deg,
649
+ var(--_at-core) 205deg,
650
+ var(--_at-a) 260deg,
651
+ var(--_at-a) 360deg
652
+ );
653
+ }
654
+
655
+ .at-root .at-ring {
656
+ inset: 0;
657
+ z-index: 2;
658
+ padding: 2px;
659
+ -webkit-mask:
660
+ linear-gradient(#000 0 0) content-box,
661
+ linear-gradient(#000 0 0);
662
+ -webkit-mask-composite: xor;
663
+ mask:
664
+ linear-gradient(#000 0 0) content-box exclude,
665
+ linear-gradient(#000 0 0);
666
+ opacity: 0.35;
667
+ transition: opacity 300ms var(--_at-ease-inout);
668
+ }
669
+
670
+ .at-root .at-glow {
671
+ inset: -3px;
672
+ z-index: 0;
673
+ filter: blur(7px);
674
+ opacity: 0;
675
+ transition: opacity 300ms var(--_at-ease-inout);
676
+ }
677
+
678
+ .at-root .at-active .at-ring {
679
+ opacity: 1;
680
+ }
681
+
682
+ .at-root .at-active .at-glow {
683
+ opacity: 0.55;
684
+ }
685
+
686
+ /* ---------- glass card ---------- */
687
+
688
+ .at-root .at-tip {
689
+ bottom: calc(100% + 12px);
690
+ transform-origin: 50% 100%;
691
+ }
692
+
693
+ .at-root .at-card {
694
+ position: relative;
695
+ display: flex;
696
+ flex-direction: column;
697
+ align-items: center;
698
+ gap: 5px;
699
+ padding: 8px 14px 9px;
700
+ border-radius: 10px;
701
+ white-space: nowrap;
702
+ background: var(--_at-card-bg);
703
+ border: 1px solid var(--_at-card-line);
704
+ box-shadow:
705
+ inset 0 1px 0 var(--_at-card-hi),
706
+ var(--_at-card-shadow);
707
+ -webkit-backdrop-filter: blur(12px) saturate(1.4);
708
+ backdrop-filter: blur(12px) saturate(1.4);
709
+ transform-origin: 50% 100%;
710
+ animation: at-fade 160ms var(--_at-ease-out) both;
711
+ }
712
+
713
+ .at-root .at-pointer {
714
+ position: absolute;
715
+ left: 50%;
716
+ bottom: -5px;
717
+ width: 9px;
718
+ height: 9px;
719
+ margin-left: -4.5px;
720
+ background: var(--_at-card-bg);
721
+ border-right: 1px solid var(--_at-card-line);
722
+ border-bottom: 1px solid var(--_at-card-line);
723
+ border-bottom-right-radius: 2px;
724
+ transform: rotate(45deg);
725
+ clip-path: polygon(100% 0, 100% 100%, 0 100%);
726
+ -webkit-backdrop-filter: blur(12px);
727
+ backdrop-filter: blur(12px);
728
+ }
729
+
730
+ .at-root .at-name {
731
+ font-size: 0.8125rem;
732
+ line-height: 1.1rem;
733
+ font-weight: 600;
734
+ letter-spacing: -0.005em;
735
+ color: var(--_at-name);
736
+ }
737
+
738
+ .at-root .at-role {
739
+ font-size: 0.6875rem;
740
+ line-height: 0.9rem;
741
+ color: var(--_at-role);
742
+ }
743
+
744
+ /* One thin line of light under the name: a faint resting hairline plus a
745
+ glint that sweeps across it once when the card opens. */
746
+ .at-root .at-rule {
747
+ position: relative;
748
+ display: block;
749
+ width: 100%;
750
+ min-width: 64px;
751
+ height: 7px;
752
+ margin: -3px 0;
753
+ overflow: clip;
754
+ background: linear-gradient(
755
+ 90deg,
756
+ transparent,
757
+ color-mix(in oklab, var(--_at-a) 50%, transparent) 30%,
758
+ color-mix(in oklab, var(--_at-b) 50%, transparent) 70%,
759
+ transparent
760
+ )
761
+ center / 100% 1px no-repeat;
762
+ }
763
+
764
+ .at-root .at-glint {
765
+ display: none;
766
+ }
767
+
768
+ @media (prefers-reduced-motion: no-preference) {
769
+ .at-root .at-item {
770
+ transition: transform 380ms var(--_at-ease-out);
771
+ }
772
+
773
+ .at-root .at-avatar {
774
+ transition: transform 420ms var(--_at-ease-out);
775
+ }
776
+
777
+ .at-root .at-ring,
778
+ .at-root .at-glow {
779
+ transition:
780
+ opacity 300ms var(--_at-ease-inout),
781
+ --at-angle 900ms var(--_at-ease-out);
782
+ }
783
+
784
+ .at-root .at-active .at-ring,
785
+ .at-root .at-active .at-glow {
786
+ --at-angle: 140deg;
787
+ }
788
+
789
+ .at-root .at-tip {
790
+ transition: transform 240ms var(--_at-ease-out);
791
+ }
792
+
793
+ .at-root .at-card {
794
+ animation: at-rise 260ms var(--_at-ease-rise) both;
795
+ }
796
+
797
+ .at-root .at-glint {
798
+ display: block;
799
+ position: absolute;
800
+ top: 3px;
801
+ left: 0;
802
+ width: 60%;
803
+ height: 1px;
804
+ border-radius: 1px;
805
+ background: linear-gradient(
806
+ 90deg,
807
+ transparent,
808
+ var(--_at-a) 25%,
809
+ var(--_at-core) 50%,
810
+ var(--_at-b) 75%,
811
+ transparent
812
+ );
813
+ /* Soft halo so a 1px line still reads as light. */
814
+ filter: drop-shadow(0 0 2px var(--_at-a)) drop-shadow(0 0 4px var(--_at-b));
815
+ transform: translateX(-110%);
816
+ animation: at-sweep 760ms var(--_at-ease-inout) 90ms both;
817
+ }
818
+ }
819
+
820
+ @keyframes at-rise {
821
+ from {
822
+ opacity: 0;
823
+ transform: translateY(6px) scale(0.94);
824
+ }
825
+ to {
826
+ opacity: 1;
827
+ transform: none;
828
+ }
829
+ }
830
+
831
+ @keyframes at-fade {
832
+ from {
833
+ opacity: 0;
834
+ }
835
+ to {
836
+ opacity: 1;
837
+ }
838
+ }
839
+
840
+ @keyframes at-sweep {
841
+ from {
842
+ transform: translateX(-110%);
843
+ }
844
+ to {
845
+ transform: translateX(180%);
846
+ }
847
+ }
358
848
  /*
359
849
  * Every colour is read at the point of use through two hooks: this card's own
360
850
  * `--ft-approval-*`, then the `--ft-status-*` vocabulary the whole AI family
@@ -778,6 +1268,240 @@
778
1268
  border-color: var(--ft-field-accent);
779
1269
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--ft-field-accent) 25%, transparent);
780
1270
  }
1271
+ /*
1272
+ * Ported from BentoGrid.svelte's <style> block. The rules were already
1273
+ * anchored on the grid's own root class (.fancy-bento) there; Svelte's
1274
+ * `:global(*)` child selector becomes a plain `*`.
1275
+ *
1276
+ * The hidden state exists only for viewers who allow motion and whose
1277
+ * browser runs scripts — everyone else gets the resting, visible grid
1278
+ * without waiting on JS. Transforms and opacity only.
1279
+ */
1280
+ @media (prefers-reduced-motion: no-preference) and (scripting: enabled) {
1281
+ .fancy-bento.bento-reveal[data-state="armed"] > * {
1282
+ opacity: 0;
1283
+ transform: translate3d(0, 18px, 0) scale(0.985);
1284
+ }
1285
+ .fancy-bento.bento-reveal[data-state="shown"] > * {
1286
+ transition:
1287
+ opacity 600ms cubic-bezier(0.4, 0, 0.2, 1),
1288
+ transform 800ms cubic-bezier(0.16, 1, 0.3, 1),
1289
+ border-color 300ms cubic-bezier(0.4, 0, 0.2, 1); /* DURATIONS.entrance, EASINGS.out */
1290
+ transition-delay: var(--bento-delay, 0ms), var(--bento-delay, 0ms), 0ms;
1291
+ }
1292
+ }
1293
+ /*
1294
+ * Ported from BentoFrame.svelte's <style> block. Every rule is anchored on
1295
+ * the tile's own root class (.bento-tile). The source's compiler-scoped
1296
+ * `.bento-panel`, `.bento-glow` and `.bento-edge` selectors are written here
1297
+ * as descendants of `.bento-tile` (port-added anchor); `:global(...)` parts
1298
+ * become plain selectors, since this file is global CSS to begin with.
1299
+ */
1300
+
1301
+ /*
1302
+ * Colour plumbing. `--_accent` reads the public `--bento-accent` (written by
1303
+ * BentoGrid's `accent` prop, or set by a consumer on any ancestor via class)
1304
+ * with a soft periwinkle fallback. `--_core` is the white-hot centre of the
1305
+ * glow, mixed from the accent.
1306
+ */
1307
+ .bento-tile {
1308
+ --_accent: var(--bento-accent, #8e9cff);
1309
+ --_core: color-mix(in oklab, var(--_accent) 45%, white);
1310
+ --_frame-bg: var(--bento-frame-bg, #f6f6f7);
1311
+ --_frame-line: var(--bento-frame-line, rgba(0, 0, 0, 0.08));
1312
+ --_panel-bg: var(--bento-panel-bg, #fdfdfd);
1313
+ --_panel-line: var(--bento-panel-line, rgba(0, 0, 0, 0.07));
1314
+ --_panel-top: rgba(255, 255, 255, 0.95);
1315
+ --_panel-top-edge: rgba(255, 255, 255, 1);
1316
+ --_glow-peak: 0.3;
1317
+
1318
+ background: var(--_frame-bg);
1319
+ border: 1px solid var(--_frame-line);
1320
+ box-shadow:
1321
+ 0 1px 2px rgba(15, 15, 20, 0.04),
1322
+ 0 12px 32px -18px rgba(15, 15, 20, 0.14);
1323
+ transition: border-color 300ms cubic-bezier(0.4, 0, 0.2, 1); /* EASINGS.inout */
1324
+ }
1325
+
1326
+ .dark .bento-tile {
1327
+ --_frame-bg: var(--bento-frame-bg, #0f0f10);
1328
+ --_frame-line: var(--bento-frame-line, rgba(255, 255, 255, 0.08));
1329
+ --_panel-bg: var(--bento-panel-bg, #141416);
1330
+ --_panel-line: var(--bento-panel-line, rgba(255, 255, 255, 0.06));
1331
+ --_panel-top: rgba(255, 255, 255, 0.06);
1332
+ --_panel-top-edge: rgba(255, 255, 255, 0.08);
1333
+ --_glow-peak: 0.35;
1334
+ box-shadow:
1335
+ 0 1px 2px rgba(0, 0, 0, 0.4),
1336
+ 0 18px 40px -22px rgba(0, 0, 0, 0.7);
1337
+ }
1338
+
1339
+ /* Inner panel: its own hairline plus a soft top light. */
1340
+ .bento-tile .bento-panel {
1341
+ background: linear-gradient(180deg, var(--_panel-top), transparent 40%), var(--_panel-bg);
1342
+ border: 1px solid var(--_panel-line);
1343
+ box-shadow: inset 0 1px 0 var(--_panel-top-edge);
1344
+ }
1345
+
1346
+ /* The glow: a blurred accent radial parked below the panel's bottom edge,
1347
+ white-hot at its centre. Hover raises it 40% → 0 and fades it in. */
1348
+ .bento-tile .bento-glow {
1349
+ position: absolute;
1350
+ z-index: -1;
1351
+ left: -10%;
1352
+ right: -10%;
1353
+ bottom: -22%;
1354
+ height: 78%;
1355
+ pointer-events: none;
1356
+ background: radial-gradient(
1357
+ 65% 85% at 50% 100%,
1358
+ var(--_core) 0%,
1359
+ var(--_accent) 30%,
1360
+ color-mix(in oklab, var(--_accent) 40%, transparent) 55%,
1361
+ transparent 80%
1362
+ );
1363
+ filter: blur(24px);
1364
+ opacity: 0;
1365
+ transform: translate3d(0, 40%, 0);
1366
+ transition:
1367
+ opacity 500ms cubic-bezier(0.4, 0, 0.2, 1),
1368
+ transform 700ms cubic-bezier(0.16, 1, 0.3, 1); /* EASINGS.out */
1369
+ }
1370
+
1371
+ /* The bottom edge catches the light as the glow arrives. */
1372
+ .bento-tile .bento-edge {
1373
+ position: absolute;
1374
+ left: 14%;
1375
+ right: 14%;
1376
+ bottom: 0;
1377
+ height: 1px;
1378
+ pointer-events: none;
1379
+ background: linear-gradient(
1380
+ 90deg,
1381
+ transparent,
1382
+ var(--_accent) 25%,
1383
+ var(--_core) 50%,
1384
+ var(--_accent) 75%,
1385
+ transparent
1386
+ );
1387
+ opacity: 0;
1388
+ transition: opacity 500ms cubic-bezier(0.4, 0, 0.2, 1);
1389
+ }
1390
+
1391
+ .bento-tile:hover,
1392
+ .bento-tile:focus-within {
1393
+ --_frame-line: var(--bento-frame-line-hover, rgba(0, 0, 0, 0.12));
1394
+ }
1395
+ .dark .bento-tile:hover,
1396
+ .dark .bento-tile:focus-within {
1397
+ --_frame-line: var(--bento-frame-line-hover, rgba(255, 255, 255, 0.13));
1398
+ }
1399
+
1400
+ .bento-tile:hover .bento-glow,
1401
+ .bento-tile:focus-within .bento-glow {
1402
+ opacity: var(--_glow-peak);
1403
+ transform: translate3d(0, 0, 0);
1404
+ }
1405
+ .bento-tile:hover .bento-edge,
1406
+ .bento-tile:focus-within .bento-edge {
1407
+ opacity: 0.85;
1408
+ }
1409
+
1410
+ /*
1411
+ * Shared content parts, styled here so Item and Card stay identical.
1412
+ * They live in the tiles' own markup.
1413
+ */
1414
+
1415
+ /* Content lift. */
1416
+ .bento-tile .bento-lift {
1417
+ transition: transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
1418
+ }
1419
+
1420
+ /* The icon sits in a small inset tile: darker well, inset hairline. */
1421
+ .bento-tile .bento-icon {
1422
+ display: grid;
1423
+ place-items: center;
1424
+ flex: none;
1425
+ border-radius: 10px;
1426
+ color: #71717a;
1427
+ background: #f0f0f2;
1428
+ border: 1px solid rgba(0, 0, 0, 0.07);
1429
+ box-shadow:
1430
+ inset 0 1px 2px rgba(0, 0, 0, 0.06),
1431
+ 0 1px 0 rgba(255, 255, 255, 0.9);
1432
+ transition:
1433
+ color 300ms cubic-bezier(0.4, 0, 0.2, 1),
1434
+ border-color 300ms cubic-bezier(0.4, 0, 0.2, 1),
1435
+ background-color 300ms cubic-bezier(0.4, 0, 0.2, 1),
1436
+ box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1);
1437
+ }
1438
+ .dark .bento-tile .bento-icon {
1439
+ color: #8a8a93;
1440
+ background: #0b0b0c;
1441
+ border-color: rgba(255, 255, 255, 0.07);
1442
+ box-shadow:
1443
+ inset 0 1px 3px rgba(0, 0, 0, 0.6),
1444
+ 0 1px 0 rgba(255, 255, 255, 0.04);
1445
+ }
1446
+ .bento-tile:is(:hover, :focus-within) .bento-icon {
1447
+ color: #18181b;
1448
+ background: #fff;
1449
+ border-color: color-mix(in oklab, var(--_accent) 45%, rgba(0, 0, 0, 0.08));
1450
+ box-shadow:
1451
+ inset 0 1px 2px rgba(0, 0, 0, 0.04),
1452
+ 0 0 0 3px color-mix(in oklab, var(--_accent) 14%, transparent);
1453
+ }
1454
+ .dark .bento-tile:is(:hover, :focus-within) .bento-icon {
1455
+ color: #fafafa;
1456
+ background: #19191c;
1457
+ border-color: color-mix(in oklab, var(--_accent) 40%, rgba(255, 255, 255, 0.1));
1458
+ box-shadow:
1459
+ inset 0 1px 0 rgba(255, 255, 255, 0.06),
1460
+ 0 0 14px -4px color-mix(in oklab, var(--_accent) 55%, transparent);
1461
+ }
1462
+
1463
+ /* The CTA slides in from the left and fades up. */
1464
+ .bento-tile .bento-cta {
1465
+ opacity: 0;
1466
+ transform: translate3d(-8px, 0, 0);
1467
+ transition:
1468
+ opacity 400ms cubic-bezier(0.4, 0, 0.2, 1),
1469
+ transform 500ms cubic-bezier(0.16, 1, 0.3, 1),
1470
+ color 300ms cubic-bezier(0.4, 0, 0.2, 1);
1471
+ }
1472
+ .bento-tile:is(:hover, :focus-within) .bento-cta {
1473
+ opacity: 1;
1474
+ transform: translate3d(0, 0, 0);
1475
+ }
1476
+ /* No hover on touch screens: keep the CTA visible. */
1477
+ @media (hover: none) {
1478
+ .bento-tile .bento-cta {
1479
+ opacity: 1;
1480
+ transform: none;
1481
+ }
1482
+ }
1483
+
1484
+ @media (prefers-reduced-motion: no-preference) {
1485
+ .bento-tile:is(:hover, :focus-within) .bento-lift {
1486
+ transform: translate3d(0, -6px, 0);
1487
+ }
1488
+ }
1489
+
1490
+ /* Reduced motion: the glow still arrives, but only as a fade — it sits in
1491
+ its final place from the start. */
1492
+ @media (prefers-reduced-motion: reduce) {
1493
+ .bento-tile .bento-glow {
1494
+ transform: none;
1495
+ transition: opacity 300ms linear;
1496
+ }
1497
+ .bento-tile .bento-cta {
1498
+ transform: none;
1499
+ transition: opacity 300ms linear;
1500
+ }
1501
+ .bento-tile .bento-lift {
1502
+ transition: none;
1503
+ }
1504
+ }
781
1505
  /*
782
1506
  * `.fancy-bg-stars` is a PORT-ADDED anchor (PORTING.md, styling rule 2). The
783
1507
  * Svelte source has no root class: its compiler scoped `.stars-parallax` /
@@ -840,58 +1564,300 @@
840
1564
  .blur-reveal-wrapper.is-visible > :nth-child(3) {
841
1565
  transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 2);
842
1566
  }
843
- .blur-reveal-wrapper.is-visible > :nth-child(4) {
844
- transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 3);
1567
+ .blur-reveal-wrapper.is-visible > :nth-child(4) {
1568
+ transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 3);
1569
+ }
1570
+ .blur-reveal-wrapper.is-visible > :nth-child(5) {
1571
+ transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 4);
1572
+ }
1573
+ .blur-reveal-wrapper.is-visible > :nth-child(6) {
1574
+ transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 5);
1575
+ }
1576
+ .blur-reveal-wrapper.is-visible > :nth-child(7) {
1577
+ transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 6);
1578
+ }
1579
+ .blur-reveal-wrapper.is-visible > :nth-child(8) {
1580
+ transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 7);
1581
+ }
1582
+ .blur-reveal-wrapper.is-visible > :nth-child(9) {
1583
+ transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 8);
1584
+ }
1585
+ .blur-reveal-wrapper.is-visible > :nth-child(10) {
1586
+ transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 9);
1587
+ }
1588
+
1589
+ /* Hard mode: no blur/translate softening, opacity snaps in via a stepped timing function */
1590
+ .blur-reveal-wrapper.mode-hard > * {
1591
+ opacity: 0;
1592
+ filter: none;
1593
+ transform: none;
1594
+ transition: opacity var(--blur-reveal-duration, 1s) steps(2, jump-none);
1595
+ }
1596
+
1597
+ .blur-reveal-wrapper.mode-hard.is-visible > * {
1598
+ opacity: 1;
1599
+ }
1600
+
1601
+ /* Respect reduced motion */
1602
+ @media (prefers-reduced-motion: reduce) {
1603
+ .blur-reveal-wrapper > * {
1604
+ transition-duration: 0.1s !important;
1605
+ transition-delay: 0s !important;
1606
+ }
1607
+ }
1608
+ /*
1609
+ * Port note: the Svelte source's rules are compiler-scoped. Here every rule is
1610
+ * anchored on the component's own root class `.border-beam` so the `bb-*`
1611
+ * hooks never match outside the component; the dark-theme block reads
1612
+ * `.dark .border-beam` (the Svelte `:global(.dark)` selector).
1613
+ */
1614
+
1615
+ .border-beam {
1616
+ /* private plumbing: every value reads a public var with a fallback */
1617
+ --_from: var(--border-beam-color-from, #8ec5ff);
1618
+ --_to: var(--border-beam-color-to, #c084fc);
1619
+ --_bw: calc(var(--border-beam-border-width, 1.5) * 1px);
1620
+ --_size: calc(var(--border-beam-size, 200) * 1px);
1621
+ --_dur: var(--border-beam-duration, 9s);
1622
+ --_tail: var(--border-beam-tail, 0.25);
1623
+ --_glow: var(--border-beam-glow, 0.6);
1624
+ --_offset: var(--border-beam-offset, 0s);
1625
+ --_start: calc(var(--border-beam-anchor, 90) * 1%);
1626
+ --_park: var(--border-beam-park, 90%);
1627
+ --_dir: var(--border-beam-dir, 1);
1628
+ --_radius: var(--border-beam-radius, 12px);
1629
+ /* perimeter of the box, read through container units */
1630
+ --_perimeter: calc(2 * (100cqw + 100cqh));
1631
+
1632
+ /* light theme: inks a touch deeper, head warm rather than white, quieter bloom */
1633
+ --_ink-from: color-mix(in oklab, var(--_from) 76%, #0b0b0c);
1634
+ --_ink-to: color-mix(in oklab, var(--_to) 76%, #0b0b0c);
1635
+ --_hot: color-mix(in oklab, var(--_from) 80%, white);
1636
+ --_core: color-mix(in oklab, var(--_ink-from) 70%, white);
1637
+ --_rail: color-mix(in oklab, var(--_ink-from) 6%, transparent);
1638
+ --_bloom-max: 0.7;
1639
+ --_halo-max: 0.7;
1640
+ --_spill-max: 0.16;
1641
+
1642
+ container-type: size;
1643
+ isolation: isolate;
1644
+ }
1645
+
1646
+ .dark .border-beam {
1647
+ --_ink-from: var(--_from);
1648
+ --_ink-to: var(--_to);
1649
+ --_hot: color-mix(in oklab, var(--_from) 40%, white);
1650
+ --_core: white;
1651
+ --_rail: color-mix(in oklab, var(--_from) 7%, transparent);
1652
+ --_bloom-max: 1;
1653
+ --_halo-max: 0.85;
1654
+ --_spill-max: 0.24;
1655
+ }
1656
+
1657
+ .border-beam > *,
1658
+ .border-beam .bb-ring > *,
1659
+ .border-beam .bb-bloom > *,
1660
+ .border-beam .bb-halo > *,
1661
+ .border-beam .bb-spill-clip > * {
1662
+ position: absolute;
1663
+ pointer-events: none;
1664
+ }
1665
+
1666
+ .border-beam .bb-spill-clip,
1667
+ .border-beam .bb-bloom,
1668
+ .border-beam .bb-halo,
1669
+ .border-beam .bb-ring,
1670
+ .border-beam .bb-rail {
1671
+ inset: 0;
1672
+ border-radius: inherit;
1673
+ }
1674
+
1675
+ /* ---------- riders: everything that travels the border ---------- */
1676
+
1677
+ .border-beam .bb-ride {
1678
+ top: 0;
1679
+ left: 0;
1680
+ /* fallback path first, then the box itself with its real radius */
1681
+ offset-path: rect(0 auto auto 0 round var(--_radius));
1682
+ offset-path: border-box;
1683
+ offset-rotate: auto;
1684
+ offset-anchor: 50% 50%;
1685
+ /* rest pose (reduced motion): parked at the anchor, tail laid out behind */
1686
+ offset-distance: calc(var(--_park) - var(--_dir) * var(--_lag, 0) * 100%);
1687
+ --_lag: 0;
1688
+ }
1689
+
1690
+ .border-beam[data-direction="reverse"] .bb-ride {
1691
+ offset-rotate: reverse;
1692
+ }
1693
+
1694
+ .border-beam .bb-piece {
1695
+ --_lag: calc(var(--_tail) * var(--bb-t));
1696
+ /* pieces overlap twice over so the tail reads as one stroke */
1697
+ width: calc(var(--_perimeter) * var(--_tail) / var(--bb-count) * 2.2);
1698
+ }
1699
+
1700
+ /* ---------- the sharp comet on the hairline ---------- */
1701
+
1702
+ .border-beam .bb-ring {
1703
+ padding: var(--_bw);
1704
+ -webkit-mask:
1705
+ linear-gradient(#000 0 0) content-box,
1706
+ linear-gradient(#000 0 0);
1707
+ -webkit-mask-composite: xor;
1708
+ mask:
1709
+ linear-gradient(#000 0 0) content-box,
1710
+ linear-gradient(#000 0 0);
1711
+ mask-composite: exclude;
1712
+ }
1713
+
1714
+ .border-beam .bb-rail {
1715
+ position: absolute;
1716
+ background: var(--_rail);
1717
+ }
1718
+
1719
+ .border-beam .bb-reach {
1720
+ width: var(--_size);
1721
+ height: var(--_size);
1722
+ border-radius: 50%;
1723
+ background: radial-gradient(
1724
+ closest-side,
1725
+ color-mix(in oklab, var(--_hot) 70%, transparent),
1726
+ color-mix(in oklab, var(--_ink-from) 40%, transparent) 45%,
1727
+ transparent
1728
+ );
1729
+ opacity: 0.7;
1730
+ }
1731
+
1732
+ .border-beam .bb-tail-piece {
1733
+ height: calc(var(--_bw) * 2 + 4px);
1734
+ opacity: var(--bb-alpha);
1735
+ background: linear-gradient(
1736
+ 90deg,
1737
+ transparent,
1738
+ color-mix(in oklab, var(--_ink-from) calc((1 - var(--bb-t)) * 100%), var(--_ink-to)) 30%,
1739
+ color-mix(in oklab, var(--_ink-from) calc((1 - var(--bb-t)) * 100%), var(--_ink-to)) 70%,
1740
+ transparent
1741
+ );
1742
+ }
1743
+
1744
+ .border-beam .bb-head {
1745
+ width: 44px;
1746
+ height: calc(var(--_bw) * 2 + 4px);
1747
+ offset-anchor: 100% 50%;
1748
+ background: linear-gradient(
1749
+ 90deg,
1750
+ transparent,
1751
+ var(--_ink-from) 45%,
1752
+ var(--_hot) 85%,
1753
+ var(--_core)
1754
+ );
845
1755
  }
846
- .blur-reveal-wrapper.is-visible > :nth-child(5) {
847
- transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 4);
1756
+
1757
+ /* ---------- bloom: the comet again, thicker and blurred ---------- */
1758
+
1759
+ .border-beam .bb-bloom {
1760
+ filter: blur(11px);
1761
+ opacity: calc(var(--_glow) * var(--_bloom-max));
848
1762
  }
849
- .blur-reveal-wrapper.is-visible > :nth-child(6) {
850
- transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 5);
1763
+
1764
+ .border-beam .bb-bloom-piece {
1765
+ height: calc(var(--_bw) + 16px * var(--bb-thick));
1766
+ opacity: var(--bb-alpha);
1767
+ border-radius: 999px;
1768
+ background: color-mix(in oklab, var(--_ink-from) calc((1 - var(--bb-t)) * 100%), var(--_ink-to));
851
1769
  }
852
- .blur-reveal-wrapper.is-visible > :nth-child(7) {
853
- transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 6);
1770
+
1771
+ .border-beam .bb-bloom-head {
1772
+ width: 56px;
1773
+ height: 22px;
1774
+ offset-anchor: 85% 50%;
1775
+ border-radius: 50%;
1776
+ background: radial-gradient(closest-side, var(--_hot), var(--_ink-from) 60%, transparent);
854
1777
  }
855
- .blur-reveal-wrapper.is-visible > :nth-child(8) {
856
- transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 7);
1778
+
1779
+ /* ---------- halo: a thin, lightly blurred glow right on the line ---------- */
1780
+
1781
+ .border-beam .bb-halo {
1782
+ filter: blur(2.5px);
1783
+ opacity: calc(var(--_glow) * var(--_halo-max));
857
1784
  }
858
- .blur-reveal-wrapper.is-visible > :nth-child(9) {
859
- transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 8);
1785
+
1786
+ .border-beam .bb-halo-piece {
1787
+ height: calc(var(--_bw) + 5px * var(--bb-thick));
1788
+ opacity: var(--bb-alpha);
1789
+ border-radius: 999px;
1790
+ background: color-mix(in oklab, var(--_ink-from) calc((1 - var(--bb-t)) * 100%), var(--_ink-to));
860
1791
  }
861
- .blur-reveal-wrapper.is-visible > :nth-child(10) {
862
- transition-delay: calc(var(--blur-reveal-delay, 0.2s) * 9);
1792
+
1793
+ /* ---------- inner spill: a wide, faint pool clipped to the inside ---------- */
1794
+
1795
+ .border-beam .bb-spill-clip {
1796
+ overflow: hidden;
863
1797
  }
864
1798
 
865
- /* Hard mode: no blur/translate softening, opacity snaps in via a stepped timing function */
866
- .blur-reveal-wrapper.mode-hard > * {
867
- opacity: 0;
868
- filter: none;
869
- transform: none;
870
- transition: opacity var(--blur-reveal-duration, 1s) steps(2, jump-none);
1799
+ .border-beam .bb-spill {
1800
+ width: calc(var(--_size) * 1.3);
1801
+ height: calc(var(--_size) * 1.3);
1802
+ border-radius: 50%;
1803
+ background: radial-gradient(
1804
+ closest-side,
1805
+ color-mix(in oklab, var(--_ink-from) 70%, transparent),
1806
+ color-mix(in oklab, var(--_ink-to) 25%, transparent) 50%,
1807
+ transparent
1808
+ );
1809
+ opacity: calc(var(--_glow) * var(--_spill-max));
871
1810
  }
872
1811
 
873
- .blur-reveal-wrapper.mode-hard.is-visible > * {
874
- opacity: 1;
1812
+ /* ---------- the spark: a tiny white-hot point on the edge ---------- */
1813
+
1814
+ .border-beam .bb-spark {
1815
+ width: calc(var(--_bw) * 2 + 8px);
1816
+ height: calc(var(--_bw) * 2 + 8px);
1817
+ border-radius: 50%;
1818
+ background: radial-gradient(
1819
+ closest-side,
1820
+ var(--_core),
1821
+ var(--_hot) 30%,
1822
+ color-mix(in oklab, var(--_hot) 35%, transparent) 62%,
1823
+ transparent
1824
+ );
875
1825
  }
876
1826
 
877
- /* Respect reduced motion */
878
- @media (prefers-reduced-motion: reduce) {
879
- .blur-reveal-wrapper > * {
880
- transition-duration: 0.1s !important;
881
- transition-delay: 0s !important;
1827
+ /* ---------- motion ---------- */
1828
+
1829
+ @media (prefers-reduced-motion: no-preference) {
1830
+ .border-beam {
1831
+ /* arrival: the comet fades in over the entrance duration, up to the root's own
1832
+ opacity (no `to` keyframe, no forwards fill), so an opacity class still applies */
1833
+ animation: bb-ignite var(--ft-duration-entrance, 600ms)
1834
+ var(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) backwards;
1835
+ }
1836
+
1837
+ .border-beam .bb-ride {
1838
+ animation: bb-travel var(--_dur) linear infinite;
1839
+ /* a piece `lag` turns behind the head is started one lap early, minus its lag,
1840
+ so it is already running on the first frame */
1841
+ animation-delay: calc(var(--_offset) - (1 - var(--_lag)) * var(--_dur));
1842
+ }
1843
+
1844
+ .border-beam[data-direction="reverse"] .bb-ride {
1845
+ animation-direction: reverse;
882
1846
  }
883
- }
884
- .border-beam::after {
885
- content: "";
886
- animation: border-beam-animation var(--border-beam-duration) infinite linear;
887
1847
  }
888
1848
 
889
- @keyframes border-beam-animation {
890
- 0% {
891
- offset-distance: 0%;
1849
+ @keyframes bb-travel {
1850
+ from {
1851
+ offset-distance: var(--_start);
892
1852
  }
893
- 100% {
894
- offset-distance: 100%;
1853
+ to {
1854
+ offset-distance: calc(var(--_start) + 100%);
1855
+ }
1856
+ }
1857
+
1858
+ @keyframes bb-ignite {
1859
+ from {
1860
+ opacity: 0;
895
1861
  }
896
1862
  }
897
1863
  /*
@@ -1512,6 +2478,224 @@
1512
2478
  * the left. `transform: scale(…)` alone composes after `translate` and
1513
2479
  * scales the panel about its own centre without touching the centring.
1514
2480
  */
2481
+ /*
2482
+ * Compare: the seam as a blade of light.
2483
+ *
2484
+ * Mirrors the <style> block of the reference component. Every rule is
2485
+ * anchored on the root `.compare` class (the reference's own root class),
2486
+ * because plain CSS is global where the reference's rules were compiler-scoped.
2487
+ * Keyframe names, class names and custom properties are kept identical.
2488
+ */
2489
+
2490
+ @property --cmp-angle {
2491
+ syntax: "<angle>";
2492
+ inherits: false;
2493
+ initial-value: 0deg;
2494
+ }
2495
+
2496
+ .compare:focus-visible {
2497
+ outline: 2px solid var(--cmp-c2);
2498
+ outline-offset: 3px;
2499
+ }
2500
+
2501
+ /* the whole beam screens onto the pictures: light adds, never darkens */
2502
+ .compare .compare-beam {
2503
+ mix-blend-mode: screen;
2504
+ }
2505
+
2506
+ .compare .compare-beam > span {
2507
+ position: absolute;
2508
+ pointer-events: none;
2509
+ }
2510
+
2511
+ /* The light the beam spills on both sides: chromatic, brighter when active. */
2512
+ .compare .compare-beam__glow {
2513
+ top: 0;
2514
+ bottom: 0;
2515
+ left: -70px;
2516
+ width: 140px;
2517
+ background: linear-gradient(
2518
+ to right,
2519
+ transparent,
2520
+ color-mix(in srgb, var(--cmp-c1) 55%, transparent) 38%,
2521
+ var(--cmp-c2) 50%,
2522
+ color-mix(in srgb, var(--cmp-c3) 55%, transparent) 62%,
2523
+ transparent
2524
+ );
2525
+ -webkit-mask: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
2526
+ mask: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
2527
+ filter: blur(18px);
2528
+ opacity: calc(0.35 + var(--cmp-energy) * 0.55);
2529
+ }
2530
+
2531
+ /* The trail: stretched out behind the seam by its speed (the sign picks the side). */
2532
+ .compare .compare-beam__trail {
2533
+ top: 12%;
2534
+ bottom: 12%;
2535
+ left: 0;
2536
+ width: 100px;
2537
+ transform-origin: left center;
2538
+ transform: scaleX(calc(var(--cmp-trail) / -100));
2539
+ background: linear-gradient(
2540
+ to right,
2541
+ #fff,
2542
+ var(--cmp-c2) 18%,
2543
+ color-mix(in srgb, var(--cmp-c3) 50%, transparent) 55%,
2544
+ transparent
2545
+ );
2546
+ -webkit-mask: linear-gradient(to bottom, transparent, #000 30%, #000 70%, transparent);
2547
+ mask: linear-gradient(to bottom, transparent, #000 30%, #000 70%, transparent);
2548
+ filter: blur(6px);
2549
+ opacity: 0.75;
2550
+ }
2551
+
2552
+ /* A chromatic fringe either side of the core. */
2553
+ .compare .compare-beam__fringe {
2554
+ top: 0;
2555
+ bottom: 0;
2556
+ left: -4px;
2557
+ width: 9px;
2558
+ background: linear-gradient(
2559
+ to right,
2560
+ var(--cmp-c1),
2561
+ transparent 45%,
2562
+ transparent 55%,
2563
+ var(--cmp-c3)
2564
+ );
2565
+ -webkit-mask: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
2566
+ mask: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
2567
+ filter: blur(1.5px);
2568
+ opacity: calc(0.55 + var(--cmp-energy) * 0.45);
2569
+ }
2570
+
2571
+ /* The white-hot core. */
2572
+ .compare .compare-beam__core {
2573
+ top: 0;
2574
+ bottom: 0;
2575
+ left: -0.75px;
2576
+ width: 1.5px;
2577
+ background: linear-gradient(
2578
+ to bottom,
2579
+ transparent,
2580
+ color-mix(in srgb, var(--cmp-c2) 40%, #fff) 6%,
2581
+ #fff 30%,
2582
+ #fff 70%,
2583
+ color-mix(in srgb, var(--cmp-c2) 40%, #fff) 94%,
2584
+ transparent
2585
+ );
2586
+ box-shadow: 0 0 6px 0.5px color-mix(in srgb, var(--cmp-c2) 80%, transparent);
2587
+ }
2588
+
2589
+ /* Pulses of light running down the seam. */
2590
+ .compare .compare-beam__pulse {
2591
+ top: 0;
2592
+ left: -1.5px;
2593
+ width: 3px;
2594
+ height: 26%;
2595
+ border-radius: 999px;
2596
+ background: linear-gradient(to bottom, transparent, #fff 50%, transparent);
2597
+ filter: drop-shadow(0 0 4px var(--cmp-c2)) drop-shadow(0 0 10px var(--cmp-c1));
2598
+ opacity: 0;
2599
+ }
2600
+
2601
+ @media (prefers-reduced-motion: no-preference) {
2602
+ .compare .compare-beam__pulse {
2603
+ animation: cmp-pulse 3.2s cubic-bezier(0.45, 0, 0.55, 1) infinite;
2604
+ }
2605
+ .compare .compare-beam__pulse--late {
2606
+ animation-delay: -1.6s;
2607
+ animation-duration: 3.2s;
2608
+ }
2609
+ .compare[data-active] .compare-beam__pulse {
2610
+ animation-duration: 1.6s;
2611
+ }
2612
+ .compare[data-active] .compare-beam__pulse--late {
2613
+ animation-delay: -0.8s;
2614
+ }
2615
+ .compare .compare-handle__ring {
2616
+ animation: cmp-spin 4s linear infinite;
2617
+ }
2618
+ .compare[data-active] .compare-handle__ring {
2619
+ animation-duration: 1.4s;
2620
+ }
2621
+ }
2622
+
2623
+ @keyframes cmp-pulse {
2624
+ 0% {
2625
+ transform: translateY(-100%);
2626
+ opacity: 0;
2627
+ }
2628
+ 15% {
2629
+ opacity: 1;
2630
+ }
2631
+ 85% {
2632
+ opacity: 1;
2633
+ }
2634
+ 100% {
2635
+ transform: translateY(385%);
2636
+ opacity: 0;
2637
+ }
2638
+ }
2639
+
2640
+ /* The handle: a glass bead on the beam with a ring of light round it. */
2641
+ .compare .compare-handle {
2642
+ position: absolute;
2643
+ top: 50%;
2644
+ left: 0;
2645
+ z-index: 30;
2646
+ display: flex;
2647
+ align-items: center;
2648
+ justify-content: center;
2649
+ width: 34px;
2650
+ height: 34px;
2651
+ border-radius: 999px;
2652
+ transform: translate(-50%, -50%) scale(calc(1 + var(--cmp-energy) * 0.08));
2653
+ color: #fff;
2654
+ background: rgb(255 255 255 / 0.12);
2655
+ backdrop-filter: blur(10px) saturate(1.4);
2656
+ -webkit-backdrop-filter: blur(10px) saturate(1.4);
2657
+ box-shadow:
2658
+ inset 0 1px 0 rgb(255 255 255 / 0.35),
2659
+ inset 0 0 0 1px rgb(255 255 255 / 0.18),
2660
+ 0 4px 16px rgb(0 0 0 / 0.35),
2661
+ 0 0 calc(8px + var(--cmp-energy) * 18px) color-mix(in srgb, var(--cmp-c2) 70%, transparent);
2662
+ cursor: inherit;
2663
+ }
2664
+
2665
+ .compare .compare-handle svg {
2666
+ filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.45));
2667
+ }
2668
+
2669
+ .compare .compare-handle__ring {
2670
+ position: absolute;
2671
+ inset: -1.5px;
2672
+ border-radius: inherit;
2673
+ padding: 1.5px;
2674
+ background: conic-gradient(
2675
+ from var(--cmp-angle),
2676
+ transparent 0%,
2677
+ var(--cmp-c1) 18%,
2678
+ #fff 30%,
2679
+ var(--cmp-c3) 42%,
2680
+ transparent 58%,
2681
+ transparent 100%
2682
+ );
2683
+ -webkit-mask:
2684
+ linear-gradient(#000 0 0) content-box,
2685
+ linear-gradient(#000 0 0);
2686
+ -webkit-mask-composite: xor;
2687
+ mask:
2688
+ linear-gradient(#000 0 0) content-box,
2689
+ linear-gradient(#000 0 0);
2690
+ mask-composite: exclude;
2691
+ opacity: calc(0.45 + var(--cmp-energy) * 0.55);
2692
+ }
2693
+
2694
+ @keyframes cmp-spin {
2695
+ to {
2696
+ --cmp-angle: 360deg;
2697
+ }
2698
+ }
1515
2699
  /*
1516
2700
  * `.fancy-star-field` is a PORT-ADDED anchor (PORTING.md, styling rule 2).
1517
2701
  * The Svelte source has no root class of its own: its compiler scoped `.star`,
@@ -1815,6 +2999,207 @@
1815
2999
  transition: background-color 120ms ease;
1816
3000
  }
1817
3001
  }
3002
+ /*
3003
+ * ContainerScroll — a scroll-driven aperture.
3004
+ * Every rule is anchored on `.cs-root` (the Svelte source's own root class):
3005
+ * the Svelte compiler scoped these selectors, plain CSS is global.
3006
+ */
3007
+
3008
+ .cs-root {
3009
+ --_accent: var(--cs-accent, #8fb2ff);
3010
+ --_accent-2: var(--cs-accent-2, #c3b1ff);
3011
+ --_core: color-mix(in oklab, var(--_accent) 45%, white);
3012
+ --_radius: 22px;
3013
+ --_glow: 75%;
3014
+ --_frame: #f4f4f5;
3015
+ --_frame-line: rgba(0, 0, 0, 0.08);
3016
+ --_surface: #fafafa;
3017
+ --_surface-line: rgba(0, 0, 0, 0.07);
3018
+ --_shadow:
3019
+ 0 1px 2px rgba(0, 0, 0, 0.04), 0 12px 28px -12px rgba(0, 0, 0, 0.12),
3020
+ 0 40px 80px -40px rgba(0, 0, 0, 0.22);
3021
+ /* 1 → closed slit (48% top + 48% bottom), 0 → fully open */
3022
+ --_shut: calc(1 - var(--cs-open, 0));
3023
+ }
3024
+
3025
+ .dark .cs-root {
3026
+ --_glow: 60%;
3027
+ --_frame: #0f0f10;
3028
+ --_frame-line: rgba(255, 255, 255, 0.08);
3029
+ --_surface: #0b0b0c;
3030
+ --_surface-line: rgba(255, 255, 255, 0.07);
3031
+ --_shadow:
3032
+ 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 20px 50px -24px rgba(0, 0, 0, 0.9),
3033
+ 0 60px 120px -60px rgba(0, 0, 0, 0.8);
3034
+ }
3035
+
3036
+ /* Title: hands the stage over by going out of focus */
3037
+ .cs-root .cs-title {
3038
+ /* starts only once the aperture is a quarter open */
3039
+ --_fade: clamp(0, (var(--cs-progress, 0) - 0.25) / 0.75, 1);
3040
+ opacity: calc(1 - var(--_fade) * 0.85);
3041
+ filter: blur(calc(var(--_fade) * 6px));
3042
+ transform: scale(calc(1 - var(--_fade) * 0.02));
3043
+ transform-origin: 50% 100%;
3044
+ }
3045
+
3046
+ .cs-root .cs-stage {
3047
+ transform: translateY(calc(var(--_shut) * 24px));
3048
+ }
3049
+
3050
+ .cs-root .cs-shadow {
3051
+ border-radius: var(--_radius);
3052
+ box-shadow: var(--_shadow);
3053
+ /* follows the opening so it never outlines the still-hidden card */
3054
+ transform: scaleY(calc(1 - var(--_shut) * 0.96));
3055
+ opacity: calc(var(--cs-open, 0) * var(--cs-open, 0));
3056
+ }
3057
+
3058
+ .cs-root .cs-card {
3059
+ border-radius: var(--_radius);
3060
+ background: var(--_frame);
3061
+ border: 1px solid var(--_frame-line);
3062
+ clip-path: inset(calc(var(--_shut) * 48%) 0 calc(var(--_shut) * 48%) 0 round var(--_radius));
3063
+ }
3064
+
3065
+ .cs-root .cs-surface {
3066
+ border-radius: calc(var(--_radius) - 7px);
3067
+ background: var(--_surface);
3068
+ box-shadow: 0 0 0 1px var(--_surface-line);
3069
+ }
3070
+
3071
+ .cs-root .cs-content {
3072
+ transform: scale(calc(1 + var(--_shut) * 0.08));
3073
+ transform-origin: 50% 50%;
3074
+ }
3075
+
3076
+ /* ---- Light ---------------------------------------------------------- */
3077
+
3078
+ .cs-root .cs-seam {
3079
+ left: 4%;
3080
+ right: 4%;
3081
+ top: 50%;
3082
+ height: 0;
3083
+ opacity: calc(1 - var(--cs-progress, 0) * 3);
3084
+ }
3085
+
3086
+ .cs-root .cs-seam-line,
3087
+ .cs-root .cs-seam-glow,
3088
+ .cs-root .cs-seam-haze {
3089
+ position: absolute;
3090
+ left: 0;
3091
+ right: 0;
3092
+ top: 0;
3093
+ border-radius: 999px;
3094
+ }
3095
+
3096
+ .cs-root .cs-seam-line {
3097
+ height: 1px;
3098
+ transform: translateY(-50%);
3099
+ background: linear-gradient(
3100
+ 90deg,
3101
+ transparent,
3102
+ color-mix(in oklab, var(--_accent-2) 70%, transparent) 14%,
3103
+ var(--_accent) 34%,
3104
+ var(--_core) 50%,
3105
+ var(--_accent) 66%,
3106
+ color-mix(in oklab, var(--_accent-2) 70%, transparent) 86%,
3107
+ transparent
3108
+ );
3109
+ box-shadow: 0 0 6px 0 color-mix(in oklab, var(--_accent) 60%, transparent);
3110
+ }
3111
+
3112
+ .cs-root .cs-seam-glow {
3113
+ height: 28px;
3114
+ transform: translateY(-50%);
3115
+ background: radial-gradient(
3116
+ 50% 50% at 50% 50%,
3117
+ color-mix(in oklab, var(--_accent) var(--_glow), transparent),
3118
+ color-mix(in oklab, var(--_accent-2) 25%, transparent) 60%,
3119
+ transparent 100%
3120
+ );
3121
+ filter: blur(10px);
3122
+ }
3123
+
3124
+ /* a wide, faint horizon of light spilling past the slit */
3125
+ .cs-root .cs-seam-haze {
3126
+ left: -6%;
3127
+ right: -6%;
3128
+ height: 90px;
3129
+ transform: translateY(-50%);
3130
+ background: radial-gradient(
3131
+ 50% 50% at 50% 50%,
3132
+ color-mix(in oklab, var(--_accent) calc(var(--_glow) * 0.45), transparent),
3133
+ transparent 70%
3134
+ );
3135
+ filter: blur(18px);
3136
+ }
3137
+
3138
+ .cs-root .cs-veil {
3139
+ background: var(--_surface);
3140
+ opacity: calc(var(--_shut) * var(--_shut) * 0.75);
3141
+ }
3142
+
3143
+ .cs-root .cs-lip {
3144
+ left: 3%;
3145
+ right: 3%;
3146
+ height: 1px;
3147
+ background: linear-gradient(
3148
+ 90deg,
3149
+ transparent,
3150
+ color-mix(in oklab, var(--_accent-2) 55%, transparent) 20%,
3151
+ color-mix(in oklab, var(--_accent) 80%, white) 50%,
3152
+ color-mix(in oklab, var(--_accent-2) 55%, transparent) 80%,
3153
+ transparent
3154
+ );
3155
+ box-shadow: 0 0 10px 0 color-mix(in oklab, var(--_accent) 45%, transparent);
3156
+ opacity: var(--_shut);
3157
+ }
3158
+
3159
+ .cs-root .cs-lip-top {
3160
+ top: calc(var(--_shut) * 48%);
3161
+ }
3162
+
3163
+ .cs-root .cs-lip-bottom {
3164
+ bottom: calc(var(--_shut) * 48%);
3165
+ }
3166
+
3167
+ /* Light theme: a white core vanishes on a pale page, so deepen the seam */
3168
+ html:not(.dark) .cs-root {
3169
+ --_core: color-mix(in oklab, var(--_accent) 75%, white);
3170
+ }
3171
+
3172
+ /* Reduced motion: a still frame — card open, title sharp, no light */
3173
+ .cs-root[data-reduced-motion] {
3174
+ --_shut: 0;
3175
+ }
3176
+ .cs-root[data-reduced-motion] .cs-title {
3177
+ opacity: 1;
3178
+ filter: none;
3179
+ transform: none;
3180
+ }
3181
+ .cs-root[data-reduced-motion] .cs-seam,
3182
+ .cs-root[data-reduced-motion] .cs-lip {
3183
+ display: none;
3184
+ }
3185
+
3186
+ @media (prefers-reduced-motion: reduce) {
3187
+ .cs-root {
3188
+ --_shut: 0;
3189
+ }
3190
+ .cs-root .cs-title {
3191
+ opacity: 1;
3192
+ filter: none;
3193
+ transform: none;
3194
+ }
3195
+ .cs-root .cs-seam,
3196
+ .cs-root .cs-lip {
3197
+ display: none;
3198
+ }
3199
+ .cs-root .cs-shadow {
3200
+ opacity: 1;
3201
+ }
3202
+ }
1818
3203
  /* Keyframe names are part of the visual contract — keep them identical to
1819
3204
  the source. */
1820
3205
  @keyframes text-flip-in {
@@ -2394,6 +3779,243 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
2394
3779
  transition-duration: 0.1s !important;
2395
3780
  }
2396
3781
  }
3782
+ /*
3783
+ * Dock shelf. Mirrors the Svelte <style> block of Dock.svelte rule for rule.
3784
+ * The Svelte compiler scoped every selector; here each one is anchored on the
3785
+ * source's own root class `.dock-shelf`, so no generic name leaks into the page.
3786
+ */
3787
+
3788
+ /* Theme hooks. Every private var reads a public one first, so a consumer
3789
+ can recolour the light from a class without touching this file. */
3790
+ .dock-shelf {
3791
+ --_dock-accent: var(--dock-accent, var(--primary, #71717a));
3792
+ --_dock-spot-size: var(--dock-spotlight-size, 140px);
3793
+ --_dock-frame: var(--dock-frame-color, rgba(0, 0, 0, 0.05));
3794
+ --_dock-core: color-mix(in oklab, var(--_dock-accent) 45%, white);
3795
+ --_dock-ease: var(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
3796
+ outline: none;
3797
+ }
3798
+ .dark .dock-shelf {
3799
+ --_dock-frame: var(--dock-frame-color, rgba(255, 255, 255, 0.045));
3800
+ }
3801
+
3802
+ .dock-shelf:focus-visible {
3803
+ outline: 2px solid color-mix(in oklab, var(--_dock-accent) 55%, transparent);
3804
+ outline-offset: 3px;
3805
+ }
3806
+
3807
+ /* Decorative stack: clipped to the shelf, painted under the icons. The
3808
+ icons are not clipped — they are siblings, above this layer. */
3809
+ .dock-shelf .dock-deco {
3810
+ position: absolute;
3811
+ inset: -1px;
3812
+ z-index: 0;
3813
+ overflow: hidden;
3814
+ border-radius: inherit;
3815
+ pointer-events: none;
3816
+ }
3817
+
3818
+ /* The nested inner frame: a second hairline just inside the shelf edge,
3819
+ with a faint top-lit fill. */
3820
+ .dock-shelf .dock-frame {
3821
+ position: absolute;
3822
+ inset: 4px;
3823
+ border: 1px solid var(--_dock-frame);
3824
+ border-radius: calc(1rem - 4px);
3825
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.5), transparent 60%);
3826
+ }
3827
+ .dark .dock-shelf .dock-frame {
3828
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.025), transparent 60%);
3829
+ }
3830
+
3831
+ /* The spotlight: a blurred pool of accent light with a white-hot core,
3832
+ moved by transform only. Light theme lays a thin tint of the accent
3833
+ into the glass; dark theme screens light onto it. */
3834
+ .dock-shelf .dock-spot {
3835
+ position: absolute;
3836
+ left: 0;
3837
+ top: 0;
3838
+ width: var(--_dock-spot-size);
3839
+ height: var(--_dock-spot-size);
3840
+ border-radius: 50%;
3841
+ background: radial-gradient(
3842
+ closest-side,
3843
+ color-mix(in oklab, var(--_dock-accent) 20%, transparent) 0%,
3844
+ color-mix(in oklab, var(--_dock-accent) 8%, transparent) 45%,
3845
+ transparent 100%
3846
+ );
3847
+ filter: blur(10px);
3848
+ opacity: 0;
3849
+ transform: translate(calc(var(--dock-x) - 50%), calc(var(--dock-y) - 50%));
3850
+ will-change: transform, opacity;
3851
+ }
3852
+ .dark .dock-shelf .dock-spot {
3853
+ background: radial-gradient(
3854
+ closest-side,
3855
+ color-mix(in oklab, var(--_dock-core) 50%, transparent) 0%,
3856
+ color-mix(in oklab, var(--_dock-accent) 20%, transparent) 45%,
3857
+ transparent 100%
3858
+ );
3859
+ mix-blend-mode: screen;
3860
+ }
3861
+
3862
+ /* The lit rim: a 1px ring (padding + mask) holding one blurred blob that
3863
+ rides the same transform, so the shelf edge brightens only near the
3864
+ pointer. */
3865
+ .dock-shelf .dock-rim {
3866
+ position: absolute;
3867
+ inset: 0;
3868
+ padding: 1px;
3869
+ border-radius: inherit;
3870
+ -webkit-mask:
3871
+ linear-gradient(#000 0 0) content-box,
3872
+ linear-gradient(#000 0 0);
3873
+ -webkit-mask-composite: xor;
3874
+ mask:
3875
+ linear-gradient(#000 0 0) content-box exclude,
3876
+ linear-gradient(#000 0 0);
3877
+ opacity: 0;
3878
+ }
3879
+ .dock-shelf .dock-rim-light {
3880
+ position: absolute;
3881
+ left: 0;
3882
+ top: 0;
3883
+ width: 220px;
3884
+ height: 96px;
3885
+ border-radius: 50%;
3886
+ background: radial-gradient(
3887
+ closest-side,
3888
+ color-mix(in oklab, var(--_dock-accent) 70%, transparent),
3889
+ transparent
3890
+ );
3891
+ transform: translate(calc(var(--dock-x) - 50%), calc(var(--dock-y) - 50%));
3892
+ }
3893
+ .dark .dock-shelf .dock-rim-light {
3894
+ background: radial-gradient(closest-side, var(--_dock-core), transparent);
3895
+ }
3896
+
3897
+ .dock-shelf[data-hover] .dock-spot {
3898
+ opacity: 1;
3899
+ }
3900
+ .dock-shelf[data-hover] .dock-rim {
3901
+ opacity: 0.9;
3902
+ }
3903
+
3904
+ @media (prefers-reduced-motion: no-preference) {
3905
+ .dock-shelf .dock-spot {
3906
+ transition:
3907
+ transform 140ms var(--_dock-ease),
3908
+ opacity 300ms var(--_dock-ease);
3909
+ }
3910
+ .dock-shelf .dock-rim-light {
3911
+ transition: transform 140ms var(--_dock-ease);
3912
+ }
3913
+ .dock-shelf .dock-rim {
3914
+ transition: opacity 300ms var(--_dock-ease);
3915
+ }
3916
+ }
3917
+
3918
+ /* Reduced motion: the light holds still, dim and centred, whether or not a
3919
+ pointer is over the dock — the same composition as a resting frame. */
3920
+ @media (prefers-reduced-motion: reduce) {
3921
+ .dock-shelf .dock-spot,
3922
+ .dock-shelf[data-hover] .dock-spot {
3923
+ left: 50%;
3924
+ top: 50%;
3925
+ transform: translate(-50%, -50%);
3926
+ opacity: 0.55;
3927
+ }
3928
+ .dock-shelf .dock-rim,
3929
+ .dock-shelf[data-hover] .dock-rim {
3930
+ opacity: 0;
3931
+ }
3932
+ }
3933
+
3934
+ /* Nothing can hover: no light to follow. */
3935
+ @media (any-hover: none) {
3936
+ .dock-shelf .dock-spot,
3937
+ .dock-shelf .dock-rim {
3938
+ display: none;
3939
+ }
3940
+ }
3941
+ /*
3942
+ * Dock icon. Mirrors the Svelte <style> block of DockIcon.svelte rule for
3943
+ * rule, anchored on the source's own root class `.dock-icon`.
3944
+ */
3945
+ .dock-icon {
3946
+ --_dock-accent: var(--dock-accent, var(--primary, #71717a));
3947
+ --_dock-ease: var(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
3948
+ }
3949
+
3950
+ /* Indicator dot: below the icon on a horizontal dock, beside it on a
3951
+ vertical one. Hidden until the icon is the one under the pointer. */
3952
+ .dock-icon::before {
3953
+ content: "";
3954
+ position: absolute;
3955
+ left: 50%;
3956
+ top: calc(100% + 4px);
3957
+ width: 4px;
3958
+ height: 4px;
3959
+ margin-left: -2px;
3960
+ border-radius: 9999px;
3961
+ background: var(--_dock-accent);
3962
+ opacity: 0;
3963
+ pointer-events: none;
3964
+ }
3965
+ .dark .dock-icon::before {
3966
+ background: color-mix(in oklab, var(--_dock-accent) 45%, white);
3967
+ box-shadow: 0 0 6px 1px color-mix(in oklab, var(--_dock-accent) 55%, transparent);
3968
+ }
3969
+ .dock-icon[data-orientation="vertical"]::before {
3970
+ left: auto;
3971
+ right: calc(100% + 4px);
3972
+ top: 50%;
3973
+ margin-left: 0;
3974
+ margin-top: -2px;
3975
+ }
3976
+ .dock-icon[data-dock-active]::before {
3977
+ opacity: 1;
3978
+ }
3979
+
3980
+ /* Floor reflection: a soft ellipse under the icon — a contact shadow on
3981
+ the light shelf, a faint glow on the dark one. Sized in percent, so it
3982
+ grows with the magnified icon for free. */
3983
+ .dock-icon[data-reflection]::after {
3984
+ content: "";
3985
+ position: absolute;
3986
+ left: 14%;
3987
+ right: 14%;
3988
+ top: calc(100% - 4px);
3989
+ height: 22%;
3990
+ border-radius: 50%;
3991
+ background: radial-gradient(closest-side, rgba(0, 0, 0, 0.22), transparent);
3992
+ filter: blur(3px);
3993
+ opacity: 0.7;
3994
+ z-index: -1;
3995
+ pointer-events: none;
3996
+ }
3997
+ .dark .dock-icon[data-reflection]::after {
3998
+ background: radial-gradient(
3999
+ closest-side,
4000
+ color-mix(in oklab, var(--_dock-accent) 26%, transparent),
4001
+ transparent
4002
+ );
4003
+ }
4004
+ .dock-icon[data-reflection][data-dock-active]::after {
4005
+ opacity: 1;
4006
+ }
4007
+
4008
+ @media (prefers-reduced-motion: no-preference) {
4009
+ .dock-icon {
4010
+ transition:
4011
+ width 180ms var(--_dock-ease),
4012
+ height 180ms var(--_dock-ease);
4013
+ }
4014
+ .dock-icon::before,
4015
+ .dock-icon[data-reflection]::after {
4016
+ transition: opacity 180ms var(--_dock-ease);
4017
+ }
4018
+ }
2397
4019
  /*
2398
4020
  * The `<style>` block of the source Drawer, moved to a colocated stylesheet
2399
4021
  * (PORTING.md §Styling rule 2). The compiler scoped that block; plain CSS is
@@ -2689,6 +4311,156 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
2689
4311
  opacity: 0.5;
2690
4312
  cursor: not-allowed;
2691
4313
  }
4314
+ /*
4315
+ * FlipCard — ported verbatim from the Svelte <style> block. Every rule is
4316
+ * anchored on the root class `.ft-flip-card`, which the Svelte source already
4317
+ * names, so nothing leaks once the compiler-scoped rules become global.
4318
+ */
4319
+
4320
+ /* The one number everything turns on: the card's rotation. Registered so
4321
+ it can transition, and inherited so the faces can light themselves
4322
+ from it. */
4323
+ @property --fc-angle {
4324
+ syntax: "<angle>";
4325
+ inherits: true;
4326
+ initial-value: 0deg;
4327
+ }
4328
+
4329
+ .ft-flip-card {
4330
+ --fc-angle: var(--fc-target);
4331
+ outline: none;
4332
+ -webkit-tap-highlight-color: transparent;
4333
+ }
4334
+
4335
+ .ft-flip-card:focus-visible .ft-flip-card__inner {
4336
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-ring, #888) 60%, transparent);
4337
+ }
4338
+
4339
+ .ft-flip-card[data-trigger="click"] {
4340
+ cursor: pointer;
4341
+ }
4342
+
4343
+ .ft-flip-card__lift,
4344
+ .ft-flip-card__inner {
4345
+ position: relative;
4346
+ }
4347
+
4348
+ .ft-flip-card[data-axis="y"] .ft-flip-card__inner {
4349
+ transform: rotateY(var(--fc-angle));
4350
+ }
4351
+
4352
+ .ft-flip-card[data-axis="x"] .ft-flip-card__inner {
4353
+ transform: rotateX(var(--fc-angle));
4354
+ }
4355
+
4356
+ .ft-flip-card[data-axis="y"] .ft-flip-card__back {
4357
+ transform: rotateY(180deg);
4358
+ }
4359
+
4360
+ .ft-flip-card[data-axis="x"] .ft-flip-card__back {
4361
+ transform: rotateX(180deg);
4362
+ }
4363
+
4364
+ /* The shadow on the "table" under the card. */
4365
+ .ft-flip-card__shadow {
4366
+ position: absolute;
4367
+ inset: 8% 6% -4%;
4368
+ border-radius: 1.25rem;
4369
+ background: rgb(0 0 0 / 0.9);
4370
+ opacity: 0.35;
4371
+ transform: translateY(10px) scale(0.92);
4372
+ filter: blur(14px);
4373
+ pointer-events: none;
4374
+ }
4375
+
4376
+ /* ---- light: off unless the browser can compute it ---------------------- */
4377
+
4378
+ .ft-flip-card__light {
4379
+ display: none;
4380
+ }
4381
+
4382
+ @supports (opacity: calc(sin(30deg))) {
4383
+ .ft-flip-card__face {
4384
+ /* the face's own turn: the back starts half a turn round */
4385
+ --fc-face: var(--fc-angle);
4386
+ /* |sin| — 0 facing you, 1 edge-on */
4387
+ --fc-edge: max(sin(var(--fc-face)), -1 * sin(var(--fc-face)));
4388
+ }
4389
+
4390
+ .ft-flip-card__back {
4391
+ --fc-face: calc(var(--fc-angle) + 180deg);
4392
+ }
4393
+
4394
+ .ft-flip-card__light {
4395
+ display: block;
4396
+ position: absolute;
4397
+ inset: 0;
4398
+ pointer-events: none;
4399
+ border-radius: inherit;
4400
+ /* a sheen that sweeps across as the face turns, over a shade that
4401
+ deepens as the face goes edge-on */
4402
+ background:
4403
+ linear-gradient(
4404
+ var(--fc-sheen-angle, 105deg),
4405
+ transparent 38%,
4406
+ rgb(255 255 255 / 0.55) 50%,
4407
+ transparent 62%
4408
+ ),
4409
+ rgb(0 0 0 / calc(var(--fc-edge) * 0.55));
4410
+ background-size:
4411
+ 300% 300%,
4412
+ 100% 100%;
4413
+ background-repeat: no-repeat;
4414
+ background-position:
4415
+ calc(50% - sin(var(--fc-face)) * 45%) calc(50% - sin(var(--fc-face)) * 45%),
4416
+ 0 0;
4417
+ }
4418
+
4419
+ /* the sheen is brightest part-way round, never facing you or edge-on */
4420
+ .ft-flip-card__light {
4421
+ opacity: calc(min(1, var(--fc-edge) * 2.4));
4422
+ }
4423
+
4424
+ .ft-flip-card[data-axis="x"] .ft-flip-card__light {
4425
+ --fc-sheen-angle: 15deg;
4426
+ }
4427
+ }
4428
+
4429
+ /* ---- motion ---------------------------------------------------------------- */
4430
+
4431
+ @media (prefers-reduced-motion: no-preference) {
4432
+ .ft-flip-card {
4433
+ /* a slight overshoot, so the card lands rather than stops */
4434
+ transition: --fc-angle var(--fc-duration) cubic-bezier(0.34, 1.25, 0.64, 1);
4435
+ }
4436
+ }
4437
+
4438
+ /* Reduced motion: no turning at all — the faces cross-fade in place. */
4439
+ @media (prefers-reduced-motion: reduce) {
4440
+ /* as specific as the axis rules above, and later, so it wins */
4441
+ .ft-flip-card[data-axis] .ft-flip-card__inner,
4442
+ .ft-flip-card[data-axis] .ft-flip-card__back {
4443
+ transform: none;
4444
+ }
4445
+
4446
+ .ft-flip-card__face {
4447
+ transition: opacity 0.2s ease;
4448
+ }
4449
+
4450
+ .ft-flip-card__front,
4451
+ .ft-flip-card[data-flipped] .ft-flip-card__back {
4452
+ opacity: 1;
4453
+ }
4454
+
4455
+ .ft-flip-card__back,
4456
+ .ft-flip-card[data-flipped] .ft-flip-card__front {
4457
+ opacity: 0;
4458
+ }
4459
+
4460
+ .ft-flip-card__light {
4461
+ display: none;
4462
+ }
4463
+ }
2692
4464
  /* Keyframe names are part of the visual contract (referenced by inline
2693
4465
  style attributes) — keep them identical to the source. */
2694
4466
  @keyframes flipFadeInWord {
@@ -2990,56 +4762,222 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
2990
4762
  var(--bg-x) var(--bg-y) / 300% no-repeat;
2991
4763
  background-blend-mode: hue, hue, hue, overlay;
2992
4764
  }
2993
- @keyframes glow {
2994
- 0% {
2995
- background-position: 0% 0%;
4765
+ @property --gb-a1 {
4766
+ syntax: "<angle>";
4767
+ inherits: true;
4768
+ initial-value: 0deg;
4769
+ }
4770
+
4771
+ @property --gb-a2 {
4772
+ syntax: "<angle>";
4773
+ inherits: true;
4774
+ initial-value: 120deg;
4775
+ }
4776
+
4777
+ @property --gb-a3 {
4778
+ syntax: "<angle>";
4779
+ inherits: true;
4780
+ initial-value: 0deg;
4781
+ }
4782
+
4783
+ /* Light pages get the deeper palette; `.dark` switches to the bright one. */
4784
+ .glow-border {
4785
+ --gb-metal: var(--gb-metal-light);
4786
+ --gb-glint: var(--gb-glint-light);
4787
+ /* on a light page a coloured halo reads as fog: keep it faint */
4788
+ --gb-halo-opacity: 0.35;
4789
+ }
4790
+
4791
+ .dark .glow-border {
4792
+ --gb-metal: var(--gb-metal-dark);
4793
+ --gb-glint: var(--gb-glint-dark);
4794
+ --gb-halo-opacity: 0.7;
4795
+ }
4796
+
4797
+ .glow-border > span,
4798
+ .glow-border__halo > span {
4799
+ position: absolute;
4800
+ border-radius: inherit;
4801
+ pointer-events: none;
4802
+ }
4803
+
4804
+ /* keep only the padding ring: the element minus its content box */
4805
+ .glow-border__metal,
4806
+ .glow-border__glint,
4807
+ .glow-border__halo > span {
4808
+ -webkit-mask:
4809
+ linear-gradient(#000 0 0) content-box,
4810
+ linear-gradient(#000 0 0);
4811
+ -webkit-mask-composite: xor;
4812
+ mask:
4813
+ linear-gradient(#000 0 0) content-box,
4814
+ linear-gradient(#000 0 0);
4815
+ mask-composite: exclude;
4816
+ }
4817
+
4818
+ /* Liquid metal: two fields, centred apart and turning against each other. */
4819
+ .glow-border__metal {
4820
+ inset: 0;
4821
+ padding: var(--glow-border-width);
4822
+ background: var(--gb-metal);
4823
+ background-blend-mode: soft-light;
4824
+ }
4825
+
4826
+ /* The glint riding the ring. */
4827
+ .glow-border__glint {
4828
+ inset: 0;
4829
+ padding: var(--glow-border-width);
4830
+ background: var(--gb-glint);
4831
+ mix-blend-mode: screen;
4832
+ opacity: var(--glow-strength);
4833
+ }
4834
+
4835
+ /* The light it spills on both sides of the edge, blurred after masking. */
4836
+ .glow-border__halo {
4837
+ inset: 0;
4838
+ filter: blur(6px);
4839
+ opacity: calc(var(--glow-strength) * var(--gb-halo-opacity));
4840
+ }
4841
+
4842
+ .glow-border__halo > span {
4843
+ inset: -5px;
4844
+ padding: calc(var(--glow-border-width) + 9px);
4845
+ border-radius: calc(var(--glow-border-radius) + 5px);
4846
+ background: var(--gb-glint);
4847
+ }
4848
+
4849
+ .glow-border.animate-glow {
4850
+ animation:
4851
+ gb-flow-1 var(--glow-duration) linear infinite,
4852
+ gb-flow-2 calc(var(--glow-duration) * 1.7) linear infinite reverse,
4853
+ gb-glint calc(var(--glow-duration) * 0.4) linear infinite;
4854
+ }
4855
+
4856
+ @keyframes gb-flow-1 {
4857
+ to {
4858
+ --gb-a1: 360deg;
2996
4859
  }
2997
- 50% {
2998
- background-position: 100% 100%;
4860
+ }
4861
+
4862
+ @keyframes gb-flow-2 {
4863
+ from {
4864
+ --gb-a2: 120deg;
2999
4865
  }
3000
- 100% {
3001
- background-position: 0% 0%;
4866
+ to {
4867
+ --gb-a2: 480deg;
3002
4868
  }
3003
4869
  }
3004
4870
 
3005
- .animate-glow {
3006
- animation: glow var(--glow-duration) linear infinite;
4871
+ @keyframes gb-glint {
4872
+ to {
4873
+ --gb-a3: 360deg;
4874
+ }
3007
4875
  }
3008
4876
 
3009
4877
  @media (prefers-reduced-motion: reduce) {
3010
- .animate-glow {
4878
+ .glow-border.animate-glow {
3011
4879
  animation: none;
4880
+ --gb-a3: 210deg;
3012
4881
  }
3013
4882
  }
4883
+ @property --gb-angle {
4884
+ syntax: "<angle>";
4885
+ inherits: true;
4886
+ initial-value: 0deg;
4887
+ }
4888
+
3014
4889
  .gradient-button {
3015
- padding: var(--gb-border-width);
3016
4890
  border-radius: var(--gb-border-radius);
3017
4891
  isolation: isolate;
4892
+ background: color-mix(in srgb, var(--gb-bg-color), #fff 8%);
4893
+ animation: gb-spin var(--gb-duration) linear infinite;
4894
+ transition: transform 0.2s ease;
4895
+ }
4896
+
4897
+ .gradient-button:active {
4898
+ transform: scale(0.98);
4899
+ }
4900
+
4901
+ .gradient-button:disabled {
4902
+ cursor: not-allowed;
4903
+ opacity: 0.5;
4904
+ animation-play-state: paused;
4905
+ }
4906
+
4907
+ /* Anchored on the root class: the Svelte compiler scoped these rules, plain CSS
4908
+ would leak them into every consumer element sharing the class name. */
4909
+ .gradient-button .gradient-content {
4910
+ position: relative;
4911
+ margin: var(--gb-border-width);
4912
+ border-radius: calc(var(--gb-border-radius) - var(--gb-border-width));
4913
+ background:
4914
+ linear-gradient(to bottom, rgb(255 255 255 / 0.06), transparent 45%), var(--gb-bg-color);
4915
+ }
4916
+
4917
+ .gradient-button .gradient-border,
4918
+ .gradient-button .gradient-glow,
4919
+ .gradient-button .gradient-glow > span {
4920
+ position: absolute;
4921
+ pointer-events: none;
4922
+ border-radius: inherit;
4923
+ }
4924
+
4925
+ /* keep only the padding ring: the element minus its content box */
4926
+ .gradient-button .gradient-border,
4927
+ .gradient-button .gradient-glow > span {
4928
+ background: var(--gb-beam);
4929
+ -webkit-mask:
4930
+ linear-gradient(#000 0 0) content-box,
4931
+ linear-gradient(#000 0 0);
4932
+ -webkit-mask-composite: xor;
4933
+ mask:
4934
+ linear-gradient(#000 0 0) content-box,
4935
+ linear-gradient(#000 0 0);
4936
+ mask-composite: exclude;
4937
+ }
4938
+
4939
+ .gradient-button .gradient-border {
4940
+ inset: 0;
4941
+ z-index: 2;
4942
+ padding: var(--gb-border-width);
4943
+ }
4944
+
4945
+ /* A wider band inside the border, blurred by its parent AFTER masking so
4946
+ it falls off softly into the face. Screen: it lights, never darkens. */
4947
+ .gradient-button .gradient-glow {
4948
+ inset: 0;
4949
+ z-index: 1;
4950
+ filter: blur(calc(var(--gb-blur) * 3));
4951
+ mix-blend-mode: screen;
4952
+ opacity: 0.45;
4953
+ transition: opacity 0.3s ease;
4954
+ }
4955
+
4956
+ .gradient-button .gradient-glow > span {
4957
+ inset: 0;
4958
+ padding: calc(var(--gb-blur) * 1.75);
3018
4959
  }
3019
4960
 
3020
- /* Anchored on the root class: the Svelte compiler scoped these rules, plain CSS
3021
- would leak them into every consumer element sharing the class name. */
3022
- .gradient-button .gradient-border {
3023
- content: "";
3024
- position: absolute;
3025
- inset: -200%;
3026
- z-index: -1;
3027
- background: conic-gradient(var(--gb-colors));
3028
- animation: rotate-gradient var(--gb-duration) linear infinite;
3029
- filter: blur(var(--gb-blur));
4961
+ .gradient-button:hover .gradient-glow,
4962
+ .gradient-button:focus-visible .gradient-glow {
4963
+ opacity: 0.8;
3030
4964
  }
3031
4965
 
3032
- .gradient-button .gradient-content {
3033
- border-radius: var(--gb-border-radius);
3034
- background-color: var(--gb-bg-color);
4966
+ .gradient-button:focus-visible {
4967
+ outline: 2px solid rgb(255 255 255 / 0.5);
4968
+ outline-offset: 2px;
3035
4969
  }
3036
4970
 
3037
- @keyframes rotate-gradient {
3038
- 0% {
3039
- transform: rotate(0deg);
4971
+ @keyframes gb-spin {
4972
+ to {
4973
+ --gb-angle: 360deg;
3040
4974
  }
3041
- 100% {
3042
- transform: rotate(360deg);
4975
+ }
4976
+
4977
+ @media (prefers-reduced-motion: reduce) {
4978
+ .gradient-button {
4979
+ animation: none;
4980
+ --gb-angle: 135deg;
3043
4981
  }
3044
4982
  }
3045
4983
  /* Keyframes referenced by the inline `animation` shorthand on each
@@ -3315,6 +5253,149 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
3315
5253
  .interactive-grid-pattern .interactive-grid-square:not(:hover) {
3316
5254
  transition-duration: 1000ms;
3317
5255
  }
5256
+ .ihb {
5257
+ /* Private copies: read the public variables (set from `className`, which a
5258
+ rule here would otherwise outrank), else the theme. */
5259
+ --_ihb-fill: var(--ihb-fill, var(--primary, var(--color-primary, currentColor)));
5260
+ --_ihb-fill-foreground: var(
5261
+ --ihb-fill-foreground,
5262
+ var(--primary-foreground, var(--color-primary-foreground, #fff))
5263
+ );
5264
+ /* where the dot sits, vertically centred */
5265
+ --ihb-dot-x: 1.75rem;
5266
+ --ihb-ease: cubic-bezier(0.22, 1, 0.36, 1);
5267
+ }
5268
+
5269
+ .ihb-fill {
5270
+ position: absolute;
5271
+ inset: 0;
5272
+ z-index: -1;
5273
+ background: var(--_ihb-fill);
5274
+ clip-path: circle(4px at var(--ihb-dot-x) 50%);
5275
+ }
5276
+
5277
+ .ihb-rest,
5278
+ .ihb-hover {
5279
+ display: flex;
5280
+ align-items: center;
5281
+ justify-content: center;
5282
+ gap: 0.5rem;
5283
+ }
5284
+
5285
+ .ihb-dot-space {
5286
+ width: 8px;
5287
+ flex: none;
5288
+ }
5289
+
5290
+ .ihb-hover {
5291
+ position: absolute;
5292
+ inset: 0;
5293
+ color: var(--_ihb-fill-foreground);
5294
+ opacity: 0;
5295
+ transform: translateY(70%);
5296
+ }
5297
+
5298
+ .ihb-arrow {
5299
+ flex: none;
5300
+ opacity: 0;
5301
+ transform: translateX(-6px);
5302
+ }
5303
+
5304
+ /* ---- hover / keyboard focus ---------------------------------------------- */
5305
+
5306
+ .ihb:hover .ihb-fill,
5307
+ .ihb:focus-visible .ihb-fill {
5308
+ clip-path: circle(150% at var(--ihb-dot-x) 50%);
5309
+ }
5310
+
5311
+ .ihb:hover .ihb-rest,
5312
+ .ihb:focus-visible .ihb-rest {
5313
+ opacity: 0;
5314
+ transform: translateY(-70%);
5315
+ filter: blur(4px);
5316
+ }
5317
+
5318
+ .ihb:hover .ihb-hover,
5319
+ .ihb:focus-visible .ihb-hover {
5320
+ opacity: 1;
5321
+ transform: none;
5322
+ }
5323
+
5324
+ .ihb:hover .ihb-arrow,
5325
+ .ihb:focus-visible .ihb-arrow {
5326
+ opacity: 1;
5327
+ transform: none;
5328
+ }
5329
+
5330
+ .ihb:focus-visible {
5331
+ outline: 2px solid var(--_ihb-fill);
5332
+ outline-offset: 2px;
5333
+ }
5334
+
5335
+ .ihb:disabled {
5336
+ cursor: not-allowed;
5337
+ opacity: 0.5;
5338
+ }
5339
+
5340
+ .ihb:disabled .ihb-fill {
5341
+ clip-path: circle(4px at var(--ihb-dot-x) 50%);
5342
+ }
5343
+
5344
+ .ihb:disabled .ihb-rest {
5345
+ opacity: 1;
5346
+ transform: none;
5347
+ filter: none;
5348
+ }
5349
+
5350
+ .ihb:disabled .ihb-hover {
5351
+ opacity: 0;
5352
+ }
5353
+
5354
+ /* ---- motion: only when the visitor has not asked for less ----------------- */
5355
+
5356
+ @media (prefers-reduced-motion: no-preference) {
5357
+ .ihb {
5358
+ transition: transform 0.2s var(--ihb-ease);
5359
+ }
5360
+
5361
+ .ihb:active {
5362
+ transform: scale(0.97);
5363
+ }
5364
+
5365
+ .ihb-fill {
5366
+ transition: clip-path 0.6s var(--ihb-ease);
5367
+ }
5368
+
5369
+ .ihb-rest {
5370
+ transition:
5371
+ opacity 0.3s var(--ihb-ease),
5372
+ transform 0.5s var(--ihb-ease),
5373
+ filter 0.5s var(--ihb-ease);
5374
+ }
5375
+
5376
+ .ihb-hover {
5377
+ transition:
5378
+ opacity 0.3s var(--ihb-ease),
5379
+ transform 0.5s var(--ihb-ease);
5380
+ }
5381
+
5382
+ .ihb-arrow {
5383
+ transition:
5384
+ opacity 0.3s var(--ihb-ease),
5385
+ transform 0.5s var(--ihb-ease);
5386
+ }
5387
+
5388
+ /* On the way in, stagger: label a beat after the fill, arrow after it. */
5389
+ .ihb:hover .ihb-hover,
5390
+ .ihb:focus-visible .ihb-hover {
5391
+ transition-delay: 60ms;
5392
+ }
5393
+
5394
+ .ihb:hover .ihb-arrow,
5395
+ .ihb:focus-visible .ihb-arrow {
5396
+ transition-delay: 160ms;
5397
+ }
5398
+ }
3318
5399
  @keyframes letterPullUp {
3319
5400
  0% {
3320
5401
  opacity: 0;
@@ -4093,15 +6174,61 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
4093
6174
  transform: none;
4094
6175
  }
4095
6176
  }
4096
- /* Anchored on the root class (added in the port — the Svelte compiler scoped
6177
+ /* Anchored on the source's root class `.marquee` (the Svelte compiler scoped
4097
6178
  these rules; as plain CSS they would leak into any consumer element named
4098
6179
  .animate-marquee). See PORTING.md, styling rule 2. */
4099
- .fancy-marquee .animate-marquee {
4100
- animation: marquee var(--duration) linear infinite;
6180
+ .marquee > .animate-marquee {
6181
+ animation: marquee calc(var(--duration) / var(--marquee-speed, 1)) linear infinite;
6182
+ will-change: transform;
6183
+ }
6184
+
6185
+ .marquee > .animate-marquee-vertical {
6186
+ animation: marquee-vertical calc(var(--duration) / var(--marquee-speed, 1)) linear infinite;
6187
+ will-change: transform;
6188
+ }
6189
+
6190
+ /* Script-owned animations have taken over: silence the CSS keyframe so the
6191
+ track is not moved twice. */
6192
+ .marquee.marquee-upgraded > .animate-marquee,
6193
+ .marquee.marquee-upgraded > .animate-marquee-vertical {
6194
+ animation: none;
6195
+ }
6196
+
6197
+ /* Edge fades: an eased (not linear) alpha ramp, so items dissolve into the
6198
+ edge instead of hitting a visible line. Width via --marquee-fade. */
6199
+ .marquee.marquee-fade {
6200
+ --_fade: var(--marquee-fade, 12%);
6201
+ --_mask: linear-gradient(
6202
+ to right,
6203
+ transparent,
6204
+ rgb(0 0 0 / 0.1) calc(var(--_fade) * 0.25),
6205
+ rgb(0 0 0 / 0.4) calc(var(--_fade) * 0.5),
6206
+ rgb(0 0 0 / 0.8) calc(var(--_fade) * 0.75),
6207
+ #000 var(--_fade),
6208
+ #000 calc(100% - var(--_fade)),
6209
+ rgb(0 0 0 / 0.8) calc(100% - var(--_fade) * 0.75),
6210
+ rgb(0 0 0 / 0.4) calc(100% - var(--_fade) * 0.5),
6211
+ rgb(0 0 0 / 0.1) calc(100% - var(--_fade) * 0.25),
6212
+ transparent
6213
+ );
6214
+ -webkit-mask-image: var(--_mask);
6215
+ mask-image: var(--_mask);
4101
6216
  }
4102
6217
 
4103
- .fancy-marquee .animate-marquee-vertical {
4104
- animation: marquee-vertical var(--duration) linear infinite;
6218
+ .marquee.marquee-fade.marquee-vertical {
6219
+ --_mask: linear-gradient(
6220
+ to bottom,
6221
+ transparent,
6222
+ rgb(0 0 0 / 0.1) calc(var(--_fade) * 0.25),
6223
+ rgb(0 0 0 / 0.4) calc(var(--_fade) * 0.5),
6224
+ rgb(0 0 0 / 0.8) calc(var(--_fade) * 0.75),
6225
+ #000 var(--_fade),
6226
+ #000 calc(100% - var(--_fade)),
6227
+ rgb(0 0 0 / 0.8) calc(100% - var(--_fade) * 0.75),
6228
+ rgb(0 0 0 / 0.4) calc(100% - var(--_fade) * 0.5),
6229
+ rgb(0 0 0 / 0.1) calc(100% - var(--_fade) * 0.25),
6230
+ transparent
6231
+ );
4105
6232
  }
4106
6233
 
4107
6234
  @keyframes marquee {
@@ -4121,22 +6248,200 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
4121
6248
  transform: translateY(calc(-100% - var(--gap)));
4122
6249
  }
4123
6250
  }
6251
+
6252
+ /* Reduced motion: a still row with the same composition — edge fades stay. */
6253
+ @media (prefers-reduced-motion: reduce) {
6254
+ .marquee > .animate-marquee,
6255
+ .marquee > .animate-marquee-vertical {
6256
+ animation: none;
6257
+ will-change: auto;
6258
+ }
6259
+ }
6260
+ /* Anchored on the source's root class `.review-card`. The Svelte compiler
6261
+ scoped these rules; here every selector starts from that class. */
6262
+ .review-card {
6263
+ --_shell: var(--review-card-shell, #f4f4f5);
6264
+ --_panel: var(--review-card-panel, #fafafa);
6265
+ --_panel-hover: var(--review-card-panel-hover, #ffffff);
6266
+ --_line: var(--review-card-line, rgba(0, 0, 0, 0.08));
6267
+ --_ink: var(--review-card-ink, #18181b);
6268
+ --_muted: var(--review-card-muted, #71717a);
6269
+ --_body: var(--review-card-body, #52525b);
6270
+ --_lift: rgba(255, 255, 255, 1);
6271
+ background: var(--_shell);
6272
+ border-color: var(--_line);
6273
+ transition:
6274
+ border-color 300ms cubic-bezier(0.4, 0, 0.2, 1),
6275
+ background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
6276
+ }
6277
+
6278
+ .dark .review-card {
6279
+ --_shell: var(--review-card-shell, #0e0e10);
6280
+ --_panel: var(--review-card-panel, #141416);
6281
+ --_panel-hover: var(--review-card-panel-hover, #17171a);
6282
+ --_line: var(--review-card-line, rgba(255, 255, 255, 0.08));
6283
+ --_ink: var(--review-card-ink, #f4f4f5);
6284
+ --_muted: var(--review-card-muted, #71717a);
6285
+ --_body: var(--review-card-body, #a1a1aa);
6286
+ --_lift: rgba(255, 255, 255, 0.1);
6287
+ }
6288
+
6289
+ .review-card .review-card-inner {
6290
+ background: var(--_panel);
6291
+ border-color: color-mix(in oklab, var(--_line) 75%, transparent);
6292
+ box-shadow: inset 0 1px 0 transparent;
6293
+ transition:
6294
+ box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1),
6295
+ background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
6296
+ }
6297
+
6298
+ /* Hover: a quiet top-edge highlight, as if the card caught light from above. */
6299
+ .review-card:hover .review-card-inner {
6300
+ background: var(--_panel-hover);
6301
+ box-shadow:
6302
+ inset 0 1px 0 var(--_lift),
6303
+ 0 1px 2px rgba(0, 0, 0, 0.04),
6304
+ 0 6px 16px -8px rgba(0, 0, 0, 0.1);
6305
+ }
6306
+
6307
+ .review-card .review-card-sheen {
6308
+ position: absolute;
6309
+ inset: 0 0 auto 0;
6310
+ height: 60%;
6311
+ pointer-events: none;
6312
+ background: radial-gradient(
6313
+ 120% 90% at 50% 0%,
6314
+ color-mix(in oklab, var(--_lift) 60%, transparent),
6315
+ transparent 70%
6316
+ );
6317
+ opacity: 0;
6318
+ transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
6319
+ }
6320
+
6321
+ .review-card:hover .review-card-sheen {
6322
+ opacity: 1;
6323
+ }
6324
+
6325
+ .review-card .review-card-avatar {
6326
+ background: var(--_shell);
6327
+ box-shadow:
6328
+ 0 0 0 2px var(--_panel),
6329
+ 0 0 0 3px var(--_line);
6330
+ }
6331
+
6332
+ @media (prefers-reduced-motion: reduce) {
6333
+ .review-card,
6334
+ .review-card .review-card-inner,
6335
+ .review-card .review-card-sheen {
6336
+ transition: none;
6337
+ }
6338
+ }
6339
+
6340
+ .review-card .review-card-name {
6341
+ color: var(--_ink);
6342
+ }
6343
+
6344
+ .review-card .review-card-handle {
6345
+ color: var(--_muted);
6346
+ }
6347
+
6348
+ .review-card .review-card-body {
6349
+ color: var(--_body);
6350
+ }
6351
+ /* Light pages get a slate meteor; dark ones a pale blue-white. A `color`
6352
+ prop (inline) overrides both. */
6353
+ .meteor {
6354
+ --_meteor-color: var(--meteor-color, #475569);
6355
+ background: var(--_meteor-color);
6356
+ transform: rotate(var(--meteor-angle, 215deg)) scale(var(--meteor-scale, 1));
6357
+ /* the head's glow */
6358
+ box-shadow:
6359
+ 0 0 2px 1px color-mix(in srgb, var(--_meteor-color) 70%, transparent),
6360
+ 0 0 8px 2px color-mix(in srgb, var(--_meteor-color) 35%, transparent);
6361
+ animation: meteor-fall 5s cubic-bezier(0.3, 0, 0.9, 1) infinite;
6362
+ }
6363
+
6364
+ .dark .meteor {
6365
+ --_meteor-color: var(--meteor-color, #dbeafe);
6366
+ }
6367
+
6368
+ /* The tail: thickest at the head, fading out behind it. */
6369
+ .meteor::before {
6370
+ content: "";
6371
+ position: absolute;
6372
+ top: 50%;
6373
+ left: 1px;
6374
+ width: var(--meteor-tail, 60px);
6375
+ height: 1px;
6376
+ transform: translateY(-50%);
6377
+ border-radius: 999px;
6378
+ background: linear-gradient(
6379
+ to right,
6380
+ var(--_meteor-color),
6381
+ color-mix(in srgb, var(--_meteor-color) 35%, transparent) 30%,
6382
+ transparent
6383
+ );
6384
+ }
6385
+
4124
6386
  @keyframes meteor-fall {
4125
6387
  0% {
4126
- transform: rotate(215deg) translateX(0);
4127
- opacity: 1;
6388
+ transform: rotate(var(--meteor-angle, 215deg)) scale(var(--meteor-scale, 1)) translateX(0);
6389
+ opacity: 0;
6390
+ }
6391
+ 8% {
6392
+ opacity: var(--meteor-brightness, 1);
4128
6393
  }
4129
6394
  70% {
6395
+ opacity: var(--meteor-brightness, 1);
6396
+ }
6397
+ 100% {
6398
+ transform: rotate(var(--meteor-angle, 215deg)) scale(var(--meteor-scale, 1))
6399
+ translateX(calc(-1 * var(--meteor-travel, 800px)));
6400
+ opacity: 0;
6401
+ }
6402
+ }
6403
+
6404
+ /* A flare: the meteor swells and brightens just before it burns out. */
6405
+ .meteor.meteor--flare {
6406
+ animation-name: meteor-fall-flare;
6407
+ }
6408
+
6409
+ @keyframes meteor-fall-flare {
6410
+ 0% {
6411
+ transform: rotate(var(--meteor-angle, 215deg)) scale(var(--meteor-scale, 1)) translateX(0);
6412
+ opacity: 0;
6413
+ }
6414
+ 8% {
6415
+ opacity: var(--meteor-brightness, 1);
6416
+ }
6417
+ 62% {
6418
+ opacity: var(--meteor-brightness, 1);
6419
+ box-shadow:
6420
+ 0 0 2px 1px color-mix(in srgb, var(--_meteor-color) 70%, transparent),
6421
+ 0 0 8px 2px color-mix(in srgb, var(--_meteor-color) 35%, transparent);
6422
+ }
6423
+ 74% {
4130
6424
  opacity: 1;
6425
+ box-shadow:
6426
+ 0 0 4px 2px var(--_meteor-color),
6427
+ 0 0 18px 6px color-mix(in srgb, var(--_meteor-color) 55%, transparent);
4131
6428
  }
4132
6429
  100% {
4133
- transform: rotate(215deg) translateX(-500px);
6430
+ transform: rotate(var(--meteor-angle, 215deg)) scale(var(--meteor-scale, 1))
6431
+ translateX(calc(-1 * var(--meteor-travel, 800px)));
4134
6432
  opacity: 0;
4135
6433
  }
4136
6434
  }
4137
6435
 
4138
- .meteor {
4139
- animation: meteor-fall 5s linear infinite;
6436
+ /* Reduced motion: a still sky — each meteor frozen part-way along its path. */
6437
+ @media (prefers-reduced-motion: reduce) {
6438
+ .meteor,
6439
+ .meteor.meteor--flare {
6440
+ animation: none;
6441
+ opacity: calc(var(--meteor-brightness, 1) * 0.7);
6442
+ transform: rotate(var(--meteor-angle, 215deg)) scale(var(--meteor-scale, 1))
6443
+ translateX(calc(-0.35 * var(--meteor-travel, 800px)));
6444
+ }
4140
6445
  }
4141
6446
  /*
4142
6447
  * `NavbarLink` declares this same fallback again, locally on itself —
@@ -4285,71 +6590,150 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
4285
6590
  color-mix(in oklch, oklch(0.8001 0.1345 225.49) 6%, transparent)
4286
6591
  );
4287
6592
  }
4288
- .neon-layer-one {
4289
- position: absolute;
4290
- overflow: hidden;
4291
- width: 100%;
4292
- height: 100%;
4293
- filter: blur(1px) drop-shadow(0 0 12px var(--neon-color1));
4294
- z-index: -1;
4295
- inset: 0;
6593
+ /* Svelte scoped these rules; here every rule is anchored on the root class
6594
+ `.neon-border-container` so nothing leaks into the consumer's page. */
6595
+
6596
+ @property --neon-angle {
6597
+ syntax: "<angle>";
6598
+ inherits: true;
6599
+ initial-value: 135deg;
4296
6600
  }
4297
6601
 
4298
- .neon-layer-one::before {
4299
- content: "";
6602
+ .neon-border-container {
6603
+ --neon-tube-width: 1.5px;
6604
+ padding: var(--neon-tube-width);
6605
+ isolation: isolate;
6606
+ }
6607
+
6608
+ .neon-border-container .neon-tube,
6609
+ .neon-border-container .neon-light,
6610
+ .neon-border-container .neon-glow,
6611
+ .neon-border-container .neon-glow > span,
6612
+ .neon-border-container .neon-core {
4300
6613
  position: absolute;
4301
- overflow: hidden;
4302
- top: 0;
4303
- left: 0;
4304
- width: var(--neon-width);
4305
- height: 100%;
6614
+ pointer-events: none;
6615
+ border-radius: inherit;
6616
+ }
6617
+
6618
+ /* keep only the padding ring: the element minus its content box */
6619
+ .neon-border-container .neon-tube,
6620
+ .neon-border-container .neon-glow > span,
6621
+ .neon-border-container .neon-core {
6622
+ -webkit-mask:
6623
+ linear-gradient(#000 0 0) content-box,
6624
+ linear-gradient(#000 0 0);
6625
+ -webkit-mask-composite: xor;
6626
+ mask:
6627
+ linear-gradient(#000 0 0) content-box,
6628
+ linear-gradient(#000 0 0);
6629
+ mask-composite: exclude;
6630
+ }
6631
+
6632
+ /* The unlit tube: always there, faint, shading from one colour to the other. */
6633
+ .neon-border-container .neon-tube {
6634
+ inset: 0;
6635
+ padding: var(--neon-tube-width);
4306
6636
  background: linear-gradient(
4307
6637
  135deg,
4308
- var(--neon-color1),
4309
- var(--neon-color1),
4310
- transparent,
4311
- transparent
6638
+ color-mix(in srgb, var(--neon-color1) 30%, transparent),
6639
+ color-mix(in srgb, var(--neon-color2) 30%, transparent)
4312
6640
  );
4313
6641
  }
4314
6642
 
4315
- .neon-layer-two {
4316
- position: absolute;
4317
- overflow: hidden;
4318
- width: 100%;
4319
- height: 100%;
4320
- filter: blur(1px) drop-shadow(0 0 12px var(--neon-color2));
4321
- z-index: -1;
6643
+ .neon-border-container .neon-light {
4322
6644
  inset: 0;
4323
6645
  }
4324
6646
 
4325
- .neon-layer-two::before {
4326
- content: "";
4327
- position: absolute;
4328
- bottom: 0;
4329
- right: 0;
4330
- overflow: hidden;
4331
- width: var(--neon-width);
4332
- height: 100%;
4333
- background: linear-gradient(
4334
- 135deg,
4335
- transparent,
4336
- transparent,
4337
- var(--neon-color2),
4338
- var(--neon-color2)
4339
- );
6647
+ /* The glow: the beams on a wide ring straddling the edge, blurred after
6648
+ masking so it spills softly both inside and outside. */
6649
+ .neon-border-container .neon-glow {
6650
+ inset: 0;
6651
+ filter: blur(7px);
6652
+ /* on a light page a coloured halo reads as a stain: keep it faint */
6653
+ opacity: var(--neon-glow-max);
6654
+ }
6655
+
6656
+ .neon-border-container {
6657
+ --neon-glow-max: 0.4;
6658
+ --neon-glow-min: 0.25;
6659
+ }
6660
+
6661
+ .dark .neon-border-container {
6662
+ --neon-glow-max: 0.9;
6663
+ --neon-glow-min: 0.6;
6664
+ }
6665
+
6666
+ .neon-border-container .neon-glow > span {
6667
+ inset: -7px;
6668
+ padding: calc(var(--neon-tube-width) + 12px);
6669
+ border-radius: calc(0.5rem + 7px);
6670
+ background: var(--neon-beams);
6671
+ }
6672
+
6673
+ /* The white-hot core of the lit tube. */
6674
+ .neon-border-container .neon-core {
6675
+ inset: 0;
6676
+ padding: var(--neon-tube-width);
6677
+ background: var(--neon-core);
6678
+ }
6679
+
6680
+ @media (prefers-reduced-motion: no-preference) {
6681
+ /* The beams chase round the tube. */
6682
+ .neon-border-container.neon-animated {
6683
+ animation: neon-travel var(--neon-duration) linear infinite;
6684
+ }
6685
+
6686
+ /* The tube strikes once on first paint… */
6687
+ .neon-border-container .neon-light {
6688
+ animation: neon-ignite 0.9s steps(1, end) both;
6689
+ }
6690
+
6691
+ /* …then hums: a slow breathing of the glow. */
6692
+ .neon-border-container .neon-glow {
6693
+ animation: neon-hum 2.8s ease-in-out 0.9s infinite alternate;
6694
+ }
6695
+ }
6696
+
6697
+ @keyframes neon-travel {
6698
+ from {
6699
+ --neon-angle: 135deg;
6700
+ }
6701
+ to {
6702
+ --neon-angle: 495deg;
6703
+ }
4340
6704
  }
4341
6705
 
4342
- @keyframes neon-rotate {
6706
+ @keyframes neon-ignite {
4343
6707
  0% {
4344
- transform: rotate(0deg);
6708
+ opacity: 0;
6709
+ }
6710
+ 10% {
6711
+ opacity: 0.8;
6712
+ }
6713
+ 18% {
6714
+ opacity: 0.1;
6715
+ }
6716
+ 30% {
6717
+ opacity: 1;
6718
+ }
6719
+ 40% {
6720
+ opacity: 0.35;
6721
+ }
6722
+ 52% {
6723
+ opacity: 1;
4345
6724
  }
4346
6725
  100% {
4347
- transform: rotate(360deg);
6726
+ opacity: 1;
4348
6727
  }
4349
6728
  }
4350
6729
 
4351
- .neon-animated {
4352
- animation: neon-rotate var(--neon-duration) linear infinite;
6730
+ @keyframes neon-hum {
6731
+ from {
6732
+ opacity: var(--neon-glow-max);
6733
+ }
6734
+ to {
6735
+ opacity: var(--neon-glow-min);
6736
+ }
4353
6737
  }
4354
6738
  .ft-number-input-field:focus-visible {
4355
6739
  --ft-number-input-accent: var(
@@ -4436,31 +6820,50 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
4436
6820
  }
4437
6821
 
4438
6822
  /*
4439
- * The newly-current page pops once, so the eye can find where it landed
4440
- * without hunting for a colour change among nine identical squares.
4441
- *
4442
- * `transform` only: `box-shadow` on this same button is the focus ring
4443
- * above, and a focus ring must never animate. `data-armed` keeps the pop
4444
- * off the first paint (see the component). `--ft-ease-out` because the pill
4445
- * arrives at its new place — it is not toggling in position.
4446
- *
4447
- * 150ms = tokens.DURATIONS.fast, cubic-bezier(0.16, 1, 0.3, 1) = tokens.EASINGS.out
6823
+ * The nav is the page buttons' `offsetParent` (no `<ul>`/`<li>` in between
6824
+ * is positioned), so `offsetLeft`/`offsetTop` and the pill's own
6825
+ * `left: 0; top: 0` share one origin. `isolation` lets the pill sit at
6826
+ * `z-index: -1` — above the nav's background, under the buttons.
4448
6827
  */
4449
- @media (prefers-reduced-motion: no-preference) {
4450
- .ft-pagination[data-armed="true"] button[aria-current="page"] {
4451
- animation: ft-pagination-pop var(--ft-pagination-pop-duration, var(--ft-duration-fast, 150ms))
4452
- var(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
4453
- }
6828
+ .ft-pagination {
6829
+ position: relative;
6830
+ isolation: isolate;
4454
6831
  }
4455
6832
 
4456
- /* 0.92 is the library's scale floor — the same value every entrance preset
4457
- starts from, so a pop and a panel opening read as one vocabulary. */
4458
- @keyframes ft-pagination-pop {
4459
- from {
4460
- transform: scale(0.92);
4461
- }
4462
- to {
4463
- transform: scale(1);
6833
+ .ft-pagination-indicator {
6834
+ position: absolute;
6835
+ left: 0;
6836
+ top: 0;
6837
+ z-index: -1;
6838
+ pointer-events: none;
6839
+ border-radius: var(--radius-md, 0.375rem);
6840
+ background: var(--ft-pagination-indicator-color, var(--color-accent));
6841
+ opacity: 0;
6842
+ }
6843
+
6844
+ .ft-pagination[data-indicator] .ft-pagination-indicator {
6845
+ opacity: 1;
6846
+ }
6847
+
6848
+ /* The pill has taken over: the current button's own fill steps aside. */
6849
+ .ft-pagination[data-indicator] button[aria-current="page"] {
6850
+ background: transparent;
6851
+ }
6852
+
6853
+ /*
6854
+ * 300ms = tokens.DURATIONS.base, cubic-bezier(0.16, 1, 0.3, 1) =
6855
+ * tokens.EASINGS.out — the same curve and length as the numbers' own
6856
+ * glide, so the pill lands with its number. Gated: under reduced
6857
+ * motion the pill still follows the page, it just arrives.
6858
+ */
6859
+ @media (prefers-reduced-motion: no-preference) {
6860
+ .ft-pagination[data-armed="true"] .ft-pagination-indicator {
6861
+ transition: transform
6862
+ var(
6863
+ --ft-pagination-slide-duration,
6864
+ var(--ft-pagination-pop-duration, var(--ft-duration-base, 300ms))
6865
+ )
6866
+ var(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
4464
6867
  }
4465
6868
  }
4466
6869
  /*
@@ -4843,6 +7246,110 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
4843
7246
  .rainbow-button::before {
4844
7247
  animation: rainbow var(--rainbow-speed, 2s) infinite linear;
4845
7248
  }
7249
+
7250
+ /*
7251
+ * Hover beam: a rainbow light that rides the border. One conic gradient,
7252
+ * turned through a registered angle, drawn twice — masked to the border ring
7253
+ * (the beam itself) and blurred around the button (its halo).
7254
+ */
7255
+ @property --beam-angle {
7256
+ syntax: "<angle>";
7257
+ inherits: true;
7258
+ initial-value: 0deg;
7259
+ }
7260
+
7261
+ .rainbow-button {
7262
+ --beam-width: 1.5px;
7263
+ --glow-reach: 6px;
7264
+ --glow-blur: 10px;
7265
+ --beam: conic-gradient(
7266
+ from var(--beam-angle),
7267
+ transparent 0%,
7268
+ var(--rainbow-4) 5%,
7269
+ var(--rainbow-3) 10%,
7270
+ var(--rainbow-2) 15%,
7271
+ var(--rainbow-1) 20%,
7272
+ hsl(35 100% 60%) 24%,
7273
+ transparent 31%,
7274
+ transparent 50%,
7275
+ var(--rainbow-5) 55%,
7276
+ var(--rainbow-4) 60%,
7277
+ var(--rainbow-3) 64%,
7278
+ transparent 71%
7279
+ );
7280
+ }
7281
+
7282
+ .rainbow-button .rainbow-button__beam,
7283
+ .rainbow-button .rainbow-button__glow {
7284
+ position: absolute;
7285
+ border-radius: inherit;
7286
+ pointer-events: none;
7287
+ opacity: 0;
7288
+ transition: opacity 0.35s ease;
7289
+ }
7290
+
7291
+ /* keep only the padding ring: the element minus its content box */
7292
+ .rainbow-button .rainbow-button__beam,
7293
+ .rainbow-button .rainbow-button__glow > span {
7294
+ background: var(--beam);
7295
+ -webkit-mask:
7296
+ linear-gradient(#000 0 0) content-box,
7297
+ linear-gradient(#000 0 0);
7298
+ -webkit-mask-composite: xor;
7299
+ mask:
7300
+ linear-gradient(#000 0 0) content-box,
7301
+ linear-gradient(#000 0 0);
7302
+ mask-composite: exclude;
7303
+ }
7304
+
7305
+ /* the beam itself, on the border */
7306
+ .rainbow-button .rainbow-button__beam {
7307
+ inset: calc(-1 * var(--beam-width));
7308
+ padding: var(--beam-width);
7309
+ z-index: 1;
7310
+ }
7311
+
7312
+ /* its halo: a wider ring, blurred AFTER masking so both edges go soft */
7313
+ .rainbow-button .rainbow-button__glow {
7314
+ inset: 0;
7315
+ filter: blur(var(--glow-blur));
7316
+ }
7317
+
7318
+ .rainbow-button .rainbow-button__glow > span {
7319
+ position: absolute;
7320
+ inset: calc(-1 * var(--glow-reach));
7321
+ padding: var(--glow-reach);
7322
+ border-radius: calc(0.75rem + var(--glow-reach));
7323
+ }
7324
+
7325
+ .rainbow-button:hover .rainbow-button__beam,
7326
+ .rainbow-button:focus-visible .rainbow-button__beam {
7327
+ opacity: 1;
7328
+ }
7329
+
7330
+ .rainbow-button:hover .rainbow-button__glow,
7331
+ .rainbow-button:focus-visible .rainbow-button__glow {
7332
+ opacity: 0.6;
7333
+ }
7334
+
7335
+ .rainbow-button:hover,
7336
+ .rainbow-button:focus-visible {
7337
+ animation: beam-spin calc(var(--rainbow-speed, 2s) * 1.5) linear infinite;
7338
+ }
7339
+
7340
+ @keyframes beam-spin {
7341
+ to {
7342
+ --beam-angle: 360deg;
7343
+ }
7344
+ }
7345
+
7346
+ @media (prefers-reduced-motion: reduce) {
7347
+ .rainbow-button:hover,
7348
+ .rainbow-button:focus-visible {
7349
+ animation: none;
7350
+ --beam-angle: 200deg;
7351
+ }
7352
+ }
4846
7353
  /*
4847
7354
  * `.fancy-reasoning-panel` is a PORT-ADDED anchor (PORTING.md, styling rule 2).
4848
7355
  * The source's <style> block was compiler-scoped and had no root class; plain
@@ -5101,22 +7608,146 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
5101
7608
  .animate-ripple-circle {
5102
7609
  animation: ripple-effect 2s ease-in-out infinite;
5103
7610
  }
5104
- @keyframes rippling {
7611
+ /* Every rule is anchored on the root class `ripple-button` (the Svelte source
7612
+ names it on the root; its compiler scoped the inner selectors). As plain CSS,
7613
+ a bare `.ripple-hover` / `.ripple-animation` would leak into any consumer
7614
+ element of that name. See PORTING.md, styling rule 2. */
7615
+
7616
+ /* Hover: a fingertip over water. */
7617
+ .ripple-button .ripple-hover {
7618
+ position: absolute;
7619
+ inset: 0;
7620
+ pointer-events: none;
7621
+ opacity: 0;
7622
+ transition: opacity 0.3s ease;
7623
+ background: radial-gradient(
7624
+ 80px circle at var(--ripple-x, 50%) var(--ripple-y, 50%),
7625
+ color-mix(in srgb, var(--ripple-color) 20%, transparent),
7626
+ transparent 70%
7627
+ );
7628
+ }
7629
+
7630
+ .ripple-button .ripple-hover::after {
7631
+ content: "";
7632
+ position: absolute;
7633
+ left: var(--ripple-x, 50%);
7634
+ top: var(--ripple-y, 50%);
7635
+ width: 72px;
7636
+ height: 72px;
7637
+ margin: -36px 0 0 -36px;
7638
+ border-radius: 50%;
7639
+ border: 1px solid color-mix(in srgb, var(--ripple-color) 55%, transparent);
7640
+ opacity: 0;
7641
+ transform: scale(0.2);
7642
+ }
7643
+
7644
+ .ripple-button:hover .ripple-hover {
7645
+ opacity: 1;
7646
+ }
7647
+
7648
+ .ripple-button:hover .ripple-hover::after {
7649
+ animation: ripple-idle 1.8s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
7650
+ }
7651
+
7652
+ .ripple-button:hover {
7653
+ border-color: color-mix(in srgb, var(--ripple-color) 35%, transparent);
7654
+ }
7655
+
7656
+ @keyframes ripple-idle {
5105
7657
  0% {
5106
- transform: scale(0);
5107
- opacity: 0.3;
7658
+ transform: scale(0.2);
7659
+ opacity: 0.9;
5108
7660
  }
5109
7661
  100% {
5110
- transform: scale(2);
7662
+ transform: scale(1);
5111
7663
  opacity: 0;
5112
7664
  }
5113
7665
  }
5114
7666
 
5115
- /* Anchored on the root class (added in the port — the Svelte compiler scoped
5116
- this rule; as plain CSS it would leak into any consumer element named
5117
- .ripple-animation). See PORTING.md, styling rule 2. */
7667
+ /* The border takes the ripple's tint while a ripple runs. */
7668
+ .ripple-button[data-rippling] {
7669
+ border-color: color-mix(in srgb, var(--ripple-color) 55%, transparent);
7670
+ }
7671
+
7672
+ /* A soft glow that blooms and fades… */
5118
7673
  .ripple-button .ripple-animation {
5119
- animation: rippling var(--ripple-duration, 600ms) ease-out forwards;
7674
+ background: radial-gradient(
7675
+ circle,
7676
+ color-mix(in srgb, var(--ripple-color) 40%, transparent) 0%,
7677
+ color-mix(in srgb, var(--ripple-color) 16%, transparent) 45%,
7678
+ transparent 70%
7679
+ );
7680
+ animation: ripple-bloom var(--ripple-duration, 900ms) cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
7681
+ }
7682
+
7683
+ /* …and two fine rings, the second a beat behind, like a wave on water. */
7684
+ .ripple-button .ripple-animation::before,
7685
+ .ripple-button .ripple-animation::after {
7686
+ content: "";
7687
+ position: absolute;
7688
+ inset: 0;
7689
+ border-radius: 50%;
7690
+ border: 2px solid color-mix(in srgb, var(--ripple-color) 90%, transparent);
7691
+ transform: scale(0);
7692
+ opacity: 0;
7693
+ animation: ripple-ring var(--ripple-duration, 900ms) cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
7694
+ }
7695
+
7696
+ .ripple-button .ripple-animation::after {
7697
+ border-width: 1.5px;
7698
+ animation-delay: calc(var(--ripple-duration, 900ms) * 0.12);
7699
+ }
7700
+
7701
+ @keyframes ripple-bloom {
7702
+ 0% {
7703
+ transform: scale(0);
7704
+ opacity: 1;
7705
+ }
7706
+ 100% {
7707
+ transform: scale(1);
7708
+ opacity: 0;
7709
+ }
7710
+ }
7711
+
7712
+ @keyframes ripple-ring {
7713
+ 0% {
7714
+ transform: scale(0);
7715
+ opacity: 1;
7716
+ }
7717
+ 60% {
7718
+ opacity: 0.8;
7719
+ }
7720
+ 100% {
7721
+ transform: scale(1);
7722
+ opacity: 0;
7723
+ }
7724
+ }
7725
+
7726
+ /* Reduced motion: no expansion, just a brief glow where you pressed. */
7727
+ @media (prefers-reduced-motion: reduce) {
7728
+ .ripple-button:hover .ripple-hover::after {
7729
+ animation: none;
7730
+ }
7731
+
7732
+ .ripple-button .ripple-animation {
7733
+ animation-name: ripple-fade;
7734
+ }
7735
+
7736
+ .ripple-button .ripple-animation::before,
7737
+ .ripple-button .ripple-animation::after {
7738
+ display: none;
7739
+ }
7740
+
7741
+ @keyframes ripple-fade {
7742
+ 0% {
7743
+ transform: scale(1);
7744
+ opacity: 1;
7745
+ }
7746
+ 100% {
7747
+ transform: scale(1);
7748
+ opacity: 0;
7749
+ }
7750
+ }
5120
7751
  }
5121
7752
  /*
5122
7753
  * Centring lives here rather than on a `-translate-x-1/2` utility: the
@@ -5296,37 +7927,123 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
5296
7927
  border-color: var(--ft-field-accent);
5297
7928
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--ft-field-accent) 25%, transparent);
5298
7929
  }
5299
- @keyframes shimmer-slide {
5300
- to {
5301
- transform: translate(calc(100cqw - 100%), 0);
5302
- }
7930
+ /* Plain CSS is global: every selector carries the component prefix (.shimmer-button or .shimmer-button__*), so nothing leaks onto consumer elements. */
7931
+ .shimmer-button__rim,
7932
+ .shimmer-button__face,
7933
+ .shimmer-button__sheen,
7934
+ .shimmer-button__spot {
7935
+ position: absolute;
7936
+ pointer-events: none;
7937
+ border-radius: inherit;
7938
+ }
7939
+
7940
+ .shimmer-button__rim {
7941
+ inset: 0;
7942
+ z-index: -2;
7943
+ background: linear-gradient(
7944
+ to bottom,
7945
+ color-mix(in srgb, var(--shimmer-color) 28%, transparent),
7946
+ color-mix(in srgb, var(--shimmer-color) 6%, transparent) 55%,
7947
+ color-mix(in srgb, var(--shimmer-color) 14%, transparent)
7948
+ );
7949
+ }
7950
+
7951
+ .shimmer-button__face {
7952
+ inset: var(--cut);
7953
+ z-index: -1;
7954
+ border-radius: calc(var(--radius) - var(--cut));
7955
+ background:
7956
+ linear-gradient(
7957
+ to bottom,
7958
+ color-mix(in srgb, var(--shimmer-color) 10%, transparent),
7959
+ transparent 50%
7960
+ ),
7961
+ var(--bg);
7962
+ box-shadow: inset 0 -10px 16px -12px color-mix(in srgb, var(--shimmer-color) 30%, transparent);
7963
+ }
7964
+
7965
+ /* Screen blend: the band lightens the face and lifts the label to full white. */
7966
+ .shimmer-button__sheen {
7967
+ inset: 0;
7968
+ z-index: 20;
7969
+ mix-blend-mode: screen;
7970
+ /* a wide soft halo with a thin brighter core */
7971
+ background:
7972
+ linear-gradient(
7973
+ 108deg,
7974
+ transparent 44%,
7975
+ color-mix(in srgb, var(--shimmer-color) 38%, transparent) 50%,
7976
+ transparent 56%
7977
+ ),
7978
+ linear-gradient(
7979
+ 108deg,
7980
+ transparent 30%,
7981
+ color-mix(in srgb, var(--shimmer-color) 12%, transparent) 42%,
7982
+ color-mix(in srgb, var(--shimmer-color) 18%, transparent) 50%,
7983
+ color-mix(in srgb, var(--shimmer-color) 12%, transparent) 58%,
7984
+ transparent 70%
7985
+ );
7986
+ background-size: 250% 100%;
7987
+ background-repeat: no-repeat;
7988
+ background-position: 100% 0;
7989
+ animation: shimmer-sweep var(--speed) cubic-bezier(0.4, 0, 0.2, 1) infinite;
7990
+ transition: opacity 0.3s ease;
7991
+ }
7992
+
7993
+ .shimmer-button__spot {
7994
+ inset: 0;
7995
+ z-index: 20;
7996
+ mix-blend-mode: screen;
7997
+ opacity: 0;
7998
+ background: radial-gradient(
7999
+ 90px circle at var(--mx, 50%) var(--my, 50%),
8000
+ color-mix(in srgb, var(--shimmer-color) 34%, transparent),
8001
+ transparent 70%
8002
+ );
8003
+ transition: opacity 0.3s ease;
8004
+ }
8005
+
8006
+ .shimmer-button:hover .shimmer-button__spot {
8007
+ opacity: 1;
8008
+ }
8009
+
8010
+ /* The pointer takes over from the sweep while hovering. */
8011
+ .shimmer-button:hover .shimmer-button__sheen {
8012
+ opacity: 0;
8013
+ }
8014
+
8015
+ .shimmer-button:focus-visible {
8016
+ outline: 2px solid color-mix(in srgb, var(--shimmer-color) 70%, transparent);
8017
+ outline-offset: 2px;
8018
+ }
8019
+
8020
+ .shimmer-button:disabled {
8021
+ cursor: not-allowed;
8022
+ opacity: 0.5;
8023
+ }
8024
+
8025
+ .shimmer-button:disabled .shimmer-button__sheen,
8026
+ .shimmer-button:disabled .shimmer-button__spot {
8027
+ display: none;
5303
8028
  }
5304
8029
 
5305
- @keyframes spin-around {
8030
+ /* Sweep across in the first 55% of the cycle, then rest off-button. */
8031
+ @keyframes shimmer-sweep {
5306
8032
  0% {
5307
- transform: translateZ(0) rotate(0);
5308
- }
5309
- 15%,
5310
- 35% {
5311
- transform: translateZ(0) rotate(90deg);
5312
- }
5313
- 65%,
5314
- 85% {
5315
- transform: translateZ(0) rotate(270deg);
8033
+ background-position: 100% 0;
5316
8034
  }
8035
+ 55%,
5317
8036
  100% {
5318
- transform: translateZ(0) rotate(360deg);
8037
+ background-position: 0% 0;
5319
8038
  }
5320
8039
  }
5321
8040
 
5322
- /* Anchored on the root class: the Svelte compiler scoped these rules, plain CSS
5323
- would leak them into every consumer element sharing the class name. */
5324
- .shimmer-button .shimmer-slide {
5325
- animation: shimmer-slide var(--speed) ease-in-out infinite alternate;
5326
- }
5327
-
5328
- .shimmer-button .spin-around {
5329
- animation: spin-around calc(var(--speed) * 2) infinite linear;
8041
+ @media (prefers-reduced-motion: reduce) {
8042
+ .shimmer-button__sheen {
8043
+ animation: none;
8044
+ background-position: 38% 0;
8045
+ opacity: 0.6;
8046
+ }
5330
8047
  }
5331
8048
  /*
5332
8049
  * `SidebarItem` declares this same fallback again, locally on itself —
@@ -5670,11 +8387,111 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
5670
8387
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--ft-slider-accent) 25%, transparent);
5671
8388
  }
5672
8389
 
5673
- .ft-slider:disabled::-webkit-slider-thumb {
5674
- box-shadow: none;
8390
+ .ft-slider:disabled::-webkit-slider-thumb {
8391
+ box-shadow: none;
8392
+ }
8393
+ .ft-slider:disabled::-moz-range-thumb {
8394
+ box-shadow: none;
8395
+ }
8396
+ /*
8397
+ * SmoothCursor — the Svelte source's scoped <style> block. Plain CSS is global,
8398
+ * so every rule is anchored on the component's own root class `.smooth-cursor`
8399
+ * (the Svelte root already carries it). Class names and custom properties are
8400
+ * identical to the Svelte source.
8401
+ */
8402
+
8403
+ .smooth-cursor {
8404
+ --_sc-hot: color-mix(in oklab, var(--_sc-color) 45%, white);
8405
+ --_sc-deep: color-mix(in oklab, var(--_sc-color) 82%, black);
8406
+ --_sc-ink: var(--smooth-cursor-label-color, #fff);
8407
+ }
8408
+
8409
+ .smooth-cursor .sc-pointer,
8410
+ .smooth-cursor .sc-label {
8411
+ position: absolute;
8412
+ top: 0;
8413
+ left: 0;
8414
+ will-change: transform;
8415
+ }
8416
+
8417
+ /* The arrow always paints over the pill when the pill catches up behind it. */
8418
+ .smooth-cursor .sc-pointer {
8419
+ z-index: 1;
8420
+ }
8421
+
8422
+ .smooth-cursor .sc-arrow {
8423
+ display: block;
8424
+ overflow: visible;
8425
+ filter: drop-shadow(0 1px 1.25px rgb(0 0 0 / 0.32))
8426
+ drop-shadow(0 4px 9px color-mix(in oklab, var(--_sc-color) 38%, transparent));
8427
+ }
8428
+
8429
+ /* Rotating mode: turn the arrowhead so it points "up" — the engine's 0deg. */
8430
+ .smooth-cursor .sc-pointer--rotate .sc-arrow {
8431
+ rotate: 32.5deg;
8432
+ }
8433
+
8434
+ .smooth-cursor .sc-arrow-rim {
8435
+ stroke: #fff;
8436
+ stroke-width: 3;
8437
+ stroke-linejoin: round;
8438
+ paint-order: stroke fill;
8439
+ }
8440
+
8441
+ .smooth-cursor .sc-stop-hot {
8442
+ stop-color: var(--_sc-hot);
8443
+ }
8444
+ .smooth-cursor .sc-stop-body {
8445
+ stop-color: var(--_sc-color);
8446
+ }
8447
+ .smooth-cursor .sc-stop-deep {
8448
+ stop-color: var(--_sc-deep);
8449
+ }
8450
+
8451
+ /* Name pill: tucked under the arrow's back edge, square-ish corner pointing at the tip. */
8452
+ .smooth-cursor .sc-label {
8453
+ translate: 13px 17px;
8454
+ transform-origin: 0 0;
8455
+ display: flex;
8456
+ align-items: center;
8457
+ max-width: 180px;
8458
+ height: 22px;
8459
+ padding: 0 9px;
8460
+ border-radius: 3px 11px 11px 11px;
8461
+ background: linear-gradient(
8462
+ 180deg,
8463
+ color-mix(in oklab, var(--_sc-color) 86%, white) 0%,
8464
+ var(--_sc-color) 55%,
8465
+ color-mix(in oklab, var(--_sc-color) 92%, black) 100%
8466
+ );
8467
+ color: var(--_sc-ink);
8468
+ box-shadow:
8469
+ 0 0 0 1px rgb(255 255 255 / 0.92),
8470
+ inset 0 1px 0 rgb(255 255 255 / 0.28),
8471
+ 0 1px 2px rgb(0 0 0 / 0.28),
8472
+ 0 6px 16px -4px color-mix(in oklab, var(--_sc-color) 45%, transparent);
8473
+ opacity: 1;
8474
+ }
8475
+
8476
+ .smooth-cursor .sc-label-text {
8477
+ overflow: hidden;
8478
+ font-size: 12px;
8479
+ font-weight: 500;
8480
+ line-height: 1;
8481
+ letter-spacing: 0.01em;
8482
+ white-space: nowrap;
8483
+ text-overflow: ellipsis;
8484
+ text-shadow: 0 1px 0 rgb(0 0 0 / 0.14);
5675
8485
  }
5676
- .ft-slider:disabled::-moz-range-thumb {
5677
- box-shadow: none;
8486
+
8487
+ .smooth-cursor .sc-label.is-idle {
8488
+ opacity: 0.35;
8489
+ }
8490
+
8491
+ @media (prefers-reduced-motion: no-preference) {
8492
+ .smooth-cursor .sc-label {
8493
+ transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
8494
+ }
5678
8495
  }
5679
8496
  .ft-sources-trigger {
5680
8497
  border: 1px solid
@@ -5810,47 +8627,69 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
5810
8627
  transform: scale(0) rotate(150deg);
5811
8628
  }
5812
8629
  }
8630
+ /*
8631
+ * Port note: Svelte scoped these rules; plain CSS is global, so every
8632
+ * selector below is anchored on a class this component owns and puts on its
8633
+ * own elements (`.ft-step`, `.ft-step-bullet*`, `.ft-step-connector*`,
8634
+ * `.ft-step-glyph`, `.ft-step-check`, `.ft-step-trigger`, `.ft-step-animate`
8635
+ * — all of them the Svelte source's own names, already on the elements'
8636
+ * class lists). No anchor had to be added. Everything below is copied
8637
+ * verbatim from the Svelte source's <style> block.
8638
+ */
8639
+
5813
8640
  /*
5814
8641
  * The only place this component reaches for the brand purple — the
5815
- * current bullet's fill/halo and a done segment's connector. No
5816
- * semantic token owns it, so it's declared locally with a light-dark()
5817
- * fallback, retintable from higher up the tree via `--ft-accent`.
5818
- * Mirrors the identical accent pattern a toggle group's items use for
5819
- * their focus ring. Never redeclare `--ft-accent` itself: that would
5820
- * shadow whatever an ancestor set, the exact bug an earlier wave shipped
5821
- * in `Link`.
8642
+ * current bullet's fill/halo and a lit rail. No semantic token owns it,
8643
+ * so it's declared locally with a light-dark() fallback, retintable from
8644
+ * higher up the tree via `--ft-accent`. Mirrors ToggleGroupItem's
8645
+ * identical `--ft-toggle-group-accent` pattern. Never redeclare
8646
+ * `--ft-accent` itself: that would shadow whatever an ancestor set, the
8647
+ * exact bug an earlier wave shipped in `Link`.
5822
8648
  *
5823
- * Svelte scoped these rules; plain CSS is global, so every selector below
5824
- * is anchored on a class this component owns and puts on its own elements
5825
- * (`.ft-step`, `.ft-step-bullet`, `.ft-step-connector`, `.ft-step-trigger`
5826
- * — all of them the Svelte source's own names, already the first token of
5827
- * the element's class list). No anchor had to be added.
8649
+ * Every private `--_x` reads a public `--ft-step-*` first, so a consumer
8650
+ * can retune the rail from a class without touching this file.
5828
8651
  */
5829
8652
  .ft-step {
5830
8653
  --ft-nav-accent: var(
5831
8654
  --ft-accent,
5832
8655
  light-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75))
5833
8656
  );
8657
+ /* What a lit rail settles to: the accent at reduced strength. */
8658
+ --_rail: var(--ft-step-rail, color-mix(in oklab, var(--ft-nav-accent) 58%, transparent));
8659
+ /* The head of the travelling light. White-hot on dark; on a light
8660
+ * surface a white core would vanish, so it stays close to the accent. */
8661
+ --_hot: var(
8662
+ --ft-step-glint,
8663
+ light-dark(
8664
+ color-mix(in oklab, var(--ft-nav-accent) 82%, white),
8665
+ color-mix(in oklab, var(--ft-nav-accent) 45%, white)
8666
+ )
8667
+ );
8668
+ --_sweep: var(--ft-step-sweep-duration, 420ms);
8669
+ /* One beat = how far behind the previous rail the next one lights, so
8670
+ * a multi-step jump reads as a single run of light. */
8671
+ --_beat: calc(var(--_sweep) * 0.7);
8672
+ --_ease-out: var(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1));
5834
8673
  }
5835
8674
 
5836
8675
  /*
5837
8676
  * A step's whole visible state — the bullet's fill, its label colour, the
5838
- * halo around the current one, and the connector behind it — used to arrive
5839
- * in a single frame. It now eases in on the same clock as everything else
5840
- * in the library.
8677
+ * halo around the current one, and the rail behind it — eases in on the
8678
+ * same clock as everything else in the library.
5841
8679
  *
5842
8680
  * Declared outside any `prefers-reduced-motion` query on purpose: none of
5843
- * these three properties moves anything. A colour that crossfades and a
5844
- * static ring that appears are state changes, not travel, and suppressing
5845
- * them under reduced motion would make the stepper flicker rather than
5846
- * settle. The focus ring is safe from this list because it lives on a
5847
- * different element (`.ft-step-trigger`), so no `box-shadow` here is ever
5848
- * a focus indicator.
8681
+ * these properties moves anything. A colour that crossfades and a static
8682
+ * ring that appears are state changes, not travel, and suppressing them
8683
+ * under reduced motion would make the stepper flicker rather than settle.
8684
+ * The focus ring is safe from this list because it lives on a different
8685
+ * element (`.ft-step-trigger`), so no `box-shadow` here is ever a focus
8686
+ * indicator.
5849
8687
  *
5850
8688
  * 150ms = tokens.DURATIONS.fast, cubic-bezier(0.4, 0, 0.2, 1) = tokens.EASINGS.inout
5851
8689
  */
5852
8690
  .ft-step-bullet,
5853
- .ft-step-connector {
8691
+ .ft-step-connector,
8692
+ .ft-step-connector::before {
5854
8693
  --ft-step-signal: var(--ft-step-signal-duration, var(--ft-duration-fast, 150ms))
5855
8694
  var(--ft-ease-inout, cubic-bezier(0.4, 0, 0.2, 1));
5856
8695
  transition:
@@ -5859,19 +8698,120 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
5859
8698
  box-shadow var(--ft-step-signal);
5860
8699
  }
5861
8700
 
8701
+ /* ---------------------------------------------------------------- bullets */
8702
+
8703
+ /*
8704
+ * The outline is an inset box-shadow, not a border: it then rides the
8705
+ * same (possibly delayed) transition as the fill, so a step the light is
8706
+ * still travelling towards keeps its ring until the light lands instead
8707
+ * of dropping it the instant `current` changes.
8708
+ */
8709
+ .ft-step-bullet-upcoming {
8710
+ background: color-mix(in oklab, currentColor 5%, transparent);
8711
+ box-shadow: inset 0 0 0 1px
8712
+ var(--ft-step-outline, var(--border, color-mix(in oklab, currentColor 22%, transparent)));
8713
+ }
8714
+
5862
8715
  .ft-step-bullet-done {
5863
8716
  background: var(--ft-status-done, light-dark(oklch(0.5 0.14 145), oklch(0.72 0.15 145)));
5864
8717
  color: light-dark(oklch(1 0 0), oklch(0.15 0 0));
8718
+ box-shadow:
8719
+ inset 0 1px 0 rgb(255 255 255 / 0.22),
8720
+ 0 1px 2px rgb(0 0 0 / 0.12);
5865
8721
  }
5866
8722
 
5867
8723
  .ft-step-bullet-current {
5868
8724
  background: var(--ft-nav-accent);
5869
8725
  color: light-dark(oklch(1 0 0), oklch(0.15 0 0));
5870
- box-shadow: 0 0 0 4px color-mix(in oklab, var(--ft-nav-accent) 20%, transparent);
8726
+ box-shadow:
8727
+ inset 0 1px 0 rgb(255 255 255 / 0.28),
8728
+ 0 1px 2px rgb(0 0 0 / 0.14);
5871
8729
  }
5872
8730
 
5873
- .ft-step-connector-done {
5874
- background: var(--ft-status-done, light-dark(oklch(0.5 0.14 145), oklch(0.72 0.15 145)));
8731
+ /*
8732
+ * The halo lives on its own layer so it can breathe through `opacity`
8733
+ * alone — never re-painting a box-shadow per frame, never scaling. At
8734
+ * rest (SSR, reduced motion) it is simply on.
8735
+ */
8736
+ .ft-step-bullet-halo::before {
8737
+ content: "";
8738
+ position: absolute;
8739
+ inset: 0;
8740
+ border-radius: inherit;
8741
+ pointer-events: none;
8742
+ box-shadow:
8743
+ 0 0 0 4px color-mix(in oklab, var(--ft-nav-accent) 18%, transparent),
8744
+ 0 0 18px 2px color-mix(in oklab, var(--ft-nav-accent) 30%, transparent);
8745
+ }
8746
+
8747
+ .ft-step-glyph {
8748
+ display: inline-block;
8749
+ }
8750
+
8751
+ /* ------------------------------------------------------------------ rails */
8752
+
8753
+ /*
8754
+ * The rail is a hairline track (the connector itself, `bg-border`) with
8755
+ * two layers on top: `::before`, the settled fill, and `::after`, the
8756
+ * light that travels once across it when it lights.
8757
+ *
8758
+ * At rest the fill is full-length and only its colour changes, so the
8759
+ * no-motion path is a pure cross-fade.
8760
+ */
8761
+ .ft-step-connector {
8762
+ /* Horizontal clip only — the travelling light may glow above and below
8763
+ * the rail but never spill past its ends onto a bullet. */
8764
+ clip-path: inset(-10px 0);
8765
+ }
8766
+ .ft-step[data-orientation="vertical"] .ft-step-connector {
8767
+ clip-path: inset(0 -10px);
8768
+ }
8769
+
8770
+ .ft-step-connector::before,
8771
+ .ft-step-connector::after {
8772
+ content: "";
8773
+ position: absolute;
8774
+ pointer-events: none;
8775
+ border-radius: inherit;
8776
+ }
8777
+
8778
+ .ft-step-connector::before {
8779
+ inset: 0;
8780
+ background-color: transparent;
8781
+ }
8782
+ .ft-step-connector-lit::before {
8783
+ background-color: var(--_rail);
8784
+ }
8785
+
8786
+ .ft-step-connector::after {
8787
+ top: 0;
8788
+ bottom: 0;
8789
+ left: 0;
8790
+ width: 45%;
8791
+ opacity: 0;
8792
+ background: linear-gradient(
8793
+ 90deg,
8794
+ transparent,
8795
+ color-mix(in oklab, var(--ft-nav-accent) 55%, transparent) 45%,
8796
+ var(--ft-nav-accent) 78%,
8797
+ var(--_hot) 97%,
8798
+ var(--_hot)
8799
+ );
8800
+ filter: drop-shadow(0 0 3px color-mix(in oklab, var(--ft-nav-accent) 80%, transparent));
8801
+ }
8802
+ .ft-step[data-orientation="vertical"] .ft-step-connector::after {
8803
+ right: 0;
8804
+ width: auto;
8805
+ height: 45%;
8806
+ bottom: auto;
8807
+ background: linear-gradient(
8808
+ 180deg,
8809
+ transparent,
8810
+ color-mix(in oklab, var(--ft-nav-accent) 55%, transparent) 45%,
8811
+ var(--ft-nav-accent) 78%,
8812
+ var(--_hot) 97%,
8813
+ var(--_hot)
8814
+ );
5875
8815
  }
5876
8816
 
5877
8817
  .ft-step-trigger:focus-visible {
@@ -5879,6 +8819,150 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
5879
8819
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--ft-nav-accent) 35%, transparent);
5880
8820
  border-radius: 0.375rem;
5881
8821
  }
8822
+
8823
+ /* ----------------------------------------------------------------- motion */
8824
+
8825
+ /*
8826
+ * Everything below travels, so it is doubly gated: the OS preference
8827
+ * (this query) and `.ft-step-animate`, which the root only hands out on
8828
+ * the client once it has read the same preference. The resting state
8829
+ * above is the fallback for both.
8830
+ *
8831
+ * `--ft-step-order` (rails) and `--ft-step-arrival` (bullets) are written
8832
+ * inline by `Step` — beats since the light set off from the previous
8833
+ * active step, so one jump across several steps lights them in sequence.
8834
+ */
8835
+ @media (prefers-reduced-motion: no-preference) {
8836
+ /* The fill grows along the rail instead of cross-fading, and retracts
8837
+ * the same way when the reader steps back. An unlit fill stays
8838
+ * transparent while collapsed: `.ft-step-animate` lands on elements
8839
+ * already painted (hydration, a post-paint effect, the OS preference
8840
+ * flipping), and a fill that turned the rail colour in that same style
8841
+ * change would show every unlit rail fully lit for its delay, then
8842
+ * retract. Its colour only drops once a retraction has finished
8843
+ * (a 0s transition delayed past the sweep); lighting switches it on
8844
+ * instantly while the fill is still collapsed. */
8845
+ .ft-step-animate .ft-step-connector::before {
8846
+ transform: scaleX(0);
8847
+ transform-origin: left center;
8848
+ transition:
8849
+ transform var(--_sweep) var(--_ease-out) calc(var(--ft-step-order, 0) * var(--_beat)),
8850
+ background-color 0s linear calc(var(--ft-step-order, 0) * var(--_beat) + var(--_sweep));
8851
+ }
8852
+ .ft-step-animate[data-orientation="vertical"] .ft-step-connector::before {
8853
+ transform: scaleY(0);
8854
+ transform-origin: center top;
8855
+ }
8856
+ .ft-step-animate .ft-step-connector-lit::before,
8857
+ .ft-step-animate[data-orientation="vertical"] .ft-step-connector-lit::before {
8858
+ transform: none;
8859
+ transition: transform var(--_sweep) var(--_ease-out)
8860
+ calc(var(--ft-step-order, 0) * var(--_beat));
8861
+ }
8862
+
8863
+ /* The light: a comet whose head rides exactly on the fill's leading
8864
+ * edge (same duration, same curve, translate range chosen so head =
8865
+ * fill edge), then fades once it has arrived. Keyed on the lit class,
8866
+ * so it plays once each time a rail lights — and once on first paint,
8867
+ * as the arrival. */
8868
+ .ft-step-animate .ft-step-connector-lit::after {
8869
+ animation:
8870
+ ft-step-sweep-x var(--_sweep) var(--_ease-out) calc(var(--ft-step-order, 0) * var(--_beat))
8871
+ both,
8872
+ ft-step-sweep-fade calc(var(--_sweep) * 0.7) linear
8873
+ calc(var(--ft-step-order, 0) * var(--_beat) + var(--_sweep) * 0.6) both;
8874
+ }
8875
+ .ft-step-animate[data-orientation="vertical"] .ft-step-connector-lit::after {
8876
+ animation-name: ft-step-sweep-y, ft-step-sweep-fade;
8877
+ }
8878
+
8879
+ /* A bullet the light is heading for settles when it gets there, not
8880
+ * the instant `current` changes. */
8881
+ .ft-step-animate .ft-step-bullet {
8882
+ transition-delay: calc(var(--ft-step-arrival, 0) * var(--_beat));
8883
+ }
8884
+
8885
+ /* The glyph swap: a short flip on whichever glyph just mounted, and the
8886
+ * check stroke drawing itself behind it. */
8887
+ .ft-step-animate .ft-step-glyph {
8888
+ transform-origin: center;
8889
+ animation: ft-step-flip 200ms var(--_ease-out) calc(var(--ft-step-arrival, 0) * var(--_beat))
8890
+ both;
8891
+ }
8892
+ .ft-step-animate .ft-step-check path {
8893
+ stroke-dasharray: 1;
8894
+ animation: ft-step-draw 320ms var(--_ease-out)
8895
+ calc(var(--ft-step-arrival, 0) * var(--_beat) + 60ms) both;
8896
+ }
8897
+
8898
+ /* The breathing halo — opacity only, no scale — faded in on arrival
8899
+ * through `filter` so the two animations never fight over `opacity`. */
8900
+ .ft-step-animate .ft-step-bullet-halo::before {
8901
+ animation:
8902
+ ft-step-halo-in 360ms var(--_ease-out) calc(var(--ft-step-arrival, 0) * var(--_beat)) both,
8903
+ ft-step-breathe 1.2s ease-in-out infinite alternate;
8904
+ }
8905
+ }
8906
+
8907
+ @keyframes ft-step-sweep-x {
8908
+ from {
8909
+ transform: translateX(-100%);
8910
+ }
8911
+ to {
8912
+ /* head (right edge, at 45% width) lands on the rail's far end */
8913
+ transform: translateX(122.3%);
8914
+ }
8915
+ }
8916
+ @keyframes ft-step-sweep-y {
8917
+ from {
8918
+ transform: translateY(-100%);
8919
+ }
8920
+ to {
8921
+ transform: translateY(122.3%);
8922
+ }
8923
+ }
8924
+ @keyframes ft-step-sweep-fade {
8925
+ from {
8926
+ opacity: 1;
8927
+ }
8928
+ to {
8929
+ opacity: 0;
8930
+ }
8931
+ }
8932
+ @keyframes ft-step-breathe {
8933
+ from {
8934
+ opacity: 0.5;
8935
+ }
8936
+ to {
8937
+ opacity: 1;
8938
+ }
8939
+ }
8940
+ @keyframes ft-step-halo-in {
8941
+ from {
8942
+ filter: opacity(0);
8943
+ }
8944
+ to {
8945
+ filter: opacity(1);
8946
+ }
8947
+ }
8948
+ @keyframes ft-step-flip {
8949
+ from {
8950
+ transform: perspective(60px) rotateX(90deg);
8951
+ opacity: 0;
8952
+ }
8953
+ to {
8954
+ transform: perspective(60px) rotateX(0deg);
8955
+ opacity: 1;
8956
+ }
8957
+ }
8958
+ @keyframes ft-step-draw {
8959
+ from {
8960
+ stroke-dashoffset: 1;
8961
+ }
8962
+ to {
8963
+ stroke-dashoffset: 0;
8964
+ }
8965
+ }
5882
8966
  /*
5883
8967
  * The two-column ⇄ stacked switch is FLEX WRAPPING, not a `@container`
5884
8968
  * query, and deliberately so: an element is styled by the query containers
@@ -6933,6 +10017,263 @@ svg[data-state="error"] .ft-statusmorph-cross-b {
6933
10017
  border-color: var(--ft-field-accent);
6934
10018
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--ft-field-accent) 25%, transparent);
6935
10019
  }
10020
+ /*
10021
+ * Timeline "light rail". Mirrors the Svelte <style> block rule for rule. The
10022
+ * Svelte compiler scoped every selector; here each one is anchored on the
10023
+ * source's own root class `.tl-root` so no generic name leaks into the page.
10024
+ */
10025
+ .tl-root {
10026
+ /* Public hooks: --timeline-accent (colour). --tl-progress is written by the component. */
10027
+ --_tl-accent: var(--timeline-accent, var(--primary, oklch(0.7 0.13 285)));
10028
+ --_tl-cool: oklch(0.72 0.1 262);
10029
+ /* Light surfaces: the glow leans on the cool tint so a dark accent never reads as a smudge. */
10030
+ --_tl-glow: color-mix(in oklab, var(--_tl-accent) 40%, var(--_tl-cool));
10031
+ --_tl-hot: color-mix(in oklab, var(--_tl-accent) 45%, white);
10032
+ --_tl-core: var(--_tl-accent);
10033
+ --_tl-hair: rgba(0, 0, 0, 0.12);
10034
+ --_tl-ring: rgba(0, 0, 0, 0.2);
10035
+ --_tl-surface: var(--background, #fafafa);
10036
+ --_tl-muted: rgba(0, 0, 0, 0.42);
10037
+ --_tl-glow-a: 38%;
10038
+ --_tl-ease: cubic-bezier(0.16, 1, 0.3, 1);
10039
+ --_tl-x: 2rem;
10040
+ }
10041
+
10042
+ .dark .tl-root {
10043
+ --_tl-glow: color-mix(in oklab, var(--_tl-accent) 72%, var(--_tl-cool));
10044
+ --_tl-core: var(--_tl-hot);
10045
+ --_tl-hair: rgba(255, 255, 255, 0.12);
10046
+ --_tl-ring: rgba(255, 255, 255, 0.2);
10047
+ --_tl-muted: rgba(255, 255, 255, 0.38);
10048
+ --_tl-glow-a: 60%;
10049
+ }
10050
+
10051
+ /* ── Rail ─────────────────────────────────────────────── */
10052
+ .tl-root .tl-rail {
10053
+ position: absolute;
10054
+ top: 0;
10055
+ left: calc(var(--_tl-x) - 1px);
10056
+ width: 3px;
10057
+ pointer-events: none;
10058
+ -webkit-mask-image: linear-gradient(
10059
+ to bottom,
10060
+ transparent 0,
10061
+ #000 7%,
10062
+ #000 93%,
10063
+ transparent 100%
10064
+ );
10065
+ mask-image: linear-gradient(to bottom, transparent 0, #000 7%, #000 93%, transparent 100%);
10066
+ }
10067
+
10068
+ .tl-root .tl-rail-line,
10069
+ .tl-root .tl-trail {
10070
+ position: absolute;
10071
+ inset: 0 auto 0 1px;
10072
+ width: 1px;
10073
+ }
10074
+
10075
+ .tl-root .tl-rail-line {
10076
+ background: var(--_tl-hair);
10077
+ }
10078
+
10079
+ /* The filled part: faint far behind, brightening into the head, nothing past it. */
10080
+ .tl-root .tl-trail {
10081
+ background: linear-gradient(
10082
+ to bottom,
10083
+ transparent 0,
10084
+ color-mix(in oklab, var(--_tl-accent) 22%, transparent) calc(var(--tl-progress) - 320px),
10085
+ color-mix(in oklab, var(--_tl-glow) 85%, transparent) var(--tl-progress),
10086
+ transparent var(--tl-progress)
10087
+ );
10088
+ }
10089
+
10090
+ /* ── Head ─────────────────────────────────────────────── */
10091
+ .tl-root .tl-head {
10092
+ position: absolute;
10093
+ top: 0;
10094
+ left: calc(var(--_tl-x) + 0.5px);
10095
+ z-index: 50;
10096
+ width: 0;
10097
+ height: 0;
10098
+ pointer-events: none;
10099
+ transform: translate3d(0, var(--tl-progress), 0);
10100
+ opacity: 0;
10101
+ }
10102
+
10103
+ .tl-root .tl-head[data-ready="true"] {
10104
+ opacity: 1;
10105
+ }
10106
+
10107
+ .tl-root .tl-head > span {
10108
+ position: absolute;
10109
+ display: block;
10110
+ border-radius: 999px;
10111
+ }
10112
+
10113
+ .tl-root .tl-head-streak {
10114
+ left: -1px;
10115
+ top: -96px;
10116
+ width: 2px;
10117
+ height: 96px;
10118
+ background: linear-gradient(
10119
+ to top,
10120
+ var(--_tl-hot),
10121
+ color-mix(in oklab, var(--_tl-glow) 55%, transparent) 35%,
10122
+ transparent
10123
+ );
10124
+ filter: blur(0.6px);
10125
+ }
10126
+
10127
+ .tl-root .tl-head-halo {
10128
+ left: -28px;
10129
+ top: -28px;
10130
+ width: 56px;
10131
+ height: 56px;
10132
+ background: radial-gradient(
10133
+ closest-side,
10134
+ color-mix(in oklab, var(--_tl-glow) var(--_tl-glow-a), transparent),
10135
+ color-mix(in oklab, var(--_tl-glow) 14%, transparent) 55%,
10136
+ transparent
10137
+ );
10138
+ filter: blur(6px);
10139
+ }
10140
+
10141
+ .tl-root .tl-head-core {
10142
+ left: -3.5px;
10143
+ top: -3.5px;
10144
+ width: 7px;
10145
+ height: 7px;
10146
+ background: var(--_tl-core);
10147
+ box-shadow:
10148
+ 0 0 0 1.5px color-mix(in oklab, var(--_tl-hot) 45%, transparent),
10149
+ 0 0 10px 1px color-mix(in oklab, var(--_tl-glow) 70%, transparent);
10150
+ }
10151
+
10152
+ /* ── Dots ─────────────────────────────────────────────── */
10153
+ .tl-root .tl-dot {
10154
+ position: relative;
10155
+ display: grid;
10156
+ place-items: center;
10157
+ width: 15px;
10158
+ height: 15px;
10159
+ border-radius: 999px;
10160
+ border: 1px solid var(--_tl-ring);
10161
+ background: var(--_tl-surface);
10162
+ transform: scale(0.8);
10163
+ }
10164
+
10165
+ .tl-root .tl-dot-core {
10166
+ width: 7px;
10167
+ height: 7px;
10168
+ border-radius: 999px;
10169
+ background: var(--_tl-core);
10170
+ opacity: 0;
10171
+ transform: scale(0.2);
10172
+ }
10173
+
10174
+ .tl-root .tl-dot-flare {
10175
+ position: absolute;
10176
+ inset: -1px;
10177
+ border-radius: 999px;
10178
+ border: 1px solid color-mix(in oklab, var(--_tl-glow) 70%, transparent);
10179
+ opacity: 0;
10180
+ pointer-events: none;
10181
+ }
10182
+
10183
+ .tl-root .tl-dot[data-lit="true"] {
10184
+ transform: scale(1);
10185
+ border-color: color-mix(in oklab, var(--_tl-accent) 55%, var(--_tl-ring));
10186
+ box-shadow: 0 0 12px color-mix(in oklab, var(--_tl-glow) 40%, transparent);
10187
+ }
10188
+
10189
+ .tl-root .tl-dot[data-lit="true"] .tl-dot-core {
10190
+ opacity: 1;
10191
+ transform: scale(1);
10192
+ }
10193
+
10194
+ /* ── Labels ───────────────────────────────────────────── */
10195
+ /* Sits above the label out of flow, so the dot centres on the label itself. */
10196
+ .tl-root .tl-index {
10197
+ position: absolute;
10198
+ bottom: calc(100% + 0.2rem);
10199
+ display: block;
10200
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
10201
+ font-size: 0.6875rem;
10202
+ letter-spacing: 0.12em;
10203
+ color: var(--_tl-muted);
10204
+ transition: color 260ms ease;
10205
+ }
10206
+
10207
+ .tl-root .tl-heading {
10208
+ color: var(--_tl-muted);
10209
+ transition: color 260ms ease;
10210
+ }
10211
+
10212
+ .tl-root .tl-row[data-state="active"] .tl-heading {
10213
+ color: color-mix(in oklab, var(--_tl-accent) 28%, var(--foreground, #0a0a0a));
10214
+ }
10215
+
10216
+ .tl-root .tl-row[data-state="active"] .tl-index {
10217
+ color: color-mix(in oklab, var(--_tl-glow) 70%, var(--foreground, #0a0a0a));
10218
+ }
10219
+
10220
+ .tl-root .tl-row[data-state="past"] .tl-label-text,
10221
+ .tl-root .tl-row[data-state="past"] .tl-content {
10222
+ opacity: 0.5;
10223
+ }
10224
+
10225
+ /* ── Motion ───────────────────────────────────────────── */
10226
+ @media (prefers-reduced-motion: no-preference) {
10227
+ .tl-root[data-motion="full"] .tl-head {
10228
+ transition: opacity 500ms var(--_tl-ease);
10229
+ }
10230
+
10231
+ .tl-root[data-motion="full"] .tl-dot {
10232
+ transition:
10233
+ transform 260ms var(--_tl-ease),
10234
+ border-color 260ms ease,
10235
+ box-shadow 260ms ease;
10236
+ }
10237
+
10238
+ .tl-root[data-motion="full"] .tl-dot-core {
10239
+ transition:
10240
+ transform 260ms var(--_tl-ease),
10241
+ opacity 200ms ease;
10242
+ }
10243
+
10244
+ .tl-root[data-motion="full"] .tl-dot[data-lit="true"] .tl-dot-flare {
10245
+ animation: tl-ignite 700ms var(--_tl-ease) both;
10246
+ }
10247
+
10248
+ .tl-root[data-motion="full"] .tl-label-text,
10249
+ .tl-root[data-motion="full"] .tl-content {
10250
+ transition: opacity 300ms var(--_tl-ease);
10251
+ }
10252
+ }
10253
+
10254
+ @keyframes tl-ignite {
10255
+ from {
10256
+ opacity: 0.9;
10257
+ transform: scale(1);
10258
+ }
10259
+ to {
10260
+ opacity: 0;
10261
+ transform: scale(2.6);
10262
+ }
10263
+ }
10264
+
10265
+ /* ── Reduced motion: a plain filled rail, no glow, dots lit by position ── */
10266
+ .tl-root[data-motion="reduced"] .tl-trail {
10267
+ background: linear-gradient(
10268
+ to bottom,
10269
+ color-mix(in oklab, var(--_tl-accent) 70%, transparent) var(--tl-progress),
10270
+ transparent var(--tl-progress)
10271
+ );
10272
+ }
10273
+
10274
+ .tl-root[data-motion="reduced"] .tl-dot[data-lit="true"] {
10275
+ box-shadow: none;
10276
+ }
6936
10277
  /*
6937
10278
  * Declared locally, once, from the shared consumer-facing `--ft-accent` —
6938
10279
  * the same indirection `Button` (`--ft-btn-accent`) and `Popover`