# Prospr design system — application implementation guide

Third source of truth, alongside [`BRAND.md`](BRAND.md) (logo, canonical hex) and
[`VISUAL-DIRECTION.md`](VISUAL-DIRECTION.md) (visual philosophy, hierarchy law).

**What this document is.** This is the current implementation guide for Prospr's authenticated
application language. The landing page (`src/components/public/landing/`) established the
material system; `/dashboard` now applies it through shared tokens, primitives, page wrappers,
and composition. The certified dashboard scope is 50 active user-facing page components plus
11 redirect/re-export entries. All nine active `/settings` pages and the 18-page sidebar
Utilities scope are also certified. `/admin-dashboard` remains a target consumer and must be
certified separately before its migration is called complete.

**What this document is not.** It does not restate marketing narrative rules. Scene
choreography, chapter environments, and campaign scenes stay on the landing page. The app
inherits the landing's *material* (surfaces, ink, shape, motion, data treatment), not its
*theatre*.

**Reference implementation.** Brand primitives originate in
`src/components/public/landing/styles/foundation.css`. The live application source of truth is
`src/app/global.css`, with React composition in `src/components/dashboard/ui/` and
`src/components/settings/SettingsComposition.js`, plus chart roles in
`src/components/chart/theme.js`. Examples below explain the contract; when a
copied value drifts, the live token block wins and this guide must be corrected in the same pass.

---

## Table of contents

1. [The seven laws](#1-the-seven-laws)
2. [Token architecture](#2-token-architecture)
3. [Surfaces and elevation](#3-surfaces-and-elevation)
4. [Color](#4-color)
5. [Typography](#5-typography)
6. [Layout and spacing](#6-layout-and-spacing)
7. [Shape, border, shadow](#7-shape-border-shadow)
8. [Motion](#8-motion)
9. [Component specifications](#9-component-specifications)
10. [Data display](#10-data-display)
11. [Charts](#11-charts)
12. [Accessibility gates](#12-accessibility-gates)
13. [Implementation status and migration record](#13-implementation-status-and-migration-record)
14. [Review checklist](#14-review-checklist)
15. [Decisions and follow-up](#15-decisions-and-follow-up)

---

## 1. The seven laws

These are the non-negotiables. Every specification later in the document is downstream of one
of them. If a rule below contradicts a local design impulse, the rule wins.

### Law 1 — Depth comes from tone, never from glow

The landing builds hierarchy with a nine-step neutral surface ramp (`#050506` → `#1a1a1f`),
1px inset highlights, and large soft shadows. It contains zero neon borders, zero pulsing
halos, and zero floating orbs.

The dashboard implements this by painting one static environment wash on `.app-shell`.
`DashboardAtmosphere` is retained as a no-paint compatibility wrapper, `AnimatedGlow` returns
no visual output, and `GlowButton` maps to `.app-btn*` without chromatic shadow layers.

> **Test:** delete every `box-shadow` involving a chromatic color from a screen. If the
> hierarchy collapses, the hierarchy was fake.

### Law 2 — Orange is an interaction, not a decoration

`#ff931c` marks the thing you can act on: the single primary action in a view, the active
navigation indicator, the focus ring. It is not a headline color, not a card border, not an
icon tint, not a section divider.

**Budget: one orange fill per viewport.** Active-state rails and the focus ring do not count
against the budget — they are system feedback, not emphasis.

### Law 3 — Blue is structure, and only structure

`#2a65d8` (Electric Blue) is the logo color. In UI it appears as grid lines, chart axes,
methodology chrome, and train-band tints — always at low opacity, never as a fill or a button.

Electric Blue at full strength measures **3.55:1** against the card surface `#121214`. That
passes WCAG AA for non-text UI (3:1) and fails for body text (4.5:1). For any *legible* blue —
links, informational badges, secondary chart series — use `--data-info` `#7fa3ff` (**7.66:1**),
which the landing already established in `campaign/shared.css` as `--campaign-blue`.

### Law 4 — Financial data is tabular and role-bound

Every number a user could compare, sum, or scan in a column uses `--font-mono` with
`font-variant-numeric: tabular-nums`. `--font-mono` resolves to Plus Jakarta Sans on every
surface, including public product theatre. Do not load IBM Plex Mono. Balances, P/L,
percentages, quantities, prices, token amounts, timestamps, hashes, and IDs all retain
tabular alignment.

Prose numbers ("3 strategies active") stay in Plus Jakarta Sans. The distinction is
*comparability*, not *numeracy*.

### Law 5 — Shape encodes commitment level

The landing has exactly one button shape because a marketing page has exactly one job. An
application has hundreds of actions at different stakes, so shape carries meaning:

| Radius | Role | Example |
| --- | --- | --- |
| `999px` pill, uppercase | **Commitment.** Irreversible, financial, or flow-completing | Deposit, Withdraw, Confirm order, Start strategy |
| `0.5rem` rect, sentence case | **Standard.** Reversible, in-page | Filter, Export, Edit, Save draft |
| No fill, `0.5rem` | **Quiet.** Tertiary or dismissive | Cancel, Reset, Show more |
| Text + `→` | **Navigation.** Moves you elsewhere | "View all fills", "Open strategy" |

**One pill per view.** This is Law 2 expressed as geometry — the pill *is* the orange fill.

### Law 6 — One focal object per screen

Inherited verbatim from `VISUAL-DIRECTION.md` §"One focal moment per section". A dashboard page
gets one dominant object: the equity curve, or the order book, or the allocation table — never
three elevated panels competing at equal weight. Everything else is demoted with smaller type,
muted ink, and fewer borders.

### Law 7 — Motion explains state, then stops

Allowed: state transitions under 200ms, entrance reveals on first paint, chart draw-in once.
Forbidden: infinite ambient loops, hover lift on cards, glow fades, parallax, anything without
a `prefers-reduced-motion` fallback.

---

## 2. Token architecture

### 2.1 Strategy: semantic roles with compatibility aliases

The dashboard references both current semantic roles (`--app-*`, `--ink-*`, `--data-*`) and
legacy aliases (`--bg-*`, `--text-*`, `--success`, `--error`). The aliases deliberately resolve
through the current roles so older call sites remain stable while new work uses the clearer
application vocabulary.

Do not remove an alias until its consumers have been migrated and verified. Do not add a new
literal color to avoid choosing the correct semantic role.

New capability that has no legacy name gets an `--app-*` prefix.

```
foundation.css  ──(values copied verbatim)──►  global.css :root
   (landing, unchanged)                            │
                                                   ├─► --app-* roles      (new, preferred)
                                                   └─► --bg-*, --text-*   (legacy aliases)
```

Landing and application token values are changed together when they express the same brand
role. Application-only capabilities remain in `global.css` under `--app-*` names.

### 2.2 Core token block

The excerpt below records the core roles introduced by the migration. It is not a paste-over
recipe: `src/app/global.css` contains the complete current block, including stage, chrome,
density, and chart-support roles added during composition parity.

```css
/* ============================================================
   PROSPR APP FOUNDATION
   Mirrors src/components/public/landing/styles/foundation.css
   Do not edit values here without editing the landing in step.
   ============================================================ */
:root {
  /* ---- 1 · Type families ------------------------------------ */
  --font-brand:   var(--font-plus-jakarta);
  --font-display: var(--font-syne), var(--font-plus-jakarta), system-ui, sans-serif;
  --font-mono:    var(--font-brand);
  --font-ui:      var(--font-brand);   /* CHANGED — was var(--font-inter) */
  --tracking-meta: 0.08em;             /* verbatim — mono eyebrow / table header */

  /* ---- 2 · Midnight stack (verbatim) ------------------------- */
  --midnight-void:     #000000;
  --midnight-ink:      #050506;
  --midnight-opening:  #0a0a0c;
  --midnight-graphite: #0c0c0e;
  --midnight-slate:    #101014;
  --midnight-raised:   #111114;
  --midnight-chrome:   #121214;
  --midnight-surface:  #141417;
  --midnight-plane:    #161618;
  --midnight-panel:    #1a1a1f;

  /* ---- 3 · App surface roles  [APP] -------------------------- */
  /* Product code should reference these, not the midnight-* names. */
  --app-ground: var(--midnight-opening);   /* #0a0a0c  page behind everything */
  --app-rail:   var(--midnight-graphite);  /* #0c0c0e  sidebar, topbar        */
  --app-card:   var(--midnight-chrome);    /* #121214  cards, panels          */
  --app-well:   var(--midnight-plane);     /* #161618  nested / inset regions */
  --app-raised: var(--midnight-panel);     /* #1a1a1f  modals, popovers       */
  --app-field-bg:       rgba(255, 255, 255, 0.03);
  --app-field-bg-hover: rgba(255, 255, 255, 0.05);
  --app-row-hover:      rgba(255, 255, 255, 0.025);
  --app-scrim:          rgba(5, 5, 6, 0.72);

  /* Legacy aliases — existing dashboard markup resolves through these */
  --bg-primary:   var(--app-ground);   /* was #0E0F14 */
  --bg-secondary: var(--app-card);     /* was #16181F */
  --bg-tertiary:  var(--app-well);     /* was #1C1F28 */
  --bg-sidebar:   var(--app-rail);
  --bg-hover:     var(--app-field-bg-hover);
  --panel-2:      var(--app-raised);

  /* ---- 4 · Lines  [APP scale, landing values] ---------------- */
  --app-line-faint:  rgba(166, 166, 166, 0.10);
  --app-line:        rgba(166, 166, 166, 0.12);
  --app-line-strong: rgba(166, 166, 166, 0.16);
  --app-line-loud:   rgba(166, 166, 166, 0.20);

  --border-primary:   var(--app-line);         /* was #2A2E38 */
  --border-secondary: var(--app-line-strong);  /* was #20232B — see note below */

  /* ---- 5 · Ink ----------------------------------------------- */
  --ink-fg:     #f2f2f2;
  --ink-muted:  #cacaca;
  --ink-subtle: #a6a6a6;
  --ink-faint:  rgba(166, 166, 166, 0.62);  /* [APP] derived from --ink-subtle */

  --text-primary:   var(--ink-fg);      /* was #ECEEF3 */
  --text-secondary: var(--ink-muted);   /* was #9BA1B0 */
  --text-tertiary:  var(--ink-subtle);  /* was #9BA1B0 */
  --text-muted:     var(--ink-faint);   /* was #6B7180 */

  /* ---- 6 · Accents (verbatim) -------------------------------- */
  --orange:       #ff931c;
  --orange-hover: #e07e0a;
  --orange-soft:  #ffb14d;
  --orange-edge:  rgba(255, 147, 28, 0.42);
  --orange-glow:  rgba(255, 147, 28, 0.08);
  --on-orange:    #0c0c0e;

  --blue:         #2a65d8;   /* structure only — see Law 3 */
  --blue-edge:    rgba(42, 101, 216, 0.12);
  --blue-reflect: rgba(42, 101, 216, 0.06);
  --logo-blue:    var(--blue);

  --brand-primary:    var(--orange);
  --brand-hover:      var(--orange-hover);
  --brand-light:      var(--orange-soft);
  --brand-on-primary: var(--on-orange);   /* CHANGED — was #FFFFFF, see §15 */

  /* ---- 7 · Semantic data colors  [APP] ----------------------- */
  --data-positive: #4ade80;   /* = landing --green-positive */
  --data-negative: #f87171;   /* [APP] tonal counterpart, 6.75:1 on --app-card */
  --data-warning:  var(--orange-soft);
  --data-info:     #7fa3ff;   /* = campaign --campaign-blue */

  --success: var(--data-positive);   /* was #2FCB8A */
  --error:   var(--data-negative);   /* was #F0556A */
  --warning: var(--data-warning);    /* was #E07E0A */
  --info:    var(--data-info);       /* was #2A65D8 — failed AA on text */

  --tint-positive: rgba(74, 222, 128, 0.12);
  --tint-negative: rgba(248, 113, 113, 0.12);
  --tint-warning:  rgba(255, 177, 77, 0.12);
  --tint-info:     rgba(127, 163, 255, 0.12);
  --tint-action:   rgba(255, 147, 28, 0.10);

  /* ---- 8 · Shape  [APP scale, landing values] ---------------- */
  --app-r-sm:   0.375rem;   /*  6px  inner wells, skeleton lines        */
  --app-r-md:   0.5rem;     /*  8px  buttons, fields  (= control)       */
  --app-r-lg:   0.75rem;    /* 12px  cards, panels    (= plane)         */
  --app-r-xl:   1.125rem;   /* 18px  modals, drawers  (= stage)         */
  --app-r-stage: 1.25rem;   /* 20px  focal product stage                */
  --app-r-pill: 999px;      /*       commit CTAs, badges, avatars       */

  /* ---- 9 · Elevation (verbatim depth-*) ---------------------- */
  --app-e-flat:   inset 0 1px 0 rgba(255, 255, 255, 0.02);
  --app-e-card:   0 8px 24px rgba(0, 0, 0, 0.32);
  --app-e-raised: inset 0 1px 0 rgba(255, 255, 255, 0.03),
                  0 16px 36px rgba(0, 0, 0, 0.42);
  --app-e-modal:  inset 0 1px 0 rgba(255, 255, 255, 0.04),
                  0 20px 56px rgba(0, 0, 0, 0.48);
  --app-e-stage:  0 60px 140px -70px rgba(0, 0, 0, 0.95);

  --app-card-face:  linear-gradient(180deg, #141417 0%, #121214 100%);
  --app-stage-face: linear-gradient(180deg, #141418 0%, #0d0d0f 100%);
  --app-stage-line: rgba(242, 242, 242, 0.12);

  /* ---- 10 · App shell geometry  [APP] ------------------------ */
  --app-sidebar-w:           14rem;
  --app-sidebar-w-collapsed: 3.25rem;
  --app-topbar-h:            3.75rem;
  --app-tabs-h:              2.75rem;
  --app-page-max:            90rem;
  --app-page-gutter:         clamp(0.75rem, 1.75vw, 1.25rem);
  --app-page-y:              clamp(0.75rem, 1.25vw, 1.125rem);
  --app-stack:               1rem;      /* gap between sibling cards   */
  --app-stack-lg:            1.5rem;    /* gap between page sections   */
  --app-card-pad:            1.25rem;
  --app-card-pad-sm:         1rem;

  /* ---- 11 · Motion (verbatim) -------------------------------- */
  --ease-enter:  cubic-bezier(0.22, 1, 0.36, 1);
  --dur-instant: 0ms;
  --dur-fast:    180ms;
  --dur-mid:     280ms;
  --dur-slow:    640ms;

  /* ---- 12 · Focus (verbatim) --------------------------------- */
  --focus-color:  var(--orange);
  --focus-width:  2px;
  --focus-offset: 3px;
  --focus-radius: 0.25rem;

  /* ---- 13 · Stacking  [APP] ---------------------------------- */
  --z-sticky:  20;
  --z-tabs:    25;
  --z-topbar:  30;
  --z-drawer:  60;
  --z-overlay: 70;
  --z-popover: 90;
  --z-modal:   100;
  --z-toast:  100;
  --z-tooltip: 110;

  --scrollbar-track:       var(--app-ground);
  --scrollbar-thumb:       var(--app-line-loud);
  --scrollbar-thumb-hover: var(--app-line-loud);

  color-scheme: dark;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: var(--dur-instant);
    --dur-mid:  var(--dur-instant);
    --dur-slow: var(--dur-instant);
  }
}
```

Then update the body rule (`global.css` line 621):

```css
body {
  background-color: var(--app-ground);
  color: var(--ink-fg);
  font-family: var(--font-brand), system-ui, sans-serif;  /* was var(--font-inter) */
  -webkit-font-smoothing: antialiased;
}
```

### 2.3 Two legacy bugs resolved

**Inverted hover borders.** The old `--border-secondary: #20232B` was darker than
`--border-primary: #2A2E38`, so card borders receded on hover. `--border-secondary` now
resolves to `--app-line-strong` and hover brightens correctly.

**Dim secondary text.** The old `--text-secondary: #9BA1B0` measured 6.4:1 on the old
background. `--ink-muted` `#cacaca` raises it to 11.9:1 on `--app-card` and aligns secondary
copy with the landing.

### 2.4 Values that intentionally change

| Token | Before | After | Why |
| --- | --- | --- | --- |
| body font | Inter | Plus Jakarta Sans | Landing and brand both mandate Jakarta |
| `--bg-primary` | `#0E0F14` | `#0a0a0c` | Landing ground; removes blue cast |
| `--bg-secondary` | `#16181F` | `#121214` | Landing `--stage-bg` |
| `--bg-tertiary` | `#1C1F28` | `#161618` | Landing `--midnight-plane` |
| `--border-primary` | `#2A2E38` | `rgba(166,166,166,0.12)` | Alpha lines layer correctly over any surface |
| `--text-secondary` | `#9BA1B0` | `#cacaca` | Landing `--ink-muted`; +legibility |
| `--success` | `#2FCB8A` | `#4ade80` | Landing `--green-positive` |
| `--error` | `#F0556A` | `#f87171` | Tonal pair with positive |
| `--info` | `#2A65D8` | `#7fa3ff` | `#2A65D8` fails AA as text on dark |
| `--brand-on-primary` | `#FFFFFF` | `#0c0c0e` | White on orange is 2.22:1 — see [§15](#15-decisions-and-follow-up) |

The blue cast is the single largest perceptual change. The old palette (`#0E0F14`, `#16181F`,
`#2A2E38`) is hue-shifted toward blue; the landing's midnight stack is neutral with a
whisper of warmth. Side by side, the old dashboard reads "generic crypto exchange" and the new
one reads like the landing.

---

## 3. Surfaces and elevation

### 3.1 Five levels, and only five

| Level | Token | Hex | Use | Border | Shadow |
| --- | --- | --- | --- | --- | --- |
| 0 · Ground | `--app-ground` | `#0a0a0c` | Page behind all chrome | — | — |
| 1 · Rail | `--app-rail` | `#0c0c0e` | Sidebar, topbar, tab bar | `--app-line-faint` | — |
| 2 · Card | `--app-card` | `#121214` | Cards, panels, tables, stat tiles | `--app-line` | `--app-e-flat` |
| 3 · Well | `--app-well` | `#161618` | Nested regions *inside* a card | `--app-line-faint` | none |
| 4 · Raised | `--app-raised` | `#1a1a1f` | Modals, drawers, popovers, dropdowns | `--app-line-strong` | `--app-e-modal` |

`.app-stage` is a focal composition built from the card/chrome materials, not a sixth surface
level. Its larger radius and diffuse neutral shadow are allowed once per selected lead view.

### 3.2 Nesting rules

- **A card never contains a card.** Nested content is a *well* (level 3), which has no shadow
  and a fainter border. Two shadowed rectangles inside each other is the single most common
  way dashboards start looking cheap.
- **Level 4 only floats.** If it is not layered above the page on the z-axis, it is not
  level 4. There is no "extra important card".
- **Maximum one shadowed object per viewport region.** Law 6 in material form. A grid of six
  stat cards uses `--app-e-flat`, not `--app-e-card` × 6.

### 3.3 Environment wash

The scroll container uses one static, non-animated gradient mirroring the landing's
`--surface-env-gradient-opening`:

```css
.app-shell {
  background:
    radial-gradient(ellipse 72% 48% at 82% 12%, rgba(42, 101, 216, 0.055), transparent 62%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, transparent 16%),
    var(--app-ground);
  background-attachment: fixed;
}
```

One blue key light from upper right, one soft top highlight. No animation, no orange, no
`filter: blur()`, no pseudo-element orbs. `DashboardAtmosphere` remains in route trees for API
compatibility only and must not paint an additional atmosphere.

---

## 4. Color

### 4.1 Role table

| Color | Token | Role | Never |
| --- | --- | --- | --- |
| `#ff931c` | `--orange` | Primary action, active indicator, focus ring | Headlines, icon tints, card borders, dividers |
| `#2a65d8` | `--blue` | Grid lines, axes, methodology chrome, train bands | Text, buttons, fills, badges |
| `#7fa3ff` | `--data-info` | Links, info badges, secondary chart series | Primary actions |
| `#4ade80` | `--data-positive` | Gains, up candles, healthy status | Success buttons, decorative accents |
| `#f87171` | `--data-negative` | Losses, down candles, errors, destructive confirm | Warnings, general emphasis |
| `#ffb14d` | `--data-warning` | Degraded status, quota warnings, pending | Anything also using `--orange` nearby |
| `#f2f2f2` → `--ink-faint` | ink ramp | All copy hierarchy | — |

### 4.2 Decision tree

```
Is the user about to act on it?
├─ Yes, and it is THE action on this screen  → --orange fill, pill  (max 1)
├─ Yes, but secondary                        → --app-field-bg + --app-line border
└─ No
   ├─ Is it a number that moved?             → --data-positive / --data-negative
   ├─ Is it a system state?                  → tint + matching ink (§9.6)
   ├─ Is it chart or grid structure?         → --blue at ≤0.2 alpha
   ├─ Is it a link?                          → --data-info
   └─ Otherwise                              → ink ramp only
```

### 4.3 Contrast, measured

Computed against `--app-card` `#121214`. The 4.5:1 line is body text; 3:1 is large text and
non-text UI.

| Foreground | Ratio | Body text | UI |
| --- | --- | --- | --- |
| `--ink-fg` `#f2f2f2` | 16.8:1 | pass | pass |
| `--ink-muted` `#cacaca` | 11.9:1 | pass | pass |
| `--ink-subtle` `#a6a6a6` | 7.6:1 | pass | pass |
| `--ink-faint` | ~4.6:1 | pass (marginal) | pass |
| `--orange` `#ff931c` | 8.4:1 | pass | pass |
| `--data-positive` `#4ade80` | 10.7:1 | pass | pass |
| `--data-negative` `#f87171` | 6.8:1 | pass | pass |
| `--data-info` `#7fa3ff` | 7.7:1 | pass | pass |
| `--blue` `#2a65d8` | **3.6:1** | **FAIL** | pass |
| `#ffffff` on `--orange` | **2.2:1** | **FAIL** | **FAIL** |
| `--on-orange` on `--orange` | 8.8:1 | pass | pass |

The last two rows are the reason `--brand-on-primary` changes. See [§15](#15-open-decisions).

`--ink-faint` is the floor. Anything dimmer is decoration and should be deleted rather than
made unreadable.

---

## 5. Typography

### 5.1 Family assignment

| Family | Token | Use in app |
| --- | --- | --- |
| Plus Jakarta Sans | `--font-brand` | Everything by default: nav, labels, copy, headings, buttons |
| Plus Jakarta Sans | `--font-mono` | Comparable financial data, table headers, IDs, and timestamps with tabular numerals |
| Syne | `--font-display` | **Not used in the app.** Public quotation / tagline only — upper & lowercase Semi Bold |

Syne is a supporting brand voice for one sung line on marketing surfaces. Inside a data tool
it reads as inconsistency. Canonical role table: [`BRAND.md`](BRAND.md).

### 5.2 Scale `[APP]`

The landing scale tops out at 5.75rem for a hero. An application needs a compressed scale with
more resolution at small sizes. This is the app scale, derived from the same base:

```css
:root {
  --app-t-micro:  0.6875rem;  /* 11px  table headers, chart axes, micro labels */
  --app-t-xs:     0.75rem;    /* 12px  badges, helper text, timestamps         */
  --app-t-sm:     0.8125rem;  /* 13px  table cells, dense controls             */
  --app-t-body:   0.875rem;   /* 14px  DEFAULT — body, buttons, inputs         */
  --app-t-lg:     1rem;       /* 16px  card titles, emphasised body            */
  --app-t-title:  1.125rem;   /* 18px  section titles                          */
  --app-t-page:   1.5rem;     /* 24px  page h1                                 */

  --app-t-metric-sm: 1.25rem; /* 20px  compact stat tile                       */
  --app-t-metric:    1.75rem; /* 28px  standard stat tile                      */
  --app-t-metric-lg: 2.25rem; /* 36px  hero balance                            */
}
```

**14px is the app default**, not 16px. The landing runs 16px because it is prose; the app is
scanning.

### 5.3 Role specifications

| Role | Size | Weight | Family | Tracking | Line height | Color |
| --- | --- | --- | --- | --- | --- | --- |
| Page title | `--app-t-page` | 700 | brand | `-0.02em` | 1.2 | `--ink-fg` |
| Page subtitle | `--app-t-body` | 400 | brand | 0 | 1.5 | `--ink-subtle` |
| Section title | `--app-t-title` | 600 | brand | `-0.015em` | 1.25 | `--ink-fg` |
| Card title | `--app-t-lg` | 600 | brand | `-0.012em` | 1.25 | `--ink-fg` |
| Eyebrow / overline | `--app-t-micro` | 500 | **mono** | `0.08em` | 1.4 | `--ink-subtle` |
| Body | `--app-t-body` | 400 | brand | 0 | 1.55 | `--ink-muted` |
| Dense body | `--app-t-sm` | 400 | brand | 0 | 1.5 | `--ink-muted` |
| Field label | `--app-t-xs` | 500 | brand | `0.01em` | 1.4 | `--ink-subtle` |
| Helper text | `--app-t-xs` | 400 | brand | 0 | 1.45 | `--ink-subtle`; `--ink-faint` only when optional |
| Table header | `--app-t-micro` | 500 | **mono** | `0.08em`, uppercase | 1.4 | `--ink-subtle` |
| Table cell | `--app-t-sm` | 400 | brand | 0 | 1.45 | `--ink-muted` |
| Table cell, numeric | `--app-t-sm` | 500 | **mono** | 0 | 1.45 | `--ink-fg` |
| Metric value | `--app-t-metric` | 600 | **mono** | `-0.01em` | 1.1 | `--ink-fg` |
| Metric delta | `--app-t-sm` | 500 | **mono** | 0 | 1.3 | positive/negative |
| Button | `--app-t-body` | 600 | brand | `0.01em` | 1 | per variant |
| Commit button | `--app-t-sm` | 700 | brand | `0.05em`, uppercase | 1 | `--on-orange` |
| Badge | `--app-t-xs` | 600 | brand | `0.03em` | 1 | per tone |
| State label | `--app-t-xs` | 600 | brand | `0.05em`, uppercase | 1.4 | `--ink-subtle` |

The mono uppercase eyebrow is the single strongest carrier of the Prospr voice at small sizes.
Use it for every table header and every section overline, and the app will read as the landing
before anything else changes.

Its tracking is `--tracking-meta` (`0.08em`, `foundation.css` line 71) rather than the
`0.14em`–`0.16em` the landing's own `.campaign-eyebrow` and `.campaign-chapter-head__eyebrow`
use. Those are set for isolated marketing overlines with room to breathe; at 11px in a table
header row, that much tracking costs measurable column width and starts to fragment the word.
`--tracking-meta` is the same design intent tuned for density, and it is already a foundation
token, so this is a selection from the system rather than a departure from it.

### 5.4 Compact terminal profile

Exchange and DOM workspaces use the same semantic roles at a tighter spatial density; they do
not define a second typography system. Kraken Pro and Bybit were used as density references:
readability comes from weight, contrast, and grouped controls rather than enlarged type.

| Terminal role | Size | Weight | Minimum geometry |
| --- | --- | --- | --- |
| Panel title | `--app-t-body` (14px) | 650 | — |
| Tabs / segmented controls | `--app-t-sm` (13px) | 500–600 | 32px desktop, 44px touch |
| Field label / helper | `--app-t-xs` (12px) | 500 / 400 | — |
| Input value / final action | `--app-t-body` (14px) | 600 / 700 | 36–40px desktop, 44px touch |
| Summary label / value | 12px / 13px | 400–500 / 600 | — |
| Micro metadata | `--app-t-micro` (11px) | 500 | Axes, table headers, timestamps only |

Terminal spacing uses the 4px base: 4–6px from label to control, 8px inside tight clusters,
10–12px between form groups, and 16px only at major boundaries. Selected Buy/Sell states use
semantic tints; the final order action is the only solid green/red fill in the ticket.

### 5.5 Prohibitions

- No gradient text. `SectionHeader` uses the ink ramp and application type scale.
- No `text-white`. Use `--ink-fg`. White is 1.6% brighter and breaks the ramp.
- No font weights above 700 in the app. 800 is reserved for landing display type.
- No uppercase except: eyebrows, table headers, commit buttons, state labels.

---

## 6. Layout and spacing

### 6.1 Shell geometry

```
┌────────────┬──────────────────────────────────────────────┐
│            │  topbar           3.75rem   --app-topbar-h   │  z 30
│  sidebar   ├──────────────────────────────────────────────┤
│  14rem     │  tab bar           2.75rem  --app-tabs-h     │  z 25
│            ├──────────────────────────────────────────────┤
│  --app-    │                                              │
│  sidebar-w │   page                                       │
│            │   max  90rem   --app-page-max                │
│  collapsed │   pad  clamp(.75rem, 1.75vw, 1.25rem)        │
│  3.25rem   │                                              │
└────────────┴──────────────────────────────────────────────┘
```

- Sidebar and topbar sit on `--app-rail`; the page scroll region sits on `--app-ground`.
  The 2-step tone difference (`#0c0c0e` vs `#0a0a0c`) is the entire separation — no shadow,
  no heavy divider, just a `--app-line-faint` hairline.
- `.app-page` uses the available shell width so professional tables and terminals retain
  density. Individual reading surfaces may cap their content; do not globally constrain a
  data workspace to a marketing-page column.
- Mobile (`< 768px`): sidebar collapses to `MobileBottomNav`, `--app-page-gutter` floors at
  `0.75rem`, and the shell reserves space for bottom navigation.

### 6.2 Vertical rhythm

| Gap | Token | Value |
| --- | --- | --- |
| Inside a card, label → value | `--space-1`/`2` | 4–8px |
| Between rows in a form | `--space-4` | 16px |
| Between sibling cards in a grid | `--app-stack` | 16px |
| Between page sections | `--app-stack-lg` | 24px |
| Page top/bottom padding | `--app-page-y` | 12–18px |

The app uses a tighter rhythm than the landing's `--scene-y-standard`
(`clamp(4.25rem, 7.75vw, 6.75rem)`). Marketing rhythm inside a dashboard wastes the viewport.

### 6.3 Grid

Use `.app-grid` for consistent application spacing, then declare the tracks required by the
page. The primitive owns display and gap; route composition owns column proportions:

```css
.app-grid { display: grid; gap: var(--app-stack); }
.app-grid--lg { gap: var(--app-stack-lg); }
```

Common desktop proportions are four-up stats, 2:1 or 8:4 lead/rail splits, and full-width
tables. Collapse to a single readable column when the data cannot scan cleanly side by side.

Breakpoints match `BRAND.md`: **1440, 1024, 768, 390, 360**. Every page must be usable at all
five with no horizontal overflow.

### 6.4 Page template

The composition API is exported from `src/components/dashboard/ui`. New routes must start with
one of the recipes below rather than rebuilding gutters, headers, toolbars, sections, or stage
anatomy with route-local utilities.

#### Standard tool page

```jsx
<DashboardPage atmosphere="minimal">
  <DashboardPageHeader
    eyebrow="Portfolio"
    title="Automated"
    description="Strategy-managed capital and live allocations."
    actions={(
      <>
      <button className="app-btn app-btn--standard">Export</button>
      <button className="app-btn app-btn--commit">Deposit</button>
      </>
    )}
  />

  <DashboardSection title="Allocation" description="Current strategy distribution.">
    <DashboardGrid columns={3}>{/* cards */}</DashboardGrid>
  </DashboardSection>
</DashboardPage>
```

#### Dense workspace

```jsx
<DashboardPage variant="workspace" atmosphere="data">
  <DashboardPageHeader
    compact
    context={<ExchangeWorkspaceNav />}
    title="Orders"
    description="4 open · 12 total orders"
  />
  <DashboardToolbar label="Order filters">{/* segments and fields */}</DashboardToolbar>
  <DashboardCard padding="custom">{/* dense table or terminal */}</DashboardCard>
</DashboardPage>
```

#### Route-family workspace

Use this for related page groups such as Portfolio, Automated Investing, Intelligence,
Earn/Rewards, Token Center, and Account/Settings. The title is the focal point, route state is
secondary, and local controls remain aligned without being mixed into navigation.
`DashboardWorkspaceNav` owns the shared bordered
selector, action-tint route state, and horizontal mobile scroll. It is the same geometry used by
Documentation and Exchange, not a separate underline tab system.

```jsx
<DashboardPage atmosphere="data">
  <DashboardWorkspaceHeader
    eyebrow="Portfolio"
    title="Manual portfolio"
    description="Assets held from spot and margin trades."
    meta={<span className="app-label-state">Simulated account</span>}
    actions={/* route-scoped controls */}
    navigation={(
      <DashboardWorkspaceNav
        label="Portfolio workspace"
        activePath={pathname}
        items={portfolioRoutes}
      />
    )}
  />
  {/* route content */}
</DashboardPage>
```

#### Stage lead

```jsx
<DashboardPage>
  <DashboardStage
    chrome={(
      <>
        <DashboardChromeGroup><span className="app-live-dot" /><h1 className="app-chrome__title">Portfolio</h1></DashboardChromeGroup>
        <DashboardChromeGroup>{/* horizon and state controls */}</DashboardChromeGroup>
      </>
    )}
    rail={/* supporting context */}
  >
    {/* one focal metric, chart, or decision */}
  </DashboardStage>
</DashboardPage>
```

#### Utility workspace

Use this inside `SettingsWorkspaceHeader` routes and for bounded account, billing, document,
security, and entitlement tasks. One `SettingsSection` owns each task group; related values use
`SettingsRows` and `SettingsRow` rather than separate cards. `SettingsNotice` communicates
status or consequence, `SettingsEmptyState` explains absent data with one next action, and
`SettingsSwitch` is the single binary-control geometry.

```jsx
<DashboardPage atmosphere="minimal">
  <SettingsWorkspaceHeader
    title="Account Settings"
    description="Identity, verification, and regional preferences."
  />

  <div className="app-settings-grid app-settings-grid--2">
    <SettingsSection title="Account details">
      <SettingsRows>
        <SettingsRow label="Public ID" action={<CopyButton />}>
          <span className="app-num">user_1234</span>
        </SettingsRow>
      </SettingsRows>
    </SettingsSection>
    <SettingsSection title="Security">
      <SettingsNotice tone="info">Passkey enrollment opens at launch.</SettingsNotice>
      <SettingsSwitch checked={enabled} onChange={setEnabled} label="Device alerts" />
    </SettingsSection>
  </div>
</DashboardPage>
```

The implementation source is `src/components/settings/SettingsComposition.js`. The CSS
contract is `.app-settings-*`, `.app-notice`, `.app-empty`, `.app-control`, and `.app-switch`.
Do not recreate settings rows, callouts, toggles, or empty states with route-local palette and
radius utilities.

#### Focused flow

Use `<DashboardPage variant="flow">` for setup/onboarding and bounded reading workflows. It
caps the content width but retains the application gutter and mobile shell contract.

The implemented CSS contract is `.app-page__head`, `.app-page__title`, `.app-page__sub`,
`.app-page__actions`, `.app-workspace-head*`, `.app-workspace-nav*`, `.app-selector`,
`.app-segment--nav`, `.app-section*`, `.app-toolbar`, `.app-grid--2/3/4/lead`, and the existing `.app-stage*` anatomy in
`src/app/global.css`. `DashboardComposition.js` owns the generic markup; route-family wrappers
only provide labels and destinations. Utility workspaces extend this contract through
`SettingsComposition.js`; they do not define a second visual system.

The stage pattern remains reserved for dashboard home and Guided/high-traffic leads that benefit
from a single theatre object: Strategies, Rewards, Total Portfolio, and Token Center overview.
Exchange remains dense by design; token, hierarchy, and responsive rules still apply.

---

## 7. Shape, border, shadow

### 7.1 Radius, assigned

| Token | px | Applies to |
| --- | --- | --- |
| `--app-r-sm` | 6 | Inner wells, skeleton lines, icon chips, checkbox |
| `--app-r-md` | 8 | Buttons, inputs, selects, dropdown items, tabs, row actions |
| `--app-r-lg` | 12 | Cards, panels, stat tiles, table container |
| `--app-r-xl` | 18 | Modals, drawers, full-bleed stages |
| `--app-r-pill` | 999 | Commit buttons, badges, status pills, avatars, toggles |

**The dashboard card radius is already correct.** `DashboardCard` uses `rounded-xl` (12px),
matching the landing's `.product-surface` → `--depth-radius-plane` `0.75rem`. Do not "upgrade"
it to `rounded-2xl`; the 16px `.prospr-card` in `global.css` is a marketing-surface value and
is the outlier, not the target.

### 7.2 Borders

Every border is a 1px alpha hairline. No 2px borders except the stat tile accent rail and the
active tab indicator.

| Context | Token |
| --- | --- |
| Card, panel | `--app-line` |
| Card hover | `--app-line-strong` |
| Divider inside a card | `--app-line-faint` |
| Table row separator | `--app-line-faint` |
| Field idle | `--app-line` |
| Field hover | `--app-line-strong` |
| Field focus | `--orange` + focus ring |
| Modal | `--app-line-strong` |
| Selected / active | `--orange` at 0.32 alpha |

Alpha borders are mandatory — an opaque `#2A2E38` line reads differently on `--app-ground`
than on `--app-card`, which caused the legacy dashboard's sidebar/page inconsistency.

### 7.3 Shadows

Four, and they are all neutral black:

```
--app-e-flat     inset 0 1px 0 rgba(255,255,255,0.02)              stat tiles, list items
--app-e-card     0 8px 24px rgba(0,0,0,0.32)                       the one focal card
--app-e-raised   inset highlight + 0 16px 36px rgba(0,0,0,0.42)    dropdowns, popovers
--app-e-modal    inset highlight + 0 20px 56px rgba(0,0,0,0.48)    modals, drawers
```

A chromatic `box-shadow` is a lint failure. Grep for it in review.

---

## 8. Motion

### 8.1 Budget

| Interaction | Duration | Easing | Properties |
| --- | --- | --- | --- |
| Hover / focus on control | `--dur-fast` 180ms | `--ease-enter` | `background-color`, `border-color`, `color` |
| Tab or panel switch | `--dur-fast` | `--ease-enter` | `opacity`, `transform` |
| Dropdown / popover open | `--dur-fast` | `--ease-enter` | `opacity`, `transform: translateY(-4px → 0)` |
| Modal open | `--dur-mid` 280ms | `--ease-enter` | `opacity`, `transform: scale(0.98 → 1)` |
| Drawer slide | `--dur-mid` | `--ease-enter` | `transform: translateX` |
| Toast in | `--dur-fast` | `--ease-enter` | `opacity`, `transform: translateY(8px → 0)` |
| Chart draw-in | `--dur-slow` 640ms | `--ease-enter` | once, on mount only |
| Skeleton pulse | 2s loop | `cubic-bezier(0.4,0,0.6,1)` | `opacity` 0.6 ↔ 0.4 |

### 8.2 Never animate

`transform` on cards (no hover lift), `box-shadow` on anything, `filter`, `width`/`height`
(use `transform`), anything on a decorative element, anything on an infinite loop except the
skeleton pulse.

The audited dashboard removed hover lift, active-tab scaling, atmosphere float keyframes, and
glow opacity fades. Treat those patterns as regression failures if they return.

### 8.3 Reduced motion

Durations already zero via the `:root` media query in §2.2. In addition, add a global guard so
component-level Tailwind durations are covered:

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
```

---

## 9. Component specifications

Implementation pattern: **CSS classes in `global.css` under `@layer components`, plus a thin
React wrapper in `src/components/dashboard/ui/`.** This mirrors how the landing does it
(`.btn` + JSX), keeps variant logic out of template strings, and means a token change
propagates without touching React.

### 9.1 Buttons

```css
@layer components {
  .app-btn {
    display: inline-flex; align-items: center; justify-content: center;
    gap: 0.5rem;
    min-height: 2.25rem;                 /* 36px — default */
    padding-inline: 0.875rem;
    border: 1px solid transparent;
    border-radius: var(--app-r-md);
    font-family: var(--font-brand);
    font-size: var(--app-t-body);
    font-weight: 600;
    letter-spacing: 0.01em;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    transition:
      background-color var(--dur-fast) var(--ease-enter),
      border-color     var(--dur-fast) var(--ease-enter),
      color            var(--dur-fast) var(--ease-enter);
  }
  .app-btn:focus-visible {
    outline: var(--focus-width) solid var(--focus-color);
    outline-offset: var(--focus-offset);
  }
  .app-btn:disabled { opacity: 0.45; cursor: not-allowed; }

  /* Tier 1 — commitment. One per view. */
  .app-btn--commit {
    padding-inline: 1.25rem;
    border-radius: var(--app-r-pill);
    background: var(--orange);
    color: var(--on-orange);
    font-size: var(--app-t-sm);
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
  }
  .app-btn--commit:hover  { background: var(--orange-hover); }
  .app-btn--commit:active { background: var(--orange-hover); box-shadow: inset 0 2px 4px rgba(0,0,0,0.18); }

  /* Tier 2 — standard */
  .app-btn--standard {
    background: var(--app-field-bg);
    border-color: var(--app-line);
    color: var(--ink-fg);
  }
  .app-btn--standard:hover { background: var(--app-field-bg-hover); border-color: var(--app-line-strong); }

  /* Tier 3 — quiet */
  .app-btn--quiet { background: transparent; color: var(--ink-subtle); }
  .app-btn--quiet:hover { background: var(--app-field-bg); color: var(--ink-fg); }

  /* Tier 4 — inline navigation */
  .app-btn--nav {
    display: inline-flex; align-items: center; gap: 0.25rem;
    color: var(--data-info);
    font-size: var(--app-t-sm); font-weight: 500;
    text-decoration: none;
  }
  .app-btn--nav:hover { color: var(--ink-fg); }

  /* Sizes */
  .app-btn--sm { height: 1.75rem; padding-inline: 0.625rem; font-size: var(--app-t-xs); gap: 0.375rem; }
  .app-btn--lg { height: 2.5rem;  padding-inline: 1.125rem; }

  /* Icon-only */
  .app-btn--icon { width: 2.125rem; padding-inline: 0; }
  .app-btn--icon.app-btn--sm { width: 1.75rem; }
}
```

Destructive commands retain the standard rectangular control and add negative ink/border
tokens. Reserve a solid red fill for the final destructive confirmation inside a modal, where
the consequence is explicit.

**Compatibility mapping from `GlowButton`.** The legacy component name and prop surface remain
so call sites do not break; its internals now resolve to `.app-btn*`.

| `GlowButton` prop | New behaviour |
| --- | --- |
| `variant="primary"` | → `.app-btn--commit` |
| `variant="secondary"` | → `.app-btn--standard` |
| `variant="outline"` | → `.app-btn--standard` |
| `variant="ghost"` | → `.app-btn--quiet` |
| `variant="success"` | → `.app-btn--commit`; no green action fill |
| `variant="danger"` | → neutral standard control with negative ink/border |
| `glow` | Accepted and ignored for API compatibility |

New code should use the direct `.app-btn` role classes when the action hierarchy is already
known. Compatibility mapping does not waive the one-commit-action budget; route composition
must still avoid multiple orange controls competing in one view.

### 9.2 Fields

```css
@layer components {
  .app-field {
    width: 100%;
    min-height: var(--app-control-h);    /* 36px desktop */
    padding: 0 0.75rem;
    background: var(--app-field-bg);
    border: 1px solid var(--app-line);
    border-radius: var(--app-r-md);
    font-family: var(--font-brand);
    font-size: var(--app-t-body);
    color: var(--ink-fg);
    transition: border-color var(--dur-fast) var(--ease-enter),
                background-color var(--dur-fast) var(--ease-enter);
  }
  .app-field::placeholder { color: var(--ink-subtle); }
  .app-field:hover  { border-color: var(--app-line-strong); }
  .app-field:focus  { outline: none; border-color: var(--orange); background: var(--app-field-bg-hover); }
  .app-field:focus-visible { outline: var(--focus-width) solid var(--focus-color); outline-offset: 1px; }
  .app-field:disabled { opacity: 0.5; cursor: not-allowed; }
  .app-field[aria-invalid="true"] { border-color: var(--data-negative); }

  .app-field--mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
  .app-field--sm   { min-height: var(--app-control-h-compact); font-size: var(--app-t-sm); }
  .app-field--lg   { height: 2.5rem; }
  textarea.app-field { height: auto; min-height: 5rem; padding-block: 0.5rem; line-height: 1.5; resize: vertical; }
  select.app-field   { padding-right: 2rem; appearance: none; background-image: /* chevron */; }

  .app-label { display: block; margin-bottom: 0.375rem; font-size: var(--app-t-xs); font-weight: 500; letter-spacing: 0.01em; color: var(--ink-subtle); }
  .app-help  { margin-top: 0.375rem; font-size: var(--app-t-xs); line-height: 1.45; color: var(--ink-subtle); }
  .app-help--quiet { color: var(--ink-faint); }
  .app-help--error { color: var(--data-negative); }
}
```

Amount inputs use `.app-field .app-field--mono` with a right-aligned unit suffix. Any field
capturing money, quantity, or percentage is mono (Law 4).

**Field shape note.** The landing's waitlist input is a pill (`border-radius: 999px`,
`min-height: 3rem`) because it is a single marketing capture. App fields are 8px rects at 36px
— pill fields in a dense form waste horizontal space and break alignment with adjacent
selects and buttons. This is Law 5 applied consistently: pill means commitment, and typing
into a filter box is not a commitment.

#### 9.2.1 Trade-ticket TP/SL attachment

For Guided market buys and Pro primary Market and Limit entries, `.trade-tpsl` sits after Amount
and before the cost/risk summary. Its collapsed row is a single compact orange checkbox; purple
is never an order state.
The expanded state accepts take-profit and stop-loss distance percentages. Each field keeps its
resolved USD trigger and estimated gain/loss on the label line, avoiding a nested summary card
or a repeated P&L block.

TP/SL always requires confirmation and the confirmation repeats both trigger prices. The demo
creates an attached one-cancels-the-other watcher only after the primary fill. It is supported
for spot buys and isolated-margin long/short positions; spot sells remain unprotected rather
than implying a position that does not exist. Demo copy must state that matching is client-side
while Trade or Orders is open and is not routed to a live venue.

### 9.3 Cards

```css
@layer components {
  .app-card {
    position: relative;
    display: flex; flex-direction: column;
    background: var(--app-card-face);
    border: 1px solid var(--app-line);
    border-radius: var(--app-r-lg);
    box-shadow: var(--app-e-flat);
    overflow: hidden;
  }
  .app-card--focal { box-shadow: var(--app-e-card); }        /* max one per viewport */
  .app-card--interactive { cursor: pointer; transition: border-color var(--dur-fast) var(--ease-enter); }
  .app-card--interactive:hover { border-color: var(--app-line-strong); }

  .app-card__head {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    padding: var(--app-card-pad-sm) var(--app-card-pad);
    border-bottom: 1px solid var(--app-line-faint);
  }
  .app-card__title { font-size: var(--app-t-lg); font-weight: 600; letter-spacing: -0.012em; color: var(--ink-fg); }
  .app-card__sub   { margin-top: 0.125rem; font-size: var(--app-t-xs); color: var(--ink-faint); }
  .app-card__body  { padding: var(--app-card-pad); flex: 1; min-height: 0; }
  .app-card__foot  { padding: var(--app-card-pad-sm) var(--app-card-pad); border-top: 1px solid var(--app-line-faint); }
  .app-card__body--flush { padding: 0; }                     /* tables, charts */

  .app-well {
    background: var(--app-well);
    border: 1px solid var(--app-line-faint);
    border-radius: var(--app-r-sm);
    padding: var(--app-card-pad-sm);
  }
}
```

`DashboardCard` keeps its API (`padding`, `variant`, `hover`, `onClick`). `variant="stage"`
selects `.app-stage` for the single focal theatre; `highlight` is an orange edge without glow.
`glass`, `glow`, and `gradient` remain compatibility inputs, but they must not create a second
visual language or reintroduce chromatic effects.

### 9.4 Stat tiles

```css
@layer components {
  .app-stat {
    display: flex; flex-direction: column; gap: 0.5rem;
    padding: var(--app-card-pad);
    background: var(--app-card);
    border: 1px solid var(--app-line);
    border-top: 2px solid var(--app-line);
    border-radius: var(--app-r-lg);
    box-shadow: var(--app-e-flat);
  }
  .app-stat--action   { border-top-color: var(--orange); }
  .app-stat--positive { border-top-color: var(--data-positive); }
  .app-stat--negative { border-top-color: var(--data-negative); }
  .app-stat--warning  { border-top-color: var(--data-warning); }

  .app-stat__label {
    font-family: var(--font-mono);
    font-size: var(--app-t-micro); font-weight: 500;
    letter-spacing: var(--tracking-meta); text-transform: uppercase;
    color: var(--ink-subtle);
  }
  .app-stat__value {
    font-family: var(--font-mono); font-variant-numeric: tabular-nums;
    font-size: var(--app-t-metric); font-weight: 600;
    letter-spacing: -0.01em; line-height: 1.1;
    color: var(--ink-fg);
  }
  .app-stat__meta { display: flex; align-items: baseline; gap: 0.5rem; font-size: var(--app-t-sm); }
  .app-stat__delta { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-weight: 500; }
  .app-stat__delta--up   { color: var(--data-positive); }
  .app-stat__delta--down { color: var(--data-negative); }
  .app-stat__delta--flat { color: var(--ink-faint); }
}
```

`StatCard` now uses a mono label, `--ink-fg` for values, and semantic accent rails instead of
hardcoded Tailwind palette colors.

Icons in stat tiles are optional and, when present, are `--ink-subtle` in a `--app-r-sm` well
— never a colored chip. A grid of six tiles with six colored icons is a Law 2 violation.

### 9.5 Tables

The most important component in the app and the one with no landing precedent. This spec is
derived from the landing's evidence-plane treatment (`--evidence-*` tokens, mono data ink,
`--evidence-rule` separators).

```css
@layer components {
  .app-table-wrap {
    background: var(--app-card);
    border: 1px solid var(--app-line);
    border-radius: var(--app-r-lg);
    overflow: hidden;
  }
  .app-table { width: 100%; border-collapse: separate; border-spacing: 0; }

  .app-table thead th {
    position: sticky; top: 0; z-index: 1;
    height: 2.25rem;
    padding: 0 0.875rem;
    background: var(--app-well);
    border-bottom: 1px solid var(--app-line);
    font-family: var(--font-mono);
    font-size: var(--app-t-micro); font-weight: 500;
    letter-spacing: var(--tracking-meta); text-transform: uppercase;
    color: var(--ink-subtle);
    text-align: left;
    white-space: nowrap;
  }
  .app-table tbody td {
    height: 3rem;
    padding: 0 0.875rem;
    border-bottom: 1px solid var(--app-line-faint);
    font-size: var(--app-t-sm);
    color: var(--ink-muted);
    vertical-align: middle;
  }
  .app-table tbody tr:last-child td { border-bottom: 0; }
  .app-table tbody tr { transition: background-color var(--dur-fast) var(--ease-enter); }
  .app-table tbody tr:hover { background: var(--app-row-hover); }

  /* Numeric columns */
  .app-table .num, .app-table th.num {
    text-align: right;
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-weight: 500;
    color: var(--ink-fg);
  }
  .app-table .num--up   { color: var(--data-positive); }
  .app-table .num--down { color: var(--data-negative); }

  .app-table--compact tbody td { height: 2.5rem; }
  .app-table--compact thead th { height: 2rem; }

  /* Row actions: revealed on hover, always reachable by keyboard */
  .app-table__actions { display: flex; gap: 0.25rem; justify-content: flex-end; opacity: 0; transition: opacity var(--dur-fast) var(--ease-enter); }
  tr:hover .app-table__actions,
  tr:focus-within .app-table__actions { opacity: 1; }
}
```

Rules:

- **Numeric columns are right-aligned, mono, tabular.** Non-negotiable. A left-aligned
  proportional price column is the clearest signal that a screen was not designed.
- **Zebra striping is banned.** Separation comes from the `--app-line-faint` rule plus row
  hover. Striping fights the tone ramp.
- Sortable headers get a chevron at `--ink-faint`, `--orange` when active.
- The first column is the entity (identifier); the last column is actions.
- Wrap in `overflow-x: auto` below 1024px; never shrink font size to fit.
- Never nest a `.app-card` inside `.app-table-wrap` — the wrap *is* the card.

### 9.6 Badges and status

```css
@layer components {
  .app-badge {
    display: inline-flex; align-items: center; gap: 0.3125rem;
    height: 1.375rem; padding-inline: 0.5rem;
    border: 1px solid transparent;
    border-radius: var(--app-r-pill);
    font-size: var(--app-t-xs); font-weight: 600; letter-spacing: 0.03em;
    line-height: 1; white-space: nowrap;
  }
  .app-badge__dot { width: 0.375rem; height: 0.375rem; border-radius: 50%; background: currentColor; }

  .app-badge--neutral  { background: var(--app-field-bg); border-color: var(--app-line);            color: var(--ink-subtle); }
  .app-badge--positive { background: var(--tint-positive); border-color: rgba(74,222,128,0.28);     color: var(--data-positive); }
  .app-badge--negative { background: var(--tint-negative); border-color: rgba(248,113,113,0.28);    color: var(--data-negative); }
  .app-badge--warning  { background: var(--tint-warning);  border-color: rgba(255,177,77,0.28);     color: var(--data-warning); }
  .app-badge--info     { background: var(--tint-info);     border-color: rgba(127,163,255,0.28);    color: var(--data-info); }
  .app-badge--action   { background: var(--tint-action);   border-color: var(--orange-edge);        color: var(--orange); }

  /* Non-negotiable state labels — never colored (VISUAL-DIRECTION.md §"State & legal labeling") */
  .app-label-state {
    display: inline-block;
    font-size: var(--app-t-xs); font-weight: 600;
    letter-spacing: 0.05em; text-transform: uppercase;
    color: var(--ink-subtle);
  }
}
```

**Status → tone mapping.** Use this table verbatim so the same word never renders two ways
across the application:

| Tone | States |
| --- | --- |
| `positive` | Active, Live, Filled, Settled, Verified, Healthy, Connected, Paid |
| `negative` | Failed, Rejected, Cancelled, Liquidated, Error, Disconnected, Overdue |
| `warning` | Pending, Partial, Degraded, Review, Expiring, Throttled |
| `info` | Scheduled, Queued, Draft, Simulated, Backtest |
| `neutral` | Inactive, Paused, Archived, Closed, Unknown |
| `action` | Requires action, Confirm, Upgrade |

`Prototype`, `Illustrative`, `Planned`, and `Simulated` use `.app-label-state`, never a badge
tone. Those labels are legal disclosure and must not read as decorative UI — this preserves
the landing's `.label-prototype` / `.label-illustrative` contract and the guardrail that the
product must never appear live.

### 9.7 Tabs and segmented controls

```css
@layer components {
  .app-tabs {
    display: flex; align-items: center; gap: 0.25rem;
    height: var(--app-tabs-h);
    padding-inline: var(--app-page-gutter);
    background: transparent; /* .app-chrome--sub owns the rail and hairline */
    overflow-x: auto; scrollbar-width: none;
  }
  .app-tab {
    position: relative;
    display: inline-flex; align-items: center;
    height: 100%; padding-inline: 0.75rem;
    font-size: var(--app-t-body); font-weight: 500;
    color: var(--ink-subtle);
    white-space: nowrap; background: none; border: 0; cursor: pointer;
    transition: color var(--dur-fast) var(--ease-enter);
  }
  .app-tab:hover { color: var(--ink-fg); }
  .app-tab[aria-current="page"] { color: var(--ink-fg); font-weight: 600; }
  .app-tab[aria-current="page"]::after {
    content: ""; position: absolute; inset-inline: 0.5rem; bottom: 0;
    height: 2px; border-radius: 1px 1px 0 0;
    background: var(--orange);
  }

  /* Shared selector geometry — routes, tabs, modes, and filters */
  .app-segment { display: inline-flex; padding: 0.1875rem; background: var(--app-well); border: 1px solid var(--app-line-faint); border-radius: var(--app-r-md); }
  .app-segment__item { height: 1.75rem; padding-inline: 0.75rem; border: 0; border-radius: calc(var(--app-r-md) - 2px); background: none; font-size: var(--app-t-sm); font-weight: 500; color: var(--ink-subtle); cursor: pointer; }
  .app-segment__item[aria-selected="true"] { background: var(--app-field-bg-hover); color: var(--ink-fg); }
  .app-segment--nav .app-segment__item[aria-current="page"],
  .app-segment--nav .app-segment__item[aria-selected="true"] { background: var(--brand-primary-10); color: var(--brand-light); }
  .app-selector { max-width: 100%; overflow-x: auto; scrollbar-width: none; }
}
```

`DashboardSelector` and `DashboardWorkspaceNav` share the same selector geometry. Route-family
navigation uses `.app-segment--nav` for a restrained orange-tint location state; Documentation
tabs use the same modifier; Exchange uses its compact density. Local modes and filters omit the
modifier and retain a neutral selected state. Shell-level `TabNavigation` has been removed from
the dashboard layout; every active route family owns its selector beside its page identity.

Sidebar active state uses a **left** rail instead of an underline: 2px `--orange`, inset 0, with
`background: var(--tint-action)` on the item.

### 9.8 Overlays

```css
@layer components {
  .app-modal-layer {
    position: fixed; inset: 0; z-index: var(--z-modal);
    display: flex; align-items: center; justify-content: center; padding: 1rem;
  }
  .app-overlay {
    position: absolute; inset: 0;
    background: var(--app-scrim);
    backdrop-filter: blur(2px);
  }
  .app-modal {
    position: relative;
    width: min(34rem, calc(100vw - 2rem));
    max-height: min(44rem, calc(100dvh - 2rem));
    display: flex; flex-direction: column;
    background: var(--app-raised);
    border: 1px solid var(--app-line-strong);
    border-radius: var(--app-r-xl);
    box-shadow: var(--app-e-modal);
    overflow: hidden;
  }
  .app-modal__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); padding: 1.25rem 1.25rem 1rem; }
  .app-modal__title { font-size: var(--app-t-title); font-weight: 600; letter-spacing: -0.015em; color: var(--ink-fg); }
  .app-modal__body { padding: 0 1.25rem 1.25rem; overflow-y: auto; }
  .app-modal__foot { display: flex; justify-content: flex-end; gap: 0.5rem; padding: 1rem 1.25rem; border-top: 1px solid var(--app-line-faint); background: var(--app-card); }

  .app-popover {
    z-index: var(--z-popover);
    min-width: 12rem; padding: 0.25rem;
    background: var(--app-raised);
    border: 1px solid var(--app-line-strong);
    border-radius: var(--app-r-md);
    box-shadow: var(--app-e-raised);
  }
  .app-popover__item {
    display: flex; align-items: center; gap: 0.5rem;
    width: 100%; padding: 0.4375rem 0.625rem;
    border: 0; border-radius: var(--app-r-sm);
    background: none; text-align: left;
    font-size: var(--app-t-sm); color: var(--ink-muted); cursor: pointer;
  }
  .app-popover__item:hover { background: var(--app-field-bg-hover); color: var(--ink-fg); }
  .app-popover__item--danger { color: var(--data-negative); }
  .app-popover__sep { height: 1px; margin: 0.25rem 0; background: var(--app-line-faint); }
}
```

Modal width ladder: `26rem` confirm, `34rem` default, `48rem` form, `64rem` data. Drawers are
`min(28rem, 100vw)` from the right, radius `--app-r-xl` on the left corners only.

Plan tiers do not receive individual hues. `Current` uses a neutral badge, stronger neutral
edge, and quiet inset rail; higher and lower tiers remain neutral. Orange is reserved for the
selected tab and a single final commit action. Purple/violet is not a plan-state color.

Footer buttons order left→right: quiet cancel, then the commit. Exactly one commit per modal.

Every modal traps focus, restores focus to the trigger on close, closes on `Escape`, and
carries `role="dialog"` + `aria-modal="true"` + `aria-labelledby`.

### 9.9 Banners, alerts, toasts

```css
@layer components {
  .app-banner {
    display: flex; align-items: flex-start; gap: 0.625rem;
    padding: 0.75rem 1rem;
    border: 1px solid var(--app-line);
    border-left-width: 2px;
    border-radius: var(--app-r-md);
    font-size: var(--app-t-sm);
    color: var(--ink-muted);
    background: var(--app-field-bg);
  }
  .app-banner--info     { border-left-color: var(--data-info);     background: var(--tint-info); }
  .app-banner--warning  { border-left-color: var(--data-warning);  background: var(--tint-warning); }
  .app-banner--negative { border-left-color: var(--data-negative); background: var(--tint-negative); }
  .app-banner--action   { border-left-color: var(--orange);        background: var(--tint-action); }
  .app-banner__title { font-weight: 600; color: var(--ink-fg); }

  .app-toast {
    z-index: var(--z-toast);
    display: flex; align-items: flex-start; gap: 0.625rem;
    min-width: 18rem; max-width: 24rem; padding: 0.75rem 1rem;
    background: var(--app-raised);
    border: 1px solid var(--app-line-strong);
    border-radius: var(--app-r-md);
    box-shadow: var(--app-e-raised);
    font-size: var(--app-t-sm); color: var(--ink-muted);
  }
}
```

The current audit found no shared toast provider. Introduce one in
`src/components/dashboard/ui/` rather than adding a dependency — the guardrails forbid new
packages. Toasts stack bottom-right, auto-dismiss at 5s (never for errors), pause on hover,
and announce via `role="status"` / `role="alert"`.

`DemoModeBanner` uses `.app-banner`; move remaining persistent plan, quota, and token-risk
banners onto the same primitive when those surfaces are touched.

### 9.10 Empty states and loading

```css
@layer components {
  .app-empty {
    display: flex; flex-direction: column; align-items: center; text-align: center;
    gap: 0.5rem;
    padding: clamp(2rem, 6vw, 3.5rem) 1.5rem;
  }
  .app-empty__icon  { width: 2.5rem; height: 2.5rem; color: var(--ink-faint); margin-bottom: 0.25rem; }
  .app-empty__title { font-size: var(--app-t-lg); font-weight: 600; color: var(--ink-fg); }
  .app-empty__body  { max-width: 26rem; font-size: var(--app-t-sm); color: var(--ink-faint); }
  .app-empty__action { margin-top: 0.75rem; }

  .app-skeleton {
    background: var(--app-well);
    border-radius: var(--app-r-sm);
    animation: app-skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  }
  @keyframes app-skeleton-pulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 0.35; } }
}
```

Every empty state has three parts: what is missing, why, and one action. Skeletons mirror the
real layout's geometry — same heights, same column count — so nothing shifts on load.
The shared `src/components/ui/Skeleton.js` uses `--app-field-bg-hover`, and
`SettingsWorkspaceSkeleton` mirrors the current settings card, row, and action geometry.
Utility loading states must use that shared composition rather than preserving legacy page
shapes.

### 9.11 Component contract summary

| Component | File | Current dashboard status |
| --- | --- | --- |
| `Button` | `dashboard/ui/GlowButton.js` | Implemented as `.app-btn*`; legacy name retained |
| `Card` | `dashboard/ui/DashboardCard.js` | Implemented; includes `stage` composition variant |
| `Stat` | `dashboard/ui/StatCard.js` | Implemented on app and semantic data tokens |
| `Table` | `dashboard/ui/GlowTable.js` | Implemented on hairline/table tokens; legacy name retained |
| `Badge` | `dashboard/ui/GlowBadge.js` | Implemented as `.app-badge*`; legacy name retained |
| `Selector` / `Segment` | `dashboard/ui/DashboardComposition.js`, `.app-selector`, `.app-segment` | One geometry for route navigation, Documentation tabs, Exchange, modes, and filters; state semantics vary by modifier |
| `Banner` | `.app-banner`, `DemoModeBanner.js` | Base primitive implemented; broader consolidation remains |
| `Atmosphere` | `dashboard/ui/DashboardAtmosphere.js`, `.app-shell` | Static shell wash; compatibility wrapper paints nothing |
| `Utility composition` | `settings/SettingsComposition.js`, `.app-settings-*` | `SettingsSection`, `SettingsRows`, `SettingsRow`, `SettingsNotice`, `SettingsEmptyState`, and `SettingsSwitch` implemented across the active utility scope |
| `Charts` | `chart/theme.js`, `chart/prsprTheme.js` | Shared series/theme implemented across audited dashboard charts |
| `Field`, overlay, empty, pagination | `.app-control`, `.app-popover`, `.app-empty`, `.app-icon-btn` | Implemented for the utility workspace; toast remains outside this scoped contract |
| `Skeleton` | `ui/Skeleton.js`, `ui/skeletons/SettingsWorkspaceSkeleton.js` | Current tokens and utility composition implemented |

---

## 10. Data display

### 10.1 Formatting

| Type | Format | Example |
| --- | --- | --- |
| Fiat | Locale grouped, 2dp, symbol prefixed | `$12,480.55` |
| Fiat, large | Abbreviate ≥ 1e6 with full value in `title` | `$1.24M` |
| Crypto | Asset-appropriate precision, ticker suffixed | `0.04182 BTC` |
| Percentage | 2dp, explicit sign on deltas | `+3.42%` |
| Delta | Sign + arrow glyph, colored | `↑ +3.42%` |
| Timestamp | `DD MMM YYYY · HH:mm` in user tz, ISO in `title` | `04 Aug 2026 · 14:22` |
| Relative time | Under 24h only, then absolute | `12m ago` |
| Address / hash | First 6 + last 4, mono, copy affordance | `0x8f21…3ac9` |
| Empty value | Em dash at `--ink-faint`, never `0` or blank | `—` |

### 10.2 Sign and color

- Color alone never conveys direction. Always pair with a sign (`+` / `−`) or arrow, so the
  meaning survives color blindness and greyscale printing.
- Zero and unchanged values are `--ink-faint`, not green.
- Colored numbers appear only where the number *changed*. A static balance is `--ink-fg`, not
  green because it is a positive quantity.

### 10.3 Balance privacy

`HideBalanceToggle` replaces digits with `••••••` at the same mono width so nothing reflows.
It must cover every element carrying a fiat or crypto value — stat tiles, table cells, chart
axis labels, and the topbar figure.

---

## 11. Charts

### 11.1 Shared theme

`src/components/chart/theme.js` is the shared export used by Recharts and SciChart. Dashboard
home, portfolio, rewards, strategies, documentation, and Token Center consume `chartTheme`
and/or `chartSeries` instead of defining local brand palettes.

```js
export const chartTheme = {
  grid:       'rgba(255, 255, 255, 0.05)',   // landing --evidence-grid
  axis:       'rgba(166, 166, 166, 0.12)',   // landing --evidence-rule
  axisInk:    '#a6a6a6',
  tooltipBg:  'rgba(18, 18, 20, 0.94)',      // landing --evidence-reading-band
  tooltipLine:'rgba(166, 166, 166, 0.12)',
  ink:        '#f2f2f2',
  inkMuted:   '#cacaca',
  positive:   '#4ade80',
  negative:   '#f87171',
  font:       'var(--font-mono)',
  fontSize:   11,
}

// Primary series, comparison series, then brand-derived categoricals.
export const chartSeries = [
  '#ff931c', // action / primary series
  '#7fa3ff', // comparison / benchmark
  '#c4a882', // suede warm
  '#4ade80', // positive
  '#9a7048', // suede chestnut
  '#a6a6a6', // storm grey
  '#ffb14d', // orange soft
  '#2a65d8', // electric blue
]
```

The categorical palette is drawn from the brand's own suede and midnight families rather than
a generic spectrum. Purple/pink/cyan allocation palettes do not belong in Prospr product UI.

### 11.2 Recharts conventions

```jsx
<CartesianGrid stroke={chartTheme.grid} strokeDasharray="0" vertical={false} />
<XAxis
  axisLine={{ stroke: chartTheme.axis }}
  tickLine={false}
  tick={{ fill: chartTheme.axisInk, fontSize: chartTheme.fontSize, fontFamily: chartTheme.font }}
  dy={8}
/>
<YAxis
  axisLine={false}
  tickLine={false}
  tick={{ fill: chartTheme.axisInk, fontSize: chartTheme.fontSize, fontFamily: chartTheme.font }}
  width={56}
/>
<Area
  type="monotone"
  stroke={chartSeries[0]}
  strokeWidth={2}
  fill="url(#gradPrimary)"
  dot={false}
  activeDot={{ r: 3, strokeWidth: 0 }}
/>
```

- Horizontal grid lines only. Vertical grid lines add noise without adding readability.
- Area fills are a gradient from the series color at `0.18` alpha to `0` — the landing's
  hero trajectory treatment. Never a solid fill.
- No dots on line series; `activeDot` `r: 3` only.
- Axis and tick labels are mono at 11px.
- Legends are custom-rendered as `.app-badge--neutral` chips, not Recharts' default.

### 11.3 Tooltips

One tooltip design across Recharts, SciChart, and any bespoke SVG:

```
background   rgba(18, 18, 20, 0.94)
border       1px solid rgba(166, 166, 166, 0.12)
radius       var(--app-r-md)
shadow       var(--app-e-raised)
padding      0.5rem 0.75rem
label        --app-t-micro, mono, uppercase, 0.08em, --ink-subtle
value        --app-t-sm, mono, tabular-nums, --ink-fg
```

`src/components/ui/charts/CustomTooltip.js` still uses legacy Tailwind palette utilities and is
outside the certified dashboard-component sweep. Retoken it before treating shared chart
tooltips as fully migrated.

### 11.4 SciChart

`src/components/chart/prsprTheme.js` reads the same theme roles:

```js
export const UP_COLOR   = '#4ade80'   // was #2EBD85
export const DOWN_COLOR = '#f87171'   // was #F6465D

theme.lineSeriesColor   = '#ff931c'
theme.mountainLineColor = '#ff931c'
theme.mountainAreaBrush = 'rgba(255, 147, 28, 0.12)'
theme.sciChartBackground = '#121214'   // was TradingView #131722
theme.majorGridLineBrush = 'rgba(255, 255, 255, 0.05)'
theme.minorGridLineBrush = 'transparent'
theme.tickTextBrush      = '#a6a6a6'
theme.axisBorder         = 'rgba(166, 166, 166, 0.12)'
```

SciChart tooltip and axis values use the shared mono/data treatment.

---

## 12. Accessibility gates

Every gate below blocks merge.

| Gate | Requirement |
| --- | --- |
| Contrast | Body text 4.5:1, large text and UI 3:1, against the *actual* surface. §4.3 is the reference |
| Focus | `2px solid var(--orange)` at `3px` offset on every interactive element. Never `outline: none` without a replacement |
| Target size | Minimum 32×32px; 44×44px on touch viewports (`< 768px`) |
| Keyboard | Full operation without a mouse. Tab order follows visual order. Modals trap and restore focus |
| Semantics | Real `<button>`, `<a>`, `<table>`, `<th scope>`. No `<div onClick>` |
| Live regions | Async results announce via `role="status"`; errors via `role="alert"` |
| Motion | Every animation has a `prefers-reduced-motion` path |
| Color independence | Direction and status never conveyed by hue alone (§10.2) |
| Overflow | No horizontal scroll at 1440, 1024, 768, 390, 360 |
| Labels | Every input has a `<label>` or `aria-label`; placeholders are not labels |

---

## 13. Implementation status and migration record

### Certified dashboard scope

| Layer | Status | Evidence |
| --- | --- | --- |
| Tokens and typography | Complete for `/dashboard` | Neutral Midnight stack, Jakarta UI, mono/tabular data, semantic ink and status ramps in `global.css` |
| Core primitives | Complete for audited dashboard use | Controls, badges, cards, stats, tables, banners, and no-paint atmosphere compatibility |
| Composition primitives | Implemented | `DashboardPage`, `DashboardPageHeader`, `DashboardWorkspaceHeader`, `DashboardWorkspaceNav`, `DashboardSelector`, `DashboardSection`, `DashboardGrid`, `DashboardToolbar`, `DashboardStage`, and `DashboardChromeGroup` |
| Shell and containment | Complete for active routes | 50/50 active dashboard page components use shared containment and dashboard surfaces |
| Account/Settings route composition | Complete | 9/9 active settings pages use `SettingsWorkspaceHeader` and `SettingsComposition` on current tokens |
| Utilities | Complete for sidebar scope | 18/18 active pages: Investor, seven Token Center routes, nine Account/Settings routes, and Documentation; redirects remain excluded |
| Composition parity | Implemented for anchors and migrated examples | Stage/chrome on home and selected Guided leads; standard, route-family, workspace, and flow recipes are live on migrated routes |
| Charts | Complete for audited high-traffic routes | Shared `chartTheme` / `chartSeries`; Token Center purple/pink palette removed |
| Exchange / DOM | Intentionally specialized | Compact terminal profile is implemented; dense spacing and semantic buy/sell green/red remain while 12–14px operational roles and touch geometry stay shared |
| Redirects and re-exports | Not UI surfaces | 11 entries require no `.app-page` wrapper |
| `/dashboard/layout-map` and demo-pack | Excluded | Internal/reference surfaces were deliberately left alone |
| `/settings` | Certified | Headers, interiors, controls, loading states, notices, fields, and semantic actions use the shared utility composition |
| `/admin-dashboard` | Not certified | Admin surfaces remain a separate system migration |

### Old to new: 26 July to 8 August 2026

| Area | Old dashboard | Current dashboard |
| --- | --- | --- |
| Ground | Blue-cast `#0E0F14` / `#16181F` panels | Neutral Midnight `#0A0A0C` → `#1A1A1F` surface stack |
| Depth | Glow borders, orange blooms, floating atmosphere | Tone steps, alpha hairlines, static shell wash, soft black shadow |
| Actions | Gradient/glow buttons and success-green CTAs | `.app-btn*`; orange commit, neutral standard/quiet, semantic destructive treatment |
| Status | `GlowBadge` paint and raw Tailwind palette colors | `.app-badge*` with neutral/positive/negative/warning/info/action roles |
| Page rhythm | Route-specific gutters and repeated padding utilities | `.app-page`, `.app-grid`, `.app-stack`, and shared responsive spacing |
| Route-family headers | Detached route pills, duplicate title rows, and a shell-level blue tab strip | `DashboardWorkspaceHeader` + shared selector navigation; title-led hierarchy and one action-tint route state |
| Utility interiors | Gradient section headings, glow states, ad hoc rows, page-local toggles, and nested cards | `SettingsSection`, `SettingsRow`, `SettingsNotice`, `SettingsSwitch`, shared empty/loading states, and consequence-aware actions |
| Leads | Flat grids of equally weighted cards | One stage/chrome focal object on selected high-traffic pages |
| Filters | Brand-gradient or colored selected states | Neutral `.app-segment` controls with restrained active state |
| Charts | Per-page hex colors and generic rainbow categoricals | Shared `chartTheme` / `chartSeries` from Prospr brand and data roles |
| Data color | Emerald/rose/white/gray utilities | `--data-positive`, `--data-negative`, and the four-level ink ramp |
| Exchange | General dashboard spacing and decorative treatment | Dense operator terminal; conventional buy/sell fills retained by intent |

### Change sequence

- **2026-07-26 · `81354de`:** refreshed landing design system, brand assets, and marketing chrome.
- **2026-08-01 · `dd9bf46`:** added this authenticated-application specification while the
  dashboard still lagged behind the landing.
- **2026-08-03 · `1371cf0`:** shipped the app token foundation, neutralized glow primitives,
  added shared chart roles, and established dashboard-home stage/chrome composition.
- **2026-08-03 · `b6d1895`:** refreshed the canonical Prospr mark, favicon set, and header
  lockup metrics without changing the interface color roles.
- **2026-08-07 · current parity pass:** completed the user-facing dashboard primitive/page
  sweep, normalized active-route containment, added stage composition to the selected leads,
  moved audited charts to shared series, and retained terminal density where workflow requires.

### Remaining cleanup, not parity blockers

- Retoken `src/components/ui/charts/CustomTooltip.js`.
- Remove the legacy jQuery/Bootstrap/AOS chain only in a separate behavior-tested task.
- Extend screenshot coverage from the anchor/migrated route matrix to every secondary route.
- Certify `/settings` and `/admin-dashboard` independently; do not infer completion from shared
  tokens alone.

### Verification contract

Run `npm run build` and `BASE_URL=http://localhost:3000 npm run test:dashboard-home`. Inspect
`/dashboard?demo=1` plus representative Guided/Pro routes at 1440, 768, and 390 pixels. Source
greps are regression evidence; they do not replace rendered composition and overflow checks.

---

## 14. Review checklist

Paste into the PR template for any UI change in the app.

**Tokens**
- [ ] No hex literal outside `global.css` and `chart/theme.js`
- [ ] No Tailwind palette color (`emerald-400`, `gray-700`, …)
- [ ] No `text-white`

**Hierarchy**
- [ ] Exactly one focal object (Law 6)
- [ ] Exactly one orange fill (Law 2) and at most one pill (Law 5)
- [ ] No card inside a card; nested content uses `.app-well`
- [ ] At most one `--app-e-card` shadow in the viewport

**Surface and shape**
- [ ] Surfaces come from the five levels in §3.1
- [ ] Radius from the §7.1 assignment
- [ ] Every border is a 1px alpha hairline
- [ ] No chromatic `box-shadow`

**Type**
- [ ] Body is 14px Plus Jakarta
- [ ] Every comparable number is mono + `tabular-nums` (Law 4)
- [ ] Eyebrows and table headers are mono, uppercase, `0.08em`
- [ ] No gradient text, no Syne
- [ ] No raw pixel text-size utilities in dashboard product UI; use `--app-t-*` roles

**Motion**
- [ ] Every transition ≤ 280ms and on an interactive element
- [ ] No hover lift, no glow fade, no ambient loop
- [ ] Reduced-motion path exists

**Data**
- [ ] Numeric table columns right-aligned and mono
- [ ] Deltas carry a sign or arrow, not color alone
- [ ] Empty values render `—`
- [ ] `HideBalanceToggle` covers every new value

**Accessibility**
- [ ] Contrast checked against the real surface
- [ ] Visible focus ring
- [ ] Targets ≥ 32px (44px on touch)
- [ ] Keyboard-complete; modal traps and restores focus
- [ ] No horizontal overflow at 1440 / 1024 / 768 / 390 / 360

**Compliance**
- [ ] Prototype / illustrative / planned / simulated labels present and using `.app-label-state`
- [ ] No new claim, figure, or capability introduced
- [ ] Canonical logo assets only
- [ ] No new dependency

---

## 15. Decisions and follow-up

### 15.1 Orange-button ink — decided

The product rule is dark `--on-orange` (`#0c0c0e`) on Bright Orange. It measures 8.8:1 and is
implemented by `.app-btn--commit` and the rendered brand index. White on `#ff931c` measures
2.22:1 and is not an acceptable application-control treatment. The landing's legacy
`.btn--primary { color: #fff; }` remains a known implementation follow-up; it does not redefine
the product rule.

### 15.2 Green and red pair — decided

Use `--data-positive: #4ade80` and `--data-negative: #f87171` for UI status, P/L, chart
direction, and exchange buy/sell semantics. Do not introduce separate emerald/rose Tailwind
families. Revisit only if thin candle testing demonstrates a measurable legibility problem.

### 15.3 Bootstrap and jQuery — follow-up

`Layout.js` lines 106–161 load jQuery, Bootstrap, AOS, and roughly ten legacy scripts inherited
from the original template. Bootstrap's base CSS competes with the token system and is a
recurring source of unexplained spacing and color. Removing it is a larger behavior change than
the design sweep and requires its own dependency audit and regression pass.

### 15.4 Broader application parity — follow-up

Shared tokens already reach other authenticated surfaces, but `/settings` and
`/admin-dashboard` are not certified by the dashboard sweep. Audit them independently rather
than extending the 50/50 dashboard result by assumption.

---

## Appendix — file map

| File | Current role |
| --- | --- |
| `public/brand/BRAND.md` | Logo, canonical hex, brand rules |
| `public/brand/VISUAL-DIRECTION.md` | Visual philosophy, hierarchy law, landing choreography |
| `public/brand/DESIGN-SYSTEM.md` | This document — app implementation spec |
| `src/components/public/landing/styles/foundation.css` | Origin for shared brand roles; keep equivalent values synchronized |
| `src/app/global.css` | App token source + `@layer components` primitives |
| `src/components/dashboard/ui/` | React primitive layer |
| `src/components/chart/theme.js` | Shared chart theme and categorical series |
| `src/components/layout/dashboard/` | App shell |

When a token needs to change, change it in `foundation.css` and `global.css` together, and note
the pair in the PR description. Divergence between those two files is how the app drifted the
first time.
