Files
zeekayandHanzo Dev 7bb6b4c2cf Resolve the lock and the generated sheets against the catalog
The catalog names @hanzo/design 0.5.24 and typescript 7.0.2, but the lock
still resolved ^0.5.18 and ^5.9.3, so a frozen install of this head refuses
("specifiers in the lockfile don't match specifiers in package.json"), and
styles.css, tailwind.css and tokens.gen.ts were last generated from 0.5.18.

This is `pnpm install` under the pinned pnpm 11.24.0 followed by
`pnpm build`, nothing hand-edited: the lock states the catalog's versions,
and the sheets inherit 0.5.24 — --font-feature-settings is now `normal` and
the scrollbar button is hidden, both substrate changes. All 26 token checks
pass and tsc emits dist/ cleanly.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-04 14:17:19 -07:00

895 lines
38 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Lux Design System — the entry point. Import THIS one file.
*
* AUTO-GENERATED by scripts/gen-tokens.mjs — DO NOT EDIT.
* Edit tokens/brand.css and re-run "pnpm gen".
*
* The neutral system below is inherited from @hanzo/[email protected] at build
* time and is not maintained here; the Lux brand layer is the last block in the
* file. Typeface: Zen — this sheet declares no @font-face and no url(), so
* install @hanzo/font and import "@hanzo/font/css" ahead of this one if you want
* the faces rather than the system stack. See scripts/gen-tokens.mjs for why.
*/
/* ── inherited: colors ───────────────────────────────────── */
:root{
color-scheme:dark;
--neutral-50:#FAFAFA;
--neutral-100:#F5F5F5;
--neutral-200:#E5E5E5;
--neutral-300:#D4D4D4;
--neutral-400:#A3A3A3;
--neutral-500:#737373;
--neutral-600:#525252;
--neutral-700:#404040;
--neutral-800:#262626;
--neutral-900:#171717;
--neutral-950:#0A0A0A;
--pure-black:#000000;
--pure-white:#FFFFFF;
--lux-black:#0A0A0B;
--lux-white:#FFFFFF;
--white-03:rgb(255 255 255 / .03);
--white-05:rgb(255 255 255 / .05);
--white-06:rgb(255 255 255 / .06);
--white-08:rgb(255 255 255 / .08);
--white-10:rgb(255 255 255 / .10);
--white-14:rgb(255 255 255 / .14);
--white-15:rgb(255 255 255 / .15);
--white-16:rgb(255 255 255 / .16);
--white-18:rgb(255 255 255 / .18);
--white-20:rgb(255 255 255 / .20);
--white-22:rgb(255 255 255 / .22);
--white-30:rgb(255 255 255 / .30);
--white-40:rgb(255 255 255 / .40);
--white-45:rgb(255 255 255 / .45);
--white-55:rgb(255 255 255 / .55);
--white-60:rgb(255 255 255 / .60);
--white-78:rgb(255 255 255 / .78);
--white-80:rgb(255 255 255 / .80);
--white-92:rgb(255 255 255 / .92);
--background:#0a0a0a;
--foreground:#e5e5e5;
--card:#0f0f0f;
--card-foreground:#e5e5e5;
--popover:#0f0f0f;
--popover-foreground:#e5e5e5;
--primary:#fafafa;
--primary-hover:#e5e5e5;
--primary-foreground:#0a0a0a;
--secondary:#262626;
--secondary-hover:#333333;
--secondary-foreground:#fafafa;
--muted:#171717;
--muted-foreground:#a3a3a3;
--accent:#262626;
--accent-foreground:#fafafa;
--destructive:var(--state-error);
--destructive-hover:#dc2626;
--destructive-foreground:#fafafa;
--border:var(--white-10);
--border-strong:var(--white-16);
--border-control:var(--white-15);
--border-focus:var(--white-22);
--border-selected:var(--white-30);
--input:var(--border-control);
--ring:var(--white-40);
--ring-halo:var(--white-10);
--brand:#e4e4e7;
--brand-foreground:#09090b;
--brand-muted:#a3a3a3;
--black:#000000;
--white:#fafafa;
--selection:var(--white-20);
--glass:var(--white-05);
--glass-strong:var(--white-08);
--surface-page:var(--background);
--surface-card:rgb(38 38 38 / .5);
--surface-card-emphasis:rgb(38 38 38 / .75);
--surface-card-quiet:rgb(38 38 38 / .35);
--surface-overlay:rgb(23 23 23 / .92);
--surface-header:rgb(0 0 0 / .7);
--surface-scrim:rgb(0 0 0 / .8);
--surface-0:var(--background);
--surface-1:var(--card);
--surface-2:var(--muted);
--surface-3:var(--secondary);
--text-primary:#fafafa;
--text-secondary:var(--white-78);
--text-tertiary:var(--white-55);
--text-helper:var(--muted-foreground);
--text-disabled:var(--white-30);
--state-error:#ef4444;
--state-error-text:#fca5a5;
--state-error-bg:rgb(239 68 68 / .1);
--state-online:#4ade80;
--state-success:#22c55e;
--chrome-dot-red:rgb(239 68 68 / .6);
--chrome-dot-yellow:rgb(234 179 8 / .6);
--chrome-dot-green:rgb(34 197 94 / .6);
}
.light{
color-scheme:light;
--background:#f7f7f7;
--foreground:#0a0a0a;
--card:#f2f2f2;
--card-foreground:#0a0a0a;
--popover:#fbfbfb;
--popover-foreground:#0a0a0a;
--primary:#0a0a0a;
--primary-hover:#262626;
--primary-foreground:#fafafa;
--secondary:#e4e4e4;
--secondary-hover:#e0e0e0;
--secondary-foreground:#0a0a0a;
--muted:#ededed;
--muted-foreground:#525252;
--accent:#e4e4e4;
--accent-foreground:#0a0a0a;
--destructive:var(--state-error);
--destructive-hover:#dc2626;
--destructive-foreground:#ffffff;
--border:rgb(0 0 0 / .10);
--border-strong:rgb(0 0 0 / .16);
--border-control:rgb(0 0 0 / .15);
--border-focus:rgb(0 0 0 / .32);
--border-selected:rgb(0 0 0 / .42);
--input:var(--border-control);
--ring:rgb(0 0 0 / .5);
--ring-halo:rgb(0 0 0 / .07);
--black:#0a0a0a;
--white:#ffffff;
--selection:rgb(0 0 0 / .16);
--glass:rgb(0 0 0 / .04);
--glass-strong:rgb(0 0 0 / .07);
--surface-card:#f2f2f2;
--surface-card-emphasis:#fdfdfd;
--surface-card-quiet:#f5f5f5;
--surface-overlay:rgb(255 255 255 / .95);
--surface-header:rgb(255 255 255 / .8);
--surface-scrim:rgb(0 0 0 / .5);
--text-primary:var(--neutral-950);
--text-secondary:rgb(10 10 10 / .78);
--text-tertiary:rgb(10 10 10 / .55);
--text-disabled:rgb(10 10 10 / .3);
}
/* ── inherited: typography ───────────────────────────────────── */
:root{
--type-scale:1;
--type-ratio:1;
--text-floor:0.625rem;
--text-ceiling:30vw;
--text-xs:clamp(var(--text-floor), calc((0.875rem - 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-xs:calc((1.35rem - 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1));
--text-sm:clamp(var(--text-floor), calc((0.875rem - 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-sm:calc((1.35rem - 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1));
--text-base:clamp(var(--text-floor), calc(0.875rem * var(--type-scale, 1)), var(--text-ceiling)); --leading-base:calc(1.35rem * var(--type-scale, 1));
--text-lg:clamp(var(--text-floor), calc((0.875rem + 0.0625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-lg:calc((1.35rem + 0.05rem * var(--type-ratio, 1)) * var(--type-scale, 1));
--text-xl:clamp(var(--text-floor), calc((0.875rem + 0.1875rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-xl:calc((1.35rem + 0.2rem * var(--type-ratio, 1)) * var(--type-scale, 1));
--text-2xl:clamp(var(--text-floor), calc((0.875rem + 0.4375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-2xl:calc((1.35rem + 0.35rem * var(--type-ratio, 1)) * var(--type-scale, 1));
--text-3xl:clamp(var(--text-floor), calc((0.875rem + 0.75rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-3xl:calc((1.35rem + 0.6rem * var(--type-ratio, 1)) * var(--type-scale, 1));
--text-4xl:clamp(var(--text-floor), calc((0.875rem + 1.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-4xl:calc((1.35rem + 0.9rem * var(--type-ratio, 1)) * var(--type-scale, 1));
--text-5xl:clamp(var(--text-floor), calc((0.875rem + 1.625rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-5xl:1.05;
--text-6xl:clamp(var(--text-floor), calc((0.875rem + 2.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-6xl:1;
--text-7xl:clamp(var(--text-floor), calc((0.875rem + 3.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-7xl:1;
--text-8xl:clamp(var(--text-floor), calc((0.875rem + 4.375rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-8xl:1;
--text-9xl:clamp(var(--text-floor), calc((0.875rem + 6.125rem * var(--type-ratio, 1)) * var(--type-scale, 1)), var(--text-ceiling)); --leading-9xl:1;
--font-size-xs:var(--text-xs);
--font-size-sm:var(--text-sm);
--font-size-base:var(--text-base);
--font-size-lg:var(--text-lg);
--font-size-xl:var(--text-xl);
--font-size-2xl:var(--text-2xl);
--font-size-3xl:var(--text-3xl);
--font-size-4xl:var(--text-4xl);
--font-size-5xl:var(--text-5xl);
--font-size-6xl:var(--text-6xl);
--font-size-7xl:var(--text-7xl);
--font-size-8xl:var(--text-8xl);
--font-size-9xl:var(--text-9xl);
--text-control:var(--text-sm);
--weight-normal:400;
--weight-medium:500;
--weight-semibold:600;
--weight-bold:700;
--tracking-tight:-0.025em;
--tracking-normal:0em;
--tracking-wide:0.025em;
--tracking-widest:0.1em;
--leading-none:1;
--leading-tight:1.25;
--leading-snug:1.375;
--leading-normal:1.5;
--leading-relaxed:1.625;
--leading-golden:1.618;
--type-hero:600 var(--text-5xl)/1.05 var(--font-display);
--type-h2:700 var(--text-4xl)/var(--leading-4xl) var(--font-display);
--type-h3:600 var(--text-xl)/var(--leading-xl) var(--font-display);
--type-lead:400 var(--text-lg)/var(--leading-relaxed) var(--font-sans);
--type-body:400 var(--text-sm)/var(--leading-sm) var(--font-sans);
--type-caption:400 var(--text-xs)/var(--leading-xs) var(--font-sans);
--type-code:400 var(--text-sm)/var(--leading-relaxed) var(--font-mono);
--type-eyebrow:600 0.625rem/1 var(--font-sans);
}
@media (pointer:coarse){
:root{--text-control:calc(1rem * var(--type-scale, 1))}
}
/* ── inherited: spacing ───────────────────────────────────── */
:root{
--density:1;
--space-0:0;
--space-1:calc(0.25rem * var(--density, 1));
--space-2:calc(0.5rem * var(--density, 1));
--space-3:calc(0.75rem * var(--density, 1));
--space-4:calc(1rem * var(--density, 1));
--space-5:calc(1.25rem * var(--density, 1));
--space-6:calc(1.5rem * var(--density, 1));
--space-8:calc(2rem * var(--density, 1));
--space-10:calc(2.5rem * var(--density, 1));
--space-12:calc(3rem * var(--density, 1));
--space-14:calc(3.5rem * var(--density, 1));
--space-16:calc(4rem * var(--density, 1));
--space-20:calc(5rem * var(--density, 1));
--space-24:calc(6rem * var(--density, 1));
--space-32:calc(8rem * var(--density, 1));
--golden-1:0.25rem;
--golden-2:0.405rem;
--golden-3:0.654rem;
--golden-4:1.059rem;
--golden-5:1.713rem;
--golden-6:2.772rem;
--golden-7:4.487rem;
--golden-8:7.26rem;
--golden-9:11.749rem;
--golden-split:38.2% 61.8%;
--container-max:80rem;
--container-prose:48rem;
--container-wide:72rem;
--gutter:max(1rem,var(--safe-left),var(--safe-right));
--gutter-sm:1.5rem;
--gutter-lg:2rem;
--section-y:2.5rem;
--section-y-lg:3.5rem;
--hero-y:3rem;
--hero-y-lg:4.5rem;
--header-height:3.5rem;
--tap-target:44px;
--control-height-sm:32px;
--control-height:36px;
--control-height-lg:40px;
--control-height-xl:52px;
--safe-top:env(safe-area-inset-top,0px);
--safe-right:env(safe-area-inset-right,0px);
--safe-bottom:env(safe-area-inset-bottom,0px);
--safe-left:env(safe-area-inset-left,0px);
}
@media (min-width:48rem){
:root{
--section-y:4rem;
--section-y-lg:6rem;
--hero-y:5rem;
--hero-y-lg:8rem;
--header-height:4rem;
}
}
/* ── inherited: grid ───────────────────────────────────── */
:root{
--grid-columns:12;
--grid-gap:var(--space-6);
--grid-gap-tight:var(--space-4);
--grid-gap-loose:var(--space-8);
--breakpoint-sm:40rem;
--breakpoint-md:48rem;
--breakpoint-lg:64rem;
--breakpoint-xl:80rem;
--breakpoint-2xl:96rem;
--grid-card-min:18rem;
--grid-card-min-wide:24rem;
}
/* ── inherited: radius ───────────────────────────────────── */
:root{
--radius:0.5rem;
--radius-sm:0.375rem;
--radius-md:0.5rem;
--radius-lg:0.75rem;
--radius-xl:1rem;
--radius-2xl:1.5rem;
--radius-composer:28px;
--radius-full:9999px;
}
/* ── inherited: elevation ───────────────────────────────────── */
:root{
--shadow-none:none;
--shadow-floating:0 24px 60px -16px rgb(0 0 0 / .75);
--shadow-inset-hairline:inset 0 0 0 1px var(--white-10);
--edge-highlight:inset 0 1px 0 0 rgb(255 255 255 / .10);
--bloom:0 0 24px -2px rgb(255 255 255 / .28);
--ring-focus:0 0 0 3px var(--ring-halo);
--shadow-sm:0 1px 2px 0 rgb(0 0 0 / .40);
--shadow:0 1px 3px 0 rgb(0 0 0 / .45), 0 1px 2px -1px rgb(0 0 0 / .45);
--shadow-md:0 4px 6px -1px rgb(0 0 0 / .50), 0 2px 4px -2px rgb(0 0 0 / .50);
--shadow-lg:0 10px 15px -3px rgb(0 0 0 / .55), 0 4px 6px -4px rgb(0 0 0 / .55);
--shadow-xl:0 20px 25px -5px rgb(0 0 0 / .60), 0 8px 10px -6px rgb(0 0 0 / .60);
--shadow-2xl:var(--shadow-floating);
--sheet-0:var(--surface-page);
--sheet-1:var(--surface-card);
--sheet-2:var(--surface-card-emphasis);
--shadow-sheet-1:var(--edge-highlight),0 1px 2px 0 rgb(0 0 0 / .40);
--shadow-sheet-2:var(--edge-highlight),0 10px 15px -3px rgb(0 0 0 / .55),0 4px 6px -4px rgb(0 0 0 / .55);
--fold:12px;
--fold-face:linear-gradient(315deg,var(--glass-strong) 0,var(--glass-strong) calc(var(--fold) - 1px),var(--border-strong) calc(var(--fold) - 1px),var(--border-strong) var(--fold),transparent var(--fold));
--glow-hero:radial-gradient(circle,rgb(255 255 255 / .12) 0%,transparent 68%);
--glow-hero-blur:120px;
--sheen-card:radial-gradient(120% 120% at 80% 0%,rgb(255 255 255 / .08) 0%,transparent 55%);
--sheen-edge:linear-gradient(90deg,transparent,rgb(255 255 255 / .14) 50%,transparent);
--gradient-chrome:linear-gradient(to right,#ffffff,var(--white-80),var(--white-60));
--gradient-chrome-2:linear-gradient(to right,#ffffff,var(--neutral-500));
--gradient-protect:linear-gradient(to bottom,var(--white-10),transparent);
}
.light{
--shadow-floating:0 24px 60px -16px rgb(0 0 0 / .18);
--shadow-sheet-1:var(--edge-highlight),0 1px 2px 0 rgb(0 0 0 / .06);
--shadow-sheet-2:var(--edge-highlight),0 10px 15px -3px rgb(0 0 0 / .09),0 4px 6px -4px rgb(0 0 0 / .07);
--shadow-inset-hairline:inset 0 0 0 1px rgb(0 0 0 / .10);
--edge-highlight:inset 0 0 0 0 transparent;
--bloom:0 6px 20px -4px rgb(0 0 0 / .18);
--shadow-sm:0 1px 2px 0 rgb(0 0 0 / .06);
--shadow:0 1px 3px 0 rgb(0 0 0 / .08), 0 1px 2px -1px rgb(0 0 0 / .06);
--shadow-md:0 4px 6px -1px rgb(0 0 0 / .08), 0 2px 4px -2px rgb(0 0 0 / .06);
--shadow-lg:0 10px 15px -3px rgb(0 0 0 / .09), 0 4px 6px -4px rgb(0 0 0 / .07);
--shadow-xl:0 20px 25px -5px rgb(0 0 0 / .10), 0 8px 10px -6px rgb(0 0 0 / .08);
--glow-hero:radial-gradient(circle,rgb(0 0 0 / .06) 0%,transparent 68%);
--sheen-card:radial-gradient(120% 120% at 80% 0%,rgb(0 0 0 / .04) 0%,transparent 55%);
--sheen-edge:linear-gradient(90deg,transparent,rgb(0 0 0 / .08) 50%,transparent);
--gradient-chrome:linear-gradient(to right,#0a0a0a,rgb(10 10 10 / .8),rgb(10 10 10 / .6));
--gradient-chrome-2:linear-gradient(to right,#0a0a0a,var(--neutral-500));
--gradient-protect:linear-gradient(to bottom,rgb(0 0 0 / .10),transparent);
}
/* ── inherited: motion ───────────────────────────────────── */
:root{
--duration-fast:150ms;
--duration-base:300ms;
--duration-slow:400ms;
--duration-slower:500ms;
--duration-glow:9s;
--duration-press:90ms;
--ease-out:cubic-bezier(0,0,0.2,1);
--ease-in-out:cubic-bezier(0.4,0,0.2,1);
--ease-emphasis:cubic-bezier(0.16,1,0.3,1);
--stagger:60ms;
--entry-rise:16px;
--entry-rise-lg:24px;
}
@keyframes lux-fade-up{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
@keyframes lux-fade-down{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
@keyframes lux-slide-up-fade{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
@keyframes lux-zoom-in{from{opacity:0;transform:scale(.97) translateY(8px)}to{opacity:1;transform:none}}
@keyframes lux-glow{0%,100%{transform:scale(1);opacity:.45}50%{transform:scale(1.08);opacity:.65}}
@keyframes lux-pulse-dot{0%,100%{opacity:1}50%{opacity:.35}}
@media (prefers-reduced-motion:reduce){
*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}
}
/* ── inherited: z ───────────────────────────────────── */
:root{
--z-base:0;
--z-raised:10;
--z-sticky:200;
--z-header:300;
--z-dropdown:400;
--z-overlay:500;
--z-modal:600;
--z-popover:700;
--z-toast:800;
--z-tooltip:var(--z-popover);
--z-notification:var(--z-toast);
}
/* ── inherited: fonts ───────────────────────────────────── */
:root{
--font-sans:"Zen",ui-sans-serif,system-ui,sans-serif;
--font-display:var(--font-sans);
--font-mono:"Zen Mono",ui-monospace,SFMono-Regular,monospace;
--font-serif:Georgia,serif;
--font-feature-settings:normal;
}
/* ── inherited: base ───────────────────────────────────── */
@layer base {
*{box-sizing:border-box;border-color:var(--border)}
html{
-webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility;
scroll-behavior:smooth;
-webkit-text-size-adjust:100%;
}
body{margin:0;background:var(--background);color:var(--foreground);font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif);font-size:var(--text-base);line-height:var(--leading-base);font-feature-settings:var(--font-feature-settings);overflow-wrap:break-word}
h1,h2,h3,h4{margin:0;font-family:var(--font-display,var(--font-sans,ui-sans-serif,system-ui,sans-serif));letter-spacing:var(--tracking-tight);color:var(--text-primary)}
p{margin:0;text-wrap:pretty}
code,pre,kbd{font-family:var(--font-mono,ui-monospace,SFMono-Regular,monospace)}
a{color:var(--text-primary);text-decoration:none;text-underline-offset:4px}
:is(p,li,blockquote,dd,dt,td,th,figcaption) a:hover{text-decoration:underline}
:where(a,button,[role=button],input,select,textarea,summary,[tabindex]){
transition:color var(--duration-fast) var(--ease-out),
background-color var(--duration-fast) var(--ease-out),
border-color var(--duration-fast) var(--ease-out),
box-shadow var(--duration-fast) var(--ease-out),
opacity var(--duration-fast) var(--ease-out),
transform var(--duration-press) var(--ease-out);
}
:where(button,[role=button],a[role=button],summary):active:not(:disabled){
transform:scale(.98);
}
:where(input,select,textarea,button){font:inherit;color:inherit}
:where(input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]),select,textarea){
background:var(--surface-2);
border:1px solid var(--border-control);
border-radius:var(--radius-md);
padding:0 var(--space-3);
--ring-offset:-2px;
}
:where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
:where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
:where(input,textarea)::placeholder{color:var(--text-disabled)}
:where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
:where(button,[role=button],summary,label,select){cursor:pointer}
:where(img,svg,video,canvas,iframe,picture,object){max-width:100%}
:where(img,video){height:auto}
:where(pre){overflow-x:auto}
@media (pointer:coarse){
:where(button,[role=button],a[role=button],summary,select,textarea,
input:not([type=checkbox]):not([type=radio]):not([type=hidden])){
min-height:var(--tap-target);
}
:where(input,select,textarea){font-size:var(--text-control)}
:where([role=checkbox],[role=radio],[role=switch],input[type=checkbox],input[type=radio]){position:relative}
:where([role=checkbox],[role=radio],[role=switch],input[type=checkbox],input[type=radio])::after{
content:'';position:absolute;top:50%;left:50%;
width:var(--tap-target);height:var(--tap-target);
transform:translate(-50%,-50%);
}
}
:focus-visible{outline:2px solid var(--ring);outline-offset:var(--ring-offset,2px)}
::selection{background:var(--selection);color:var(--text-primary)}
:where(html){scrollbar-color:var(--white-15) transparent;scrollbar-width:thin}
.light{scrollbar-color:rgb(0 0 0 / .18) transparent}
:where(*)::-webkit-scrollbar{width:6px;height:6px}
:where(*)::-webkit-scrollbar-track{background:transparent}
:where(*)::-webkit-scrollbar-thumb{background:var(--white-15);border-radius:var(--radius-full)}
:where(*)::-webkit-scrollbar-thumb:hover{background:var(--white-30)}
:where(*)::-webkit-scrollbar-corner{background:transparent}
:where(*)::-webkit-scrollbar-button{display:none;width:0;height:0}
.light :where(*)::-webkit-scrollbar-thumb{background:rgb(0 0 0 / .18)}
.light :where(*)::-webkit-scrollbar-thumb:hover{background:rgb(0 0 0 / .32)}
}
/* ── Lux: tokens/brand.css ────────────────────────────── */
/* Lux — the brand layer, and the only hand-authored token file in this package.
*
* Everything else you get from @luxfi/design comes from the shared substrate:
* the neutral ladder, the opacity rungs, the surface recipes, the type scale,
* elevation, motion. That is deliberate, and it is the shape of the whole
* estate — the substrate carries the design-system facts, a brand package
* carries the brand facts, and there is exactly one copy of each.
*
* Lux is the MINIMAL end of that family. Hanzo is monochrome; Lux is monochrome
* taken further, and "further" here is four specific things, each measured
* rather than felt:
*
* the ground pure #000000, where Hanzo's token sits at #0a0a0a
* the accent pure #ffffff, where Hanzo sits at #e4e4e7
* the lift ONE raised surface, where Hanzo has two
* the hairline .08, where Hanzo draws .10 — and paints #262626 OPAQUE
*
* Nothing below invents a value. Every one is either a rung of the inherited
* ladder or a literal @luxfi/brand declares.
*
* The numbers come from a headless render of the live surfaces at 1440px, not
* from source: every painted border, fill and ink value on lux.financial and
* hanzo.ai, counted by element. Where a comment below cites a count, that is
* where it is from.
*
* Before adding a token here, ask whether it is a LUX fact or a DESIGN-SYSTEM
* fact. Design-system facts go upstream, where every brand gets them.
*
* ─── THE GOLD IS REVERSED. DO NOT RESTORE IT. ────────────────────────────
*
* 0.1.0 of this package shipped a gold accent, #D4AF37, in --brand. 0.1.1
* removed it on the CTO's instruction, quoted in full so nobody has to guess
* at the intent later:
*
* "no gold in lux, only monochrome white and black, more severe and
* minimal than hanzo.ai"
*
* The archaeology that produced the gold was real and it is not worth
* repeating: lux.network's globals.css once declared it under a comment
* reading "Gold accent: #D4AF37", the founder's commit message named it, and
* the Lux Exchange carried `gold: '#D4AF37'` as a Tailwind literal. All true,
* all HISTORICAL. Every live Lux surface has since flattened that slot to
* white, and the rebrand that did it also added an end-to-end test on
* lux.network asserting the page paints zero pixels in hue band 20–60. THAT
* TEST IS CORRECT AND MUST STAY. Read it as the fence it is: the estate
* already decided this once, and 0.1.0 climbed the fence.
*
* There is no --lux-gold token here to "just re-enable". That is deliberate.
* An unused brand-colour token is exactly what a future agent re-wires by
* accident, so the hue is gone from the package rather than parked in it.
*
* THE TRAP, NAMED, because it is still live and it is what produced 0.1.0:
* lux.network AS DEPLOYED TODAY STILL PAINTS THE GOLD. A render of it at
* 1440px puts 18,198 saturated pixels on the page, 100% of them in hue bucket
* 40–49 — 0.66% of the first screen — from `--accent: oklch(0.75 0.14 85)`,
* which resolves to #d6a62e. So the deployed site and the test in its own repo
* contradict each other, and anyone who probes the site for "what Lux looks
* like" will find gold and conclude it belongs. It does not. The two surfaces
* that carry the CURRENT brand are lux.financial (0 saturated pixels above the
* fold; its only colour anywhere is one photograph at y=2737) and
* @luxfi/[email protected]. Measure those. The lux.network deployment is stale, and
* bringing it in line is that repo's job, not this package's.
*
* This package's own shipped doctrine never agreed with the gold either —
* prompts/system.md has said "Monochrome. True black. White type. Color only
* as state, never as decoration." since before 0.1.0, and the specimen cards
* in guidelines/ have always painted their ground #000. 0.1.1 is the tokens
* catching up to the words.
*/
/* The Lux layer wins on SPECIFICITY, not on load order.
Both sheets declare --brand. At equal specificity the later one wins, so
importing the substrate after this sheet silently reverts the brand to its
default — and only partly: --brand-hover, --brand-bg and --brand-border
survive, so the swatch row shows one brand value beside a different
brand-hover. Nothing errors. Doubling the selector takes this block to
(0,2,0) and the question of order stops existing.
`.light` is doubled rather than qualified with :root for the same reason the
substrate leaves it bare: a surface may scope the light theme to a SUBTREE,
and `:root.light` would only ever match the document element. `.light.light`
matches any element and still outranks the substrate.
A consumer deliberately retuning these must match (0,2,0) — doubling their
own selector is the whole of it. */
:root:root{
/* ——— the accent ———
#FFFFFF, and it is not a choice made here. @luxfi/[email protected] ships two
product configs — brand.json (Lux Exchange) and financial.json (Lux Pay) —
and they declare the SAME theme, to the digit:
dark { accent1:#FFFFFF, surface1:#000000, surface2:#1A1A1A }
light { accent1:#000000, surface1:#FFFFFF, surface2:#F9F9F9 }
accent1 is the brand accent. In dark it is pure white; in light it is pure
black. Two independent product surfaces agreeing on all five values is as
close to an authoritative brand file as this estate has.
WHY A PURE WHITE ACCENT IS AN ACCENT AT ALL, on a surface whose text is
also white-ish: because nothing else on it is PURE. The ink ramp tops out
at #fafafa and the substrate explains why — #ffffff on #000000 is 21:1,
the maximum contrast pair that exists, and at paragraph size it halates.
So the ink stays a shade under, and the one thing painted at the actual
extreme reads as the accent. The polarity IS the brand.
That is also precisely where Lux parts from Hanzo. Hanzo's --brand is
#e4e4e7 — a near-white sitting BELOW its own ink, and carrying a faint
blue cast (~6% HSL saturation; B is three levels above R and G). Lux's
accent sits ABOVE the ink and has no cast at all. Brighter than everything
and perfectly neutral, spent rarely and small. */
--brand:var(--pure-white);
/* A hover on a fill that is already the brightest value available can only
step DOWN, so the only question is how far. The substrate answered it for
--primary, which has the same problem: a large drop reads as the control
going DISABLED under the cursor, which is the one thing a hover must never
look like, so the step is small and --bloom (elevation.css, a white glow at
.28) does the actual lighting. --neutral-200 is one rung, and it is a rung
rather than a number picked to feel right. */
--brand-hover:var(--neutral-200);
/* Ink on the accent. The substrate's inherited value is #09090b, which is
correct on contrast (20.5:1) and wrong on principle here: 9,9,11 is not
achromatic — blue runs two levels above red and green, about 10% HSL
saturation. On a brand whose whole claim is zero chroma, the one colour
sitting inside the brand's own button should not be the exception nobody
looks at. --pure-black is 21:1 and has no cast. */
--brand-foreground:var(--pure-black);
/* How the accent is actually spent: the eyebrow pill, and the hairline on a
featured tile. Both surfaces that carry this component draw it as a fill
inside a border of the same colour — lux.financial as `${BRAND_COLOR}20`
on `1px solid ${BRAND_COLOR}40`, the Exchange as `bg-gold/20
border-gold/40`.
Those two are NOT the same numbers, and 0.1.0's note that they were is
worth correcting because it is the kind of error that propagates. The
first pair is eight-digit hex: 0x20/255 = 12.5% and 0x40/255 = 25.1%. The
second is Tailwind opacity syntax: a literal 20% and 40%. lux.financial —
the live monochrome surface, and the one this package follows — is
therefore 12.5% on 25%.
Which lands between rungs, and this package ships a rule about that:
"Never invent an in-between step like 12% or 37%" (prompts/rules.md).
0.1.0 broke it, writing bespoke .12 and .25 alphas. So the pill snaps to
the ladder it was always meant to sit on — .10 and .22 — a hair quieter
than measured, which is the direction Lux is going anyway. */
--brand-bg:var(--white-10);
--brand-border:var(--white-22);
/* ——— the mark ———
The press-kit constants the logo is drawn from. --lux-white was already
#FFFFFF; --lux-black arrived as #0A0A0B, and that is not a Lux value at
all — it is Hanzo's press-kit black, carried in by the inherit's textual
hanzo- -> lux- rename, which turns a Hanzo fact into something that LOOKS
like a Lux fact. It is also not grey: blue sits one level above red and
green, hue 240deg, and the achromatic check below is what noticed.
components/core/LuxLogo.jsx paints the black variant of the mark with it.
@luxfi/[email protected] gives the answer in three places at once — primaryColor
#000000, dark surface1 #000000, light accent1 #000000. Lux's black is
black.
Restated identically in .light: this is a brand constant, invariant by
nature, and saying so in both blocks is cheaper than an exemption in the
gate that some later token would slip through. */
--lux-black:var(--pure-black);
/* ——— severity 1 of 4: the ground goes to true black ———
@luxfi/brand's surface1, in both product configs. It is also what this
package's own specimen cards have hardcoded as `background:#000` since the
first release, while the token underneath them said #0a0a0a — one fact in
two places, disagreeing. This settles it in favour of the brand file.
It is the highest-leverage single token in the layer. The ink gains
contrast without moving (#fafafa goes 18.97:1 → 20.12:1), the accent
reaches the full 21:1, and every card lifts further off the page for free:
--card was five levels above the old ground and is ten above this one.
Worth knowing before someone "corrects" this back: hanzo.ai's own screen is
also pure black — 77% of its pixels are #000000, because a `bg-black`
wrapper paints over the #0a0a0a its token declares. Hanzo's design system
and Hanzo's site disagree about the ground. Lux's does not. */
--background:var(--pure-black);
/* ——— severity 2 of 4: ONE lift, not two ———
Hanzo's ladder has four rungs: page #0a0a0a, card #0f0f0f, muted #171717,
control #262626. Lux has three. --card and --muted are the same surface
here, and that is the point: at this severity "a panel" and "a quieter
panel" are not two objects, they are one object, and the second rung was a
distinction the system was paying for without using. Text rank still
separates them — --muted-foreground is untouched and reads 7.85:1 here.
#0a0a0a is --neutral-950, and it is what lux.financial paints its raised
cards — 73 elements of it, the second-most-common pixel value on the page
after the black ground itself.
@luxfi/brand calls surface2 #1A1A1A; the ladder's nearest rung is #171717,
three levels off and visually identical, and staying ON the ladder is
worth more than three levels — so the control rung is left inherited
(--secondary / --accent, #262626) rather than nudged to a bespoke value.
Consequence, stated plainly so it is not later "fixed": --surface-1 and
--surface-2 resolve to the same value in Lux. The numeric ladder has three
distinct rungs, not four. If a surface needs a second lift, it is building
something Lux does not have. */
--card:var(--neutral-950);
--popover:var(--card);
--muted:var(--card);
/* ——— severity 3 of 4: a quieter hairline ———
.08, measured off lux.financial, against the substrate's .10 — and it is
the single highest-traffic token in the package: 44 of the var() references
in the shipped components are this one. Every card, table row, divider and
dialog edge steps back one rung.
The measurement is not marginal. Of 331 painted edges on lux.financial,
286 are exactly rgba(255,255,255,0.08) at 1px — one value doing nearly all
the work. hanzo.ai draws the same job with 48 edges of OPAQUE #262626
(about 15% white, and opaque, so it vanishes the moment it lands on a
lifted surface) plus 28 at .10. That is the difference in one number: Lux's
edges are softer, and they are all the same edge.
Only the DECORATIVE boundary moves. --border-control, --border-focus,
--border-selected and --ring are untouched, because they are not decoration
— they are how a keyboard user knows where they are, and --ring still owes
WCAG 2.4.11's 3:1 against every canvas. Lux quiets what merely separates.
It does not quiet what tells you where you are. */
--border:var(--white-08);
/* ——— severity 4 of 4: the fourth permitted hue is withdrawn ———
The substrate permits exactly four hue exceptions (DESIGN.md §2.4): error
red, online green, success green, and the macOS window-chrome dot trio.
Lux permits three.
This is not taste. --chrome-dot-yellow is rgb(234 179 8 / .6) — hue 45.6°,
which sits squarely inside the 20–60 band lux.network's end-to-end test
asserts is empty. A Lux surface rendering a window mock (guidelines/
color-semantic.card.html ships one) would paint gold-band pixels and fail
that test, having never gone near the gold. The other three exceptions are
SIGNALS and stay: "delete everything" and "you cannot click this" must not
be the same colour, and red at 0° and green at 142° are nowhere near the
fence. Window chrome is decoration, and decoration does not get a hue.
One value for all three, aliased onto --border-selected so it inverts with
the theme on its own. Three identical dots is what a monochrome window mock
looks like; the names are the substrate's and are kept so the vocabulary
matches. */
--chrome-dot-red:var(--border-selected);
--chrome-dot-yellow:var(--border-selected);
--chrome-dot-green:var(--border-selected);
}
/* Light theme.
Every token above whose value is a fixed literal is restated here, because
`:root` and `.light` have identical specificity and this layer is written
LAST — so a token declared only above would outrank the substrate's own
light value and silently keep its dark one. Tokens whose value is a bare
var() onto something that already inverts (--popover, --muted, the chrome
dots) need no restatement, and check-tokens.mjs knows the difference.
The accent inverts cleanly and completely, which is the reward for it being
monochrome: #000000 on #ffffff is the same 21:1 pair seen from the other
side. This is where 0.1.0's real trouble was — a gold tuned on black
measured 1.96:1 on white, far under the 3:1 floor for any non-text mark, and
could not simply be darkened because sRGB yellow below about L*45 turns
olive. 0.1.0 shipped a bronze, #7E5A16, as the honest ceiling and documented
it as such. Monochrome has no hue to darken, so that whole compromise
dissolves: there is nothing to tune, and nothing to explain. */
.light.light{
--brand:var(--pure-black);
/* The mirror of --neutral-200 in dark, one rung further out. The substrate's
rule: black-on-white at a given step reads FAINTER than white-on-black at
the same step, so light is pushed harder — .22/.30 becomes .32/.42 for its
focus and selection rungs. #262626 is the value the substrate itself uses
for --primary-hover in light, for this exact reason. */
--brand-hover:var(--neutral-800);
--brand-foreground:var(--pure-white);
--brand-bg:rgb(0 0 0 / .10);
--brand-border:rgb(0 0 0 / .22);
/* Invariant — the mark is the mark. Restated so the gate can see it is
deliberate rather than forgotten. */
--lux-black:var(--pure-black);
--background:var(--pure-white);
/* The complement of dark's lift, rounded to the ladder: #0a0a0a is ten levels
off black, #f5f5f5 is ten off white, and it is --neutral-100.
@luxfi/brand's surface2 here is #F9F9F9 — four levels apart, same call as
#1A1A1A vs #171717 above, same answer: stay on the ladder. */
--card:var(--neutral-100);
/* NOT quieted to .08. The dark hairline is a measured Lux value, taken off
lux.financial, which is a dark surface — there is no measured Lux light
hairline, and inventing one would be taste rather than evidence. So light
keeps the substrate's .10. This restatement exists only because declaring
--border above would otherwise strand the light theme on the dark value;
the number is upstream's, unchanged, and should be left alone until
somebody has a light Lux surface to measure. */
--border:rgb(0 0 0 / .10);
}