Files
proof-of-work/design/DESIGN.md
T
Keysat 7fda9ceb7e
CI / proof-of-work (Next.js app) (push) Waiting to run
CI / start9/0.4 (StartOS package code) (push) Waiting to run
design: extract document-as-is UI contract (DESIGN.md + DTCG tokens)
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.
2026-06-19 12:14:51 -05:00

12 KiB
Raw Blame History

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/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 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 (h1h3), 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 buttonbg-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 / panelsbg-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 / selectsbg-zinc-800 (or bg-zinc-900), border border-zinc-700, radius 4px, white text, placeholder in zinc-500, focus ring ring-white/20ring-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 bannersbg-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-900zinc-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.