diff --git a/.claude/skills/nothing-design/SKILL.md b/.claude/skills/nothing-design/SKILL.md
new file mode 100644
index 00000000..32f7f421
--- /dev/null
+++ b/.claude/skills/nothing-design/SKILL.md
@@ -0,0 +1,177 @@
+---
+name: nothing-design
+description: This skill should be used when the user explicitly says "Nothing style", "Nothing design", "/nothing-design", or directly asks to use/apply the Nothing design system. NEVER trigger automatically for generic UI or design tasks.
+version: 3.0.0
+allowed-tools: [Read, Write, Edit, Glob, Grep]
+---
+
+# Nothing-Inspired UI/UX Design System
+
+A senior product designer's toolkit trained in Swiss typography, industrial design (Braun, Teenage Engineering), and modern interface craft. Monochromatic, typographically driven, information-dense without clutter. Dark and light mode with equal rigor.
+
+**Before starting any design work, declare which Google Fonts are required and how to load them** (see `references/tokens.md` Section 1). Never assume fonts are already available.
+
+---
+
+## 1. DESIGN PHILOSOPHY
+
+- **Subtract, don't add.** Every element must earn its pixel. Default to removal.
+- **Structure is ornament.** Expose the grid, the data, the hierarchy itself.
+- **Monochrome is the canvas.** Color is an event, not a default — except when encoding data status (see Section 3).
+- **Type does the heavy lifting.** Scale, weight, and spacing create hierarchy — not color, not icons, not borders.
+- **Both modes are first-class.** Dark mode: OLED black. Light mode: warm off-white. Neither is "derived" — both get full design attention. Ask the user which mode to start with.
+- **Industrial warmth.** Technical and precise, but never cold. A human hand should be felt.
+
+---
+
+## 2. CRAFT RULES — HOW TO COMPOSE
+
+### 2.1 Visual Hierarchy: The Three-Layer Rule
+
+Every screen has exactly **three layers of importance.** Not two, not five. Three.
+
+| Layer | What | How |
+|-------|------|-----|
+| **Primary** | The ONE thing the user sees first. A number, a headline, a state. | Doto or Space Grotesk at display size. `--text-display`. 48–96px breathing room. |
+| **Secondary** | Supporting context. Labels, descriptions, related data. | Space Grotesk at body/subheading. `--text-primary`. Grouped tight (8–16px) to the primary. |
+| **Tertiary** | Metadata, navigation, system info. Visible but never competing. | Space Mono at caption/label. `--text-secondary` or `--text-disabled`. ALL CAPS. Pushed to edges or bottom. |
+
+**The test:** Squint at the screen. Can you still tell what's most important? If two things compete, one needs to shrink, fade, or move.
+
+**Common mistake:** Making everything "secondary." Evenly-sized elements with even spacing = visual flatness. Be brave — make the primary absurdly large and the tertiary absurdly small. The contrast IS the hierarchy.
+
+### 2.2 Font Discipline
+
+Per screen, use maximum:
+- **2 font families** (Space Grotesk + Space Mono. Doto only for hero moments.)
+- **3 font sizes** (one large, one medium, one small)
+- **2 font weights** (Regular + one other — usually Light or Medium, rarely Bold)
+
+Think of it as a budget. Every additional size/weight costs visual coherence. Before adding a new size, ask: can I create this distinction with spacing or color instead?
+
+| Decision | Size | Weight | Color |
+|----------|:---:|:---:|:---:|
+| Heading vs. body | Yes | No | No |
+| Label vs. value | No | No | Yes |
+| Active vs. inactive nav | No | No | Yes |
+| Hero number vs. unit | Yes | No | No |
+| Section title vs. content | Yes | Optional | No |
+
+**Rule of thumb:** If reaching for a new font-size, it's probably a spacing problem. Add distance instead.
+
+### 2.3 Spacing as Meaning
+
+Spacing is the primary tool for communicating relationships.
+
+```
+Tight (4–8px) = "These belong together" (icon + label, number + unit)
+Medium (16px) = "Same group, different items" (list items, form fields)
+Wide (32–48px) = "New group starts here" (section breaks)
+Vast (64–96px) = "This is a new context" (hero to content, major divisions)
+```
+
+**If a divider line is needed, the spacing is probably wrong.** Dividers are a symptom of insufficient spacing contrast. Use them only in data-dense lists where items are structurally identical.
+
+### 2.4 Container Strategy (prefer top)
+
+1. **Spacing alone** (proximity groups items)
+2. A single divider line
+3. A subtle border outline
+4. A surface card with background change
+
+Each step down adds visual weight. Use the lightest tool that works. Never box the most important element — let it float on the background.
+
+### 2.5 Color as Hierarchy
+
+In a monochrome system, the gray scale IS the hierarchy. Max 4 levels per screen:
+
+```
+--text-display (100%) → Hero numbers. One per screen.
+--text-primary (90%) → Body text, primary content.
+--text-secondary (60%) → Labels, captions, metadata.
+--text-disabled (40%) → Disabled, timestamps, hints.
+```
+
+**Red (#D71921) is not part of the hierarchy.** It's an interrupt — "look HERE, NOW." If nothing is urgent, no red on the screen.
+
+**Data status colors** (success green, warning amber, accent red) are exempt from the "one accent" rule when encoding data values. Apply color to the **value itself**, not labels or row backgrounds. See `references/tokens.md` for the full color system.
+
+### 2.6 Consistency vs. Variance
+
+**Be consistent in:** Font families, label treatment (always Space Mono ALL CAPS), spacing rhythm, color roles, component shapes, alignment.
+
+**Break the pattern in exactly ONE place per screen:** An oversized number, a circular widget among rectangles, a red accent among grays, a Doto headline, a vast gap where everything else is tight.
+
+This single break IS the design. Without it: sterile grid. With more than one: visual chaos.
+
+### 2.7 Compositional Balance
+
+**Asymmetry > symmetry.** Centered layouts feel generic. Favor deliberately unbalanced composition:
+- **Large left, small right:** Hero metric + metadata stack.
+- **Top-heavy:** Big headline near top, sparse content below.
+- **Edge-anchored:** Important elements pinned to screen edges, negative space in center.
+
+Balance heavy elements with more empty space, not with more heavy elements.
+
+### 2.8 The Nothing Vibe
+
+1. **Confidence through emptiness.** Large uninterrupted background areas. Resist filling space.
+2. **Precision in the small things.** Letter-spacing, exact gray values, 4px gaps. Micro-decisions compound into craft.
+3. **Data as beauty.** `36GB/s` in Space Mono at 48px IS the visual. No illustrations needed.
+4. **Mechanical honesty.** Controls look like controls. A toggle = physical switch. A gauge = instrument.
+5. **One moment of surprise.** A dot-matrix headline. A circular widget. A red dot. Restraint makes the one expressive moment powerful.
+6. **Percussive, not fluid.** Imagine UI sounds: click not swoosh, tick not chime. Design transitions that feel mechanical and precise.
+
+### 2.9 Visual Variety in Data-Dense Screens
+
+When 3+ data sections appear on one screen, vary the visual form:
+
+| Form | Best for | Weight |
+|------|----------|--------|
+| Hero number (large Doto/Space Mono) | Single key metric | Heavy — use once |
+| Segmented progress bar | Progress toward goal | Medium |
+| Concentric rings / arcs | Multiple related percentages | Medium |
+| Inline compact bar | Secondary metrics in rows | Light |
+| Number-only with status color | Values without proportion | Lightest |
+| Sparkline | Trends over time | Medium |
+| Stat row (label + value) | Simple data points | Light |
+
+Lead section → heaviest treatment. Secondary → different form. Tertiary → lightest. The FORM varies, the VOICE stays the same.
+
+---
+
+## 3. ANTI-PATTERNS — WHAT TO NEVER DO
+
+- No gradients in UI chrome
+- No shadows. No blur. Flat surfaces, border separation.
+- No skeleton loading screens. Use `[LOADING...]` text or segmented spinner.
+- No toast popups. Use inline status text: `[SAVED]`, `[ERROR: ...]`
+- No sad-face illustrations, cute mascots, or multi-paragraph empty states
+- No zebra striping in tables
+- No filled icons, multi-color icons, or emoji as UI
+- No parallax, scroll-jacking, or gratuitous animation
+- No spring/bounce easing. Use subtle ease-out only.
+- No border-radius > 16px on cards. Buttons are pill (999px) or technical (4–8px).
+- Data visualization: differentiate with **opacity** (100%/60%/30%) or **pattern** (solid/striped/dotted) before introducing color.
+
+---
+
+## 4. WORKFLOW
+
+1. **Declare fonts** — tell the user which Google Fonts to load (see `references/tokens.md`)
+2. **Ask mode** — dark or light? Neither is default.
+3. **Sketch hierarchy** — identify the 3 layers before writing any code
+4. **Compose** — apply craft rules (Sections 2.1–2.9)
+5. **Check tokens** — consult `references/tokens.md` for exact values
+6. **Build components** — consult `references/components.md` for patterns
+7. **Adapt to platform** — consult `references/platform-mapping.md` for output conventions
+
+---
+
+## 5. REFERENCE FILES
+
+For detailed token values, component specs, and platform-specific guidance:
+
+- **`references/tokens.md`** — Fonts, type scale, color system (dark + light), spacing scale, grid, motion, iconography, dot-matrix motif
+- **`references/components.md`** — Cards, buttons, inputs, lists, tables, nav, tags, segmented controls, progress bars, charts, widgets, overlays, state patterns
+- **`references/platform-mapping.md`** — HTML/CSS, SwiftUI, React/Tailwind, Paper output conventions
diff --git a/.claude/skills/nothing-design/references/components.md b/.claude/skills/nothing-design/references/components.md
new file mode 100644
index 00000000..d25768db
--- /dev/null
+++ b/.claude/skills/nothing-design/references/components.md
@@ -0,0 +1,153 @@
+# Nothing Design System — Components
+
+## 1. CARDS / SURFACES
+
+- Background: `--surface` or `--surface-raised`
+- Border: `1px solid --border`, or none. Radius: 12–16px cards, 8px compact, 4px technical
+- Padding: 16–24px. No shadows. Flat surfaces, border separation.
+
+---
+
+## 2. BUTTONS
+
+| Variant | Background | Border | Text | Radius |
+|---------|-----------|--------|------|--------|
+| Primary | `--text-display` (#FFF) | none | `--black` | 999px (pill) |
+| Secondary | transparent | `1px solid --border-visible` | `--text-primary` | 999px |
+| Ghost | transparent | none | `--text-secondary` | 0 |
+| Destructive | transparent | `1px solid --accent` | `--accent` | 999px |
+
+All buttons: `Space Mono`, 13px, ALL CAPS, letter-spacing 0.06em, padding 12px 24px. Min height 44px.
+
+---
+
+## 3. INPUTS
+
+- Underline preferred (`1px solid --border-visible` bottom) or full border 8px radius
+- Label above: `--label` style (Space Mono, ALL CAPS, `--text-secondary`)
+- Focus: border → `--text-primary`. Error: border → `--accent`, message below in `--accent`
+- Data-entry fields: `Space Mono` for input text
+
+---
+
+## 4. LISTS / DATA ROWS
+
+- Dividers: `1px solid --border`, full-width. Row padding: 12–16px vertical
+- Left: label (Space Mono caps, `--text-secondary`). Right: value (`--text-primary`)
+- Never alternating row backgrounds. Use dividers.
+
+**Stat rows:** Label left (Space Mono, ALL CAPS, `--text-secondary`), value right (color = status color), unit adjacent in `--label` size. Trend arrow same color as value.
+
+**Hierarchical rows:** Sub-items indented 16–24px, same divider treatment. No tree lines or expand/collapse — indentation IS the hierarchy.
+
+---
+
+## 5. TABLES / DATA GRIDS
+
+- Header: `--label` style, bottom border `--border-visible`
+- Cell text: `Space Mono` numeric, `Space Grotesk` text. Cell padding: 12px 16px
+- Numbers right, text left. No zebra striping, no cell backgrounds.
+- Active row: `--surface-raised` background, left `2px solid --accent` indicator
+
+---
+
+## 6. NAVIGATION
+
+- Bottom bar mobile, horizontal text bar desktop
+- Labels: Space Mono, ALL CAPS. Active: `--text-display` + dot/underline. Inactive: `--text-disabled`
+- Bracket `[ HOME ] GALLERY INFO` or pipe `HOME | GALLERY | INFO`
+- **Back button:** Circular 40–44px, `--surface` bg, thin chevron `<`, top-left 16px from edges
+
+---
+
+## 7. TAGS / CHIPS
+
+- Border: `1px solid --border-visible`, no fill. Text: Space Mono, `--caption`, ALL CAPS
+- Radius: 999px (pill) or 4px (technical). Padding: 4px 12px. Active: `--text-display` border+text
+
+---
+
+## 8. SEGMENTED CONTROL
+
+- Container: `1px solid --border-visible`, pill or 8px rounded
+- Active: `--text-display` bg, `--black` text (inverted). Inactive: transparent, `--text-secondary`
+- Text: Space Mono, ALL CAPS, `--label` size. Height: 36–44px. Transition: 200ms ease-out
+- Max 2–4 segments
+
+---
+
+## 9. DATE / PERIOD NAVIGATION
+
+- Layout: `< LABEL >` — back arrow, label, forward arrow
+- Label: Space Mono/Grotesk, ALL CAPS. Arrows: thin chevrons, `--text-secondary`, 44px touch
+- No calendar popovers — linear stepping IS the interaction
+
+---
+
+## 10. TOGGLES / SWITCHES
+
+- Pill track, circle thumb. Off: `--border-visible` track, `--text-disabled` thumb
+- On: `--text-display` track, `--black` thumb. Min touch target: 44px
+
+---
+
+## 11. SEGMENTED PROGRESS BARS
+
+The signature data visualization. Discrete blocks — mechanical, instrument-like.
+
+**Anatomy:** Label + value above, full-width bar of discrete rectangular segments with 2px gaps below.
+
+**Segments:** Square-ended blocks, no border-radius. Filled = solid status color. Empty = `--border` (dark) / `#E0E0E0` (light).
+
+| State | Fill | When |
+|-------|------|------|
+| Neutral | `--text-display` | Within normal range |
+| Over limit | `--accent` | Exceeds target |
+| Good | `--success` | Healthy range |
+| Moderate | `--warning` | Caution zone |
+
+**Overflow:** Filled segments continue past "full" mark in status color (typically red).
+
+**Sizes:** Hero 16–20px, Standard 8–12px, Compact 4–6px height.
+
+Always pair with numeric readout. Bar = proportion, number = precision.
+
+---
+
+## 12. OTHER DATA VISUALIZATION
+
+- **Bar charts:** Vertical, white fill, `--border` remainder. Square ends.
+- **Gauges:** Thin stroke circles + tick marks, numeric readout centered/adjacent.
+- **Dot grids:** Vary opacity/size for heat maps. Uniform spacing.
+- **Category differentiation:** Opacity → pattern → line style → color (last resort).
+- Always show numeric value alongside any visual.
+
+**Charts:** Line 1.5–2px `--text-display`, average dashed 1px `--text-secondary`. Axis labels: Space Mono, `--caption`. Grid: `--border`, horizontal only. No area fill, no legend boxes — label lines directly.
+
+---
+
+## 13. WIDGETS (DASHBOARD CARDS)
+
+- `--surface` bg, 16px radius. Hero metric: large Doto/Space Mono, left-aligned
+- Unit: `--label` size, adjacent. Category: ALL CAPS Space Mono top-left
+- Instrument gauges: compass, thermometer, dial motifs
+
+---
+
+## 14. OVERLAYS & LAYERING
+
+No shadows. Layering through background contrast and borders.
+
+- **Modals:** Backdrop `rgba(0,0,0,0.8)`, dialog `--surface` + `1px solid --border-visible` + 16px radius, centered max 480px. Close: `[ X ]` top-right ghost button.
+- **Bottom sheets:** `--surface`, 2px handle bar centered, 16px top radius, drag-to-dismiss. Full-page sheets: title centered + dismiss button right, sections with `--text-secondary` headings.
+- **Dropdowns:** `--surface-raised`, `1px solid --border-visible` 8px radius, 44px items. Selected: left 2px accent bar. No shadow.
+- **Toasts:** None. Use inline status text: `[SAVED]`, `[ERROR: ...]`. Space Mono, `--caption`, near trigger.
+
+---
+
+## 15. STATE PATTERNS
+
+- **Error:** Input border → `--accent` + message below. Form-level: summary box `1px solid --accent`. Inline: `[ERROR]` prefix. Never red backgrounds or alert banners.
+- **Empty:** Centered, 96px+ padding. Headline `--text-secondary`, 1 sentence description `--text-disabled`. Optional dot-matrix illustration. No mascots.
+- **Loading:** Segmented spinner (hardware-style), or segmented bar + percentage. No skeletons — use `[LOADING]` bracket text.
+- **Disabled:** Opacity 0.4 or `--text-disabled`. Borders fade to `--border`.
diff --git a/.claude/skills/nothing-design/references/platform-mapping.md b/.claude/skills/nothing-design/references/platform-mapping.md
new file mode 100644
index 00000000..5d5201ab
--- /dev/null
+++ b/.claude/skills/nothing-design/references/platform-mapping.md
@@ -0,0 +1,64 @@
+# Nothing Design System — Platform Mapping
+
+## 1. HTML / CSS / WEB
+
+Load fonts via Google Fonts `` or `@import`. Use CSS custom properties, `rem` for type, `px` for spacing/borders. Dark/light via `prefers-color-scheme` or class toggle.
+
+```css
+:root {
+ --black: #000000;
+ --surface: #111111;
+ --surface-raised: #1A1A1A;
+ --border: #222222;
+ --border-visible: #333333;
+ --text-disabled: #666666;
+ --text-secondary: #999999;
+ --text-primary: #E8E8E8;
+ --text-display: #FFFFFF;
+ --accent: #D71921;
+ --accent-subtle: rgba(215,25,33,0.15);
+ --success: #4A9E5C;
+ --warning: #D4A843;
+ --interactive: #5B9BF6;
+ --space-xs: 4px;
+ --space-sm: 8px;
+ --space-md: 16px;
+ --space-lg: 24px;
+ --space-xl: 32px;
+ --space-2xl: 48px;
+ --space-3xl: 64px;
+ --space-4xl: 96px;
+}
+```
+
+---
+
+## 2. SWIFTUI / iOS
+
+Register fonts in Info.plist, bundle `.ttf` files. Use `@Environment(\.colorScheme)` for mode switching.
+
+```swift
+extension Color {
+ static let ndBlack = Color(hex: "000000")
+ static let ndSurface = Color(hex: "111111")
+ static let ndSurfaceRaised = Color(hex: "1A1A1A")
+ static let ndBorder = Color(hex: "222222")
+ static let ndBorderVisible = Color(hex: "333333")
+ static let ndTextDisabled = Color(hex: "666666")
+ static let ndTextSecondary = Color(hex: "999999")
+ static let ndTextPrimary = Color(hex: "E8E8E8")
+ static let ndTextDisplay = Color.white
+ static let ndAccent = Color(hex: "D71921")
+ static let ndSuccess = Color(hex: "4A9E5C")
+ static let ndWarning = Color(hex: "D4A843")
+ static let ndInteractive = Color(hex: "5B9BF6")
+}
+```
+
+Light mode values in tokens.md Dark/Light table. Derive Font extension from font stack table (trivial: `.custom("Doto"/"SpaceGrotesk-Regular"/"SpaceMono-Regular", size:)`).
+
+---
+
+## 3. PAPER (DESIGN TOOL)
+
+Use `get_font_family_info` to verify fonts before writing styles. Direct hex values (no CSS variables). Dark mode as default canvas, light mode as separate artboard.
diff --git a/.claude/skills/nothing-design/references/tokens.md b/.claude/skills/nothing-design/references/tokens.md
new file mode 100644
index 00000000..38578adc
--- /dev/null
+++ b/.claude/skills/nothing-design/references/tokens.md
@@ -0,0 +1,142 @@
+# Nothing Design System — Tokens
+
+## 1. TYPOGRAPHY
+
+### Font Stack
+
+| Role | Font | Fallback | Weight |
+|------|------|----------|--------|
+| **Display** | `"Doto"` | `"Space Mono", monospace` | 400–700, variable dot-size |
+| **Body / UI** | `"Space Grotesk"` | `"DM Sans", system-ui, sans-serif` | Light 300, Regular 400, Medium 500, Bold 700 |
+| **Data / Labels** | `"Space Mono"` | `"JetBrains Mono", "SF Mono", monospace` | Regular 400, Bold 700 |
+
+**Why these fonts:** Doto = variable dot-matrix (closest to NDot 57). Space Grotesk + Space Mono by Colophon Foundry — same foundry as Nothing's actual typefaces. Shared design DNA.
+
+### Type Scale
+
+| Token | Size | Line Height | Letter Spacing | Use |
+|-------|------|-------------|----------------|-----|
+| `--display-xl` | 72px | 1.0 | -0.03em | Hero numbers, time displays |
+| `--display-lg` | 48px | 1.05 | -0.02em | Section heroes, percentages |
+| `--display-md` | 36px | 1.1 | -0.02em | Page titles |
+| `--heading` | 24px | 1.2 | -0.01em | Section headings |
+| `--subheading` | 18px | 1.3 | 0 | Subsections |
+| `--body` | 16px | 1.5 | 0 | Body text |
+| `--body-sm` | 14px | 1.5 | 0.01em | Secondary body |
+| `--caption` | 12px | 1.4 | 0.04em | Timestamps, footnotes |
+| `--label` | 11px | 1.2 | 0.08em | ALL CAPS monospace labels |
+
+### Typographic Rules
+
+- **Doto:** 36px+ only, tight tracking, never for body text
+- **Labels:** Always Space Mono, ALL CAPS, 0.06–0.1em spacing, 11–12px ("instrument panel" labels)
+- **Data/Numbers:** Always Space Mono. Units as `--label` size, slightly raised, adjacent
+- **Hierarchy:** display (Doto) > heading (Space Grotesk) > label (Space Mono caps) > body (Space Grotesk). Four levels max.
+
+---
+
+## 2. COLOR SYSTEM
+
+### Primary Palette (Dark Mode)
+
+| Token | Hex | Contrast on #000 | Role |
+|-------|-----|-------------------|------|
+| `--black` | `#000000` | — | Primary background (OLED) |
+| `--surface` | `#111111` | 1.3:1 | Elevated surfaces, cards |
+| `--surface-raised` | `#1A1A1A` | 1.5:1 | Secondary elevation |
+| `--border` | `#222222` | — | Subtle dividers (decorative only) |
+| `--border-visible` | `#333333` | — | Intentional borders, wireframe lines |
+| `--text-disabled` | `#666666` | 4.0:1 | Disabled text, decorative elements |
+| `--text-secondary` | `#999999` | 6.3:1 | Labels, captions, metadata |
+| `--text-primary` | `#E8E8E8` | 16.5:1 | Body text |
+| `--text-display` | `#FFFFFF` | 21:1 | Headlines, hero numbers |
+
+### Accent & Status Colors
+
+| Token | Hex | Usage |
+|-------|-----|-------|
+| `--accent` | `#D71921` | Signal light: active states, destructive, urgent. One per screen as UI element. Never decorative. |
+| `--accent-subtle` | `rgba(215,25,33,0.15)` | Accent tint backgrounds |
+| `--success` | `#4A9E5C` | Confirmed, completed, connected |
+| `--warning` | `#D4A843` | Caution, pending, degraded |
+| `--error` | `#D71921` | Shares accent red — errors ARE the accent moment |
+| `--info` | `#999999` | Uses secondary text color |
+| `--interactive` | `#007AFF` / `#5B9BF6` | Tappable text: links, picker values. Not for buttons. |
+
+**Data status colors:** `--success` = good/in range, `--warning` = moderate/attention, `--accent` = bad/over limit, `--text-primary` = neutral. Apply color to **value**, not label or background. Labels stay `--text-secondary`. Trend arrows inherit value color.
+
+### Dark / Light Mode
+
+| Token | Dark | Light |
+|-------|------|-------|
+| `--black` | `#000000` | `#F5F5F5` |
+| `--surface` | `#111111` | `#FFFFFF` |
+| `--surface-raised` | `#1A1A1A` | `#F0F0F0` |
+| `--border` | `#222222` | `#E8E8E8` |
+| `--border-visible` | `#333333` | `#CCCCCC` |
+| `--text-disabled` | `#666666` | `#999999` |
+| `--text-secondary` | `#999999` | `#666666` |
+| `--text-primary` | `#E8E8E8` | `#1A1A1A` |
+| `--text-display` | `#FFFFFF` | `#000000` |
+| `--interactive` | `#5B9BF6` | `#007AFF` |
+
+**Identical across modes:** Accent red, status colors, ALL CAPS labels, fonts, type scale, spacing, component shapes.
+
+**Dark feel:** Instrument panel in a dark room. OLED black, white data glowing.
+**Light feel:** Printed technical manual. Off-white paper (#F5F5F5), black ink. Cards = `#FFFFFF` on off-white page = subtle elevation without shadows.
+
+---
+
+## 3. SPACING
+
+### Spacing Scale (8px base)
+
+| Token | Value | Use |
+|-------|-------|-----|
+| `--space-2xs` | 2px | Optical adjustments only |
+| `--space-xs` | 4px | Icon-to-label gaps, tight padding |
+| `--space-sm` | 8px | Component internal spacing |
+| `--space-md` | 16px | Standard padding, element gaps |
+| `--space-lg` | 24px | Group separation |
+| `--space-xl` | 32px | Section margins |
+| `--space-2xl` | 48px | Major section breaks |
+| `--space-3xl` | 64px | Page-level vertical rhythm |
+| `--space-4xl` | 96px | Hero breathing room |
+
+---
+
+## 4. MOTION & INTERACTION
+
+- **Duration:** 150–250ms micro, 300–400ms transitions
+- **Easing:** `cubic-bezier(0.25, 0.1, 0.25, 1)` — subtle ease-out. No spring/bounce.
+- Prefer opacity over position. Elements fade, don't slide.
+- Hover: border/text brightens. No scale, no shadows.
+- No parallax, scroll-jacking, gratuitous animation.
+
+---
+
+## 5. ICONOGRAPHY
+
+- Monoline, 1.5px stroke, no fill. 24x24 base, 20x20 live area. Round caps/joins.
+- Color inherits text color. Max 5–6 strokes.
+- Preferred: Lucide (thin), Phosphor (thin). Never filled or multi-color.
+
+---
+
+## 6. DOT-MATRIX MOTIF
+
+**When to use:** Hero typography (Doto), decorative grid backgrounds, dot-grid data viz, loading indicators, empty state illustrations.
+
+### CSS Implementation
+```css
+.dot-grid {
+ background-image: radial-gradient(circle, var(--border-visible) 1px, transparent 1px);
+ background-size: 16px 16px;
+}
+.dot-grid-subtle {
+ background-image: radial-gradient(circle, var(--border) 0.5px, transparent 0.5px);
+ background-size: 12px 12px;
+}
+```
+
+Dots 1–2px, uniform 12–16px grid. Opacity 0.1–0.2 for backgrounds, full for data. Never as container border or button style.
diff --git a/specs/028-nothing-design-redesign/README.md b/specs/028-nothing-design-redesign/README.md
new file mode 100644
index 00000000..850bd21e
--- /dev/null
+++ b/specs/028-nothing-design-redesign/README.md
@@ -0,0 +1,109 @@
+# 028 — Nothing Design Redesign
+
+## Overview
+
+This spec proposes replacing the AI Developer Hub's current, inconsistent theming with one
+coherent visual system inspired by Nothing's instrument-panel / printed-manual aesthetic. The
+existing UI leans on a green-primary shadcn theme (`--primary: oklch(0.78 0.19 120)`) applied
+unevenly across screens, with mixed type, ad-hoc accent usage, and shadow-heavy surfaces.
+
+**Goal:** a single source of truth — `mockups/styles/nothing.css` — that styles every screen with
+the same monochrome canvas, one disciplined red interrupt, a type-driven hierarchy, and flat
+bordered surfaces. Both dark and light are first-class, not an afterthought. Dark reads as an
+instrument panel; light reads as a printed manual.
+
+The `mockups/` folder contains a standalone, framework-free HTML mockup per screen plus a gallery
+index, all sharing `nothing.css`. These are static design references — the eventual implementation
+maps these tokens onto the app's real `src/app/globals.css`.
+
+## Font declaration
+
+Three families, each with one job. Maximum two families visible per screen plus a single Doto hero
+moment.
+
+| Role | Family | Usage |
+| --- | --- | --- |
+| **Display / hero** | **Doto** | Hero display numbers and titles only, 36px+. |
+| **UI / body** | **Space Grotesk** | All interface chrome, headings, and prose. |
+| **Labels + all data** | **Space Mono** | ALL-CAPS tracked labels, plus every number / metric / table value. |
+
+Exact `@import` line used at the top of `mockups/styles/nothing.css`:
+
+```css
+@import url('https://fonts.googleapis.com/css2?family=Doto:wght@400;500;700&family=Space+Grotesk:wght@300;400;500;700&family=Space+Mono:wght@400;700&display=swap');
+```
+
+These are exposed as tokens:
+
+```css
+--font-display: "Doto", "Space Mono", monospace;
+--font-ui: "Space Grotesk", "DM Sans", system-ui, sans-serif;
+--font-mono: "Space Mono", "JetBrains Mono", "SF Mono", monospace;
+```
+
+## How to view
+
+Open **`mockups/index.html`** in a browser. It is a self-contained gallery — no build step, no
+server. Each card links to a sibling screen mockup. Use the Dark / Light toggle (top-right of every
+page) to verify both modes; the choice persists across pages via `localStorage`.
+
+## Screens
+
+| # | Screen | Route | Mockup |
+| --- | --- | --- | --- |
+| 01 | Dashboard | `/` | `mockups/dashboard.html` |
+| 02 | AI Tools | `/tools` | `mockups/tools.html` |
+| 03 | Users | `/users` | `mockups/users.html` |
+| 04 | License Assignments | `/assignments` | `mockups/assignments.html` |
+| 05 | Access Requests | `/requests` | `mockups/requests.html` |
+| 06 | Budget | `/budget` | `mockups/budget.html` |
+| 07 | Reports | `/reports` | `mockups/reports.html` |
+| 08 | GitHub Copilot | `/copilot` | `mockups/copilot.html` |
+| 09 | Claude Console | `/claude` | `mockups/claude.html` |
+| 10 | Invoices | `/invoices` | `mockups/invoices.html` |
+| 11 | Settings | `/settings` | `mockups/settings.html` |
+| 12 | Sign In | `/login` | `mockups/login.html` |
+
+## Nothing principles applied
+
+- **Monochrome canvas + one red interrupt.** Greyscale carries the entire hierarchy. The `--accent`
+ red (`#d71921`) appears at most once per screen — an active state, a destructive action, or the
+ single urgent number. If nothing is urgent, there is no red.
+- **Type-driven 3-layer hierarchy.** Exactly three layers per screen: one large primary (hero number
+ or title), secondary context, and small tertiary metadata. The size contrast *is* the hierarchy —
+ no boxes or color needed to separate them.
+- **Flat bordered surfaces / no shadows.** Cards and panels are defined by 1px borders and spacing,
+ never drop shadows, gradients, or blur. Status colors apply to value text only, never to row
+ backgrounds or labels.
+- **Segmented-bar data viz.** The signature visualization is a segmented progress bar (~20 discrete
+ segments) rather than smooth fills; overflow segments turn red. Charts are thin inline SVG lines,
+ labeled directly with no legend boxes or area fills.
+- **Both modes first-class.** Every value is a token — no hardcoded `#fff` / `#000`. Flipping the
+ toggle yields two intentional looks: dark = instrument panel, light = printed manual.
+
+## Migration notes
+
+`mockups/styles/nothing.css` is authored as the proposed replacement for the app's
+`src/app/globals.css` design tokens. The Nothing tokens map directly onto the existing shadcn token
+names, so adoption is largely a values swap (plus wiring the three font families and switching the
+dark-mode selector). The headline change is retiring the **oklch green primary**
+(`oklch(0.78 0.19 120)`) in favor of a monochrome canvas with a single red interrupt.
+
+Key token swaps (current `globals.css` → proposed Nothing equivalent):
+
+| shadcn token | Current value | Nothing equivalent (dark / light) |
+| --- | --- | --- |
+| `--background` | `oklch(0.16 0 0)` dark · `oklch(1 0 0)` light | `--black` `#000000` / `#f5f5f5` |
+| `--foreground` | `oklch(0.97 0 0)` dark · `oklch(0.145 0 0)` light | `--text-primary` `#e8e8e8` / `#1a1a1a` (display text → `--text-display` `#ffffff` / `#000000`) |
+| `--card` / `--popover` | `oklch(0.21 0 0)` dark · `oklch(1 0 0)` light | `--surface` `#111111` / `#ffffff` (raised → `--surface-raised` `#1a1a1a` / `#f0f0f0`) |
+| `--primary` | `oklch(0.78 0.19 120)` **green** (both modes) | `--text-display` (monochrome) — primary actions become high-contrast greyscale, **not** a brand hue |
+| `--accent` | `oklch(0.25 0.03 120)` / `oklch(0.95 0.03 120)` green-tinted | `--accent` `#d71921` red interrupt (subtle fill → `--accent-subtle`) |
+| `--destructive` | `oklch(0.704 0.191 22.216)` / `oklch(0.577 0.245 27.325)` | `--error` `#d71921` (unified with the single accent red) |
+| `--border` / `--input` | `oklch(1 0 0 / 10%)` dark · `oklch(0.91 0 0)` light | `--border` `#222222` / `#e8e8e8` (visible → `--border-visible` `#333333` / `#cccccc`) |
+| `--ring` | `oklch(0.78 0.19 120)` green | `--border-visible` / `--text-primary` focus, no green ring |
+| Fonts | shadcn default (system / Geist) | `--font-ui` Space Grotesk, `--font-mono` Space Mono, `--font-display` Doto (see `@import` above) |
+
+Status colors (`--success #4a9e5c`, `--warning #d4a843`) are new value-text-only tokens with no
+direct shadcn equivalent; the existing rainbow `--chart-1..5` would collapse to greyscale +
+segmented bars. The app's `.dark` class selector maps to the mockup's `:root` (dark) default and
+`:root[data-theme="light"]` override pattern.
diff --git a/specs/028-nothing-design-redesign/implementation-notes.html b/specs/028-nothing-design-redesign/implementation-notes.html
new file mode 100644
index 00000000..8125be93
--- /dev/null
+++ b/specs/028-nothing-design-redesign/implementation-notes.html
@@ -0,0 +1,286 @@
+
+
+
A running record of how the implementation interprets, extends, or departs from
+ spec 028 and its implementation-plan.html. Updated continuously as phases land.
+ Categories: decisions, deviations,
+ tradeoffs, open questions.
Choices made where the spec or plan left room for interpretation.
+
+
+ Decision
+ P0shadcn --accent stays NEUTRAL; the Nothing red routes through --destructive
+
The plan's flagged naming collision: shadcn's --accent is a subtle hover background used
+ all over (bg-accent, data-[state]:bg-accent), NOT the loud Nothing red.
+ So --accent is mapped to a neutral surface (#f0f0f0 light / #1a1a1a
+ dark) and the single red interrupt + every error/destructive state route through
+ --destructive = #d71921 (also --ring). This dodges the collision
+ cleanly and means re-skinned primitives keep their hover semantics. New tokens shadcn lacks were added
+ and registered in @theme inline: --color-ink (100% display text),
+ --color-faint (40% tertiary), --color-success, --color-warning,
+ --color-seg-empty, --color-destructive-subtle, --color-border-strong.
+
+
+
+ Decision
+ P0Direct per-mode token assignment (two blocks), not a raw-token+alias indirection
+
Values are assigned straight onto the shadcn var names per mode — light on :root, dark on
+ .dark — mirroring the file's original two-block shape. This keeps the diff legible and the
+ theme mechanism identical to before (next-themes attribute="class", defaultTheme
+ and system retained). --radius drops to 0.5rem (technical scale);
+ cards get an explicit 14px in P1. The color vocabulary for downstream phases:
+ text-ink (hero/headings, 100%), text-foreground (body, 90%),
+ text-muted-foreground (labels, 60%), text-faint (tertiary, 40%),
+ bg-primary/text-primary-foreground (greyscale fill),
+ text/border/bg-destructive (the one red), text-success/text-warning
+ (value text only).
+
+
+
+ Decision
+ P0next/font variable names are font-specific, not --font-sans directly
+
To avoid a circular --font-sans: var(--font-sans) in Tailwind v4's @theme inline,
+ next/font injects --font-space-grotesk / --font-space-mono / --font-doto,
+ and @theme inline chains --font-sans/--font-mono/--font-display
+ to those (with fallbacks). The mockup vocabulary alias --font-ui is also defined
+ (= var(--font-sans)) so any ported Nothing class keeps resolving.
+
+
+
+ Decision
+ P0Nothing component classes become React components, not global CSS classes
+
The mockups use generic class names (.card, .grid, .label,
+ .table, .row). Dumping those into globals.css would collide with
+ Tailwind utilities (notably .grid) and arbitrary classNames. Instead the app
+ keeps using re-skinned shadcn primitives (Button, Card, Badge, …) plus a small set of new, clearly-named
+ React components for Nothing-specific idioms (<SegmentedBar>, <Metric>,
+ <StatusText>, <LoadingState>, <PeriodNav>). Only
+ non-colliding type/utility helpers are added globally. This is the idiomatic React/Tailwind path and
+ keeps the mockup look without the collision risk.
+
+
+
+ Decision
+ P3Stacked bar charts: cap at top-4 + "Other" with a monotonic grey ramp + separators
+
Greyscale has only ~4–5 perceptually-distinct steps, so the Claude console's stacked daily-spend
+ charts (by workspace and by user) were illegible once my P0 swap replaced their spectral palette with
+ greys — 8–9 stacked grey segments blur together. Fix (in response to review feedback): fold all but the
+ top 4 ranks into an aggregated "Other" (≤5 segments), colour them in rank order
+ along a single monotonic ramp --chart-1…--chart-5 (no repeats, so the legend reads as a
+ dark→light gradient), and draw a 1px --card separator stroke on each
+ segment so adjacent tones keep crisp edges. Full per-row precision stays in the tooltip + the
+ workspace/user list below. The "Use workspace colors" toggle still switches to per-workspace hues for
+ users who need to track many specific workspaces. Ranked (non-stacked) bars like "Top 10 users" keep the
+ grey ramp as-is — separated, axis-labelled bars don't suffer the adjacency problem.
+
+
+
+
+
+
02
Deviations
+
Intentional departures from the spec/plan, with rationale.
+
+
+ Deviation
+ P2Sonner kept as a flattened transitional toast; full 183-site removal staged
+
The plan bans toasts and removes Sonner, replacing all ~183 toast.* sites with inline
+ <StatusText>. The inline primitive + useInlineStatus hook are built and
+ adopted on migrated surfaces. But removing the global <Toaster/> while 183 call sites
+ remain turns them into silent no-ops (lost success/error + lost screen-reader announcements) — a
+ regression that can't be safely verified across 41 files in one pass. So the global Toaster stays
+ mounted, re-skinned to the Nothing look (flat, no shadow, Space Mono, monochrome
+ surface, status color on text only, red errors), and toast→inline conversion proceeds incrementally on
+ key forms. Net: feedback never breaks and looks on-system; finishing the long tail of conversions +
+ deleting Sonner is tracked as remaining work (P5 grep gate will still flag residual toast.*).
+
+
+
+ Deviation
+ P2Simplified shell: fixed rail + mobile drawer, no collapsible/Cmd+B/cookie
+
The plan keeps the shadcn SidebarProvider machinery (collapsible rail, Cmd+B, cookie
+ persistence, mobile Sheet) and re-skins it. I replaced it with a plain CSS-grid .app +
+ a self-contained responsive sidebar: a fixed 248px desktop rail and a lightweight mobile drawer
+ (state + backdrop). The Nothing design has a fixed rail (no collapse), so the collapsible/keyboard/cookie
+ features aren't part of the target; dropping them removes a dependency on the large sidebar.tsx
+ and keeps the shell legible. sidebar.tsx remains in the repo (unused by the shell).
+ Nav is text-only (Lucide row icons dropped) to match the mockups' text nav.
+
+
+
+ Deviation
+ P0Light-mode status colors darkened for WCAG, unlike the mockups
+
The mockups (and nothing.css) use the same --success #4a9e5c /
+ --warning #d4a843 in both modes. As value text on the light canvas those fail the
+ plan's own P5 a11y gate (≥4.5:1): amber #d4a843 on #f5f5f5 is ~1.9:1 and the
+ green is ~3:1. Since the spec mandates status colors be value-text-only AND demands ≥4.5:1, I kept the
+ spec values in dark mode (they pass on black) and darkened the light-mode values to
+ --success #2e7d32 and --warning #835f00. Trade: light-mode warning reads
+ more olive/brown than the mockup amber; revert if you prefer literal mockup hues over the a11y gate.
+
Measured (verified in-browser P5): light — foreground 15.96, destructive 4.76,
+ success 4.70, warning 5.35 on the #f5f5f5 canvas (all ≥4.5 ✓; warning was bumped from
+ #946c00=4.37 to #835f00=5.35 to clear the canvas). dark — foreground 17.14,
+ success 6.34, warning 9.48 (✓). The brand red #d71921 is 4.05 on #000 / 3.64 on
+ #111: it clears WCAG AA for graphical/UI (≥3:1) and large text, and matches the value the spec's
+ own contrast table documents as accepted, but is just under the 4.5 normal-text bar on the dark canvas.
+ Per the spec's explicit "use #d71921 verbatim in both modes" brand decision, the red is kept as-is
+ (the redesign uses it mostly for borders/fills/large numbers + uppercase mono labels); Lighthouse
+ accessibility scored 98 on the audited route.
+
+
+
+
+
+
03
Tradeoffs
+
Alternatives considered and why the chosen path won.
+
+
+ Tradeoff
+ P1Parallel agent fan-out to re-skin primitives vs hand-editing all 22
+
The 4 defining primitives (button/badge/card/input) + the net-new shared helpers were hand-built for
+ control; the remaining 18 were re-skinned by one agent each in a single parallel workflow (each owns one
+ file, so no write conflicts), against a shared "Nothing primitive contract" + per-file spec. Faster and
+ consistent, at the cost of a review pass — caught by the build/lint gate and a browser spot-check.
+
+
+
+ Tradeoff
+ P3Re-theme Recharts in place vs rebuilding every chart as CSS/SVG
+
Per the plan's P3 gate: re-theme the shared ui/chart.tsx wrapper once (mono axes, no legend
+ boxes, shadowless tooltip, monochrome ramp) and keep Recharts for line/bar (preserves
+ tooltips/responsiveness/accessibility), rebuilding only the idioms Nothing forbids (donut → segmented
+ proportion; area fill → line). The signature segmented bar becomes a reusable React
+ <SegmentedBar> replacing the continuous SpendProgressBar / multi-marker bar.
+
+
+
+
+
+
04
Open questions
+
Things worth a confirm or a later revision.
+
+
+ Open question
+ "One red per screen" on dense admin screens
+
The admin dashboard legitimately surfaces several reds at once: the active-nav bar, the budget-at-risk
+ card (border + icon + tag), and failed-invoice dots in the activity timeline. The plan's mitigation is
+ to demote the active-nav / active-row indicator to neutral --ink when an error/at-risk
+ state is present on that view. Confirm you want that auto-demotion logic wired globally, or whether a
+ per-screen judgement (e.g. dashboard keeps the budget red as the one moment, nav goes neutral) is
+ enough. Currently the nav active bar is always red.
+
+
+
+ Open question
+ Theme toggle placement — sidebar footer vs page topbar
+
The mockups put the Dark/Light toggle in each page's top-right action area. Because the 40 routes
+ already render their own page headers (no shared Topbar contract was force-adopted), I placed the
+ segmented toggle in the sidebar footer instead (always visible, no per-page change). If you want it in
+ the topbar to match the mockups exactly, that requires a shared <Topbar> the pages
+ adopt — a larger P4 change.
+
+
+
+
+
+
05
Remaining work (not yet done)
+
What this pass delivered: the full design system (P0 tokens+fonts, P1 every primitive/overlay,
+ P2 shell, P3 shared tables + chart wrapper + segmented bar + loaders + monochrome charts), verified in a
+ browser on an isolated Neon branch in both modes. Because the app is built on shared components, the
+ token + primitive + shell + table/chart re-skin already propagates the Nothing look to essentially every
+ route. The items below are the scoped follow-ups that remain.
+
+
Page-level literal color sweep — DONE. The 23-file workflow first hit the account
+ session limit; on re-run it completed. All scattered literal Tailwind tints now map to value-text-only
+ Nothing tokens (text-emerald/amber/green-* → text-success/text-warning,
+ bg-amber-500 fills → bg-warning, green success callouts → token borders on
+ transparent bg, chart hex series → greyscale chart tokens), routine month-over-month deltas neutralized,
+ and the last animate-pulse placeholders → <LoadingState>. Anti-pattern
+ grep gate now: 0 literal rainbow classes, 0animate-pulse,
+ 0<Skeleton>, 0shadow-* on ui primitives.
+
Full toast removal — DONE. All 38 toast.* components converted to inline
+ <StatusText>/useInlineStatus (success-on-navigate dropped, errors kept
+ inline, aria-live preserved); the global <Toaster/> was removed and
+ sonner.tsx deleted. Zero toast.* / sonner imports remain.
+
Mono-caps form labels — DONE.FormLabel now renders the Space Mono
+ ALL-CAPS instrument label across every RHF form (login shows EMAIL/PASSWORD); plain <Label>
+ stays sentence-case for inline checkbox/switch labels.
+
Native confirm() → modal — DONE. Both call sites (request cancel,
+ ingestion-filter delete) now use the Nothing AlertDialog. Zero confirm() remain.
+
Light-mode contrast / a11y — DONE. Ratios verified in-browser (see the status-color
+ deviation note); Lighthouse accessibility 98. The dark-mode brand red sits at the spec's documented
+ 4.05:1 (graphical/large-text AA) by deliberate brand decision.
+
Still open (low value / deferred): the two single-date pickers still use the
+ Popover + react-day-picker calendar — but it was re-skinned to monochrome tokens in P1, so it's a
+ documented-acceptable exception rather than an anti-pattern; converting to a Space-Mono date input is
+ optional polish. Hero Doto display moments and the per-screen "one red" auto-demotion
+ (see Open questions) are the remaining purely-aesthetic refinements.
+
+
+
+
+
AI Developer Hub · Spec 028 — Nothing Design Redesign · Implementation Notes ·
+ Styled with the Nothing design system (tokens only). Dark default; flip the toggle top-right.
Migrate the AI Developer Hub from a stock shadcn/ui oklch system to one coherent
+ Nothing design system — monochrome canvas, a single red interrupt, Space Grotesk /
+ Space Mono / Doto, flat surfaces, inline status text. This document is itself styled with
+ nothing.css: we eat our own dog food.
+
+
+
+
+ 00
+
Overview & goals
+
+
+
The redesign is a presentation-layer migration of ~40 routes and 14 subsystems
+ to the Nothing design language. No server actions, data shapes, or business logic change. The
+ dominant cost drivers are the toast removal (~183 toast.* call sites), the
+ ~20 Recharts components, and the page-by-page Card→.card /
+ status→.tag / form→.field conversion across every route.
+
+
Why migrate
+
+
One coherent, intentional system in both dark and light, instead of stock shadcn defaults that drift per screen.
+
Instrument-panel hierarchy: greyscale carries the structure; color is reserved as an interrupt, not decoration.
+
Remove accessibility/UX anti-patterns already proven present in the codebase: toast-only feedback, skeleton shimmer, red alert banners, calendar popovers, multi-hue charts.
+
Retire the drifting green: the app's lime --primaryoklch(0.78 0.19 120) becomes greyscale fills, and the single Nothing interrupt reuses the red the app already ships as --destructive — so the accent isn't a new color, just a re-scoping of existing tokens.
+
+
+
Current state
+
+
Tokens: oklch shadcn "new-york" semantic vars in src/app/globals.css; light is the :root default, dark is the .dark override via next-themes attribute="class", defaultTheme="system".
+
Accent:--primary = green/chartreuse, reused as --ring, --chart-1, --sidebar-primary. --destructive is a separate red.
+
Font: Inter via next/font/google applied globally to <body>; no monospace data font, no display font.
+
Feedback: Sonner <Toaster/> mounted in layout; toasts everywhere. Skeleton loaders in 15 loading.tsx route files. A destructive shadcn alert banner in the global layout. shadcn Sidebar shell with Lucide row icons + filled active background.
+
+
+
Target state
+
+
One Nothing token layer in globals.css, re-keyed onto the existing :root(light)/.dark(dark) selectors so next-themes keeps working unchanged. shadcn semantic var names stay aliased to Nothing tokens so no primitive loses color.
+
Three-family type stack (Doto / Space Grotesk / Space Mono) via next/font; ALL-CAPS Space Mono labels, Space Mono numbers, Doto for 36px+ hero numbers only.
+
CSS-grid .app shell, text-nav sidebar with a 2px red active bar, topbar eyebrow + title + actions, inline [SAVED]/[ERROR] status, [LOADING…], segmented bars and direct-labeled charts.
+
+
+
+
How to read this doc & the mockups
+
The 12 mockups demonstrate layout and hierarchy and render the final accent:
+ the stock Nothing red#d71921, linked from nothing.css
+ verbatim. The brand accent was evaluated — Unic lime #A4C400 was considered
+ and rejected (see §01) — and the decision is to keep Nothing red, so
+ the mockups already show the target. Each section links the mockup(s) that map to the screens
+ it covers.
The token layer is the foundational dependency for every other subsystem. The
+ migration re-keys the Nothing value sets onto the app's existing :root(light) /
+ .dark(dark) selectors — do not copy nothing.css's
+ :root[data-theme="light"] selectors verbatim. Keep the next-themes class strategy.
+
+
Token migration map
+
Every current oklch shadcn token re-points to a Nothing token (kept aliased so
+ bg-background, text-muted-foreground, etc. keep resolving).
+
+
+
+
+
+
Current shadcn token (oklch)
+
Nothing token
+
Notes
+
+
+
+
--background
var(--black)
Canvas. Dark #000 / light #F5F5F5.
+
--foreground
var(--text-primary)
Body text. #e8e8e8 dark / #1a1a1a light.
+
--card / --popover
var(--surface) / var(--surface-raised)
Cards → surface; dropdowns/popovers → surface-raised. No translucency (bg-card/60 dropped).
+
--card-foreground / --popover-foreground
var(--text-primary)
Plus add --text-display for titles/strong cells (shadcn lacks this).
+
--primary
var(--text-display) (fills) + --accent for the one interrupt
Primary fills become greyscale white/black per Nothing; red is reserved for the single per-screen interrupt. --primary-foreground → --black.
+
--secondary
var(--surface-raised) / transparent + 1px border
Collapse shadcn secondary(fill)+outline onto one bordered .btn--secondary.
+
--muted / --muted-foreground
var(--surface) / var(--text-secondary)
Muted backgrounds become neutral surfaces; muted text → secondary.
+
--accent (shadcn hover tint)
var(--surface-raised) (neutral)
Naming collision: shadcn --accent is a subtle hover bg, NOT the Nothing loud interrupt. Keep bg-accent neutral; reserve the Nothing red interrupt under a deliberately-scoped token. Audit all bg-accent/data-[state]:bg-accent usages.
+
--destructive
var(--accent) = #D71921
Unified with --accent — errors ARE the red accent (Nothing default). A --danger alias = --accent is kept only for readable code naming.
+
--border / --input
var(--border) / var(--border-visible)
--border for dividers; --border-visible for interactive edges/inputs.
+
--ring
var(--accent) (both modes)
Focus moves from 3px ring to border-color brighten; red passes in both modes, so no per-mode variant.
+
--chart-1
var(--text-display)
chart-1 == primary today; re-derive as monochrome top series. Red reserved for over/active.
+
--chart-2..5
greys via opacity ramp (--text-secondary, --border-visible)
Drop the multi-hue palette + duplicated 8-color spectral FALLBACK_PALETTE. var(--destructive) chart usages → the --accent red.
Cap card radius at 14px (≤16px rule); pills for buttons/tags.
+
--sidebar-* (primary/accent/...)
Nothing nav tokens
--sidebar-primary == primary today → active nav becomes --text-display text + 2px --accent left bar.
+
+
+
+
+
Brand accent decision confirmed: Nothing red
+
The single interrupt stays the stock Nothing red #D71921 — the
+ mockups already render it. Error and destructive states share this same red, per the Nothing
+ default "errors ARE the accent moment": no separate danger color, no per-mode variant.
+
+
+
--accent#D71921 · the one interrupt (also errors)
+
--success#4A9E5C · in-range data
+
--warning#D4A843 · attention data
+
+
+
+
--accent = #D71921 is the single interrupt: active nav indicator (2px left bar), the one emphasis/active-state moment per screen, segmented-bar "over/active", table active-row rail, selected dropdown/command item left bar, brand dot, error/destructive surfaces, ::selection (with --black text). Hold the rule: at most ONE red moment per screen.
+
Errors share the accent:btn--destructive, .tag--accent, .field.is-error, .error-msg, .status-text--err and the active-tab underline all resolve to the same --accent red — nothing.css verbatim, nothing to decouple. (A --danger alias = --accent may be kept purely for readable code naming.)
+
Rejected alternative — Unic lime #A4C400: brand-aligned and matches the app's current green --primary, but it would force a light-mode a11y workaround (lime fails ~2:1 on #F5F5F5, needing a darkened ~#4F6800 for any accent-as-text/border) and a danger-red decoupling so delete/error states don't read as lime. Rejected in favor of one color and one contrast story.
+
+
+
Contrast — red passes both modes
+
#D71921 needs no per-mode variant: it clears WCAG as text and as a graphical
+ element on both canvases, and as a fill it carries contrast through white/black text. There is
+ exactly ONE accent color to verify.
+
+
+
+ As text / thin border — pass
+
+
#D71921 on #F5F5F5 (light)~4.76:1 · PASS
+
#D71921 on #000 (dark)~4.05:1 · PASS
+
#D71921 on #111 surface≥3:1 · graphical OK
+
+
+
+ As fill — text carries it
+
+
#FFF text on #D71921 fill~5.2:1 · PASS
+
fill vs canvas (graphical)≥3:1 · OK
+
identical dark + lightno variant
+
+
+
+
+
+
Use #D71921verbatim in both modes for text, thin borders (2px nav bar, tab underline, table rail, .tag--active), fills, and strokes — no darkened light-mode variant needed.
+
--accent-subtle stays an rgba of #D71921 (already correct in nothing.css for both modes); no recompute.
+
Because errors share the accent, P5 verifies one accent color in light mode — no separate lime/danger contrast matrix.
+
Status colors (--success / --warning) stay on value text only, never on labels or backgrounds.
+
+
+
Fonts
+
Replace Inter with the three-family Nothing stack via next/font/google
+ (self-hosted, auto fallback-metrics) — not the mockup's render-blocking
+ Google @import.
weight ['400','500','700'] (pin if variable axis unreliable), variable:'--font-display', display:'swap', preload:false
--font-display
+
+
+
+
+
Register --font-sans/--font-mono/--font-display in @theme inline (Tailwind v4, no tailwind.config) so font-sans/font-mono utilities AND the Nothing .ui/.mono/.doto classes both resolve.
+
Naming reconciliation: mockups use --font-ui; Tailwind expects --font-sans. Define both, with --font-ui: var(--font-sans). Chain tokens to the next/font-injected var name (not a raw string) to preserve fallback metrics.
+
The 28 existing font-mono call sites (18 files) re-point automatically to Space Mono once the token is wired — no call-site changes, just an audit for width regressions (count badges, KPI cards, table columns).
+
Transactional email (src/emails/invite-email.tsx) cannot use next/font — document the gap; recommend it stays system-font.
+
+
+
Dark/light strategy
+
Keep next-themes attribute="class" with the .dark class and the
+ three states light/dark/system. Re-key Nothing's dark values onto .dark
+ and light values onto plain :root. The segmented theme toggle is wired to
+ setTheme (next-themes), not the mockup's data-theme/
+ theme.js. Decision: keep system as a third segment or as the default.
+ suppressHydrationWarning already present guards FOUC; decide whether to remove
+ disableTransitionOnChange for a subtle cross-fade.
+
+
+
+
+
+ 02
+
Component migration matrix
+
+
Re-skin the 16 shadcn primitives in place (rewrite CVA/className
+ strings; keep imports) so the ~30 consuming components keep working. Mockup references:
+ tools.html
+ settings.html
+ users.html.
Error text points at --danger (= the --accent red). Logic unchanged.
+
calendar.tsx (react-day-picker popover)
For period selection → .period-nav linear stepper. Restyle DayPicker to tokens only for true arbitrary-date pickers
L
Largest semantic shift; calendar effectively deprecated for month/period navigation.
+
+
+
+
Foundational fonts/radii also flow through here: Inter→Space Grotesk/Mono/Doto;
+ --radius 0.625rem → Nothing 4/8/14/pill; sonner Toaster removal → inline
+ .status-text. All covered as cross-cutting items in §03.
+
+
+
+
+
+ 03
+
Anti-patterns to resolve
+
+
Four Nothing violations, all confirmed in real code, touch nearly every interactive
+ screen. Three of the four already have proven classes in nothing.css; the only true gap is a
+ hardware-style segmented spinner (must be authored, respecting prefers-reduced-motion).
Inline [SAVED]/[ERROR: …] via .status-text/--ok/--err near each trigger; build one shared <StatusText>/useInlineStatus with aria-live="polite"; remove the global Toaster only after the primitive exists. Bulk/multi-event outcomes get an inline summary box.
[LOADING…] via .loading-text + a NEW hardware-style segmented/dot-matrix spinner. Collapse the 15 near-identical grids to a shared <LoadingState label/>. Drop Card scaffolds.
+
M
+
+
+
Alert banner (red)
+
alert-banner.tsx (shadcn Alert variant=destructive, AlertTriangle) at layout.tsx:58; + ~8 other <Alert> consumers (api-preview, cost-tracking, sync/ingestion tables, setup/login forms)
+
Flat inline status box: 1px border (--danger critical / --warning approaching) on BORDER + value text only, never a background fill; [ X ] ghost dismiss; .stat-row utilization rows. Keep localStorage fingerprint + aria-live.
Per components.md §9 prefer linear .period-nav stepping; but these are single-date fields — recommend a Space Mono underline date input. Reserve .period-nav for genuine period browsing (dashboard/reports/claude month nav). Documented exception.
+
M
+
+
+
Status-as-floating-popover
+
error-popover.tsx (ingestion/sync tables)
+
Inline expandable .status-text--err (click-to-expand), no Popover.
+
S
+
+
+
+
+
+
+
Error-heavy subsystem — one red, watch the count
+
Error/destructive is the dominant accent use here (toast.error, Alert destructive,
+ failed outcome badges). Under the confirmed unified-red model these all resolve to the same
+ --accent #D71921 — nothing to decouple. The watch-item is quantity:
+ a dialog or row showing an active/selected state and an error can surface two reds.
+ Hold "one red region per screen" — let the error be the red moment and de-emphasize the
+ active indicator when both are present.
+
+
+
+
+
+
+ 04
+
App shell & navigation
+
+
Replace the shadcn SidebarProvider/Sidebar/SidebarInset
+ composition with the Nothing CSS-grid .app shell + sticky text-nav .sidebar.
+ The shell owns the single red accent moment per screen (active nav bar). Every mockup shares this
+ shell — see
+ dashboard.html
+ for the canonical rendered chrome.
+
+
+
+
Shell element
From (shadcn)
To (Nothing)
Effort
+
+
Root grid
SidebarProvider + Sidebar + SidebarInset (flex)
CSS-grid .app + sticky .sidebar + .main (max-width 1280px). Keep SidebarProvider context ONLY for mobile open state — re-skin, don't delete.
SidebarMenuButton + Lucide icons, "Navigation" group label
.nav > a.nav-item ALL-CAPS Space Mono; drop the group label; icons optional (decide monoline vs text-only). Role filtering + active-path logic carry over.
M
+
Active state
data-[active]:bg-sidebar-accent (filled)
.nav-item.active::before 2px left bar in --accent + text→--text-display. THE single accent moment per screen (red, both modes).
.sidebar-footer > .user-chip (.avatar initials + name/role) + a .nav-item Sign Out. Decide where "My Profile" goes (chip→/profile, or keep a shadowless Nothing dropdown).
M
+
Topbar
<header h-14 border-b> with only SidebarTrigger
.topbar > {.label.eyebrow + h1.display-md} + .topbar-actions. Needs a shared <Topbar eyebrow title actions> contract pages consume in P4.
L
+
Mobile trigger
SidebarTrigger (PanelLeftIcon)
NET-NEW: re-skinned monoline trigger at mobile breakpoint; add a @media rule to off-canvas the sidebar. Mockups are desktop-only — design/approve this.
M
+
Theme toggle
Sun/Moon DropdownMenu (Light/Dark/System)
Segmented .theme-toggle (DARK | LIGHT [| SYSTEM]) wired to next-themes setTheme. Reconcile 2-state mockup vs 3-state app.
M
+
Breadcrumb
shadcn Breadcrumb (budget only, ChevronRight)
Fold into the topbar .eyebrow as a Space Mono "SECTION / CONTEXT" trail (only 1 real consumer). Preserve aria-current / nav[aria-label=breadcrumb] if kept standalone.
The shell owns the red accent (active nav bar). Therefore in-content tab underlines and
+ progress "within-range" fills are NEUTRAL (--border-visible / --text-display)
+ — proven by the scoped overrides in settings.html, reports.html, copilot.html and claude.html.
+ This keeps "one interrupt per screen" intact.
+
+
+
+
+
+
+ 05
+
Charts & data visualization
+
+
~20 Recharts components + 2 Sparkline impls + 3 non-Recharts bar/gauge visuals,
+ all routed through one shared wrapper (ui/chart.tsx). Migrate the wrapper FIRST —
+ every chart inherits its grid/tooltip/legend/axis styling. References:
+ reports.html
+ claude.html
+ copilot.html
+ budget.html.
+
+
Recharts theming rules
+
+
Monochrome-first categories: differentiate by opacity → pattern → line-style → color (color last). Use 2–3 greys (--text-display / --text-secondary / --border-visible) + an opacity ramp. Delete the duplicated 8-color spectral FALLBACK_PALETTE; define one shared getCategoryStyle(rank).
+
Reserve red for one over/active series only. Demote it from its current default --chart-1 data role; var(--destructive) chart usages → the --accent red.
Lines: 1.5–2px --text-display current; secondary by opacity; projection/average 1px dashed --text-secondary (3 3); no dots (or one end dot); horizontal-only grid --border; direct line labels, no legend boxes.
+
No area fill, no gradients, no shadows. Remove the viewerSpendArea linearGradient; restyle the tooltip card to surface-raised + 1px border-visible + 8px radius (drop shadow-xl).
+
Axis/labels: Space Mono at caption/label size, --text-disabled/--text-secondary; set tick={{fontFamily:'var(--font-mono)'}} centrally in the wrapper.
+
Light-mode strokes: the red accent passes as a stroke/axis color in light mode (~4.76:1), so no darkened variant is needed; keep non-accent series differentiated by opacity, not hue.
+
+
+
Segmented-bar / gauge / sparkline approach
+
+
Segmented bar (signature):.seg-bar / .seg-bar--hero / .seg-bar--compact of ~20 discrete .seg; .is-filled (text-display) / .is-good / .is-warn / .is-over (accent). Build one reusable React component computing segment counts from percentages; replaces the rounded continuous SpendProgressBar, the multi-marker budget-health bar, and the per-model mini bars.
+
Gauge / proportion: the forbidden donut (copilot/activity-distribution) becomes a 4-row segmented bar or horizontal stacked proportion bar with direct .legend labels and numeric readouts (sign-off needed). Pies are banned.
+
Sparkline: standardize on ui/sparkline.tsx (already Nothing-correct: 1.5px currentColor polyline + end dot). Delete the duplicate Recharts reports/sparkline.tsx and re-point consumers.
+
Re-theme vs rebuild (P3 gate): re-theme the wrapper once and keep Recharts for line/bar (preserves tooltips/responsiveness); rebuild only the donut and area chart where Nothing forbids the idiom. Mockups render some charts as pure CSS columns/seg-bars — decide per chart family.
+
+
+
Ripple files: cumulative-pacing, twelve-month-bar, cost-distribution-histogram,
+ workspace-daily, plan-vs-actual, forecast-cumulative, daily-by-user, top-users, billing-trend,
+ cost-utilization, language, editor, usage-trend, global-metrics, spend-trend, my-usage. Verify each
+ in BOTH modes for legibility/contrast.
+
+
+
+
+
+ 06
+
Dialogs & overlays
+
+
User priority — exhaustive inventory. Nothing overlay spec (components.md §14):
+ NO shadows; layering via background contrast + 1px --border-visible; modals = backdrop
+ rgba(0,0,0,0.8) + 16px radius + ghost [ X ]; dropdowns = surface-raised +
+ 8px + 44px items + left 2px accent bar on selected; fade-only (no zoom/slide); toasts BANNED.
+
+
Primitives (6)
+
+
+
Primitive
From
Nothing pattern
Effort
+
+
dialog.tsx — Overlay
bg-black/50 + fade
Backdrop bg-black/80; fade only
S
+
dialog.tsx — Content
rounded-lg bg-background shadow-lg zoom-in-95
--surface + 1px --border-visible + 16px, NO shadow, fade only; cap ~480px but allow wide overrides
M
+
dialog.tsx — [X] close
opacity-70 icon, data-[state]:bg-accent
Bordered/ghost [ X ] top-right; remove accent tint
S
+
alert-dialog.tsx
shadow-lg, rounded-lg, bg-black/50
Same Nothing modal; AlertDialogAction destructive → .btn--destructive (--danger outline)
M
+
sheet.tsx
4 sides, w-3/4, shadow-lg, slide 300/500ms
--surface, NO shadow; right edge 1px border-visible; bottom-sheet adds 16px top radius + handle; soften slide
M
+
popover.tsx
rounded-md bg-popover shadow-md zoom+slide
--surface-raised + 1px border-visible + 8px, NO shadow, fade only
S
+
dropdown-menu.tsx
shadow-md, focus:bg-accent, items ~32px
surface-raised + 8px, no shadow, 44px items, selected = left 2px --accent bar; destructive item text --danger
M
+
command.tsx
CommandDialog p-0, data-[selected]:bg-accent
On Nothing modal; selected → left 2px accent bar + surface-raised row (not full-row tint); 44px rows; input underline border-b
users/tools/budget/assignments[/[id]]/claude-users/integrations/ingestion/tool-detail/user-detail — one primitive fix covers chrome; per-file work is destructive-item + label tweaks
M
+
ui/sonner.tsx + ~25 call sites
Global toast
Remove Toaster; per-surface inline [SAVED]/[ERROR] status with auto-clear + aria-live. Defining change of this subsystem
L
+
+
+
+
+
+
Wide-modal exception
+
Keep max-w-3xl/max-w-4xl for approval / completion / template-editor /
+ sync-results / member-sync — these are data-dense 2–3-column editors, not confirmations.
+ Document as an accepted deviation from the §14 480px modal cap.
+
+
+
+
+
+
+ 07
+
Page-by-page migration
+
+
All 40 routes + the AuthGuard access-denied gate, grouped by section, with mapped mockup, effort and notes. Subpages
+ (detail / new / import) included.
1:1 mockup map. Hero→seg-bar + Doto; at-risk = the one accent (red --danger). Shared SpendProgressBar/Timeline/KpiWithMom/WhatChanged migrate as a unit.
Hard-sequence foundation → primitives → shell → shared-data →
+ pages → QA so shared surfaces stabilize before per-page work. shadcn semantic var names stay
+ aliased throughout so primitives never lose color mid-migration.
+
+
+
P0Foundation: Decisions, Tokens & FontsL
+
Ratify the brand-accent decision, port the Nothing token layer into globals.css (re-keyed onto next-themes .dark, NOT data-theme), load the three-family font stack via next/font.
Full Nothing token set on :root(light)/.dark(dark); --accent=#D71921 single interrupt (errors share it, no decoupling; red verified ≥4.5:1 text / ≥3:1 graphical both modes); radii 4/8/14/pill; --ease. shadcn names aliased (no primitive loses color). @theme inline registers font tokens; next/font loads all three (Space Grotesk preloaded, Doto preload:false), Inter removed. Utility classes available. typecheck + build pass; one route smoke-tested both modes, no FOUC.
+
+
+
+
+
P1Primitives & Overlays Re-skinXL
+
Re-skin the 16 control primitives + 6 overlay primitives in place; build shared cross-cutting helpers (inline status-text, [LOADING], segmented spinner, Nothing dropdown/modal/sheet chrome).
All primitives Nothing-styled (pill/underline/flat/no-shadow, Space Mono caps, square greyscale checkbox/switch, border-only tags). Overlays use surface/surface-raised, 1px border-visible, 16/8px, 0.8 backdrop, NO shadow, fade-only, [X] ghost close, left 2px accent on selected. Destructive primitives point at --danger (outline). Shared <StatusText>/useInlineStatus + [LOADING]/spinner documented. Variant collapse reviewed vs call sites. Verified vs tools.html/settings.html both modes; typecheck/lint/build green.
+
+
+
+
+
P2App Shell, Sidebar, Topbar & Global LayoutXL
+
Migrate the shell so global chrome is correct and owns the one accent moment. Establish the Topbar contract. Remove global Toaster + AlertBanner from layout.tsx.
CSS-grid .app with re-skinned shadcn sidebar machinery preserved (Cmd+B, cookie, mobile Sheet/useIsMobile); ALL-CAPS nav with 2px red active bar; AI·HUB wordmark + dot; user chip + Sign Out; segmented toggle wired to next-themes (System retained). Topbar exposes eyebrow+title+actions + mobile SidebarTrigger (net-new, approved). Global Toaster removed; AlertBanner→flat bordered status box (red critical / amber approaching) keeping aria-live + dismiss. Breadcrumb folded into eyebrow. Verified vs every shared shell both modes + mobile; build green.
+
+
+
+
+
P3Shared Data Layer: Tables & ChartsXL
+
Re-skin the shared DataTable + ui/table + column header + faceted filters ONCE (cascades to ~9 tables); migrate the shared Recharts wrapper + shared viz (SpendProgressBar→seg-bar, Sparkline consolidation, Timeline, KpiWithMom, WhatChanged, budget-health-hero). Resolve re-theme-vs-rebuild.
+
+
Includes
Data tables (TanStack) & filters · Charts (Recharts) & data viz
+
Depends on
P0, P1, P2
+
Exit
Tables: label headers, --border dividers (no zebra), .num mono right, .cell-strong, hover=surface, active row=surface-raised + 2px left accent; search=.search; pager=segmented+period-nav. OutcomeBadge/status→.tag/.led (failed=red). Chart wrapper: horizontal-only grid, mono axes, shadowless tooltip, no legend boxes, monochrome ramp (opacity>pattern>color), red for one over/active series. SpendProgressBar→seg-bar; one canonical Sparkline; donut + area replacements signed off. Re-theme-vs-rebuild decided per family. Verified both modes (red strokes pass in light).
+
+
+
+
+
P4Page Migration by SectionXL
+
Migrate route content for all feature areas. Sequence by risk: Auth first (most-referenced, mocked), then Dashboards, then CRUD list/detail, then the chart-heavy financial + console screens.
All ~40 routes Nothing-correct in both modes vs mockups; viewer + setup-password composed/signed off. Every former toast is inline [SAVED]/[ERROR] with aria-live; every skeleton/loading.tsx is [LOADING]; no red row/tinted-bg statuses (value text only); exactly one accent interrupt per screen audited; native confirm() replaced; JSON viewer monochrome. Per-section typecheck/lint/build green.
+
+
+
+
+
P5Anti-pattern Sweep, A11y & Visual QA GateL
+
Final cross-cutting verification: zero orphaned toasts/skeletons/shadows/zebra/calendar-popovers; automated contrast/a11y (the single red accent in both modes); mockup-fidelity visual regression across all routes in both themes + mobile; no stale oklch leaks in edge states. Gate before merge.
+
+
Includes
Anti-pattern resolution · All subsystems (verification pass)
+
Depends on
P4
+
Exit
Lint/grep gate: zero toast.*/sonner imports, zero <Skeleton>/animate-pulse, zero shadow-* on chrome, zero calendar popovers except sanctioned exceptions. axe + Lighthouse pass; WCAG check confirms the red accent and amber/green status pass ≥4.5:1 text / ≥3:1 graphical in both modes, and no screen shows more than one red region. Visual regression matches mockups across 40 routes in dark+light+mobile. "One accent interrupt per screen" audited. Full typecheck/lint/build/test green; ready to merge as one coordinated replacement.
+
+
+
+
+
+
+
+ 09
+
Risks & mitigations
+
+
+
+
Risk
Severity
Mitigation
+
+
+
One-red-per-screen at scale — accent and error/destructive share #D71921, so a screen with BOTH an active/selected state AND an error or destructive surface can show two reds, breaking the "single interrupt" rule. Most acute in the error-heavy console/sync/dialog flows.
+
Medium
+
Make it a hard checklist item in P4/P5: exactly one red region per screen. When an error is present, IT is the red moment — render the active-nav / active-row / tab indicator in neutral --text-display on that view. The consistency sweep that cleared the mockups (neutral in-content tabs) is the template; the P5 manual audit asserts the per-route count in both modes.
+
+
+
Recharts theming effort — ~20 Recharts components + 2 sparklines must go monochrome (square bars, no area fill, no legend boxes, direct labels, mono axes, shadowless tooltips). Mockups render many as pure CSS/inline-SVG (no 1:1 map); pies/donut + area are bans needing redesign.
+
High
+
P3 gate per chart family: re-theme the shared wrapper once; keep Recharts for line/bar; rebuild only donut (→segmented/stacked-proportion) and area (→line). Shared getCategoryStyle(rank) (opacity>pattern>color); delete duplicated 8-color palettes. Verify both modes; the red accent strokes pass in light (no darkened variant).
+
+
+
Removing Sonner is app-wide behavioral — ~183 toast.* sites / 39 files are the ONLY success/error feedback (incl. multi-event "sent 12 of 15", bulk created/updated/skipped/failed). Naive removal = silent failures + lost screen-reader announcements.
+
High
+
Build ONE shared inline-status primitive (StatusText/useInlineStatus, aria-live=polite) in P1 BEFORE touching call sites; remove the global Toaster only in P2 once it exists. Per-form placement convention + inline summary-box for bulk outcomes. P5 lint/grep gate asserts zero remaining sonner imports/toast.* calls.
+
+
+
Font FOUT/CLS & loading — three families via the mockup's render-blocking @import would reintroduce FOUT/CLS + payload; Inter→Space Grotesk/Mono glyph-width changes can overflow tight UI; Doto variable axis may be unreliable in next/font.
+
Medium
+
Use next/font/google per family (self-hosted, auto fallback metrics), NOT @import; display:'swap', preload Space Grotesk only, preload:false on Doto/Space Mono. Pin Doto weights ['400','500','700'] if the axis misbehaves; validate hero rendering. Define both --font-sans and --font-ui (aliased). Screenshot-diff key routes for width overflow.
+
+
+
Reconciling theme strategy — app authors LIGHT as :root default + .dark via class; nothing.css authors DARK as :root + data-theme=light. Mis-mapping inverts colors. Also --primary==--ring==--chart-1==--sidebar-primary, so the accent change recolors buttons/rings/sidebar-active/primary-chart at once. Toggle is 2-state in mockup vs 3-state in app.
+
Medium
+
Re-key nothing.css value sets onto the existing :root(light)/.dark(dark) selectors (don't copy data-theme). Keep attribute=class + System. Change primary/ring/chart-1/sidebar-primary in one coordinated edit and verify all four surfaces together. Wire the segmented toggle to next-themes (not theme.js); suppressHydrationWarning guards FOUC.
+
+
+
Scope/regression across ~40 routes — shared blast radius: tokens (every screen), 16 primitives (every screen), shared DataTable (~9 tables), shared viz (SpendProgressBar/Timeline/KpiWithMom/WhatChanged used by dashboard AND reports AND claude). One bad shared rule regresses the whole app; cannot ship incrementally per page.
+
High
+
Hard-sequence P0→P4 so shared surfaces stabilize before page work. Keep shadcn var names aliased so primitives never lose color mid-migration. Migrate shared viz as a single unit and regression-test ALL consumers. Mockup-based visual regression from P1 onward + final P5 cross-route gate in both themes + mobile before the single-shot merge.
+
+
+
+
+
+
+
+
+
+ 10
+
Testing & verification
+
+
+
Per-phase build gates: run pnpm typecheck, pnpm lint (zero warnings) and pnpm build at the end of every phase and before merging any P4 page-section batch. A red gate is blocking; no phase advances until its predecessor is green.
+
Mockup-fidelity visual regression: use the chrome-devtools / playwright-skill to render each migrated route at desktop + mobile widths in BOTH themes and diff against the matching mockups/*.html. Build the harness in P1 (primitives vs tools.html/settings.html); run cumulatively through P4/P5.
+
Auth-gated coverage: use the agent-browser-session skill to mint a NextAuth session cookie for the seeded agent user so the browser reaches every non-public route without the /login flow. Respect the agent deny-list (no DELETE /api/users, invite, reset-password, etc.). Test login.html / setup-password directly via the public routes.
+
Automated a11y + contrast: run Lighthouse (lighthouse_audit) and axe (chrome-devtools a11y-debugging skill) on representative routes in BOTH themes, with a SPECIFIC assertion that the red accent #D71921 and the amber/green status colors meet ≥4.5:1 text / ≥3:1 graphical in both modes, and that no screen shows more than one red region. Verify focus rings/borders, tap targets (44px vs sanctioned 32px compact icon-btn), keyboard nav.
+
Anti-pattern grep/lint gate (P5): assert zero remaining from "sonner" / toast. sites, zero <Skeleton>/animate-pulse, zero shadow-* on chrome, zero zebra striping, zero calendar popovers except documented exceptions, zero stale oklch leaks in edge states (rails/disabled/focus/skeleton). Confirm exactly one accent interrupt per screen via a manual checklist.
+
Shared-component regression sweeps: after P1 and P3, re-test EVERY consumer route of each shared component (not just one) — e.g. verify the DataTable re-skin against tools/users/assignments/copilot-seats/ingestion/invoices simultaneously, and seg-bar/timeline/KPI against dashboard+reports+claude.
+
Functional behavior preservation: confirm API-key reveal/copy stays masked-by-default, RHF/Zod still fires (.field.is-error/.error-msg), TanStack sort/filter/pagination works, sync polling (30s/5s) updates rows with the new live LED, theme toggle persists to DB without FOUC, and bulk import/invite multi-event outcomes surface via the inline summary-box pattern.
+
+
+
+
+
+
+ 11
+
Rollout & effort
+
+
+
Rollout strategy
+
Execute the entire migration on a single long-lived feature branch (off main) as a
+ coordinated full replacement — the goal is a complete swap, and the shared-surface blast radius
+ (tokens, 16 primitives, shared DataTable, shared viz) makes true page-by-page production rollout
+ impossible without leaving the app half-styled. Within the branch, sequence strictly P0→P5 so
+ foundation stabilizes before consumers.
+
+
No broken middle: because next-themes is class-based and shadcn var names are aliased to Nothing tokens, author the Nothing token VALUES so the app compiles and renders at every commit. Keep the old oklch values reachable behind a build flag / scoped wrapper class until P5 to A/B against mockups and bail per-surface if a shared edit regresses.
+
No permanent dual theme: do NOT ship a user-facing "classic vs nothing" toggle (doubles maintenance, contradicts full-replacement). The parallel theme is a dev-time scaffold only, removed before merge.
+
Preview review: land the branch behind a Vercel preview so reviewers click through all ~40 routes (agent-browser-session cookie) in both themes before merge.
+
Single merge: merge to main as one squashed/coordinated PR only after the P5 gate (visual regression + a11y/contrast + grep gate + green build) passes; no interim commits to main. Per project memory, if any gh-aw workflow files are touched, recompile lock.yml in the same commit (not expected here).
+
+
+
Sequencing notes
+
P0 (tokens+fonts) is foundational for EVERYTHING and lands first; the accent decision is
+ settled (Nothing red #D71921, errors share it), so token values can be written without further sign-off.
+ P1 (primitives+overlays) precedes shell/pages because ~30 components import the primitives; the
+ shared inline-status + [LOADING] helpers are built here. P2 (shell) precedes pages
+ because it removes the global Toaster/AlertBanner, defines the Topbar contract, and owns the single
+ red active-nav accent so in-content tabs stay NEUTRAL. P3 (shared tables + chart wrapper + shared
+ viz) precedes pages because ~9 tables and 6 dashboard/reports/console screens inherit from them. P4
+ (pages) is ordered Auth → Dashboards → CRUD → financial/console (which stress P3
+ hardest). P5 is the cross-cutting cleanup + QA gate.
+
Cross-cutting decisions to lock at P0/P1 so they don't churn: accent =
+ Nothing red #D71921 as the single per-screen interrupt, shared by destructive/error (no separate danger
+ color, no darkened variant — red passes both modes); keep next-themes class strategy +
+ System; full toast removal via inline status; full skeleton removal via [LOADING];
+ period-nav vs Space-Mono date input for single-date pickers; wide-modal exception
+ (max-w-3xl/4xl) for data-dense editors; sanctioned 32px compact icon-btn in
+ dense tables. The pleasant surprise: the app ALREADY ships red as --destructive,
+ so the Nothing red accent reuses an existing token while the green --primary collapses to greyscale.
+
+
Total effort
+
+
+
P0 · FoundationL
+
P1 · Primitives + OverlaysXL
+
P2 · ShellXL
+
P3 · Shared Tables + ChartsXL
+
P4 · Page Migration (largest by volume)XL
+
P5 · QA gateL
+
TOTALXL · multi-week, focused team
+
+
+
A full-app redesign across ~40 routes and 14
+ subsystems. Dominant cost drivers: ~183 toast call sites (app-wide behavioral refactor), ~20 Recharts
+ components (re-theme/rebuild), and the page-by-page Card→.card /
+ status→.tag / form→.field conversion across every route.
+ De-risked substantially by (a) complete on-contract mockups for 12 screens + nothing.css shipping
+ nearly every needed class, and (b) the app already shipping red as --destructive, so the accent reuses an existing
+ token while the green --primary collapses to greyscale.
+ ~60% of the leverage lands from a handful of shared-surface edits (P0–P3).
+
+
+
+
AI Developer Hub · Spec 028 — Nothing Design Redesign · Implementation Plan ·
+ Styled with the Nothing design system (tokens only). Dark default; flip the toggle top-right.
+
+
+
+
diff --git a/specs/028-nothing-design-redesign/mockups/_CONTRACT.md b/specs/028-nothing-design-redesign/mockups/_CONTRACT.md
new file mode 100644
index 00000000..3be6459f
--- /dev/null
+++ b/specs/028-nothing-design-redesign/mockups/_CONTRACT.md
@@ -0,0 +1,175 @@
+# Mockup Authoring Contract — Nothing redesign
+
+Every screen mockup is a standalone `.html` file in this `mockups/` folder. It MUST link the shared
+`./styles/nothing.css` and `./styles/theme.js` and reuse the shell + component classes below verbatim.
+This file is the single source of consistency. Do not invent new color/spacing values, new fonts, or
+ad-hoc component styles. If a screen needs something truly bespoke, add a small scoped `
+
+
+