56 KiB
| name | colors | typography | fontFamily | rounded | spacing | shadows | gradients | transition | breakpoints | container | score-formula | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| EvalScope Console |
|
|
|
|
|
|
|
|
|
|
|
Design System: EvalScope Console
Overview
EvalScope's web dashboard is a developer-platform brand for LLM evaluation and benchmarking — the page is an instrument panel for engineers running evals, written for people who already know the syntax. It earns that posture through two equally weighted themes rather than one canonical mode with a translated companion. Both themes share the same vocabulary — same type, same spacing, same radii, same components — but each carries its own surface philosophy. They are two voices of one brand, not one design re-tinted.
Dark Console (default) is a stark dark-indigo system: near-black {colors.bg} canvas, ice-cool {colors.text} body, a 3-step text ladder. Hairlines are translucent violet at 10-28 % alpha because the near-black bg lets even a faint violet tint read as a definite edge. Shadows are single deep drops at rgba(0,0,0,0.55) — near-black eats soft shadows. The mood is late-night terminal session: low-light, low-distraction, the violet glow on a primary CTA is the only saturated thing on screen.
Warm Console (light theme) is a warm-cream system: cream {colors.bg-light} canvas (#faf9f5), pure white {colors.bg-card-light} cards, warm-ink {colors.text-light} body (#141413). Hairlines are solid warm-grey ({colors.border-light} #e6dfd8, #d6cdbe, #c1b6a3) — not translucent violet, because violet at 20 % alpha disappears against a white card and leaves every card boundary undefined. Shadows are two-stop stacks tinted with the same warm-ink as the text, so cards lift off the cream without printing a cool slate smudge. The mood is morning code review: high-contrast, restful on the eyes, the violet CTA is the only cool note in an otherwise warm palette.
The brand constant across both themes is the single violet {colors.accent} (#816DF8 dark / #6c57e8 light) used for primary CTAs, active nav states, focus rings, and the wordmark accent — plus the dynamic HSL score gradient (hsl(score × 120, 70%, 45%)) that maps a 0-1 metric to red → yellow → green. Both signals work over either canvas. Everything else — surface ladder, hairline material, shadow tint, on-canvas text colour — is theme-specific by design, because dark and light surfaces need different materials to produce the same hierarchy.
Type is the second decisive voice and is theme-agnostic. The brand uses cross-platform system font stacks (no web font is loaded) — system-ui, -apple-system, "Segoe UI", Roboto, ... for narrative and ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", ... for technical labels. Each OS resolves to its own native UI face. Headlines are sentence-case with tracking-tight on display numbers; all-caps + tracking-wider is reserved for tiny section eyebrows (12 px / 10 px), never headlines. Weight ceiling is 700; the working set is 400 / 500 / 600 / 700.
Key Characteristics:
- Dual-theme parity, not dual-theme translation. Dark uses translucent violet hairlines on near-black; light uses solid warm-grey hairlines on cream. They produce the same hierarchy through opposite material choices. Theme is persisted to
localStorageand applied viadata-themeon<html>before first paint to avoid a flash. - A single violet primary CTA
{colors.accent}carries every conversion target on both themes, paired with a transparent ghost secondary. The brand uses a{rounded.sm}8-px button shape for primary/secondary in the console (no marketing pills — this is an in-product surface). - The primary CTA glows on hover with violet at 20-25 % alpha on both themes. That violet glow is the brand's signature interaction — identical animation, identical colour, identical timing across themes.
- Every card section title, form label, and table header sets in
{typography.label-xs}— 12 px (10 px for tables),font-semibold, UPPERCASE,tracking-wider, muted color. Body and titles stay sentence-case. The contrast between these two voices does most of the hierarchy work. - A dynamic HSL score chip (
hsl(score × 120, 70%, 45%)) is the second-most-recognizable component after the brand violet — it is how the product communicates pass/fail. Identical formula on both themes; the chip's saturation works over cream and over near-black. - Light theme uses solid hex hairlines, not translucent violet. This is the most important light-theme rule, and the one most often broken on first attempt: a violet alpha overlay disappears into a near-white page, so light surfaces require concrete warm-grey edges (
#e6dfd8/#d6cdbe/#c1b6a3) to keep their boundaries. - A complete domain token set exists for chat bubbles (5 semantic roles: user / bot / tool / reasoning / system), compare slots (3 per-model accent colors), and KPI gradients (4 named gradient pairs) — these are first-class brand tokens, not ad-hoc colors.
- A glassmorphic sticky top-nav (52 px, 12 px backdrop-blur, 1-px violet-to-transparent gradient hairline along the top edge) is the only "marketing-y" flourish the product allows itself. Dark uses translucent indigo glass; light uses translucent cream glass.
Colors
Note on dual theme. Every color token below has a dark (default) and light value. Hex pairs are listed as
dark / light. Components reference tokens by name — never by raw hex — so theme switching is free.Light values use SOLID hex for hairlines, not translucent violet. This is the structural difference from earlier light-theme generations and the single most-broken light-theme rule. See
{colors.border-light}below and Elevation & Depth for the reason.
Brand & Accent
The accent family is identical in spirit on both themes — a single violet handles every conversion target. Slightly different hex values per theme (the dark violet is brighter to read on near-black; the light violet is a half-step deeper to hold weight against white cards) but the same brand voltage.
- Violet (
{colors.accent}—#816DF8/#6c57e8): The single brand color. Primary CTA fill, active nav fill, focus ring, brand-wordmark accent, table-header active-sort color. Used sparingly — should occupy under ~10 % of any screen. - Violet Deep (
{colors.accent-dark}—#5B3FD6/#4f3ec8): Hover state for primary CTAs. - Lavender (
{colors.purple}—#a78bfa/#7c6be0): The gradient companion stop; the second half of{gradients.brand}. - Violet Mist (
{colors.accent-dim}—rgba(129,109,248,0.12)/rgba(108,87,232,0.14)): The low-alpha violet used as pill background and focus-ring fill. Light theme runs slightly stronger (0.14 vs 0.12) because white cards need a touch more saturation to read the mist. - Violet Glow (
{shadows.glow}—0 0 20px rgba(129,109,248,0.25)/0 0 20px rgba(108,87,232,0.22)): The signature hover halo on primary buttons and active nav. Same effect, same magnitude, on both themes.
Surface
Each theme operates with a 4-step surface ladder, sunken-to-elevated. The ladder structure is shared; the material is different — dark walks an indigo ladder, light walks a warm-cream ladder. The semantic of each step is the same: bg-deep is below the page, bg-card is the working surface, bg-card2 is the elevated state.
- Page (
{colors.bg}—#0c0c1a/#faf9f5): The page body. Dark = near-black instrument-panel mood. Light = warm cream canvas (Claude-style), deliberately not cool grey-white — the cool variant (#f5f6fa) reads as "any other SaaS dashboard" and washes out white cards. - Sunken (
{colors.bg-deep}—#09091a/#f0ebe1): One step below the page — used for input wells, the deep-well that holds pill-style tab containers, and the icon tile in empty-states. Dark goes deeper-black; light goes warmer-cream. On both themes, inputs read as "wells" because they're one step below the surface that contains them. - Card (
{colors.bg-card}—#12122b/#ffffff): The default card / dialog / table surface. Dark = indigo card on near-black page. Light = pure white card on cream canvas — the cream-to-white contrast (ΔL ≈ 7) is what gives a light-theme card its lift, NOT a heavy shadow. - Card Elevated (
{colors.bg-card2}—#16163a/#f5f0e7): Hover state for clickable cards and rows; also the inactive-tab fill in pill-tab containers. Light theme's elevated state is warm-cream-soft — the elevated state is darker on dark theme but lighter-than-card-but-warmer on light theme (the white card with a soft-cream hover reads as "depressed into the cream canvas"). - Glass (
{colors.surface-glass}—rgba(18,18,43,0.7)/rgba(250,249,245,0.80)): Translucent surface for the sticky top-nav, used with a 12-px backdrop-blur. Light theme uses tinted cream glass (matches the canvas), NOT pure white — white glass on cream reads as a foreign sheet floating in space.
Text
- Ink (
{colors.text}—#e2e8f0/#141413): All headings, body, table cell values, button labels on non-filled surfaces. Light theme uses warm-near-black (#141413, ≈ the same value Claude.com uses) rather than a cool slate (#1a1f2e), so the text temperature matches the canvas temperature. - Muted (
{colors.text-muted}—#8896aa/#6c6a64): Secondary labels, nav-link inactive text, card-header micro-labels, button "ghost" idle text. This is also the color section-eyebrow uppercase labels are set in. Light theme uses warm-grey (#6c6a64) rather than cool-slate (#5a6378) to stay coherent with the warm canvas. - Dim (
{colors.text-dim}—#7a8195/#8e8b82): Lowest-priority text — placeholder text, timestamps in compact rows, table empty-state. Contrast tuned to ~3.6 : 1 against{colors.bg-card}on both themes — sits just above the WCAG AA Large floor (3 : 1), still below AA Normal (4.5 : 1). ⚠️ Reserve for ≥ 14 px non-essential metadata. Light theme uses a warm-grey at the same luminance step as the dark theme's cool-grey — the perceived hierarchy stays identical. - On Filled (
{colors.on-filled}—#ffffff/#ffffff): Text on{colors.accent}and other saturated fills. Identical on both themes — the violet CTA is dark enough on both that white text holds.
Hairlines (the structural difference between themes)
- Border (
{colors.border}—rgba(129,109,248,0.10)/#e6dfd8): The default 1-px card / input / divider boundary. Dark uses translucent violet at 10 % alpha because the near-black bg-to-card luminance step already does most of the boundary work — the violet hairline just tints it. Light uses a SOLID warm-grey hex (#e6dfd8, Claude-style cream-hairline) because the white-card-on-cream luminance step is gentle enough that a translucent violet overlay disappears into the page. Borders on light theme are concrete materials, not tints. - Border Emphasized (
{colors.border-md}—rgba(129,109,248,0.18)/#d6cdbe): One step stronger — used on form inputs after focus, on the active-state of hover cards, on the boundary between a card and a nested section. - Border Strong (
{colors.border-strong}—rgba(129,109,248,0.28)/#c1b6a3): The strongest boundary — used by{components.card-hover}on hover lift, and by elevated cards in modal contexts. On both themes this is the "this thing is grabbing attention" hairline.
Semantic
- Success (
{colors.success}—#10b981/#059669) / Success Bg (rgba(16,185,129,0.08)) / Success Border (rgba(16,185,129,0.20)): Confirmed / passed states; success toasts; chat-bot bubble border. - Warning (
{colors.warning}—#f59e0b/#d97706) / Warning Bg (rgba(245,158,11,0.08)) / Warning Border (rgba(245,158,11,0.20)): Pending / caution; tool-call chat bubbles. - Danger (
{colors.danger}—#ef4444/#dc2626) / Danger Bg (rgba(239,68,68,0.08)) / Danger Border (rgba(239,68,68,0.20)): Errors, fail badges, validation, destructive actions. - Info (
{colors.info}—#60a5fa/#3b82f6): Latency chart series, informational toasts. - Pass (
{colors.pass}—rgb(45,104,62)/rgb(16,108,55)) / Fail ({colors.fail}—rgb(151,31,44)/rgb(180,30,42)): Deep saturated greens / crimsons for boolean pass/fail badges where the tone needs more weight than the soft semantic family.
Score Gradient (Signature)
The product's emotional core. A 0-1 score maps to hsl(score × 120, 70%, 45%): 0 → red, 0.5 → yellow, 1 → green. Used as both foreground and translucent background on score chips, dataset chips, and group-header best-score callouts. This is computed inline (scoreColor / scoreBg helpers), never stored as a static palette. Treat the formula as a brand asset — do not reskin to a 5-step bucket, do not introduce a 4th hue.
Foreground uses HSL for predictable hue progression; background uses an RGB-interpolated stop pair (rich mid-tones, more saturated yellows) for visual continuity with the existing chip/badge appearance. Do not rewrite the bg to HSL alpha — the mid-tone shift will break existing visual reads.
Per-theme legibility knobs: hsl(h 70%, 45%) reads cleanly on the near-black dark canvas but the yellow mid (hue ≈ 60) collapses into warm-cream #faf9f5 on the light theme. To keep the brand formula intact while fixing legibility, three CSS vars scope per-theme:
--score-fg-s— saturation for the HSL foreground. Dark:70%. Light:85%(the +15pt bump compensates for the lightness compression at hue ≈ 60; without it, mid scores render as washed-out olive on cream).--score-fg-l— lightness for the HSL foreground. Dark:45%. Light:32%(darker olive/forest/maroon read cleanly on cream).--score-bg-a-mul— alpha multiplier for the RGB-interpolated bg. Dark:1. Light:1.6(boosts washed-out yellows so the pill bg is actually visible).
scoreColor / scoreBg emit CSS expressions (hsl(h var(--score-fg-s) var(--score-fg-l)), rgb(r g b / calc(α * var(--score-bg-a-mul)))) — the browser picks the right value per active theme. Do not hardcode these in JS or fork separate light/dark helpers.
Score Ring ({components.score-ring} — SVG circular progress used in <ReportSummaryStats> and the "Overall Score" callout in <DetailsTab>): the active arc is stroke={scoreColor(score)}. Stroke width must be ≥6 px for the 48 × 48 mini ring and 8 px for the 72 × 80 summary ring — anything thinner reduces the colored area to the point where the mid-hue olive stops carrying. Background arc uses var(--border) for a neutral track.
Compare Slots
Three per-model accent colors used to tag side-by-side model comparisons. Each slot has a dot, border, bg, and bg-header tint at ~10-30 % alpha:
- Slot 0 (
{compare.0.dot}—#818cf8/#6c57e8): Indigo. - Slot 1 (
{compare.1.dot}—#34d399/#0a8a6e): Mint. - Slot 2 (
{compare.2.dot}—#fbbf24/#d97706): Amber.
If a comparison view exceeds 3 models, do not invent a 4th brand color — collapse into a numbered legend instead.
Chat Bubble Roles
Five semantic roles, each with a complete 7-token set (bg, bg-hl, border, border-hl, icon-bg, icon-border, color):
- User — Indigo family (
#818cf8color). - Bot — Emerald family (
#34d399color). - Tool — Amber family (
#fbbf24color). - Reasoning — Dim emerald (lower-saturation companion to Bot).
- System — Slate gray (
rgba(148,163,184,*)).
Bubble containers are {rounded.md} with the role's tint background and border; hover/highlight states use the *-hl variants.
KPI Gradients
Four named linear gradients for the four hero KPI tiles on the dashboard:
- Indigo→Violet (
{gradients.kpi-0}—linear-gradient(135deg, #6366f1, #8b5cf6)) - Emerald→Teal (
{gradients.kpi-1}—linear-gradient(135deg, #10b981, #06b6d4)) - Amber→Orange (
{gradients.kpi-2}—linear-gradient(135deg, #f59e0b, #f97316)) - Pink→Violet (
{gradients.kpi-3}—linear-gradient(135deg, #ec4899, #8b5cf6))
Always applied to the 40 × 40 {rounded.md} icon tile inside a {components.kpi-card}. Same gradient values on both themes — they're saturated enough to work over either canvas.
Chart Palette (Perf Metrics)
Four hue tokens used to mark perf-metric series (latency / TTFT / TPOT / token-usage) across the KPI strip, the chart series legends, and the percentile-table accent headers in <PerfMetricsPanel>. The two themes use different RGB values for the same hue — unlike the KPI gradients, these aren't shared across themes:
- Latency (
{chart.latency}—#60a5fadark /#2563eblight) - TTFT (
{chart.ttft}—#34d399dark /#047857light) - TPOT (
{chart.tpot}—#a78bfadark /#5b48e0light) - Token (
{chart.token}—#94a3b8dark /#5a6378light)
Why the fork: warm-cream bg ({colors.bg-card-light} / {colors.bg-deep-light}) compresses cool hues via simultaneous contrast. Mid-tone slate blue / violet that pop on near-black dark canvases read as washed-out lavender on cream. The light palette pulls each hue darker and more saturated to restore the data-bearing punch of the same hue family. Same hue identity, different RGB — not a re-tint.
KPI strip surface: the strip in <PerfMetricsPanel> uses {colors.bg-card} (matching the outer card), not {colors.bg-deep}. On light theme, {colors.bg-deep} is even warmer than the cards and pushes the chart hues into the warm-on-warm range — losing the contrast that makes the colored numbers carry. Visual separation comes from the border + dividers, not bg differentiation.
Brand Gradients (Decorative)
- Brand (
{gradients.brand}—linear-gradient(135deg, #816DF8 → #a78bfa)): Forgradient-textand large brand moments. - Accent (
{gradients.accent}—linear-gradient(135deg, #0F9C7E → #06b6d4)): For the optional emerald-to-cyan accent text. - Surface (
{gradients.surface}—linear-gradient(135deg, rgba(129,109,248,0.08) → rgba(167,139,250,0.05))): The subtle violet wash layered behind KPI cards via::before.
Typography
Font Family
Two cross-platform system font stacks carry the entire system — each OS resolves to its own native UI face. No @font-face is loaded; this is deliberate. Each stack starts with the modern CSS system-ui / ui-monospace generic family and falls back to named faces for older browsers and per-OS targets:
- System sans (
{typography.font-sans}—system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif): Every display, body, button, link, and label. Working weights: 400 / 500 / 600 / 700. Resolves to SF Pro on macOS / iOS, Segoe UI on Windows, Roboto on Android / ChromeOS, and the desktop default (Cantarell / Noto / DejaVu) on Linux. - System mono (
{typography.font-mono}—ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace): Timestamps, scores, model IDs, and any tabular-numeric data. Weight 400 only. Resolves to SF Mono on macOS, Consolas on Windows, Liberation Mono / DejaVu Sans Mono on Linux. The full chain matters — without the OS-specific named fallbacks, non-mac users land onCourier New, breaking the tabular-numeric look.
Antialiasing is forced (-webkit-font-smoothing: antialiased). No web font is loaded — the brand reads as "native developer tool, not marketing site" precisely because of this. The trade-off is per-OS rendering variance; if pixel-identical screenshots across platforms are required, see Note on Font Substitutes below.
Hierarchy
| Token | Size | Weight | Tracking | Use |
|---|---|---|---|---|
{typography.display-xl} |
24px | 700 | tight | KPI value, hero number (dashboard {components.kpi-card}). |
{typography.title-md} |
16px | 700 | normal | Card-title model name, group-header titles, brand wordmark. |
{typography.body-sm} |
14px | 400 / 500 | normal | Default body text, button-md label, table-cell text, paragraph copy. |
{typography.label-xs} |
12px | 600 | wider | UPPERCASE card-section header, form label, badge text — the brand's signature eyebrow. |
{typography.body-xs} |
12px | 400 / 500 | normal | Empty-state hint, pill / badge body, mobile-nav link. |
{typography.table-xs} |
10px | 600 | wider | UPPERCASE table-header micro-text — whispers, doesn't shout. |
{typography.caption-mono} |
12px | 400 (mono) | normal | Timestamps, score values, dataset names in chips. |
{typography.code} |
13–14px | 400 (mono) | normal | Log viewer, JSON viewer, terminal-style output. |
{typography.button-sm} |
12px | 500 | normal | {components.button} size="sm". |
{typography.button-md} |
14px | 500 | normal | {components.button} size="md" (default). |
{typography.button-lg} |
16px | 500 | normal | {components.button} size="lg" — for hero callouts only. |
Principles
- UPPERCASE +
tracking-wideris the eyebrow voice — never the headline voice. It marks "this introduces a region." Card titles, model names, and KPI labels stay sentence-case (or lower-case for the brand wordmark's lowercase "v"). tracking-tightis reserved for the display tier — KPI numbers and the brand wordmark only. It tells the reader "this is set big on purpose."tabular-nums+font-monoeverywhere numbers must align — KPI values, score chips, timestamps, percentage cells. Treat numbers as data, not prose.- Weight 700 is the display ceiling. No
font-extrabold/font-black. The brand reads as a calmer system because of this. - Line-heights inherit Tailwind defaults (1.5 for body, 1.25 for headings). Don't override globally; rely on padding for vertical rhythm in tight stacks (cards, chips).
- No web font. The system stack is the system. Loading Inter or Geist on top would break the "native console" feel.
Note on Font Substitutes
EvalScope uses the OS-native stack, so there are no proprietary faces to substitute. If a future skin needs to enforce a single face across all OSes for screenshot consistency:
- Sans substitute — Inter (400 / 500 / 600 / 700) is the closest stylistic match to the SF-on-macOS rendering; preserves the geometric / neutral character.
- Mono substitute — JetBrains Mono (400) at 12–14 px matches the technical voice well; IBM Plex Mono is a close second.
Layout
Spacing System
- Base unit: 4 px (Tailwind's default scale).
- Tokens (Tailwind-aligned):
{spacing.xs}4 px ·{spacing.sm}8 px ·{spacing.md}12 px ·{spacing.lg}16 px ·{spacing.xl}20 px ·{spacing.2xl}24 px ·{spacing.3xl}32 px ·{spacing.4xl}48 px ·{spacing.5xl}64 px.
- Page padding: 16 px horizontal, 20 px vertical (
px-4 py-5) at all breakpoints — no responsive expansion. The 1600 px container does the breathing for us. - Card interior padding: 20 px body (
{spacing.xl}), 12 px header strips ({spacing.md} {spacing.xl}). - Inline gap:
{spacing.md}(12 px) for component rows inside a card,{spacing.xl}(20 px) for inter-section gaps between major page blocks. - Pill / chip gap:
{spacing.sm}(6-8 px) — tight, scan-friendly. Pills are meant to wrap.
Grid & Container
- Max width:
1600px(max-w-[1600px]). Centered (mx-auto). Wide enough for 4-up KPI strip + side-by-side comparison; narrow enough that line-length never sprawls. - Column patterns:
- KPI strip: 4-up at
lg+, 2-up below (grid-cols-2 lg:grid-cols-4). - Compare view: 2-3 columns at desktop, vertical stack on mobile.
- Eval timeline: single column of full-width rows, no grid.
- Form pairs:
grid-cols-1 md:grid-cols-2for label-value pairs.
- KPI strip: 4-up at
- Gutters: 16 px horizontal at all sizes.
Whitespace Philosophy
Whitespace separates the bands — not the components inside a band. Section spacing is generous (flex flex-col gap-5 → 20 px between major blocks); card interiors are tight (gap-2 / gap-3 between rows inside a card). The page reads as engineered — large gaps + tight interior, never the other way around. The dark page lets cards float visually without needing margin to assert themselves; the hairline border does the bordering work.
The brand's voice is information-rich but uncluttered — a typical dashboard packs a 4-tile KPI strip, a 30-row sortable eval list, search + sort + view-toggle controls, and the page still feels scannable because:
- Tokens enforce a 3-step text hierarchy on every row.
- UPPERCASE eyebrows visually section the layout without horizontal rules.
- Score chips compress a percentage + benchmark name + color signal into 6 characters of mono.
Responsive Strategy
Breakpoints (Tailwind defaults)
| Name | Width | Key Changes |
|---|---|---|
| Mobile | < 640px | Nav collapses to hamburger drawer; KPI grid drops to 2-up; all controls stack. |
| Small | 640–767px | GitHub icon and locale toggle appear in nav; KPI still 2-up. |
| Tablet | 768–1023px | Nav switches to icon-only mode with tooltips; main content full-width. |
| Desktop | 1024–1279px | Full pill-style nav with icon + label; KPI strip goes 4-up. |
| Wide | ≥ 1280px | Container holds at max-w-[1600px]; bands stretch but content centers. |
Touch Targets
The top-nav icon-only buttons (tablet) are 32 × 32 — under the 44 × 44 WCAG floor. This is a known compromise for the developer-tool density; on actual touch devices, hit areas are extended via padding. Primary buttons reach ~36 px tall in md size and ~44 px in lg — meet the floor at lg.
Collapsing Strategy
- Nav: Desktop = pill-row with icon + label; tablet = icon-only pills; mobile = logo + hamburger toggling a stacked drop-down (
max-heightanimated, 300 ms). - KPI strip: 4-up → 2-up below
lg. Each tile keeps its{rounded.md}12-px shape and 20-px padding. - Eval timeline: Single column at all sizes — already a vertical list.
- Forms: Two-column label/value at
md+, single column below. - Table: Horizontal scroll wrapper preserves all columns rather than dropping them. The card-shell border keeps the scroll area framed.
Image / Icon Behavior
- Iconography:
lucide-react, almost always 14–18 px, color inheritscurrentColor. The only non-Lucide mark is the brand SVG in the top-nav (a hand-drawn triangle with an amber check, rendered withcurrentColorso it follows theme). - No marketing imagery: This is a console — no hero photo, no customer-logo strip, no illustrated empty-states. Empty states use a single Lucide icon in a 64 × 64
{rounded.lg}deep-well tile. - Charts: Plotly-based, theme-aware — chart series colors are
{colors.chart-*}tokens (latency / TTFT / TPOT / token).
Elevation & Depth
| Level | Treatment | Use |
|---|---|---|
| L0 — Flat | No border, no shadow. | Page body, large empty regions, full-bleed dark sections. |
| L1 — Hairline | 1-px {colors.border} only. Dark: translucent violet at 10 % alpha (the near-black page lets even a faint hairline read; the slight violet tint stays on-brand). Light: solid warm-grey hex #e6dfd8 (translucent violet at any plausible alpha composites to invisible against a white card on cream — solid hex is the only thing that reads). |
Default content cards ({components.card}), table chrome, form inputs. |
| L2 — Lifted | {shadows.sm} + L1 hairline. Dark: single deep drop 0 2px 8px rgba(0,0,0,0.4). Light: two-stop stack 0 1px 2px rgba(20,20,19,0.04), 0 4px 12px rgba(20,20,19,0.06) — warm-ink tinted (matches {colors.text-light}), not slate. Slate-tinted drops on cream read as a cool-grey smudge against the warm canvas. |
KPI cards, path-bar at the top of the dashboard, anything that needs to read as "above the page." |
| L3 — Floating | {shadows.md} + L1 hairline. Dark 0 4px 20px rgba(0,0,0,0.55). Light 0 4px 16px rgba(20,20,19,0.07), 0 12px 32px rgba(20,20,19,0.05). |
Default for the primary card variants when the page already has heavy chrome. |
| L4 — Elevated | {shadows.lg} + {colors.border-strong}. Dark 0 8px 40px rgba(0,0,0,0.6). Light 0 12px 24px rgba(20,20,19,0.09), 0 24px 48px rgba(20,20,19,0.07). |
Hover state for clickable cards, modal / dialog surfaces, dropdown menus. |
| L5 — Glow | {shadows.glow} on top of L1-L3. Dark 0 0 20px rgba(129,109,248,0.25). Light 0 0 20px rgba(108,87,232,0.22). |
The signature violet halo — primary button hover, active nav pill, sometimes active tab. Identical magnitude across themes — the glow is the brand's interaction constant. |
Brand rule (depth): dark and light themes are not the same shadow scaled down. Dark surfaces use a single deep drop (rgba(0,0,0,0.4-0.6)) — the near-black canvas eats soft drops. Light surfaces use a two-stop stack tinted with warm-ink rgba(20,20,19,*) (the same hex as {colors.text-light}), NOT slate rgba(15,23,42,*) — the warm-ink tint stays coherent with the cream canvas and the warm-ink body text. Light cards lift through the combination of cream-to-white surface contrast + a solid warm-grey hairline + the two-stop warm-ink shadow stack — no single layer carries the weight.
Brand rule (hairlines): light theme borders are SOLID HEX, not translucent violet at any alpha. The earlier light-theme generation used rgba(108,87,232,0.20-0.40) and the violet alpha composited to within a few luminance steps of the white card — borders effectively dissolved, especially on outline buttons (Go to Index, Find msg id) and on input rings (Score Threshold field). The current system uses three concrete warm-grey hex values (#e6dfd8 / #d6cdbe / #c1b6a3) — each step is a definite material, not a tint. The dark theme keeps translucent violet at 10-28 % because the indigo-bg-to-card luminance delta is already doing most of the boundary work; light theme has no such luminance assist and must rely on the hairline alone.
Decorative Depth
- Backdrop-blur: 12 px on
{colors.surface-glass}for the sticky top-nav. This is the only blur effect in the system. - Hairline gradient line: The top of the nav draws a 1-px
transparent → {colors.accent} → transparentline at 40 % opacity. The closest the design comes to "decoration." - Card lift: Hover lifts L1/L2 cards
-2px({components.card-hover}) or-3px({components.kpi-card}), simultaneously upgrading their shadow ladder one step. - Gradient text:
.gradient-textand.gradient-text-accentutilities apply{gradients.brand}/{gradients.accent}to text viabackground-clip: text. Use sparingly — reserved for hero brand moments, never for body or table cells.
Shapes
Border Radius Scale
| Token | Value | Use |
|---|---|---|
{rounded.none} |
0px | Full-bleed sections, table cell interiors. |
{rounded.xs} |
4px | {tokens.radius-xs} — tightest inline pill (rarely used directly). |
{rounded.sm} |
8px | {tokens.radius-sm} — buttons (sm/md), inputs, tabs, view-toggle buttons. Most in-product chrome lives here. |
{rounded.md} |
12px | {tokens.radius} — default card radius. Cards, KPI tiles, group-header containers, large buttons. |
{rounded.lg} |
16px | {tokens.radius-lg} — empty-state icon tile, KPI icon tile. |
{rounded.xl} |
20px | {tokens.radius-xl} — reserved (used by occasional rounded-2xl Tailwind class on welcome / empty states). |
{rounded.full} |
9999px | Badges, chips, score pills, mobile-nav icon buttons. |
No pill (100-px) shape. Unlike Vercel's marketing pill, EvalScope is an in-product surface; all CTAs use {rounded.sm} 8 px. Pills ({rounded.full}) are exclusively for data — badges, chips, score indicators.
"Photography" — Iconography Geometry
- Brand mark: Hand-drawn SVG triangle with an amber check; rendered inline at 28 × 25 px with
currentColorso it follows theme. - Lucide icons: 14 px in dense lists, 16 px in nav, 18 px in form controls, 28 px in empty-state hero tiles. Stroke width 1.5–2.
- KPI icon tile: 40 × 40,
{rounded.md}, filled with one of{gradients.kpi-0..3}, white icon centered. - Chart: Plotly canvas, no rounded corners; sits inside a
{rounded.md}card frame.
Components
Buttons
The brand operates with three button variants — all in-product scale, no marketing pill:
{components.button.primary} — the canonical violet CTA.
- Background
{colors.accent}, text{colors.on-filled}, shape{rounded.sm}8 px (md/sm) or{rounded.md}12 px (lg). Hover adds{shadows.glow}violet halo +bg → {colors.accent-dark}. Press scales to 0.98. The glow IS the brand interaction.
{components.button.ghost} — the transparent secondary.
- Transparent background →
{colors.bg-card}on hover →{colors.bg-card2}on active. Text{colors.text}. Used inside top-nav, toolbars, and inline icon-buttons.
{components.button.outline} — the hairline tertiary.
- Transparent background with 1-px
{colors.border-md}border. Hover swaps both border and text to{colors.accent}(no fill). For "alternative action" CTAs.
Sizes:
sm— 12-px text ({typography.button-sm}), 6/12 padding,{rounded.sm}, ~28 px tall.md— 14-px text ({typography.button-md}), 8/16 padding,{rounded.sm}, ~36 px tall (default).lg— 16-px text ({typography.button-lg}), 10/24 padding,{rounded.md}, ~44 px tall.
Disabled = opacity: 0.5 + cursor: not-allowed. Transitions use {tokens.transition} (0.18 s ease) consistently — never custom per button.
Cards & Containers
{components.card} — the canonical card.
- Background
{colors.bg-card}, text{colors.text}, padding 20 px ({spacing.xl}), shape{rounded.md}12 px, border 1-px{colors.border}, shadow{shadows.sm}(L2). Optional uppercase section header in a 12 px / 20 px header strip with a bottom border. Collapsible variant rotates a chevron 90 °.
{components.card-hover} — utility applied to clickable cards.
- Adds
-2 px translateYon hover, upgrades shadow to{shadows.lg}(L4), strengthens border to{colors.border-strong}. All transitions in{tokens.transition}.
{components.kpi-card} — the dashboard hero metric tile.
- Same chrome as
{components.card}but layered with{gradients.surface}(subtle violet wash, 5-8 % alpha) via::before. Hosts a 40 × 40{rounded.md}icon tile filled with{gradients.kpi-0..3}. Hover lifts-3 pxand ramps to L4 shadow. Stagger-animated on first paint (60 ms steps).
{components.card-glass} — the glassmorphic surface used by the top-nav.
{colors.surface-glass}background + 12-px backdrop-blur. Always combined with a 1-px hairline border. Reserve for sticky-positioned surfaces — diffuse blur is performance-sensitive.
{components.row-card} — borderless button styled as a list row (the eval-run rows on the dashboard).
- Background
{colors.bg-card}, padding 16-20 px,{rounded.md}, 1-px{colors.border}border. Hover only changes border tint to{colors.border-md}— no transform — kept calm for dense lists.
Inputs & Forms
{components.input} — the canonical text input.
- Background
{colors.bg-deep}(one step below its container — inputs read as "wells"), 1-px{colors.border}border, text{colors.text}in{typography.body-sm}. Focus: border →{colors.accent}+ 1-px ring in{colors.accent-dim}. Soft halo, never harsh outline. Padding 8/12,{rounded.sm}8 px. - Error state: border + ring swap to the danger family; 12 px
{colors.danger}helper line below.
{components.label} — input label.
- 12 px,
font-medium, UPPERCASE,tracking-wider,{colors.text-muted}, placed above the input with 6-px gap.
{components.select} / {components.search-input} — same chrome as {components.input}. The select uses the native <select> styled with the same class.
Tabs
{components.tabs} — pill-container.
- An outer container with
{colors.bg-deep}background and 1-px border, holding inline buttons. Active tab fills with{colors.accent}+ white text + soft glow0 0 12 px rgba(129,109,248,0.2); inactive uses{colors.bg-card}→{colors.bg-card2}on hover. Reads as a segmented control, not a tab strip.
Navigation
{components.top-nav} — sticky top navigation.
- 52 px tall,
position: sticky,z-50,{components.card-glass}chrome with 12-px blur, 1-px violet-to-transparent gradient line on the top edge. Container caps at 1600 px. Three modes:- Desktop (lg+) — pill links with icon + label; active = solid
{colors.accent}+ glow. - Tablet (md–lg) — 32 × 32 icon-only buttons with
titletooltips; same active state. - Mobile (< md) — logo + hamburger toggling a stacked drop-down with animated
max-height(300 ms ease-in-out).
- Desktop (lg+) — pill links with icon + label; active = solid
{components.nav-link} — the pill-style nav button.
- Padding 6/12,
{rounded.sm}, text{colors.text-muted}→{colors.text}on hover, active ={colors.accent}background + white text + violet glow shadow.
{components.icon-button} — circular ghost icon container in the nav.
- 32 × 32,
{rounded.sm}, transparent background →{colors.bg-card2}on hover, icon inherits{colors.text-muted}→{colors.text}.
Tables
{components.table} — sortable data table.
- Wrapped in
{components.card}chrome ({rounded.md}border + bg) withoverflow-x-auto. - Header cells:
{typography.table-xs}— 10 px, semibold, UPPERCASE,tracking-wider,{colors.text-dim}. They whisper. Sortable headers show a triple-state chevron (ChevronsUpDownidle,ChevronUp/Downactive) and lift to{colors.text}on hover; active sort column turns{colors.accent}. - Row dividers: 1-px
{colors.border}. Clickable rows hover-fill{colors.bg-card2}. - Empty state: Centered, dimmed "No data" cell — no illustration.
Badges, Chips & Pills
{components.badge} — fully rounded inline pill.
{rounded.full}, 8/2 padding,{typography.body-xs}. Four variants pair an 8-10 % alpha background with a saturated foreground:default → accent,success → green,warning → yellow,danger → red.
{components.filter-chip} — dismissable filter chip.
- Same pill shape as
{components.badge}, with an optional 3.5 × 3.5 circular dismiss button (Xicon).
{components.score-chip} — the signature dynamic-score pill.
{rounded.full}, 8/2 padding,{typography.caption-mono}, outline treatment: transparent bg + 1-pxscoreColorborder +scoreColortext. Format:"benchmark-name 87.3"— the chip is the data point. Used in the eval timeline, dashboard tiles, and the leaderboard rows.- Outline (not filled) on purpose: a filled pill at hue ≈ 60 paints high-luminance yellow (
rgb(255, 255, 0)) which dominates whatever surface it sits on, regardless of theme. Outline keeps hue legible while letting the chip recede into the row.
Signature Components
{components.kpi-card} — see Cards & Containers above.
{components.eval-run-card} — a full-width borderless button-styled card row.
- Two rows of content: bold model name + colored score pill on the right; second row has a mono timestamp + a wrap of
{components.score-chip}entries (one per benchmark). Hover only changes border tint (no lift).
{components.chat-bubble} — five-role chat surface.
{rounded.md}container with role-specific bg / border / icon-bg / icon-border / color from the{colors.bubble-*}token family (user / bot / tool / reasoning / system). Hover strengthens border via*-hlvariants.
Two variants:
bar(default in chat lists) —rounded-smcontainer, 3-px left vertical accent in the role's color, role's tint as bg. Used by the streaming chat log where many bubbles stack and visual weight must stay low.card—rounded-mdcontainer with role's full bg + 1-px border. Used in standalone bubbles outside a scrolling list (eval result preview, single-message dialogs).
{components.empty-state} — first-contact / no-data state.
- Vertical-center stack: 64 × 64
{rounded.lg}deep-well tile holding a 28-px Lucide icon ({colors.accent}for welcome states,{colors.text-dim}for empty/no-results), followed by a 2-line message — title in{typography.body-sm}{colors.text}(welcome) or{colors.text-muted}(empty), hint in{typography.body-xs}{colors.text-dim}.
{components.path-bar} — the dashboard's "scan this directory" input row.
{components.card}chrome at L2, hosting an icon, an input, and a primary button — flex-row with 12-px gap.
{components.score-badge} — the bold percentage pill at the top of an eval row.
{rounded.full}, 10/2 padding,{typography.body-sm}bold +tabular-nums, HSL-computed fg/bg. Distinct from{components.score-chip}by size and weight.
Examples (illustrative)
These
ex-*surfaces mirror the brand-native primitives for downstream consumers (kits, mockups, Stitch generation). Each references existing components so a re-skin re-skins all surfaces consistently.
ex-metric-tile — Dashboard KPI tile. Re-uses {components.kpi-card} with gradient icon tile + tabular value + uppercase label.
- Properties:
backgroundColor,iconGradient,rounded,padding,valueTypography,labelTypography.
ex-eval-run-row — A row in the eval timeline. Re-uses {components.eval-run-card} chrome.
- Properties:
backgroundColor,borderColor,rounded,padding,scoreColor,chipColor.
ex-score-chip — Dynamic HSL score pill. Re-uses {components.score-chip}.
- Properties:
score (0-1),label, computedbgandfgviascoreColor.
ex-model-group-header — Collapsible header for the grouped-by-model dashboard view. Re-uses {components.row-card} chrome with a chevron, model name, run-count, and best-score callout.
- Properties:
backgroundColor,borderColor,rounded,expanded.
ex-leaderboard-row — A row in a benchmark leaderboard. Re-uses {components.table} row chrome.
- Properties:
rowBackground,borderColor,cellTypography,scoreChip.
ex-chat-bubble-user / ex-chat-bubble-bot / ex-chat-bubble-tool — Three of the five chat-bubble roles. Re-use {components.chat-bubble} with the role's token family.
- Properties:
backgroundColor,borderColor,iconBackground,color,rounded,padding.
ex-empty-state-card — No-data state. Re-uses {components.empty-state} inside a {components.card} shell.
- Properties:
backgroundColor,iconColor,messageTypography.
ex-skeleton-row — Loading state for a table or list row.
- Properties:
backgroundColor,pulseAnimation(skeletonPulse, 1.5 s).
ex-form-field — Label + input + optional error helper. Re-uses {components.label} + {components.input}.
- Properties:
labelTypography,inputBackground,borderColor,focusRingColor,errorState.
ex-compare-column — A model column in the compare view, using one of the three {compare.0..2} slot colors.
- Properties:
dotColor,borderColor,headerBackground.
Do's and Don'ts
Do
- Reserve
{colors.accent}(#816DF8) for primary CTAs, active states, focus rings, and the wordmark accent. Brand violet IS the conversion target — keep it under ~10 % of any screen. - Use
{rounded.sm}8 px for buttons / inputs / tabs,{rounded.md}12 px for cards,{rounded.full}only for badges / chips / score pills. Each shape signals its category. - Set every section eyebrow, form label, and table header in
{typography.label-xs}— UPPERCASE +tracking-wider. This is the brand's hierarchy signal; without it, the design flattens. - Use
{typography.caption-mono}+tabular-numsfor any numeric column — scores, timestamps, percentages. Treat numbers as data, not prose. - Compute score colors with
hsl(score × 120, 70%, 45%)and use the same formula for foreground and a translucent background. The dynamic chip is the product's emotional signal. - Layer stacked shadows (a deep multi-stop shadow + a 1-px hairline) rather than single heavy drops. Cards sit on the page, not above it. On dark the hairline is translucent violet; on light the hairline is solid warm-grey hex — see
{colors.border}. - Cycle page chrome through the surface ladder
{colors.bg}→{colors.bg-deep}→{colors.bg-card}→{colors.bg-card2}. The ladder semantics are theme-agnostic: inputs sit deeper than cards, hover sits higher — even though dark walks an indigo ladder and light walks a warm-cream ladder. - Theme parity: when adding a new colour token, define BOTH a dark and a light value at the same time, in the same commit. The light value is not a translation of the dark — pick it for the warm-cream context. Token names without a light pair will eventually fall back to a default that breaks one of the themes.
- Animate page transitions with
fadeInUp(12 px translate + opacity, 400 ms ease-out) and stagger children at 60 ms. The motion is subtle — don't lengthen it. - Persist the
data-themetolocalStorageand apply it pre-paint inindex.htmlto avoid FOUC. Tokens are theme-agnostic by name; values flip.
Don't
- Don't introduce a 6th brand hue or a 4th compare-slot color. The palette is closed at violet + emerald + amber + red + slate (plus the dynamic HSL score). New accents flatten the voice.
- Don't render headlines in all-caps. UPPERCASE is the eyebrow voice (12 px / 10 px micro-labels) — never the title voice. Card titles and model names stay sentence-case.
- Don't promote the sans to
font-extrabold/font-black. The display weight ceiling is 700. - Don't use
{colors.text-dim}for essential UI text on either theme — its ~3.6 : 1 contrast against{colors.bg-card}clears AA Large (3 : 1) but is below WCAG AA Normal (4.5 : 1). Reserve for ≥ 14 px non-essential metadata (timestamps, "empty" labels, scrollbar thumb). Every code-side use must carry the inline note// text-dim allowed: non-essential ≥14px metadata (DESIGN.md §Text)so reviewers can audit it. - Don't reuse a dark-theme shadow value verbatim on light. The light palette stacks two warm-ink-tinted drops (
rgba(20,20,19,0.04)+rgba(20,20,19,0.06)) — a singlergba(0,0,0,0.07)drop on cream reads as a page smudge, not as a lifted card. Do not slate-tint the light shadows either (rgba(15,23,42,*)) — slate on cream reads as a cool-grey smear that fights the warm canvas. See Elevation & Depth. - Don't use translucent violet for light-theme hairlines. This is the most-broken light-theme rule. The light
{colors.border-light}is a SOLID warm-grey hex (#e6dfd8) — translucent violet at any plausible alpha (0.10 / 0.20 / 0.30 / 0.40) composites to near-invisible against a white card on cream and leaves every card boundary undefined. Outline buttons and input rings will vanish. The dark theme uses translucent violet because the near-black bg-to-card luminance delta carries the boundary; the light theme has no such delta and must use a concrete material. - Don't introduce a cool-grey or pure-white surface to the light theme. The light palette is warm-cream by design (
#faf9f5canvas,#f0ebe1deep,#f5f0e7elevated,#ffffffcards). A cool-grey#f5f6faor#eef0f7band breaks the warm-coherent rhythm and reverts the system to "any other AI dashboard." - Don't drop a single heavy 8-px-blur drop-shadow on a card. The dark theme requires deeper multi-stop shadows (
rgba(0,0,0,0.55)at 20-40 px) — soft drops disappear on near-black. - Don't apply
{gradients.brand}to body text or table cells. Gradient-text is for hero / wordmark moments only. - Don't bypass
{components.button}to write custombg-[var(--accent)]buttons inline. The button variants encode the glow, the scale-press, and the disabled state — re-deriving them by hand drifts the brand. - Don't use inline
style={{ background: 'var(--xxx)' }}when a Tailwind class or theformStyleshelper would do. Inline styles bypass the token abstraction and break theme switching for the hover state. - Don't pair
{rounded.full}pill shapes with{rounded.md}cards as siblings inside the same control group — pills are for data, sm/md radii are for interactive containers. Mixing them on the same row breaks the shape grammar. - Don't ignore
prefers-color-schemeon first visit. If the user has never toggled, fall back to the OS preference before defaulting to dark. - Don't widen
{spacing.xl}(20 px) section gaps past{spacing.2xl}(24 px). The product is information-dense by design; extra whitespace makes the dashboard feel half-empty rather than airy.