7fda9ceb7e
Case-B extract of the as-built dark UI into design/: 9-section DESIGN.md, W3C DTCG tokens.tokens.json, brand/palette.css + assets, and inspiration/ provenance (incl. the rendered red-shade decision). Two owner calls layered in: red elevated to the single brand accent (#DC2626), two-tier radius (4px controls / 8px containers). AGENTS.md gains the read-before-UI Design line + design/ in the layout tree; ROADMAP gains the design-checker cleanup backlog. No UI code changed.
208 lines
12 KiB
Markdown
208 lines
12 KiB
Markdown
# Proof of Work — Design Brief
|
||
|
||
The durable brand brief for Proof of Work's user-facing UI. Read this and
|
||
[`tokens.tokens.json`](./tokens.tokens.json) before building or changing any UI, and conform
|
||
to them. Machine-readable values live in the tokens file and
|
||
[`brand/palette.css`](./brand/palette.css); this file is the *why* and the rules.
|
||
|
||
> **Provenance.** Established 2026-06-19 by a **document-as-is extract** (Case B) of the
|
||
> as-built UI — there were no prior brand guidelines; the look grew in the code. Values were
|
||
> harvested by frequency census of the Tailwind classes in `proof-of-work/app` +
|
||
> `proof-of-work/components` and reconciled with the owner. The only owner-driven *elevations*
|
||
> over the literal as-built state: red promoted from error-only to a **brand accent**
|
||
> (canonical `#DC2626`), and a documented two-tier radius rule. See
|
||
> [`inspiration/README.md`](./inspiration/README.md) for what served as the reference.
|
||
|
||
---
|
||
|
||
## 1. Visual theme
|
||
|
||
**Monochrome gym-brutalist.** A near-black canvas, a single cool-gray (zinc) neutral ramp,
|
||
white as the primary voice, and **red as the one accent** — the heat in an otherwise
|
||
black-and-white system. The feeling is a piece of gym equipment: heavy, blunt, high-contrast,
|
||
no ornament. The product's voice ("Track. Lift. Dominate.") shows up visually as
|
||
**UPPERCASE, wide-tracked, condensed display type** and flat, edge-defined surfaces.
|
||
|
||
It is **not** soft, pastel, glassy, or playful. No gradients-as-decoration, no drop-shadow
|
||
depth, no rounded-pill friendliness, no multi-hue palette. Restraint is the brand: color is
|
||
rare and earns its place.
|
||
|
||
## 2. Color palette
|
||
|
||
The system is **one neutral ramp (zinc) + white/black + four semantic hues**, on a near-black
|
||
canvas. Red is both the **brand accent** and the **error/destructive** hue, told apart by
|
||
*treatment*, never by a second red (see §7).
|
||
|
||
**Canvas & surfaces** (depth is built by stepping *up* in lightness, not by shadow):
|
||
|
||
| Role | Token | Hex | Notes |
|
||
|------|-------|-----|-------|
|
||
| Canvas (app background) | `color.bg.canvas` | `#0A0A0A` | The PWA `theme_color`; `<body>` bg. The anchor. |
|
||
| Surface (cards, panels) | `color.bg.surface` | `#18181B` | zinc-900 — the default raised surface. |
|
||
| Surface raised (inputs, chips, hover) | `color.bg.raised` | `#27272A` | zinc-800 — controls and the next step up. |
|
||
| Surface inset (rare deep wells) | `color.bg.inset` | `#09090B` | zinc-950 — slightly below canvas. |
|
||
|
||
**Borders** (the primary depth cue alongside bg layering):
|
||
|
||
| Role | Token | Hex |
|
||
|------|-------|-----|
|
||
| Subtle (default hairline) | `color.border.subtle` | `#27272A` (zinc-800) |
|
||
| Default | `color.border.default` | `#3F3F46` (zinc-700) |
|
||
| Strong (emphasis/hover) | `color.border.strong` | `#52525B` (zinc-600) |
|
||
|
||
**Text** (on the dark canvas):
|
||
|
||
| Role | Token | Hex |
|
||
|------|-------|-----|
|
||
| Primary | `color.text.primary` | `#FFFFFF` |
|
||
| Secondary | `color.text.secondary` | `#A1A1AA` (zinc-400) |
|
||
| Muted | `color.text.muted` | `#71717A` (zinc-500) |
|
||
| Subtle / disabled | `color.text.subtle` | `#52525B` (zinc-600) |
|
||
| Inverted (on white surfaces) | `color.text.inverted` | `#000000` |
|
||
|
||
**Accent / semantic.** The accent and error share `#DC2626`; success/warning/info round out
|
||
the state palette. On the dark canvas the *text/icon* tint is the lighter -400 step; fills and
|
||
edges use the -600 step; washes use a translucent dark step.
|
||
|
||
| Role | Token | Hex | Use |
|
||
|------|-------|-----|-----|
|
||
| **Accent / error (canonical red)** | `color.accent.red` | `#DC2626` (red-600) | Brand emphasis fills/edges **and** destructive intent. |
|
||
| Accent hover/pressed | `color.accent.red-strong` | `#B91C1C` (red-700) | |
|
||
| Red text/icon on dark | `color.accent.red-text` | `#F87171` (red-400) | Error text, destructive links, accent labels. |
|
||
| Red border | `color.accent.red-border` | `#991B1B` (red-800) | Error/destructive outlines. |
|
||
| Red wash (bg) | `color.accent.red-wash` | `rgba(127,29,29,.30)` (red-900/30) | Error banners, destructive hover. |
|
||
| Success | `color.state.success` | `#34D399` (emerald-400) text / `#059669` (emerald-600) fill | PRs, saved, positive deltas. |
|
||
| Warning | `color.state.warning` | `#FBBF24` (amber-400) text / `#78350F` (amber-900) edge | Cautions, cost/limit notices. |
|
||
| Info | `color.state.info` | `#60A5FA` (blue-400) text / `#172554` (blue-950) wash | Neutral notices (used sparingly). |
|
||
|
||
**Primary action color is not a hue — it's white.** The primary button is `#FFFFFF` bg /
|
||
`#000000` text (see §4). White, not red, is the loudest thing on screen.
|
||
|
||
## 3. Typography
|
||
|
||
Two families, both already wired as CSS variables in `app/layout.tsx`:
|
||
|
||
- **Display — Bebas Neue** (`var(--font-display)`): condensed, all-caps by nature. Used for
|
||
**all headings (h1–h3), buttons, and labels**, always `text-transform: uppercase` with
|
||
`letter-spacing: 0.05em` (Tailwind `tracking-wider`). This UPPERCASE + tracking pairing is
|
||
the single strongest brand signal — ~115 uses in the as-built UI. Don't set body copy in it.
|
||
- **Body — Space Grotesk** (`var(--font-sans)`): all running text, form values, data, numbers.
|
||
Use `tabular-nums` for stat/metric columns.
|
||
|
||
**Type scale** (Tailwind rem, the app is data-dense so the small end dominates):
|
||
|
||
| Token | Size | Typical use |
|
||
|-------|------|-------------|
|
||
| `font.size.xs` | 12px | Labels, meta, table cells (the workhorse — ~240 uses) |
|
||
| `font.size.sm` | 14px | Default body / form text (~175 uses) |
|
||
| `font.size.base` | 16px | Comfortable body |
|
||
| `font.size.lg` | 18px | Sub-headings |
|
||
| `font.size.xl` | 20px | |
|
||
| `font.size.2xl` | 24px | Section headings (Bebas) |
|
||
| `font.size.3xl` | 30px | Page headings (Bebas) |
|
||
| `font.size.4xl` | 36px | Hero / display (Bebas) |
|
||
|
||
**Weights:** 400 normal, 500 medium (default for emphasis), 600 semibold, 700 bold. Bebas
|
||
ships a single weight; weight contrast lives in the body font.
|
||
|
||
## 4. Component styling
|
||
|
||
- **Primary button** — `bg-white text-black`, `font-bold uppercase tracking-wider`,
|
||
`hover:bg-zinc-200`, `disabled:bg-zinc-700 disabled:text-zinc-500`. Radius 4px (`rounded`),
|
||
padding ≈ `px-5 py-2` (inline) / `py-3` (full-width). **This is the signature; keep it white.**
|
||
- **Secondary / accent (ghost) button** — transparent bg, `border` + text in the accent red
|
||
(`#DC2626` border, `text-red-400`), uppercase tracking. For secondary emphasis (e.g.
|
||
"Refine"). Never a *solid* red fill (see §7).
|
||
- **Destructive button** — same ghost treatment in red (`text-red-400`/`text-red-500`,
|
||
`hover:bg-red-950/30`) or a red-wash block. Destructive is red-as-*outline/wash*, so it never
|
||
competes with the white primary.
|
||
- **Cards / panels** — `bg-zinc-900`, `border border-zinc-800`, radius 8px (`rounded-lg`),
|
||
padding `p-4`. Accent a card by adding a **left edge** `border-l-4` in `#DC2626`.
|
||
- **Inputs / selects** — `bg-zinc-800` (or `bg-zinc-900`), `border border-zinc-700`, radius 4px,
|
||
white text, `placeholder` in zinc-500, focus ring `ring-white/20`–`ring-white/30`.
|
||
- **Badges / chips** — uppercase, `text-xs`, tracked; semantic ones use the wash pattern
|
||
(tinted text + matching translucent bg + matching border at ~45% — e.g. a red "PR" badge:
|
||
`text-red-400 bg-red-900/30 border border-red-800`).
|
||
- **Active nav / selected state** — accent red: `text-red-400` + a `border-b-2` underline or a
|
||
left indicator in `#DC2626`; inactive items in `text-zinc-500`.
|
||
- **Error / alert banners** — `bg-red-900/30 border border-red-800 text-red-400`, radius 8px.
|
||
|
||
## 5. Layout
|
||
|
||
- **Mobile-first PWA**, portrait-primary, installable. Most styling is unprefixed (mobile);
|
||
`sm:` is the dominant breakpoint, `md:` introduces the desktop sidebar.
|
||
- **App shell:** a fixed **240px sidebar** on `md+` (`md:pl-[var(--sidebar-width)]` via the
|
||
`.app-content` utility) and a **64px bottom nav** on mobile. Top nav height 64px. These three
|
||
dimensions are CSS vars in `globals.css` (`--sidebar-width`, `--nav-height`,
|
||
`--bottom-nav-height`) — reuse them, don't hardcode.
|
||
- **Spacing** follows Tailwind's 4px scale. Dense by intent: `p-4` cards, `px-4 py-3` controls,
|
||
`gap-2`/`gap-3` between elements.
|
||
- **Content** is the focus; chrome is minimal. Single-column on mobile; the sidebar is the only
|
||
persistent chrome on desktop.
|
||
|
||
## 6. Depth & elevation
|
||
|
||
**Flat by design — depth comes from background layering + 1px borders, not shadows.** The
|
||
surface ladder (`#0A0A0A` canvas → `zinc-900` → `zinc-800`) plus zinc-700/800 hairlines *is*
|
||
the elevation system. Shadows are reserved for **truly floating overlays only** (modals,
|
||
popovers, dropdowns) — `shadow-lg`/`shadow-xl`/`shadow-2xl`. Never put a shadow on a static
|
||
card; raise it with bg + border instead.
|
||
|
||
## 7. Do's and don'ts
|
||
|
||
**Do**
|
||
- Keep the **primary button white** (`bg-white text-black`). It's the brand's loudest element.
|
||
- Use **UPPERCASE + `tracking-wider` Bebas** for headings, buttons, and labels.
|
||
- Reach for **zinc** for every neutral — backgrounds, borders, secondary text.
|
||
- Use **`#DC2626` red as the single accent**: active states, emphasis edges, key deltas, links,
|
||
the destructive intent. Make it rare and deliberate.
|
||
- Build depth with **bg steps + borders**; keep surfaces flat.
|
||
- Use the **wash pattern** for semantic blocks (tinted text + translucent bg + matching border).
|
||
|
||
**Don't**
|
||
- ❌ Don't make a **solid red button** — it collides with the white primary *and* with the red
|
||
destructive meaning. Red buttons are ghost/outline/wash only. (Red as a solid *fill* is for
|
||
small accents — nav indicators, edges, badges — not full buttons.)
|
||
- ❌ Don't introduce a **second neutral** (gray/slate/stone) — zinc only. (`gray-100` strays
|
||
exist in the code; they're cleanup, not precedent.)
|
||
- ❌ Don't introduce a **second red, second green, or second yellow** — `#DC2626` red, `emerald`
|
||
success, `amber` warning. (`green-*`/`yellow-*` strays are cleanup.)
|
||
- ❌ Don't add **decorative shadows or gradients**. Flat only.
|
||
- ❌ Don't set **body text in Bebas Neue**, or leave headings/labels lowercase.
|
||
- ❌ Don't add a **new arbitrary radius** — controls are 4px, containers 8px (see §8 below).
|
||
|
||
## 8. Responsive behavior
|
||
|
||
- **Mobile-first**: author the mobile layout unprefixed, layer desktop with `sm:`/`md:`.
|
||
- The **bottom nav** is the mobile primary navigation; the **240px sidebar** replaces it at
|
||
`md+`. The main content reserves the sidebar via `md:pl-[var(--sidebar-width)]`.
|
||
- Touch targets stay ≥ 44px tall on mobile; the dense `text-xs`/`text-sm` scale is for
|
||
*information density*, not for shrinking tap targets.
|
||
- Viewport is locked (`maximum-scale=1, user-scalable=no`) — this is an app, not a document.
|
||
|
||
**Radius scale (canonical):**
|
||
|
||
| Token | Value | Use |
|
||
|-------|-------|-----|
|
||
| `radius.control` | 4px (`rounded`) | Buttons, inputs, chips, small elements |
|
||
| `radius.container` | 8px (`rounded-lg`) | Cards, panels, modals, banners |
|
||
| `radius.full` | 9999px (`rounded-full`) | Pills, avatars, dots |
|
||
|
||
(`rounded-md`/6px exists in the code as a third value; treat it as drift toward one of the two.)
|
||
|
||
## 9. Agent prompt guide
|
||
|
||
When building or editing UI in `proof-of-work/`:
|
||
|
||
> Build it **monochrome-first**: near-black canvas (`#0A0A0A`), **zinc** for every neutral,
|
||
> **white** for primary text and the primary button (`bg-white text-black font-bold uppercase
|
||
> tracking-wider`). Headings/labels/buttons are **Bebas Neue, UPPERCASE, `tracking-wider`**;
|
||
> body is **Space Grotesk**, dense (`text-xs`/`text-sm`). The **only accent is red `#DC2626`** —
|
||
> use it sparingly for active states, emphasis edges (`border-l-4`), key deltas, links, and
|
||
> destructive intent; **never as a solid button fill**. Build depth with **background layering +
|
||
> 1px zinc borders**, not shadows (shadows are for floating overlays only). Radius: **4px**
|
||
> controls, **8px** containers. Pull exact values from `design/tokens.tokens.json` /
|
||
> `design/brand/palette.css` rather than re-deriving hexes; reuse the layout CSS vars
|
||
> (`--sidebar-width`, `--nav-height`, `--bottom-nav-height`). Stay in the system — no second
|
||
> neutral, no second red/green/yellow, no decorative gradients or shadows.
|