DESIGN.md · diff

git:20260827.53823ed to git:20260830.a1c9e1b

12 added, 0 removed. Audit A to A.

# LangAlpha Web Design System — "Quiet Workspace"
The visual language of the web app (`web/`): a quiet, charcoal-and-paper research
workspace where **content is the interface** and color is reserved for meaning.
Tokens are the single source of truth — every value below lives in
`web/src/styles/tokens.css`, and every `var(--color-*)` referenced anywhere must
be declared there (enforced by `web/src/styles/__tests__/tokenRefs.test.ts`).
## Identity
- **Temperature**: dark charcoal layers / light warm paper. No decorative
gradients anywhere (`--color-accent-gradient` is intentionally flat).
- **Accent**: burnished amber — `#E9954A` (dark) / `#D07D33` (light). It is an
**annotation color, not a brand fill** (see Accent discipline).
- **Radius**: soft, `--radius: 0.5rem` everywhere; no per-component overrides.
- **Depth**: borders first, shadows second (`--shadow-card` is the only card
shadow); never glows.
## Palette (both themes live in tokens.css)
| Role | Dark | Light |
|---|---|---|
| Page ground | `hsl(var(--background))` charcoal ~#191919 | white |
| Canvas under card grids | = page | `#F5F4F1` warm paper |
| Cards | `hsl(var(--card))` ~#202020 | white + `#E8E8E6` hairline |
| Elevated (menus, tooltips) | `#2A2B2E` | `#F7F7F6` |
| Primary text | `#E6E6E4` | `#1F1F1E` |
| Accent (annotation) | `#E9954A` | `#D07D33` |
| Primary button | `#ECECEA` bg / `#1A1B1D` text | `#1F1D1A` bg / `#FAF9F7` text |
| Profit / loss | `#3FB950` / `#F85149` | `#1A7F37` / `#CF222E` |
| Warning | `#D8B04C` | `#B45309` |
Background roles (`bg-page` → `bg-canvas` → `bg-card`/`bg-tool-card` →
`bg-elevated`, `bg-input`, `bg-popover`) are documented in the comment block
above the background group in `tokens.css` — pick by surface role, never by
matching a hex. Floating surfaces deliberately do not share one fill yet; don't
converge one of them in isolation.
## Typography
| Role | Face | How |
|---|---|---|
| Display / headings | **Sora** | `.title-font` (`--font-ui`, tracking −0.01em) |
| UI + body | **Geist** (+ Noto Sans SC for CJK) | `--font-ui` / `--font-content` |
| Data, numbers, kickers | **JetBrains Mono** | literal stack at call sites (`'JetBrains Mono', 'Menlo', monospace`) |
Tabular numerals (`font-variant-numeric: tabular-nums`) wherever digits align.
## Accent discipline (the rule that makes the system)
Amber is **annotation-only**: small icons, dots, chips, thin indicators, the
ascii liveness glyph. Never:
- a button or CTA fill, wash, ring, or glow;
- a **one-sided accent bar** on a card, panel, or blockquote — liveness and
state are carried by glyphs + text, and borders stay uniform neutral. The
only sanctioned edge markers are **nav selection** (sidebar rail edge,
bottom-bar underline, watchlist active row).
The rule governs the product's own surfaces. Illustrations that *depict* the
product are not one of them: the onboarding intro panels draw a miniature of the
app rather than being part of it, and they carry the accent structurally, in
washes, glows and fills. A picture of a branded thing reads as brand, not as
state. The alternative is a picture whose color has to be kept in step with the
brand by hand, which is how that panel spent a rebrand still showing the old one.
+ **The sign-in page is the one product surface that is exempt**, and the reason
+ is that the rule has nothing to bite on there. It carries no chrome, no
+ navigation and no data, so there is nothing for the accent to annotate, and the
+ neutral focus ring would paint in a color the page uses nowhere else. Amber is
+ its whole interaction vocabulary instead: the submit button has always taken the
+ fill on hover, and keyboard focus takes the same fill rather than inventing a
+ second language for the same "about to act" state. Focus is not left to hue
+ alone, since hue is what a color deficiency flattens: an underline by default, a
+ doubled edge on the boxed controls, an outline under forced colors. The exemption
+ is the `.login-page` surface itself, not the route it mounts on: the same build
+ serves the app from that route once someone is signed in.
+
Semantic color (profit/loss/warning/danger) is separate from the accent and
never substitutes for it.
## Status & liveness vocabulary
- **Live work** renders the shared ascii `Loader` glyph (amber) + text — never
a spinning lucide icon.
- **Completed** on dense surfaces (nav subagent rows) renders **no glyph**:
absence of the liveness glyph is the done state. Roomier surfaces (status
bar) may keep a celebratory check.
- Exceptional terminal states keep informative glyphs (error = alert,
cancelled = stop, spawned-idle = hollow circle).
- Status/icon mappings live in one table per vocabulary
(`web/src/pages/ChatAgent/components/taskStatusUi.tsx`); surfaces declare
overrides there instead of restating ladders.
## Motion
Entrance animations are shared utilities (`web/src/styles/animations.css`):
staggered fade-up with `cubic-bezier(0.16, 1, 0.3, 1)`, ≤ 500ms. No ambient or
looping decoration; motion signals state change only. Respect
`prefers-reduced-motion`.
## Empty states
Dot-grid texture is reserved for empty states only — never behind content.
## Working rules
1. New colors enter through `tokens.css` (both themes, plus the role comment)
— never a hex literal in a component. Canvas painters that can't read CSS
variables go through `web/src/lib/themeTokens.ts`.
2. Style both themes in the same change; the light theme is a first-class
surface, not an inversion.
3. Buttons are quiet: `--color-btn-primary-*` for primary, neutral borders for
secondary. `--color-text-on-accent` exists solely for text sitting on an
amber accent fill (rare, annotation-scale).
4. Liveness = glyph + text, per the vocabulary above.
5. When in doubt, remove decoration rather than add it.