---
inclusion: always
---

# Design System: Gearious (Web)

A working example of a design-system steering file. Abridged for sharing.

The visual language as tokens, scales, and — most importantly — *when to use each value*. Tokens are expressed in the shadcn/ui + Tailwind idiom (semantic CSS variables consumed by utility classes).

## Dual-Theme from Day One

Light and dark are **co-equal themes** — dark is not a later add-on.

> **Components reference semantic tokens only — never a raw hex, never a theme-specific value.** Theming happens entirely at the token layer. If a component needs a `dark:` one-off override, the token set is wrong; fix the token, not the component.

A component is not "done" until it has been checked in both themes.

## Surface Character

The app is **flat, clean, legible, and border-driven** in both themes. This is where riders plan: forms, the planner, gear tables, the balance dashboard. Chrome stays calm and information-dense; visual drama is not the job here.

The brand stays recognizable across both themes through four constants: the **amber accent**, the **green/amber/red status system**, the **mono label motif**, and **tight radii**.

## Principles

- **Both themes are first-class.** Build, test, and verify contrast in light *and* dark.
- **Consistency over creativity.** Reach for a token; don't invent a value.
- **Border-first, flat by default.** Separate surfaces with 1px borders, not shadows. Reserve shadows for floating overlays only.
- **Mono for data.** Numbers — weights, percentages, capacities — are the product. Render them in the mono face with tabular figures so columns align.
- **Status is never color alone.** Every balance/capacity signal pairs color with an icon and text. Hard accessibility rule (see `audience.md`).
- **Restraint with the accent.** Amber marks the primary action and brand moments. If everything is amber, nothing is.

## Color System

Each semantic token maps to a **Tailwind palette step** — referenced as a `--color-*` variable, not a raw hex. The light theme rides a warm **taupe** neutral ramp; dark rides an **olive** ramp; the brand and most status colors come from **amber** and **orange**; success greens map to **lime**. Define light values in `:root` and dark in `.dark` under identical token names, and consume only through Tailwind classes.

### Neutrals

| Token | Light | Dark | Use for |
| --- | --- | --- | --- |
| `background` | `taupe-50` | `olive-950` | The app canvas. |
| `card` / `popover` | `white` | `olive-900` | Raised surfaces: cards, panels, dialogs, menus. |
| `foreground` | `taupe-900` | `olive-200` | Primary text and headings. |
| `muted` | `taupe-100` | `olive-800` | Subtle fills: table headers, disabled fields. |
| `muted-foreground` | `taupe-500` | `olive-400` | Secondary text, captions, placeholders. |
| `border` / `input` | `taupe-200` | `olive-800` | Default 1px separators and input outlines. |
| `border-strong` | `taupe-300` | `olive-700` | Emphasis/hover borders. |

### Brand Accent (amber)

The amber fill (`primary`) is the same step in both themes — a brand constant. Only the states around it shift.

| Token | Light | Dark | Use for |
| --- | --- | --- | --- |
| `primary` | `amber-600` | `amber-600` | The single primary action per view; brand marks. |
| `primary-foreground` | `white` | `white` | Text/icons on amber fills. |
| `primary-hover` | `amber-700` | `amber-500` | Hover/active of amber controls. |
| `accent` | `orange-100` | `orange-950` | Selected rows, hover backgrounds. |
| `ring` | `amber-600` | `amber-600` | Focus rings. |

### Status System

Semantic names beyond shadcn's defaults, so the same tokens drive the same colors everywhere. Status colors run **deeper on light, brighter on dark**.

| Token | Light | Dark | Use for |
| --- | --- | --- | --- |
| `success` | `lime-700` / fill `lime-600` | `lime-500` / fill `lime-600` | Balanced / within capacity. |
| `warning` | `amber-600` | `amber-500` | Caution. Shares brand amber — so it **must** always carry an icon + text and never be a clickable control. |
| `destructive` | `orange-700` | `orange-600` | Significant imbalance / over capacity; destructive buttons. |
| `destructive-strong` | `orange-800` | `orange-500` | The most severe capacity state. |

Exact numeric thresholds live in the engine and its tests, not here. This file only names the tokens those states consume.

## Typography

Theme-independent. Three faces, three jobs.

| Role | Font | Use for | Never for |
| --- | --- | --- | --- |
| Display | **Anton** | Large page titles, sparingly, uppercase. | Body, buttons, table headers, anything small or dense. |
| UI / Body | **DM Sans** | Everything in the interface. | — |
| Mono / Data | **JetBrains Mono** | Numeric readouts, eyebrow labels, badges. Use `tabular-nums`. | Long-form reading text. |

### Type Scale

| Step | Size | Use for |
| --- | --- | --- |
| Display L / S | `text-3xl md:text-4xl` / `text-2xl` | Page title; occasional section title. |
| Heading / Subhead | `text-lg` / `text-base` semibold | Card titles, panel headers, emphasized labels. |
| Body / Body S | `text-base` / `text-sm` | Default and dense reading text. |
| Label | `text-xs` uppercase, tracked out, mono | Eyebrows, field labels, section markers. |
| Data | contextual, mono, `tabular-nums` | Weight figures, balance %, capacity. Emphasize with weight, not color. |
| Caption | `text-xs` mono | Meta, timestamps, fine print. |

Stay on the scale. If a genuinely smaller caption is needed, define a named step rather than an arbitrary value.

## Spacing Scale

A 4px base grid (Tailwind's default). Stay on it.

| Token | px | Use for |
| --- | --- | --- |
| `1`–`2` | 4–8 | Tight internal gaps: icon-to-label, chip padding. |
| `3` | 12 | Control padding; gap between closely related items. |
| `4` | 16 | Default component padding; gap between fields. |
| `6` | 24 | Card padding; gap between item groups. |
| `8` | 32 | Page gutters (desktop); spacing within a section. |
| `12`–`16` | 48–64 | Space between major sections. |

Layout: content max-width `max-w-7xl`; narrow content like forms `max-w-2xl`. Page gutter `px-8` desktop, `px-5` mobile.

## Radii, Borders & Elevation

- **Radii are tight and technical.** Inputs, buttons, chips: 2–4px. Cards and dialogs: 4px. Avoid large radii except small pill badges and avatars.
- **Borders carry the layout** in both themes. 1px default; stronger for hover/emphasis.
- **Elevation is minimal.** No shadows on resting cards. Subtle shadows only for genuinely floating overlays.

## Motion

Fast and functional, never decorative. Hovers and toggles 150ms; popovers and dialogs 200–300ms; ease-out on enter. The balance bar should feel instant — don't gate the number behind a long animation. The theme switch itself should be instant (no cross-fade).

## Theming Architecture

- **Two token blocks, identical names.** `:root` maps each semantic token to its light step; `.dark` maps the same names to their dark steps.
- **Class strategy** (`attribute="class"`, system preference by default, persisted explicit choice).
- **No hardcoded `prefers-color-scheme` media queries in component CSS** — theme switching must flow through the class so an in-app toggle works.
- A theme toggle (light / dark / system) ships with the app; it is not deferred.
- Guard against a flash of the wrong theme on first paint.

## Brand Motifs in the App

Carry the brand in *lightly*: the mono uppercase section marker and coordinate-style meta text. Keep app chrome clean in both themes — no heavy textures behind working UI.

## Implementation Notes

- **Tokens reference Tailwind scale variables, not raw hex.** Consume via Tailwind classes only; no custom CSS files (per `tech.md`).
- Register extra semantic names (`success`, `warning`, `destructive-strong`, `border-strong`) so they're available as utilities.
- Use shadcn components as the base; theme them through these tokens, never per-component overrides.
- If the Tailwind version in use lacks `taupe` or `olive`, fall back to the nearest warm neutrals — or define the ramps as custom `--color-*` entries.
- **Verify contrast in both themes** before locking. Nudge a step if a pairing sits on the AA boundary.
