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.
12 KiB
Proof of Work — Design Brief
The durable brand brief for Proof of Work's user-facing UI. Read this and
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; 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/componentsand 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. Seeinspiration/README.mdfor 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, alwaystext-transform: uppercasewithletter-spacing: 0.05em(Tailwindtracking-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. Usetabular-numsfor 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 (#DC2626border,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), paddingp-4. Accent a card by adding a left edgeborder-l-4in#DC2626. - Inputs / selects —
bg-zinc-800(orbg-zinc-900),border border-zinc-700, radius 4px, white text,placeholderin zinc-500, focus ringring-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+ aborder-b-2underline or a left indicator in#DC2626; inactive items intext-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-contentutility) and a 64px bottom nav on mobile. Top nav height 64px. These three dimensions are CSS vars inglobals.css(--sidebar-width,--nav-height,--bottom-nav-height) — reuse them, don't hardcode. - Spacing follows Tailwind's 4px scale. Dense by intent:
p-4cards,px-4 py-3controls,gap-2/gap-3between 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-widerBebas for headings, buttons, and labels. - Reach for zinc for every neutral — backgrounds, borders, secondary text.
- Use
#DC2626red 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-100strays exist in the code; they're cleanup, not precedent.) - ❌ Don't introduce a second red, second green, or second yellow —
#DC2626red,emeraldsuccess,amberwarning. (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 viamd:pl-[var(--sidebar-width)]. - Touch targets stay ≥ 44px tall on mobile; the dense
text-xs/text-smscale 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 fromdesign/tokens.tokens.json/design/brand/palette.cssrather 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.