@motir/design-system 0.1.2 → 0.2.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.
package/theme.css CHANGED
@@ -222,6 +222,35 @@
222
222
  --height-input: 44px;
223
223
  --height-control: 36px; /* single-line / square small control */
224
224
 
225
+ /* The space the collapsed nav rail puts AROUND its control: its `px-2`
226
+ gutters (8px a side), its 1px right border, and the 3px of slack the
227
+ shipped rail has always carried. Not the rail's width — the width is this
228
+ PLUS one `--height-control` square, composed at the point of use in
229
+ `components/ui/AppLayout.tsx` so the sum re-resolves under whatever
230
+ [data-style] is in scope rather than freezing the value `:root` happened
231
+ to have.
232
+
233
+ Calibrated so the DEFAULT style is unchanged: 36px + 20px is the 56px the
234
+ rail has always been. A denser style now gets a narrower rail and a roomier
235
+ one a wider rail, which is the density axis working rather than a constant
236
+ ignoring it. Deliberately NOT overridden per style — a style moves the
237
+ CONTROL, and the rail follows it (MOTIR-4232). */
238
+ --width-rail-chrome: 20px;
239
+
240
+ /* The inset a FLOATING rail is held away from the frame on all four sides, so
241
+ the shell canvas reads around it (MOTIR-4253). ZERO here, and that is the
242
+ point: the rail has a shared right EDGE in ten of the eleven styles, and an
243
+ edge is flush by definition. Only a style that turns the rail into a panel
244
+ with a shadow moves it.
245
+
246
+ It is composed into `AppLayout`'s two grid columns rather than applied to
247
+ the rail alone, because the rail FILLS its cell — a margin would shrink the
248
+ rail inside an unchanged column and take the collapsed rail's content box
249
+ below one `--height-control` square, which is exactly the invariant
250
+ MOTIR-4232 established. At `0px` both sums compile to the numbers every
251
+ other style already had. */
252
+ --spacing-rail-inset: 0px;
253
+
225
254
  /* ── Interaction ────────────────────────────────────────────────── */
226
255
  --transition-duration: 150ms;
227
256
  --transition-fast: 100ms;
@@ -349,7 +378,8 @@
349
378
  /* ═══════════════════════════════════════════════════════════════════════
350
379
  TIER 2 — Named-style overrides ([data-style], Axis 2)
351
380
  `warm-editorial` is the base (the Tier-0 defaults above — Notion-sober
352
- rectangles), so it needs no override block.
381
+ rectangles). Its explicit block repeats that shape/feel contract so a nested
382
+ base-style scope shadows a foreign active style instead of inheriting it.
353
383
  `soft-playful` adopts Figma's pill buttons + larger radius + softer shadows
354
384
  + roomier spacing + a gentler motion feel. Each later style (Swiss,
355
385
  Neo-Brutalism, Glassmorphism, …) adds its own `[data-style='<id>']` block
@@ -357,6 +387,70 @@
357
387
  motion / type) — NEVER a colour token (that is the data-palette axis).
358
388
  ═══════════════════════════════════════════════════════════════════════ */
359
389
 
390
+ [data-style='warm-editorial'] {
391
+ --radius-xs: 4px;
392
+ --radius-sm: 6px;
393
+ --radius-md: 8px;
394
+ --radius-lg: 12px;
395
+ --radius-xl: 16px;
396
+ --radius-pill: 9999px;
397
+ --radius-btn: 8px;
398
+ --radius-card: 12px;
399
+ --radius-input: 8px;
400
+ --radius-modal: 12px;
401
+ --radius-badge: 9999px;
402
+ --radius-control: 6px;
403
+ --radius-kbd: 4px;
404
+ --shadow-flat: none;
405
+ --shadow-subtle: 0 1px 2px rgba(15, 15, 15, 0.04);
406
+ --shadow-card: 0 4px 12px rgba(15, 15, 15, 0.08);
407
+ --shadow-elevated: 0 8px 24px rgba(15, 15, 15, 0.12);
408
+ --shadow-modal: 0 16px 48px -8px rgba(15, 15, 15, 0.16);
409
+ --shadow-hero-mockup: 0 24px 48px -8px rgba(15, 15, 15, 0.2);
410
+ --spacing-xxs: 4px;
411
+ --spacing-xs: 8px;
412
+ --spacing-sm: 12px;
413
+ --spacing-md: 16px;
414
+ --spacing-lg: 20px;
415
+ --spacing-xl: 24px;
416
+ --spacing-2xl: 32px;
417
+ --spacing-3xl: 40px;
418
+ --spacing-section: 64px;
419
+ --spacing-section-lg: 96px;
420
+ --spacing-hero: 120px;
421
+ --spacing-btn-x: 18px;
422
+ --spacing-btn-y: 10px;
423
+ --spacing-btn-x-sm: 12px;
424
+ --spacing-btn-y-sm: 8px;
425
+ --spacing-input-x: 16px;
426
+ --spacing-input-y: 12px;
427
+ --spacing-card-padding: 24px;
428
+ --spacing-control-x: 10px;
429
+ --spacing-control-y: 6px;
430
+ --spacing-icon-btn: 4px;
431
+ --spacing-chip-x: 10px;
432
+ --spacing-chip-y: 2px;
433
+ --spacing-kbd-x: 6px;
434
+ --spacing-kbd-y: 2px;
435
+ --spacing-tooltip-x: 8px;
436
+ --spacing-tooltip-y: 4px;
437
+ /* The rail keeps its shared edge under this style, so it is flush — the
438
+ default's mirror of the Tier-0 value, not an override (MOTIR-4253). */
439
+ --spacing-rail-inset: 0px;
440
+ --height-btn-sm: 32px;
441
+ --height-btn-md: 40px;
442
+ --height-btn-lg: 48px;
443
+ --height-input: 44px;
444
+ --height-control: 36px;
445
+ --transition-duration: 150ms;
446
+ --transition-fast: 100ms;
447
+ --transition-slow: 250ms;
448
+ --hover-scale: 1;
449
+ --active-scale: 0.98;
450
+ --focus-ring-width: 2px;
451
+ --focus-ring-offset: 2px;
452
+ }
453
+
360
454
  [data-style='soft-playful'] {
361
455
  /* Figma's pill geometry */
362
456
  --radius-btn: 9999px; /* pill */
@@ -548,80 +642,132 @@
548
642
  they are NOT the bare [data-style] token block and never carry a hue token
549
643
  there — the disjoint-axis token-block test still passes. ───────────────── */
550
644
 
645
+ /* ═══════════════════════════════════════════════════════════════════════
646
+ ⚠️ A STYLE CANVAS IS PAINTED ON `body` AND ON `[data-app-shell]`, FROM ONE
647
+ RULE (MOTIR-4230 for 3D / Immersive, MOTIR-4234 for the rest)
648
+ ═══════════════════════════════════════════════════════════════════════
649
+ Six of the eleven registered styles express part of their identity as a
650
+ `body`-level CANVAS: glassmorphism's vibrant wash, cybercore's tech grid,
651
+ aurora's drifting ribbons, 3D / Immersive's atmosphere, neumorphism's
652
+ moulded field and retrofuturism's synthwave sky. `body` alone covers the
653
+ anonymous surfaces — auth, the public site, `/tokens` — where nothing is
654
+ stacked over the canvas. On a SIGNED-IN route it covers nothing at all: the
655
+ shell root (`components/ui/AppLayout.tsx`, and the platform-admin
656
+ `AdminShell`) is a `h-dvh overflow-hidden` box carrying `bg-(--el-page-bg)`,
657
+ an OPAQUE fill over the whole viewport. The canvas was painted and then
658
+ hidden, so a user who picked one of these styles got the surface treatment
659
+ on individual panels and a flat frame around them — and glass is the sharpest
660
+ case, because the thing its frosted rail was blurring was that flat fill
661
+ rather than the wash the blur exists to refract.
662
+
663
+ So each of those rules names TWO members, `body` and the `data-app-shell`
664
+ hook both shells carry, and the shell repaints the SAME layers over its own
665
+ page fill. ONE RULE, TWO SELECTORS, DELIBERATELY — a second rule restating a
666
+ gradient stack is a copy that drifts on the first tune, and the whole defect
667
+ was the canvas and the frame disagreeing about what the canvas is. Where the
668
+ body rule fixes `background-attachment`, both members inherit it from the one
669
+ declaration, which is what makes the two pixel-identical rather than merely
670
+ similar: each resolves against the viewport instead of against its own box.
671
+
672
+ A style that paints NO canvas of its own (`warm-editorial`, `soft-playful`,
673
+ `swiss-minimal-flat`, `neo-brutalism`, `hand-drawn-indie`) is untouched by
674
+ all of this and the shell keeps the `--el-page-bg` fill it has always had:
675
+ the hook is a hook, not a style.
676
+
677
+ `tests/theme/styleShellCanvas.test.ts` pins the pairing for EVERY style that
678
+ paints a canvas — derived from the stylesheet, not from a list — and
679
+ `tests/e2e/shell-immersive-atmosphere.spec.ts` is the rendered half.
680
+ ═══════════════════════════════════════════════════════════════════════ */
681
+
551
682
  /* The canvas: the palette's page bg + three soft corner washes. The hue sources
552
683
  are the `--el-*` accent roles (NOT Tier-0 `--color-*`), so the washes re-tint
553
684
  with the active palette — the `data-palette` axis (7.3.48 / MOTIR-1065)
554
685
  overrides `--el-*`, never `--color-*`, so reading `--el-*` here is what keeps
555
686
  the glass orthogonal to the palette. The gradient is what the frosted panels
556
687
  blur — without it, the blur is invisible. */
557
- [data-style='glassmorphism'] body {
558
- background-image:
559
- radial-gradient(
560
- 85% 80% at 6% -12%,
561
- color-mix(in srgb, var(--el-accent) var(--glass-wash), transparent),
562
- transparent 62%
563
- ),
564
- radial-gradient(
565
- 80% 75% at 110% 4%,
566
- color-mix(in srgb, var(--el-highlight) var(--glass-wash), transparent),
567
- transparent 60%
568
- ),
569
- radial-gradient(
570
- 95% 90% at 50% 120%,
571
- color-mix(in srgb, var(--el-link) var(--glass-wash), transparent),
572
- transparent 62%
573
- ),
574
- radial-gradient(
575
- 60% 60% at 85% 95%,
576
- color-mix(in srgb, var(--el-accent) var(--glass-wash), transparent),
577
- transparent 60%
578
- );
579
- background-attachment: fixed;
688
+ @scope ([data-style='glassmorphism']) to ([data-style]) {
689
+ body,
690
+ [data-app-shell] {
691
+ background-image:
692
+ radial-gradient(
693
+ 85% 80% at 6% -12%,
694
+ color-mix(in srgb, var(--el-accent) var(--glass-wash), transparent),
695
+ transparent 62%
696
+ ),
697
+ radial-gradient(
698
+ 80% 75% at 110% 4%,
699
+ color-mix(in srgb, var(--el-highlight) var(--glass-wash), transparent),
700
+ transparent 60%
701
+ ),
702
+ radial-gradient(
703
+ 95% 90% at 50% 120%,
704
+ color-mix(in srgb, var(--el-link) var(--glass-wash), transparent),
705
+ transparent 62%
706
+ ),
707
+ radial-gradient(
708
+ 60% 60% at 85% 95%,
709
+ color-mix(in srgb, var(--el-accent) var(--glass-wash), transparent),
710
+ transparent 60%
711
+ );
712
+ background-attachment: fixed;
713
+ }
580
714
  }
581
715
 
582
716
  /* Frosted container surfaces — cards & popovers float as translucent sheets. */
583
- [data-style='glassmorphism'] [data-surface='card'],
584
- [data-style='glassmorphism'] [data-surface='popover'] {
585
- /* The translucent fill lets the vibrant canvas through; the linear sheen is
586
- the light raking across the glass (top-left fading), the inset RIM is the
587
- lit glass edge, and the soft drop makes the sheet FLOAT. White light is
588
- palette-agnostic; the colour still comes only from the palette-derived
589
- fill/border, so the axes stay disjoint. */
590
- background-color: color-mix(in srgb, var(--el-page-bg) var(--glass-surface-alpha), transparent);
591
- background-image: linear-gradient(150deg, var(--glass-sheen), transparent 55%);
592
- border-color: color-mix(in srgb, var(--el-border) var(--glass-border-alpha), transparent);
593
- box-shadow:
594
- inset 0 1px 0 0 var(--glass-rim),
595
- inset 0 0 0 1px rgba(255, 255, 255, 0.06),
596
- 0 8px 32px -8px rgba(15, 15, 15, 0.18);
597
- -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
598
- backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
717
+ @scope ([data-style='glassmorphism']) to ([data-style]) {
718
+ [data-surface='card'],
719
+ [data-surface='popover'] {
720
+ /* The translucent fill lets the vibrant canvas through; the linear sheen is
721
+ the light raking across the glass (top-left fading), the inset RIM is the
722
+ lit glass edge, and the soft drop makes the sheet FLOAT. White light is
723
+ palette-agnostic; the colour still comes only from the palette-derived
724
+ fill/border, so the axes stay disjoint. */
725
+ background-color: color-mix(in srgb, var(--el-page-bg) var(--glass-surface-alpha), transparent);
726
+ background-image: linear-gradient(150deg, var(--glass-sheen), transparent 55%);
727
+ border-color: color-mix(in srgb, var(--el-border) var(--glass-border-alpha), transparent);
728
+ box-shadow:
729
+ inset 0 1px 0 0 var(--glass-rim),
730
+ inset 0 0 0 1px rgba(255, 255, 255, 0.06),
731
+ 0 8px 32px -8px rgba(15, 15, 15, 0.18);
732
+ -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
733
+ backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
734
+ }
599
735
  }
600
736
 
601
737
  /* Modal panel — a touch more opaque + a stronger blur (body-text AA + lift). */
602
- [data-style='glassmorphism'] [data-surface='modal'] {
603
- background-color: color-mix(in srgb, var(--el-page-bg) var(--glass-modal-alpha), transparent);
604
- background-image: linear-gradient(150deg, var(--glass-sheen), transparent 55%);
605
- border-color: color-mix(in srgb, var(--el-border) var(--glass-border-alpha), transparent);
606
- box-shadow:
607
- inset 0 1px 0 0 var(--glass-rim),
608
- 0 28px 70px -16px rgba(15, 15, 15, 0.34);
609
- -webkit-backdrop-filter: blur(var(--glass-blur-strong)) saturate(var(--glass-saturate));
610
- backdrop-filter: blur(var(--glass-blur-strong)) saturate(var(--glass-saturate));
738
+ @scope ([data-style='glassmorphism']) to ([data-style]) {
739
+ [data-surface='modal'] {
740
+ background-color: color-mix(in srgb, var(--el-page-bg) var(--glass-modal-alpha), transparent);
741
+ background-image: linear-gradient(150deg, var(--glass-sheen), transparent 55%);
742
+ border-color: color-mix(in srgb, var(--el-border) var(--glass-border-alpha), transparent);
743
+ box-shadow:
744
+ inset 0 1px 0 0 var(--glass-rim),
745
+ 0 28px 70px -16px rgba(15, 15, 15, 0.34);
746
+ -webkit-backdrop-filter: blur(var(--glass-blur-strong)) saturate(var(--glass-saturate));
747
+ backdrop-filter: blur(var(--glass-blur-strong)) saturate(var(--glass-saturate));
748
+ }
611
749
  }
612
750
 
613
751
  /* The sidebar rail — frosted chrome over the canvas. */
614
- [data-style='glassmorphism'] [data-surface='sidebar'] {
615
- background-color: color-mix(in srgb, var(--el-sidebar-bg) var(--glass-chrome-alpha), transparent);
616
- -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
617
- backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
752
+ @scope ([data-style='glassmorphism']) to ([data-style]) {
753
+ [data-surface='sidebar'] {
754
+ background-color: color-mix(
755
+ in srgb,
756
+ var(--el-sidebar-bg) var(--glass-chrome-alpha),
757
+ transparent
758
+ );
759
+ -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
760
+ backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
761
+ }
618
762
  }
619
763
 
620
764
  /* Form controls — a translucent fill so inputs read as glass too. */
621
- [data-style='glassmorphism'] [data-surface='input'] {
622
- background-color: color-mix(in srgb, var(--el-page-bg) var(--glass-input-alpha), transparent);
623
- -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
624
- backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
765
+ @scope ([data-style='glassmorphism']) to ([data-style]) {
766
+ [data-surface='input'] {
767
+ background-color: color-mix(in srgb, var(--el-page-bg) var(--glass-input-alpha), transparent);
768
+ -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
769
+ backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
770
+ }
625
771
  }
626
772
 
627
773
  /* The MODAL / command-palette BACKDROP — the most important glass surface, since
@@ -629,10 +775,12 @@
629
775
  light, palette-derived veil + a page BLUR, so the whole page behind frosts and
630
776
  the (also-frosted) modal panel refracts it — the proper glass-modal look.
631
777
  Overrides the primitives' `bg-black/40` utility. Palette-derived (no hue). */
632
- [data-style='glassmorphism'] [data-surface='overlay'] {
633
- background-color: color-mix(in srgb, var(--el-page-bg) 24%, transparent);
634
- -webkit-backdrop-filter: blur(8px) saturate(var(--glass-saturate));
635
- backdrop-filter: blur(8px) saturate(var(--glass-saturate));
778
+ @scope ([data-style='glassmorphism']) to ([data-style]) {
779
+ [data-surface='overlay'] {
780
+ background-color: color-mix(in srgb, var(--el-page-bg) 24%, transparent);
781
+ -webkit-backdrop-filter: blur(8px) saturate(var(--glass-saturate));
782
+ backdrop-filter: blur(8px) saturate(var(--glass-saturate));
783
+ }
636
784
  }
637
785
 
638
786
  /* ─────────────────────────────────────────────────────────────────────────
@@ -711,8 +859,10 @@
711
859
  descendant rule, not a bare token block, so the disjoint-axis test (which
712
860
  inspects token blocks) does not treat it as one — and it sets no colour
713
861
  anyway, so the two axes stay disjoint regardless. */
714
- [data-style='neo-brutalism'] .border {
715
- border-width: 2px;
862
+ @scope ([data-style='neo-brutalism']) to ([data-style]) {
863
+ .border {
864
+ border-width: 2px;
865
+ }
716
866
  }
717
867
 
718
868
  /* ─────────────────────────────────────────────────────────────────────────
@@ -795,12 +945,15 @@
795
945
  so it adapts to the active palette and sits BEHIND content (text never
796
946
  renders on it), keeping AA intact. Dark-first — on the dark theme this reads
797
947
  as a Tron grid over the near-black canvas. */
798
- [data-style='cybercore-y2k'] body {
799
- background-image:
800
- linear-gradient(color-mix(in srgb, currentColor 6%, transparent) 1px, transparent 1px),
801
- linear-gradient(90deg, color-mix(in srgb, currentColor 6%, transparent) 1px, transparent 1px);
802
- background-size: 32px 32px;
803
- background-position: center top;
948
+ @scope ([data-style='cybercore-y2k']) to ([data-style]) {
949
+ body,
950
+ [data-app-shell] {
951
+ background-image:
952
+ linear-gradient(color-mix(in srgb, currentColor 6%, transparent) 1px, transparent 1px),
953
+ linear-gradient(90deg, color-mix(in srgb, currentColor 6%, transparent) 1px, transparent 1px);
954
+ background-size: 32px 32px;
955
+ background-position: center top;
956
+ }
804
957
  }
805
958
 
806
959
  /* ─────────────────────────────────────────────────────────────────────────
@@ -897,31 +1050,34 @@
897
1050
  `--el-*`, never `--color-*`, which is what keeps the aurora orthogonal to the
898
1051
  palette. background-size > 100% gives the gradient room to drift; the whole
899
1052
  field eases back and forth over `--aurora-drift-duration`. */
900
- [data-style='aurora'] body {
901
- background-image:
902
- radial-gradient(
903
- 58% 75% at 12% 0%,
904
- color-mix(in srgb, var(--el-accent) var(--aurora-wash), transparent),
905
- transparent 60%
906
- ),
907
- radial-gradient(
908
- 54% 70% at 92% 8%,
909
- color-mix(in srgb, var(--el-highlight) var(--aurora-wash), transparent),
910
- transparent 58%
911
- ),
912
- radial-gradient(
913
- 68% 82% at 62% 104%,
914
- color-mix(in srgb, var(--el-link) var(--aurora-wash), transparent),
915
- transparent 62%
916
- ),
917
- radial-gradient(
918
- 48% 64% at 34% 58%,
919
- color-mix(in srgb, var(--el-info) var(--aurora-wash-soft), transparent),
920
- transparent 55%
921
- );
922
- background-size: 180% 180%;
923
- background-attachment: fixed;
924
- animation: aurora-drift var(--aurora-drift-duration) ease-in-out infinite alternate;
1053
+ @scope ([data-style='aurora']) to ([data-style]) {
1054
+ body,
1055
+ [data-app-shell] {
1056
+ background-image:
1057
+ radial-gradient(
1058
+ 58% 75% at 12% 0%,
1059
+ color-mix(in srgb, var(--el-accent) var(--aurora-wash), transparent),
1060
+ transparent 60%
1061
+ ),
1062
+ radial-gradient(
1063
+ 54% 70% at 92% 8%,
1064
+ color-mix(in srgb, var(--el-highlight) var(--aurora-wash), transparent),
1065
+ transparent 58%
1066
+ ),
1067
+ radial-gradient(
1068
+ 68% 82% at 62% 104%,
1069
+ color-mix(in srgb, var(--el-link) var(--aurora-wash), transparent),
1070
+ transparent 62%
1071
+ ),
1072
+ radial-gradient(
1073
+ 48% 64% at 34% 58%,
1074
+ color-mix(in srgb, var(--el-info) var(--aurora-wash-soft), transparent),
1075
+ transparent 55%
1076
+ );
1077
+ background-size: 180% 180%;
1078
+ background-attachment: fixed;
1079
+ animation: aurora-drift var(--aurora-drift-duration) ease-in-out infinite alternate;
1080
+ }
925
1081
  }
926
1082
 
927
1083
  @keyframes aurora-drift {
@@ -942,10 +1098,16 @@
942
1098
  }
943
1099
 
944
1100
  /* Respect a reduced-motion preference — the aurora settles to a still gradient
945
- (the identity holds; only the drift stops). */
1101
+ (the identity holds; only the drift stops). BOTH members, for the reason the
1102
+ canvas has two: the shell now carries the same `animation` declaration, so an
1103
+ arm that stilled only `body` would leave a reduced-motion user with a drifting
1104
+ frame around a still page — the defect inverted rather than fixed. */
946
1105
  @media (prefers-reduced-motion: reduce) {
947
- [data-style='aurora'] body {
948
- animation: none;
1106
+ @scope ([data-style='aurora']) to ([data-style]) {
1107
+ body,
1108
+ [data-app-shell] {
1109
+ animation: none;
1110
+ }
949
1111
  }
950
1112
  }
951
1113
 
@@ -955,43 +1117,49 @@
955
1117
  rather than lifted by a hard shadow. The sheen sits on background-image over
956
1118
  the opaque background-color, so it never pulls the surface far enough from its
957
1119
  base tone to threaten contrast. */
958
- [data-style='aurora'] [data-surface='card'],
959
- [data-style='aurora'] [data-surface='popover'] {
960
- background-color: var(--el-surface);
961
- background-image: linear-gradient(
962
- 180deg,
963
- color-mix(in srgb, var(--el-highlight) var(--aurora-sheen), transparent),
964
- transparent 42%
965
- );
966
- box-shadow:
967
- var(--shadow-card),
968
- 0 0 26px -8px color-mix(in srgb, var(--el-accent) var(--aurora-glow), transparent),
969
- 0 0 40px -14px color-mix(in srgb, var(--el-link) var(--aurora-glow), transparent);
1120
+ @scope ([data-style='aurora']) to ([data-style]) {
1121
+ [data-surface='card'],
1122
+ [data-surface='popover'] {
1123
+ background-color: var(--el-surface);
1124
+ background-image: linear-gradient(
1125
+ 180deg,
1126
+ color-mix(in srgb, var(--el-highlight) var(--aurora-sheen), transparent),
1127
+ transparent 42%
1128
+ );
1129
+ box-shadow:
1130
+ var(--shadow-card),
1131
+ 0 0 26px -8px color-mix(in srgb, var(--el-accent) var(--aurora-glow), transparent),
1132
+ 0 0 40px -14px color-mix(in srgb, var(--el-link) var(--aurora-glow), transparent);
1133
+ }
970
1134
  }
971
1135
 
972
1136
  /* Modal panel — opaque, with a deeper glow so the dialog separates cleanly from
973
1137
  the drifting canvas behind it (and body text stays on the solid surface). */
974
- [data-style='aurora'] [data-surface='modal'] {
975
- background-color: var(--el-surface);
976
- background-image: linear-gradient(
977
- 180deg,
978
- color-mix(in srgb, var(--el-highlight) var(--aurora-sheen), transparent),
979
- transparent 38%
980
- );
981
- box-shadow:
982
- var(--shadow-modal),
983
- 0 0 60px -16px color-mix(in srgb, var(--el-accent) var(--aurora-glow-strong), transparent);
1138
+ @scope ([data-style='aurora']) to ([data-style]) {
1139
+ [data-surface='modal'] {
1140
+ background-color: var(--el-surface);
1141
+ background-image: linear-gradient(
1142
+ 180deg,
1143
+ color-mix(in srgb, var(--el-highlight) var(--aurora-sheen), transparent),
1144
+ transparent 38%
1145
+ );
1146
+ box-shadow:
1147
+ var(--shadow-modal),
1148
+ 0 0 60px -16px color-mix(in srgb, var(--el-accent) var(--aurora-glow-strong), transparent);
1149
+ }
984
1150
  }
985
1151
 
986
1152
  /* The sidebar rail — opaque chrome carrying the faint aurora sheen so it reads as
987
1153
  part of the lit surface family, not a flat slab. */
988
- [data-style='aurora'] [data-surface='sidebar'] {
989
- background-color: var(--el-sidebar-bg);
990
- background-image: linear-gradient(
991
- 180deg,
992
- color-mix(in srgb, var(--el-accent) var(--aurora-sheen), transparent),
993
- transparent 30%
994
- );
1154
+ @scope ([data-style='aurora']) to ([data-style]) {
1155
+ [data-surface='sidebar'] {
1156
+ background-color: var(--el-sidebar-bg);
1157
+ background-image: linear-gradient(
1158
+ 180deg,
1159
+ color-mix(in srgb, var(--el-accent) var(--aurora-sheen), transparent),
1160
+ transparent 30%
1161
+ );
1162
+ }
995
1163
  }
996
1164
 
997
1165
  /* ─────────────────────────────────────────────────────────────────────────
@@ -1067,6 +1235,13 @@
1067
1235
  --height-control: 40px;
1068
1236
  --height-input: 46px;
1069
1237
 
1238
+ /* The rail FLOATS in this style, so it is held off the frame on all four
1239
+ sides and the immersive atmosphere reads around it (MOTIR-4253). The base
1240
+ layer is `0px`, so this is the only override — every other style's rail
1241
+ stays flush against its shared edge, and both of `AppLayout`'s grid sums
1242
+ compile to the numbers they have today. */
1243
+ --spacing-rail-inset: 10px;
1244
+
1070
1245
  /* Motion — smooth and floaty. A slower ease for the depth to feel weighty.
1071
1246
  The signature INTERACTION is the pointer-parallax TILT below (the standard
1072
1247
  "3D card" effect — cards tip toward the cursor), driven by the
@@ -1085,25 +1260,46 @@
1085
1260
  never a raw hue), so the style/palette axes stay disjoint and the material-
1086
1261
  rule test passes: a palette swap re-tints the atmosphere, a style swap leaves
1087
1262
  hues alone. This is what makes the table/list/board pages (no card surfaces of
1088
- their own) read as immersive too. */
1089
- [data-style='3d-immersive'] body {
1090
- background-image:
1091
- radial-gradient(
1092
- 130% 85% at 50% -15%,
1093
- color-mix(in srgb, var(--el-accent) 9%, transparent),
1094
- transparent 60%
1095
- ),
1096
- radial-gradient(
1097
- 95% 75% at 108% 112%,
1098
- color-mix(in srgb, var(--el-link) 8%, transparent),
1099
- transparent 55%
1100
- ),
1101
- radial-gradient(
1102
- 150% 130% at 50% 42%,
1103
- transparent 52%,
1104
- color-mix(in srgb, var(--el-text) 8%, transparent)
1105
- );
1106
- background-attachment: fixed;
1263
+ their own) read as immersive too.
1264
+
1265
+ ⚠️ IT IS PAINTED ON THE SHELL CANVAS AS WELL AS ON `body`, AND THAT IS WHY THE
1266
+ SELECTOR LIST HAS TWO MEMBERS (MOTIR-4230). `body` alone covers the anonymous
1267
+ surfaces auth, the public site, `/tokens` — where nothing is stacked over the
1268
+ canvas. On a SIGNED-IN route the shell root (`components/ui/AppLayout.tsx`, and
1269
+ the platform-admin `AdminShell`) is a `h-dvh` box carrying `bg-(--el-page-bg)`,
1270
+ an OPAQUE fill covering the whole viewport: the body's atmosphere was painted
1271
+ and then hidden behind it, so every card had its 3D tokens while the frame they
1272
+ sit in stayed flat — the style did not read as immersive anywhere a user
1273
+ actually works. The shells therefore carry a `data-app-shell` hook and repaint
1274
+ the SAME layers over their own page fill, which is what the body would have
1275
+ shown. `background-attachment: fixed` on both members is what makes the two
1276
+ pixel-identical: each resolves against the viewport, not against its own box.
1277
+
1278
+ ONE RULE, TWO SELECTORS, DELIBERATELY — a second rule restating this gradient
1279
+ stack is a copy that drifts on the first tune, and the whole defect was the
1280
+ canvas and the frame disagreeing about what the canvas is. `tests/theme/
1281
+ immersiveShellAtmosphere.test.ts` pins the pairing and the hook. */
1282
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1283
+ body,
1284
+ [data-app-shell] {
1285
+ background-image:
1286
+ radial-gradient(
1287
+ 130% 85% at 50% -15%,
1288
+ color-mix(in srgb, var(--el-accent) 9%, transparent),
1289
+ transparent 60%
1290
+ ),
1291
+ radial-gradient(
1292
+ 95% 75% at 108% 112%,
1293
+ color-mix(in srgb, var(--el-link) 8%, transparent),
1294
+ transparent 55%
1295
+ ),
1296
+ radial-gradient(
1297
+ 150% 130% at 50% 42%,
1298
+ transparent 52%,
1299
+ color-mix(in srgb, var(--el-text) 8%, transparent)
1300
+ );
1301
+ background-attachment: fixed;
1302
+ }
1107
1303
  }
1108
1304
 
1109
1305
  /* EVERY PANEL FLOATS — systematic depth across the WHOLE app, not just the
@@ -1118,11 +1314,15 @@
1118
1314
  Per-surface exceptions (the board column's clip-safe shadow, modals) override
1119
1315
  below by source order. Modals carry their own deeper shadow via the modal
1120
1316
  radius (next rule). */
1121
- [data-style='3d-immersive'] .rounded-\(--radius-card\) {
1122
- box-shadow: var(--shadow-card);
1317
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1318
+ .rounded-\(--radius-card\) {
1319
+ box-shadow: var(--shadow-card);
1320
+ }
1123
1321
  }
1124
- [data-style='3d-immersive'] .rounded-\(--radius-modal\) {
1125
- box-shadow: var(--shadow-modal);
1322
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1323
+ .rounded-\(--radius-modal\) {
1324
+ box-shadow: var(--shadow-modal);
1325
+ }
1126
1326
  }
1127
1327
 
1128
1328
  /* 3D / Immersive — the POINTER-PARALLAX TILT (the standard "3D card" effect,
@@ -1147,10 +1347,12 @@
1147
1347
  never establishes a transform stacking context that could reposition a
1148
1348
  non-portaled fixed/absolute descendant. On leave the engine drops the
1149
1349
  attribute and the transform eases back to none over 400ms. */
1150
- [data-style='3d-immersive'] [data-tilt] {
1151
- transition:
1152
- transform 400ms cubic-bezier(0.22, 1, 0.36, 1),
1153
- box-shadow 260ms ease-out;
1350
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1351
+ [data-tilt] {
1352
+ transition:
1353
+ transform 400ms cubic-bezier(0.22, 1, 0.36, 1),
1354
+ box-shadow 260ms ease-out;
1355
+ }
1154
1356
  }
1155
1357
  /* While the pointer is over the tile the engine sets data-tilt-active='1':
1156
1358
  tip it toward the cursor (the per-tile --tilt-rx/ry the engine writes),
@@ -1159,14 +1361,16 @@
1159
1361
  establish a 3D context (`preserve-3d`) so the card's slots can sit on
1160
1362
  separate translateZ planes and PARALLAX (a flat-plane tilt without this is
1161
1363
  the "halfway / flat-3D mix" tell). `position: relative` anchors the glare. */
1162
- [data-style='3d-immersive'] [data-tilt][data-tilt-active='1'] {
1163
- transform: perspective(var(--tilt-perspective, 900px)) rotateX(var(--tilt-rx, 0deg))
1164
- rotateY(var(--tilt-ry, 0deg));
1165
- transform-style: preserve-3d;
1166
- transition:
1167
- transform 80ms linear,
1168
- box-shadow 260ms ease-out;
1169
- box-shadow: var(--shadow-elevated);
1364
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1365
+ [data-tilt][data-tilt-active='1'] {
1366
+ transform: perspective(var(--tilt-perspective, 900px)) rotateX(var(--tilt-rx, 0deg))
1367
+ rotateY(var(--tilt-ry, 0deg));
1368
+ transform-style: preserve-3d;
1369
+ transition:
1370
+ transform 80ms linear,
1371
+ box-shadow 260ms ease-out;
1372
+ box-shadow: var(--shadow-elevated);
1373
+ }
1170
1374
  }
1171
1375
 
1172
1376
  /* DEPTH PLANES — the card's slots lift onto different translateZ planes so
@@ -1175,13 +1379,17 @@
1175
1379
  content never renders skewed). Only while actively tilting, so a resting
1176
1380
  card is perfectly flat. This is the standard layered-parallax technique
1177
1381
  (vanilla-tilt / Atropos): perspective + preserve-3d + per-layer translateZ. */
1178
- [data-style='3d-immersive'] [data-tilt][data-tilt-active='1'] [data-tilt-layer='front'] {
1179
- transform: translateZ(42px);
1180
- transition: transform 80ms linear;
1382
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1383
+ [data-tilt][data-tilt-active='1'] [data-tilt-layer='front'] {
1384
+ transform: translateZ(42px);
1385
+ transition: transform 80ms linear;
1386
+ }
1181
1387
  }
1182
- [data-style='3d-immersive'] [data-tilt][data-tilt-active='1'] [data-tilt-layer='back'] {
1183
- transform: translateZ(14px);
1184
- transition: transform 80ms linear;
1388
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1389
+ [data-tilt][data-tilt-active='1'] [data-tilt-layer='back'] {
1390
+ transform: translateZ(14px);
1391
+ transition: transform 80ms linear;
1392
+ }
1185
1393
  }
1186
1394
 
1187
1395
  /* GLARE — a cursor-tracked specular sweep across the face (the light catching
@@ -1189,33 +1397,39 @@
1189
1397
  hue → passes the material-rule test; a palette swap re-tints the sheen). It
1190
1398
  rides a high translateZ so it reads as light on the front glass, fades in
1191
1399
  only while active, and never eats pointer events. */
1192
- [data-style='3d-immersive'] [data-tilt]::after {
1193
- content: '';
1194
- position: absolute;
1195
- inset: 0;
1196
- border-radius: inherit;
1197
- background: radial-gradient(
1198
- circle at var(--tilt-glare-x, 50%) var(--tilt-glare-y, 50%),
1199
- color-mix(in srgb, var(--el-page-bg) 65%, transparent),
1200
- transparent 55%
1201
- );
1202
- opacity: 0;
1203
- transition: opacity 200ms ease-out;
1204
- pointer-events: none;
1400
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1401
+ [data-tilt]::after {
1402
+ content: '';
1403
+ position: absolute;
1404
+ inset: 0;
1405
+ border-radius: inherit;
1406
+ background: radial-gradient(
1407
+ circle at var(--tilt-glare-x, 50%) var(--tilt-glare-y, 50%),
1408
+ color-mix(in srgb, var(--el-page-bg) 65%, transparent),
1409
+ transparent 55%
1410
+ );
1411
+ opacity: 0;
1412
+ transition: opacity 200ms ease-out;
1413
+ pointer-events: none;
1414
+ }
1205
1415
  }
1206
- [data-style='3d-immersive'] [data-tilt][data-tilt-active='1']::after {
1207
- opacity: 0.45;
1208
- /* Just above the card FACE but BELOW the popped header/footer planes, so it
1209
- lights the surface without washing the floating title. */
1210
- transform: translateZ(2px);
1416
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1417
+ [data-tilt][data-tilt-active='1']::after {
1418
+ opacity: 0.45;
1419
+ /* Just above the card FACE but BELOW the popped header/footer planes, so it
1420
+ lights the surface without washing the floating title. */
1421
+ transform: translateZ(2px);
1422
+ }
1211
1423
  }
1212
1424
  }
1213
1425
 
1214
1426
  /* `position: relative` anchors the glare ::after to each tile. Static (outside
1215
1427
  the motion gate) so the anchor is stable; the glare itself only shows while
1216
1428
  tilting (above). */
1217
- [data-style='3d-immersive'] [data-tilt] {
1218
- position: relative;
1429
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1430
+ [data-tilt] {
1431
+ position: relative;
1432
+ }
1219
1433
  }
1220
1434
 
1221
1435
  /* ── 3D / Immersive — BOARD COLUMNS ─────────────────────────────────────────
@@ -1226,33 +1440,37 @@
1226
1440
  clip/occlusion-safe float shadow (small horizontal bleed, modest drop), and
1227
1441
  the scroll row widens its gap + adds bottom room so the shadow shows. Result:
1228
1442
  each column floats as its own distinct card. */
1229
- [data-style='3d-immersive'] [data-board-col-panel] {
1230
- box-shadow:
1231
- inset 0 1px 0 0 rgba(255, 255, 255, 0.55),
1232
- 0 1px 3px rgba(15, 15, 15, 0.08),
1233
- 0 8px 18px -8px rgba(15, 15, 15, 0.22);
1234
- /* The column is `h-[calc(100dvh - 10.5rem - var(--shell-bottom-clearance))]` —
1235
- sized to fill the board area. In the `overflow-x-auto` row that
1236
- exactly-filling height means the column's rounded BOTTOM corner + its drop
1237
- shadow sit at the overflow-clip boundary and get sheared flat (the reported
1238
- "sharp bottom corner"). Trim the height by a further 2.5rem so the rounded
1239
- corner + shadow clear the clip and the column reads as a fully-rounded
1240
- floating card. Also clip the column's OWN children to its radius so a flush
1241
- inner surface can't square a corner the board cards pop on hover but the
1242
- DragOverlay clone is portaled, so nothing 3D is lost.
1243
-
1244
- ⚠️ This tracks BoardColumn's height and must keep spending the same
1245
- `--shell-bottom-clearance` the shell sets (MOTIR-2763), not a flat number:
1246
- a stylesheet cannot read the `showPlanWithAi` prop, so the custom property
1247
- is the only way this stays conditional on the orb actually mounting. The
1248
- 13rem below is 10.5rem of chrome + this style's own 2.5rem trim. */
1249
- height: calc(100dvh - 13rem - var(--shell-bottom-clearance, 1.5rem));
1250
- overflow: hidden;
1251
- }
1252
- [data-style='3d-immersive'] [data-board-scroll] {
1253
- gap: 1.5rem;
1254
- padding-bottom: 2.25rem;
1255
- padding-top: 0.25rem;
1443
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1444
+ [data-board-col-panel] {
1445
+ box-shadow:
1446
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.55),
1447
+ 0 1px 3px rgba(15, 15, 15, 0.08),
1448
+ 0 8px 18px -8px rgba(15, 15, 15, 0.22);
1449
+ /* The column is `h-[calc(100dvh - 10.5rem - var(--shell-bottom-clearance))]`
1450
+ sized to fill the board area. In the `overflow-x-auto` row that
1451
+ exactly-filling height means the column's rounded BOTTOM corner + its drop
1452
+ shadow sit at the overflow-clip boundary and get sheared flat (the reported
1453
+ "sharp bottom corner"). Trim the height by a further 2.5rem so the rounded
1454
+ corner + shadow clear the clip and the column reads as a fully-rounded
1455
+ floating card. Also clip the column's OWN children to its radius so a flush
1456
+ inner surface can't square a corner the board cards pop on hover but the
1457
+ DragOverlay clone is portaled, so nothing 3D is lost.
1458
+
1459
+ ⚠️ This tracks BoardColumn's height and must keep spending the same
1460
+ `--shell-bottom-clearance` the shell sets (MOTIR-2763), not a flat number:
1461
+ a stylesheet cannot read the `showPlanWithAi` prop, so the custom property
1462
+ is the only way this stays conditional on the orb actually mounting. The
1463
+ 13rem below is 10.5rem of chrome + this style's own 2.5rem trim. */
1464
+ height: calc(100dvh - 13rem - var(--shell-bottom-clearance, 1.5rem));
1465
+ overflow: hidden;
1466
+ }
1467
+ }
1468
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1469
+ [data-board-scroll] {
1470
+ gap: 1.5rem;
1471
+ padding-bottom: 2.25rem;
1472
+ padding-top: 0.25rem;
1473
+ }
1256
1474
  }
1257
1475
 
1258
1476
  /* ── 3D / Immersive — CONTROL DEPTH: every control sits on a named plane ────
@@ -1317,7 +1535,7 @@
1317
1535
  The `.justify-center` limb is the square icon-button shape: a centred control
1318
1536
  on the control radius is a key (the 3-dots trigger, the fullscreen toggle,
1319
1537
  every close ×). A row is never centred — it is `w-full` and `text-left`. */
1320
- [data-style='3d-immersive']
1538
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1321
1539
  :where(
1322
1540
  .rounded-\(--radius-btn\),
1323
1541
  button.rounded-\(--radius-input\),
@@ -1333,27 +1551,34 @@
1333
1551
  [data-depth='flat']
1334
1552
  )
1335
1553
  ) {
1336
- box-shadow:
1337
- 0 3px 0 0 var(--el-border-strong),
1338
- 0 5px 10px -3px rgba(15, 15, 15, 0.18);
1339
- transition:
1340
- transform 90ms ease-out,
1341
- box-shadow 90ms ease-out,
1342
- background-color var(--transition-duration);
1343
- }
1344
- [data-style='3d-immersive'] [data-variant='primary'] {
1345
- box-shadow:
1346
- 0 4px 0 0 var(--el-accent-pressed),
1347
- 0 7px 12px -3px rgba(15, 15, 15, 0.32);
1348
- }
1349
- [data-style='3d-immersive'] [data-variant='danger'] {
1350
- box-shadow:
1351
- 0 4px 0 0 color-mix(in srgb, var(--el-danger), black 32%),
1352
- 0 7px 12px -3px rgba(15, 15, 15, 0.32);
1554
+ box-shadow:
1555
+ 0 3px 0 0 var(--el-border-strong),
1556
+ 0 5px 10px -3px rgba(15, 15, 15, 0.18);
1557
+ transition:
1558
+ transform 90ms ease-out,
1559
+ box-shadow 90ms ease-out,
1560
+ background-color var(--transition-duration);
1561
+ }
1562
+ }
1563
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1564
+ [data-variant='primary'] {
1565
+ box-shadow:
1566
+ 0 4px 0 0 var(--el-accent-pressed),
1567
+ 0 7px 12px -3px rgba(15, 15, 15, 0.32);
1568
+ }
1569
+ }
1570
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1571
+ [data-variant='danger'] {
1572
+ box-shadow:
1573
+ 0 4px 0 0 color-mix(in srgb, var(--el-danger), black 32%),
1574
+ 0 7px 12px -3px rgba(15, 15, 15, 0.32);
1575
+ }
1353
1576
  }
1354
1577
  /* Ghost — the deliberately-flat quiet button; no thickness. */
1355
- [data-style='3d-immersive'] [data-variant='ghost'] {
1356
- box-shadow: none;
1578
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1579
+ [data-variant='ghost'] {
1580
+ box-shadow: none;
1581
+ }
1357
1582
  }
1358
1583
 
1359
1584
  /* THE FLAT SET — rows, chips and switches, which §4 keeps on their parent's
@@ -1361,7 +1586,7 @@
1361
1586
  (0,1,0) wherever the two overlap (a centred control inside an open menu, a
1362
1587
  `data-depth='flat'` icon button). A menu row raised inside an already-floating
1363
1588
  popover reads as debris, not depth — the overlay is the thing that floats. */
1364
- [data-style='3d-immersive']
1589
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1365
1590
  :is(
1366
1591
  [role='menuitem'],
1367
1592
  [role='option'],
@@ -1370,50 +1595,57 @@
1370
1595
  [role='switch'],
1371
1596
  [data-depth='flat']
1372
1597
  ),
1373
- [data-style='3d-immersive']
1374
- :is([data-surface='popover'], [data-menu-surface])
1375
- :is(button, a).rounded-\(--radius-control\),
1376
- [data-style='3d-immersive'] :is(button, a).w-full.rounded-\(--radius-control\) {
1377
- box-shadow: none;
1598
+ :is([data-surface='popover'], [data-menu-surface]) :is(button, a).rounded-\(--radius-control\),
1599
+ :is(button, a).w-full.rounded-\(--radius-control\) {
1600
+ box-shadow: none;
1601
+ }
1378
1602
  }
1379
1603
 
1380
1604
  /* THE PRESS — the raised set drops by its thickness and the base edge
1381
1605
  compresses, like a real key. Overrides the base `active:scale`. */
1382
- [data-style='3d-immersive']
1606
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1383
1607
  :where(
1384
- .rounded-\(--radius-btn\),
1385
- button.rounded-\(--radius-input\),
1386
- :is(button, a).rounded-\(--radius-control\).justify-center,
1387
- [data-depth='key']
1388
- ):active:not(:disabled) {
1389
- transform: translateY(3px);
1390
- box-shadow:
1391
- 0 1px 0 0 var(--el-border-strong),
1392
- 0 2px 4px rgba(15, 15, 15, 0.14);
1608
+ .rounded-\(--radius-btn\),
1609
+ button.rounded-\(--radius-input\),
1610
+ :is(button, a).rounded-\(--radius-control\).justify-center,
1611
+ [data-depth='key']
1612
+ ):active:not(:disabled) {
1613
+ transform: translateY(3px);
1614
+ box-shadow:
1615
+ 0 1px 0 0 var(--el-border-strong),
1616
+ 0 2px 4px rgba(15, 15, 15, 0.14);
1617
+ }
1393
1618
  }
1394
- [data-style='3d-immersive'] [data-variant='primary']:active:not(:disabled) {
1395
- box-shadow:
1396
- 0 1px 0 0 var(--el-accent-pressed),
1397
- 0 2px 5px rgba(15, 15, 15, 0.22);
1619
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1620
+ [data-variant='primary']:active:not(:disabled) {
1621
+ box-shadow:
1622
+ 0 1px 0 0 var(--el-accent-pressed),
1623
+ 0 2px 5px rgba(15, 15, 15, 0.22);
1624
+ }
1398
1625
  }
1399
- [data-style='3d-immersive'] [data-variant='danger']:active:not(:disabled) {
1400
- box-shadow:
1401
- 0 1px 0 0 color-mix(in srgb, var(--el-danger), black 32%),
1402
- 0 2px 5px rgba(15, 15, 15, 0.22);
1626
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1627
+ [data-variant='danger']:active:not(:disabled) {
1628
+ box-shadow:
1629
+ 0 1px 0 0 color-mix(in srgb, var(--el-danger), black 32%),
1630
+ 0 2px 5px rgba(15, 15, 15, 0.22);
1631
+ }
1403
1632
  }
1404
- [data-style='3d-immersive'] [data-variant='ghost']:active:not(:disabled) {
1405
- box-shadow: none;
1633
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1634
+ [data-variant='ghost']:active:not(:disabled) {
1635
+ box-shadow: none;
1636
+ }
1406
1637
  }
1407
1638
 
1408
1639
  /* THE QUIET PRESS — a control with no resting thickness still has to answer the
1409
1640
  pointer, or the style reads as flat wherever it is not a key: menu rows, list
1410
1641
  rows, nav links, inline affordances, the ghost button. One pixel of travel is
1411
1642
  the whole treatment; it is what separates "quiet" from "inert". */
1412
- [data-style='3d-immersive']
1643
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1413
1644
  :where(:is(button, a).rounded-\(--radius-control\), [data-variant='ghost']):active:not(
1414
- :disabled
1415
- ) {
1416
- transform: translateY(1px);
1645
+ :disabled
1646
+ ) {
1647
+ transform: translateY(1px);
1648
+ }
1417
1649
  }
1418
1650
 
1419
1651
  /* THE RECESSED SET — text fields are pressed INTO the surface, not raised off
@@ -1424,16 +1656,17 @@
1424
1656
  not a copy of it. `[data-surface='input']` is the Input primitive's shell;
1425
1657
  the bare element selectors catch the hand-rolled fields. Checkbox and radio
1426
1658
  are excluded: they are chips, not wells. */
1427
- [data-style='3d-immersive']
1659
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1428
1660
  :where(
1429
1661
  [data-surface='input'],
1430
1662
  input:not([type='checkbox'], [type='radio'], [type='range'], [type='color']),
1431
1663
  textarea,
1432
1664
  select
1433
1665
  ) {
1434
- box-shadow:
1435
- inset 0 2px 6px -2px rgba(15, 15, 15, 0.26),
1436
- inset 0 1px 2px 0 rgba(15, 15, 15, 0.05);
1666
+ box-shadow:
1667
+ inset 0 2px 6px -2px rgba(15, 15, 15, 0.26),
1668
+ inset 0 1px 2px 0 rgba(15, 15, 15, 0.05);
1669
+ }
1437
1670
  }
1438
1671
 
1439
1672
  /* Hero controls paint their own shadow in an INLINE style, which beats every
@@ -1452,18 +1685,20 @@
1452
1685
  override every shape role. A descendant rule is neither, which is where the
1453
1686
  immersive background (which reads `--el-*` for the same reason) already
1454
1687
  lives. Custom properties inherit, so every control below `body` sees these. */
1455
- [data-style='3d-immersive'] body {
1456
- --plan-hero-shadow:
1457
- 0 4px 0 0 var(--el-accent-pressed),
1458
- inset 0 1px 0 color-mix(in srgb, var(--el-accent-text) 38%, transparent),
1459
- inset 0 0 0 1px color-mix(in srgb, var(--el-accent-text) 18%, transparent),
1460
- 0 9px 18px -5px color-mix(in srgb, var(--el-accent) 75%, transparent),
1461
- 0 0 22px -3px color-mix(in srgb, var(--el-highlight) 50%, transparent);
1462
- --plan-orb-shadow:
1463
- 0 5px 0 0 var(--el-accent-pressed),
1464
- inset 0 1px 0 color-mix(in srgb, var(--el-accent-text) 40%, transparent),
1465
- 0 12px 24px -6px color-mix(in srgb, var(--el-accent) 80%, transparent),
1466
- 0 0 28px -2px color-mix(in srgb, var(--el-highlight) 55%, transparent);
1688
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1689
+ body {
1690
+ --plan-hero-shadow:
1691
+ 0 4px 0 0 var(--el-accent-pressed),
1692
+ inset 0 1px 0 color-mix(in srgb, var(--el-accent-text) 38%, transparent),
1693
+ inset 0 0 0 1px color-mix(in srgb, var(--el-accent-text) 18%, transparent),
1694
+ 0 9px 18px -5px color-mix(in srgb, var(--el-accent) 75%, transparent),
1695
+ 0 0 22px -3px color-mix(in srgb, var(--el-highlight) 50%, transparent);
1696
+ --plan-orb-shadow:
1697
+ 0 5px 0 0 var(--el-accent-pressed),
1698
+ inset 0 1px 0 color-mix(in srgb, var(--el-accent-text) 40%, transparent),
1699
+ 0 12px 24px -6px color-mix(in srgb, var(--el-accent) 80%, transparent),
1700
+ 0 0 28px -2px color-mix(in srgb, var(--el-highlight) 55%, transparent);
1701
+ }
1467
1702
  }
1468
1703
 
1469
1704
  /* ── 3D / Immersive — DROPDOWNS / POPOVERS / MENUS unfold in 3D ──────────────
@@ -1489,22 +1724,129 @@
1489
1724
  rule above would otherwise flatten any `shadow-(--shadow-elevated)` surface to
1490
1725
  --shadow-card). Global + static so menus, tooltips, drag-overlays all keep the
1491
1726
  right depth. */
1492
- [data-style='3d-immersive'] .shadow-\(--shadow-elevated\) {
1493
- box-shadow: var(--shadow-elevated);
1727
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1728
+ .shadow-\(--shadow-elevated\) {
1729
+ box-shadow: var(--shadow-elevated);
1730
+ }
1494
1731
  }
1495
1732
  @media (prefers-reduced-motion: no-preference) {
1496
1733
  /* The 3D UNFOLD is scoped to actual MENU surfaces (the Popover primitive +
1497
1734
  the `data-menu-surface` hook on Combobox / MultiSelect / BoardSwitcher
1498
1735
  menus) — NOT every elevated surface, so a drag-overlay or toast doesn't
1499
1736
  unexpectedly fold open. */
1500
- [data-style='3d-immersive'] [data-surface='popover'],
1501
- [data-style='3d-immersive'] [data-menu-surface] {
1502
- transform-origin: top center;
1503
- animation: immersive-menu-open 200ms cubic-bezier(0.22, 1, 0.36, 1);
1737
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1738
+ [data-surface='popover'],
1739
+ [data-menu-surface] {
1740
+ transform-origin: top center;
1741
+ animation: immersive-menu-open 200ms cubic-bezier(0.22, 1, 0.36, 1);
1742
+ }
1504
1743
  }
1505
1744
  /* A Radix popover that flips ABOVE its trigger folds from the bottom edge. */
1506
- [data-style='3d-immersive'] [data-surface='popover'][data-side='top'] {
1507
- transform-origin: bottom center;
1745
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1746
+ [data-surface='popover'][data-side='top'] {
1747
+ transform-origin: bottom center;
1748
+ }
1749
+ }
1750
+ }
1751
+
1752
+ /* 3D / Immersive — THE SHELL CHROME (MOTIR-4253, drawn by MOTIR-4252).
1753
+ docs/styles/3d-immersive.md §4's `shell chrome` row + design/shell/
1754
+ 3d-immersive-shell.mock.html, which injects this block verbatim so a frame in
1755
+ the asset cannot flatter a rule the implementation could not write.
1756
+
1757
+ ── The defect this closes ─────────────────────────────────────────────────
1758
+ The plane ladder named ten surfaces and stopped before the FRAME: this block
1759
+ carried no [data-surface='header'] and no [data-surface='sidebar'] rule at
1760
+ all, while both hooks are emitted and five of the eleven styles already use
1761
+ them. So roughly a quarter of every signed-in screen rendered byte-identically
1762
+ to the default style — and §4's own `Quiet control / row` row was left
1763
+ asserting that a sidebar nav link is flat *because the surface it sits in
1764
+ floats*, of a surface no rule made float. §4b is the closure rule that keeps
1765
+ the ladder from going silent about a surface again.
1766
+
1767
+ ── TWO OBJECTS, TWO PLANES, and the ladder says which ─────────────────────
1768
+ How far a surface floats is how far you can see UNDER its edges. The rail has
1769
+ four edges in the scene, so it can be an object at a distance: --shadow-card.
1770
+ The bar has exactly one — its bottom — so the only thing it can express is a
1771
+ CONTACT, which is --shadow-subtle, the ladder's tightest rung. A bar wearing
1772
+ --shadow-card would out-float the panel it frames on the strength of one
1773
+ visible edge.
1774
+
1775
+ Four things here are DECISIONS rather than syntax:
1776
+ 1. box-shadow ONLY on the bar, never `position`. hand-drawn-indie carries the
1777
+ same guard explicitly a few hundred lines below — the sticky header is
1778
+ already a containing block and overriding it breaks the sticky — and the
1779
+ pleasing part is that the property is also the MECHANISM: TopNav's
1780
+ `sticky … z-30` is the stacking context that lets the lid's shadow paint
1781
+ over the region beneath it. A static header's downward shadow is painted
1782
+ behind the next sibling's background and is invisible. The constraint and
1783
+ the mechanism are the same line of the component.
1784
+ 2. The rail's shared edge goes `border-right-color: transparent`, NOT
1785
+ `border: none`. The 1px stays in the box, so --width-rail-chrome (the
1786
+ rail's px-2 gutters plus that border) is unchanged. A floating panel has a
1787
+ shadow instead of a shared edge; it does not have a different width.
1788
+ 3. Both a11y fallbacks restore a LINE the treatment made transparent, and
1789
+ NEITHER MOVES A PIXEL. The rail's hairline comes back as an `outline` at
1790
+ `outline-offset: -1px` — drawn outside the box, follows the radius, costs
1791
+ no layout — and the bar's `border-bottom` already has its width; only its
1792
+ colour was taken away. This is neumorphism's pattern (its own
1793
+ prefers-contrast / forced-colors blocks) applied to a treatment whose
1794
+ structure rests on a shadow.
1795
+ 4. --spacing-rail-inset is `0px` in the base layer, so every other style is
1796
+ untouched and AppLayout's two sums compile to today's numbers everywhere
1797
+ else. That is what makes this a style-axis change rather than a shell one.
1798
+
1799
+ Colour: every value is an existing --el-* or an existing style-axis shadow
1800
+ token. The block adds and changes NO colour token, so the style and palette
1801
+ axes stay disjoint. */
1802
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1803
+ /* The RAIL — a floating PANEL, inset from the frame on all four sides so the
1804
+ immersive atmosphere reads around it. */
1805
+ [data-surface='sidebar'] {
1806
+ margin: var(--spacing-rail-inset);
1807
+ height: calc(100% - 2 * var(--spacing-rail-inset));
1808
+ border-radius: var(--radius-card);
1809
+ border-right-color: transparent;
1810
+ box-shadow: var(--shadow-card);
1811
+ }
1812
+ }
1813
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1814
+ /* The TOP BAR — the LID. No fill (the atmosphere runs under it), no hairline,
1815
+ and a contact shadow cast down onto the region below. It keeps full-bleed
1816
+ edges and square corners: a bar with a radius and a margin is a second
1817
+ floating slab, not a lid. */
1818
+ [data-surface='header'] {
1819
+ background-color: transparent;
1820
+ border-bottom-color: transparent;
1821
+ box-shadow: var(--shadow-subtle);
1822
+ }
1823
+ }
1824
+
1825
+ /* Accessibility fallbacks — where STRUCTURE rests on a shadow, restore a solid
1826
+ line. Both are geometry-neutral by construction (see decision 3 above), so a
1827
+ high-contrast or forced-colours user gets the same layout, not a shifted one. */
1828
+ @media (prefers-contrast: more) {
1829
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1830
+ [data-surface='sidebar'] {
1831
+ outline: 1px solid var(--el-border-strong);
1832
+ outline-offset: -1px;
1833
+ }
1834
+ [data-surface='header'] {
1835
+ border-bottom-color: var(--el-border-strong);
1836
+ }
1837
+ }
1838
+ }
1839
+ @media (forced-colors: active) {
1840
+ @scope ([data-style='3d-immersive']) to ([data-style]) {
1841
+ [data-surface='sidebar'] {
1842
+ outline: 1px solid var(--el-border-strong);
1843
+ outline-offset: -1px;
1844
+ box-shadow: none;
1845
+ }
1846
+ [data-surface='header'] {
1847
+ border-bottom-color: var(--el-border-strong);
1848
+ box-shadow: none;
1849
+ }
1508
1850
  }
1509
1851
  }
1510
1852
 
@@ -1591,52 +1933,62 @@
1591
1933
  dividers `.border-b` / `.border-t` (table rows, section rules) — the latter
1592
1934
  scoped INSIDE `[data-surface]` panels to bound the count to real surfaces (and
1593
1935
  their rows are virtualised, so only ~viewport-many overlays ever render). */
1594
- [data-style='hand-drawn-indie'] .border {
1595
- border-width: 2px;
1936
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
1937
+ .border {
1938
+ border-width: 2px;
1939
+ }
1596
1940
  }
1597
- [data-style='hand-drawn-indie'] .border:not(.fixed):not(.absolute):not(.sticky) {
1598
- position: relative;
1941
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
1942
+ .border:not(.fixed):not(.absolute):not(.sticky) {
1943
+ position: relative;
1944
+ }
1599
1945
  }
1600
- [data-style='hand-drawn-indie'] .border::after {
1601
- content: '';
1602
- position: absolute;
1603
- inset: -1px;
1604
- pointer-events: none;
1605
- border: 2px solid var(--el-border-strong);
1606
- border-radius: inherit;
1607
- filter: url(#hd-rough);
1946
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
1947
+ .border::after {
1948
+ content: '';
1949
+ position: absolute;
1950
+ inset: -1px;
1951
+ pointer-events: none;
1952
+ border: 2px solid var(--el-border-strong);
1953
+ border-radius: inherit;
1954
+ filter: url(#hd-rough);
1955
+ }
1608
1956
  }
1609
1957
 
1610
1958
  /* Internal grid dividers — make the inner rules as rough as the frame. Scoped
1611
1959
  inside framed surfaces; `:not(.border)` so a full-box element isn't handled
1612
1960
  twice; `:not(:last-child)` mirrors the common `last:border-b-0` strip so the
1613
1961
  final row gets no phantom line. */
1614
- [data-style='hand-drawn-indie']
1615
- [data-surface]
1616
- :is(.border-b, .border-t):not(.border):not(.fixed):not(.absolute):not(.sticky) {
1617
- position: relative;
1618
- }
1619
- [data-style='hand-drawn-indie'] [data-surface] .border-b:not(.border)::after {
1620
- content: '';
1621
- position: absolute;
1622
- left: 0;
1623
- right: 0;
1624
- bottom: -1px;
1625
- height: 0;
1626
- pointer-events: none;
1627
- border-bottom: 2px solid var(--el-border-strong);
1628
- filter: url(#hd-rough);
1629
- }
1630
- [data-style='hand-drawn-indie'] [data-surface] .border-t:not(.border)::after {
1631
- content: '';
1632
- position: absolute;
1633
- left: 0;
1634
- right: 0;
1635
- top: -1px;
1636
- height: 0;
1637
- pointer-events: none;
1638
- border-top: 2px solid var(--el-border-strong);
1639
- filter: url(#hd-rough);
1962
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
1963
+ [data-surface] :is(.border-b, .border-t):not(.border):not(.fixed):not(.absolute):not(.sticky) {
1964
+ position: relative;
1965
+ }
1966
+ }
1967
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
1968
+ [data-surface] .border-b:not(.border)::after {
1969
+ content: '';
1970
+ position: absolute;
1971
+ left: 0;
1972
+ right: 0;
1973
+ bottom: -1px;
1974
+ height: 0;
1975
+ pointer-events: none;
1976
+ border-bottom: 2px solid var(--el-border-strong);
1977
+ filter: url(#hd-rough);
1978
+ }
1979
+ }
1980
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
1981
+ [data-surface] .border-t:not(.border)::after {
1982
+ content: '';
1983
+ position: absolute;
1984
+ left: 0;
1985
+ right: 0;
1986
+ top: -1px;
1987
+ height: 0;
1988
+ pointer-events: none;
1989
+ border-top: 2px solid var(--el-border-strong);
1990
+ filter: url(#hd-rough);
1991
+ }
1640
1992
  }
1641
1993
  /* Suppress the overlay only where the REAL divider is removed: the last row of a
1642
1994
  fully-rendered list strips its border via `last:border-b-0`. (We can't use
@@ -1644,8 +1996,10 @@
1644
1996
  own `role="rowgroup"`, so it is itself a `:last-child` and would be wrongly
1645
1997
  skipped, which left the work-item table header's divider straight.) Same
1646
1998
  specificity as the draw rule, placed after, so it wins on the real last row. */
1647
- [data-style='hand-drawn-indie'] [data-surface] .last\:border-b-0:last-child::after {
1648
- content: none;
1999
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
2000
+ [data-surface] .last\:border-b-0:last-child::after {
2001
+ content: none;
2002
+ }
1649
2003
  }
1650
2004
 
1651
2005
  /* The APP SHELL FRAME — the sidebar rail's right edge + the top bar's bottom edge
@@ -1659,31 +2013,36 @@
1659
2013
  ONLY to a STATIC host (the rail) — never the STICKY header, which is already a
1660
2014
  containing block (overriding it would break the sticky), mirroring the `.border`
1661
2015
  rule's guard. */
1662
- [data-style='hand-drawn-indie']
2016
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
1663
2017
  :is([data-surface='sidebar'], [data-surface='header']):not(.fixed):not(.absolute):not(.sticky) {
1664
- position: relative;
1665
- }
1666
- [data-style='hand-drawn-indie'] [data-surface='sidebar']::after {
1667
- content: '';
1668
- position: absolute;
1669
- top: 0;
1670
- bottom: 0;
1671
- right: -1px;
1672
- width: 0;
1673
- pointer-events: none;
1674
- border-right: 2px solid var(--el-border-strong);
1675
- filter: url(#hd-rough);
1676
- }
1677
- [data-style='hand-drawn-indie'] [data-surface='header']::after {
1678
- content: '';
1679
- position: absolute;
1680
- left: 0;
1681
- right: 0;
1682
- bottom: -1px;
1683
- height: 0;
1684
- pointer-events: none;
1685
- border-bottom: 2px solid var(--el-border-strong);
1686
- filter: url(#hd-rough);
2018
+ position: relative;
2019
+ }
2020
+ }
2021
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
2022
+ [data-surface='sidebar']::after {
2023
+ content: '';
2024
+ position: absolute;
2025
+ top: 0;
2026
+ bottom: 0;
2027
+ right: -1px;
2028
+ width: 0;
2029
+ pointer-events: none;
2030
+ border-right: 2px solid var(--el-border-strong);
2031
+ filter: url(#hd-rough);
2032
+ }
2033
+ }
2034
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
2035
+ [data-surface='header']::after {
2036
+ content: '';
2037
+ position: absolute;
2038
+ left: 0;
2039
+ right: 0;
2040
+ bottom: -1px;
2041
+ height: 0;
2042
+ pointer-events: none;
2043
+ border-bottom: 2px solid var(--el-border-strong);
2044
+ filter: url(#hd-rough);
2045
+ }
1687
2046
  }
1688
2047
 
1689
2048
  /* ─────────────────────────────────────────────────────────────────────────
@@ -1795,8 +2154,11 @@
1795
2154
  panels look MOULDED OUT OF the same material rather than placed on top. Both
1796
2155
  the canvas and the surfaces below share `var(--el-surface)`, which is the whole
1797
2156
  neumorphic illusion. Palette-derived (no hue of its own). */
1798
- [data-style='neumorphism'] body {
1799
- background-color: var(--el-surface);
2157
+ @scope ([data-style='neumorphism']) to ([data-style]) {
2158
+ body,
2159
+ [data-app-shell] {
2160
+ background-color: var(--el-surface);
2161
+ }
1800
2162
  }
1801
2163
 
1802
2164
  /* Raised container surfaces — cards & popovers swell OUT of the field on a paired
@@ -1806,45 +2168,53 @@
1806
2168
  from hue. The faint hairline is kept as the accessibility fallback so structure
1807
2169
  doesn't depend on the shadow alone. Opaque `--el-surface` keeps text on the
1808
2170
  palette's AA-tuned surface. */
1809
- [data-style='neumorphism'] [data-surface='card'],
1810
- [data-style='neumorphism'] [data-surface='popover'] {
1811
- background-color: var(--el-surface);
1812
- border-color: color-mix(in srgb, var(--el-border) 45%, transparent);
1813
- box-shadow:
1814
- var(--neu-distance) var(--neu-distance) var(--neu-blur) var(--neu-shadow),
1815
- calc(-1 * var(--neu-distance)) calc(-1 * var(--neu-distance)) var(--neu-blur) var(--neu-light);
2171
+ @scope ([data-style='neumorphism']) to ([data-style]) {
2172
+ [data-surface='card'],
2173
+ [data-surface='popover'] {
2174
+ background-color: var(--el-surface);
2175
+ border-color: color-mix(in srgb, var(--el-border) 45%, transparent);
2176
+ box-shadow:
2177
+ var(--neu-distance) var(--neu-distance) var(--neu-blur) var(--neu-shadow),
2178
+ calc(-1 * var(--neu-distance)) calc(-1 * var(--neu-distance)) var(--neu-blur) var(--neu-light);
2179
+ }
1816
2180
  }
1817
2181
 
1818
2182
  /* Modal panel — the same raised mould, a touch deeper so the dialog separates
1819
2183
  from the field behind it (body text stays on the opaque surface). */
1820
- [data-style='neumorphism'] [data-surface='modal'] {
1821
- background-color: var(--el-surface);
1822
- border-color: color-mix(in srgb, var(--el-border) 45%, transparent);
1823
- box-shadow:
1824
- calc(var(--neu-distance) + 2px) calc(var(--neu-distance) + 2px) calc(var(--neu-blur) + 8px)
1825
- var(--neu-shadow),
1826
- calc(-1 * var(--neu-distance)) calc(-1 * var(--neu-distance)) calc(var(--neu-blur) + 4px)
1827
- var(--neu-light);
2184
+ @scope ([data-style='neumorphism']) to ([data-style]) {
2185
+ [data-surface='modal'] {
2186
+ background-color: var(--el-surface);
2187
+ border-color: color-mix(in srgb, var(--el-border) 45%, transparent);
2188
+ box-shadow:
2189
+ calc(var(--neu-distance) + 2px) calc(var(--neu-distance) + 2px) calc(var(--neu-blur) + 8px)
2190
+ var(--neu-shadow),
2191
+ calc(-1 * var(--neu-distance)) calc(-1 * var(--neu-distance)) calc(var(--neu-blur) + 4px)
2192
+ var(--neu-light);
2193
+ }
1828
2194
  }
1829
2195
 
1830
2196
  /* The sidebar rail — moulded from the same field as the chrome, gently raised so
1831
2197
  it reads as part of the one continuous surface. */
1832
- [data-style='neumorphism'] [data-surface='sidebar'] {
1833
- background-color: var(--el-surface);
1834
- box-shadow:
1835
- var(--neu-distance) 0 var(--neu-blur) var(--neu-shadow),
1836
- calc(-1 * var(--neu-distance)) 0 var(--neu-blur) var(--neu-light);
2198
+ @scope ([data-style='neumorphism']) to ([data-style]) {
2199
+ [data-surface='sidebar'] {
2200
+ background-color: var(--el-surface);
2201
+ box-shadow:
2202
+ var(--neu-distance) 0 var(--neu-blur) var(--neu-shadow),
2203
+ calc(-1 * var(--neu-distance)) 0 var(--neu-blur) var(--neu-light);
2204
+ }
1837
2205
  }
1838
2206
 
1839
2207
  /* Form controls — the inverse mould: a CONCAVE INSET well pressed INTO the field
1840
2208
  (the soft-UI input look), the paired shadow flipped to `inset`. */
1841
- [data-style='neumorphism'] [data-surface='input'] {
1842
- background-color: var(--el-surface);
1843
- border-color: color-mix(in srgb, var(--el-border) 45%, transparent);
1844
- box-shadow:
1845
- inset var(--neu-distance-sm) var(--neu-distance-sm) var(--neu-blur-sm) var(--neu-shadow),
1846
- inset calc(-1 * var(--neu-distance-sm)) calc(-1 * var(--neu-distance-sm)) var(--neu-blur-sm)
1847
- var(--neu-light);
2209
+ @scope ([data-style='neumorphism']) to ([data-style]) {
2210
+ [data-surface='input'] {
2211
+ background-color: var(--el-surface);
2212
+ border-color: color-mix(in srgb, var(--el-border) 45%, transparent);
2213
+ box-shadow:
2214
+ inset var(--neu-distance-sm) var(--neu-distance-sm) var(--neu-blur-sm) var(--neu-shadow),
2215
+ inset calc(-1 * var(--neu-distance-sm)) calc(-1 * var(--neu-distance-sm)) var(--neu-blur-sm)
2216
+ var(--neu-light);
2217
+ }
1848
2218
  }
1849
2219
 
1850
2220
  /* Accessibility fallback — when a user asks for more contrast (or forced colours),
@@ -1852,23 +2222,27 @@
1852
2222
  moulded surface and flatten the paired shadow to a single readable lift.
1853
2223
  Palette-derived (var(--el-border)); no hue. */
1854
2224
  @media (prefers-contrast: more) {
1855
- [data-style='neumorphism'] [data-surface='card'],
1856
- [data-style='neumorphism'] [data-surface='popover'],
1857
- [data-style='neumorphism'] [data-surface='modal'],
1858
- [data-style='neumorphism'] [data-surface='input'],
1859
- [data-style='neumorphism'] [data-surface='sidebar'] {
1860
- border-color: var(--el-border-strong);
1861
- box-shadow: var(--shadow-card);
2225
+ @scope ([data-style='neumorphism']) to ([data-style]) {
2226
+ [data-surface='card'],
2227
+ [data-surface='popover'],
2228
+ [data-surface='modal'],
2229
+ [data-surface='input'],
2230
+ [data-surface='sidebar'] {
2231
+ border-color: var(--el-border-strong);
2232
+ box-shadow: var(--shadow-card);
2233
+ }
1862
2234
  }
1863
2235
  }
1864
2236
  @media (forced-colors: active) {
1865
- [data-style='neumorphism'] [data-surface='card'],
1866
- [data-style='neumorphism'] [data-surface='popover'],
1867
- [data-style='neumorphism'] [data-surface='modal'],
1868
- [data-style='neumorphism'] [data-surface='input'],
1869
- [data-style='neumorphism'] [data-surface='sidebar'] {
1870
- border-color: var(--el-border-strong);
1871
- box-shadow: none;
2237
+ @scope ([data-style='neumorphism']) to ([data-style]) {
2238
+ [data-surface='card'],
2239
+ [data-surface='popover'],
2240
+ [data-surface='modal'],
2241
+ [data-surface='input'],
2242
+ [data-surface='sidebar'] {
2243
+ border-color: var(--el-border-strong);
2244
+ box-shadow: none;
2245
+ }
1872
2246
  }
1873
2247
  }
1874
2248
 
@@ -1972,62 +2346,92 @@
1972
2346
  axis overrides `--el-*`, never `--color-*`, which keeps the horizon orthogonal
1973
2347
  to the palette. Painted as the body's own background (always behind content),
1974
2348
  the proven 3D/Immersive atmosphere approach. */
1975
- [data-style='retrofuturism'] body {
1976
- background-image:
1977
- linear-gradient(
1978
- 180deg,
1979
- transparent 54%,
1980
- color-mix(in srgb, var(--el-highlight) var(--retro-horizon), transparent) 60.5%,
1981
- color-mix(in srgb, var(--el-accent) var(--retro-horizon), transparent) 62.5%,
1982
- transparent 69%
1983
- ),
1984
- linear-gradient(
1985
- 180deg,
1986
- color-mix(in srgb, var(--el-link) var(--retro-sky), transparent),
1987
- transparent 46%
1988
- ),
1989
- linear-gradient(
1990
- 0deg,
1991
- color-mix(in srgb, var(--el-accent) var(--retro-ground), transparent),
1992
- transparent 42%
1993
- );
1994
- background-size: 100% 100%;
1995
- background-attachment: fixed;
2349
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2350
+ body,
2351
+ [data-app-shell] {
2352
+ background-image:
2353
+ linear-gradient(
2354
+ 180deg,
2355
+ transparent 54%,
2356
+ color-mix(in srgb, var(--el-highlight) var(--retro-horizon), transparent) 60.5%,
2357
+ color-mix(in srgb, var(--el-accent) var(--retro-horizon), transparent) 62.5%,
2358
+ transparent 69%
2359
+ ),
2360
+ linear-gradient(
2361
+ 180deg,
2362
+ color-mix(in srgb, var(--el-link) var(--retro-sky), transparent),
2363
+ transparent 46%
2364
+ ),
2365
+ linear-gradient(
2366
+ 0deg,
2367
+ color-mix(in srgb, var(--el-accent) var(--retro-ground), transparent),
2368
+ transparent 42%
2369
+ );
2370
+ background-size: 100% 100%;
2371
+ background-attachment: fixed;
2372
+ }
2373
+ }
2374
+
2375
+ /* ⚠️ THE SHELL BECOMES A STACKING CONTEXT — the one place the shell-canvas
2376
+ pairing needs more than a second selector (MOTIR-4234). The grid floor below
2377
+ is `position: fixed; z-index: -1`, and both shell roots are `relative` with
2378
+ `z-index: auto`, so they establish NO stacking context: a negative-z-index
2379
+ descendant of the shell would be painted into the ROOT stacking context, at
2380
+ the step BEFORE positioned elements — i.e. behind the shell's own background,
2381
+ which is exactly where `body::after` already is and exactly why the floor was
2382
+ invisible on every signed-in route. `isolation: isolate` makes the shell its
2383
+ own context, so its `::after` resolves INSIDE it: above the shell canvas
2384
+ (step 1) and below all shell content (step 3 onwards), which is the
2385
+ disposition the floor is designed for.
2386
+
2387
+ Scoped to retrofuturism, because it is the only style with a canvas LAYER
2388
+ below content; the shell is left alone under every other style. It creates no
2389
+ containing block, so the `position: fixed` floor still resolves against the
2390
+ viewport and the shell's `overflow: hidden` does not clip it. */
2391
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2392
+ [data-app-shell] {
2393
+ isolation: isolate;
2394
+ }
1996
2395
  }
1997
2396
 
1998
2397
  /* The Outrun GRID FLOOR — a receding neon grid below the horizon. A separate
1999
- fixed layer (`body::after`) so it can be perspective-tilted and masked to fade
2398
+ fixed layer (`body::after`, and `[data-app-shell]::after` for the signed-in
2399
+ shell — the pairing above) so it can be perspective-tilted and masked to fade
2000
2400
  into the horizon; `z-index: -1` keeps it BEHIND all app content (above only the
2001
- body's own sky background), so it textures the floor without ever covering the
2002
- UI. Lines are `color-mix()` over `--el-*` accents (palette-derived, no hue of
2003
- their own); the mask uses the achromatic `black` keyword (no hex). The grid is
2004
- STATIC depth — like 3D/Immersive's atmosphere it carries the identity, so it is
2005
- not motion-gated (there is no animation to suppress). */
2006
- [data-style='retrofuturism'] body::after {
2007
- content: '';
2008
- position: fixed;
2009
- left: -30%;
2010
- right: -30%;
2011
- bottom: 0;
2012
- height: 42vh;
2013
- background-image:
2014
- repeating-linear-gradient(
2015
- 0deg,
2016
- color-mix(in srgb, var(--el-highlight) var(--retro-grid), transparent) 0 2px,
2017
- transparent 2px var(--retro-grid-size)
2018
- ),
2019
- repeating-linear-gradient(
2020
- 90deg,
2021
- color-mix(in srgb, var(--el-accent) var(--retro-grid), transparent) 0 2px,
2022
- transparent 2px var(--retro-grid-size)
2023
- );
2024
- background-size: var(--retro-grid-size) var(--retro-grid-size);
2025
- transform: perspective(300px) rotateX(75deg);
2026
- transform-origin: bottom center;
2027
- -webkit-mask-image: linear-gradient(to top, black 2%, transparent 80%);
2028
- mask-image: linear-gradient(to top, black 2%, transparent 80%);
2029
- pointer-events: none;
2030
- z-index: -1;
2401
+ sky background of whichever of the two is painting), so it textures the floor
2402
+ without ever covering the UI. Lines are `color-mix()` over `--el-*` accents
2403
+ (palette-derived, no hue of their own); the mask uses the achromatic `black`
2404
+ keyword (no hex). The grid is STATIC depth — like 3D/Immersive's atmosphere it
2405
+ carries the identity, so it is not motion-gated (there is no animation to
2406
+ suppress). */
2407
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2408
+ body::after,
2409
+ [data-app-shell]::after {
2410
+ content: '';
2411
+ position: fixed;
2412
+ left: -30%;
2413
+ right: -30%;
2414
+ bottom: 0;
2415
+ height: 42vh;
2416
+ background-image:
2417
+ repeating-linear-gradient(
2418
+ 0deg,
2419
+ color-mix(in srgb, var(--el-highlight) var(--retro-grid), transparent) 0 2px,
2420
+ transparent 2px var(--retro-grid-size)
2421
+ ),
2422
+ repeating-linear-gradient(
2423
+ 90deg,
2424
+ color-mix(in srgb, var(--el-accent) var(--retro-grid), transparent) 0 2px,
2425
+ transparent 2px var(--retro-grid-size)
2426
+ );
2427
+ background-size: var(--retro-grid-size) var(--retro-grid-size);
2428
+ transform: perspective(300px) rotateX(75deg);
2429
+ transform-origin: bottom center;
2430
+ -webkit-mask-image: linear-gradient(to top, black 2%, transparent 80%);
2431
+ mask-image: linear-gradient(to top, black 2%, transparent 80%);
2432
+ pointer-events: none;
2433
+ z-index: -1;
2434
+ }
2031
2435
  }
2032
2436
 
2033
2437
  /* Chrome container surfaces — OPAQUE (text AA is preserved against the base
@@ -2039,73 +2443,79 @@
2039
2443
  so they never pull a surface far from its base tone; the inset top/bottom lines
2040
2444
  are the lit chrome rim. Applied to EVERY panel via the global card-radius
2041
2445
  utility (the 3D/Immersive coverage approach), not just the <Card> primitive. */
2042
- [data-style='retrofuturism'] [data-surface='card'],
2043
- [data-style='retrofuturism'] [data-surface='popover'],
2044
- [data-style='retrofuturism'] .rounded-\(--radius-card\) {
2045
- background-color: var(--el-surface);
2046
- background-image:
2047
- linear-gradient(
2048
- 158deg,
2049
- transparent 36%,
2050
- color-mix(in srgb, white var(--retro-spec), transparent) 49%,
2051
- transparent 62%
2052
- ),
2053
- linear-gradient(
2054
- 180deg,
2055
- color-mix(in srgb, white var(--retro-bevel-light), transparent) 0%,
2056
- transparent 16%,
2057
- transparent 78%,
2058
- color-mix(in srgb, black var(--retro-bevel-shade), transparent) 100%
2059
- );
2060
- box-shadow:
2061
- var(--shadow-card),
2062
- inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2063
- inset 0 -1px 0 color-mix(in srgb, black 24%, transparent),
2064
- 0 0 28px -8px color-mix(in srgb, var(--el-accent) var(--retro-glow), transparent);
2446
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2447
+ [data-surface='card'],
2448
+ [data-surface='popover'],
2449
+ .rounded-\(--radius-card\) {
2450
+ background-color: var(--el-surface);
2451
+ background-image:
2452
+ linear-gradient(
2453
+ 158deg,
2454
+ transparent 36%,
2455
+ color-mix(in srgb, white var(--retro-spec), transparent) 49%,
2456
+ transparent 62%
2457
+ ),
2458
+ linear-gradient(
2459
+ 180deg,
2460
+ color-mix(in srgb, white var(--retro-bevel-light), transparent) 0%,
2461
+ transparent 16%,
2462
+ transparent 78%,
2463
+ color-mix(in srgb, black var(--retro-bevel-shade), transparent) 100%
2464
+ );
2465
+ box-shadow:
2466
+ var(--shadow-card),
2467
+ inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2468
+ inset 0 -1px 0 color-mix(in srgb, black 24%, transparent),
2469
+ 0 0 28px -8px color-mix(in srgb, var(--el-accent) var(--retro-glow), transparent);
2470
+ }
2065
2471
  }
2066
2472
 
2067
2473
  /* Modal panel — the same chrome, with a deeper palette glow so the dialog
2068
2474
  separates cleanly from the grid behind it (and body text stays on the solid
2069
2475
  surface). Overrides the card-radius rule above for the modal radius. */
2070
- [data-style='retrofuturism'] [data-surface='modal'] {
2071
- background-color: var(--el-surface);
2072
- background-image:
2073
- linear-gradient(
2074
- 158deg,
2075
- transparent 34%,
2076
- color-mix(in srgb, white var(--retro-spec), transparent) 49%,
2077
- transparent 64%
2078
- ),
2079
- linear-gradient(
2080
- 180deg,
2081
- color-mix(in srgb, white var(--retro-bevel-light), transparent) 0%,
2082
- transparent 14%,
2083
- transparent 80%,
2084
- color-mix(in srgb, black var(--retro-bevel-shade), transparent) 100%
2085
- );
2086
- box-shadow:
2087
- var(--shadow-modal),
2088
- inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2089
- 0 0 56px -14px color-mix(in srgb, var(--el-highlight) var(--retro-glow-strong), transparent);
2476
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2477
+ [data-surface='modal'] {
2478
+ background-color: var(--el-surface);
2479
+ background-image:
2480
+ linear-gradient(
2481
+ 158deg,
2482
+ transparent 34%,
2483
+ color-mix(in srgb, white var(--retro-spec), transparent) 49%,
2484
+ transparent 64%
2485
+ ),
2486
+ linear-gradient(
2487
+ 180deg,
2488
+ color-mix(in srgb, white var(--retro-bevel-light), transparent) 0%,
2489
+ transparent 14%,
2490
+ transparent 80%,
2491
+ color-mix(in srgb, black var(--retro-bevel-shade), transparent) 100%
2492
+ );
2493
+ box-shadow:
2494
+ var(--shadow-modal),
2495
+ inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2496
+ 0 0 56px -14px color-mix(in srgb, var(--el-highlight) var(--retro-glow-strong), transparent);
2497
+ }
2090
2498
  }
2091
2499
 
2092
2500
  /* The sidebar rail — opaque chrome with a quieter specular + bevel so it reads as
2093
2501
  part of the polished-metal family, not a flat slab. */
2094
- [data-style='retrofuturism'] [data-surface='sidebar'] {
2095
- background-color: var(--el-sidebar-bg);
2096
- background-image:
2097
- linear-gradient(
2098
- 158deg,
2099
- transparent 40%,
2100
- color-mix(in srgb, white var(--retro-sheen), transparent) 50%,
2101
- transparent 60%
2102
- ),
2103
- linear-gradient(
2104
- 180deg,
2105
- color-mix(in srgb, white var(--retro-sheen), transparent),
2106
- transparent 24%,
2107
- color-mix(in srgb, black var(--retro-bevel-shade), transparent) 100%
2108
- );
2502
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2503
+ [data-surface='sidebar'] {
2504
+ background-color: var(--el-sidebar-bg);
2505
+ background-image:
2506
+ linear-gradient(
2507
+ 158deg,
2508
+ transparent 40%,
2509
+ color-mix(in srgb, white var(--retro-sheen), transparent) 50%,
2510
+ transparent 60%
2511
+ ),
2512
+ linear-gradient(
2513
+ 180deg,
2514
+ color-mix(in srgb, white var(--retro-sheen), transparent),
2515
+ transparent 24%,
2516
+ color-mix(in srgb, black var(--retro-bevel-shade), transparent) 100%
2517
+ );
2518
+ }
2109
2519
  }
2110
2520
 
2111
2521
  /* Text inputs (a TYPING field) — a RECESSED chrome well: the bevel inverts (a
@@ -2113,14 +2523,16 @@
2113
2523
  metal. Clickable controls (dropdown / select / picker triggers + buttons) get
2114
2524
  the RAISED chrome below instead — matching the CTA — so a typing field stays
2115
2525
  recessed while everything you click reads as a raised chrome control. */
2116
- [data-style='retrofuturism'] [data-surface='input'] {
2117
- background-color: var(--el-page-bg);
2118
- background-image: linear-gradient(
2119
- 180deg,
2120
- color-mix(in srgb, black var(--retro-sheen), transparent) 0%,
2121
- transparent 34%
2122
- );
2123
- box-shadow: inset 0 1px 3px color-mix(in srgb, black var(--retro-glow), transparent);
2526
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2527
+ [data-surface='input'] {
2528
+ background-color: var(--el-page-bg);
2529
+ background-image: linear-gradient(
2530
+ 180deg,
2531
+ color-mix(in srgb, black var(--retro-sheen), transparent) 0%,
2532
+ transparent 34%
2533
+ );
2534
+ box-shadow: inset 0 1px 3px color-mix(in srgb, black var(--retro-glow), transparent);
2535
+ }
2124
2536
  }
2125
2537
 
2126
2538
  /* ── EVERY clickable control reads as RETRO CHROME (7.3.40) ──────────────────
@@ -2151,46 +2563,50 @@
2151
2563
  /* FILLED CTAs — accent (primary) keeps its hue under a diagonal specular glint +
2152
2564
  a vertical metallic sweep + a palette glow. Keyed on the fill class so it
2153
2565
  covers the `Button` primitive AND `buttonVariants()` anchors alike. */
2154
- [data-style='retrofuturism'] .rounded-\(--radius-btn\).bg-\(--el-accent\) {
2155
- background-image:
2156
- linear-gradient(
2157
- 158deg,
2158
- transparent 38%,
2159
- color-mix(in srgb, white var(--retro-spec), transparent) 50%,
2160
- transparent 62%
2161
- ),
2162
- linear-gradient(
2163
- 180deg,
2164
- color-mix(in srgb, white var(--retro-spec), var(--el-accent)) 0%,
2165
- var(--el-accent) 46%,
2166
- color-mix(in srgb, black var(--retro-bevel-shade), var(--el-accent)) 100%
2167
- );
2168
- box-shadow:
2169
- var(--shadow-subtle),
2170
- inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2171
- 0 0 22px -6px color-mix(in srgb, var(--el-accent) var(--retro-glow-strong), transparent);
2566
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2567
+ .rounded-\(--radius-btn\).bg-\(--el-accent\) {
2568
+ background-image:
2569
+ linear-gradient(
2570
+ 158deg,
2571
+ transparent 38%,
2572
+ color-mix(in srgb, white var(--retro-spec), transparent) 50%,
2573
+ transparent 62%
2574
+ ),
2575
+ linear-gradient(
2576
+ 180deg,
2577
+ color-mix(in srgb, white var(--retro-spec), var(--el-accent)) 0%,
2578
+ var(--el-accent) 46%,
2579
+ color-mix(in srgb, black var(--retro-bevel-shade), var(--el-accent)) 100%
2580
+ );
2581
+ box-shadow:
2582
+ var(--shadow-subtle),
2583
+ inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2584
+ 0 0 22px -6px color-mix(in srgb, var(--el-accent) var(--retro-glow-strong), transparent);
2585
+ }
2172
2586
  }
2173
2587
 
2174
2588
  /* Danger CTAs — the same machined sweep, in the danger hue (so a destructive
2175
2589
  button reads as red chrome, not a flat red, matching the accent CTA). */
2176
- [data-style='retrofuturism'] .rounded-\(--radius-btn\).bg-\(--el-danger\) {
2177
- background-image:
2178
- linear-gradient(
2179
- 158deg,
2180
- transparent 38%,
2181
- color-mix(in srgb, white var(--retro-spec), transparent) 50%,
2182
- transparent 62%
2183
- ),
2184
- linear-gradient(
2185
- 180deg,
2186
- color-mix(in srgb, white var(--retro-spec), var(--el-danger)) 0%,
2187
- var(--el-danger) 46%,
2188
- color-mix(in srgb, black var(--retro-bevel-shade), var(--el-danger)) 100%
2189
- );
2190
- box-shadow:
2191
- var(--shadow-subtle),
2192
- inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2193
- 0 0 22px -6px color-mix(in srgb, var(--el-danger) var(--retro-glow-strong), transparent);
2590
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2591
+ .rounded-\(--radius-btn\).bg-\(--el-danger\) {
2592
+ background-image:
2593
+ linear-gradient(
2594
+ 158deg,
2595
+ transparent 38%,
2596
+ color-mix(in srgb, white var(--retro-spec), transparent) 50%,
2597
+ transparent 62%
2598
+ ),
2599
+ linear-gradient(
2600
+ 180deg,
2601
+ color-mix(in srgb, white var(--retro-spec), var(--el-danger)) 0%,
2602
+ var(--el-danger) 46%,
2603
+ color-mix(in srgb, black var(--retro-bevel-shade), var(--el-danger)) 100%
2604
+ );
2605
+ box-shadow:
2606
+ var(--shadow-subtle),
2607
+ inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2608
+ 0 0 22px -6px color-mix(in srgb, var(--el-danger) var(--retro-glow-strong), transparent);
2609
+ }
2194
2610
  }
2195
2611
 
2196
2612
  /* NEUTRAL STEEL chrome — every OTHER clickable control: bordered buttons + links
@@ -2199,67 +2615,446 @@
2199
2615
  `:not([role='group'])` excludes the segmented TRACK (its own rule below); the
2200
2616
  FILLED CTAs are excluded automatically (they carry a fill class, not `.border`).
2201
2617
  The gunmetal base is set here because secondary controls ship `bg-transparent`. */
2202
- [data-style='retrofuturism'] .rounded-\(--radius-btn\).border:not([role='group']),
2203
- [data-style='retrofuturism'] button.rounded-\(--radius-input\) {
2204
- background-color: color-mix(in srgb, black 24%, var(--el-surface));
2205
- background-image:
2206
- linear-gradient(
2207
- 158deg,
2208
- transparent 36%,
2209
- color-mix(in srgb, white var(--retro-spec), transparent) 50%,
2210
- transparent 64%
2211
- ),
2212
- linear-gradient(
2213
- 180deg,
2214
- color-mix(in srgb, white var(--retro-bevel-light), transparent) 0%,
2215
- transparent 42%,
2216
- transparent 60%,
2217
- color-mix(in srgb, black var(--retro-bevel-shade), transparent) 100%
2218
- );
2219
- box-shadow:
2220
- var(--shadow-subtle),
2221
- inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2222
- inset 0 -1px 0 color-mix(in srgb, black 24%, transparent),
2223
- 0 0 18px -8px color-mix(in srgb, var(--el-accent) var(--retro-glow), transparent);
2618
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2619
+ .rounded-\(--radius-btn\).border:not([role='group']),
2620
+ button.rounded-\(--radius-input\) {
2621
+ background-color: color-mix(in srgb, black 24%, var(--el-surface));
2622
+ background-image:
2623
+ linear-gradient(
2624
+ 158deg,
2625
+ transparent 36%,
2626
+ color-mix(in srgb, white var(--retro-spec), transparent) 50%,
2627
+ transparent 64%
2628
+ ),
2629
+ linear-gradient(
2630
+ 180deg,
2631
+ color-mix(in srgb, white var(--retro-bevel-light), transparent) 0%,
2632
+ transparent 42%,
2633
+ transparent 60%,
2634
+ color-mix(in srgb, black var(--retro-bevel-shade), transparent) 100%
2635
+ );
2636
+ box-shadow:
2637
+ var(--shadow-subtle),
2638
+ inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2639
+ inset 0 -1px 0 color-mix(in srgb, black 24%, transparent),
2640
+ 0 0 18px -8px color-mix(in srgb, var(--el-accent) var(--retro-glow), transparent);
2641
+ }
2224
2642
  }
2225
2643
 
2226
2644
  /* SEGMENTED control (the board "Group by" switch + every other `Segmented` use):
2227
2645
  the classic metal segmented control — a RECESSED gunmetal TRACK (the
2228
2646
  `[role='group']` wrapper) with the ACTIVE segment (`aria-pressed='true'`) raised
2229
2647
  out of it as a chrome chip. Inactive segments stay transparent on the track. */
2230
- [data-style='retrofuturism'] [role='group'].rounded-\(--radius-btn\) {
2231
- background-color: color-mix(in srgb, black 15%, var(--el-surface));
2232
- box-shadow: inset 0 1px 3px color-mix(in srgb, black var(--retro-glow), transparent);
2648
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2649
+ [role='group'].rounded-\(--radius-btn\) {
2650
+ background-color: color-mix(in srgb, black 15%, var(--el-surface));
2651
+ box-shadow: inset 0 1px 3px color-mix(in srgb, black var(--retro-glow), transparent);
2652
+ }
2233
2653
  }
2234
- [data-style='retrofuturism'] [role='group'] [aria-pressed='true'] {
2235
- background-color: color-mix(in srgb, black 24%, var(--el-surface));
2236
- background-image:
2237
- linear-gradient(
2238
- 158deg,
2239
- transparent 36%,
2240
- color-mix(in srgb, white var(--retro-spec), transparent) 50%,
2241
- transparent 64%
2242
- ),
2243
- linear-gradient(
2244
- 180deg,
2245
- color-mix(in srgb, white var(--retro-bevel-light), transparent) 0%,
2246
- transparent 42%,
2247
- transparent 60%,
2248
- color-mix(in srgb, black var(--retro-bevel-shade), transparent) 100%
2249
- );
2250
- box-shadow:
2251
- var(--shadow-subtle),
2252
- inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2253
- inset 0 -1px 0 color-mix(in srgb, black 24%, transparent);
2654
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2655
+ [role='group'] [aria-pressed='true'] {
2656
+ background-color: color-mix(in srgb, black 24%, var(--el-surface));
2657
+ background-image:
2658
+ linear-gradient(
2659
+ 158deg,
2660
+ transparent 36%,
2661
+ color-mix(in srgb, white var(--retro-spec), transparent) 50%,
2662
+ transparent 64%
2663
+ ),
2664
+ linear-gradient(
2665
+ 180deg,
2666
+ color-mix(in srgb, white var(--retro-bevel-light), transparent) 0%,
2667
+ transparent 42%,
2668
+ transparent 60%,
2669
+ color-mix(in srgb, black var(--retro-bevel-shade), transparent) 100%
2670
+ );
2671
+ box-shadow:
2672
+ var(--shadow-subtle),
2673
+ inset 0 1px 0 color-mix(in srgb, white 60%, transparent),
2674
+ inset 0 -1px 0 color-mix(in srgb, black 24%, transparent);
2675
+ }
2254
2676
  }
2255
2677
 
2256
2678
  /* Hover — brighten the steel a touch (the controls' own `hover:bg-*` utilities
2257
2679
  are out-specified by the rules above, so restore a clear hover signal). */
2258
- [data-style='retrofuturism']
2680
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
2259
2681
  .rounded-\(--radius-btn\).border:not([role='group']):hover:not(:disabled),
2260
- [data-style='retrofuturism'] button.rounded-\(--radius-input\):hover:not(:disabled),
2261
- [data-style='retrofuturism'] [role='group'] [aria-pressed='false']:hover:not(:disabled) {
2262
- background-color: color-mix(in srgb, black 14%, var(--el-surface));
2682
+ button.rounded-\(--radius-input\):hover:not(:disabled),
2683
+ [role='group'] [aria-pressed='false']:hover:not(:disabled) {
2684
+ background-color: color-mix(in srgb, black 14%, var(--el-surface));
2685
+ }
2686
+ }
2687
+
2688
+ /* ─────────────────────────────────────────────────────────────────────────
2689
+ THE HERO AI CONTROL, PER STYLE (drawn by MOTIR-4742, wired by MOTIR-4743).
2690
+
2691
+ The two controls that summon the planning workspace — the header pill
2692
+ (`components/planning/PlanWithAILauncher.tsx`) and the floating M orb
2693
+ (`components/planning/PlanWithAIFab.tsx`) — are the product's headline
2694
+ affordance and the sanctioned exception to the flat-button norm
2695
+ (`design/ai-chat/design-notes.md` § *The hero control is STYLE-AWARE*). Each
2696
+ style gives them its own material. Before this section they rendered
2697
+ byte-identically under all eleven: the hook the design prescribed was emitted
2698
+ nowhere and the fill was painted inline, where no rule can reach it.
2699
+
2700
+ ── TWO ATTRIBUTES, AND THE SECOND IS LOAD-BEARING ─────────────────────────
2701
+ `data-ai-cta` distinguishes the two because they do not share a fill recipe:
2702
+ the orb's `radial-gradient` first stop is `--orb-lit-mix`, a guarded contrast
2703
+ knob (MOTIR-3207, `tests/theme/orb-glyph-contrast.test.ts`), and a shared
2704
+ `background-image` would silently overwrite it. So the shape-agnostic
2705
+ properties — border, outer glow, type, ink — are written on
2706
+ `[data-surface='ai-cta']`, and every FILL under `[data-ai-cta='pill']` or
2707
+ `[data-ai-cta='orb']`.
2708
+
2709
+ ── THE TWO INLINE-DECLARED PROPERTIES TRAVEL ON CUSTOM PROPERTIES ─────────
2710
+ The components declare exactly two things in an inline `style` prop —
2711
+ `background-image` and `box-shadow` — and an inline declaration beats every
2712
+ stylesheet rule. So BOTH are read through a `var()` seam:
2713
+ `background-image: var(--plan-hero-fill, <the base recipe>)` (MOTIR-4743) and
2714
+ `box-shadow: var(--plan-hero-shadow, <the base look>)` (MOTIR-3522), with the
2715
+ orb's twins beside them.
2716
+
2717
+ `design/ai-chat/design-notes.md` § *The CSS, verbatim* writes each treatment
2718
+ as plain `background-image` / `box-shadow` declarations. Both are transcribed
2719
+ here onto those four variables, because written literally they would be
2720
+ **INERT** — which is the very defect this section exists to close, one
2721
+ property over. It is the same mechanism the design's own row 8 relies on and
2722
+ the only one consistent with its "Warm Editorial has no block by design"
2723
+ decision: painting the base in a `warm-editorial` block instead would give the
2724
+ Tier-0 base a second copy of its own recipe to drift from.
2725
+
2726
+ ⚠️ THE SHADOW HALF WAS MEASURED, NOT REASONED. `tests/e2e/hero-ai-control-styles.spec.ts`
2727
+ compares every style against the base in a real browser, and with the shadows
2728
+ written literally exactly ONE control came back byte-identical to the base:
2729
+ the AURORA ORB — the only one whose row sets nothing but a shadow. Every other
2730
+ style also moves a fill, a border or the type, so its rule "worked" while its
2731
+ shadow silently did not. A source-reading review passes that; the render does
2732
+ not.
2733
+
2734
+ A shared `[data-surface='ai-cta']` rule sets BOTH shadow variables, since the
2735
+ design gives the pill and the orb the same shadow and each control reads only
2736
+ its own name. Everything the components do NOT declare inline —
2737
+ `background-color`, `background-size`, `background-position`,
2738
+ `background-repeat`, `border`, `backdrop-filter`, the type properties — is
2739
+ written exactly as the design states it.
2740
+
2741
+ Radius, padding and height are NOT set here: they already flow through
2742
+ `--radius-badge` / `--spacing-btn-x` / `--height-btn-md`, and re-stating them
2743
+ would freeze the one axis that was already working. Palette-derived
2744
+ throughout — `color-mix()` over `--el-*`, no raw hex — so a `data-palette`
2745
+ swap re-tints all eleven treatments. Every label ratio in the design's table
2746
+ was measured on the painted pixel; the tightest is glassmorphism at 4.87:1.
2747
+ ───────────────────────────────────────────────────────────────────────── */
2748
+
2749
+ /* 1 · Warm Editorial — NO BLOCK, by design. It is the Tier-0 base, and the
2750
+ components' own `var()` fallbacks ARE its treatment; a block repeating them
2751
+ would be a second copy to drift. */
2752
+
2753
+ /* 2 · Soft / Playful — pillowy stacked shadow, no border. */
2754
+ @scope ([data-style='soft-playful']) to ([data-style]) {
2755
+ [data-surface='ai-cta'] {
2756
+ font-weight: 800;
2757
+ --plan-hero-shadow:
2758
+ 0 7px 0 -1px color-mix(in srgb, var(--el-accent) 22%, var(--el-page-bg)),
2759
+ 0 14px 22px -6px color-mix(in srgb, var(--el-accent) 55%, transparent);
2760
+ --plan-orb-shadow:
2761
+ 0 7px 0 -1px color-mix(in srgb, var(--el-accent) 22%, var(--el-page-bg)),
2762
+ 0 14px 22px -6px color-mix(in srgb, var(--el-accent) 55%, transparent);
2763
+ }
2764
+ }
2765
+
2766
+ /* 3 · Swiss / Minimal-Flat — flat solid, sharp, uppercase, NO shadow. */
2767
+ @scope ([data-style='swiss-minimal-flat']) to ([data-style]) {
2768
+ [data-surface='ai-cta'] {
2769
+ --plan-hero-shadow: none;
2770
+ --plan-orb-shadow: none;
2771
+ font-weight: 700;
2772
+ text-transform: uppercase;
2773
+ letter-spacing: 0.07em;
2774
+ }
2775
+ }
2776
+ @scope ([data-style='swiss-minimal-flat']) to ([data-style]) {
2777
+ [data-surface='ai-cta'][data-ai-cta='pill'] {
2778
+ --plan-hero-fill: none;
2779
+ background-color: var(--el-accent);
2780
+ }
2781
+ }
2782
+
2783
+ /* 4 · Glassmorphism — frosted translucency over the rim. 86% is a FLOOR, not a
2784
+ preference: white is the lightest backdrop the light theme can put behind it,
2785
+ so 86% accent composited over anything measures >= 4.87:1. */
2786
+ @scope ([data-style='glassmorphism']) to ([data-style]) {
2787
+ [data-surface='ai-cta'] {
2788
+ border: 1px solid var(--glass-rim);
2789
+ -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
2790
+ backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
2791
+ --plan-hero-shadow:
2792
+ inset 0 1px 0 var(--glass-rim),
2793
+ 0 8px 22px -6px color-mix(in srgb, var(--el-accent) 45%, transparent);
2794
+ --plan-orb-shadow:
2795
+ inset 0 1px 0 var(--glass-rim),
2796
+ 0 8px 22px -6px color-mix(in srgb, var(--el-accent) 45%, transparent);
2797
+ }
2798
+ }
2799
+ @scope ([data-style='glassmorphism']) to ([data-style]) {
2800
+ [data-surface='ai-cta'][data-ai-cta='pill'] {
2801
+ --plan-hero-fill: none;
2802
+ background-color: color-mix(in srgb, var(--el-accent) 86%, transparent);
2803
+ }
2804
+ }
2805
+ @scope ([data-style='glassmorphism']) to ([data-style]) {
2806
+ [data-surface='ai-cta'][data-ai-cta='orb'] {
2807
+ /* Crown-confined: across the whole circle this sheen lightens the 26/56
2808
+ glyph box to 3.17:1 light / 2.67:1 dark and breaks the `--orb-lit-mix`
2809
+ floor the style layer may not touch. Confined, it measures the shipped
2810
+ 3.78:1 / 3.10:1 — identical to no sheen at all. */
2811
+ background-size:
2812
+ 100% 20%,
2813
+ 100% 100%;
2814
+ background-position:
2815
+ top left,
2816
+ top left;
2817
+ background-repeat: no-repeat;
2818
+ --plan-orb-fill:
2819
+ linear-gradient(160deg, var(--glass-sheen), transparent 90%),
2820
+ radial-gradient(
2821
+ circle at 33% 27%,
2822
+ color-mix(in srgb, var(--el-accent-text) var(--orb-lit-mix), var(--el-accent)),
2823
+ var(--el-accent) 56%,
2824
+ color-mix(in srgb, var(--el-accent) 68%, var(--el-highlight))
2825
+ );
2826
+ }
2827
+ }
2828
+
2829
+ /* 5 · Neo-Brutalism — hard 2px border + zero-blur offset shadow. */
2830
+ @scope ([data-style='neo-brutalism']) to ([data-style]) {
2831
+ [data-surface='ai-cta'] {
2832
+ border: 2px solid var(--el-text);
2833
+ --plan-hero-shadow: 4px 4px 0 0 var(--el-text);
2834
+ --plan-orb-shadow: 4px 4px 0 0 var(--el-text);
2835
+ font-weight: 800;
2836
+ text-transform: uppercase;
2837
+ }
2838
+ }
2839
+ @scope ([data-style='neo-brutalism']) to ([data-style]) {
2840
+ [data-surface='ai-cta'][data-ai-cta='pill'] {
2841
+ --plan-hero-fill: none;
2842
+ background-color: var(--el-accent);
2843
+ }
2844
+ }
2845
+
2846
+ /* 6 · Cybercore / Y2K — dark HUD ground, neon halo, mono label. The mixes toward
2847
+ the achromatic `black` KEYWORD are sanctioned by this style's own material
2848
+ layer, which uses them to read as lightness rather than as a hue, so the
2849
+ palette axis stays disjoint. */
2850
+ @scope ([data-style='cybercore-y2k']) to ([data-style]) {
2851
+ [data-surface='ai-cta'] {
2852
+ border: 1px solid color-mix(in srgb, var(--el-highlight) 70%, transparent);
2853
+ --plan-hero-shadow:
2854
+ 0 0 10px color-mix(in srgb, var(--el-highlight) 60%, transparent),
2855
+ 0 0 26px -4px color-mix(in srgb, var(--el-accent) 70%, transparent);
2856
+ --plan-orb-shadow:
2857
+ 0 0 10px color-mix(in srgb, var(--el-highlight) 60%, transparent),
2858
+ 0 0 26px -4px color-mix(in srgb, var(--el-accent) 70%, transparent);
2859
+ font-family: var(--font-mono);
2860
+ letter-spacing: 0.04em;
2861
+ }
2862
+ }
2863
+ @scope ([data-style='cybercore-y2k']) to ([data-style]) {
2864
+ [data-surface='ai-cta'][data-ai-cta='pill'] {
2865
+ --plan-hero-fill: linear-gradient(
2866
+ 135deg,
2867
+ color-mix(in srgb, var(--el-accent) 88%, black),
2868
+ color-mix(in srgb, color-mix(in srgb, var(--el-accent) 70%, var(--el-highlight)) 78%, black)
2869
+ );
2870
+ }
2871
+ }
2872
+ @scope ([data-style='cybercore-y2k']) to ([data-style]) {
2873
+ [data-surface='ai-cta'][data-ai-cta='orb'] {
2874
+ --plan-orb-fill:
2875
+ linear-gradient(
2876
+ 160deg,
2877
+ color-mix(in srgb, var(--el-highlight) 22%, transparent),
2878
+ transparent 55%
2879
+ ),
2880
+ radial-gradient(
2881
+ circle at 33% 27%,
2882
+ color-mix(in srgb, var(--el-accent-text) var(--orb-lit-mix), var(--el-accent)),
2883
+ var(--el-accent) 56%,
2884
+ color-mix(in srgb, var(--el-accent) 68%, var(--el-highlight))
2885
+ );
2886
+ }
2887
+ }
2888
+
2889
+ /* 7 · Aurora — lit from within, colour halo, no hard shadow. */
2890
+ @scope ([data-style='aurora']) to ([data-style]) {
2891
+ [data-surface='ai-cta'] {
2892
+ --plan-hero-shadow:
2893
+ inset 0 1px 0 color-mix(in srgb, var(--el-accent-text) var(--aurora-sheen), transparent),
2894
+ 0 0 26px -2px color-mix(in srgb, var(--el-accent) var(--aurora-glow), transparent),
2895
+ 0 0 40px -6px color-mix(in srgb, var(--el-highlight) var(--aurora-glow), transparent);
2896
+ --plan-orb-shadow:
2897
+ inset 0 1px 0 color-mix(in srgb, var(--el-accent-text) var(--aurora-sheen), transparent),
2898
+ 0 0 26px -2px color-mix(in srgb, var(--el-accent) var(--aurora-glow), transparent),
2899
+ 0 0 40px -6px color-mix(in srgb, var(--el-highlight) var(--aurora-glow), transparent);
2900
+ }
2901
+ }
2902
+ @scope ([data-style='aurora']) to ([data-style]) {
2903
+ [data-surface='ai-cta'][data-ai-cta='pill'] {
2904
+ background-size:
2905
+ 100% 28%,
2906
+ 100% 100%;
2907
+ background-position:
2908
+ top left,
2909
+ top left;
2910
+ background-repeat: no-repeat;
2911
+ --plan-hero-fill:
2912
+ linear-gradient(
2913
+ 180deg,
2914
+ color-mix(in srgb, var(--el-accent-text) var(--aurora-sheen), transparent),
2915
+ transparent 90%
2916
+ ),
2917
+ linear-gradient(
2918
+ 135deg,
2919
+ var(--el-accent),
2920
+ color-mix(in srgb, var(--el-accent) 86%, var(--el-highlight))
2921
+ );
2922
+ }
2923
+ }
2924
+
2925
+ /* 8 · 3D / Immersive — NOTHING. `--plan-hero-shadow` / `--plan-orb-shadow` own
2926
+ this control's depth and the components read them through a `var()` seam an
2927
+ inline `box-shadow` would otherwise beat (MOTIR-3522). A rule here would
2928
+ either lose to that inline declaration or take a `data-depth="key"` control
2929
+ off the plane ladder it is declared on (docs/styles/3d-immersive.md §4 / §4b).
2930
+ Deliberately empty — recorded so the absence reads as a decision, not an
2931
+ omission. */
2932
+
2933
+ /* 9 · Hand-Drawn / Indie — a drawn ink outline + a hand-placed offset.
2934
+ ⚠️ THE `position: relative` IS GUARDED, where the design's transcription
2935
+ states it bare. The orb is `.fixed`, and a fixed element is already a
2936
+ containing block for its own `::after`; re-declaring it `relative` would drop
2937
+ the orb out of fixed positioning entirely. Same `:not(.fixed):not(.absolute)
2938
+ :not(.sticky)` guard this style's own `.border` rule carries, for the same
2939
+ reason it carries it. */
2940
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
2941
+ [data-surface='ai-cta']:not(.fixed):not(.absolute):not(.sticky) {
2942
+ position: relative;
2943
+ }
2944
+ }
2945
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
2946
+ [data-surface='ai-cta'] {
2947
+ font-weight: 700;
2948
+ --plan-hero-shadow: 3px 4px 0 -1px color-mix(in srgb, var(--el-border-strong) 55%, transparent);
2949
+ --plan-orb-shadow: 3px 4px 0 -1px color-mix(in srgb, var(--el-border-strong) 55%, transparent);
2950
+ }
2951
+ }
2952
+ @scope ([data-style='hand-drawn-indie']) to ([data-style]) {
2953
+ [data-surface='ai-cta']::after {
2954
+ content: '';
2955
+ position: absolute;
2956
+ inset: -1px;
2957
+ pointer-events: none;
2958
+ border: 2px solid var(--el-border-strong);
2959
+ border-radius: inherit;
2960
+ filter: url(#hd-rough);
2961
+ }
2962
+ }
2963
+
2964
+ /* 10 · Neumorphism — moulded, NOT raised-and-glowing: no gradient, no glow, and
2965
+ the hairline is KEPT (structure never relies on shadow alone). */
2966
+ @scope ([data-style='neumorphism']) to ([data-style]) {
2967
+ [data-surface='ai-cta'] {
2968
+ border: 1px solid color-mix(in srgb, var(--el-accent) 70%, var(--el-text));
2969
+ --plan-hero-shadow:
2970
+ var(--neu-distance) var(--neu-distance) var(--neu-blur) var(--neu-shadow),
2971
+ calc(var(--neu-distance) * -1) calc(var(--neu-distance) * -1) var(--neu-blur) var(--neu-light);
2972
+ --plan-orb-shadow:
2973
+ var(--neu-distance) var(--neu-distance) var(--neu-blur) var(--neu-shadow),
2974
+ calc(var(--neu-distance) * -1) calc(var(--neu-distance) * -1) var(--neu-blur) var(--neu-light);
2975
+ }
2976
+ }
2977
+ @scope ([data-style='neumorphism']) to ([data-style]) {
2978
+ [data-surface='ai-cta'][data-ai-cta='pill'] {
2979
+ --plan-hero-fill: none;
2980
+ background-color: var(--el-accent);
2981
+ }
2982
+ }
2983
+ @scope ([data-style='neumorphism']) to ([data-style]) {
2984
+ [data-surface='ai-cta'][data-ai-cta='orb'] {
2985
+ --plan-orb-fill: none;
2986
+ background-color: var(--el-accent);
2987
+ }
2988
+ }
2989
+
2990
+ /* 11 · Retrofuturism — a chrome bevel + a CROWN-CONFINED specular streak.
2991
+ ⚠️ THE STREAK IS THE THING THAT HAD TO MOVE, not the bevel. Across the full
2992
+ box the diagonal at `--retro-spec` washes the label to 2.55:1 light / 2.22:1
2993
+ dark, and thinning it does not rescue it (18% still measures 3.67:1 dark, by
2994
+ which point the chrome has stopped reading). Sized to the top band it sits
2995
+ above the cap height, the label sits on the bevel BODY at 6.76:1 / 5.16:1, and
2996
+ the chrome still reads — a machined bevel IS a vertical light-to-dark ramp
2997
+ with a highlight on the crown. The `white` / `black` keywords are this style's
2998
+ own sanctioned lightness mixes, not a hue. */
2999
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
3000
+ [data-surface='ai-cta'] {
3001
+ border: 1px solid color-mix(in srgb, var(--el-accent-text) 30%, transparent);
3002
+ --plan-hero-shadow:
3003
+ inset 0 1px 0 color-mix(in srgb, var(--el-accent-text) var(--retro-bevel-light), transparent),
3004
+ 0 0 24px -4px color-mix(in srgb, var(--el-accent) var(--retro-glow), transparent);
3005
+ --plan-orb-shadow:
3006
+ inset 0 1px 0 color-mix(in srgb, var(--el-accent-text) var(--retro-bevel-light), transparent),
3007
+ 0 0 24px -4px color-mix(in srgb, var(--el-accent) var(--retro-glow), transparent);
3008
+ }
3009
+ }
3010
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
3011
+ [data-surface='ai-cta'][data-ai-cta='pill'] {
3012
+ background-size:
3013
+ 100% 28%,
3014
+ 100% 100%;
3015
+ background-position:
3016
+ top left,
3017
+ top left;
3018
+ background-repeat: no-repeat;
3019
+ --plan-hero-fill:
3020
+ linear-gradient(
3021
+ 104deg,
3022
+ transparent 30%,
3023
+ color-mix(in srgb, var(--el-accent-text) var(--retro-spec), transparent) 46%,
3024
+ transparent 62%
3025
+ ),
3026
+ linear-gradient(
3027
+ 180deg,
3028
+ color-mix(in srgb, var(--el-accent) 82%, white) 0%,
3029
+ var(--el-accent) 20%,
3030
+ color-mix(in srgb, var(--el-accent) 84%, black) 100%
3031
+ );
3032
+ }
3033
+ }
3034
+ @scope ([data-style='retrofuturism']) to ([data-style]) {
3035
+ [data-surface='ai-cta'][data-ai-cta='orb'] {
3036
+ /* Crown-confined, for the same reason as glass: unbounded it measures
3037
+ 3.34:1 light / 2.78:1 dark inside the glyph box. */
3038
+ background-size:
3039
+ 100% 26%,
3040
+ 100% 100%;
3041
+ background-position:
3042
+ top left,
3043
+ top left;
3044
+ background-repeat: no-repeat;
3045
+ --plan-orb-fill:
3046
+ linear-gradient(
3047
+ 180deg,
3048
+ color-mix(in srgb, var(--el-accent-text) var(--retro-bevel-light), transparent),
3049
+ transparent 90%
3050
+ ),
3051
+ radial-gradient(
3052
+ circle at 33% 27%,
3053
+ color-mix(in srgb, var(--el-accent-text) var(--orb-lit-mix), var(--el-accent)),
3054
+ var(--el-accent) 56%,
3055
+ color-mix(in srgb, var(--el-accent) 68%, var(--el-highlight))
3056
+ );
3057
+ }
2263
3058
  }
2264
3059
 
2265
3060
  /* ─────────────────────────────────────────────────────────────────────────
@@ -2533,7 +3328,25 @@
2533
3328
  nobody renders — the base layer IS how the browser reaches these values. */
2534
3329
  --el-status-implemented: color-mix(in srgb, var(--color-accent) 75%, var(--color-charcoal));
2535
3330
  --el-status-in-review: var(--color-primary); /* brand — differentiate from in-progress */
2536
- --el-status-done: var(--color-success); /* green resolved */
3331
+ /* green — RESOLVED (MOTIR-3991). The bare --color-success (#1aae39) was
3332
+ 2.93:1 on --el-card and 2.69:1 on --el-surface in the BASE palette's LIGHT
3333
+ theme — under 1.4.11's 3:1 for a graphical object, and the one palette x
3334
+ theme in which this token missed the bar. --color-success is NOT free to
3335
+ move: the same Tier-0 hue paints --el-success-surface's companion ink, the
3336
+ success toast/notice family and the chart-success step, each held to its
3337
+ own AA bar that pulls opposite to a dot's 3:1 — so the ramp takes one step
3338
+ of the palette's own emphasis ink instead of re-tuning the source. 85% of
3339
+ --color-success toward --color-charcoal gives #1e9c38, 3.58 / 3.29 / 3.58
3340
+ on card / surface / page-bg. --color-charcoal flips with the theme
3341
+ (#37352f light, #e5e5e5 dark), so the same expression DARKENS on white and
3342
+ LIGHTENS on near-black — dark goes 7.29 / 6.62 / 7.29:1. Both inputs stay
3343
+ palette-tuned, so the nine palettes that ride this base value re-skin it as
3344
+ before and each keeps its own --color-success; ΔE2000 to the nearest
3345
+ sibling stays >= 11.3 in all ten palettes x both themes. Why the base and
3346
+ not a [data-palette='motir'] block: same as --el-status-implemented above —
3347
+ <html> carries no data-appearance-scope, so the base layer IS how the
3348
+ browser reaches these values. */
3349
+ --el-status-done: color-mix(in srgb, var(--color-success) 85%, var(--color-charcoal));
2537
3350
  --el-status-blocked: var(--color-warning); /* amber — can't proceed (gap fixed) */
2538
3351
  --el-status-cancelled: var(--color-steel); /* terminal grey — not red (cancel ≠ error) */
2539
3352
 
@@ -2601,10 +3414,27 @@
2601
3414
  --el-label-4: var(--color-tint-lavender);
2602
3415
  --el-label-5: var(--color-tint-sky);
2603
3416
  --el-label-6: var(--color-tint-yellow);
2604
- /* Avatar ramp — KEEP the named keys (peach…yellow): lib/projects/avatar.ts persists
2605
- project.avatarColor these strings, so numbering them would break stored rows
2606
- (spec §7.1, rung-2 migration safety). The fallback fixes --el-type-* misuse #2 —
2607
- the mono initials tile keeps its blue but stops borrowing --el-type-task. */
3417
+ /* Avatar ramp — KEEP the named keys (peach…yellow): they are addressed BY NAME, as
3418
+ literal `bg-(--el-avatar-<key>)` class strings, by
3419
+ `app/(authed)/triage/_components/TriageAvatar.tsx` (a hash of the display name
3420
+ indexes a fixed six-entry array its ORDER is load-bearing too) and by the
3421
+ `--el-avatar-fallback` initial tiles in
3422
+ `app/(authed)/settings/workspace/_components/gitSettingsPrimitives.tsx` and
3423
+ `app/(authed)/settings/project/code-access/_components/CodeAccessSettings.tsx`.
3424
+ `app/tokens/page.tsx` enumerates all seven. Numbering them would break those
3425
+ lookups, and Tailwind cannot rewrite an arbitrary-value class it never sees.
3426
+ ⚠️ Until MOTIR-4344 this comment reached the same conclusion from MIGRATION
3427
+ safety instead: a module under `lib/projects/` was said to persist a per-project
3428
+ colour column whose values were these key names, so numbering them would break
3429
+ stored rows (spec §7.1, rung-2). Both halves of that warrant went in MOTIR-2680
3430
+ (commit c73fb9112, 2026-08-11, landed in #2046) —
3431
+ `prisma/migrations/20260811223000_drop_project_preset_avatar/migration.sql` drops
3432
+ the column outright ("The values are LOST, deliberately") and the same change
3433
+ deleted the module. The CONCLUSION survived its warrant; the warrant above is the
3434
+ one that is true today. Neither dead address is repeated here on purpose — a
3435
+ citation is only worth as much as the thing it addresses. The fallback fixes
3436
+ --el-type-* misuse #2 — the mono initials tile keeps its blue but stops
3437
+ borrowing --el-type-task. */
2608
3438
  --el-avatar-peach: var(--color-tint-peach);
2609
3439
  --el-avatar-rose: var(--color-tint-rose);
2610
3440
  --el-avatar-mint: var(--color-tint-mint);
@@ -2621,8 +3451,9 @@
2621
3451
  --el-notif-commented: var(--color-info); /* was --el-type-task */
2622
3452
  --el-notif-assigned: var(--color-accent-green); /* was --el-type-story */
2623
3453
  --el-notif-transitioned: var(--color-accent-teal); /* was --el-type-subtask */
2624
- /* The EXECUTOR badge on a Home row's assignee avatar (Story MOTIR-2649 ·
2625
- Subtask MOTIR-2653, per design/home/design-notes.md §"The agent state").
3454
+ /* The EXECUTOR badge on a Workbench row's assignee avatar (Story MOTIR-2649 ·
3455
+ Subtask MOTIR-2653, per design/workbench/design-notes.md §"The agent state"
3456
+ — that area was renamed with the surface by MOTIR-4779).
2626
3457
  Same mapping form as --el-notif-mentioned above and for the same reason:
2627
3458
  it resolves to the accent fill today, but owning its own slot means a
2628
3459
  palette can move agent badges without dragging the CTA with them. The
@@ -2964,7 +3795,7 @@
2964
3795
 
2965
3796
  Not part of the Tier-3 layer above and deliberately not named `--el-*`:
2966
3797
  that layer is a COLOUR contract (every member aliases a Tier-0 --color-*
2967
- and is asserted palette-dependent by tests/theme/paletteTokenCoverage).
3798
+ and is asserted palette-dependent by tests/theme/paletteTokenCoverage.test.ts).
2968
3799
  A knob here is a NUMBER inside a color-mix() — one value for every
2969
3800
  palette and both themes — so it fails both halves of that contract while
2970
3801
  being exactly the thing that has to be expressed once.
@@ -3026,7 +3857,7 @@
3026
3857
  `@layer utilities`; this rule is UNLAYERED, and an unlayered declaration
3027
3858
  outranks a layered one whatever their specificity. That is the whole of the
3028
3859
  mechanism — it does not depend on source order or on this file's position in
3029
- the `@import` list. Measured, not reasoned: tests/theme/reducedMotionSpinner
3860
+ the `@import` list. Measured, not reasoned: tests/theme/reducedMotionSpinner.test.ts
3030
3861
  reads it back off the COMPILED stylesheet for both consumer shapes.
3031
3862
  ═══════════════════════════════════════════════════════════════════════ */
3032
3863
  @media (prefers-reduced-motion: reduce) {