> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sesameterminal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tokens

# 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.

| Token | Light before → after | Dark before → after |
| - | - | - |
| `--background` | `0.975 0.002 286` → `0.968 0.004 85` | `0.155 0.004 286` → `0.22 0.009 255` |
| `--foreground` | `0.2 0.006 286` → `0.31 0.012 265` | `0.965 0.002 286` → `0.87 0.006 255` |
| `--card` | `1 0 0` → `0.985 0.003 85` | `0.19 0.005 286` → `0.245 0.009 255` |
| `--muted-foreground` | `0.49 0.014 286` → `0.515 0.014 265` | `0.72 0.012 286` → `0.69 0.012 255` |
| `--primary` | `0.24 0.006 286` → `0.33 0.012 265` | `0.93 0.003 286` → `0.8 0.006 255` |
| `--bid` | `0.44 0.115 152` → `0.46 0.085 150` | `0.78 0.17 152` → `0.78 0.12 150` |
| `--ask` | `0.545 0.2 25` → `0.525 0.14 27` | `0.68 0.19 22` → `0.715 0.13 25` |
| `--destructive` | `0.53 0.2 27` → `0.525 0.14 27` | `0.66 0.2 23` → `0.715 0.13 25` |
| `--warning` | `0.83 0.15 80` → `0.84 0.11 82` | `0.83 0.15 80` → `0.8 0.11 80` |
| `--ring` | `0.53 0.19 262` → `0.55 0.13 258` | `0.72 0.14 252` → `0.74 0.1 250` |

The full before/after ratios are in the [contrast table](#contrast-wcag-2x).

### Surfaces and text (shadcn names)

| Token | Purpose | Light | Dark |
| - | - | - | - |
| `--background` | App background behind panels | `oklch(0.968 0.004 85)` #f6f4f1 | `oklch(0.22 0.009 255)` #181b1f |
| `--foreground` | Primary text, prices in neutral cells | `oklch(0.31 0.012 265)` #2d3037 | `oklch(0.87 0.006 255)` #d2d4d8 |
| `--card` | Panel surface: board, ladder, tables, cards | `oklch(0.985 0.003 85)` #fbfaf8 | `oklch(0.245 0.009 255)` #1e2125 |
| `--card-foreground` | Text on card | = foreground | = foreground |
| `--popover` | Menus, palette, tooltips, sheets | `oklch(0.985 0.003 85)` #fbfaf8 | `oklch(0.275 0.01 255)` #24282d |
| `--popover-foreground` | Text on popover | = foreground | = foreground |
| `--primary` | Neutral primary button (Log in, Save). Not green or red, so it never reads as a trade | `oklch(0.33 0.012 265)` #32353c | `oklch(0.8 0.006 255)` #bbbec2 |
| `--primary-foreground` | Text on primary | `oklch(0.975 0.003 85)` #f8f7f4 | `oklch(0.245 0.009 255)` #1e2125 |
| `--secondary` | Secondary button, inactive segment, chip background | `oklch(0.935 0.005 85)` #ebe9e6 | `oklch(0.305 0.01 255)` #2c2f34 |
| `--secondary-foreground` | Text on secondary | = foreground | = foreground |
| `--muted` | Disabled and stale cell background, skeletons, spread rows in the ladder | `oklch(0.95 0.004 85)` #f0eeeb | `oklch(0.29 0.01 255)` #282c30 |
| `--muted-foreground` | Secondary text, column headers, stale values | `oklch(0.515 0.014 265)` #636870 | `oklch(0.69 0.012 255)` #969ca3 |
| `--accent` | Hover background of rows and ghost buttons, selected palette row | `oklch(0.945 0.005 85)` #eeede9 | `oklch(0.3 0.01 255)` #2a2e33 |
| `--accent-foreground` | Text on accent | = foreground | = foreground |
| `--destructive` | Cancel all, cancel buttons, reject and error text, geoblocked pill. Same value as `--ask` | `oklch(0.525 0.14 27)` #ac433c | `oklch(0.715 0.13 25)` #e98079 |
| `--destructive-foreground` | Text on destructive fill | `oklch(0.985 0.003 85)` #fbfaf8 | `oklch(0.2 0.03 25)` #22100f |
| `--border` | Panel and row dividers (decorative) | `oklch(0.9 0.006 85)` #e0deda | `oklch(0.34 0.01 255)` #35383d |
| `--input` | Input and checkbox borders (meets 3:1) | `oklch(0.64 0.01 85)` #8f8c85 | `oklch(0.575 0.012 255)` #747980 |
| `--ring` | Focus ring and focused-panel ring. Blue, so it never reads as buy, sell or warning | `oklch(0.55 0.13 258)` #3e71bc | `oklch(0.74 0.1 250)` #79b0e8 |
| `--radius` | Base radius (= `--radius-lg`) | `0.5rem` | same |

### Trading

| Token | Purpose | Light | Dark |
| - | - | - | - |
| `--bid` | Bid, buy, profit, Yes. Text on card; solid fill for pressed and hovered buy buttons | `oklch(0.46 0.085 150)` #31653e | `oklch(0.78 0.12 150)` #7ccd8e |
| `--bid-foreground` | Text on a solid `--bid` fill | `oklch(0.985 0.003 85)` #fbfaf8 | `oklch(0.21 0.03 150)` #0e1c11 |
| `--bid-muted` | Translucent tint: buy button at rest, bid depth bars, your buy-order chip background | `oklch(0.6 0.1 150 / 0.12)` | `oklch(0.78 0.12 150 / 0.14)` |
| `--ask` | Ask, sell, loss, No. Same roles as `--bid` | `oklch(0.525 0.14 27)` #ac433c | `oklch(0.715 0.13 25)` #e98079 |
| `--ask-foreground` | Text on a solid `--ask` fill | `oklch(0.985 0.003 85)` #fbfaf8 | `oklch(0.2 0.03 25)` #22100f |
| `--ask-muted` | Translucent tint, same roles as `--bid-muted` | `oklch(0.6 0.14 27 / 0.1)` | `oklch(0.715 0.13 25 / 0.13)` |
| `--warning` | Armed, stale, delayed, pre-game wipe. **Fill only**: it fails as text on a light card | `oklch(0.84 0.11 82)` #eec474 | `oklch(0.8 0.11 80)` #e3b667 |
| `--warning-foreground` | Text and icons on a `--warning` fill | `oklch(0.32 0.05 65)` #442d15 | `oklch(0.25 0.04 65)` #2f1d0b |
| `--flash-up` | Price uptick flash (translucent, `--dur-flash`) | `oklch(0.7 0.12 150 / 0.24)` | `oklch(0.74 0.12 150 / 0.24)` |
| `--flash-down` | Price downtick flash | `oklch(0.66 0.14 27 / 0.16)` | `oklch(0.6 0.12 25 / 0.22)` |

**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](motion.md#price-flash)).

### Red and green without hue

Measured as a WCAG contrast ratio between the two colours (1.0 = same luminance):

| Pair | Light | Dark |
| - | - | - |
| `--bid` vs `--ask` | 1.18 (green darker) | 1.40 (green lighter) |

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](index.md#principles) 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](#palette)).

| Foreground on background | Min | Light | Light before | Dark | Dark before |
| - | - | - | - | - | - |
| foreground on background | 4.5 | 12.00 | 16.85 | 11.68 | 17.66 |
| foreground on card | 4.5 | 12.61 | 18.11 | 10.95 | 16.69 |
| popover-foreground on popover | 4.5 | 12.61 | 18.11 | 10.01 | 15.46 |
| foreground on muted | 4.5 | 11.38 | 16.12 | 9.52 | 14.67 |
| secondary-foreground on secondary | 4.5 | 10.87 | 15.41 | 9.04 | 13.62 |
| accent-foreground on accent | 4.5 | 11.21 | 15.88 | 9.20 | 12.98 |
| primary-foreground on primary | 4.5 | 11.37 | 15.77 | 8.69 | 15.03 |
| muted-foreground on background | 4.5 | 5.13 | 5.84 | 6.24 | 7.86 |
| muted-foreground on card | 4.5 | 5.39 | 6.28 | 5.85 | 7.43 |
| muted-foreground on muted (stale cell) | 4.5 | 4.86 | 5.59 | 5.09 | 6.53 |
| muted-foreground on popover | 4.5 | 5.39 | 6.28 | 5.35 | 6.88 |
| muted-foreground on accent (hovered row) | 4.5 | 4.79 | 5.51 | 4.92 | 5.78 |
| muted-foreground on secondary (inactive segment) | 4.5 | 4.65 | 5.35 | 4.83 | 6.07 |
| destructive-foreground on destructive | 4.5 | 5.54 | 5.61 | 6.82 | 5.62 |
| destructive (error text) on card | 4.5 | 5.54 | 5.85 | 6.07 | 5.40 |
| bid on card | 4.5 | 6.56 | 7.35 | 8.49 | 9.86 |
| bid on background | 4.5 | 6.24 | 6.84 | 9.06 | 10.43 |
| bid on popover | 4.5 | 6.56 | 7.35 | 7.76 | 9.13 |
| bid on accent (hovered row) | 4.5 | 5.83 | 6.45 | 7.13 | 7.67 |
| bid on bid-muted over card (buy button at rest) | 4.5 | 5.76 | 6.55 | 6.34 | 7.76 |
| ask on card | 4.5 | 5.54 | 5.49 | 6.07 | 5.87 |
| ask on background | 4.5 | 5.27 | 5.11 | 6.48 | 6.21 |
| ask on popover | 4.5 | 5.54 | 5.49 | 5.55 | 5.44 |
| ask on accent (hovered row) | 4.5 | 4.92 | 4.82 | 5.10 | 4.57 |
| ask on ask-muted over card (sell button at rest) | 4.5 | 4.91 | 4.92 | 4.95 | 5.10 |
| bid on own buy-order chip (ladder) | 4.5 | 5.28 | 5.87 | 6.59 | 7.91 |
| ask on own sell-order chip (ladder) | 4.5 | 4.54 | 4.51 | 4.94 | 5.01 |
| bid-foreground on bid (hovered/pressed buy) | 4.5 | 6.56 | 7.15 | 9.20 | 9.59 |
| ask-foreground on ask (hovered/pressed sell) | 4.5 | 5.54 | 5.34 | 6.82 | 6.11 |
| warning-foreground on warning | 4.5 | 7.79 | 8.64 | 8.54 | 9.68 |
| foreground on flash-up over card | 4.5 | 10.35 | 14.59 | 6.77 | 10.80 |
| foreground on flash-down over card | 4.5 | 10.66 | 15.29 | 8.49 | 13.44 |
| bid on flash-up over card | 4.5 | 5.38 | 5.92 | 5.24 | 6.38 |
| ask on flash-down over card | 4.5 | 4.68 | 4.64 | 4.71 | 4.73 |
| ring on background | 3 | 4.46 | 5.09 | 7.55 | 7.90 |
| ring on card | 3 | 4.69 | 5.47 | 7.07 | 7.46 |
| ring on popover | 3 | 4.69 | 5.47 | 6.46 | 6.92 |
| input border on card | 3 | 3.22 | 3.37 | 3.71 | 3.35 |
| input border on background | 3 | 3.07 | 3.13 | 3.96 | 3.54 |

For information (no minimum):

| Pair | Light | Light before | Dark | Dark before | Note |
| - | - | - | - | - | - |
| warning fill vs card | 1.58 | 1.71 | 8.60 | 10.78 | A light-theme warning chip needs its `--warning-foreground` text; the fill alone is not a boundary |
| border vs card | 1.29 | 1.33 | 1.38 | 1.35 | Decorative divider; never the only way to identify a control |
| flash-up over card vs card | 1.22 | 1.24 | 1.62 | 1.55 | Flash visibility |
| flash-down over card vs card | 1.18 | 1.18 | 1.29 | 1.24 | Flash visibility. Red is kept low so ask text stays ≥ 4.5 during the flash |

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](motion.md).

| Token | Value | Use |
| - | - | - |
| `--dur-instant` | `0ms` | Price and size changes, order position on the ladder |
| `--dur-fast` | `120ms` | Hover, press, focus rings, toggles, tooltips, order chip colour |
| `--dur-base` | `180ms` | Palette, dropdowns, toasts, tab switches, route cross-fade |
| `--dur-slow` | `240ms` | Bottom sheets, side panels, portfolio row expand. The longest transition |
| `--dur-flash` | `400ms` | One-shot highlights: price flash (PLAN §4.1 table), score change, Safe nudge. The one value above `--dur-slow`; see motion.md |
| `--ease-out` | `cubic-bezier(0.2, 0, 0, 1)` | Entering |
| `--ease-in` | `cubic-bezier(0.4, 0, 1, 1)` | Leaving. Exit duration = `calc(var(--dur-*) * 0.75)` |

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

| Token | Value | Use |
| - | - | - |
| `--row-h` | `36px` | Table rows, board outcome lines, list rows, desktop buttons in rows |
| `--touch-min` | `44px` | Minimum touch target on coarse pointers; mobile rows and buttons |
| `--ladder-row-h` | `28px`; `44px` on `(pointer: coarse)` | Ladder price levels (the one denser surface; see price-ladder.md) |
| `--topbar-h` | `48px` | Desktop top bar |
| `--rail-w` | `64px` | Desktop left rail |
| `--mobile-header-h` | `52px` | Mobile header, excluding the top safe-area inset |
| `--tabbar-h` | `56px` | Mobile tab bar, excluding the bottom safe-area inset |

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

| Token | Value | Use |
| - | - | - |
| `--radius-sm` | 4px | Order chips, status pills, badges, depth bars |
| `--radius-md` | 6px | Buttons, inputs, price buttons |
| `--radius-lg` | 8px (`--radius`) | Cards, panels, popovers, palette |
| `--radius-xl` | 12px | Bottom sheets (top corners), dialogs |
| `--radius-full` | 9999px | Dots, avatar, countdown ring |

The steps use the same formula shadcn uses, so they agree with shadcn's generated theme.

## Elevation and stacking

| Token | Use |
| - | - |
| `--shadow-sm` | Sticky headers while scrolled |
| `--shadow-md` | Dropdowns, tooltips, cursor tag, toasts |
| `--shadow-lg` | Palette, dialogs, bottom sheets |

In dark mode, separation comes from surface steps (background → card → popover) and
borders; shadows only lift overlays.

| Token | Value | Layer |
| - | - | - |
| `--z-base` | 0 | Content |
| `--z-sticky` | 10 | Sticky table and league headers |
| `--z-dock` | 20 | Bottom dock, ladder dock |
| `--z-rail` | 30 | Left rail, mobile tab bar |
| `--z-topbar` | 40 | Top bar, mobile header. Mobile sheets and their overlay start below the header rather than above it, so Armed and Cancel all stay reachable |
| `--z-overlay` | 50 | Dimmed backdrop under sheets and dialogs |
| `--z-sheet` | 60 | Bottom sheets, dialogs |
| `--z-popover` | 70 | Menus, selects, cursor tag |
| `--z-palette` | 80 | Command palette |
| `--z-toast` | 90 | Toasts |
| `--z-tooltip` | 100 | Tooltips |

## 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.

| Role | Size / line | Weight | Tailwind | Used for |
| - | - | - | - | - |
| display | 30 / 36 | 700 | `text-3xl font-bold tracking-tight` | Login title, portfolio total on mobile |
| title | 18 / 28 | 700 | `text-lg font-bold` | Page title, sheet title, market name in market view |
| heading | 14 / 20 | 700 | `text-sm font-bold` | Panel headers, league headers, table group headers |
| body | 14 / 20 | 500 | `text-sm font-medium` | Default text, team names, table text |
| body-sm | 13 / 18 | 500 | `text-[13px] leading-[18px] font-medium` | Secondary lines, event subtitle, toast body |
| label | 12 / 16 | 600 | `text-xs font-semibold` | Button text in chips and pills, form labels |
| caption | 11 / 16 | 600 | `text-[11px] leading-4 font-semibold` | Column headers, section labels, mobile price-button labels, in sentence case with no letter spacing (it replaces the old upper-case overline). The smallest text in the app |
| numeric | 14 / 20 | 600 | `text-sm font-semibold tabular-nums` | Table numbers: sizes, avg, value, P\&L |
| numeric-price | 15 / 20 | 700 | `text-[15px] leading-5 font-bold tabular-nums` | Price buttons on board, watchlist, portfolio actions |
| numeric-lg | 20 / 28 | 700 | `text-xl font-bold tabular-nums` | Top-bar cash, summary totals, P\&L on mobile position cards |
| numeric-dense | 13 / 16 | 600 (best bid and best ask rows 800) | `text-[13px] leading-4 font-semibold tabular-nums` | Ladder cells at 28 px rows |
| numeric-dense (coarse) | 15 / 20 | 600 / 800 | `text-[15px] leading-5` | Ladder cells at 44 px rows |

Inputs are 16 px on mobile (`text-base`), so iOS does not zoom on focus.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.