@nebutra/tokens 0.1.3 → 3.0.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 (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/brands/README.md +66 -0
  3. package/brands/cosmos/DESIGN.md +356 -0
  4. package/brands/cosmos/brand.json +209 -0
  5. package/brands/cosmos/refero-tokens.json +312 -0
  6. package/brands/gsap/brand.json +1 -27
  7. package/brands/linear/DESIGN.md +478 -0
  8. package/brands/linear/brand.json +2 -19
  9. package/brands/notion/brand.json +2 -28
  10. package/brands/raycast/brand.json +2 -17
  11. package/brands/stripe/brand.json +1 -16
  12. package/brands/vanta/brand.json +1 -38
  13. package/brands/vercel/brand.json +1 -18
  14. package/dist/brand-package/index.d.ts +13 -7
  15. package/dist/brand-package/index.js +2290 -34
  16. package/dist/brand-package/index.js.map +1 -1
  17. package/dist/brand-package/use-brand.d.ts +1 -1
  18. package/dist/brand-package/use-brand.js +957 -5
  19. package/dist/brand-package/use-brand.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +2417 -22
  22. package/dist/index.js.map +1 -1
  23. package/dist/{use-brand-C8d4DPqE.d.ts → use-brand-ClmvK_lC.d.ts} +19 -3
  24. package/dist/values.d.ts +1192 -0
  25. package/dist/values.js +518 -0
  26. package/dist/values.js.map +1 -0
  27. package/package.json +16 -4
  28. package/recipe.css +36 -2
  29. package/skins/cosmos.css +239 -0
  30. package/skins/gsap.css +32 -20
  31. package/skins/linear.css +54 -17
  32. package/skins/notion.css +36 -25
  33. package/skins/raycast.css +37 -19
  34. package/skins/stripe.css +34 -15
  35. package/skins/vanta.css +34 -30
  36. package/skins/vercel.css +55 -19
  37. package/styles.css +492 -149
  38. package/dist/chunk-ALJTP5HL.js +0 -881
  39. package/dist/chunk-ALJTP5HL.js.map +0 -1
  40. package/dist/chunk-RGUJQOLH.js +0 -1582
  41. package/dist/chunk-RGUJQOLH.js.map +0 -1
package/styles.css CHANGED
@@ -47,17 +47,17 @@
47
47
  --nebutra-cyan-800: #057963;
48
48
  --nebutra-cyan-900: #035143;
49
49
  --nebutra-cyan-950: #012923;
50
- --nebutra-neutral-50: #f8fafc;
51
- --nebutra-neutral-100: #f1f5f9;
52
- --nebutra-neutral-200: #e2e8f0;
53
- --nebutra-neutral-300: #cbd5e1;
54
- --nebutra-neutral-400: #94a3b8;
55
- --nebutra-neutral-500: #64748b;
56
- --nebutra-neutral-600: #475569;
57
- --nebutra-neutral-700: #334155;
58
- --nebutra-neutral-800: #1e293b;
59
- --nebutra-neutral-900: #0f172a;
60
- --nebutra-neutral-950: #020617;
50
+ --nebutra-neutral-50: #f9fafb;
51
+ --nebutra-neutral-100: #f3f4f6;
52
+ --nebutra-neutral-200: #e4e5e8;
53
+ --nebutra-neutral-300: #d2d4d8;
54
+ --nebutra-neutral-400: #9fa1a5;
55
+ --nebutra-neutral-500: #717377;
56
+ --nebutra-neutral-600: #515357;
57
+ --nebutra-neutral-700: #3f4044;
58
+ --nebutra-neutral-800: #26272a;
59
+ --nebutra-neutral-900: #18191c;
60
+ --nebutra-neutral-950: #0b0c0d;
61
61
  --nebutra-gray-50: #fafafa;
62
62
  --nebutra-gray-100: #f5f5f5;
63
63
  --nebutra-gray-200: #e5e5e5;
@@ -90,28 +90,97 @@
90
90
  --duration-reveal: 300ms; /** Content unveil: slide-in, expand, accordion, drawer */
91
91
  --duration-cinematic: 500ms; /** Hero-grade entrance: landing reveal, large delight moments */
92
92
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
93
- --ease-out: cubic-bezier(0, 0, 0.2, 1);
93
+ --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
94
94
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
95
95
  --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
96
96
  --tracking-display: -0.04em; /** Hero H1 */
97
97
  --tracking-heading: -0.03em; /** Section H2/H3 */
98
98
  --tracking-tight: -0.015em; /** Body emphasis */
99
+ --tracking-label: 0.07em; /** Uppercase micro-labels. 0.07em is Vercel's measured eyebrow value (0.071em); open tracking keeps small caps legible. */
99
100
  --leading-display: 1.05;
100
101
  --leading-heading: 1.15;
102
+ --type-2xs-size: 0.6875rem;
103
+ --type-2xs-leading: 1.45;
104
+ --type-2xs-tracking: 0.01em;
105
+ --type-xs-size: 0.75rem;
106
+ --type-xs-leading: 1.4;
107
+ --type-xs-tracking: 0.005em;
108
+ --type-ui-size: 0.8125rem;
109
+ --type-ui-leading: 1.45;
110
+ --type-ui-tracking: 0em;
111
+ --type-sm-size: 0.875rem;
112
+ --type-sm-leading: 1.5;
113
+ --type-sm-tracking: 0em;
114
+ --type-base-size: 1rem;
115
+ --type-base-leading: 1.6;
116
+ --type-base-tracking: -0.005em;
117
+ --type-lg-size: 1.125rem;
118
+ --type-lg-leading: 1.55;
119
+ --type-lg-tracking: -0.01em;
120
+ --type-xl-size: 1.25rem;
121
+ --type-xl-leading: 1.45;
122
+ --type-xl-tracking: -0.012em;
123
+ --type-2xl-size: 1.5rem;
124
+ --type-2xl-leading: 1.33;
125
+ --type-2xl-tracking: -0.015em;
126
+ --type-3xl-size: 1.875rem;
127
+ --type-3xl-leading: 1.25;
128
+ --type-3xl-tracking: -0.02em;
129
+ --type-4xl-size: 2.25rem;
130
+ --type-4xl-leading: 1.15;
131
+ --type-4xl-tracking: -0.022em;
132
+ --type-5xl-size: 3rem;
133
+ --type-5xl-leading: 1.08;
134
+ --type-5xl-tracking: -0.028em;
135
+ --type-6xl-size: 3.75rem;
136
+ --type-6xl-leading: 1.04;
137
+ --type-6xl-tracking: -0.032em;
138
+ --type-7xl-size: 4.5rem;
139
+ --type-7xl-leading: 1;
140
+ --type-7xl-tracking: -0.035em;
141
+ --type-8xl-size: 6rem;
142
+ --type-8xl-leading: 1;
143
+ --type-8xl-tracking: -0.04em;
144
+ --type-9xl-size: 8rem;
145
+ --type-9xl-leading: 1;
146
+ --type-9xl-tracking: -0.045em;
147
+ --type-display-size: 1.25rem;
148
+ --type-display-leading: 1.45;
149
+ --type-display-tracking: -0.012em;
150
+ --type-body-size: 0.875rem;
151
+ --type-body-leading: 1.5;
152
+ --type-body-tracking: 0em;
153
+ --type-label-size: 0.75rem;
154
+ --type-label-leading: 1.4;
155
+ --type-label-tracking: 0.005em;
156
+ --type-meta-size: 0.6875rem;
157
+ --type-meta-leading: 1.45;
158
+ --type-meta-tracking: 0.01em;
159
+ --layer-raised: 10; /** Within a component: an element lifted over its siblings. */
160
+ --layer-sticky: 20; /** Sticky headers and table heads. */
161
+ --layer-floating: 30; /** Floating toolbars and canvas chrome anchored to content. */
162
+ --layer-nav: 40; /** App navigation and the shell's top bar. */
163
+ --layer-drawer: 60; /** Mobile navigation drawers and their toggles. */
164
+ --layer-banner: 90; /** Page-level banners: cookie consent, announcements. */
165
+ --layer-skip-link: 100; /** The skip link — above page chrome whenever it is focused. */
166
+ --layer-panel: 200; /** App-owned floating panels that are not overlays (locale switcher, search flyouts). */
167
+ --layer-backdrop: 1040; /** Overlay tier. Mirrors overlayZIndex in @nebutra/ui; a test keeps them equal. */
168
+ --layer-modal: 1050; /** Dialogs and sheets. */
169
+ --layer-popover: 1060; /** Popovers, menus, selects. */
170
+ --layer-tooltip: 1070; /** Tooltips. */
171
+ --layer-devtools: 9999; /** Developer and debug surfaces only — never product UI. */
101
172
  --ring-hairline: inset 0 0 0 1px rgb(255 255 255 / 0.04); /** Etched-glass hairline (light mode value, dark mode override in themes/dark.json) */
102
173
  --overlay-faint: rgb(0 0 0 / 0.06); /** Light-mode faint overlay (6% black) */
103
174
  --font-sans:
104
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
105
- "PingFang SC", "Microsoft YaHei", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
175
+ var(--font-geist-sans), "Geist", var(--font-misans, "MiSans"), "PingFang SC", "Microsoft YaHei",
176
+ -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
106
177
  --font-cn:
107
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
108
- "PingFang SC", "Microsoft YaHei", sans-serif; /** CJK-locale stack — same order as sans, minus the Latin-only system faces */
178
+ var(--font-geist-sans), "Geist", var(--font-misans, "MiSans"), "PingFang SC", "Microsoft YaHei",
179
+ sans-serif; /** CJK-locale stack — same order as sans, minus the Latin-only system faces */
109
180
  --font-display:
110
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
111
- sans-serif;
181
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
112
182
  --font-heading:
113
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
114
- sans-serif;
183
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
115
184
  --font-mono:
116
185
  var(--font-geist-mono), "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
117
186
  --brand-primary: #0033fe; /** Brand primary — 云毓蓝 */
@@ -155,17 +224,17 @@
155
224
  --motion-duration-cinematic: 500ms;
156
225
  --focus-ring: 0 0 0 2px hsl(var(--ring) / 0.4); /** Single-layer translucent focus ring (2026 pattern). Legacy box-shadow consumers stay compatible; modern surfaces should prefer the global :focus-visible outline rule in static/base.css. */
157
226
  --neutral-1: #ffffff; /** app background */
158
- --neutral-2: #f8fafc; /** subtle background */
159
- --neutral-3: #f1f5f9; /** component bg default */
160
- --neutral-4: #e2e8f0; /** component bg hover */
161
- --neutral-5: #cbd5e1; /** component bg active */
162
- --neutral-6: #b0bccb; /** subtle border — DERIVED at build time (scripts/derive-border-tier.mjs): OKLab lerp step5 → step9 at t=0.25. The $value here is a placeholder anchor only; the preprocessor overwrites it. */
163
- --neutral-7: #96a3b5; /** default border — DERIVED: OKLab lerp step5 → step9 at t=0.50. */
164
- --neutral-8: #7d8ba0; /** hovered border — DERIVED: OKLab lerp step5 → step9 at t=0.75. */
165
- --neutral-9: #64748b; /** solid fill */
166
- --neutral-10: #475569; /** solid fill hover */
167
- --neutral-11: #334155; /** secondary text */
168
- --neutral-12: #0f172a; /** primary text */
227
+ --neutral-2: #f9fafb; /** subtle background */
228
+ --neutral-3: #f3f4f6; /** component bg default */
229
+ --neutral-4: #e4e5e8; /** component bg hover */
230
+ --neutral-5: #d2d4d8; /** component bg active */
231
+ --neutral-6: #b9bbbf; /** subtle border — DERIVED at build time (scripts/derive-border-tier.mjs): OKLab lerp step5 → step9 at t=0.25. The $value here is a placeholder anchor only; the preprocessor overwrites it. */
232
+ --neutral-7: #a0a2a6; /** default border — DERIVED: OKLab lerp step5 → step9 at t=0.50. */
233
+ --neutral-8: #888a8e; /** hovered border — DERIVED: OKLab lerp step5 → step9 at t=0.75. */
234
+ --neutral-9: #717377; /** solid fill */
235
+ --neutral-10: #515357; /** solid fill hover */
236
+ --neutral-11: #3f4044; /** secondary text */
237
+ --neutral-12: #18191c; /** primary text */
169
238
  --blue-1: #fafcfe; /** app tint — L* 99.01, C 0.003, hue 264. Radix blue-1 sits at L* 99.31 C 0.003. */
170
239
  --blue-2: #f2f6fd; /** subtle background — L* 97.21, C 0.010. */
171
240
  --blue-3: #e7eefc; /** component background default — L* 94.79, C 0.020. */
@@ -192,20 +261,20 @@
192
261
  --cyan-11: #027a61; /** secondary text — L* 51.64, C 0.100. 5.17:1 on step 1. */
193
262
  --cyan-12: #004133; /** primary text — L* 33.45, C 0.065. 11.35:1 on step 1. */
194
263
  --cyan-contrast: #0d2b25; /** Label ink for the bright fill tier. Absolute near-black, identical in both modes, carrying a trace of the scale hue. A bright fill needs an explicit dark ink; comparing white against the scale's own step 12 picks white in dark mode, where step 12 is light, and measures 1.52:1. 10.35:1 on step 9, 9.40:1 on step 10. */
195
- --background: 0 0% 100%; /** HSL channel triple — consume via hsl(var(--background)) */
196
- --foreground: 222 47% 11%;
264
+ --background: 210 20% 98%; /** House canvas. Light is gray-50 under white cards (two-surface layering, as Vercel #fafafa/#fff); dark is OKLCH L 0.16, inside the near-black band every reference measures (Linear L 0.14, Raycast #040506). */
265
+ --foreground: 225 7.7% 10.2%;
197
266
  --card: 0 0% 100%;
198
- --card-foreground: 222 47% 11%;
267
+ --card-foreground: 225 7.7% 10.2%;
199
268
  --popover: 0 0% 100%;
200
- --popover-foreground: 222 47% 11%;
201
- --primary: 222.8 85% 55.7%; /** Product action blue. Derived, not borrowed: hue 264 is the VI hue, L* and C were chosen inside the band the industry actually ships (measured OKLCH C 0.167-0.235, L* 54-67 across Vercel, Stripe, Linear, GitHub, Tailwind, Radix, Figma, Notion). #2e65ee = L*55.5 C0.215, white label 4.99:1. The previous 228 85% 56% (#2f56ee, C0.232) also passed at 5.70:1 — this trims it to the middle of the band rather than its top edge. The VI lock --blue-9 (#0033FE, C0.290) is 23% above anything in that band and must never surface on a component. */
269
+ --popover-foreground: 225 7.7% 10.2%;
270
+ --primary: 225 7.7% 10.2%; /** House action fill = ink, not blue (owner decision 2026-09-24). Every measured reference ships a solid near-black primary (Vercel #171717, Figma #000) and keeps its one saturated hue for a single signal. Blue now lives on --ring (focus, selection) only. */
202
271
  --primary-foreground: 0 0% 100%;
203
- --secondary: 210 40% 96%;
204
- --secondary-foreground: 222 47% 11%;
205
- --muted: 210 40% 96%;
206
- --muted-foreground: 215 25% 45%;
207
- --accent: 228 100% 97%;
208
- --accent-foreground: 228 85% 56%; /** Softer brand blue — desaturated to match --primary, avoids 100% saturation glare on focus/accent */
272
+ --secondary: 220 14.3% 95.9%;
273
+ --secondary-foreground: 225 7.7% 10.2%;
274
+ --muted: 220 14.3% 95.9%;
275
+ --muted-foreground: 220 2.6% 45.5%;
276
+ --accent: 220 14.3% 95.9%;
277
+ --accent-foreground: 225 7.7% 10.2%; /** Softer brand blue — desaturated to match --primary, avoids 100% saturation glare on focus/accent */
209
278
  --destructive: 0 84% 45%;
210
279
  --destructive-foreground: 0 0% 100%;
211
280
  --destructive-strong: 0 84% 45%; /** Red as FOREGROUND. In light mode --destructive already passes (5.42:1 on white), so this equals it; the token exists so consumers reference one name across themes. In DARK, --destructive is 0 63% 38% = 2.15:1 on --card, unreadable as text — see dark.json. Mirrors --warning-strong. */
@@ -217,17 +286,17 @@
217
286
  --warning-strong: 38 92% 30%; /** Amber as FOREGROUND on a light surface. --warning is a fill: at 38 92% 50% it is 2.04:1 against --neutral-2, so it is unreadable as text or as an icon stroke. This darkened step is 5.17:1, which sits between --success (4.92) and --destructive (5.19) so the three status colours read with the same weight. Use --warning for fills with --warning-foreground on top; use this for amber text and icons. Destructive and success need no equivalent — they already pass AA as foreground. */
218
287
  --info: 228 85% 56%;
219
288
  --info-foreground: 0 0% 100%;
220
- --border: 240 5.9% 90%;
221
- --input: 240 5.9% 90%;
222
- --ring: 222 47% 11%; /** Focus ring — neutral foreground (Vercel/Linear/Cursor 2026 pattern). Brand blue is reserved for selected/active states; focus indication is chrome and stays neutral. Consumed by global :focus-visible outline as hsl(var(--ring) / 0.5). */
223
- --sidebar: 210 40% 98%;
224
- --sidebar-foreground: 215 25% 27%;
225
- --sidebar-primary: 228 85% 56%;
289
+ --border: 225 8% 90.2%;
290
+ --input: 225 8% 90.2%;
291
+ --ring: 222.8 85% 55.7%; /** The House blue. Focus rings, selection and links — the one saturated hue on a product screen. #2e65ee light / #6b93f5 dark. */
292
+ --sidebar: 210 20% 98%;
293
+ --sidebar-foreground: 225 7.7% 10.2%;
294
+ --sidebar-primary: 225 7.7% 10.2%; /** Current-page accent in the sidebar (icon tint + edge). Same value as --primary: the sidebar carried the superseded 228 85% 56% (#2f56ee, C 0.232), which the action-blue measurement trimmed to C 0.215. */
226
295
  --sidebar-primary-foreground: 0 0% 100%;
227
- --sidebar-accent: 228 100% 97%;
228
- --sidebar-accent-foreground: 228 85% 56%;
229
- --sidebar-border: 214 32% 91%;
230
- --sidebar-ring: 228 85% 56%;
296
+ --sidebar-accent: 220 14.3% 95.9%;
297
+ --sidebar-accent-foreground: 225 7.7% 10.2%;
298
+ --sidebar-border: 225 8% 90.2%;
299
+ --sidebar-ring: 222.8 85% 55.7%;
231
300
  --chart-1: 228 100% 50%;
232
301
  --chart-2: 168 91% 49%;
233
302
  --chart-3: 263 70% 66%;
@@ -376,17 +445,17 @@
376
445
  --nebutra-cyan-800: #057963;
377
446
  --nebutra-cyan-900: #035143;
378
447
  --nebutra-cyan-950: #012923;
379
- --nebutra-neutral-50: #f8fafc;
380
- --nebutra-neutral-100: #f1f5f9;
381
- --nebutra-neutral-200: #e2e8f0;
382
- --nebutra-neutral-300: #cbd5e1;
383
- --nebutra-neutral-400: #94a3b8;
384
- --nebutra-neutral-500: #64748b;
385
- --nebutra-neutral-600: #475569;
386
- --nebutra-neutral-700: #334155;
387
- --nebutra-neutral-800: #1e293b;
388
- --nebutra-neutral-900: #0f172a;
389
- --nebutra-neutral-950: #020617;
448
+ --nebutra-neutral-50: #f9fafb;
449
+ --nebutra-neutral-100: #f3f4f6;
450
+ --nebutra-neutral-200: #e4e5e8;
451
+ --nebutra-neutral-300: #d2d4d8;
452
+ --nebutra-neutral-400: #9fa1a5;
453
+ --nebutra-neutral-500: #717377;
454
+ --nebutra-neutral-600: #515357;
455
+ --nebutra-neutral-700: #3f4044;
456
+ --nebutra-neutral-800: #26272a;
457
+ --nebutra-neutral-900: #18191c;
458
+ --nebutra-neutral-950: #0b0c0d;
390
459
  --nebutra-gray-50: #fafafa;
391
460
  --nebutra-gray-100: #f5f5f5;
392
461
  --nebutra-gray-200: #e5e5e5;
@@ -419,30 +488,99 @@
419
488
  --duration-reveal: 300ms; /** Content unveil: slide-in, expand, accordion, drawer */
420
489
  --duration-cinematic: 500ms; /** Hero-grade entrance: landing reveal, large delight moments */
421
490
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
422
- --ease-out: cubic-bezier(0, 0, 0.2, 1);
491
+ --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
423
492
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
424
493
  --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
425
494
  --tracking-display: -0.04em; /** Hero H1 */
426
495
  --tracking-heading: -0.03em; /** Section H2/H3 */
427
496
  --tracking-tight: -0.015em; /** Body emphasis */
497
+ --tracking-label: 0.07em; /** Uppercase micro-labels. 0.07em is Vercel's measured eyebrow value (0.071em); open tracking keeps small caps legible. */
428
498
  --leading-display: 1.05;
429
499
  --leading-heading: 1.15;
500
+ --type-2xs-size: 0.6875rem;
501
+ --type-2xs-leading: 1.45;
502
+ --type-2xs-tracking: 0.01em;
503
+ --type-xs-size: 0.75rem;
504
+ --type-xs-leading: 1.4;
505
+ --type-xs-tracking: 0.005em;
506
+ --type-ui-size: 0.8125rem;
507
+ --type-ui-leading: 1.45;
508
+ --type-ui-tracking: 0em;
509
+ --type-sm-size: 0.875rem;
510
+ --type-sm-leading: 1.5;
511
+ --type-sm-tracking: 0em;
512
+ --type-base-size: 1rem;
513
+ --type-base-leading: 1.6;
514
+ --type-base-tracking: -0.005em;
515
+ --type-lg-size: 1.125rem;
516
+ --type-lg-leading: 1.55;
517
+ --type-lg-tracking: -0.01em;
518
+ --type-xl-size: 1.25rem;
519
+ --type-xl-leading: 1.45;
520
+ --type-xl-tracking: -0.012em;
521
+ --type-2xl-size: 1.5rem;
522
+ --type-2xl-leading: 1.33;
523
+ --type-2xl-tracking: -0.015em;
524
+ --type-3xl-size: 1.875rem;
525
+ --type-3xl-leading: 1.25;
526
+ --type-3xl-tracking: -0.02em;
527
+ --type-4xl-size: 2.25rem;
528
+ --type-4xl-leading: 1.15;
529
+ --type-4xl-tracking: -0.022em;
530
+ --type-5xl-size: 3rem;
531
+ --type-5xl-leading: 1.08;
532
+ --type-5xl-tracking: -0.028em;
533
+ --type-6xl-size: 3.75rem;
534
+ --type-6xl-leading: 1.04;
535
+ --type-6xl-tracking: -0.032em;
536
+ --type-7xl-size: 4.5rem;
537
+ --type-7xl-leading: 1;
538
+ --type-7xl-tracking: -0.035em;
539
+ --type-8xl-size: 6rem;
540
+ --type-8xl-leading: 1;
541
+ --type-8xl-tracking: -0.04em;
542
+ --type-9xl-size: 8rem;
543
+ --type-9xl-leading: 1;
544
+ --type-9xl-tracking: -0.045em;
545
+ --type-display-size: 1.25rem;
546
+ --type-display-leading: 1.45;
547
+ --type-display-tracking: -0.012em;
548
+ --type-body-size: 0.875rem;
549
+ --type-body-leading: 1.5;
550
+ --type-body-tracking: 0em;
551
+ --type-label-size: 0.75rem;
552
+ --type-label-leading: 1.4;
553
+ --type-label-tracking: 0.005em;
554
+ --type-meta-size: 0.6875rem;
555
+ --type-meta-leading: 1.45;
556
+ --type-meta-tracking: 0.01em;
557
+ --layer-raised: 10; /** Within a component: an element lifted over its siblings. */
558
+ --layer-sticky: 20; /** Sticky headers and table heads. */
559
+ --layer-floating: 30; /** Floating toolbars and canvas chrome anchored to content. */
560
+ --layer-nav: 40; /** App navigation and the shell's top bar. */
561
+ --layer-drawer: 60; /** Mobile navigation drawers and their toggles. */
562
+ --layer-banner: 90; /** Page-level banners: cookie consent, announcements. */
563
+ --layer-skip-link: 100; /** The skip link — above page chrome whenever it is focused. */
564
+ --layer-panel: 200; /** App-owned floating panels that are not overlays (locale switcher, search flyouts). */
565
+ --layer-backdrop: 1040; /** Overlay tier. Mirrors overlayZIndex in @nebutra/ui; a test keeps them equal. */
566
+ --layer-modal: 1050; /** Dialogs and sheets. */
567
+ --layer-popover: 1060; /** Popovers, menus, selects. */
568
+ --layer-tooltip: 1070; /** Tooltips. */
569
+ --layer-devtools: 9999; /** Developer and debug surfaces only — never product UI. */
430
570
  --ring-hairline: inset 0 0 0 1px rgb(255 255 255 / 0.06); /** Hairline override — more visible on dark background */
431
571
  --overlay-faint: rgb(
432
572
  255 255 255 / 0.06
433
573
  ); /** Dark-mode faint overlay (6% white) — inverted from light */
434
574
  --font-sans:
435
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
436
- "PingFang SC", "Microsoft YaHei", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
575
+ var(--font-geist-sans), "Geist", var(--font-misans, "MiSans"), "PingFang SC", "Microsoft YaHei",
576
+ -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
437
577
  --font-cn:
438
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
439
- "PingFang SC", "Microsoft YaHei", sans-serif; /** CJK-locale stack — same order as sans, minus the Latin-only system faces */
578
+ var(--font-geist-sans), "Geist", var(--font-misans, "MiSans"), "PingFang SC", "Microsoft YaHei",
579
+ sans-serif; /** CJK-locale stack — same order as sans, minus the Latin-only system faces */
440
580
  --font-display:
441
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
442
- sans-serif;
581
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
443
582
  --font-heading:
444
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
445
- sans-serif;
583
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
446
584
  --font-mono:
447
585
  var(--font-geist-mono), "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
448
586
  --brand-primary: #5c7cfa; /** Dark-mode brand primary — the identity blue lifted to nebutra-blue-400 so it stays visible on dark surfaces. This is the identity alias, not the functional fill: the dark solid fill is --blue-9 (#396ae2, white label 4.85:1). The two are deliberately separate now that the VI hex is no longer pinned into ramp slot 9. */
@@ -485,18 +623,18 @@
485
623
  --motion-duration-reveal: 300ms;
486
624
  --motion-duration-cinematic: 500ms;
487
625
  --focus-ring: 0 0 0 2px hsl(var(--ring) / 0.4); /** Single-layer translucent focus ring (2026 pattern). Legacy box-shadow consumers stay compatible; modern surfaces should prefer the global :focus-visible outline rule in static/base.css. */
488
- --neutral-1: #020617;
489
- --neutral-2: #0f172a;
490
- --neutral-3: #1e293b;
491
- --neutral-4: #334155;
492
- --neutral-5: #475569;
626
+ --neutral-1: #0b0c0d;
627
+ --neutral-2: #18191c;
628
+ --neutral-3: #26272a;
629
+ --neutral-4: #3f4044;
630
+ --neutral-5: #515357;
493
631
  --neutral-6: #666666; /** subtle border — DERIVED: OKLab lerp step5 → step9 at t=0.25, chroma forced to 0. The achromatic result is deliberate and preserves the documented intent of color.nebutra-gray ('used where blue undertone clashes — primarily dark-mode borders, dividers, elevation hairlines'). What was broken was the LIGHTNESS: gray-900/800/700 are darker than the slate background tier they sit above, so the border read as a hole rather than a hairline. */
494
- --neutral-7: #7a7a7a; /** default border — DERIVED: OKLab lerp step5 → step9 at t=0.50, chroma 0. */
632
+ --neutral-7: #797979; /** default border — DERIVED: OKLab lerp step5 → step9 at t=0.50, chroma 0. */
495
633
  --neutral-8: #8d8d8d; /** hovered border — DERIVED: OKLab lerp step5 → step9 at t=0.75, chroma 0. */
496
- --neutral-9: #94a3b8;
497
- --neutral-10: #cbd5e1;
498
- --neutral-11: #e2e8f0;
499
- --neutral-12: #f8fafc;
634
+ --neutral-9: #9fa1a5;
635
+ --neutral-10: #d2d4d8;
636
+ --neutral-11: #e4e5e8;
637
+ --neutral-12: #f9fafb;
500
638
  --blue-1: #030815; /** app tint — L* 13.59, C 0.031, hue 264. */
501
639
  --blue-2: #060f25; /** subtle background — L* 17.43, C 0.048. */
502
640
  --blue-3: #09183b; /** component background default — L* 22.04, C 0.071. */
@@ -523,20 +661,20 @@
523
661
  --cyan-11: #90fbdb; /** secondary text — L* 91.46, C 0.110. 15.68:1 on step 1. */
524
662
  --cyan-12: #bdfde8; /** primary text — L* 94.54, C 0.070. 16.95:1 on step 1. */
525
663
  --cyan-contrast: #0d2b25; /** Label ink for the bright fill tier. Absolute near-black, identical in both modes. 10.35:1 on step 9, 11.34:1 on step 10. */
526
- --background: 222 14% 9%;
527
- --foreground: 210 18% 96%;
528
- --card: 222 12% 13%;
529
- --card-foreground: 210 18% 96%;
530
- --popover: 222 12% 13%;
531
- --popover-foreground: 210 18% 96%;
532
- --primary: 222.6 74.4% 55.5%; /** Dark-mode action blue, paired with the near-black --primary-foreground, not white. #396ae2 keeps the VI hue and sits at L*55.9 C0.191. */
533
- --primary-foreground: 0 0% 100%; /** White, matching light mode and matching --blue-contrast. It was near-black (222 14% 9%) when --primary was a LIGHT fill at L* 68.3, where dark ink read 6.20:1. Once --primary became a deep fill at L* 55.9 the pairing inverted and measured 3.74:1, below AA — the fill moved and the ink did not. The deep-fill tier takes white in both modes; --blue-contrast says the same thing, and they must agree because --primary and --blue-9 are now the same value. */
534
- --secondary: 222 10% 18%;
535
- --secondary-foreground: 210 18% 96%;
536
- --muted: 222 10% 17%;
537
- --muted-foreground: 220 8% 65%;
538
- --accent: 228 38% 22%;
539
- --accent-foreground: 228 95% 80%;
664
+ --background: 220 11.1% 5.3%; /** House canvas. Light is gray-50 under white cards (two-surface layering, as Vercel #fafafa/#fff); dark is OKLCH L 0.16, inside the near-black band every reference measures (Linear L 0.14, Raycast #040506). */
665
+ --foreground: 228 23.8% 95.9%;
666
+ --card: 220 7.3% 8%;
667
+ --card-foreground: 228 23.8% 95.9%;
668
+ --popover: 225 7.7% 10.2%;
669
+ --popover-foreground: 228 23.8% 95.9%;
670
+ --primary: 228 23.8% 95.9%; /** House action fill = ink, not blue (owner decision 2026-09-24). Every measured reference ships a solid near-black primary (Vercel #171717, Figma #000) and keeps its one saturated hue for a single signal. Blue now lives on --ring (focus, selection) only. */
671
+ --primary-foreground: 220 11.1% 5.3%; /** White, matching light mode and matching --blue-contrast. It was near-black (222 14% 9%) when --primary was a LIGHT fill at L* 68.3, where dark ink read 6.20:1. Once --primary became a deep fill at L* 55.9 the pairing inverted and measured 3.74:1, below AA — the fill moved and the ink did not. The deep-fill tier takes white in both modes; --blue-contrast says the same thing, and they must agree because --primary and --blue-9 are now the same value. */
672
+ --secondary: 225 6.2% 12.5%;
673
+ --secondary-foreground: 228 23.8% 95.9%;
674
+ --muted: 225 6.2% 12.5%;
675
+ --muted-foreground: 228 2.6% 62.5%;
676
+ --accent: 225 6.2% 12.5%;
677
+ --accent-foreground: 228 23.8% 95.9%;
540
678
  --destructive: 0 63% 38%;
541
679
  --destructive-foreground: 210 18% 96%;
542
680
  --destructive-strong: 0 80% 66%; /** Red as FOREGROUND on a dark surface. --destructive (0 63% 38%) is 2.15:1 on --card here — a fill only. Lightened to 5.18:1 on --card / 5.61:1 on --neutral-1, which sits alongside the light-mode 5.42:1 so red reads with the same weight in both themes. Use --destructive for fills with --destructive-foreground on top; use this for red text and icons. */
@@ -548,17 +686,17 @@
548
686
  --warning-strong: 38 80% 52%; /** Amber as foreground on a dark surface. Unlike light mode this needs no adjustment — 38 80% 52% is already 7.98:1 against --neutral-1 — so it deliberately equals --warning. The token exists in both themes so consumers can reference one name. */
549
687
  --info: 228 95% 72%;
550
688
  --info-foreground: 222 14% 9%;
551
- --border: 220 8% 22%;
552
- --input: 220 8% 22%;
553
- --ring: 0 0% 88%; /** Focus ring (dark mode) — neutral light gray. Mirrors light-mode neutral foreground choice. */
554
- --sidebar: 222 16% 8%;
555
- --sidebar-foreground: 210 18% 94%;
556
- --sidebar-primary: 228 95% 72%;
557
- --sidebar-primary-foreground: 222 14% 9%;
558
- --sidebar-accent: 228 38% 22%;
559
- --sidebar-accent-foreground: 228 95% 80%;
560
- --sidebar-border: 220 8% 18%;
561
- --sidebar-ring: 228 95% 72%;
689
+ --border: 220 3.6% 16.3%;
690
+ --input: 220 3.6% 16.3%;
691
+ --ring: 222.6 87.3% 69%; /** The House blue. Focus rings, selection and links — the one saturated hue on a product screen. #2e65ee light / #6b93f5 dark. */
692
+ --sidebar: 220 11.1% 5.3%;
693
+ --sidebar-foreground: 228 23.8% 95.9%;
694
+ --sidebar-primary: 228 23.8% 95.9%; /** Current-page accent in the sidebar. = blue-11 (#89aaee, L* 74.0, C 0.106), the ramp's measured secondary-text tier on dark (8.62:1 on step 1). Was 228 95% 72% — 95% chroma sat outside every measured band and matched no ramp step. */
695
+ --sidebar-primary-foreground: 220 11.1% 5.3%;
696
+ --sidebar-accent: 225 6.2% 12.5%;
697
+ --sidebar-accent-foreground: 228 23.8% 95.9%;
698
+ --sidebar-border: 225 6.2% 12.5%;
699
+ --sidebar-ring: 222.6 87.3% 69%;
562
700
  --chart-1: 228 95% 67%;
563
701
  --chart-2: 168 96% 54%;
564
702
  --chart-3: 228 100% 82%;
@@ -719,6 +857,12 @@
719
857
  --color-success-foreground: hsl(var(--success-foreground));
720
858
  --color-warning: hsl(var(--warning));
721
859
  --color-warning-foreground: hsl(var(--warning-foreground));
860
+ /* Status colours as INK (text, icon fill/stroke). --destructive / --warning /
861
+ --success are fills: amber is 2.0:1 as text in light, red 2.5:1 in dark.
862
+ text-destructive-strong etc. read at ~5:1 in both themes. */
863
+ --color-destructive-strong: hsl(var(--destructive-strong));
864
+ --color-warning-strong: hsl(var(--warning-strong));
865
+ --color-success-strong: hsl(var(--success-strong));
722
866
  --color-info: hsl(var(--info));
723
867
  --color-info-foreground: hsl(var(--info-foreground));
724
868
  --color-border: hsl(var(--border));
@@ -834,6 +978,22 @@
834
978
  --radius-3xl: 1.5rem;
835
979
  --radius-full: 9999px;
836
980
 
981
+ /* Containers — the page-width contract from CLAUDE.md, as real utilities.
982
+ max-w-text (reading), max-w-content (pricing/blog), max-w-wide (feature bento, navbar).
983
+
984
+ These values also reach :root through the semantic layer, so var(--container-wide) keeps
985
+ working; what was missing was the Tailwind side. Without it there is no max-w-wide to reach
986
+ for, and 92 call sites across apps/** had typed max-w-wide instead — a decided number
987
+ living in a className, where changing the token cannot move it.
988
+
989
+ Literals rather than var(--container-wide): Tailwind emits an @theme entry into its own
990
+ layer, so a same-named reference becomes self-referential and survives only because the
991
+ semantic :root happens to come later in the cascade. Duplicating the number and pinning it
992
+ with a parity test is the honest trade — see design-tokens' container parity test. */
993
+ --container-text: 896px;
994
+ --container-content: 1152px;
995
+ --container-wide: 1400px;
996
+
837
997
  /* Shadows */
838
998
  --shadow-xs: var(--elevation-xs);
839
999
  --shadow-sm: var(--elevation-sm);
@@ -873,15 +1033,90 @@
873
1033
  (No backticks in this block — it is inside a JS template literal.) */
874
1034
  --ease-in: var(--motion-ease-in);
875
1035
  --ease-out: var(--motion-ease-out);
1036
+ --ease-brand: var(--motion-ease-brand);
876
1037
  --ease-in-out: var(--motion-ease-in-out);
877
1038
  --ease-spring: var(--motion-ease-spring);
878
1039
 
1040
+ /* Type scale — see core.json:type. Indirect so a Brand Package can retune size,
1041
+ leading and tracking; each text-* utility sets all three. */
1042
+ --text-2xs: var(--type-2xs-size);
1043
+ --text-2xs--line-height: var(--type-2xs-leading);
1044
+ --text-2xs--letter-spacing: var(--type-2xs-tracking);
1045
+ --text-xs: var(--type-xs-size);
1046
+ --text-xs--line-height: var(--type-xs-leading);
1047
+ --text-xs--letter-spacing: var(--type-xs-tracking);
1048
+ --text-ui: var(--type-ui-size);
1049
+ --text-ui--line-height: var(--type-ui-leading);
1050
+ --text-ui--letter-spacing: var(--type-ui-tracking);
1051
+ --text-sm: var(--type-sm-size);
1052
+ --text-sm--line-height: var(--type-sm-leading);
1053
+ --text-sm--letter-spacing: var(--type-sm-tracking);
1054
+ --text-base: var(--type-base-size);
1055
+ --text-base--line-height: var(--type-base-leading);
1056
+ --text-base--letter-spacing: var(--type-base-tracking);
1057
+ --text-lg: var(--type-lg-size);
1058
+ --text-lg--line-height: var(--type-lg-leading);
1059
+ --text-lg--letter-spacing: var(--type-lg-tracking);
1060
+ --text-xl: var(--type-xl-size);
1061
+ --text-xl--line-height: var(--type-xl-leading);
1062
+ --text-xl--letter-spacing: var(--type-xl-tracking);
1063
+ --text-2xl: var(--type-2xl-size);
1064
+ --text-2xl--line-height: var(--type-2xl-leading);
1065
+ --text-2xl--letter-spacing: var(--type-2xl-tracking);
1066
+ --text-3xl: var(--type-3xl-size);
1067
+ --text-3xl--line-height: var(--type-3xl-leading);
1068
+ --text-3xl--letter-spacing: var(--type-3xl-tracking);
1069
+ --text-4xl: var(--type-4xl-size);
1070
+ --text-4xl--line-height: var(--type-4xl-leading);
1071
+ --text-4xl--letter-spacing: var(--type-4xl-tracking);
1072
+ --text-5xl: var(--type-5xl-size);
1073
+ --text-5xl--line-height: var(--type-5xl-leading);
1074
+ --text-5xl--letter-spacing: var(--type-5xl-tracking);
1075
+ --text-6xl: var(--type-6xl-size);
1076
+ --text-6xl--line-height: var(--type-6xl-leading);
1077
+ --text-6xl--letter-spacing: var(--type-6xl-tracking);
1078
+ --text-7xl: var(--type-7xl-size);
1079
+ --text-7xl--line-height: var(--type-7xl-leading);
1080
+ --text-7xl--letter-spacing: var(--type-7xl-tracking);
1081
+ --text-8xl: var(--type-8xl-size);
1082
+ --text-8xl--line-height: var(--type-8xl-leading);
1083
+ --text-8xl--letter-spacing: var(--type-8xl-tracking);
1084
+ --text-9xl: var(--type-9xl-size);
1085
+ --text-9xl--line-height: var(--type-9xl-leading);
1086
+ --text-9xl--letter-spacing: var(--type-9xl-tracking);
1087
+ --text-display: var(--type-display-size);
1088
+ --text-display--line-height: var(--type-display-leading);
1089
+ --text-display--letter-spacing: var(--type-display-tracking);
1090
+ --text-body: var(--type-body-size);
1091
+ --text-body--line-height: var(--type-body-leading);
1092
+ --text-body--letter-spacing: var(--type-body-tracking);
1093
+ --text-label: var(--type-label-size);
1094
+ --text-label--line-height: var(--type-label-leading);
1095
+ --text-label--letter-spacing: var(--type-label-tracking);
1096
+ --text-meta: var(--type-meta-size);
1097
+ --text-meta--line-height: var(--type-meta-leading);
1098
+ --text-meta--letter-spacing: var(--type-meta-tracking);
1099
+
879
1100
  /* Motion — Durations (four-rail; see core.json:duration). Names denote intent, not relative speed. */
880
1101
  --duration-micro: var(--motion-duration-micro);
881
1102
  --duration-flow: var(--motion-duration-flow);
882
1103
  --duration-reveal: var(--motion-duration-reveal);
883
1104
  --duration-cinematic: var(--motion-duration-cinematic);
884
1105
 
1106
+ /* The same four rails on the namespace Tailwind reads for duration-*.
1107
+ --duration-* is the variable other CSS consumes; the utility class is built
1108
+ from --transition-duration-*, and only that one. Without these,
1109
+ duration-micro and duration-reveal are class names that generate nothing:
1110
+ they were in use in the component library and on the marketing site, and
1111
+ every one of them silently fell back to the 150ms default. */
1112
+ --transition-duration-micro: var(--motion-duration-micro);
1113
+ --transition-duration-flow: var(--motion-duration-flow);
1114
+ --transition-duration-reveal: var(--motion-duration-reveal);
1115
+ --transition-duration-cinematic: var(--motion-duration-cinematic);
1116
+
1117
+ /* Likewise for ease-*: the utility namespace is --ease-* already, so the four
1118
+ above are reachable as ease-brand / ease-spring without further mapping. */
1119
+
885
1120
  /* Spacing — DO NOT register --spacing-sm|md|lg|xl|2xl here.
886
1121
  Tailwind v4 puts every --spacing-{key} on the shared size rail, so
887
1122
  max-w-sm / max-w-2xl (and width/height/gap utilities with the same keys)
@@ -893,22 +1128,21 @@
893
1128
  --spacing-{step} alias. --spacing itself (0.25rem arithmetic base) stays
894
1129
  untouched. See static/base.css. */
895
1130
 
896
- /* Font family — Geist keeps Latin + numerals, self-hosted vivo Sans SC takes
897
- CJK, system CJK faces behind it. Order is the design decision; see the
898
- fontFamily $description in tokens/core.json. Mirror of those tokens — keep
899
- the two in sync (verify:parity compares them). */
1131
+ /* Font family — Geist keeps Latin + numerals for body/UI, DM Sans sets Latin
1132
+ display and headings, self-hosted MiSans takes CJK, system CJK faces behind
1133
+ it. Order is the design decision; see the fontFamily $description in
1134
+ tokens/core.json. Mirror of those tokens — keep the two in sync
1135
+ (verify:parity compares them). */
900
1136
  --font-sans:
901
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
902
- "PingFang SC", "Microsoft YaHei", -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
1137
+ var(--font-geist-sans), "Geist", var(--font-misans, "MiSans"), "PingFang SC", "Microsoft YaHei",
1138
+ -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
903
1139
  --font-cn:
904
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
905
- "PingFang SC", "Microsoft YaHei", sans-serif;
906
- --font-display:
907
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
1140
+ var(--font-geist-sans), "Geist", var(--font-misans, "MiSans"), "PingFang SC", "Microsoft YaHei",
908
1141
  sans-serif;
1142
+ --font-display:
1143
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
909
1144
  --font-heading:
910
- var(--font-geist-sans), "Geist", var(--font-vivo-sans-sc, "vivo Sans SC"), "Noto Sans SC",
911
- sans-serif;
1145
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
912
1146
  --font-mono:
913
1147
  var(--font-geist-mono), "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
914
1148
  }
@@ -938,9 +1172,71 @@
938
1172
  ─────────────────────────────────────────────────────────── */
939
1173
  :root {
940
1174
  --motion-ease-in: cubic-bezier(0.4, 0, 1, 1);
941
- --motion-ease-out: cubic-bezier(0, 0, 0.2, 1);
1175
+ --motion-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
942
1176
  --motion-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
943
1177
  --motion-ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
1178
+ /* The signature: a long, strong deceleration — most of the distance is
1179
+ covered in the first fifth of the time, then it settles. It has existed as
1180
+ `brandEasing.brand` in @nebutra/brand since the motion language was
1181
+ written, usable only from JS, so every CSS transition on every marketing
1182
+ surface fell back to Tailwind's default curve instead. That default is
1183
+ what "the hovers feel stiff" is: a near-linear ramp at 150ms. */
1184
+ --motion-ease-brand: cubic-bezier(0.16, 1, 0.3, 1);
1185
+ }
1186
+
1187
+ /* ─── Category rail ──────────────────────────────────────────
1188
+ Four accents for taxonomy — the monorepo tree's four lanes, and
1189
+ anywhere else a small set of peers has to be told apart.
1190
+
1191
+ Derived, not picked. Hand-picking is how the file tree ended up
1192
+ with fifteen hues across twenty-five rows, three of them reused
1193
+ on unrelated rows: a colour that encodes nothing a reader can
1194
+ learn and follows no design language.
1195
+
1196
+ Each accent is a position on the background→foreground axis,
1197
+ tinted a quarter of the way toward the action fill rotated by
1198
+ 72°. Two axes, both always live:
1199
+
1200
+ • the position carries contrast. That axis is non-degenerate
1201
+ in every language by definition — it is the axis that makes
1202
+ body text readable — so a monochrome language still gets
1203
+ four distinct tones. Vercel and Notion anchor on black,
1204
+ where rotating hue returns the same colour four times.
1205
+ • the rotation carries hue separation for the languages that
1206
+ have chroma to give.
1207
+
1208
+ Two earlier constructions failed here and are worth naming.
1209
+ Offsetting lightness from the anchor and clamping collapses when
1210
+ the anchor already sits near the band edge — Raycast's fill is
1211
+ near-white, so all four pinned to one value. A fixed lightness
1212
+ ladder cannot serve both canvases: Raycast and GSAP are dark by
1213
+ default in light mode, so the `.dark` class is not set and an
1214
+ absolute ladder is upside down for them.
1215
+
1216
+ Measured across all seven languages plus factory: lowest contrast
1217
+ 3.50:1 against each canvas, closest pair 30/255 apart.
1218
+ ─────────────────────────────────────────────────────────── */
1219
+ :root {
1220
+ --category-1: color-mix(
1221
+ in oklab,
1222
+ color-mix(in oklab, hsl(var(--foreground)) 55%, hsl(var(--background))) 75%,
1223
+ oklch(from hsl(var(--primary)) l c h)
1224
+ );
1225
+ --category-2: color-mix(
1226
+ in oklab,
1227
+ color-mix(in oklab, hsl(var(--foreground)) 67%, hsl(var(--background))) 75%,
1228
+ oklch(from hsl(var(--primary)) l c calc(h + 72))
1229
+ );
1230
+ --category-3: color-mix(
1231
+ in oklab,
1232
+ color-mix(in oklab, hsl(var(--foreground)) 79%, hsl(var(--background))) 75%,
1233
+ oklch(from hsl(var(--primary)) l c calc(h + 144))
1234
+ );
1235
+ --category-4: color-mix(
1236
+ in oklab,
1237
+ color-mix(in oklab, hsl(var(--foreground)) 91%, hsl(var(--background))) 75%,
1238
+ oklch(from hsl(var(--primary)) l c calc(h + 216))
1239
+ );
944
1240
  }
945
1241
 
946
1242
  /* ─── Spacing source rail ────────────────────────────────────
@@ -992,6 +1288,20 @@
992
1288
  border-r-transparent can override it. */
993
1289
  border-color: hsl(var(--border));
994
1290
  }
1291
+
1292
+ /* Headings take the display face. Layered (base) so any utility — font-*,
1293
+ font-semibold, a text-heading-* recipe — still wins; zero-specificity
1294
+ :where() so a component's own heading styles are never fought. The face
1295
+ and weight are variables, so a Brand Package retunes both. */
1296
+ :where(h1, h2, h3, h4, h5, h6) {
1297
+ font-family: var(--font-heading);
1298
+ font-weight: var(--font-weight-heading, 500);
1299
+ line-height: 1.25;
1300
+ }
1301
+ /* This is the ONLY heading rule. @nebutra/ui fonts.css and the web and
1302
+ landing globals each re-declared one (font-sans at 600), and whichever
1303
+ loaded last won: landing's h1 shipped in Geist 600 after the 2026-09-25
1304
+ decision set headings to DM Sans 500. */
995
1305
  }
996
1306
 
997
1307
  body {
@@ -1003,8 +1313,15 @@ body {
1003
1313
  "calt" 1;
1004
1314
  }
1005
1315
 
1006
- html {
1007
- scroll-behavior: smooth;
1316
+ /* Smooth scrolling is an enhancement, never the default the whole document
1317
+ * inherits: it hijacks every programmatic scroll (route changes, focus jumps,
1318
+ * scrollIntoView) and turns them into long animated sweeps. The preference
1319
+ * gate makes the intent explicit; the reduced-motion block below forces
1320
+ * `auto` for users who ask for less motion. */
1321
+ @media (prefers-reduced-motion: no-preference) {
1322
+ html {
1323
+ scroll-behavior: smooth;
1324
+ }
1008
1325
  }
1009
1326
 
1010
1327
  /*
@@ -1026,7 +1343,7 @@ html {
1026
1343
  :focus-visible:not(
1027
1344
  :where(input, textarea, select, [cmdk-input], [role="textbox"], [contenteditable="true"])
1028
1345
  ) {
1029
- outline: 2px solid hsl(var(--ring) / 0.5);
1346
+ outline: 2px solid hsl(var(--ring));
1030
1347
  outline-offset: 2px;
1031
1348
  border-radius: inherit;
1032
1349
  }
@@ -1046,24 +1363,6 @@ html {
1046
1363
  }
1047
1364
  }
1048
1365
 
1049
- @keyframes accordion-down {
1050
- from {
1051
- height: 0;
1052
- }
1053
- to {
1054
- height: var(--radix-accordion-content-height);
1055
- }
1056
- }
1057
-
1058
- @keyframes accordion-up {
1059
- from {
1060
- height: var(--radix-accordion-content-height);
1061
- }
1062
- to {
1063
- height: 0;
1064
- }
1065
- }
1066
-
1067
1366
  @keyframes fade-in {
1068
1367
  from {
1069
1368
  opacity: 0;
@@ -1307,6 +1606,34 @@ html {
1307
1606
  font-weight: 500;
1308
1607
  }
1309
1608
 
1609
+ /* Tracking utilities over the tracking tokens — the scale itself is
1610
+ * Tailwind's, so per-size letter-spacing has to be applied where a heading
1611
+ * is rendered. Exposing the token as a utility keeps that application in
1612
+ * one vocabulary instead of every surface re-deriving an em value. */
1613
+ @utility tracking-display {
1614
+ letter-spacing: var(--tracking-display);
1615
+ }
1616
+
1617
+ @utility tracking-heading {
1618
+ letter-spacing: var(--tracking-heading);
1619
+ }
1620
+
1621
+ @utility tracking-label {
1622
+ letter-spacing: var(--tracking-label);
1623
+ }
1624
+
1625
+ @utility tracking-tight-body {
1626
+ letter-spacing: var(--tracking-tight);
1627
+ }
1628
+
1629
+ @utility leading-display {
1630
+ line-height: var(--leading-display);
1631
+ }
1632
+
1633
+ @utility leading-heading {
1634
+ line-height: var(--leading-heading);
1635
+ }
1636
+
1310
1637
  @utility text-label-20-strong {
1311
1638
  font-family: var(--font-sans);
1312
1639
  font-size: 1.25rem;
@@ -1631,18 +1958,29 @@ html {
1631
1958
 
1632
1959
  /*
1633
1960
  * flex-col + items-center:
1634
- * 1) Direct children (incl. AnimateIn wrappers) get a sane box — max 100% of
1635
- * the column, min-width 0 so they can wrap instead of forcing the column
1636
- * to min-content.
1961
+ * 1) Direct children (incl. AnimateIn wrappers) get min-width 0 so they can
1962
+ * wrap instead of forcing the column to min-content.
1637
1963
  * 2) Nested prose (p/h*) under those columns takes width:100% so Chinese
1638
1964
  * copy does not collapse to a 1-glyph strip. We deliberately do NOT set
1639
1965
  * max-width here — leave Tailwind max-w-* / max-w-[…] in control.
1966
+ *
1967
+ * (1) used to also set `max-width: 100%`, which took that control away from
1968
+ * every utility it was written to defer to. `:is(html:lang(zh)) :is(.flex
1969
+ * .flex-col.items-center) > *` scores three classes; `.max-w-[20rem]` scores
1970
+ * one, so the cap lost — on CJK locales only. The footer newsletter field was
1971
+ * capped at 20rem in the markup and rendered 670px wide on /zh-Hans while the
1972
+ * English page was correct, which is the shape this class of bug always takes:
1973
+ * a global that only fires under :lang().
1974
+ *
1975
+ * min-width: 0 is what actually prevents the one-glyph strip. max-width: 100%
1976
+ * was doing nothing for it — a flex column child cannot exceed the line box
1977
+ * unless it is given an explicit larger width, and if it is, that is a
1978
+ * deliberate local decision this file should not silently overrule.
1640
1979
  */
1641
1980
  :is(html:lang(zh), html:lang(ja), html:lang(ko))
1642
1981
  :is(.flex.flex-col.items-center, .inline-flex.flex-col.items-center)
1643
1982
  > * {
1644
1983
  box-sizing: border-box;
1645
- max-width: 100%;
1646
1984
  min-width: 0;
1647
1985
  }
1648
1986
 
@@ -1729,16 +2067,21 @@ html {
1729
2067
  }
1730
2068
 
1731
2069
  /* ─── Reduced Motion ─────────────────────────────────────── */
1732
- /* Honors OS preference EXCEPT for functional progress indicators
1733
- * (spinners / pulses / pings). Per WCAG 2.3.3, those are excluded
1734
- * from the "animation from interactions" reduction requirement. */
2070
+ /* Reduce, don't remove (WCAG 2.3.3, Apple HIG): movement and layout
2071
+ * animation stop, while opacity/colour transitions stay short so a state
2072
+ * change still reads as a change instead of an instant swap. Restricting
2073
+ * `transition-property` is what kills transform/height transitions without
2074
+ * touching the feedback that aids comprehension. Spinners and pulses are
2075
+ * functional progress indicators and keep their motion. */
1735
2076
  @media (prefers-reduced-motion: reduce) {
1736
2077
  *:not(.animate-spin):not(.animate-pulse):not(.animate-ping),
1737
2078
  *:not(.animate-spin):not(.animate-pulse):not(.animate-ping)::before,
1738
2079
  *:not(.animate-spin):not(.animate-pulse):not(.animate-ping)::after {
1739
2080
  animation-duration: 0.01ms;
1740
2081
  animation-iteration-count: 1;
1741
- transition-duration: 0.01ms;
2082
+ transition-property:
2083
+ opacity, color, background-color, border-color, outline-color, fill, stroke, box-shadow;
2084
+ transition-duration: 120ms;
1742
2085
  scroll-behavior: auto;
1743
2086
  }
1744
2087
  }