Web design system
The visual language for agent-tempo — a calm, document-leaning operations tool where one human (the maestro) conducts ensembles of long-running coding agents (players). This spec is generated from the live prototype: every component below is rendered with the same CSS the dashboard uses, so what you see is the truth. The TUI has its own system; the two share the metronome motif and the terracotta accent, but otherwise speak different visual languages by design.
Principles
Document, not dashboard
Avoid HUD-y data slop. Prefer reading-grade type, generous whitespace, and quiet panels over neon stat tiles and gradient cards.
Warm, not corporate
Cream-on-charcoal with terracotta. The serif italic is the brand — use it for moments, not chrome.
Operator-grade density
Power users will run this all day. Density is a first-class token, not an afterthought — every spacing rule scales from one variable.
Status earns color
Five semantic colors total. Anything that isn't ok / warn / err / info / accent is neutral. Resist decorative color.
Brand & voice
The metronome is the brand. A pendulum that swings on the actual tempo of the ensemble — not a logo, an instrument. The wordmark is mono with a terracotta hyphen. Italic serif is reserved for the maestro (the human operator) and one-word headline accents.
Mark + wordmark
animation-duration: 60/bpm s) and frozen when paused —
the only continuous animation in the chrome.
Wordmark: JetBrains Mono 600, lowercase, with a
terracotta hyphen (-) as the only color accent. Use the
full lockup for primary brand moments; mark-only for app chrome
(favicon, sidebar at sub-tablet widths).
Maestro mark
conductor player (a software agent). It appears in the
composer header, on outbound chat messages, and as the sidebar
identity badge. Glyph: bare italic
M in Fraunces (with Instrument Serif as fallback) at the
weight/letter-spacing of a hand-set initial. Bone color
(#F5EBDD) on a faintly tinted square, so it reads as
quill on parchment rather than a UI badge.
Voice samples
- "Conductor is processing"
- "3 active · 1 detached · ready to dispatch"
- "@my-band · 23 msgs/min"
- "critic's adapter detached ~2 min ago"
- "PR #287 is up — needs a review when you have a moment"
- "🚀 Let's get your AI agents working!"
- "Your AI workforce is ready"
- "Powerful. Intuitive. Orchestration."
- "Crushing it: 12 tasks completed"
- "Player_4f3a (Researcher Bot) is now active"
conductor, lead, tuner,
critic) read like teammates, not bots. Ensembles are
@-mentioned (@my-band). Status sentences are observational,
not breathless — the dashboard reports, it doesn't sell.
Color tokens
All color is token-driven. Tokens are CSS custom properties on
:root with a [data-theme="light"] override.
Tailwind reads them via theme.extend.colors — never hardcode hex in components.
Surfaces & ink
Accent
--accent-soft is the only acceptable accent fill.
Status
shadcn token mapping
/* globals.css */ @layer base { :root { /* shadcn tokens — point to ours */ --background: var(--bg); --foreground: var(--text); --card: var(--bg-1); --card-foreground: var(--text); --popover: var(--bg-2); --popover-foreground: var(--text); --primary: var(--accent); --primary-foreground: var(--bg); --secondary: var(--bg-2); --secondary-foreground: var(--text); --muted: var(--bg-2); --muted-foreground: var(--dim); --accent: var(--accent-soft); --accent-foreground: var(--accent-ink); --destructive: var(--err); --destructive-foreground: #fff; --border: var(--rule); --input: var(--rule); --ring: var(--accent); --radius: 8px; } }
<Button>, <Input>,
<Dialog> read these standard names. By aliasing them to
our brand tokens, every shadcn primitive becomes Maestro-themed for free.
Type system
Three families. Instrument Sans for everything you read or click. Instrument Serif (upright) is the display face for headings, page titles, ensemble names, and the dialog title — anything that wants to read as composed rather than chrome. JetBrains Mono for IDs, timestamps, code, the wordmark, and numerics that need to align.
Family pairing
font-display, font-sans, font-mono
point at the three CSS variables.
Italic discipline
- The maestro mark (Fraunces italic
M) - One
<em>accent inside a display heading (e.g. listening) - Routed messages in the chat log (visually demoted, "overhearing" voice)
- Empty-state hero text
- Body copy (illegible at length)
- Buttons or labels
- Tables or data
- The page title or section title (those are upright serif)
- Player or ensemble names (those are mono or upright serif)
Density scale
Density is a single integer token (4–9) on
html[data-density="..."].
It drives all spacing and type sizes in lockstep. Components don't
set padding directly — they consume --density-* vars.
The scale
--density-pad,
--density-pad-y, --density-gap,
--density-fs, --density-fs-sm,
--density-line. Set them once at level; every panel,
row, button, and chat bubble updates automatically.
Layout & grid
Three layered grids. The app shell is permanent. Page content is flexible. The workspace inside an ensemble is its own 2-column rhythm.
App shell
244px 1fr. Sidebar is full-height,
scrollable independently. Sidebar sections: brand
+ metronome at top, ensembles list, navigation (hosts, types,
schedules, settings), and a maestro identity row at the bottom.
At ≤900px the sidebar collapses to 64px icon-only; at ≤520px it
becomes a top app bar + bottom tab bar (mobile).
Ensemble workspace
grid-template-columns: 1fr 340px. Left:
conductor chat (scrollable log + composer pinned to bottom), with
the roster either stacked beneath or popped into the side column via
the "Details" toggle. Right (340px): stacked panels
(Roster, Tempo, Events) at natural height; the column scrolls if
total height exceeds viewport. At ≤1200px tightens to 300px;
at ≤900px collapses to single-column with the side panel becoming
a pop-out drawer; at ≤520px the side panel is a bottom sheet.
Spacing scale
| Token | Maps to | Used for |
|---|---|---|
| --density-pad | 14px @ d6 | Panel body padding · main horizontal rhythm |
| --density-pad-y | 10px @ d6 | Vertical padding for rows, headers, buttons |
| --density-gap | 14px @ d6 | Gap between siblings (panel grid, rows) |
| --density-pad × 1.6 | 22px @ d6 | Page-level padding (header, workspace) |
| --density-pad-y × 0.5 | 5px @ d6 | KV row padding · chat-log gap |
| --density-fs | 13px @ d6 | Body font-size · table cell text |
| --density-fs-sm | 11.5px @ d6 | Event row · table headers · captions |
| --density-line | 1.5 @ d6 | Body line-height |
html[data-density="..."]. The seven tokens above scale
together; components don't set padding, font-size, or line-height
directly — they consume these vars. Set the density once at the root
and everything reflows: panels, rows, buttons, chat bubbles, tables.
Shape, elevation, motion
Restrained. Two radii, two shadows, two motion timings. If you find yourself reaching for a third, simplify.
Radius
Elevation
Motion
| Token | Value | Used for |
|---|---|---|
| duration-fast | 120ms · ease-out | Hover, focus, button press |
| duration-base | 220ms · ease-out | Modal in, sheet slide, tab change |
| tempo-pulse | on-beat · scale 1 → 1.06 | The metronome tick (only) |
Button shadcn: Button
Three variants. Primary is terracotta — exactly one per page region. Secondary is the workhorse. Ghost is for low-priority chrome actions.
Variants
<Button> with our
--primary & --secondary tokens. Add a custom
variant="ghost" mapping to transparent + --text-2.
Min height 32px @ d6, scales with --density-pad-y.
Phase chips, type badges, mentions shadcn: Badge
Pill-shaped metadata. Three flavors that look similar but mean
different things: phase chips for player state
(drawn as .phase-dot — icon + optional label),
type badges for player types (drawn as
.type-badge — colored by type hue), and
generic chips for everything else (filter, tag,
cadence label).
Phase chips
● ○ ◐ ◔ ✕) so a single mental model works across web
and terminal. Pulse: processing is the
only phase that animates — a subtle opacity blink on the icon, so
you can tell at a glance which players are doing work right now.
Phase data + colors live in window.PHASES and the
<PhaseDot> component reads from it.
Type badges
window.TYPE_HUES).
The badge derives color, border, and
background from that one hue at fixed L/C — so adding a
new type only requires picking a hue. This is also how
<PlayerAvatar> tints itself.
Generic chips
--accent-soft background + accent ink.
Don't use these for status — use phase chips. Don't use these for
types — use type badges.
Panel
The fundamental container. A panel has a head (label + meta + actions) and a body. Panels never nest more than one level — if you need to, the inner thing is a card, not a panel.
Anatomy
--density-pad.rounded-[10px] border border-rule
bg-bg-1. Head: border-b,
px-[var(--density-pad)] py-[var(--density-pad-y)].
Headline is uppercase 11px tracked-wide; meta is mono 10.5px dim.
Composer shadcn: Textarea
The text input where the operator addresses the conductor. Auto-grows to ~6 lines, then scrolls. Slash-commands and @-mentions are the only adornments.
Default
<Textarea> wrapped in
a styled container. The footer is a peer flex row, not part of the
textarea. Send button is primary; ⌘↵ shortcut.
Chat row
The conversation between the maestro (you) and the conductor player. Three voices: the conductor talks back to you in plain rows; your messages are right-aligned cards with a terracotta edge; routed messages between the conductor and other players are visually demoted (smaller avatar, italic body, "overhearing" rail) — present but not loud, because they're not addressed to you.
Three voices
.msg): grid
28px 1fr, treble-clef avatar in conductor gold, body in
plain text up to 72ch wide. Maestro
(.msg.out): right-aligned card,
--bg-chat-out background, 3px terracotta right border,
78% max-width, no avatar — the right-edge accent is the
identity. Route (.msg.route): indented,
shrunk avatar (20px), italic body in --text-2, and a
2px rail on the left to convey "overhearing." This is critical: in a
dashboard with hundreds of inter-agent messages, you must instantly
tell which ones are for you.
Roster row
A single player, condensed. Avatar, name, phase chip, last-event snippet, host. Dense — operator scans this list often.
Anatomy
--bg-2. Avatar
is a serif italic letter from the player type name — no images.
Event row
The audit trail. Mono, dense, no avatars. Time on the left, actor in accent, action in neutral. Filterable but never decorated.
Stream
Tempo strip
A horizontal sparkline of message activity over the session window —
one bar per minute, recent bars in --accent, older bars
in neutral. Current bpm sits in the top-right as an overlay. There's
no ticking dot, no beat grid in the visible sense — the sparkline
is the tempo. It tells you "how loud is this ensemble right
now, and is it speeding up or slowing down."
Default
.tempo-strip-label) and an SVG that fills 100% width
via preserveAspectRatio="none" — bars stretch to the
viewport. Bar width 4px + 2px gap; every 10th column gets a dashed
ruler line so the eye can chunk time. Color rule:
last 10 bars (the most recent ~10 minutes of activity) render in
--accent; older bars render in --rule-strong
at 0.75 opacity. bpm overlay is computed
server-side from the rolling-window message count, not from a fake
metronome — it's a real signal. Sits in the page header above the
workspace, full-width.
Key/value table
Player metadata, ensemble settings, host configuration. Two columns: label dim/uppercase, value default. Mono for IDs, sans for everything else.
Player metadata
Modal & sheet shadcn: Dialog · Sheet
Modals interrupt; sheets reveal. Use a modal for confirmations and quick forms (≤ 4 fields). Use a sheet for player detail, longer forms, and anything with its own scroll.
Modal · confirmation
--err.
Wizard
Multi-step flows: create ensemble, recruit player, add host. Steps listed at the top, current step bold + accented, completed steps muted with a check, future steps dim.
Stepper
Status colors
Five total. Each has a single canonical use and a single canonical soft-fill. Don't tint backgrounds with status colors — use them for ink, borders, or 1-pixel accents only.
Status semantics
| Token | Meaning | Used for |
|---|---|---|
| --ok | Healthy, active, succeeded | ▶ playing chip · tempo heartbeat ✓ · stepper checkmark |
| --warn | Paused, stale, attention | ⏸ paused chip · slow-host indicator · "1 player idle" |
| --err | Failed, missed, destructive | ● error chip · disband button · failed heartbeat |
| --info | Neutral notice, route hint | conductor route arrow · system info banners |
| --accent | Brand, primary action, mention | primary CTA · brand mark · conductor name · @mention |
Scroll & focus
Scrollbars are quiet. Focus rings are unmistakable. The dashboard is a keyboard-first tool — every interactive element must show a clear focus state.
Focus ring
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2.
shadcn's focus-visible:ring is rebound to our accent
via --ring.
Scrollbar
Tech stack
How the design lands in code.
Stack
| Layer | Choice | Notes |
|---|---|---|
| Framework | React 18 · Vite or Next | SPA-style; SSR optional, not required |
| Styling | Tailwind 3 + CSS vars | Tokens live as CSS vars; tailwind.config maps to them |
| Components | shadcn/ui | Button, Dialog, Sheet, Textarea, Badge, Input, Select, Tabs, Tooltip |
| Icons | lucide-react | 1.5px stroke · 16px default · accent only when state-bearing |
| State | Zustand or React Query | Server state via Query; UI state local or Zustand |
| Routing | react-router or Next app router | Routes mirror sidebar: /ensembles/:id, /hosts, /settings |
| Realtime | SSE or WebSocket | Tempo + event log are streamed; chat is request/response |
tailwind.config.ts
export default { content: ['./src/**/*.{ts,tsx}'], theme: { extend: { colors: { bg: 'var(--bg)', 'bg-1': 'var(--bg-1)', 'bg-2': 'var(--bg-2)', text: 'var(--text)', 'text-2': 'var(--text-2)', dim: 'var(--dim)', rule: 'var(--rule)', accent: 'var(--accent)', ok: 'var(--ok)', warn: 'var(--warn)', err: 'var(--err)', info: 'var(--info)', }, fontFamily: { sans: ['Instrument Sans', 'ui-sans-serif', 'system-ui'], display: ['Instrument Serif', 'serif'], mono: ['JetBrains Mono', 'ui-monospace'], }, borderRadius: { md: '6px', lg: '10px' }, }, }, }
File layout suggestion
src/
styles/
globals.css // tokens + density + base
components/
ui/ // shadcn primitives (Button, Dialog, …)
Panel.tsx // Maestro panel wrapper
Composer.tsx
ChatRow.tsx
RosterRow.tsx
EventRow.tsx
TempoStrip.tsx
Wizard.tsx
pages/
Overview.tsx
Ensemble.tsx // the workspace
Host.tsx
Settings.tsx
lib/
bus.ts // SSE / WS client
tokens.ts // density level setter