Overdeck dashboard UI style guide — canonical typography, color-signal semantics, badge formula, and color-restraint rules, for both the Ledger (legacy) and Broadsheet (default) themes. Use whenever writing, reviewing, or mocking up dashboard frontend UI (components, badges, colors, fonts, status indicators, kanban cards, tree rows). Prevents the common violations: Inter as a font, bold/semibold outside its weight tier, pill badges, decorative color, green "running" agents, cyan misuse.
Install
npx skillscat add eltmon/overdeck/pan-style-guide Install via the SkillsCat registry.
Overdeck Style Guide (dev skill)
Sources of truth — read in this order
- The law:
design/style-guide/STYLE-GUIDE.md(v2.0) — typography canon
for both themes, color system, surfaces, radius/spacing scales, component
specs, forbidden patterns. Everything below is a distillation; when in
doubt, the guide wins. - Visual reference:
design/style-guide/mockups/system-map.html— the
PAN-1148 unified-redesign system map; open in a browser. Section
05 · Color discipline is the signal-color table as live swatches;
sections 02–03 show the surfaces and shared primitives composed. For the
Broadsheet-specific vocabulary (display scale, chips, soft cards, keycap
CTAs, dot-metadata lines), seedesign/style-guide/mockups/style-guide-v2.html— the working prototype
the theme-scoped tokens and pattern recipes were ported from. - Written tightening:
docs/prds/planned/pan-dashboard-unified-redesign.md
§4.5 ("Color & Style Discipline") — the PAN-1148 "Always means / Never used
for" table now folded into the guide. - Tokens:
src/dashboard/frontend/src/index.css— the unscoped:root/.darkblocks (Ledger's untouched default),@font-face
declarations, theme font tokens, the.display-xl/.display-lg/.eyebrowBroadsheet utilities, and, after.darkcloses, the PAN-3706
four-scope region carrying every color/surface/border/shadow/radius/
type/motion token plus the.chip/.soft-card/.large-cta/.keycap/.dot-metadatacomponent patterns — andsrc/dashboard/frontend/ tailwind.config.js(boxShadow,fontFamily.mono-ui, and thesafelistentries for classes not yet consumed by any.tsxfile).
Conformance is exercised bytests/e2e/styleguide-conformance.spec.ts.
Themes: Ledger (legacy) vs. Broadsheet (default)
Overdeck ships two named design languages behind a single ui.theme setting
(~/.overdeck/config.yaml, default broadsheet) and a data-theme attribute
on <html> (independent of the .dark class, which carries light/dark
color-scheme — the two axes never interact). Ledger is the pre-PAN-3410
style — DM Sans, flat font-medium cap, cool blue-gray neutrals, no display
tier — now frozen: bug fixes only, never new patterns. Broadsheet is the
active theme: Geist/Geist Mono, a tiered weight system, a display scale, and
the new component vocabulary below — plus, as of PAN-3706, its own warm
neutral color ramp, stepped surfaces, opaque borders, a shadow/elevation
scale, a smaller base radius, 13px/1.6 body type, and an ambient-wash
background in place of Ledger's noise film. Before PAN-3706 the color/
surface/elevation/radius/texture system was accidentally shared between both
themes (a font-only migration); it is not anymore — see "Color, surface, and
elevation now differ by theme" below.
The theme rule for new surfaces: author against the Broadsheet
vocabulary (.display-xl, .display-lg, .eyebrow, chips, soft cards,
keycap CTAs, dot-metadata lines) using the class names as given. Never writeif (theme === 'broadsheet') or any other conditional-markup branch. This is
not optional — a component that branches on theme is a style-guide violation
regardless of which theme it produces correct output for.
Only three of those classes actually have a Ledger-safe fallback — they
resolve to a scaled-down value at :root instead of being conditionally
rendered, so a surface built from them degrades in scale and texture under
Ledger but never breaks:
| Broadsheet class | Ledger-safe fallback |
|---|---|
.display-xl |
text-xl font-medium |
.display-lg |
text-lg font-medium |
.eyebrow |
text-xs uppercase tracking-wider font-medium |
Chips, soft cards, keycap CTAs, and dot-metadata lines have no Ledger
fallback — no :root or [data-theme="ledger"] rule exists for them at all.
This is authoring discipline, not a CSS or runtime guard: nothing detects or
blocks their use under Ledger, they would just render unstyled (bare Tailwind
utilities, no theming) if reached while Ledger is active. A component built
with them is a Broadsheet-only component by construction; don't reach for
them on a surface that must still render under Ledger.
Color, surface, and elevation now differ by theme (PAN-3706)
The color system is no longer identical between themes (it was, pre-PAN-3706
— see guide §2/§4). :root/.dark in index.css stay the untouched Ledger
default; Broadsheet's real values live in a later region that overrides them
per the four-scope contract below. What's still shared: signal-color
semantics (what --destructive/--warning/etc. mean) and the badge tint
formula (8%/32%). What now differs: the neutral ramp (warm HSL vs cool
gray), --background/--card/--popover step apart under Broadsheet
instead of collapsing to one flat color, --border/--input are opaque
under Broadsheet instead of near-invisible color-mix(), --radius itself
(10px Ledger / 6px Broadsheet — not just the badge tier), a role-named--shadow-sm/md/lg/xl/card/floating/overlay scale that actually resolves to
something visible on dark surfaces, body type (13px/1.6/antialiased under
Broadsheet vs. browser default under Ledger), and the background treatment
(Ledger keeps the fractal-noise film; Broadsheet swaps it for an ambient
corner wash via a new --wash token — not a retargeted --accent, which
stays the hover-surface token in both themes). New tokens: --primary-ink
(text-safe accent), --body-foreground/--faint-foreground (rounding--foreground/--muted-foreground out to a four-tier ramp), --wash, a--type-size-*/--type-leading-*/--type-tracking-* vocabulary (defined,
not yet adopted by components), and --ease-*/--duration-* motion tokens
(same — defined, not yet wired into existing animations). --font-mono-ui
splits UI-chrome monospace (labels, IDs, timestamps) from --font-mono
(code + the eyebrow exception) — Ledger resolves both to the same SF Mono
stack, Broadsheet gives UI-mono a tighter system stack instead of Geist
Mono's code-tuned metrics.
The four-scope selector contract is what makes every one of those tokens
resolve correctly by theme. Because .dark and data-theme both live on<html> at once, and because Settings → Appearance nests a [data-theme]
scope inside the ambient document for its comparison specimens, a
theme-varying token needs four blocks, placed after :root/.dark in
source order (equal-specificity attribute selectors win on source order, not
specificity):
[data-theme="ledger"] — light, restate today's value
[data-theme="broadsheet"] — light, new value
.dark[data-theme="ledger"], .dark [data-theme="ledger"] — dark, restate today's value
.dark[data-theme="broadsheet"], .dark [data-theme="broadsheet"] — dark, new valueSame-element (.dark[data-theme=...]) matches <html> itself; descendant
(.dark [data-theme=...]) matches a nested Appearance-picker specimen —
both are required or a nested specimen inherits the wrong theme. For a
non-color-token rule (a body background, a utility class), gate it on a
custom property with a var() fallback rather than a bare descendant
selector — --badge-radius and the .display-xl/.display-lg/.eyebrow
properties are the established examples; a descendant selector matches
through any ancestor with the attribute and breaks nesting, a custom
property cascades by DOM proximity and doesn't. Full mechanics: guide §4a.
The rules agents violate most (memorize these)
Typography — Ledger (legacy baseline, verbatim)
- DM Sans for ALL UI prose. Inter, SF Pro, Segoe UI, -apple-system are
deprecated — never reintroduce. - SF Mono (
font-mono) ONLY for technical identifiers: code, paths,
branch names, PR numbers, issue IDs, session/run IDs, model IDs, hashes,
env vars. Never on titles, badge labels, or button text. - Space Grotesk (
font-display) ONLY for the sidebar "Overdeck"
wordmark. Nowhere else (God View is the lone scoped exception). font-medium(500) for everything. No semibold, no bold. Hierarchy
comes from size and color contrast, not weight.
Typography — Broadsheet (tiered weights replace the flat cap)
- Geist Variable (
font-body/font-display) for ALL UI prose and the
sidebar wordmark. Geist Mono Variable (font-mono) for technical
identifiers and the eyebrow pattern below — the one deliberate widening
of the mono rule. All four families (Geist, Geist Mono, DM Sans, Space
Grotesk) are self-hostedwoff2— never a Google Fonts<link>, never a
hardcoded font-family string in component code. - Weight tiers, not a flat cap: body/UI 400–500, headings 500–600,
display tiers only 600–800 (.display-xlweight 750,.display-lg
weight 680). Nothing outside.display-xl/.display-lgmay exceed 600 —
"no bold outside its tier" replaces Ledger's "no bold anywhere." - Display scale (
.display-xl,.display-lg) — page/section heroes and
hero-editable titles only. One.display-xlper page.
Signal colors — each token means exactly one thing
| Token | Always means | Never |
|---|---|---|
--destructive (red) |
Action required — broken, stuck, failed, urgent | decoration, label backgrounds |
--warning (amber) |
A human must act — In Review phase, awaiting approval, paused | machine activity, costs |
--info/--primary (blue) |
A machine is working — running agents, In Progress | static state |
--signal-review (purple) |
Specialist activity — review/ship/planning verbs, live convoys | the In Review phase (amber) |
--success (emerald) |
Outcome — merged, done, gates passing | running agents (blue!), idle, queued |
--signal-cost (cyan) |
Money only, tabular numerals | token counts or any non-currency metric |
--muted-foreground |
The rest state — labels, idle, Backlog/Todo | hiding live signals |
Mnemonic: amber = human, blue = machine, emerald = outcome, red = broken,
purple = specialist verb, cyan = money, neutral = everything else.
State tokens (PAN-4197). Agent-state UI (Agents page badges, Live view rows, glyphs, left
rails) uses --state-*; every other signal use stays on --info/--warning/--destructive/--success.
Each has a -foreground text variant (≥ 4.5:1 on every surface, enforced bystate-tokens-contrast.test.ts). Classes: badge-bg-state-<s>, badge-border-state-<s>,text-state-<s>, bg-state-<s>.
| Token | Always means | Never |
|---|---|---|
--state-live (blue) |
A machine is working now | green, ever — live is blue, never green |
--state-needs-you (amber) |
The operator must act — blocked input, operator pause, ready to merge | machine waits |
--state-stuck (red) |
Stuck or broken — stale, API error, CI failed | idle agents |
--state-waiting (warm neutral) |
Waiting on the pipeline or idle | anything a human must answer |
--state-done (emerald) |
Finished | running agents |
Color restraint (data-dense views: kanban, trees, lists)
- Maximum ONE colored signal per card/row. When everything is colored,
nothing is. Labels (bug/feature/frontend) are taxonomy → always neutral. - Three-tier hierarchy: left border (priority/status) · column/group header ·
at most one special-state badge. All elsetext-muted-foreground. - Action links monochromatic; the only colored actions are the single primary
CTA (text-primary) and the single destructive action. - Exception: live, immediately-actionable agent-state badges (⚠ Stuck,
⏸ Paused + Unpause) may carry semantic color.
Badges & indicators
- Tint formula: 8% background / 32% border
(bg-{signal}/8 border-{signal}/32 text-{signal}-foreground) — identical
in both themes (D-11, unchanged). No pills (rounded-full) for status
badges. - Radius:
rounded-smunder Ledger,rounded-mdunder Broadsheet — the one
deliberate radius-tier change from PAN-3410, layered on top of PAN-3706's
smaller Broadsheet base--radius(10px Ledger / 6px Broadsheet), which
now also shrinks every otherrounded-*utility computed from it (buttons,
cards, dialogs — see "Color, surface, and elevation" above). Nothing else
about the badge formula (8%/32% tint) differs between themes. - Pick ONE indicator pattern per context — don't mix dots + badges + colored
text for the same concept in one view.
Broadsheet-only patterns (never under Ledger)
- Eyebrow — uppercase
font-mono,text-[11px],tracking-[0.12em],text-muted-foreground. Category/kicker labels and quiet text-buttons. - Chip —
rounded-lg h-9 border border-border, leading 16px icon.
Dashed variant (border-dashed) = additive/tentative action. Selected =border-foreground/40 bg-muted, never a color. - Soft card —
rounded-xl bg-muted/50 p-5, optional hairline border, no
hard border by default. Suggestion/idea/preview content. - Keycap hint —
rounded-md bg-foreground/8 px-1.5 font-mono text-xs
glyph (↵, ⌘L) inside or beside a large CTA button (h-12 rounded-xl).
One CTA per view — same single-primary rule as ordinary buttons. - Dot-metadata line — 6px status dot +
font-monoquiet text, items
joined by·. Ambient state summaries only.
None of these exist under Ledger — don't backport them, and don't gate them
behind a theme conditional (see "The theme rule for new surfaces" above).
Page-not-modal doctrine (theme-independent)
Major creation/composition flows (new project, new workspace, future
editors) are routed pages, not modals. Modals are reserved for
confirmations and forms of 4 fields or fewer. This applies under either
theme — it is not a Broadsheet-only rule. If a diff adds a modal for a
flow with more than 4 fields or a genuine "create X" action, that's a
finding.
Forbidden patterns (hard bans)
bg-gray-800 / text-white / border-gray-700 → bg-card / text-foreground / border-border
bg-blue-600 / text-blue-400 → bg-primary / text-primary
hardcoded hex (#22c55e, #ef4444, …) → semantic tokens
slate-* grays (cold) → neutral-* (warm) — everywhereSurfaces: depth via tonal layering (--card, --card-2), not shadows —
shadows stay reserved for floating elements only (dialogs, dropdowns,
tooltips, popovers, dragged items), never inline cards/panels. Border/wash
values are theme-scoped and no longer share one description: Ledger keeps
low-alpha color-mix() borders (white/6% dark, black/8% light,
near-invisible by design, light-mode cards borderless with an ambient
shadow instead) and the noise-film texture; Broadsheet uses opaque HSL
borders and an ambient corner wash instead (see "Color, surface, and
elevation" above).
Brand mark — the control ring (guide §19)
The Overdeck logo is the control ring: orbit ring + center hub + one agent
satellite on the ring (reads as the O of Overdeck). In-app, render it ONLY via
the OverdeckMark component (src/dashboard/frontend/src/components/OverdeckMark.tsx)
with text-primary — never inline a copy of the geometry, never use the lucide
Eye, the old stacked diamonds, or tan/copper colors. Canonical SVGs:/favicon.svg, logo/overdeck-{light,dark}.svg, desktop icons inapps/desktop/resources/. Two-tone indigo (#4f46e5 + #818cf8 light bg,
#6366f1 + #a5b4fc dark bg); minimum size 16px, mark-only below wordmark scale.
Workflow
- Before styling anything new, open the system map mockup (or, for
Broadsheet-specific patterns,style-guide-v2.html) and find the nearest
existing primitive — match it, don't invent. - For mockups: build with the real tokens — copy the
@font-face
declarations and the light/dark color blocks fromindex.css, self-hosted
(never a Google Fonts<link>) — and state conformance deltas explicitly.
Put mockups indocs/design/. - Reviewing UI diffs: grep the diff for forbidden patterns above and for
font-bold|font-semiboldoutside.display-xl/.display-lg,Inter,rounded-full.*badge,slate-, and any hardcodedfont-family:string in
component code (it should always befont-body/font-display/font-mono)
— each hit is a finding.