@motir/design-system 0.1.3 → 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,6 +642,43 @@
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)
@@ -555,7 +686,8 @@
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
688
  @scope ([data-style='glassmorphism']) to ([data-style]) {
558
- body {
689
+ body,
690
+ [data-app-shell] {
559
691
  background-image:
560
692
  radial-gradient(
561
693
  85% 80% at 6% -12%,
@@ -814,7 +946,8 @@
814
946
  renders on it), keeping AA intact. Dark-first — on the dark theme this reads
815
947
  as a Tron grid over the near-black canvas. */
816
948
  @scope ([data-style='cybercore-y2k']) to ([data-style]) {
817
- body {
949
+ body,
950
+ [data-app-shell] {
818
951
  background-image:
819
952
  linear-gradient(color-mix(in srgb, currentColor 6%, transparent) 1px, transparent 1px),
820
953
  linear-gradient(90deg, color-mix(in srgb, currentColor 6%, transparent) 1px, transparent 1px);
@@ -918,7 +1051,8 @@
918
1051
  palette. background-size > 100% gives the gradient room to drift; the whole
919
1052
  field eases back and forth over `--aurora-drift-duration`. */
920
1053
  @scope ([data-style='aurora']) to ([data-style]) {
921
- body {
1054
+ body,
1055
+ [data-app-shell] {
922
1056
  background-image:
923
1057
  radial-gradient(
924
1058
  58% 75% at 12% 0%,
@@ -964,10 +1098,14 @@
964
1098
  }
965
1099
 
966
1100
  /* Respect a reduced-motion preference — the aurora settles to a still gradient
967
- (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. */
968
1105
  @media (prefers-reduced-motion: reduce) {
969
1106
  @scope ([data-style='aurora']) to ([data-style]) {
970
- body {
1107
+ body,
1108
+ [data-app-shell] {
971
1109
  animation: none;
972
1110
  }
973
1111
  }
@@ -1097,6 +1235,13 @@
1097
1235
  --height-control: 40px;
1098
1236
  --height-input: 46px;
1099
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
+
1100
1245
  /* Motion — smooth and floaty. A slower ease for the depth to feel weighty.
1101
1246
  The signature INTERACTION is the pointer-parallax TILT below (the standard
1102
1247
  "3D card" effect — cards tip toward the cursor), driven by the
@@ -1115,9 +1260,28 @@
1115
1260
  never a raw hue), so the style/palette axes stay disjoint and the material-
1116
1261
  rule test passes: a palette swap re-tints the atmosphere, a style swap leaves
1117
1262
  hues alone. This is what makes the table/list/board pages (no card surfaces of
1118
- their own) read as immersive too. */
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. */
1119
1282
  @scope ([data-style='3d-immersive']) to ([data-style]) {
1120
- body {
1283
+ body,
1284
+ [data-app-shell] {
1121
1285
  background-image:
1122
1286
  radial-gradient(
1123
1287
  130% 85% at 50% -15%,
@@ -1585,6 +1749,107 @@
1585
1749
  }
1586
1750
  }
1587
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
+ }
1850
+ }
1851
+ }
1852
+
1588
1853
  /* ── Hand-Drawn / Indie (data-style="hand-drawn-indie") — Subtask 7.3.41 ─────
1589
1854
  DEFERRED follow-up. A sketchy, friendly, zine-like aesthetic. TWO moves make
1590
1855
  it read as genuinely hand-drawn, not just "softly rounded":
@@ -1890,7 +2155,8 @@
1890
2155
  the canvas and the surfaces below share `var(--el-surface)`, which is the whole
1891
2156
  neumorphic illusion. Palette-derived (no hue of its own). */
1892
2157
  @scope ([data-style='neumorphism']) to ([data-style]) {
1893
- body {
2158
+ body,
2159
+ [data-app-shell] {
1894
2160
  background-color: var(--el-surface);
1895
2161
  }
1896
2162
  }
@@ -2081,7 +2347,8 @@
2081
2347
  to the palette. Painted as the body's own background (always behind content),
2082
2348
  the proven 3D/Immersive atmosphere approach. */
2083
2349
  @scope ([data-style='retrofuturism']) to ([data-style]) {
2084
- body {
2350
+ body,
2351
+ [data-app-shell] {
2085
2352
  background-image:
2086
2353
  linear-gradient(
2087
2354
  180deg,
@@ -2105,16 +2372,41 @@
2105
2372
  }
2106
2373
  }
2107
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
+ }
2395
+ }
2396
+
2108
2397
  /* The Outrun GRID FLOOR — a receding neon grid below the horizon. A separate
2109
- 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
2110
2400
  into the horizon; `z-index: -1` keeps it BEHIND all app content (above only the
2111
- body's own sky background), so it textures the floor without ever covering the
2112
- UI. Lines are `color-mix()` over `--el-*` accents (palette-derived, no hue of
2113
- their own); the mask uses the achromatic `black` keyword (no hex). The grid is
2114
- STATIC depth — like 3D/Immersive's atmosphere it carries the identity, so it is
2115
- not motion-gated (there is no animation to suppress). */
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). */
2116
2407
  @scope ([data-style='retrofuturism']) to ([data-style]) {
2117
- body::after {
2408
+ body::after,
2409
+ [data-app-shell]::after {
2118
2410
  content: '';
2119
2411
  position: fixed;
2120
2412
  left: -30%;
@@ -2393,6 +2685,378 @@
2393
2685
  }
2394
2686
  }
2395
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
+ }
3058
+ }
3059
+
2396
3060
  /* ─────────────────────────────────────────────────────────────────────────
2397
3061
  Style-preview vignette material canvas (7.3.37 / MOTIR-1050).
2398
3062
  The StyleVignette preview specimen carries its OWN axis attribute on a
@@ -2750,10 +3414,27 @@
2750
3414
  --el-label-4: var(--color-tint-lavender);
2751
3415
  --el-label-5: var(--color-tint-sky);
2752
3416
  --el-label-6: var(--color-tint-yellow);
2753
- /* Avatar ramp — KEEP the named keys (peach…yellow): lib/projects/avatar.ts persists
2754
- project.avatarColor these strings, so numbering them would break stored rows
2755
- (spec §7.1, rung-2 migration safety). The fallback fixes --el-type-* misuse #2 —
2756
- 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. */
2757
3438
  --el-avatar-peach: var(--color-tint-peach);
2758
3439
  --el-avatar-rose: var(--color-tint-rose);
2759
3440
  --el-avatar-mint: var(--color-tint-mint);
@@ -2770,8 +3451,9 @@
2770
3451
  --el-notif-commented: var(--color-info); /* was --el-type-task */
2771
3452
  --el-notif-assigned: var(--color-accent-green); /* was --el-type-story */
2772
3453
  --el-notif-transitioned: var(--color-accent-teal); /* was --el-type-subtask */
2773
- /* The EXECUTOR badge on a Home row's assignee avatar (Story MOTIR-2649 ·
2774
- 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).
2775
3457
  Same mapping form as --el-notif-mentioned above and for the same reason:
2776
3458
  it resolves to the accent fill today, but owning its own slot means a
2777
3459
  palette can move agent badges without dragging the CTA with them. The
@@ -3113,7 +3795,7 @@
3113
3795
 
3114
3796
  Not part of the Tier-3 layer above and deliberately not named `--el-*`:
3115
3797
  that layer is a COLOUR contract (every member aliases a Tier-0 --color-*
3116
- and is asserted palette-dependent by tests/theme/paletteTokenCoverage).
3798
+ and is asserted palette-dependent by tests/theme/paletteTokenCoverage.test.ts).
3117
3799
  A knob here is a NUMBER inside a color-mix() — one value for every
3118
3800
  palette and both themes — so it fails both halves of that contract while
3119
3801
  being exactly the thing that has to be expressed once.
@@ -3175,7 +3857,7 @@
3175
3857
  `@layer utilities`; this rule is UNLAYERED, and an unlayered declaration
3176
3858
  outranks a layered one whatever their specificity. That is the whole of the
3177
3859
  mechanism — it does not depend on source order or on this file's position in
3178
- the `@import` list. Measured, not reasoned: tests/theme/reducedMotionSpinner
3860
+ the `@import` list. Measured, not reasoned: tests/theme/reducedMotionSpinner.test.ts
3179
3861
  reads it back off the COMPILED stylesheet for both consumer shapes.
3180
3862
  ═══════════════════════════════════════════════════════════════════════ */
3181
3863
  @media (prefers-reduced-motion: reduce) {