# DESIGN.md — Tidal Design System
> **Audience:** AI coding agents and engineers contributing to this repo or consuming it.
> **Purpose:** the rules that keep the system coherent. Read this before writing UI code.
---
## 0. Source of truth
1. **Figma** is the design source of truth. File: `N7zMXtdBdI71vQcBW3retD`.
2. **`/design-tokens/`** at the repo root is the canonical token export (DTCG format) — re-exported from Figma via the Tokens plugin. Versioned in git so every clone is reproducible.
3. **`packages/tokens/scripts/build.ts`** ingests `/design-tokens/` and emits everything in `packages/tokens/dist/` (CSS vars, JSON, Tailwind preset). Never edit `dist/` or `src/_generated.ts` by hand.
4. If Figma changes, re-export to `/design-tokens/` and run `pnpm tokens:build`. See `FIGMA_SYNC.md`.
---
## 0.5. Design principles
Short list of invariants. When a design-in-Figma or a judgment call conflicts with one of these, push back before you implement.
1. **A heading is never smaller than its content.** Any label, title, or heading that introduces a block must be **≥ the size of the text it introduces** — equal size at minimum, usually one or more steps larger. If you feel tempted to set a heading to `text-sm` above a body of `text-base`, the heading is wrong. Applies to card titles, popover/section headings, form-field labels, legend-style group titles, and every other "title → content" pair.
2. **Hierarchy is read top-down.** The eye lands on the largest thing first. Don't invert that with tiny titles above big bodies — it reads as a label/caption, not a heading.
3. **Default to the calmest variant.** Reach for Primary (button), elevation > 0 (card), or destructive/success tones only when the content genuinely warrants them. See rules §1 and §1a.
4. **Token before hex.** If Figma shows a value, there's a token. If there isn't, add one — never inline a hex in component code (§1, §7).
5. **Compose before extending.** If `Card`, `Input`, `InputGroup`, `Popover` exist, use them. Don't reinvent a surface with a `
` (§5).
6. **Accessibility is part of "done".** Keyboard nav, focus-visible rings, `aria-*` wiring. See §5, §6.
## 1. Token taxonomy
There are **three layers**. Always reach for the highest layer that fits.
| Layer | Where | When to use |
|---|---|---|
| **1. Primitives** | `Tailwind Colors`, `Liquid colors` (Lilac/Viola/Mandarin), `Tailwind Primitives` — exposed via `liquid` export and Tailwind palette | Almost never in components. Reference these only when defining a semantic token in Figma. |
| **2. Semantic** | `--background`, `--foreground`, `--card`, `--popover`, `--border-default`, `--border-focus`, `--input`, `--accent`, `--ring`, `--button-{primary,secondary,destructive,success,default}` (+ `-foreground`, `-hover`), `--sidebar-*`, plus gradients (`--gradient-button-*`) and rings (`--ring-{primary,destructive}`). | **Default choice for all component code.** |
| **3. Component** | Add a CSS var scoped to the component when a value doesn't generalize. | When a component has a tweak that does not generalize. |
**Specials**:
- `--background-destructive-fill-10`, `--background-success-fill-10` (and `-hover-20`) — faded backgrounds for callouts, secondary destructive surfaces.
- `--neon-green` (`#adfa1d`) — accent for special highlights only.
### Hard rules
- ❌ **Never** write a raw hex (`#cd82f0`) in component code or MDX.
- ❌ **Never** reference Tailwind palette ramps (`bg-zinc-100`) in components — go through a semantic token.
- ❌ **Never** inline `style={{ color: '...' }}` for design values.
- ✅ Use `bg-primary text-primary-foreground` (Tailwind preset) **or** `var(--primary)` in raw CSS.
- ✅ When a value is missing, **add a semantic token** rather than reaching down a layer.
---
## 2. Light & dark mode contract
- Every semantic token has a `light` and `dark` value in `packages/tokens/src/semantic.ts`.
- The `dark` mode is activated by `class="dark"` on `` (Tailwind convention).
- Components must work in both modes **without conditionals**. If you find yourself writing `dark:` overrides on a token-driven class, the token is wrong — fix the token.
- ⚠️ **Current state**: dark values in `semantic.ts` are seeded from shadcn defaults. They have NOT yet been pulled from Figma. Sync them when the dark frame is ready (see `FIGMA_SYNC.md`).
---
## 2.4. Disabled state
**One strategy: 50% opacity via class.** Every disabled control (and any descendants) dims uniformly. Applied via the twin classes `disabled:opacity-50 data-[disabled]:opacity-50` so it fires for both native `
` / ` ` and Radix Slot / `asChild` paths (which only receive the `data-disabled` attribute). For paired labels: `peer-disabled:opacity-50 peer-data-[disabled]:opacity-50`. **No inline `style`** — class-based means consumers can override via `className` (e.g. `data-[disabled]:opacity-75`).
Pair with `disabled:cursor-not-allowed` (text fields) or `disabled:pointer-events-none` (interactive surfaces). Loading states (Button/IconButton) reuse this path by setting both `disabled` and `data-disabled` when `loading` is true.
## 2.5. Text size rule
**Body, emphasis, and descriptions all share `text-base leading-6`.** The difference between them is **weight and color**, not size — so they sit on the same baseline grid and compose without size-mismatch awkwardness.
- **Body (default)** — `text-base leading-6 font-normal`. Paragraphs, prose, list items, PageHeader descriptions, article body.
- **Emphasis paragraph** — `text-base leading-6 font-medium`. Bump a single paragraph or sentence within prose. Don't use this for titles — that's what the heading scale is for.
- **Description / secondary** — `text-base leading-6 font-normal text-muted-foreground`. Same size as body, tonally quieter. Use for field descriptions, captions under content, muted metadata, source-file paths, prop-table hints.
- **Caption / metadata** — `text-xs leading-4 font-normal`. Tiny labels, timestamps, badge text. The only place smaller than body is allowed.
- **Code** — `text-sm leading-5 font-mono`. Inline code references, prop names, file paths.
If prose feels too small to read comfortably, you're probably reaching for `text-sm` where Description / secondary (`text-base` muted) is the right call — same readability as body, lower visual weight via color.
**Headings are never smaller than the content they introduce** (see §0.5 principle 1). A `text-base` body means the label above it is `text-base` or larger.
For the full canonical pairings (sizes × leading × weight × font, including headings), see [Typography → Text styles](/tokens/typography).
## 2.6. Size scale (canonical)
All interactive / control components use a **single size vocabulary**: `xs | sm | md | lg`.
- A component that needs fewer steps (e.g. Checkbox has only `md | lg`, Tabs has only `sm | md`) just implements the steps it needs — **don't invent new names** (`xss`, `xxs`, `xl-button`, etc.).
- `CardTitle` is a **type-scale primitive** with `sm | md | lg` (no `xl`). Defaults to `sm` (text-base + medium) so a card title never outranks a section's `` (text-lg). Bump to `md` / `lg` only for cards that *are* a top-level page region.
- Canonical pixel heights: **`sm` = 28px**, **`md` = 32px (default)**, **`lg` = 36px**. `md` is the workhorse; it's the size of the docs search trigger and every form control. `lg` is the presence-size for hero actions and inspector-density triggers.
- Text + icon per size: `sm` → `text-sm` + `size-3` (12px icons), `md`/`lg` → `text-base` + `size-4` (16px icons). **lg keeps text-base** — "same type, bigger box," not bigger type.
- Menu rows use `min-h-8` + `py-2` rather than fixed `h-8` so multi-line items grow naturally; single-line rows still land at 32px.
- File and number inputs render via custom layout (`` + hidden native ` `); the native rendering can't be vertically centered reliably.
## 2.8. Side panels collapse at narrow widths
Sidebars, inspectors, and any fixed-edge panel **must** collapse to an offcanvas drawer (or condense into a dropdown menu) below the `lg` breakpoint (1024px). Two side panels competing with main content on a 640px viewport is always wrong — the content area becomes unreadable and the panels become un-tappable.
- `` enforces this automatically: below `lg` it renders as a Radix Dialog drawer with backdrop, regardless of the desktop `collapsible` mode.
- For bespoke panels (not using ``), the same rule applies: hide the panel below `lg` and surface its controls via a trigger that opens an offcanvas drawer or a `` collapse.
- Never try to fit a sidebar into a mobile viewport by shrinking width — icon rails don't have room for text, offcanvas keeps both controls and content legible.
This is about behavior, not aesthetics. If you need a side panel on mobile, it slides over.
## 2.7. Reduced motion
Handled **once**, at the token layer. `packages/tokens/dist/tokens.css` ships a global
```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;
}
}
```
…which zeroes every transition and animation duration system-wide when the user has set `prefers-reduced-motion`. **Components must never gate animations on `motion-safe:` / `motion-reduce:`** — the global rule covers it. The only exception is when a component has an animation that is *structural* (e.g. collapsible content height) and must explicitly be preserved; document the exception in the component.
## 3. Typography
- **Families** (mode-aware):
- `font-sans` — **Inter** (Internal) / **Söhne** (External). Resolved via `var(--font-sans)`.
- `font-serif` — **Georgia** (system font, no download needed).
- `font-mono` — **JetBrains Mono**.
- `font-heading` — page headings and display text. Resolves to `var(--font-sans)` in Internal (Inter), `Georgia` in External. Use `font-heading` class on any heading that should switch to serif in External mode.
- **Heading weight** is mode-aware: `--font-heading-weight` is `600` (semibold) in Internal, `400` (normal) in External. Georgia reads heavier than sans at the same weight, so External dials it back. Apply via inline style `fontWeight: "var(--font-heading-weight)"`.
- **Allowed weights (only)**: `font-normal` (400), `font-medium` (500), `font-semibold` (600). Bold / extrabold / black / light / thin are deliberately **not exposed** through the Tailwind preset. If Figma ships a design that uses them, push back or translate to semibold.
- **Söhne weight files shipped**: Leicht (300, unused), Buch (400), Kräftig (500). `font-semibold` (600) has no `.otf` yet — falls back to Kräftig until you license/add the file.
- **Two size scales** (selected via CSS class on a parent):
- **Internal** (default, `:root` / `.typography-internal`) — compact app scale, Inter sans: `text-xs=11, text-sm=12, text-base=14, text-2xl=20`. Use for internal apps and ML tools.
- **External** (`.typography-external`) — larger scale, Söhne sans, Georgia headings: `text-xs=12, text-sm=14, text-base=16, text-2xl=24`. Use for marketing sites, landing pages, and public-facing docs.
- The Tailwind utility `text-base` resolves to `var(--text-base)`, which the active mode controls. Components don't need to change classes between modes.
---
## 3.5. Gradients & focus rings
Buttons in Tidal use **gradients**, not flat fills:
- **Primary** (Lilac): horizontal lilac base (`#dc92ff → #9d82f0` at 306°) + subtle vertical darken overlay. Hover swaps the lilac stops for deeper viola tones (`#964aba → #6666bf`) — sourced from Figma "Gradient styles/primary/hover".
- **Default / Secondary / Destructive / Success**: vertical fill→hover, where `fill` and `hover` come from the corresponding `--button-{variant}` and `--button-{variant}-hover` tokens.
Use the Tailwind utilities `bg-button-{primary,primary-hover,default,secondary,destructive,success}` or the underlying `--gradient-button-*` CSS vars.
**Focus rings** are compound `box-shadow` tokens, not outlines:
- `shadow-ring-primary` — 1px solid `--border-focus` + 1px halo (lilac @ 70%)
- `shadow-ring-destructive` — 2px background spacer + 2px destructive ring (shadcn-style "gap" focus ring)
Apply via `focus-visible:shadow-ring-primary` (and `focus-visible:outline-none`).
## 4. Spacing, radius, shadows
- **Spacing — Tidal Compact scale.** Class numbers do **not** map to standard Tailwind pixels. Use this lookup or just trust the rules below:
| Class | px | | Class | px |
|---|---|---|---|---|
| `gap-1` | 2 | | `gap-3` | 10 |
| `gap-1.5` | 4 | | `gap-3.5` | 12 |
| `gap-2` | 6 | | `gap-4` | 14 |
| `gap-2.5` | 8 | | `gap-5` | 18 |
Heights (`h-*`) use the standard scale (`h-8 = 32px`).
- **Radius**: `--radius` (10px) is the system base. `rounded-sm` = 6px, `rounded-md` = 8px, `rounded-lg` = 12px, `rounded-full` = pills.
- **`--radius-popover` (12px)** is the canonical radius for **floating surfaces** (Popover, DropdownMenu, Command, future Tooltip). Apply via `rounded-[var(--radius-popover)]`. Do **not** use `rounded-lg` or `rounded-[var(--radius)]` on a floating surface — target the role, not the number, so the slot can drift independently.
- **Shadows**: `shadow-sm` (subtle), `shadow` (default elevation), `shadow-popover` (with backdrop blur). Never craft a one-off shadow inline.
### Canonical spacing rules
| Rule | Value | Class | Used by |
|---|---|---|---|
| Control horizontal padding | sm 8 / md 10 / lg 12 px | `px-2` / `px-2.5` / `px-3` | Button, Select, Toggle, Input |
| Internal gap (icon ↔ text within a control) | 6px | `gap-1.5` | Button, Badge, SelectTrigger |
| Group gap (sibling controls in a bar) | 4px | `gap-1` | TabsList, ToggleGroup, Pagination |
| List-item gap (menu rows) | 2px | `gap-0.5` | MenuItem, DropdownMenu |
| Form stack (Field) | 6px | `gap-2` | Field default — Label → control → description/error |
### Container density
`Card`, `Dialog`, `AlertDialog`, `Sheet`, `Popover`, `SidePanel` all accept a `density` prop (compact / comfortable). Card uses `padding="sm" | "lg"` for the same idea. Density cascades to sub-components via React context.
| Slot | Compact | Comfortable |
|---|---|---|
| Shell | 8–12px | 20–24px |
| Section gap (header / body / footer) | 8px | 16px |
| Header stack (title ↔ description) | 4px | 8px |
| Body stack | 8px | 16px |
| Anchor offset (floating surfaces) | 6px | n/a |
### Notification surfaces aligned
`Alert`, `Toast`, and `Sonner` share padding/gap math: shell `p-4` (14px), icon→body `gap-3` / `gap-x-[10px]` (10px), title↔description gap (2px) via `gap-y-1` (Alert grid) / `gap-1` (Toast wrapper) / `mt-1` (Sonner description).
### New semantic tokens (use these, not hex)
| Token | What | When |
|---|---|---|
| `--border-subtle` | Hairline border — `rgba(0,0,0,0.12)` light / `rgba(255,255,255,0.12)` dark | Floating surfaces (Popover/Dropdown/Command) where `--border-default` is too faint against the translucent fill. |
| `--surface-hover-overlay` | Subtle fill overlay — `rgba(0,0,0,0.03)` light / `rgba(255,255,255,0.04)` dark | Hover overlays on tokenized surfaces (Input, Textarea, Command input). Eliminates the `dark:hover:…` duplicate rule. |
| `--radius-popover` | `12px` | Radius of floating surfaces (see above). |
| `--header-height` | `56px` | Page header AND `Sidebar`/`SidePanel` header heights. Override on a parent to scale chrome together. |
| `--button-primary-border` | `#964aba` | Deeper viola for accent-tone button borders. The light lilac `--button-primary` loses saturation at 1px. |
**`dark:` is forbidden in components.** Every case that previously required a `dark:` override on a tokenized element must go through a token whose light/dark values are defined in `packages/tokens`. If you encounter a new case that isn't covered — add a token, don't patch with `dark:`.
---
## 4.5. Textarea action bar pattern
When a Textarea has an action bar below it (chat composer, command prompt, etc.), the bar splits into **two button groups**:
- **Left** — context: model picker, mode toggles, scope selectors. Anything that changes *where the input goes* or *what it means*.
- **Right** — actions: file upload, mic, then the **Send** button. Send is the only Primary button in the bar and sits at the far right.
Both groups use Ghost buttons (`variant="ghost"`, `size="sm"`) except for Send, which is Primary IconButton. Upload/mic on the right are IconButtons too. On the left, use labeled Ghost buttons (`"Claude Opus 4.6 ▾"`, `"+ Add"`).
**The hover / focus / invalid ring wraps the whole composer** (textarea + action bar) — not just the textarea. Strip the Textarea's own border/shadow/focus and move them to the outer wrapper via `focus-within:` and `has-[[aria-invalid=true]]:`. The action bar is part of the field, visually.
## 5. Component composition rules
1. **Reserve Accent for one CTA per page.** `` (high-contrast inverse default) is the workhorse CTA. `` (lilac) is the brand moment — reserved for the single most important action or CTA on a given view. If a design calls for multiple Accent buttons, push back: either keep them all on `tone="default"`, or identify the one that genuinely outranks the others and keep only that one on `tone="accent"`. Same rule for ``.
1a. **Card elevation: default to 0.** Most cards should render flat (`elevation={0}`). Only promote to `1` or `2` when a clear information hierarchy calls for it (a floating callout, a single "featured" card among peers). If multiple cards on a page would qualify for elevated, reconsider — probably only one should be.
1b. **CardHeader padding scales with CardTitle size.** CardTitle exposes a `size` prop (`sm | md | lg | xl`). When you bump the title to `lg` or `xl`, CardHeader automatically steps up both vertical and horizontal padding (via a `has-[[data-title-size]]:` selector) so the header gets proportional breathing room. You don't need to touch Card's `padding` prop to make a hero card feel right — just set `… `.
1d. **`variant × tone` is the canonical component API shape.** Any component with both a visual style axis and an intent axis uses two props: `variant` (style) and `tone` (intent). This matches `Button`, `IconButton`, and `Badge`:
- `Button` / `IconButton`: `variant: primary | default | outline | ghost | link` × `tone: default | destructive | success | accent`.
- `Badge`: `variant: solid | secondary | outline` × `tone: default | destructive | success`.
Don't conflate the two into a single flat `variant` list (e.g. `"primary" | "destructive"`) — that muddles the mental model and makes compound cases like "outline + success" unreachable. Use `cva` compound variants to express the cell-by-cell styling.
1c. **Reuse `CardTitle` + `CardDescription` anywhere you need a title + description pair.** They work standalone — no `` parent required. Use them inside ``, ``, toolbar headers, whatever. For Popover/Dialog, pass `` (text-base). For Card, the default (`size="md"` → text-lg) is right. Never hand-roll `` + ` ` — the type scale will drift.
2. **Compose, don't reinvent.** If `Card`, `Button`, `Input` exists, use it. Don't wrap a `
` to imitate a card.
3. **Polymorphic via `asChild`** (Radix pattern) is preferred over `as` props.
4. **Variants live in the component** via `cva` (class-variance-authority). Don't pass `className` to override variant styles — add a variant.
5. **Forward refs** on every interactive component.
6. **Accept `className`** and merge with `cn()` (`packages/react/src/lib/cn.ts`).
7. **Accessibility is non-negotiable**: keyboard navigation, focus states (`focus-visible:ring-2 ring-primary`), proper roles. Use Radix Primitives for any interactive widget.
---
## 6. New-component checklist
When adding a component (P2+), every PR must include:
- [ ] `packages/react/src/components/
.tsx` — implementation, with `cva` variants, `forwardRef`, `displayName`
- [ ] `apps/docs/content/docs/components/.mdx` — usage, props table, all variants rendered, do/don't, accessibility notes
- [ ] A live preview (` `) in the MDX
- [ ] Source-of-truth note linking back to the Figma node ID
- [ ] Updated `CHANGELOG.md` entry
---
## 7. Forbidden patterns
| Don't | Why | Do instead |
|---|---|---|
| `font-bold` / `font-extrabold` / `font-black` / `font-light` | not in the allowed weight set | `font-semibold` (heaviest we use) |
| `bg-[#cd82f0]` | bypasses the system | `bg-primary` |
| `style={{ padding: 13 }}` | off-scale | `p-3` (12) or `p-3.5` (14) |
| `dark:…` on a tokenized component | duplicates dark logic | fix the token (or add a new semantic token — see §4) |
| `style={{ opacity: 0.5 }}` for disabled state | inline style can't be overridden | use `disabled:opacity-50 data-[disabled]:opacity-50` classes (see §2.4) |
| `disabled:text-foreground-disabled` / `disabled:bg-muted` chrome dimming | drifts from canonical 50% opacity rule | use `disabled:opacity-50` (see §2.4) |
| `motion-safe:` / `motion-reduce:` in a component | reduced motion is global (see §2.7) | do nothing — the global rule handles it |
| `rounded-lg` on a Popover/Dropdown surface | wrong semantic | `rounded-[var(--radius-popover)]` |
| New component without MDX docs | invisible to consumers | follow §6 |
| Editing `dist/` files | regenerated on every build | edit the source in `src/` |
| Importing from `tailwindcss/colors` in a component | leaks primitives | go through a semantic token |
---
## 8. Screen-building patterns
Patterns codified from iterative design reviews of production screen mocks.
### 8.1 Components over custom markup
Always check `@tidal-ds/react` for an existing component before hand-building UI. Use design system primitives even if the result deviates from a reference design (e.g., an IDE mock). Consistency with the system matters more than mimicking an external product.
### 8.2 Defaults over overrides
Use components with zero `className` overrides first. Only add styling after confirming the default doesn't work. Overrides fighting component defaults are the #1 source of visual bugs in screen mocks.
### 8.3 No redundant information
If tabs already label a panel's content, don't add a separate header above them. Every element in a dense tool UI must earn its space.
### 8.4 Collapsible panels: header always visible
The trigger/header bar stays visible at all times — only the content body collapses. Hiding the header removes the user's ability to re-expand.
- Place the trigger row **outside** `CollapsibleContent`.
- `CollapsibleContent` wraps only the expandable body.
- Use `CollapsibleTrigger asChild` when the trigger needs to be a custom element (e.g., a row with tabs + badges + a toggle button).
### 8.5 Status color system
| Meaning | Color | Token | Tailwind utility |
|---------|-------|-------|-----------------|
| Error / failure | Red | `--status-error` | `text-status-error` |
| Warning / caution | Amber / gold | `--status-warning` | `text-status-warning` |
| Success / healthy | Green | `--status-success` | `text-status-success` |
| Informational | Cerulean | `--status-info` | `text-status-info` |
Never use green for warnings. Always pair color with an icon shape or text label — never rely on color alone. Prefer `text-status-*` utilities over raw `text-[var(--button-*)]` or Tailwind color classes (`text-amber-500`).
### 8.6 PageTabBar vs Tabs — when to use which
| Scenario | Component |
|----------|-----------|
| File tabs, editor tabs, query tabs, open documents — closeable, with icons | `PageTabBar` + `PageTab` |
| View switching within a panel (Terminal / Problems / Output, Settings sections) | `Tabs` + `TabsTrigger` |
`PageTab` has an `icon` prop for left-aligned decorations. Pass icons via `icon`, not as children — children become the text label. This prevents vertical stacking in small tab spaces.
### 8.7 Text sizing in tool UIs
| Size | Use for |
|------|---------|
| `text-sm` (14px) | Tool content: code, terminal output, diagnostics, form fields, panel body text |
| `text-xs` (12px) | Metadata only: status bars, column headers, timestamps, breadcrumbs |
Don't default to `text-xs` for primary content — it reads as uncomfortably compact.
### 8.8 Dark mode for screen mocks
Force dark mode via `next-themes` `setTheme("dark")` (see `apps/docs/app/(screens)/force-dark.tsx`). Do **not** add `class="dark"` to a wrapper div — CSS custom properties for dark mode are scoped to `html.dark`, so a dark class on a descendant div won't trigger them. Components like `PageTab` (`bg-button-default`) will resolve to light-mode values if the `html` element isn't in dark mode.
### 8.9 Dev workflow: component library changes
After editing files in `packages/react/src/components/`, the Next.js dev server HMR cache won't reliably update. Run:
```bash
rm -rf apps/docs/.next && npx next dev -p
```
---
## 9. File map (where things live)
```
design-tokens/ ← canonical Figma export (DTCG, do not edit by hand)
manifest.json
Semantics.{light,dark}/liquid.tokens.json
Typography.Liquid {Internal,External}.tokens.json
Tailwind Colors.Mode 1.tokens.json
Tailwind Primitives.{Liquid,Mode 2}.tokens.json
Liquid colors.Mode 1.tokens.json
Primitives.Mode 1.tokens.json
*.styles.tokens.json ← gradients/effects/text styles (TODO: importer)
packages/tokens/
scripts/build.ts ← importer: design-tokens/ → src/_generated.ts + dist/
src/
_generated.ts ← generated, do not edit
gradients.ts ← hand-curated supplements (until styles importer lands)
shadows.ts ← hand-curated supplements
index.ts ← public API
dist/ ← generated outputs (tokens.css, tokens.json, tailwind-preset.js)
apps/docs/
app/ ← Next.js app router pages
public/fonts/ ← Söhne .otf files
```
---
## 10. Before you build — clarify and propose
### 10.1 Ask for clarity when the brief is underspecified
If a PRD, prompt, or design brief doesn't answer these questions, **stop and ask the prompter** before writing any code:
1. **What is the primary user task on this screen?** (If unclear, you can't identify Level 1.)
2. **What data is displayed, and how much of it?** (Determines table vs list vs cards, and whether pagination/filtering is needed.)
3. **What states need to exist?** (Empty, loading, error, partial, no-access — if the brief only describes the happy path, ask about the others.)
4. **What actions can the user take?** (Create, edit, delete, bulk operations — determines button placement, modals, confirmation flows.)
5. **What existing components should be used?** (If the brief doesn't reference the design system, ask which components from `@tidal-ds/react` are expected.)
Don't guess on ambiguous requirements. A 30-second clarification question saves hours of rework.
### 10.2 Propose the build plan before implementing
Before writing code for a new screen or major feature, present a short build plan to the prompter for approval:
```
## Build Plan: [Screen/Feature Name]
**Purpose:** What is this screen for?
**Layout:** Which page structure pattern? (List View, Detail View, Dashboard, Settings, etc.)
Reference: [cite which products/patterns from ux_patterns_researched.md inform this choice]
**Key components:**
- [Component] → [purpose] → [why this component vs alternatives]
**States:** Loading / Empty / Error / No-access — one line each describing what the user sees.
**Open questions:** Anything still ambiguous after reading the brief.
```
The prompter may approve, adjust, or redirect. **Do not write code until the plan is approved.** This applies to: new pages, new major components, layout changes, and navigation changes. It does not apply to trivial changes (adding a field, fixing a color, updating copy).
---
## 11. When in doubt
- Check Figma first (`mcp__figma__get_design_context` with the node ID).
- If it's not in Figma, **ask before inventing**. Don't extrapolate the system on your own.
- If you're patching a bug in code that drifts from Figma, fix the code to match Figma (or open a sync request).