Show structure. Hide ornament.
Hairlines, generous whitespace, strong type hierarchy. No drop shadows, no gradient backgrounds, no decorative blobs. If a pixel doesn't earn its place, cut it.
One token model, two surfaces. The product's shipped tokens are canonical; the marketing site vendors the same colors, type, and radius. Direct. Confident. Never decorative.
Paperclip is the app people use to manage AI agents for work. The brand and the product share one design system: serious software for serious work — direct, confident, never decorative.
Hairlines, generous whitespace, strong type hierarchy. No drop shadows, no gradient backgrounds, no decorative blobs. If a pixel doesn't earn its place, cut it.
Same tokens, same type ramp, same motion. The marketing site feels like a continuation of the product, not a different app. Differences are density and content, not vocabulary.
The 1:2 capsule shape is reserved for agent representation — capsule fields, org chart nodes, status indicators, avatars. Never on chrome, never on buttons. That's why it stays meaningful.
Pause, resume, override, reassign, terminate. The visual language emphasizes legibility, traceability and accountability. Buttons read like commands. Status reads like telemetry. The interface never pretends the agent is choosing — the operator is.
One token model spans two production surfaces. The product's ui/src/index.css is canonical; the website vendors those shipped tokens (npm run tokens:sync) and layers a few documented brand exceptions. Theme, density, and content adapt; the vocabulary doesn't.
Editorial, persuasion-first. Next (Vercel) in production; Astro is frozen legacy. Vendors the product tokens; keeps Inter Tight + JetBrains Mono + pill CTAs as brand exceptions.
Grid-disciplined, scanning-first. Light + dark. React 19 + Tailwind v4 (@theme, no config) + shadcn/Radix. Defines the tokens.
The Paperclip mark is a minimalist line drawing of a paperclip. Functional. Recognizable at any size. Always rendered as a stroke — never filled, never decorated.
Maintain minimum clear space equal to the icon width on all sides. Below 16px, drop the wordmark and use the icon only.
Use the stroke-based mark. Pure currentColor — adapts to surface.
Never fill the paperclip shape. The line is the brand.
No gradients. No drop shadows. No effects.
Never rotate, skew, or transform the mark beyond the default ‑12°.
Always title-case. Not "PaperClip", "paperclip", or "PAPERCLIP".
Never substitute the wordmark typeface. Inter Semibold only.
Inter Tight carries display and editorial weight. Inter does every other word on the page. JetBrains Mono carries the engineering credibility — telemetry, code, timestamps. Nothing else.
The display voice. All headings, display copy, big moments. Default weight 600 — tight tracking provides the editorial gravity without leaning on bold.
font-family: 'Inter Tight', sans-serifweight: 600 · line-height: 1.02–1.2 · letter-spacing: -0.04em → -0.02emThe workhorse. Body, labels, navigation, buttons, the wordmark. ss01 + cv11 are enabled site-wide.
font-family: 'Inter', sans-seriffont-feature-settings: 'ss01', 'cv11'The engineering layer. Timestamps, hex codes, agent names, tool calls, terminal, telemetry chips. tabular-nums always on.
$ paperclip status --agent ceo ▸ alive budget 64% 4h 32m
font-family: 'JetBrains Mono', monospaceDocumented brand exceptions: Inter Tight (--font-serif) and JetBrains Mono (--font-mono) are the website's display and mono faces. The product uses self-hosted InterVariable for everything — headings included (--font-heading = --font-sans) — and the system ui-monospace stack for mono. Inter (body) is shared across both.
Use the role, not the size. Display roles use Inter Tight at 600. Body and labels use Inter. Mono is JetBrains Mono.
| ROLE | SAMPLE | SPEC |
|---|---|---|
| display-1 | Headline. | clamp(42, 5.5vw, 80) · 600 · ‑0.04em |
| display-2 | Section heading | clamp(40, 5.5vw, 72) · 600 · ‑0.035em |
| display-3 | Subsection | clamp(28, 3.5vw, 44) · 600 · ‑0.03em |
| display-4 | Card title | 26 · 600 · ‑0.02em |
| lede | Subhead / hero lede | 20 / 1.45 · 400 · 0 |
| body-lg | Body large — feature copy | 18 / 1.5 · 400 · 0 |
| body | Body — running text, paragraphs, default UI copy. | 16 / 1.6 · 400 · 0 |
| body-sm | Body small — secondary text, table cells, dense UI. | 14 / 1.5 · 400 · 0 |
| caption | EYEBROWS · LABELS · CATEGORY TAGS | 11 · 500 · +0.08em · UPPER |
| mono | $ paperclip status — 1042 events | 13 / 1.4 · 400 · tabular-nums |
| mono-sm | 14:32:07 · #1042 · CEO Hermes | 11 / 1.4 · 400 |
Agents wake on a schedule, check their work, and act. Delegation flows up and down the org chart automatically.
Pause. Resume. Override. Reassign. Terminate.
Inter Tight at 600. Tight tracking. Sentence-case. The tight spacing does the editorial work.
No all-caps display. No loose tracking on display sizes. Caps are reserved for the caption role.
Use the mono stack for timestamps, IDs, tool calls, agent names, hex values, terminal output. tabular-nums keeps columns aligned.
Don't dress system telemetry in editorial type. Mono is the layer that signals "this is real, this happened, this was logged."
A neutral spine, color only where color carries meaning. The product ships an OKLCH grayscale — white surfaces separated by borders, not tonal fills — and the website vendors the same scale. The warm "manila" ramp has been retired: it survives only inside the agent-4 capsule gradient. Status colors carry product meaning; the agent palette is saturated gradients reserved for agents and org-chart nodes, never for chrome or buttons.
The product's canonical neutrals are OKLCH grays (--background → --foreground); the website surfaces them as named literals so components can reference a fixed tone. There is no warm cast — --card equals --background (both white), and surfaces are told apart by --border, not by tinted fills. manila, linen, and vellum are retired aliases that now resolve to white / neutral gray.
Components reference these — they swap with theme. In the product they are OKLCH (--background, --foreground, --card, --muted-foreground, --border, --primary); the website maps them onto the compat names below. Note --surface equals --bg in light: cards are white and separated by --rule, not a tonal fill.
| TOKEN | LIGHT | DARK | USAGE |
|---|---|---|---|
| --bg | bond · oklch(1 0 0) | charcoal · oklch(0.145) | Page background (--background) |
| --surface | bond (= --bg) | oklch(0.205) | Cards · panels (--card) — bordered, not filled |
| --rule | stone · oklch(0.922) | white / 10% | Hairline borders + dividers (--border) |
| --text | ink · oklch(0.145) | off-white · oklch(0.985) | Primary text (--foreground) |
| --text-2 | graphite | #A3A3A3 | Secondary text · subhead |
| --text-muted | aluminum · oklch(0.556) | oklch(0.708) | Captions · placeholders · mono labels (--muted-foreground) |
| --accent | ink (neutral) | off-white | Primary action · focus (--primary) — neutral; green is deferred |
Six semantic colors carry meaning across both surfaces — gray is a first-class status color (inert / resting), not just chrome. Each card documents the full chip recipe per theme: light chips use a tinted fill + saturated text; dark chips use a translucent fill of the solid hue, a solid-color text, and a 45%-alpha border. Alpha suffixes (e.g. #2563EB·18%) are written in the codebase as 8-digit hex — #2563eb2e.
Violet is a shipped status token — --status-task-in_review (#7C3AED), rendered with the CircleDot glyph. It's scoped to the in_review state, not a general-purpose chrome color. Full chip semantics + live examples in §13 States & Status.
Brand use only. Twelve saturated gradients reserved for the capsule motif — agent representation, org-chart nodes, status indicators, testimonial avatars. Never on buttons, status pills, or generic chrome. That's why the palette stays meaningful.
Use ink + neutral grays for chrome. Green is a status color (success / done), not the accent — it earns its place only when a state truly is "succeeded."
No gradient buttons. No agent-palette colors on chrome. Saturation in chrome dilutes the brand-language signal.
Agent palette belongs on the capsule motif — agent representation only.
Never on buttons or product chrome. A gradient badge is fine as a marketing flourish — the website landing page uses one — but the palette is never a product-UI decoration kit.
The website's rhythm is a small t‑shirt scale on an 8‑pixel base, with the large end expressed in CSS clamp() so whitespace stays generous on big screens and collapses gracefully on small ones. The product deliberately ships no fixed spacing scale — its DESIGN.md defers the scale to a human and only requires that spacing route through tokens, never raw px.
These are the website tokens (src/styles/global.css). Steps xs–md are fixed; lg and up are fluid. --space-section is the single vertical‑rhythm token for marketing sections.
| TOKEN | VALUE | VISUAL | USAGE |
|---|---|---|---|
| --space-xs | 0.5rem · 8px | Inline gaps, chip padding, label→input | |
| --space-sm | 1rem · 16px | Tight card padding, list row gap | |
| --space-md | 1.5rem · 24px | Default card padding, control gaps | |
| --space-lg | clamp(2rem, 1.5rem + 1.5vw, 3rem) | Card‑to‑card spacing in marketing | |
| --space-xl | clamp(3rem, 2rem + 3vw, 5rem) | Subsection blocks | |
| --space-2xl | clamp(4rem, 2.5rem + 4.5vw, 7rem) | Large editorial gaps | |
| --space-3xl | clamp(5rem, 3rem + 6vw, 10rem) | Page‑top, hero padding | |
| --space-section | clamp(6.25rem, +0.75vw, 6.9rem) | fluid | Marketing section padding‑y (single rhythm token) |
| --space-section-gap | clamp(2rem, 5vw, 6rem) | fluid | Gap between stacked sections |
Product note: the product has no --space-* scale — spacing uses Tailwind's step utilities through tokens, and the canonical scale is deliberately deferred (DESIGN.md: "spacing routes through tokens; the final scale is a human decision"). Don't cite a numeric product spacing token — there isn't one yet.
Radius isn't a set of magic numbers — it's a single knob. --radius: 0.5rem (8px) is the anchor, and every other step is calc()'d off it, so the entire corner language retunes from one variable. Shape still carries meaning: a rounded rect is a card, a full‑round pill is a badge, and a stadium capsule is an agent.
--radius: 0.5rem = rounded-lg = 8px is the anchor. Every step is calc(--radius × n), so the scale is strictly monotonic and retunes from one variable. The website exposes --r-sm / --r-md / --r-lg mapped onto it.
4.8 → 6.4 → 8 (lg · anchor) → 11.2 → 14.4 → 17.6 → 20.8 px · usage: sm inputs/chips, md buttons (rounded-md), lg cards/popovers, xl+ dialogs/sheets
Badges · status pills · the marketing CTA (a website brand exception)
Agents only — a 1:2 stadium (AgentCapsule)
In the product, buttons are rounded-md — pill is reserved for badges, status pills, and the agent capsule. The marketing site keeps a pill primary CTA as a documented brand exception, echoing the capsule at the chrome level; everything else uses the rounded‑rect ladder.
Button = rounded-md. Badge + capsule = rounded-full. Each shape reads as a distinct kind of thing.
Don't square off buttons or badges, and don't invent per‑component radii. Retune the one --radius anchor instead.
Icons come from lucide-react ^0.577.0 — one library, consistently stroked. They label actions, states, or functionality; they never compete with the capsule motif for attention. The task-status glyphs in §13 are a canonical subset (CircleDashed, Circle, RotateCw, CircleDot, CircleCheck, CircleMinus, Ban).
Stroke only. currentColor. Rounded line caps + joins.
No filled glyphs in chrome. Fills compete with capsule-motif fills.
No emoji in product UI or technical docs. Emoji is a different visual language.
Every agent is a capsule. Every capsule is an agent. The mark and the metaphor are the same shape.
Construction: border-radius: 9999px (or 50% of width). Fill: smooth linear-gradient(to bottom, anchor-a, anchor-b). Flat single-color variant available for contexts that need a solid mark.
Eight agents. Each its own gradient. Different heights signal cost, status, or output.
One initial on the agent gradient until a profile photo is uploaded. Same gradient classes as capsules — every visual of an agent shares one DNA.
The graphic generator produces on-brand artifacts — OG images, blog covers, social posts, slide backdrops — by arranging capsules into templates and colorizing them from palettes. Below: a mini, in-doc version. Production tool: paperclip-graphic-generator.
Paperclip speaks with authority about complex systems but never obscures meaning. Short sentences. Active verbs. Technical when needed, never when not.
"Paperclip uses those agents. It orchestrates them into a company — with org charts, budgets, goals, governance, and accountability."
"Pause. Resume. Override. Reassign. Terminate. You have full control over every agent in the org."
"Not a chatbot. Not an agent framework. Not a workflow builder."
"Full observability. Every task traced. Every decision explained. Every tool call logged."
"Paperclip is a revolutionary, AI-powered next-gen platform that empowers teams to leverage the full potential of their agentic workforce!"
"Paperclip orchestrates a team of AI agents. Org charts, budgets, goals. You hire. You approve. You ship."
"Various optimizations have been applied to ensure that runaway costs are mitigated through robust budget enforcement mechanisms."
"Every agent gets a monthly budget. When they hit it, they stop. No runaway costs. Hard limits, enforced by the system."
"🚀 Get started with Paperclip today and unlock the future of work! 💼✨"
"Open source. Self-hosted. npx paperclipai onboard and you're running."
Animation signals life and state. Entrance reveals stagger. Active elements pulse. Transitions ease at 250ms. Nothing bounces unless it has a reason — and almost nothing does.
These are the website's motion tokens (src/styles/tokens.css). The product ships no --ease/--dur tokens — its motion lives in named @keyframes and utility classes instead (see below).
| TOKEN | VALUE | WHERE |
|---|---|---|
| --ease-enter | cubic-bezier(.2,.8,.2,1) | Entrance, hover lifts, primary motion |
| --ease-exit | cubic-bezier(.4,0,1,.6) | Exit, dismissal |
| --t-micro | 150ms | Hover color shifts, focus rings |
| --t-short | 250ms | Default UI transitions, button hover |
| --t-medium | 400ms | Theme swap, panel slide-in |
Product motion = keyframe utilities. Reserved for states that demand attention: .hb-pulse (1.6s — running agents), .hb-blink (1.2s — errored agents), .typing-dots, .shimmer-text, .activity-row-enter, .cot-line-enter/-exit. All honor prefers-reduced-motion.
450ms ease-out · 140ms stagger · translateY 12 → 0
1.6s ease-in-out · infinite · alive · scale 0.85 ↔ 1.0
200ms ease-out · color shift only · no transform, no bounce
700ms ease-out · opacity 0 → 1 · translateY 24 → 0
400ms ease-out · background + color crossfade
Every motion pattern has a no-motion fallback. If the user prefers reduced motion, animations stop or shorten to a 100ms color crossfade.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 100ms !important;
scroll-behavior: auto !important;
}
}
The product ships one component per job — shadcn/ui patterns on Radix primitives, variants via class-variance-authority, icons from lucide-react. Everything renders in light and dark; every interactive control has explicit default, hover, focus, active, and disabled states. Marketing chrome (pill CTAs, hero flourishes) is a documented brand exception layered on the same tokens.
alert-dialog · avatar · badge · breadcrumb · button · card · checkbox · collapsible · command · dialog · dropdown-menu · input · label · popover · radio-card · resizable-panels · scroll-area · select · separator · sheet · skeleton · tabs · textarea · toggle-switch · tooltip. One of each — variants are props, not new components.
The Button (CVA) ships six variants: default · secondary · outline · ghost · destructive · link, all rounded-md in the product. Two semantic treatments override the fill by meaning — approve is green (bg-green-700), terminate red (destructive). The marketing site renders its primary CTA as a fully-rounded ink pill — a documented brand exception. Green is never the neutral accent; it earns its place only on the approve action.
Radios ship as radio-card (a selectable card, not a bare dot). There is no slider or stepper primitive — a numeric budget is a plain input.
There is one Card. It's a bordered white surface (--card = --background, separated by --border) — the variants below are compositions (content, an inverted dark callout, a selected state), not separate components.
Agents wake on schedule or on demand. Every action logged.
Same card pattern, dark surface. Used in marketing callouts and product modals.
Radix-backed overlays. A confirming Dialog and a Popover are shown; the same family covers alert-dialog (destructive confirms), sheet (side panels), and tooltip. There is no toast primitive.
The onboarding terminal is a marketing depiction. In-product, rich text and code run through @mdxeditor/editor + CodeMirror; both use the --font-mono token and the same neutral surfaces.
$ npx paperclipai onboard --yes ✓ Embedded postgres started ✓ Company created: note-taking-app ✓ CEO Hermes online (claude-sonnet-4-5) ✓ Dashboard at http://localhost:3100 $ ▍
// Self-host config export default { company: 'note-taking-app', db: 'postgres://localhost', heartbeat: 4, // hours budgets: { ceo: 60 }, }
Charts read like instruments. Ink lines on hairline grids. Mono labels. No gradients in chart fills — saturation stays in the agent palette where it carries meaning.
| AGENT | BUDGET USED | COST |
|---|---|---|
| CEO Hermes | $23.40 / $60 | |
| CMO OpenClaw | $32.00 / $40 | |
| CTO Codex | $50.00 / $50 | |
| COO Claude | $5.40 / $30 |
One color language across every entity — agents, tasks, projects. The governing rule: blue = liveness. Actively running work is blue; queued, idle, or inert work is not. Every status below is rendered with the real chip CSS, so what you see is the spec.
Six semantic colors. Each is a brand .task-chip: a 1px-bordered pill with distinct light and dark values. Light chips use a tinted fill + saturated text; dark chips use a translucent fill of the solid hue + solid-color text + a 45%-alpha border. Toggle the page theme to see both states live.
| COLOR | CHIP | LIGHT — BG / TEXT / BORDER | DARK — BG / TEXT / BORDER |
|---|---|---|---|
| gray inert / resting |
gray | #F5F3F0 / #52585D / #A8AEB2 | #6E6960·14% / #9A958A / #9E958A·45% |
| blue live / running |
blue | #DBEAFE / #1D4ED8 / #2563EB | #2563EB·18% / #5B8DEF / #2563EB·45% |
| amber queued / paused |
amber | #FEF3C7 / #B45309 / #F59E0B | #F59E0B·14% / #F59E0B / #F59E0B·45% |
| green done / online |
green | #DCFCE7 / #188A3C / #22C55E | #22C55E·12% / #34D06F / #22C55E·45% |
| red error / blocked |
red | #FEE2E2 / #991B1B / #DC2626 | #DC2626·18% / #EF4444 / #DC2626·45% |
| violet awaiting review · reserved |
violet | #EDE9FE / #5B21B6 / #7C3AED | #7C3AED·18% / #A78BFA / #7C3AED·45% |
Alpha suffixes (e.g. ·18%) are written in the codebase as 8-digit hex — #2563eb2e = #2563EB at 18% alpha. Violet is a shipped chip — it's the sixth entry in brandChipBadge, scoped to task in_review (it isn't a general-purpose chrome color).
Every task lives in exactly one of these states. The blue = liveness rule applies: in_progress is blue (actively worked), todo is amber (queued — ready but not live). The icon, color, and copy are fixed so the same status reads identically on the board, in a row, and in a chip.
| STATUS | LUCIDE ICON | GLYPH HUE — --status-task-icon-* (light / dark) |
|---|---|---|
| backlog | CircleDashed | #52585D / #9A958A |
| todo | Circle | #CC7A00 / #FBBF24 |
| in_progress | RotateCw | #2563EB (both) |
| in_review | CircleDot | #7C3AED / #9474F0 |
| done | CircleCheck | #16A34A / #34D06F |
| blocked | CircleMinus | #DC2626 (both) |
| cancelled | Ban | #52585D / #9A958A |
| in_queue (synthetic) | CircleMinus → blue | #2563EB |
| SUB-STATE | CHIP | COLOR | SELF-HEALS? |
|---|---|---|---|
| In queue blocked behind a live child |
In queue | blue | Yes — unblocks when the child finishes. No action needed. |
| blocked plain blocked = needs a human |
Blocked | red | No — needs a human decision / triage / unblock. |
The agents list renders four states. The indicator is a heartbeat capsule (8×16, r4, solid fill); the row chip is the matching brand chip. active aliases idle; terminated and pending_approval are excluded from the list — the hiring gate lives in the task thread as a violet in_review task.
| STATUS | CAPSULE | CHIP | COLOR | BEHAVIOUR |
|---|---|---|---|---|
| idle active aliases this |
idle | gray | Online, no active run — the resting / available state. Capsule is still. | |
| running | running | blue · live | A heartbeat is executing right now. Capsule pulses — hb-pulse 1.6s. | |
| paused | paused | amber | Halted — manual / budget / system. Capsule is still. (Amber, not gray, so it doesn't collide with idle.) | |
| error | error | red | The agent itself is broken (e.g. not connected to a model) — distinct from a task failing. Capsule blinks — hb-blink 1.2s. |
Five project statuses. Unlike tasks, projects render as text-only chips (the generic statusBadge — Tailwind classes, no glyph): in_progress is blue (liveness), completed green, and backlog / planned / cancelled are neutral muted. paused and archived are overlays, not enum values.
| STATUS | CHIP | COLOR | MEANING |
|---|---|---|---|
| backlog | backlog | gray · muted | Parked / unscheduled — not yet picked up. |
| planned | planned | gray · muted | Committed but not started (bg-muted, like backlog). |
| in_progress | in progress | blue · live | Actively being worked — has live work. Liveness. |
| completed | completed | green | Delivered / closed out (matches task done). |
| cancelled | cancelled | gray · muted | Abandoned, not resumed. Muted + strikethrough. |
| OVERLAY | CHIP | COLOR | MEANING |
|---|---|---|---|
| paused pauseReason |
paused | amber | Halted (manual / budget / system). Overlay on the base status — consistent with a paused agent. |
| archived archivedAt |
— hidden from the list (no chip) — | — | Filtered out of All Projects, like terminated agents. |
Colors mean the same thing whether the row is an agent, a task, or a project. When you build new UI, choose the color by what the state means, then apply the chip token.
| COLOR | MEANS | AGENT | TASK | PROJECT |
|---|---|---|---|---|
| blue | live — actively running now | running | in_progress · In queue | in_progress |
| amber | queued / paused — ready or halted, not live | paused | todo | planned · paused |
| green | done / online — finished or healthy | — | done | completed |
| violet | awaiting review — needs a human look | (hiring gate → task) | in_review | — |
| red | error / blocked — broken, needs a human | error | blocked | — |
| gray | inert — resting, parked, or cancelled | idle · terminated | backlog · cancelled | backlog · cancelled |
Motion is reserved for states that demand attention: only running pulses and only error blinks — idle and paused are still. Product token: 8×16, r4, solid fill (rendered larger here for display). Both animations honor prefers-reduced-motion.
Capsule grays are theme-aware: #A8AEB2 light / #6E6960 dark. Blue, amber, and red fills are literal in both themes.
Every chip above maps to these exact exports in ui/src/lib/status-colors.ts. The chip recipe (bg/text/border) and the standalone glyph hue are two layers: chips use brandChipBadge; a bare StatusGlyph next to text uses the AA-tuned --status-task-icon-* vars so it clears WCAG 3:1. Use the maps; don't hand-write hex.
// Brand .task-chip palette — light + dark, 1px border (all six colors)
export const brandChipBadge = {
gray: "bg-[#F5F3F0] text-[#52585D] border-[#A8AEB2] dark:bg-[#6e696024] dark:text-[#9A958A] dark:border-[#9e958a73]",
blue: "bg-[#DBEAFE] text-[#1D4ED8] border-[#2563EB] dark:bg-[#2563eb2e] dark:text-[#2563EB] dark:border-[#2563eb73]",
amber: "bg-[#FEF3C7] text-[#B45309] border-[#F59E0B] dark:bg-[#f59e0b24] dark:text-[#F59E0B] dark:border-[#f59e0b73]",
green: "bg-[#DCFCE7] text-[#188A3C] border-[#22C55E] dark:bg-[#22c55e1f] dark:text-[#22C55E] dark:border-[#22c55e73]",
violet: "bg-[#EDE9FE] text-[#5B21B6] border-[#7C3AED] dark:bg-[#7c3aed2e] dark:text-[#7C3AED] dark:border-[#7c3aed73]",
red: "bg-[#FEE2E2] text-[#991B1B] border-[#DC2626] dark:bg-[#dc26262e] dark:text-[#DC2626] dark:border-[#dc262673]",
};
// Status → brand color name
issueStatusColor = { backlog:"gray", todo:"amber", in_progress:"blue", in_review:"violet", done:"green", blocked:"red", cancelled:"gray" };
agentStatusColor = { idle:"gray", active:"gray", running:"blue", paused:"amber", error:"red" };
// projects have no brand-chip map — they render text-only via statusBadge (Tailwind)
// Chip base hue vs. AA-tuned GLYPH hue (two separate var layers)
taskStatusVar = { in_progress:"--status-task-in_progress", todo:"--status-task-todo", /* … */ };
taskStatusIconVar = { todo:"--status-task-icon-todo", done:"--status-task-icon-done", /* … */ };
// Standalone glyph = lucide-react ^0.577.0 (StatusGlyph.tsx)
statusGlyph = { backlog:CircleDashed, todo:Circle, in_progress:RotateCw,
in_review:CircleDot, done:CircleCheck, blocked:CircleMinus, cancelled:Ban };
// Heartbeat capsule (agents) — 8×16, r4, solid fill
agentStatusCapsule = { gray:"#A8AEB2 / dark #6E6960", blue:"#2563EB", amber:"#F59E0B", red:"#DC2626" };
agentStatusMotion = { running:"hb-pulse", error:"hb-blink" }; // durations in index.css keyframes
The core product's ui/src/index.css is the single source of truth: every value is a CSS custom property, exposed to Tailwind v4 through @theme inline — there is no tailwind.config file. The website vendors those shipped tokens and maps them onto its own names, kept aligned by npm run tokens:sync. Same values, one flow, no hand-copied hex.
Defined once in ui/src/index.css. The semantic layer is OKLCH and swaps under .dark; status hues and agent gradients are literal in both themes. Radius is a single anchor plus a calc() ladder.
:root {
/* ── Semantic — OKLCH, shadcn core (light) ── */
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0); /* = --background (bordered, not filled) */
--popover: oklch(1 0 0);
--primary: oklch(0.205 0 0); /* neutral primary / accent */
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0); /* … grayscale ramp … */
--chart-5: oklch(0.269 0 0);
/* ── Radius — one anchor, calc ladder ── */
--radius: 0.5rem; /* rounded-lg IS the anchor (8px) */
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
/* ── Status hues — one base hue per state (mode-independent) ── */
--status-task-backlog: #a8aeb2;
--status-task-todo: #f59e0b;
--status-task-in_progress: #2563eb; /* liveness */
--status-task-in_review: #7c3aed;
--status-task-done: #22c55e;
--status-task-blocked: #dc2626;
--status-task-cancelled: #a8aeb2;
--status-agent-idle: #a8aeb2; --status-agent-running: #2563eb;
--status-agent-paused: #f59e0b; --status-agent-error: #dc2626;
/* AA-tuned glyph hues get light+dark overrides, e.g.
--status-task-icon-todo: #cc7a00; --status-task-icon-done: #16a34a; */
/* ── Agent capsule gradients — fixed hex, identical both themes ── */
--agent-1a: #f7cfdc; --agent-1b: #1f7a3a; /* … through --agent-10a/10b … */
--agent-4a: #f3e6c4; --agent-4b: #e3a21a; /* the one surviving manila hex */
/* ── Type — self-hosted InterVariable + system mono ── */
--font-sans: "InterVariable", "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
--font-heading: var(--font-sans);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--primary: oklch(0.922 0 0); /* flips near-white on dark */
--muted-foreground: oklch(0.708 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
}
There is no tailwind.config.js. Tokens are wired to utilities in CSS via @theme inline, so bg-background, text-foreground, border-border, and rounded-lg resolve to the vars above — and dark mode flips through the .dark class without duplicating hex in TypeScript.
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
@theme inline {
/* semantic colors point at the CSS vars → dark mode flips for free */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-primary: var(--primary);
--color-secondary: var(--secondary);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
/* radius ladder + fonts */
--radius-sm: var(--radius-sm);
--radius-md: var(--radius-md);
--radius-lg: var(--radius-lg);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
}
/* → utilities: bg-background · text-foreground · border-border ·
rounded-lg · font-mono · text-muted-foreground … */
The website is a different stack (vanilla CSS · Astro + Next), so the product's shipped tokens are vendored, not imported. A generated copy carries the OKLCH neutrals/status/radius verbatim; a hand-authored map renames them to the site's component tokens plus the documented brand exceptions. It's CSS-to-CSS — there is no tokens.json or Figma export.
paperclip/ui/src/index.css CANONICAL — the product ships it
│
│ npm run tokens:sync (reads the core repo, stamps the commit)
▼
web/app/styles/core-tokens.css GENERATED · do not edit
│ the :root + .dark neutral / semantic / status / radius tokens,
│ copied verbatim (OKLCH). Product-only domain tokens omitted.
│
│ @import
▼
web/app/styles/tokens.css CANONICAL MAP · Next (web/) = production
│ --bg = var(--background) --rule = var(--border)
│ --surface = var(--card) --r-md = var(--radius)
│ --success/--warning/--error/--info = var(--status-task-*)
│ BRAND EXCEPTIONS (website ≠ product, on purpose):
│ --font-serif = Inter Tight 600 (product headings = Inter)
│ --font-mono = JetBrains Mono (product = system ui-monospace)
│ --color-accent = var(--ink-t) (neutral; green swap deferred)
│ --r-pill = 9999px (pill CTA — website only)
│
│ npm run tokens:sync mirrors DOWN to
▼
src/styles/tokens.css FROZEN Astro mirror · never hand-edit
# verify: grep -ri manila src web → only #f3e6c4 (agent-4 gradient)