@evinvest/uikit 0.29.4 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +294 -90
  2. package/dist/{chunk-3IPB4QQJ.js → chunk-35SPIU4U.js} +10 -6
  3. package/dist/chunk-35SPIU4U.js.map +1 -0
  4. package/dist/components/account-list.js +128 -0
  5. package/dist/components/account-list.js.map +1 -0
  6. package/dist/components/account-menu-profile.js +114 -0
  7. package/dist/components/account-menu-profile.js.map +1 -0
  8. package/dist/components/account-menu.js +50 -23
  9. package/dist/components/account-menu.js.map +1 -1
  10. package/dist/components/account-settings-dialog.js +84 -0
  11. package/dist/components/account-settings-dialog.js.map +1 -0
  12. package/dist/components/alert-dialog.js +3 -2
  13. package/dist/components/alert-dialog.js.map +1 -1
  14. package/dist/components/animated-number-columns.js +105 -0
  15. package/dist/components/animated-number-columns.js.map +1 -0
  16. package/dist/components/animated-number.js +157 -0
  17. package/dist/components/animated-number.js.map +1 -0
  18. package/dist/components/app-shell.js +9 -0
  19. package/dist/components/app-shell.js.map +1 -1
  20. package/dist/components/aside.js +140 -0
  21. package/dist/components/aside.js.map +1 -0
  22. package/dist/components/badge.js +1 -1
  23. package/dist/components/badge.js.map +1 -1
  24. package/dist/components/button.js +3 -11
  25. package/dist/components/button.js.map +1 -1
  26. package/dist/components/chat-composer.js +141 -0
  27. package/dist/components/chat-composer.js.map +1 -0
  28. package/dist/components/chat-message.js +195 -0
  29. package/dist/components/chat-message.js.map +1 -0
  30. package/dist/components/chat-panel.js +137 -0
  31. package/dist/components/chat-panel.js.map +1 -0
  32. package/dist/components/chat-stream-text.js +69 -0
  33. package/dist/components/chat-stream-text.js.map +1 -0
  34. package/dist/components/chat-typing.js +26 -0
  35. package/dist/components/chat-typing.js.map +1 -0
  36. package/dist/components/command-palette.js +119 -0
  37. package/dist/components/command-palette.js.map +1 -0
  38. package/dist/components/command.js +3 -1
  39. package/dist/components/command.js.map +1 -1
  40. package/dist/components/confirm-dialog.js +123 -0
  41. package/dist/components/confirm-dialog.js.map +1 -0
  42. package/dist/components/context-menu.js +53 -5
  43. package/dist/components/context-menu.js.map +1 -1
  44. package/dist/components/dialog.js +3 -2
  45. package/dist/components/dialog.js.map +1 -1
  46. package/dist/components/drawer.js +3 -1
  47. package/dist/components/drawer.js.map +1 -1
  48. package/dist/components/dropdown-menu.js +64 -5
  49. package/dist/components/dropdown-menu.js.map +1 -1
  50. package/dist/components/form-row.js +51 -0
  51. package/dist/components/form-row.js.map +1 -0
  52. package/dist/components/header-controls.js +149 -0
  53. package/dist/components/header-controls.js.map +1 -0
  54. package/dist/components/icon-button.js +35 -0
  55. package/dist/components/icon-button.js.map +1 -0
  56. package/dist/components/key-value.js +68 -0
  57. package/dist/components/key-value.js.map +1 -0
  58. package/dist/components/menubar.js +39 -5
  59. package/dist/components/menubar.js.map +1 -1
  60. package/dist/components/native-select.js +3 -4
  61. package/dist/components/native-select.js.map +1 -1
  62. package/dist/components/nav-collapse.js +142 -0
  63. package/dist/components/nav-collapse.js.map +1 -0
  64. package/dist/components/nav-model.js.map +1 -1
  65. package/dist/components/navigation-menu.js +4 -5
  66. package/dist/components/navigation-menu.js.map +1 -1
  67. package/dist/components/page-frame.js +2 -5
  68. package/dist/components/page-frame.js.map +1 -1
  69. package/dist/components/popover-form.js +60 -0
  70. package/dist/components/popover-form.js.map +1 -0
  71. package/dist/components/resource-error.js +1 -1
  72. package/dist/components/resource-error.js.map +1 -1
  73. package/dist/components/section-nav.js +73 -20
  74. package/dist/components/section-nav.js.map +1 -1
  75. package/dist/components/select.js +16 -6
  76. package/dist/components/select.js.map +1 -1
  77. package/dist/components/settings.js +94 -0
  78. package/dist/components/settings.js.map +1 -0
  79. package/dist/components/sheet.js +3 -2
  80. package/dist/components/sheet.js.map +1 -1
  81. package/dist/components/shell-nav.js +80 -38
  82. package/dist/components/shell-nav.js.map +1 -1
  83. package/dist/components/smart-header.js +53 -0
  84. package/dist/components/smart-header.js.map +1 -0
  85. package/dist/components/status-item.js +42 -0
  86. package/dist/components/status-item.js.map +1 -0
  87. package/dist/components/system-banner.js +22 -12
  88. package/dist/components/system-banner.js.map +1 -1
  89. package/dist/components/top-bar.js +4 -3
  90. package/dist/components/top-bar.js.map +1 -1
  91. package/dist/components/ui-icons.js +77 -0
  92. package/dist/components/ui-icons.js.map +1 -0
  93. package/dist/components/use-count-tween.js +42 -0
  94. package/dist/components/use-count-tween.js.map +1 -0
  95. package/dist/components/workspace-tab.js +140 -0
  96. package/dist/components/workspace-tab.js.map +1 -0
  97. package/dist/generated/accent.js +2 -2
  98. package/dist/generated/accent.js.map +1 -1
  99. package/dist/generated/account-list.js +35 -0
  100. package/dist/generated/account-list.js.map +1 -0
  101. package/dist/generated/account-settings-dialog.js +27 -0
  102. package/dist/generated/account-settings-dialog.js.map +1 -0
  103. package/dist/generated/alert-dialog.js +1 -1
  104. package/dist/generated/alert-dialog.js.map +1 -1
  105. package/dist/generated/alert.js +2 -2
  106. package/dist/generated/alert.js.map +1 -1
  107. package/dist/generated/aside.js +36 -0
  108. package/dist/generated/aside.js.map +1 -0
  109. package/dist/generated/badge.js +7 -6
  110. package/dist/generated/badge.js.map +1 -1
  111. package/dist/generated/band.js +1 -1
  112. package/dist/generated/band.js.map +1 -1
  113. package/dist/generated/breadcrumb.js +2 -2
  114. package/dist/generated/breadcrumb.js.map +1 -1
  115. package/dist/generated/button-group.js +2 -2
  116. package/dist/generated/button-group.js.map +1 -1
  117. package/dist/generated/button.js +4 -4
  118. package/dist/generated/button.js.map +1 -1
  119. package/dist/generated/card.js +6 -6
  120. package/dist/generated/card.js.map +1 -1
  121. package/dist/generated/chat-composer.js +21 -0
  122. package/dist/generated/chat-composer.js.map +1 -0
  123. package/dist/generated/chat-message.js +86 -0
  124. package/dist/generated/chat-message.js.map +1 -0
  125. package/dist/generated/chat-typing.js +9 -0
  126. package/dist/generated/chat-typing.js.map +1 -0
  127. package/dist/generated/command-palette.js +17 -0
  128. package/dist/generated/command-palette.js.map +1 -0
  129. package/dist/generated/command.js +3 -3
  130. package/dist/generated/command.js.map +1 -1
  131. package/dist/generated/confirm-dialog.js +28 -0
  132. package/dist/generated/confirm-dialog.js.map +1 -0
  133. package/dist/generated/context-menu.js +1 -1
  134. package/dist/generated/context-menu.js.map +1 -1
  135. package/dist/generated/date-time-picker.js +1 -1
  136. package/dist/generated/date-time-picker.js.map +1 -1
  137. package/dist/generated/dialog.js +1 -1
  138. package/dist/generated/dialog.js.map +1 -1
  139. package/dist/generated/drawer.js +1 -1
  140. package/dist/generated/drawer.js.map +1 -1
  141. package/dist/generated/dropdown-menu.js +1 -1
  142. package/dist/generated/dropdown-menu.js.map +1 -1
  143. package/dist/generated/form-row.js +17 -0
  144. package/dist/generated/form-row.js.map +1 -0
  145. package/dist/generated/header-controls.js +56 -0
  146. package/dist/generated/header-controls.js.map +1 -0
  147. package/dist/generated/icon-button.js +13 -0
  148. package/dist/generated/icon-button.js.map +1 -0
  149. package/dist/generated/key-value.js +15 -0
  150. package/dist/generated/key-value.js.map +1 -0
  151. package/dist/generated/menubar.js +1 -1
  152. package/dist/generated/menubar.js.map +1 -1
  153. package/dist/generated/navigation-menu.js +1 -1
  154. package/dist/generated/navigation-menu.js.map +1 -1
  155. package/dist/generated/popover-form.js +13 -0
  156. package/dist/generated/popover-form.js.map +1 -0
  157. package/dist/generated/section-label.js +10 -0
  158. package/dist/generated/section-label.js.map +1 -0
  159. package/dist/generated/select.js +15 -3
  160. package/dist/generated/select.js.map +1 -1
  161. package/dist/generated/settings.js +36 -0
  162. package/dist/generated/settings.js.map +1 -0
  163. package/dist/generated/sheet.js +1 -1
  164. package/dist/generated/sheet.js.map +1 -1
  165. package/dist/generated/slider.js +1 -1
  166. package/dist/generated/slider.js.map +1 -1
  167. package/dist/generated/smart-header.js +11 -0
  168. package/dist/generated/smart-header.js.map +1 -0
  169. package/dist/generated/sonner.js +4 -4
  170. package/dist/generated/sonner.js.map +1 -1
  171. package/dist/generated/status-item.js +17 -0
  172. package/dist/generated/status-item.js.map +1 -0
  173. package/dist/generated/system-banner.js +33 -0
  174. package/dist/generated/system-banner.js.map +1 -0
  175. package/dist/generated/top-bar.js +9 -0
  176. package/dist/generated/top-bar.js.map +1 -0
  177. package/dist/generated/workspace-tab.js +17 -0
  178. package/dist/generated/workspace-tab.js.map +1 -0
  179. package/dist/index.d.ts +852 -156
  180. package/dist/index.js +21 -1
  181. package/dist/lib/cn.js +8 -1
  182. package/dist/lib/cn.js.map +1 -1
  183. package/dist/lib/number-parts.js +90 -0
  184. package/dist/lib/number-parts.js.map +1 -0
  185. package/dist/palette-cli.js +1 -1
  186. package/dist/palette.d.ts +6 -4
  187. package/dist/palette.js +3 -1
  188. package/dist/primitives/dismissable-layer.js +76 -4
  189. package/dist/primitives/dismissable-layer.js.map +1 -1
  190. package/dist/primitives/focus-scope.js +20 -5
  191. package/dist/primitives/focus-scope.js.map +1 -1
  192. package/dist/primitives/use-floating.js +16 -1
  193. package/dist/primitives/use-floating.js.map +1 -1
  194. package/dist/primitives/use-reduced-motion.js +20 -0
  195. package/dist/primitives/use-reduced-motion.js.map +1 -0
  196. package/package.json +1 -1
  197. package/styles/ev.css +281 -49
  198. package/styles/theme.css +335 -39
  199. package/styles/tokens.css +643 -103
  200. package/dist/chunk-3IPB4QQJ.js.map +0 -1
  201. package/dist/components/terminal.js +0 -206
  202. package/dist/components/terminal.js.map +0 -1
  203. package/dist/generated/terminal.js +0 -70
  204. package/dist/generated/terminal.js.map +0 -1
package/styles/tokens.css CHANGED
@@ -12,41 +12,53 @@
12
12
  * writes (or generates — `evinvest-palette`, see the uikit README)
13
13
  * its own palette against the same names under `[data-brand="…"]`.
14
14
  *
15
- * SCOPE :root · [data-brand] — a theme root; .light · .dark — a polarity
16
- * SURFACES background · card · popover · muted · hover
17
- * INK ink · ink-mid · ink-soft
18
- * LINES border · input · ring
15
+ * SCOPE :root · [data-brand] — a theme root; .light · .dark — a polarity;
16
+ * [data-theme="oldschool"] — EV's pre-1.0 navy look, opt-in
17
+ * SURFACES background · card · popover · muted · secondary
18
+ * STATES hover · active · selected — surface tints; scrim — behind a modal
19
+ * INK ink · ink-mid · ink-soft · ink-faint (disabled only)
20
+ * LINES border-subtle · border · border-strong · input · ring
19
21
  * ROLES {role} and on-{role} for the ones that get filled:
20
22
  * brand · primary · secondary · positive ·
21
23
  * accent-trace · accent-debug · accent-info · accent-warn · accent-error
22
24
  * primary-ink — the primary role as ink on a surface, where the
23
- * fill and the readable value are different colours
24
- * accent-error-ink — the error role as text on its own tint;
25
- * derived from accent-error and ink, a palette may pin it
26
- * SCALARS radius · control-* · display-scale · page-* · shell-* · shadows · fonts
25
+ * fill and the readable value are different colours;
26
+ * primary-hover · primary-border — the solid button's states
27
+ * positive-ink · accent-info-ink — a quiet status role as text on
28
+ * a surface; derived from the role and the ink, a palette may pin
29
+ * them. warn and error are text in their own colour
30
+ * FACES button-primary-* · button-danger-* ({bg,hover,border,ink}) and
31
+ * chip-{neutral,primary,positive,info,warn,error}-{bg,border}:
32
+ * what a Button or Badge paints, pinned per theme by EV and
33
+ * derived for a brand. Not part of what a brand file owes.
34
+ * TYPE text-caption · body-s · body · body-l · h3 · h2 · h1 ·
35
+ * figure-m · figure-l (size and line height); fonts
36
+ * SCALARS radius · control-radius · chip-radius · control-* · display-scale ·
37
+ * page-* · shell-* · depth-* (behind shadow-card/elevated/overlay)
27
38
  * MOTION ev-ease-* · ev-dur-* · ev-rise · ev-stagger(-section)
28
- * CHARTS chart-1 … chart-5
39
+ * CHARTS chart-1 … chart-6
29
40
  * MARK brand-mark · brand-aspect — supplied, never defined here
30
41
  *
31
42
  * Custom properties inherit, so a scope on a `<section>` re-themes its subtree:
32
43
  * `bg-card text-ink` is correct under any brand and either polarity with no
33
- * prop. A brand's palette declares `.light` and `.dark` beneath its own
34
- * `[data-brand]`, so a `<Section polarity="dark">` inside it takes that
35
- * brand's dark values — never EV's. EV is single-polarity: its palette sits on
36
- * the theme root alone and both polarity classes inherit it unchanged.
44
+ * prop. EV has two polarities of its own (Dark is the default, `.light` is
45
+ * Light) and a third theme; each pins every EV token, so a wrapper of one theme
46
+ * inside another replaces all of it. A brand's palette declares `.light` and
47
+ * `.dark` beneath its own `[data-brand]`, so a `<Section polarity="dark">`
48
+ * inside it takes that brand's dark values — never EV's: EV's rules do not
49
+ * match inside a brand scope.
37
50
  * Brand scopes do not nest: CSS has no "nearest ancestor", so a polarity class
38
51
  * inside a brand inside another brand may match either.
39
52
  *
40
53
  * Accents are ordered by significance, not by meaning; see docs/spec/accents.md.
41
54
  * Their values are read outside CSS (gitops mirrors them into Discord embed
42
- * bars), so they stay plain hex — no color-mix. `accent-error-ink` is not an
43
- * accent but a reading of one, so it lives with the derived tokens.
55
+ * bars), so they stay plain hex — no color-mix.
44
56
  */
45
57
 
46
58
  /*
47
59
  * Toast (sonner) and Drawer enter/exit motion, the reduced-motion fade for Sheet and
48
60
  * the dialogs, the entrances (rise / stagger / settled / skeleton) and the app-shell
49
- * markers, shared by both ports. Imported by the token
61
+ * markers, the chat caret and the docked aside, shared by both ports. Imported by the token
50
62
  * sheets so the lifecycle ships with the theme contract.
51
63
  */
52
64
 
@@ -493,7 +505,198 @@
493
505
  opacity: 0;
494
506
  }
495
507
  }
508
+ /* ── Chat: streaming text, caret, thinking dots, entrances ───────────────────
509
+ Streamed words (`chat-word`, from ChatStreamText) fade in from a 2px blur with
510
+ a small rise; each is an inline-block so `translate` applies, and the space
511
+ between words stays outside it so lines still wrap normally. The caret trails a
512
+ bubble that is still receiving text (`data-streaming`) and pulses instead of
513
+ blinking; once streaming ends the bubble keeps `data-caret`, so the caret fades
514
+ out by transition. The assistant's text sits a little dimmer while it arrives
515
+ and settles to full ink when it is done. Turns, markers and attachments fade in
516
+ as they mount; the log fades its top edge once scrolled, and the jump button
517
+ fades with its state. A docked aside is mounted and unmounted by its owner, so
518
+ it only has an enter: it slides in from the edge it docks to. Everything falls
519
+ back under reduced motion. */
520
+ @keyframes ev-word-in {
521
+ from {
522
+ opacity: 0;
523
+ filter: blur(2px);
524
+ translate: 0 3px;
525
+ }
526
+ }
527
+ [data-slot="chat-word"] {
528
+ display: inline-block;
529
+ animation: ev-word-in var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out) backwards;
530
+ }
531
+
532
+ [data-slot="chat-bubble"][data-variant="assistant"] {
533
+ transition: opacity var(--ev-dur-slow, 400ms) var(--ev-ease-out, ease-out);
534
+ }
535
+ [data-slot="chat-bubble"][data-variant="assistant"][data-streaming] {
536
+ opacity: 0.86;
537
+ }
538
+
539
+ [data-slot="chat-bubble"][data-caret]::after {
540
+ content: "";
541
+ display: inline-block;
542
+ width: 0.5em;
543
+ height: 1em;
544
+ margin-left: 2px;
545
+ vertical-align: text-bottom;
546
+ background: var(--ink-mid);
547
+ opacity: 0;
548
+ transition: opacity var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out);
549
+ }
550
+ [data-slot="chat-bubble"][data-streaming]::after {
551
+ animation: ev-caret-pulse 1.4s var(--ev-ease-in-out, ease-in-out) infinite;
552
+ }
553
+ @keyframes ev-caret-pulse {
554
+ 0%,
555
+ 100% {
556
+ opacity: 0.15;
557
+ }
558
+ 50% {
559
+ opacity: 0.9;
560
+ }
561
+ }
562
+
563
+ [data-slot="chat-typing"] {
564
+ animation: ev-enter-fade var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out) backwards;
565
+ }
566
+ [data-slot="chat-typing"] [data-still] {
567
+ display: none;
568
+ }
569
+ [data-slot="chat-typing"] [data-dot] {
570
+ animation: ev-typing-wave 1.2s var(--ev-ease-in-out, ease-in-out) infinite;
571
+ }
572
+ [data-slot="chat-typing"] [data-dot]:nth-child(2) {
573
+ animation-delay: 160ms;
574
+ }
575
+ [data-slot="chat-typing"] [data-dot]:nth-child(3) {
576
+ animation-delay: 320ms;
577
+ }
578
+ @keyframes ev-typing-wave {
579
+ 0%,
580
+ 60%,
581
+ 100% {
582
+ opacity: 0.25;
583
+ }
584
+ 30% {
585
+ opacity: 1;
586
+ }
587
+ }
588
+
589
+ @keyframes ev-chat-in {
590
+ from {
591
+ opacity: 0;
592
+ filter: blur(3px);
593
+ translate: 0 10px;
594
+ }
595
+ }
596
+ @keyframes ev-chat-in-user {
597
+ from {
598
+ opacity: 0;
599
+ filter: blur(3px);
600
+ translate: 0 10px;
601
+ scale: 0.98;
602
+ }
603
+ }
604
+ @keyframes ev-chat-part-in {
605
+ from {
606
+ opacity: 0;
607
+ translate: 0 4px;
608
+ }
609
+ }
610
+ [data-slot="chat-message"][data-enter="rise"] {
611
+ animation: ev-chat-in var(--ev-dur-slow, 400ms) var(--ev-ease-out, ease-out) var(--enter-delay, 0ms) backwards;
612
+ }
613
+ [data-slot="chat-message"][data-role="user"][data-enter="rise"] {
614
+ transform-origin: right bottom;
615
+ animation-name: ev-chat-in-user;
616
+ }
617
+ [data-slot="chat-marker"],
618
+ [data-slot="chat-attachment"] {
619
+ animation: ev-chat-part-in var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out) backwards;
620
+ }
621
+
622
+ @property --ev-chat-fade {
623
+ syntax: "<length>";
624
+ inherits: false;
625
+ initial-value: 0px;
626
+ }
627
+ [data-slot="chat-log"] {
628
+ mask-image: linear-gradient(to bottom, transparent, black var(--ev-chat-fade));
629
+ transition: --ev-chat-fade var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out);
630
+ }
631
+ [data-slot="chat-log"][data-scrolled] {
632
+ --ev-chat-fade: 28px;
633
+ }
634
+ [data-chat-jump] {
635
+ transition:
636
+ opacity var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out),
637
+ translate var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out);
638
+ }
639
+ [data-chat-jump="hidden"] {
640
+ opacity: 0;
641
+ translate: 0 6px;
642
+ pointer-events: none;
643
+ }
644
+
645
+ [data-slot="aside-panel"][data-mode="docked"] {
646
+ --ev-aside-from: 24px;
647
+ animation: ev-aside-enter var(--ev-dur-base, 280ms) var(--ev-ease-out, ease-out) backwards;
648
+ }
649
+ [data-slot="aside-panel"][data-mode="docked"][data-side="left"] {
650
+ --ev-aside-from: -24px;
651
+ }
652
+ @keyframes ev-aside-enter {
653
+ from {
654
+ opacity: 0;
655
+ transform: translateX(var(--ev-aside-from));
656
+ }
657
+ }
658
+
496
659
  @media (prefers-reduced-motion: reduce) {
660
+ [data-slot="chat-word"] {
661
+ animation: ev-enter-fade 1ms backwards;
662
+ /* beats the per-word stagger delay set inline */
663
+ animation-delay: 0ms !important;
664
+ }
665
+ [data-slot="chat-bubble"][data-variant="assistant"],
666
+ [data-slot="chat-bubble"][data-caret]::after,
667
+ [data-slot="chat-log"] {
668
+ transition: none;
669
+ }
670
+ [data-slot="chat-bubble"][data-variant="assistant"][data-streaming] {
671
+ opacity: 1;
672
+ }
673
+ [data-slot="chat-bubble"][data-streaming]::after {
674
+ animation: none;
675
+ opacity: 0.6;
676
+ }
677
+ [data-slot="chat-typing"],
678
+ [data-slot="chat-typing"] [data-dot] {
679
+ animation: none;
680
+ }
681
+ [data-slot="chat-typing"] [data-dot] {
682
+ display: none;
683
+ }
684
+ [data-slot="chat-typing"] [data-still] {
685
+ display: inline;
686
+ }
687
+ [data-slot="chat-message"][data-enter="rise"],
688
+ [data-slot="chat-message"][data-role="user"][data-enter="rise"],
689
+ [data-slot="chat-marker"],
690
+ [data-slot="chat-attachment"] {
691
+ animation: ev-enter-fade var(--ev-dur-fast, 180ms) linear backwards;
692
+ }
693
+ [data-chat-jump] {
694
+ transition: opacity var(--ev-dur-fast, 180ms) linear;
695
+ translate: none;
696
+ }
697
+ [data-slot="aside-panel"][data-mode="docked"] {
698
+ animation-name: ev-enter-fade;
699
+ }
497
700
  [data-enter="rise"],
498
701
  [data-enter="stagger"] > *,
499
702
  [data-slot="settled"][data-state="revealed"] {
@@ -518,29 +721,47 @@
518
721
  --radius-xl: calc(var(--radius) + 4px);
519
722
  --radius-2xl: calc(var(--radius) + 8px);
520
723
 
521
- /* Depth has to be a ladder, not a single drop shadow: on a dark theme the
522
- * value steps alone are too close to separate a resting card from a menu
523
- * from a modal, so each tier gets its own spread + opacity. */
524
- --shadow-card: 0 1px 2px 0 rgb(0 0 0 / 0.30);
525
- --shadow-elevated:
526
- 0 4px 12px -2px rgb(0 0 0 / 0.40),
527
- 0 2px 4px -2px rgb(0 0 0 / 0.30);
528
- --shadow-overlay: 0 16px 48px -12px rgb(0 0 0 / 0.60);
529
-
530
- /* The families are parameters: a brand scope sets `--brand-font-sans`,
531
- * `--brand-font-display` and `--brand-font-mono` (evinvest-palette writes
532
- * them from a brand file's [fonts]); `font-serif` is the display role under
533
- * its older name. Unset, each falls through to EV: next/font (landing) sets
534
- * `--font-inter` / `--font-playfair`, a Rust consumer `@font-face`s the same
535
- * families itself. Every leading var() carries its own fallback — an
724
+ /* The names stay, the values are per theme: u2 is flat (`--depth-*` is a
725
+ * zero shadow), Oldschool brings the ladder back in ev.css. */
726
+ --shadow-card: var(--depth-card);
727
+ --shadow-elevated: var(--depth-elevated);
728
+ --shadow-overlay: var(--depth-overlay);
729
+
730
+ /* Type scale: size / line-height in pairs, the Figma text styles. */
731
+ --text-caption: 0.6875rem;
732
+ --text-caption--line-height: 0.875rem;
733
+ --text-body-s: 0.75rem;
734
+ --text-body-s--line-height: 1rem;
735
+ --text-body: 0.8125rem;
736
+ --text-body--line-height: 1.125rem;
737
+ --text-body-l: 0.875rem;
738
+ --text-body-l--line-height: 1.25rem;
739
+ --text-h3: 0.875rem;
740
+ --text-h3--line-height: 1.25rem;
741
+ --text-h2: 1.5rem;
742
+ --text-h2--line-height: 1.875rem;
743
+ --text-h1: 2rem;
744
+ --text-h1--line-height: 2.375rem;
745
+ --text-figure-m: 1.25rem;
746
+ --text-figure-m--line-height: 1.5rem;
747
+ --text-figure-l: 1.75rem;
748
+ --text-figure-l--line-height: 2rem;
749
+
750
+ /* The families are parameters, apart from the colour contract: a brand scope
751
+ * sets `--brand-font-sans`, `--brand-font-serif` and `--brand-font-mono`
752
+ * (evinvest-palette writes them from a brand file's [fonts]). `serif` is the
753
+ * second face (headings, figures, wordmarks, italic accents), not necessarily
754
+ * one with serifs. Unset, each falls through to EV's: Inter, Inter Tight and
755
+ * JetBrains Mono (Oldschool sets its own, see ev.css). next/font (landing)
756
+ * sets `--font-inter` / `--font-inter-tight` / `--font-jetbrains-mono` /
757
+ * `--font-playfair`, a Rust consumer `@font-face`s the same families itself. Every leading var() carries its own fallback — an
536
758
  * undefined one would invalidate the whole `font-family` and drop the
537
759
  * browser to its default, so it must never resolve to guaranteed-invalid.
538
760
  * `inline` puts the var() chain in the utility itself, so it resolves on
539
761
  * the element and a brand scope re-fonts its subtree. */
540
762
  --font-sans: var(--brand-font-sans, var(--font-inter, "Inter")), ui-sans-serif, system-ui, sans-serif;
541
- --font-serif: var(--brand-font-display, var(--font-playfair, "Playfair Display")), ui-serif, Georgia, serif;
542
- --font-mono: var(--brand-font-mono, var(--font-inter, "Inter")), ui-monospace, monospace;
543
- --font-display: var(--brand-font-display, var(--font-playfair, "Playfair Display")), ui-serif, Georgia, serif;
763
+ --font-serif: var(--brand-font-serif, var(--font-inter-tight, "Inter Tight")), ui-sans-serif, system-ui, sans-serif;
764
+ --font-mono: var(--brand-font-mono, var(--font-jetbrains-mono, "JetBrains Mono")), ui-monospace, monospace;
544
765
 
545
766
  /* The colour contract: every `--color-X: var(--X)` below is a name a palette
546
767
  * owes, except the derived ones in the next rule. `evinvest-palette` reads
@@ -552,14 +773,20 @@
552
773
  --color-popover: var(--popover);
553
774
  --color-muted: var(--muted);
554
775
  --color-hover: var(--hover);
776
+ --color-active: var(--active);
777
+ --color-selected: var(--selected);
778
+ --color-scrim: var(--scrim);
555
779
 
556
780
  /* ink — hierarchy, loudest first */
557
781
  --color-ink: var(--ink);
558
782
  --color-ink-mid: var(--ink-mid);
559
783
  --color-ink-soft: var(--ink-soft);
784
+ --color-ink-faint: var(--ink-faint);
560
785
 
561
786
  /* lines */
787
+ --color-border-subtle: var(--border-subtle);
562
788
  --color-border: var(--border);
789
+ --color-border-strong: var(--border-strong);
563
790
  --color-input: var(--input);
564
791
  --color-ring: var(--ring);
565
792
 
@@ -568,21 +795,47 @@
568
795
  --color-primary: var(--primary);
569
796
  --color-on-primary: var(--on-primary);
570
797
  --color-primary-ink: var(--primary-ink);
798
+ --color-primary-hover: var(--primary-hover);
799
+ --color-primary-border: var(--primary-border);
571
800
  --color-secondary: var(--secondary);
572
801
  --color-on-secondary: var(--on-secondary);
573
802
  --color-positive: var(--positive);
574
803
  --color-on-positive: var(--on-positive);
804
+ --color-positive-ink: var(--positive-ink);
575
805
  --color-accent-trace: var(--accent-trace);
576
806
  --color-on-accent-trace: var(--on-accent-trace);
577
807
  --color-accent-debug: var(--accent-debug);
578
808
  --color-on-accent-debug: var(--on-accent-debug);
579
809
  --color-accent-info: var(--accent-info);
580
810
  --color-on-accent-info: var(--on-accent-info);
811
+ --color-accent-info-ink: var(--accent-info-ink);
581
812
  --color-accent-warn: var(--accent-warn);
582
813
  --color-on-accent-warn: var(--on-accent-warn);
583
814
  --color-accent-error: var(--accent-error);
584
815
  --color-on-accent-error: var(--on-accent-error);
585
- --color-accent-error-ink: var(--accent-error-ink);
816
+
817
+ /* component faces: derived below, pinned per theme by EV, so `bg-button-primary-bg`
818
+ * and `border-chip-error-border` are real utilities the class merge understands */
819
+ --color-button-primary-bg: var(--button-primary-bg);
820
+ --color-button-primary-hover: var(--button-primary-hover);
821
+ --color-button-primary-border: var(--button-primary-border);
822
+ --color-button-primary-ink: var(--button-primary-ink);
823
+ --color-button-danger-bg: var(--button-danger-bg);
824
+ --color-button-danger-hover: var(--button-danger-hover);
825
+ --color-button-danger-border: var(--button-danger-border);
826
+ --color-button-danger-ink: var(--button-danger-ink);
827
+ --color-chip-neutral-bg: var(--chip-neutral-bg);
828
+ --color-chip-neutral-border: var(--chip-neutral-border);
829
+ --color-chip-primary-bg: var(--chip-primary-bg);
830
+ --color-chip-primary-border: var(--chip-primary-border);
831
+ --color-chip-positive-bg: var(--chip-positive-bg);
832
+ --color-chip-positive-border: var(--chip-positive-border);
833
+ --color-chip-info-bg: var(--chip-info-bg);
834
+ --color-chip-info-border: var(--chip-info-border);
835
+ --color-chip-warn-bg: var(--chip-warn-bg);
836
+ --color-chip-warn-border: var(--chip-warn-border);
837
+ --color-chip-error-bg: var(--chip-error-bg);
838
+ --color-chip-error-border: var(--chip-error-border);
586
839
 
587
840
  /* charts — distinguishable hues carrying no significance */
588
841
  --color-chart-1: var(--chart-1);
@@ -590,42 +843,98 @@
590
843
  --color-chart-3: var(--chart-3);
591
844
  --color-chart-4: var(--chart-4);
592
845
  --color-chart-5: var(--chart-5);
846
+ --color-chart-6: var(--chart-6);
593
847
  }
594
848
 
595
849
  /* Derived from the palette, and redeclared on every scope a palette can be
596
850
  * declared on. A var() inside a custom property is resolved where it is
597
851
  * declared and the result is what inherits — declared on `:root` alone, a brand
598
852
  * scope would change the text and keep EV's borders, hover and focus ring.
599
- * Zero specificity, so a palette that sets one of these outright wins. */
600
- :where(:root, .dark, .light, [data-brand]) {
601
- /* the menu-item tint: a surface state, so it is named for the state */
602
- --hover: color-mix(in srgb, var(--ink) 8%, transparent);
853
+ * Zero specificity, so a palette that sets one of these outright wins. These
854
+ * are the formulas a brand file gets; EV pins its own per theme in ev.css. */
855
+ :where(:root, .dark, .light, [data-brand], [data-theme]) {
856
+ /* surface states: the menu-item tint, the pressed tint, the chosen row */
857
+ --hover: color-mix(in srgb, var(--ink) 6%, transparent);
858
+ --active: color-mix(in srgb, var(--ink) 10%, transparent);
859
+ --selected: color-mix(in srgb, var(--ink) 10%, transparent);
860
+ /* behind a modal layer; black on either polarity keeps the page readable
861
+ * through it without tinting the brand */
862
+ --scrim: color-mix(in srgb, black 50%, transparent);
603
863
  --ink-mid: color-mix(in srgb, var(--ink) 82%, transparent);
604
864
  /* the most-used secondary text token, so it has to clear AA on its own */
605
- --ink-soft: color-mix(in srgb, var(--ink) 65%, transparent);
606
- /* a divider: decoration, so it stays quiet */
865
+ --ink-soft: color-mix(in srgb, var(--ink) 60%, transparent);
866
+ /* disabled only: exempt from the text floor because nothing can be done with it */
867
+ --ink-faint: color-mix(in srgb, var(--ink) 38%, transparent);
868
+ /* three steps of line: a row rule, a divider, and a panel edge. Decoration,
869
+ * so they stay quiet; none of them is a control edge (see --input) */
870
+ --border-subtle: color-mix(in srgb, var(--ink) 8%, transparent);
607
871
  --border: color-mix(in srgb, var(--ink) 14%, transparent);
872
+ --border-strong: color-mix(in srgb, var(--ink) 26%, transparent);
608
873
  /* the only edge an unfilled control has, so it owes the non-text 3:1 on every
609
874
  * surface (lib#154). One mix serves both polarities; light is the binding
610
875
  * side — ink over white needs ~47% to clear it, dark lands near 4:1. */
611
876
  --input: color-mix(in srgb, var(--ink) 48%, transparent);
612
877
  --ring: var(--primary-ink);
613
- /* the error role as text on its own 10 % tint — a destructive menu row
614
- * (lib#166). The fill alone read 3.76:1 there on EV and 2.48:1 on a dark
615
- * brand; 30 % of the ink pulls it toward whichever polarity the text needs.
616
- * Derived rather than required so a brand file that predates it still
617
- * renders; one that misses the floor pins its own (docs/spec/accents.md). */
618
- --accent-error-ink: color-mix(in srgb, var(--accent-error) 70%, var(--ink));
878
+ --primary-hover: color-mix(in srgb, var(--primary) 95%, black);
879
+ --primary-border: color-mix(in srgb, var(--primary-ink) 55%, transparent);
880
+ /* the quiet status roles as text on a surface: a fill is a shape, and as text
881
+ * it reads far below AA on a light plane. warn and error have none: they are
882
+ * text in their own colour (docs/spec/accents.md). 60 % of the role toward
883
+ * the ink is the fallback for a brand that pins nothing. */
884
+ --positive-ink: color-mix(in srgb, var(--positive) 60%, var(--ink));
885
+ --accent-info-ink: color-mix(in srgb, var(--accent-info) 60%, var(--ink));
886
+ }
887
+
888
+ /* Component faces built from the palette. Derived like the rule above (a brand
889
+ * file owes none of them and may pin any), but kept in a rule of their own: the
890
+ * same scopes, listed in another order so the contract reader finds one rule
891
+ * per selector. */
892
+ :where([data-theme], [data-brand], .light, .dark, :root) {
893
+ /* primary is an outline button, danger the same in the error role */
894
+ --button-primary-bg: transparent;
895
+ --button-primary-hover: var(--hover);
896
+ --button-primary-border: color-mix(in srgb, var(--ink) 60%, transparent);
897
+ --button-primary-ink: var(--ink);
898
+ --button-danger-bg: transparent;
899
+ --button-danger-hover: color-mix(in srgb, var(--accent-error) 10%, transparent);
900
+ --button-danger-border: var(--accent-error);
901
+ --button-danger-ink: var(--accent-error);
902
+
903
+ /* chips (Badge, status pills). The source is the colour the label wears, so
904
+ * the edge and the label are one hue; --chip-fill and --chip-edge are in the
905
+ * geometry. */
906
+ --chip-neutral-bg: var(--hover);
907
+ --chip-neutral-border: var(--border);
908
+ --chip-primary-bg: color-mix(in srgb, var(--primary-ink) var(--chip-fill), transparent);
909
+ --chip-primary-border: color-mix(in srgb, var(--primary-ink) var(--chip-edge), transparent);
910
+ --chip-positive-bg: color-mix(in srgb, var(--positive-ink) var(--chip-fill), transparent);
911
+ --chip-positive-border: color-mix(in srgb, var(--positive-ink) var(--chip-edge), transparent);
912
+ --chip-info-bg: color-mix(in srgb, var(--accent-info-ink) var(--chip-fill), transparent);
913
+ --chip-info-border: color-mix(in srgb, var(--accent-info-ink) var(--chip-edge), transparent);
914
+ --chip-warn-bg: color-mix(in srgb, var(--accent-warn) var(--chip-fill), transparent);
915
+ --chip-warn-border: color-mix(in srgb, var(--accent-warn) var(--chip-edge), transparent);
916
+ --chip-error-bg: color-mix(in srgb, var(--accent-error) var(--chip-fill), transparent);
917
+ --chip-error-border: color-mix(in srgb, var(--accent-error) var(--chip-edge), transparent);
619
918
  }
620
919
 
621
920
  /* Geometry. A `[data-brand]` scope is a theme root of its own, so it restarts
622
921
  * from these defaults (and from the scale-ups below) rather than inheriting
623
- * whatever an ancestor computed — `--control-radius` in particular is derived
624
- * from `--radius` and has to be recomputed wherever `--radius` may change. */
922
+ * whatever an ancestor computed. */
625
923
  :where(:root, [data-brand]) {
626
- --radius: 0.375rem; /* 6px on mobile; bumped to 8px ≥ sm (see @media below) */
924
+ --radius: 0.375rem; /* panels and menus */
627
925
  /* control geometry, so a consumer reshapes every button/input at once */
628
- --control-radius: calc(var(--radius) - 2px);
926
+ --control-radius: 0.25rem;
927
+ --chip-radius: 0.25rem;
928
+ /* how much of its ink a chip's fill and edge take, for a brand that pins
929
+ * nothing: an outline by default, a tint where a theme raises the fill */
930
+ --chip-fill: 0%;
931
+ --chip-edge: 50%;
932
+ /* u2 is flat: depth is surface steps and hairlines. Never `none` — inside
933
+ * Tailwind's comma-separated box-shadow list it invalidates the whole
934
+ * declaration, and the focus ring goes with it. */
935
+ --depth-card: 0 0 #0000;
936
+ --depth-elevated: 0 0 #0000;
937
+ --depth-overlay: 0 0 #0000;
629
938
  --control-py: 0.5rem;
630
939
  /* the Xl (call-to-action) button reads all three */
631
940
  --control-px: 2rem;
@@ -681,11 +990,10 @@
681
990
  --ev-shell-offset: 0px;
682
991
  }
683
992
 
684
- /* Mobile-first scale-ups: roomier gutters + slightly larger radii on bigger
685
- viewports. Lives here (not per-component) so every consumer scales alike. */
993
+ /* Mobile-first scale-ups: roomier gutters on bigger viewports. Lives here (not
994
+ per-component) so every consumer scales alike. */
686
995
  @media (min-width: 640px) {
687
996
  :where(:root, [data-brand]) {
688
- --radius: 0.5rem;
689
997
  --page-px: 1.5rem;
690
998
  }
691
999
  }
@@ -705,75 +1013,307 @@
705
1013
 
706
1014
  /* ═══ PALETTE: EV ════════════════════════════════════════════════
707
1015
  *
708
- * Zero specificity: EV is the default a brand scope overrides, whatever order
709
- * the two sheets are imported in.
1016
+ * Three themes, each a zero-specificity rule: a brand scope overrides them
1017
+ * whatever order the sheets are imported in, and later rules here win over
1018
+ * earlier ones on the same element (so the order below is load-bearing).
1019
+ *
1020
+ * u2 Dark the default: `:root`, `[data-brand="ev"]`, or a `.dark` wrapper
1021
+ * u2 Light a `.light` wrapper (on <html> or on any section)
1022
+ * Oldschool `[data-theme="oldschool"]`: the navy look the kit shipped before
1023
+ * 1.0, single-polarity, rounder, with shadows
1024
+ *
1025
+ * Each rule pins every EV-specific token, so a wrapper of one theme nested in
1026
+ * another replaces all of it and leaks nothing. A brand scope is not EV: the
1027
+ * `:not(…)` guard keeps these pins out of a `[data-brand]` subtree (and off the
1028
+ * brand's own element), where the brand's palette and the derived formulas in
1029
+ * theme.css apply instead. Without it a brand's `.light` would inherit EV
1030
+ * Light's hover and borders.
1031
+ *
1032
+ * Surface ladder — the u2 planes sit 1.04–1.14:1 apart, which is the design
1033
+ * (flat, no tinted fills) and under the ~1.2 floor in AGENTS.md. Planes are
1034
+ * separated by a `--border` hairline instead: 1.37–1.46:1 on Dark and 1.35:1 on
1035
+ * Light, measured with the alpha composited onto the plane. Do not lift a plane
1036
+ * to chase the ratio; retune the hairline, and tokens.test.ts will measure it.
1037
+ * Oldschool keeps its own, wider ladder: #070d18 → card #16233c (1.24:1) →
1038
+ * popover #1d2c49 (1.13:1), with --primary held at 3:1 against the popover.
710
1039
  *
711
- * Surface ladder — dark UI reads as unfinished when every plane is the same
712
- * value, so the steps below are deliberate and measured; keep them apart if you
713
- * retune. background #070d18 → secondary #0d1526 (1.07:1) → card #16233c (1.24:1
714
- * vs background) → popover #1d2c49 (1.13:1 vs card) → muted #26375c. Elevation
715
- * rides the same ladder via --shadow-card / -elevated / -overlay. Card is also
716
- * pinned from above by a second floor: --primary has to clear 3:1 against it
717
- * (3.39:1 today), so lifting card one step breaks the tokens test.
1040
+ * Where a Figma value failed a floor the nearest passing one is pinned
1041
+ * instead; the comment beside each says which.
718
1042
  */
719
- :where(:root, [data-brand="ev"]) {
1043
+
1044
+ /* ── u2 Dark ─────────────────────────────────────────────────── */
1045
+ :where(:is(:root, [data-brand="ev"], .dark):not([data-brand]:not([data-brand="ev"]), [data-brand]:not([data-brand="ev"]) *)) {
720
1046
  /* the polarity, for what the platform draws itself (a native <select>'s
721
1047
  * popup); a custom property so nothing but the kit's opt-ins reads it */
722
1048
  --scheme: dark;
723
1049
 
724
- /* ── surfaces ────────────────────────────────────────────────── */
1050
+ /* surfaces */
1051
+ --background: #0d0d0f;
1052
+ --card: #151518;
1053
+ --secondary: #1c1c20;
1054
+ --on-secondary: var(--ink);
1055
+ --popover: #252529;
1056
+ --muted: #070709;
1057
+ --hover: #e6e1d30f;
1058
+ --active: #e6e1d31a;
1059
+ --selected: #e6e1d31a;
1060
+ --scrim: #000000a8;
1061
+
1062
+ /* ink: the Figma steps, opaque, so the text floor is measured on the value
1063
+ * that paints (tertiary is 4.53:1 on the popover, the tightest plane) */
1064
+ --ink: #e6e1d3;
1065
+ --ink-mid: #bebab1;
1066
+ --ink-soft: #8d8c87;
1067
+ --ink-faint: #5e5e5b;
1068
+
1069
+ /* lines. --input stays the derived 48 % ink: the control edge owes 3:1 and
1070
+ * Figma has no variable for it. */
1071
+ --border-subtle: #e6e1d314;
1072
+ --border: #e6e1d324;
1073
+ --border-strong: #e6e1d342;
1074
+ --ring: #35a395;
1075
+
1076
+ /* brand and primary. A fill and an ink are different jobs (docs/spec/accents.md):
1077
+ * --primary carries white at 4.62:1 and reads as a shape on every plane (3.30:1
1078
+ * on the popover); --primary-ink is the teal that reads as text (4.96:1 there).
1079
+ * Figma's hover #18998b is 3.52:1 under white, so #127c71 (5.06:1) is pinned. */
1080
+ --brand: #0d1627;
1081
+ --primary: #138377;
1082
+ --on-primary: #ffffff;
1083
+ --primary-ink: #35a395;
1084
+ --primary-hover: #127c71;
1085
+ --primary-border: #35a3958c;
1086
+
1087
+ /* status. Fills carry the dark label (white fails 3:1–3.4:1 on these); the
1088
+ * *-ink values are the ones to use as text. */
1089
+ --positive: #2e9e5c;
1090
+ --on-positive: #0d0d0f;
1091
+ --positive-ink: #4abf76;
1092
+ --accent-trace: #6b7fa0;
1093
+ --on-accent-trace: #0d0d0f;
1094
+ --accent-debug: #35a395;
1095
+ --on-accent-debug: #0d0d0f;
1096
+ --accent-info: #4990d8;
1097
+ --on-accent-info: #0d0d0f;
1098
+ --accent-info-ink: #6ca6e4;
1099
+ --accent-warn: #f5c84e;
1100
+ --on-accent-warn: #0d0d0f;
1101
+ --accent-error: #f05b52;
1102
+ --on-accent-error: #0d0d0f;
1103
+
1104
+ --chart-1: #35a395;
1105
+ --chart-2: #4990d8;
1106
+ --chart-3: #f5c84e;
1107
+ --chart-4: #e68aaf;
1108
+ --chart-5: #2e9e5c;
1109
+ --chart-6: #988eef;
1110
+
1111
+ --button-primary-bg: #e6e1d300;
1112
+ --button-primary-hover: #e6e1d314;
1113
+ --button-primary-border: #e6e1d399;
1114
+ --button-primary-ink: #e6e1d3;
1115
+ --button-danger-bg: #f05b5200;
1116
+ --button-danger-hover: #f05b521a;
1117
+ --button-danger-border: #f05b52;
1118
+ --button-danger-ink: var(--accent-error);
1119
+
1120
+ /* chips are outlines: a transparent fill and the label's hue at 50 % */
1121
+ --chip-neutral-bg: var(--hover);
1122
+ --chip-neutral-border: var(--border);
1123
+ --chip-primary-bg: #35a39500;
1124
+ --chip-primary-border: #35a3958c;
1125
+ --chip-positive-bg: #2e9e5c00;
1126
+ --chip-positive-border: #4abf7680;
1127
+ --chip-info-bg: #4990d800;
1128
+ --chip-info-border: #6ca6e480;
1129
+ --chip-warn-bg: #f5c84e00;
1130
+ --chip-warn-border: #f5c84e80;
1131
+ --chip-error-bg: #f05b5200;
1132
+ --chip-error-border: #f05b5280;
1133
+ }
1134
+
1135
+ /* ── u2 Light ────────────────────────────────────────────────── */
1136
+ :where(.light:not([data-brand]:not([data-brand="ev"]), [data-brand]:not([data-brand="ev"]) *)) {
1137
+ --scheme: light;
1138
+
1139
+ --background: #efebdf;
1140
+ --card: #f6f3eb;
1141
+ --secondary: #fbfaf6;
1142
+ --on-secondary: var(--ink);
1143
+ --popover: #ffffff;
1144
+ --muted: #e6e1d3;
1145
+ --hover: #0d0d0f0d;
1146
+ --active: #0d0d0f17;
1147
+ --selected: #0d0d0f14;
1148
+ --scrim: #00000066;
1149
+
1150
+ --ink: #0d0d0f;
1151
+ --ink-mid: #3d3d41;
1152
+ --ink-soft: #67696c;
1153
+ --ink-faint: #9a9b9e;
1154
+
1155
+ --border-subtle: #0d0d0f14;
1156
+ --border: #0d0d0f24;
1157
+ --border-strong: #0d0d0f47;
1158
+ --ring: #138377;
1159
+
1160
+ --brand: #080e1a;
1161
+ --primary: #138377;
1162
+ --on-primary: #ffffff;
1163
+ --primary-ink: #116f65;
1164
+ --primary-hover: #116f65;
1165
+ --primary-border: #116f658c;
1166
+
1167
+ /* Figma's positive #228a4e carries neither label at 4.5:1 (white 4.36, dark
1168
+ * 4.45), so the fill is one step darker: white reads at 5.01:1. */
1169
+ --positive: #1f7f48;
1170
+ --on-positive: #ffffff;
1171
+ --positive-ink: #1b7943;
1172
+ --accent-trace: #5a6b7c;
1173
+ --on-accent-trace: #ffffff;
1174
+ --accent-debug: #116f65;
1175
+ --on-accent-debug: #ffffff;
1176
+ --accent-info: #2d79c2;
1177
+ --on-accent-info: #ffffff;
1178
+ --accent-info-ink: #25639f;
1179
+ /* Figma's #d8a523 is 1.89:1 on the page plane; warn is text in its own
1180
+ * colour, so it is darkened toward Figma's warn text #846305 until it clears
1181
+ * the non-text 3:1 (3.10:1) */
1182
+ --accent-warn: #a77f12;
1183
+ --on-accent-warn: #0d0d0f;
1184
+ --accent-error: #db3834;
1185
+ --on-accent-error: #ffffff;
1186
+
1187
+ --chart-1: #116f65;
1188
+ --chart-2: #2d79c2;
1189
+ --chart-3: #d8a523;
1190
+ --chart-4: #b74679;
1191
+ --chart-5: #228a4e;
1192
+ --chart-6: #6d5acc;
1193
+
1194
+ --button-primary-bg: #0d0d0f00;
1195
+ --button-primary-hover: #0d0d0f0f;
1196
+ --button-primary-border: #0d0d0fa6;
1197
+ --button-primary-ink: #080e1a;
1198
+ --button-danger-bg: #db383400;
1199
+ --button-danger-hover: #db383414;
1200
+ --button-danger-border: #db3834;
1201
+ --button-danger-ink: var(--accent-error);
1202
+
1203
+ /* the solid hue at 50 % (warning 60 %), where Dark uses the ink's */
1204
+ --chip-neutral-bg: var(--hover);
1205
+ --chip-neutral-border: var(--border);
1206
+ --chip-primary-bg: #13837700;
1207
+ --chip-primary-border: #116f658c;
1208
+ --chip-positive-bg: #228a4e00;
1209
+ --chip-positive-border: #228a4e80;
1210
+ --chip-info-bg: #2d79c200;
1211
+ --chip-info-border: #2d79c280;
1212
+ --chip-warn-bg: #a77f1200;
1213
+ --chip-warn-border: #a77f1299;
1214
+ --chip-error-bg: #db383400;
1215
+ --chip-error-border: #db383480;
1216
+ }
1217
+
1218
+ /* ── Oldschool ───────────────────────────────────────────────── *
1219
+ * Opt-in. Dark-only, so a `.light` or `.dark` inside it takes these values
1220
+ * too. The surfaces are the ones the kit shipped (they pass every floor in
1221
+ * tokens.test.ts); Figma's raised/overlay #1f2d4b measured 2.96:1 for the fill
1222
+ * and 4.06:1 for tertiary text, so the popover stays #1d2c49. Its ink steps and
1223
+ * --border are the derived formulas for the same reason: Figma's opaque
1224
+ * border #1b2843 is 1.1:1 on the card and would leave the planes unseparated.
1225
+ */
1226
+ :where([data-theme="oldschool"], [data-theme="oldschool"] :is(.light, .dark)) {
1227
+ --scheme: dark;
1228
+
1229
+ --brand-font-serif: var(--font-playfair, "Playfair Display");
1230
+ --brand-font-mono: var(--font-inter, "Inter");
1231
+
725
1232
  --background: #070d18;
726
1233
  --card: #16233c;
1234
+ --secondary: #0d1526;
1235
+ --on-secondary: var(--ink);
727
1236
  --popover: #1d2c49;
728
1237
  --muted: #26375c;
1238
+ --hover: #e6e1d312;
1239
+ --active: #e6e1d31f;
1240
+ --selected: #35a3952e;
1241
+ --scrim: #02050ea3;
729
1242
 
730
- /* ── ink ─────────────────────────────────────────────────────── */
731
1243
  --ink: #e6e1d3;
1244
+ --ink-mid: color-mix(in srgb, var(--ink) 82%, transparent);
1245
+ --ink-soft: color-mix(in srgb, var(--ink) 60%, transparent);
1246
+ --ink-faint: color-mix(in srgb, var(--ink) 38%, transparent);
732
1247
 
733
- /* ── roles ───────────────────────────────────────────────────── */
734
- --brand: #001e4e;
735
- /* teal, not the brand navy: navy filled a Button/Switch at ~1.1:1 against
736
- * these surfaces, i.e. an invisible control, and consumers hardcoded teal to
737
- * get around it. The brand navy still lives in --brand.
738
- *
739
- * A fill and an ink are different jobs, so the primary role carries two
740
- * values (docs/spec/accents.md). --primary-ink is the teal that reads on the
741
- * surfaces: 5.85:1 on background, 4.72:1 on card. --primary is the same hue
742
- * and chroma one step down in lightness — OKLCH L 0.550 C 0.094 H 183°
743
- * against the ink's L 0.630 C 0.101 H 183° — so the CTA carries white:
744
- * --on-primary reads on it at 4.63:1, and the fill clears the 3:1 non-text
745
- * floor against background (4.20:1), secondary (3.94:1), card (3.39:1) and
746
- * popover (3.01:1), so a checked Switch on a Card still reads as one. Muted
747
- * is the one surface under the floor (2.54:1); the controls that fill there
748
- * carry an on-primary mark, see the spec. Darkening the single value instead
749
- * would have dropped every teal label below AA. */
750
- --primary: #128377;
1248
+ --border-subtle: #e6e1d312;
1249
+ --border: color-mix(in srgb, var(--ink) 14%, transparent);
1250
+ --border-strong: #2e3e64;
1251
+ --ring: #35a395;
1252
+
1253
+ --brand: #080e1a;
1254
+ --primary: #138377;
751
1255
  --on-primary: #ffffff;
752
- --primary-ink: #2a9d8f;
753
- --secondary: #0d1526;
754
- --on-secondary: var(--ink);
755
- /* valence, not significance: a success reads as good while staying quiet */
756
- --positive: #2e9e5b;
1256
+ --primary-ink: #35a395;
1257
+ --primary-hover: #127c71;
1258
+ --primary-border: #35a39500;
1259
+
1260
+ --positive: #2e9e5c;
757
1261
  --on-positive: #070d18;
1262
+ --positive-ink: #4abf76;
758
1263
  --accent-trace: #6b7fa0;
759
1264
  --on-accent-trace: #070d18;
760
- --accent-debug: #2a9d8f;
1265
+ --accent-debug: #35a395;
761
1266
  --on-accent-debug: #070d18;
762
- --accent-info: #4a90d9;
1267
+ --accent-info: #4990d8;
763
1268
  --on-accent-info: #070d18;
764
- --accent-warn: #f2c94c;
1269
+ --accent-info-ink: #6ca6e4;
1270
+ --accent-warn: #f5c84e;
765
1271
  --on-accent-warn: #070d18;
766
- /* lifted off the brand red #da251d, which failed AA on a card. This one value
767
- * is the fill and the text on a bare surface; on its own 10 % tint (a
768
- * destructive menu row) the text is the derived --accent-error-ink, which
769
- * EV leaves to the contract: #ec8379, 4.83:1 on that tint over popover. The
770
- * vivid brand red is a gitops constant, not a UI token. */
771
- --accent-error: #ef5b52;
1272
+ --accent-error: #f05b52;
772
1273
  --on-accent-error: #070d18;
773
1274
 
774
- --chart-1: #2a9d8f;
775
- --chart-2: #2e9e5b;
776
- --chart-3: #f2c94c;
777
- --chart-4: #e58aae;
778
- --chart-5: #4a90d9;
1275
+ --chart-1: #35a395;
1276
+ --chart-2: #4990d8;
1277
+ --chart-3: #f5c84e;
1278
+ --chart-4: #e68aaf;
1279
+ --chart-5: #2e9e5c;
1280
+ --chart-6: #988eef;
1281
+
1282
+ /* filled buttons. The danger label stays dark: white on #f05b52 is 3.32:1. */
1283
+ --button-primary-bg: #138377;
1284
+ --button-primary-hover: #127c71;
1285
+ --button-primary-border: #138377;
1286
+ --button-primary-ink: #ffffff;
1287
+ --button-danger-bg: #f05b52;
1288
+ --button-danger-hover: #f06c61;
1289
+ --button-danger-border: #f05b52;
1290
+ --button-danger-ink: #070d18;
1291
+
1292
+ /* chips are tinted pills with no edge */
1293
+ --chip-neutral-bg: var(--hover);
1294
+ --chip-neutral-border: #e6e1d300;
1295
+ /* Figma tints the accent chip 16 %, which leaves its teal label at 4.03:1 on
1296
+ * the card; 8 % holds 4.56:1 */
1297
+ --chip-primary-bg: #35a39514;
1298
+ --chip-primary-border: #35a39500;
1299
+ --chip-positive-bg: #2e9e5c2e;
1300
+ --chip-positive-border: #4abf7600;
1301
+ --chip-info-bg: #4990d82e;
1302
+ --chip-info-border: #6ca6e400;
1303
+ --chip-warn-bg: #f5c84e29;
1304
+ --chip-warn-border: #f5c84e00;
1305
+ --chip-error-bg: #f05b522e;
1306
+ --chip-error-border: #f06c6100;
1307
+
1308
+ --radius: 0.5rem;
1309
+ --control-radius: 0.375rem;
1310
+ --chip-radius: 9999px;
1311
+ /* the ladder is three tiers, not one drop shadow: on a dark theme the value
1312
+ * steps alone are too close to separate a resting card from a menu from a
1313
+ * modal, so each tier gets its own spread and opacity */
1314
+ --depth-card: 0 1px 2px 0 rgb(0 0 0 / 0.3);
1315
+ --depth-elevated:
1316
+ 0 4px 12px -2px rgb(0 0 0 / 0.4),
1317
+ 0 2px 4px -2px rgb(0 0 0 / 0.3);
1318
+ --depth-overlay: 0 16px 48px -12px rgb(0 0 0 / 0.6);
779
1319
  }