@nebutra/tokens 2.0.0 → 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.
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;
@@ -96,20 +96,91 @@
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-noto-sans-sc, "Noto Sans SC"), "PingFang SC",
105
- "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-noto-sans-sc, "Noto Sans SC"), "PingFang SC",
108
- "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-noto-sans-sc, "Noto Sans SC"), sans-serif;
181
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
111
182
  --font-heading:
112
- var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), sans-serif;
183
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
113
184
  --font-mono:
114
185
  var(--font-geist-mono), "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
115
186
  --brand-primary: #0033fe; /** Brand primary — 云毓蓝 */
@@ -153,17 +224,17 @@
153
224
  --motion-duration-cinematic: 500ms;
154
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. */
155
226
  --neutral-1: #ffffff; /** app background */
156
- --neutral-2: #f8fafc; /** subtle background */
157
- --neutral-3: #f1f5f9; /** component bg default */
158
- --neutral-4: #e2e8f0; /** component bg hover */
159
- --neutral-5: #cbd5e1; /** component bg active */
160
- --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. */
161
- --neutral-7: #96a3b5; /** default border — DERIVED: OKLab lerp step5 → step9 at t=0.50. */
162
- --neutral-8: #7d8ba0; /** hovered border — DERIVED: OKLab lerp step5 → step9 at t=0.75. */
163
- --neutral-9: #64748b; /** solid fill */
164
- --neutral-10: #475569; /** solid fill hover */
165
- --neutral-11: #334155; /** secondary text */
166
- --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 */
167
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. */
168
239
  --blue-2: #f2f6fd; /** subtle background — L* 97.21, C 0.010. */
169
240
  --blue-3: #e7eefc; /** component background default — L* 94.79, C 0.020. */
@@ -190,20 +261,20 @@
190
261
  --cyan-11: #027a61; /** secondary text — L* 51.64, C 0.100. 5.17:1 on step 1. */
191
262
  --cyan-12: #004133; /** primary text — L* 33.45, C 0.065. 11.35:1 on step 1. */
192
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. */
193
- --background: 0 0% 100%; /** HSL channel triple — consume via hsl(var(--background)) */
194
- --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%;
195
266
  --card: 0 0% 100%;
196
- --card-foreground: 222 47% 11%;
267
+ --card-foreground: 225 7.7% 10.2%;
197
268
  --popover: 0 0% 100%;
198
- --popover-foreground: 222 47% 11%;
199
- --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. */
200
271
  --primary-foreground: 0 0% 100%;
201
- --secondary: 210 40% 96%;
202
- --secondary-foreground: 222 47% 11%;
203
- --muted: 210 40% 96%;
204
- --muted-foreground: 215 25% 45%;
205
- --accent: 228 100% 97%;
206
- --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 */
207
278
  --destructive: 0 84% 45%;
208
279
  --destructive-foreground: 0 0% 100%;
209
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. */
@@ -215,17 +286,17 @@
215
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. */
216
287
  --info: 228 85% 56%;
217
288
  --info-foreground: 0 0% 100%;
218
- --border: 240 5.9% 90%;
219
- --input: 240 5.9% 90%;
220
- --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). */
221
- --sidebar: 210 40% 98%;
222
- --sidebar-foreground: 215 25% 27%;
223
- --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. */
224
295
  --sidebar-primary-foreground: 0 0% 100%;
225
- --sidebar-accent: 228 100% 97%;
226
- --sidebar-accent-foreground: 228 85% 56%;
227
- --sidebar-border: 214 32% 91%;
228
- --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%;
229
300
  --chart-1: 228 100% 50%;
230
301
  --chart-2: 168 91% 49%;
231
302
  --chart-3: 263 70% 66%;
@@ -374,17 +445,17 @@
374
445
  --nebutra-cyan-800: #057963;
375
446
  --nebutra-cyan-900: #035143;
376
447
  --nebutra-cyan-950: #012923;
377
- --nebutra-neutral-50: #f8fafc;
378
- --nebutra-neutral-100: #f1f5f9;
379
- --nebutra-neutral-200: #e2e8f0;
380
- --nebutra-neutral-300: #cbd5e1;
381
- --nebutra-neutral-400: #94a3b8;
382
- --nebutra-neutral-500: #64748b;
383
- --nebutra-neutral-600: #475569;
384
- --nebutra-neutral-700: #334155;
385
- --nebutra-neutral-800: #1e293b;
386
- --nebutra-neutral-900: #0f172a;
387
- --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;
388
459
  --nebutra-gray-50: #fafafa;
389
460
  --nebutra-gray-100: #f5f5f5;
390
461
  --nebutra-gray-200: #e5e5e5;
@@ -423,22 +494,93 @@
423
494
  --tracking-display: -0.04em; /** Hero H1 */
424
495
  --tracking-heading: -0.03em; /** Section H2/H3 */
425
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. */
426
498
  --leading-display: 1.05;
427
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. */
428
570
  --ring-hairline: inset 0 0 0 1px rgb(255 255 255 / 0.06); /** Hairline override — more visible on dark background */
429
571
  --overlay-faint: rgb(
430
572
  255 255 255 / 0.06
431
573
  ); /** Dark-mode faint overlay (6% white) — inverted from light */
432
574
  --font-sans:
433
- var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), "PingFang SC",
434
- "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;
435
577
  --font-cn:
436
- var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), "PingFang SC",
437
- "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 */
438
580
  --font-display:
439
- var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), sans-serif;
581
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
440
582
  --font-heading:
441
- var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), sans-serif;
583
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
442
584
  --font-mono:
443
585
  var(--font-geist-mono), "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
444
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. */
@@ -481,18 +623,18 @@
481
623
  --motion-duration-reveal: 300ms;
482
624
  --motion-duration-cinematic: 500ms;
483
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. */
484
- --neutral-1: #020617;
485
- --neutral-2: #0f172a;
486
- --neutral-3: #1e293b;
487
- --neutral-4: #334155;
488
- --neutral-5: #475569;
626
+ --neutral-1: #0b0c0d;
627
+ --neutral-2: #18191c;
628
+ --neutral-3: #26272a;
629
+ --neutral-4: #3f4044;
630
+ --neutral-5: #515357;
489
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. */
490
- --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. */
491
633
  --neutral-8: #8d8d8d; /** hovered border — DERIVED: OKLab lerp step5 → step9 at t=0.75, chroma 0. */
492
- --neutral-9: #94a3b8;
493
- --neutral-10: #cbd5e1;
494
- --neutral-11: #e2e8f0;
495
- --neutral-12: #f8fafc;
634
+ --neutral-9: #9fa1a5;
635
+ --neutral-10: #d2d4d8;
636
+ --neutral-11: #e4e5e8;
637
+ --neutral-12: #f9fafb;
496
638
  --blue-1: #030815; /** app tint — L* 13.59, C 0.031, hue 264. */
497
639
  --blue-2: #060f25; /** subtle background — L* 17.43, C 0.048. */
498
640
  --blue-3: #09183b; /** component background default — L* 22.04, C 0.071. */
@@ -519,20 +661,20 @@
519
661
  --cyan-11: #90fbdb; /** secondary text — L* 91.46, C 0.110. 15.68:1 on step 1. */
520
662
  --cyan-12: #bdfde8; /** primary text — L* 94.54, C 0.070. 16.95:1 on step 1. */
521
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. */
522
- --background: 222 14% 9%;
523
- --foreground: 210 18% 96%;
524
- --card: 222 12% 13%;
525
- --card-foreground: 210 18% 96%;
526
- --popover: 222 12% 13%;
527
- --popover-foreground: 210 18% 96%;
528
- --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. */
529
- --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. */
530
- --secondary: 222 10% 18%;
531
- --secondary-foreground: 210 18% 96%;
532
- --muted: 222 10% 17%;
533
- --muted-foreground: 220 8% 65%;
534
- --accent: 228 38% 22%;
535
- --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%;
536
678
  --destructive: 0 63% 38%;
537
679
  --destructive-foreground: 210 18% 96%;
538
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. */
@@ -544,17 +686,17 @@
544
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. */
545
687
  --info: 228 95% 72%;
546
688
  --info-foreground: 222 14% 9%;
547
- --border: 220 8% 22%;
548
- --input: 220 8% 22%;
549
- --ring: 0 0% 88%; /** Focus ring (dark mode) — neutral light gray. Mirrors light-mode neutral foreground choice. */
550
- --sidebar: 222 16% 8%;
551
- --sidebar-foreground: 210 18% 94%;
552
- --sidebar-primary: 228 95% 72%;
553
- --sidebar-primary-foreground: 222 14% 9%;
554
- --sidebar-accent: 228 38% 22%;
555
- --sidebar-accent-foreground: 228 95% 80%;
556
- --sidebar-border: 220 8% 18%;
557
- --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%;
558
700
  --chart-1: 228 95% 67%;
559
701
  --chart-2: 168 96% 54%;
560
702
  --chart-3: 228 100% 82%;
@@ -715,6 +857,12 @@
715
857
  --color-success-foreground: hsl(var(--success-foreground));
716
858
  --color-warning: hsl(var(--warning));
717
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));
718
866
  --color-info: hsl(var(--info));
719
867
  --color-info-foreground: hsl(var(--info-foreground));
720
868
  --color-border: hsl(var(--border));
@@ -889,6 +1037,66 @@
889
1037
  --ease-in-out: var(--motion-ease-in-out);
890
1038
  --ease-spring: var(--motion-ease-spring);
891
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
+
892
1100
  /* Motion — Durations (four-rail; see core.json:duration). Names denote intent, not relative speed. */
893
1101
  --duration-micro: var(--motion-duration-micro);
894
1102
  --duration-flow: var(--motion-duration-flow);
@@ -920,20 +1128,21 @@
920
1128
  --spacing-{step} alias. --spacing itself (0.25rem arithmetic base) stays
921
1129
  untouched. See static/base.css. */
922
1130
 
923
- /* Font family — Geist keeps Latin + numerals, self-hosted Noto Sans SC takes
924
- CJK, system CJK faces behind it. Order is the design decision; see the
925
- fontFamily $description in tokens/core.json. Mirror of those tokens — keep
926
- 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). */
927
1136
  --font-sans:
928
- var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), "PingFang SC",
929
- "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;
930
1139
  --font-cn:
931
- var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), "PingFang SC",
932
- "Microsoft YaHei", sans-serif;
1140
+ var(--font-geist-sans), "Geist", var(--font-misans, "MiSans"), "PingFang SC", "Microsoft YaHei",
1141
+ sans-serif;
933
1142
  --font-display:
934
- var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), sans-serif;
1143
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
935
1144
  --font-heading:
936
- var(--font-geist-sans), "Geist", var(--font-noto-sans-sc, "Noto Sans SC"), sans-serif;
1145
+ var(--font-dm-sans), "DM Sans", var(--font-misans, "MiSans"), "PingFang SC", sans-serif;
937
1146
  --font-mono:
938
1147
  var(--font-geist-mono), "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
939
1148
  }
@@ -1079,6 +1288,20 @@
1079
1288
  border-r-transparent can override it. */
1080
1289
  border-color: hsl(var(--border));
1081
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. */
1082
1305
  }
1083
1306
 
1084
1307
  body {
@@ -1090,8 +1313,15 @@ body {
1090
1313
  "calt" 1;
1091
1314
  }
1092
1315
 
1093
- html {
1094
- 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
+ }
1095
1325
  }
1096
1326
 
1097
1327
  /*
@@ -1113,7 +1343,7 @@ html {
1113
1343
  :focus-visible:not(
1114
1344
  :where(input, textarea, select, [cmdk-input], [role="textbox"], [contenteditable="true"])
1115
1345
  ) {
1116
- outline: 2px solid hsl(var(--ring) / 0.5);
1346
+ outline: 2px solid hsl(var(--ring));
1117
1347
  outline-offset: 2px;
1118
1348
  border-radius: inherit;
1119
1349
  }
@@ -1133,24 +1363,6 @@ html {
1133
1363
  }
1134
1364
  }
1135
1365
 
1136
- @keyframes accordion-down {
1137
- from {
1138
- height: 0;
1139
- }
1140
- to {
1141
- height: var(--radix-accordion-content-height);
1142
- }
1143
- }
1144
-
1145
- @keyframes accordion-up {
1146
- from {
1147
- height: var(--radix-accordion-content-height);
1148
- }
1149
- to {
1150
- height: 0;
1151
- }
1152
- }
1153
-
1154
1366
  @keyframes fade-in {
1155
1367
  from {
1156
1368
  opacity: 0;
@@ -1394,6 +1606,34 @@ html {
1394
1606
  font-weight: 500;
1395
1607
  }
1396
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
+
1397
1637
  @utility text-label-20-strong {
1398
1638
  font-family: var(--font-sans);
1399
1639
  font-size: 1.25rem;
@@ -1718,18 +1958,29 @@ html {
1718
1958
 
1719
1959
  /*
1720
1960
  * flex-col + items-center:
1721
- * 1) Direct children (incl. AnimateIn wrappers) get a sane box — max 100% of
1722
- * the column, min-width 0 so they can wrap instead of forcing the column
1723
- * 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.
1724
1963
  * 2) Nested prose (p/h*) under those columns takes width:100% so Chinese
1725
1964
  * copy does not collapse to a 1-glyph strip. We deliberately do NOT set
1726
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.
1727
1979
  */
1728
1980
  :is(html:lang(zh), html:lang(ja), html:lang(ko))
1729
1981
  :is(.flex.flex-col.items-center, .inline-flex.flex-col.items-center)
1730
1982
  > * {
1731
1983
  box-sizing: border-box;
1732
- max-width: 100%;
1733
1984
  min-width: 0;
1734
1985
  }
1735
1986
 
@@ -1816,16 +2067,21 @@ html {
1816
2067
  }
1817
2068
 
1818
2069
  /* ─── Reduced Motion ─────────────────────────────────────── */
1819
- /* Honors OS preference EXCEPT for functional progress indicators
1820
- * (spinners / pulses / pings). Per WCAG 2.3.3, those are excluded
1821
- * 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. */
1822
2076
  @media (prefers-reduced-motion: reduce) {
1823
2077
  *:not(.animate-spin):not(.animate-pulse):not(.animate-ping),
1824
2078
  *:not(.animate-spin):not(.animate-pulse):not(.animate-ping)::before,
1825
2079
  *:not(.animate-spin):not(.animate-pulse):not(.animate-ping)::after {
1826
2080
  animation-duration: 0.01ms;
1827
2081
  animation-iteration-count: 1;
1828
- transition-duration: 0.01ms;
2082
+ transition-property:
2083
+ opacity, color, background-color, border-color, outline-color, fill, stroke, box-shadow;
2084
+ transition-duration: 120ms;
1829
2085
  scroll-behavior: auto;
1830
2086
  }
1831
2087
  }