Files
zeekayandHanzo Dev 29b214ae90 feat: no gold in Lux — monochrome, and four numbers more severe than hanzo.ai
The CTO reversed the accent: "no gold in lux, only monochrome white and
black, more severe and minimal than hanzo.ai". 0.1.0's archaeology was
real but historical — every live Lux surface had already flattened that
slot to white, and lux.network's repo carries an e2e asserting the page
paints zero pixels in hue band 20-60. 0.1.0 climbed that fence.

--lux-gold is deleted rather than parked. An unused brand-colour token is
exactly what gets re-wired by accident. The light-theme bronze (#7E5A16)
dissolves with it: monochrome has no hue to darken.

Severity is not an adjective here, it is four measured values, each from
@luxfi/[email protected] or from a headless render of the live surfaces:

  ground    #000000  (Hanzo #0a0a0a)   brand surface1, both product configs
  accent    #ffffff  (Hanzo #e4e4e7)   brand accent1 — above the ink, not below
  lift      one      (Hanzo two)       73 cards measured at #0a0a0a
  hairline  .08      (Hanzo .10)       286 of 331 painted edges

Nothing is invented; every value is a rung of the inherited ladder. Only
the decorative boundary was quieted — control, focus, selected and ring
are untouched, because they tell a keyboard user where they are.

Lux also withdraws the fourth permitted hue. --chrome-dot-yellow is hue
45.6, inside the fenced band, so a Lux window mock would have failed
lux.network's e2e having never gone near the gold.

Four things the gate could not see before, each found by adding the check:

  --lux-black was #0A0A0B — Hanzo's press-kit black wearing a Lux name
  after the hanzo- -> lux- rename, and not even grey. It paints the mark.
  assets/brand/hero-visualization.png was a full-spectrum rainbow, 39%
  saturated, in a card captioned "no warm gradients, no illustration
  style". The specimen cards captioned --border as #1f1f1f and --ring as
  #333 beside chips correctly painting the current tokens — fossils of
  the fork. And prompts/system.md still quoted the Hanzo brand hex.

Gate: 26 checks. New — every token resolves achromatic in both themes
except three named signals, each proven outside band 20-60; unreadable
colour syntax fails rather than passes; specimen captions match the
sheet; brand imagery is achromatic (PNG decoded in ~40 lines on
node:zlib, no dependency). The .light rule is now value-shaped — exempt
only if the value is a bare var() onto something that inverts — replacing
a name-shaped --lux-* exemption that existed for a token now deleted.

Verified: build green; the packed tarball rendered headless in both
themes paints 0 saturated pixels of 8,780,800; eight mutations each fail
the build and restore byte-identically.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 16:45:11 -07:00

300 lines
17 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 — 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);
}