Skip to main content

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 are oklch(). 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 --accent and 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. --destructive shares the --ask value in both themes, so the app has one red.
  • Filled buttons are softer. The dark --primary fill 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 by node 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:
  • --warning is a fill. Text on a warning fill uses --warning-foreground. Never put --warning text on a card.
  • Stale values use --muted-foreground on --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 ask on its order chip in light (4.54), muted-foreground on --secondary in light (4.65) and ask on the downtick flash (4.68 / 4.71). They leave little room for a change to --ask, --secondary or --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.