Developer docs
Design

Design & style guide

Tokens, primitives, and the live Style Lab.

docs/DESIGN.md

Tokens

Everything reads from CSS custom properties in apps/web/src/app/globals.css. Change a token and every component, backdrop and animation follows — including the mermaid diagrams on the dashboard, which resolve the tokens to concrete values at render time.

GroupTokens
Surface--background --card --popover --muted
Text--foreground --card-foreground --muted-foreground
Brand--primary --secondary --accent --ring
State--success --warning --destructive
Shape--radius (sm/md/lg/xl derived)
Type--font-sans / --font-display (Inter) · --font-mono (JetBrains Mono)
Signature--highlight --highlight-foreground --ink --ink-2

The palette is Verjson's

Taken from verjson.com and verjson.ai, not invented here. This is an internal Verjson tool; one that looked nothing like the company running it would be its own kind of unfinished.

TokenValueRole
--background#faf8f3warm cream ground
--foreground / --ink#0a0a0anear-black ink
--primary#1700b8electric indigo — action, links, the _ in a label
--highlight#e8ff47acid lime — the signature
--accent#16794ddeep green — approved / success
--border#e2ddcewarm hairline

Lime is never text. At 88% luminance it fails contrast on cream at any size, so it is not --accent — it lives in --highlight and appears only as a fill with --highlight-foreground (near-black) on top, or as an accent on an ink band. This is how the brand itself uses it: the verJSON logo is indigo on light and inverts to lime on dark.

Devices

Three things carry the identity. They are cheap, and they are most of why the UI does not read as a component library with the colours changed:

ClassWhat it does
.labelThe _section kicker — lowercase, monospaced, underscore-prefixed. The underscore takes --label-accent, which .band flips to lime so it stays visible on ink.
.markThe lime highlighter: a skewed block behind a word, drawn as a pseudo-element so it never affects layout.
.bandA full-bleed ink section with photography under a scrim. These exist to break rhythm — consecutive card grids on one background is what made the landing page read as generated.

Headlines are Inter at font-extrabold, leading-[1.05], tracking-[-0.03em]. Ids, timestamps, spend figures, counts and paths are all monospaced — that is what makes a data-dense page scannable, and it is not decoration.

Photography

Lives in apps/web/public/img/, with provenance in public/img/CREDITS.json. Unsplash, under the Unsplash License, downloaded rather than hotlinked — the page then has no runtime dependency on a third-party CDN, and no viewer's IP is disclosed to one.

Images are used where the product genuinely has images — the media library (F-033), the ad asset pipeline (F-055), the composer's attached creative — plus two atmospheric bands. Decorative photography is alt="" and aria-hidden; nothing a reader must know is carried by a picture.

The Style Lab

/style-guide retheme the whole app live. In development, Save writes the chosen values back into globals.css between marker comments — so a theme decision becomes a commit rather than a screenshot. It 404s in production; it is an authoring tool, not a runtime feature.

Primitives

apps/web/src/components/ui/ — button, card, input, badge, table, modal, feedback. Add new ones with npx shadcn@latest add <name> and re-point them at the tokens above.

Animation

Ambient motion (the aurora backdrop, the gradient shimmer) keeps playing under prefers-reduced-motion, because it conveys no information and cannot trap focus. Functional motion — transitions, scroll reveals, count-ups — collapses to near-zero. Mark ambient elements .ambient.

Rules

Never a raw colourtext-muted-foreground, not text-slate-500. A hard-coded colour survives a retheme and then looks wrong.
Wide content scrolls in its own boxThe page body never scrolls sideways.
Scrollbars are hidden, not disabledCustom scroll containers use .no-scrollbar; they still scroll by wheel, drag, keyboard and touch.
Empty states say what to do"No projects yet" plus the button, not a blank panel.
Disabled controls say whytitle explaining the missing permission or precondition.
Icons are decorativearia-hidden, with a real accessible name on the control.