Tokens
Source:frontend/src/styles/tokens.css. Light values sit on :root, dark values on
.dark (the default theme; FE-Platform puts .dark on <html> unless the user picks
light). Motion, density, radius and z tokens are defined once on :root and are the same
in both themes.
FE-Platform maps these into Tailwind (bg-background, text-bid, bg-bid-muted,
rounded-lg, and so on). Components use only these tokens: no raw colours, no hard-coded
durations.
FE-Platform should also set color-scheme: light on :root and color-scheme: dark on
.dark in its global CSS, so scrollbars and native controls follow the theme.
tokens.css holds custom properties only.
Colour
All colours areoklch(). Light neutrals are a warm off-white paper (hue 85, chroma
0.003–0.006) with a soft ink for text (hue 265, chroma 0.012). Dark neutrals are a cool
charcoal (hue 255, chroma 0.006–0.01). Neither theme uses pure white or near-black. The hex
column is the sRGB rendering, for reference only; never use hex in code.
Palette
The app is watched for long live sessions, so the palette keeps a moderate luminance gap and low saturation instead of maximum contrast (changed in phase 6; the first palette gave body text about 17:1 on near-black and on pure white).- Body text is 11–12.6:1 on the page and on panels in both themes: well above the 4.5:1 AA minimum and far from 17:1. Dark: background L 0.22, text L 0.87 (off-white). Light: page L 0.968, panels L 0.985, text L 0.31. The light text sits at L 0.31 rather than 0.27–0.30, which would push body text above 12:1 on the paper background.
- Muted text is about 5:1 on the page (light 5.13, dark 6.24). In dark it is lighter than
5:1 on the page so it keeps 4.5:1 on the hovered-row
--accentand on--secondary. - Trading colours carry about 30% less chroma: a sage green (hue 150) and a brick red
(hue 25–27), each still clearly green or red.
--destructiveshares the--askvalue in both themes, so the app has one red. - Filled buttons are softer. The dark
--primaryfill drops from L 0.93 to L 0.8, and buy/sell fills from L 0.78/0.68 at chroma 0.17/0.19 to L 0.78/0.715 at chroma 0.12/0.13. - Tints and flashes follow the new hues at the same or slightly lower strength, so they stay subtle and never pull ask or bid text below 4.5:1.
The full before/after ratios are in the contrast table.
Surfaces and text (shadcn names)
Trading
The colour rule. The colour of a price or button is the side of the order it sends
or shows: a green button sends a buy (a bid), a red one a sell (an ask). So the board’s
Buy button is green and shows the best ask price, because that is the price you pay.
The book’s own bid sizes are green and ask sizes red. P&L is green when positive and red
when negative. In Yes/No markets the outcome label is coloured (
Yes in --bid,
No in --ask); the price buttons on each outcome line still follow the side rule, so
Buy on the No line is green.
Tinted price buttons and the flash. A flash never stacks on a tint. When a tinted
price button flashes, its tint layer goes to opacity 0 at the same moment the flash layer
goes to 1, and both return over --dur-flash. The contrast table checks the text on the
flash over --card for this reason (see motion.md).
Red and green without hue
Measured as a WCAG contrast ratio between the two colours (1.0 = same luminance):
Both themes separate them less by luminance than the first palette did (1.34 and 1.68),
because both colours now sit closer to the text luminance. Light mode is the tighter one,
because both colours must stay above 4.5:1 on the paper background. That is why rule 6 in
the README requires a non-colour cue on every coloured value:
column position, a Buy/Sell label, or a
+/− sign. Under reduced motion, price direction
also gets a lucide [arrow-up] / [arrow-down] icon.
Contrast (WCAG 2.x)
Produced bynode docs/design/contrast.mjs, which reads tokens.css, converts OKLCH to
sRGB, composites translucent tokens over --card the way browsers do, and mixes the
ladder’s order-chip background (color-mix(in oklab, <side> 14%, var(--card))). Rerun it
after any colour change; it exits non-zero on a failure. Body text needs 4.5:1; focus rings
and input borders need 3:1. The “before” columns are the first palette, replaced in phase 6
(see Palette).
For information (no minimum):
Rules that follow from the table:
--warningis a fill. Text on a warning fill uses--warning-foreground. Never put--warningtext on a card.- Stale values use
--muted-foregroundon--muted(4.86 / 5.09), so they stay legible while looking disabled. - Disabled controls (not stale values) may drop to 50% opacity; WCAG exempts inactive controls.
- The tightest pairs are
askon its order chip in light (4.54),muted-foregroundon--secondaryin light (4.65) andaskon the downtick flash (4.68 / 4.71). They leave little room for a change to--ask,--secondaryor--flash-down.
Motion
Same in both themes. The full per-component spec is in motion.md.
Under
prefers-reduced-motion: reduce, tokens.css sets --dur-fast, --dur-base,
--dur-slow and --dur-flash to 0ms, so every CSS transition built on the tokens
turns instant without per-component code. vaul, sonner and View Transitions need their
own switch (motion.md).
Density and layout
Spacing uses the Tailwind 4 px scale. Panel padding: 12 px desktop, 16 px mobile. Gaps
between panels: 1 px
--border lines, not gutters, so more data fits.
Radius
The steps use the same formula shadcn uses, so they agree with shadcn’s generated theme.
Elevation and stacking
In dark mode, separation comes from surface steps (background → card → popover) and
borders; shadows only lift overlays.
Type scale
Font:--font-sans from fonts.css (Nunito Sans variable, 200–1000). Every number also
gets tabular-nums (Nunito Sans digits are already equal-width; the class keeps that
true if the font changes). Weights below 500 are not used: they look thin on dark
surfaces.
Inputs are 16 px on mobile (
text-base), so iOS does not zoom on focus.