33 KiB
Executable File
Frosty design system
Canonical design source of truth for the Frosty Control Plane UI
(apps/control-ui). Tokens are materialized in
tokens.css and documented live in
design-system.html. Change values here first, then
regenerate the other two files.
design-system.html is a static snapshot pinned to the token values as of
the last manual regeneration. Nothing regenerates it automatically and no check
compares it against tokens.css, so it drifts silently whenever a token changes
here and only tokens.css is updated. Re-read it whenever you change a value.
Revision: ds-r2. This revision SUPERSEDES ds-r1 (glacier-blue). The register is now a dense, near-neutral monochrome developer console. See Provenance for the rationale and the ds-r1 to ds-r2 delta.
Brand
- Name: Frosty (product surface: "Frosty Control Plane")
- Personality, one line: A quiet, near-monochrome instrument panel that keeps the operator's eye on the data, not the chrome.
- Feeling words: precise, restrained, dense
Frosty is a self-hosted control plane for a Deno-native LLM gateway. The
audience is developers and platform operators doing keyboard-heavy work over
dense telemetry: providers, keys, budgets, MCP servers, live request logs,
analytics. The identity is its own; it keeps Frosty's sk-/vk- key prefixes
and does not adopt any reference product's branding or key prefixes.
Design read and dials
Design read: A trust-first operator console for developers and platform operators. The visual language is a near-neutral monochrome (shadcn "new-york"/neutral register): cool-tinted near-grays carrying only a whisper of blue, a light-on-dark primary, and a single restrained accent blue reserved for focus, links, and the active-nav marker. Dark is the default; light is a derived, fully-tokenized theme. Density is high: compact rows, small labels, large stat values.
| Dial | Value | Rationale |
|---|---|---|
| DESIGN_VARIANCE | 3 | Operator tooling is trust-first: predictable, symmetric, repeatable layouts beat expressive composition when someone is debugging a failing provider at 2am. |
| MOTION_INTENSITY | 2 | Telemetry surfaces should feel near-instant; motion exists only as state feedback (hover, focus, enter/exit of overlays) and the live-stream pulse, never as show. |
| VISUAL_DENSITY | 8 | Data-dense tables, log streams, chart cards, and stat tiles are the product; the scale is compact (13.5px body), rows are tight, and controls are shorter than the ds-r1 baseline. |
Baseline (8 / 6 / 4) is explicitly overridden by the brief: this is a dense dashboard, not a landing page. ds-r2 is one density step tighter than ds-r1.
Color
Near-neutral monochrome. Neutrals are cool near-grays at hue 265 with chroma at or below 0.006: never a pure gray, never a saturated tint. Dark is the default theme.
Primary is the shadcn-neutral inversion. In dark, --primary is near-white
with an ink label; in light, it is near-black with a paper label. Primary is
the emphasis surface (primary buttons, the one solid "default" chip), not a hue.
One accent blue, used sparingly. A single restrained cool-blue (hue 252 in
dark, 255 in light) is the only chromatic accent in the chrome. It is used ONLY
for the focus ring (--ring), links, and the active-nav indicator
(--sidebar-primary). It is never a fill: no blue buttons, no blue banners.
Semantic status colors are a functional vocabulary, not extra accents: success (green 155), warning (amber 70 to 82), destructive (red 25), info (blue 250 to 255). One rule keeps their solid fills legible: on dark they are light tones with ink text; on light they are deep tones with paper text.
Charts get their own --chart-1..5 family (blue, green, amber, red,
violet) derived from the accent blue plus the semantic hues. Each chart mark is
verified at 3:1 or better as a non-text mark on the card surface, in both
themes.
Color tokens
All values are OKLCH with hex fallback. Light theme is the :root base; dark
theme is the .dark override (mounted by default). Contrast column summarizes
the computed check in the Accessibility section.
| Token | Role | Light | Dark | Contrast note |
|---|---|---|---|---|
--background |
App canvas | oklch(0.99 0.002 265) #fbfcfd |
oklch(0.15 0.004 265) #0a0b0d |
fg on it 17.59:1 light, 18.84:1 dark (AAA) |
--foreground |
Body text | oklch(0.2 0.006 265) #151619 |
oklch(0.985 0.001 265) #fafafb |
AAA on all surfaces both themes |
--card |
Raised surface, tiles, tables | oklch(1 0 0) #ffffff |
oklch(0.185 0.004 265) #121314 |
fg on it 18.10:1 light, 17.85:1 dark (AAA) |
--card-foreground |
Text on card | same as foreground | same as foreground | see foreground |
--popover |
Dialogs, menus, sheets, command palette | oklch(1 0 0) #ffffff |
oklch(0.205 0.004 265) #161719 |
fg on it 18.10:1 light, 17.16:1 dark (AAA) |
--popover-foreground |
Text on popover | same as foreground | same as foreground | see foreground |
--primary |
Emphasis surface: primary buttons, solid default chip | oklch(0.24 0.006 265) #1e1f22 |
oklch(0.92 0.004 265) #e3e4e7 |
label on it 15.76:1 light, 14.14:1 dark (AAA) |
--primary-foreground |
Label on primary fill | oklch(0.985 0.001 265) #fafafb |
oklch(0.205 0.006 265) #16171a |
see primary |
--secondary |
Secondary button fill | oklch(0.965 0.003 265) #f2f3f5 |
oklch(0.255 0.004 265) #222325 |
label 16.36:1 light, 15.10:1 dark (AAA) |
--secondary-foreground |
Label on secondary | same as foreground | same as foreground | see secondary |
--muted |
Quiet fills: skeletons, disabled, table header | oklch(0.965 0.003 265) #f2f3f5 |
oklch(0.235 0.004 265) #1d1e20 |
muted fg on it 6.04:1 light, 6.52:1 dark (AA) |
--muted-foreground |
Secondary text, captions, timestamps | oklch(0.475 0.008 265) #5a5c61 |
oklch(0.712 0.008 265) #a0a2a7 |
6.49:1 light (AA), 7.70:1 dark (AAA) on canvas |
--accent |
Hover and selection wash | oklch(0.965 0.004 265) #f2f3f6 |
oklch(0.255 0.006 265) #212326 |
label 14.86:1 light, 15.11:1 dark (AAA) |
--accent-foreground |
Text on accent wash | oklch(0.24 0.006 265) #1e1f22 |
same as foreground | see accent |
--destructive |
Errors, deletion, unhealthy | oklch(0.52 0.2 25) #c21725 |
oklch(0.665 0.19 25) #f25855 |
label on it 5.85:1 light, 5.43:1 dark (AA) |
--destructive-foreground |
Label on destructive fill | oklch(0.985 0.005 25) #fdf9f8 |
oklch(0.205 0.04 25) #280e0c |
see destructive |
--success |
Healthy, set, enabled, live | oklch(0.48 0.13 155) #00723b |
oklch(0.72 0.15 155) #43c07a |
as text on canvas 5.89:1 light (AA), 8.45:1 dark (AAA) |
--success-foreground |
Label on success fill | oklch(0.985 0.005 155) #f8fbf9 |
oklch(0.18 0.04 155) #021709 |
5.82:1 light (AA), 8.01:1 dark (AAA) |
--warning |
Missing config, needs confirmation | oklch(0.52 0.11 70) #8f5d14 |
oklch(0.8 0.13 82) #e7b551 |
as text on canvas 5.49:1 light (AA), 10.42:1 dark (AAA) |
--warning-foreground |
Label on warning fill | oklch(0.985 0.005 80) #fcfaf6 |
oklch(0.24 0.04 82) #291d07 |
5.40:1 light (AA), 8.74:1 dark (AAA) |
--info |
Neutral-informational: transports, processing | oklch(0.5 0.15 255) #1762b6 |
oklch(0.68 0.13 250) #549de5 |
as text on canvas 5.89:1 light (AA), 6.86:1 dark (AA) |
--info-foreground |
Label on info fill | oklch(0.985 0.005 250) #f8fafd |
oklch(0.17 0.04 250) #021020 |
5.81:1 light (AA), 6.67:1 dark (AA) |
--border |
Hairlines, card and table borders | oklch(0.92 0.004 265) #e3e4e7 |
oklch(0.27 0.006 265) #252629 |
decorative; not a text pair |
--input |
Input borders (stronger than border) | oklch(0.89 0.004 265) #d9dbdd |
oklch(0.3 0.006 265) #2c2e31 |
decorative; field identity via fill, label, focus ring |
--ring |
Focus ring, links, inline accents (accent blue) | oklch(0.55 0.15 255) #2971c6 |
oklch(0.62 0.13 252) #4589d2 |
vs canvas 4.76:1 light, 5.41:1 dark (link AA, UI 3:1 pass) |
--chart-1 |
Chart series 1 (blue) | oklch(0.52 0.15 255) #1f68bc |
oklch(0.66 0.14 252) #4b95e5 |
vs card 5.56:1 light, 6.00:1 dark (3:1 pass) |
--chart-2 |
Chart series 2 (green) | oklch(0.52 0.14 155) #007f43 |
oklch(0.72 0.15 155) #43c07a |
vs card 5.09:1 light, 8.00:1 dark (3:1 pass) |
--chart-3 |
Chart series 3 (amber) | oklch(0.6 0.12 70) #ad721c |
oklch(0.8 0.13 82) #e7b551 |
vs card 4.05:1 light, 9.88:1 dark (3:1 pass) |
--chart-4 |
Chart series 4 (red) | oklch(0.52 0.2 25) #c21725 |
oklch(0.665 0.19 25) #f25855 |
vs card 6.12:1 light, 5.59:1 dark (3:1 pass) |
--chart-5 |
Chart series 5 (violet) | oklch(0.5 0.18 300) #7541b8 |
oklch(0.62 0.16 300) #966cd7 |
vs card 6.55:1 light, 4.80:1 dark (3:1 pass) |
--sidebar |
Sidebar surface | oklch(0.975 0.003 265) #f6f7f9 |
oklch(0.13 0.004 265) #070709 |
nav label 15.31:1 light, 10.77:1 dark (AAA) |
--sidebar-foreground |
Nav item labels | oklch(0.24 0.006 265) #1e1f22 |
oklch(0.8 0.006 265) #bcbec2 |
see sidebar |
--sidebar-primary |
Active-item inset bar, live dot (accent blue) | oklch(0.52 0.15 255) #1f68bc |
oklch(0.62 0.13 252) #4589d2 |
vs sidebar 5.18:1 light, 5.53:1 dark (UI 3:1 pass) |
--sidebar-primary-foreground |
Label on sidebar-primary fill | oklch(0.985 0.001 265) #fafafb |
oklch(0.985 0.001 265) #fafafb |
paper label |
--sidebar-accent |
Active and hovered nav item fill | oklch(0.955 0.005 265) #eef0f4 |
oklch(0.235 0.006 265) #1d1e21 |
active label 14.43:1 light, 15.97:1 dark (AAA) |
--sidebar-accent-foreground |
Active nav item label | oklch(0.24 0.006 265) #1e1f22 |
oklch(0.985 0.001 265) #fafafb |
see sidebar-accent |
--sidebar-border |
Sidebar hairline | oklch(0.91 0.004 265) #e0e1e4 |
oklch(0.24 0.006 265) #1e1f22 |
decorative |
--sidebar-ring |
Focus ring inside sidebar | same as ring | same as ring | see ring |
Typography
Two stacks, no shipped font files, no fetched webfonts.
- Sans (UI):
ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif - Mono (telemetry):
"JetBrains Mono", ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace
JetBrains Mono is named first as an optional progressive enhancement: it is free and open (SIL OFL 1.1) and renders only where locally installed. Every position after it is a system face, so the file is fully self-contained.
Mono is the telemetry register. Latency, cost, token counts, model ids, versions, URLs, key names, and log lines are always mono. Prose, labels, headings, and navigation are always sans. Serif is banned on this surface.
Scale
Compact scale, px-snapped, tuned one density step tighter than ds-r1. Body is 13.5px. Hierarchy comes from size, weight step, and color, never from oversized type. The stat-tile value is the one deliberately large figure (3xl mono).
| Step | Size / line height | Weight | Use |
|---|---|---|---|
| 3xl | 27px / 32px | 600, tracking -0.01em | display, large stat-tile values |
| 2xl | 21px / 26px | 600, tracking -0.01em | page titles |
| xl | 18px / 24px | 600 | section and dialog titles |
| lg | 16px / 22px | 500 | emphasized body |
| base | 13.5px / 20px | 400 | body, table cells, forms |
| sm | 13px / 18px | 400 | secondary text, helper text, nav items |
| xs | 12px / 16px | 400 | captions, badges, timestamps |
| 2xs | 11px / 16px | 500, tracking 0.02em | micro mono labels (column headers, section labels) |
Weights: 400 regular, 500 medium (labels, nav items, buttons), 600 semibold (headings, stat numbers). Nothing heavier.
Spacing, radius, shadow
- Spacing base unit: 4px. Scale: 4, 8, 12, 16, 20, 24, 32, 40, 48, 64
(
--space-1through--space-16). Stay on the scale; when in doubt round down. - Radius: one family from a single 6px base.
--radius-sm4px (badges, checkboxes),--radius-md6px (buttons, inputs),--radius-lg8px (cards, panels, chart cards),--radius-xl12px (dialogs, sheets),--radius-fullfor pills and the switch. No mixed radius systems. - Shadows: three steps, near-neutral, never glacier.
--shadow-sm(inputs, rows),--shadow-md(popovers, toasts),--shadow-lg(dialogs, sheets, command palette). Dark theme leans on borders first, shadows second. No glassmorphism: overlays are opaque--popover, not blurred. - Control metrics (denser than ds-r1): default control height 34px
(
--control-h), dense 30px (--control-h-sm), prominent 42px (--control-h-lg). Every interactive component guarantees a 44px minimum hit area (--tap-target); controls with a smaller visual height extend their hit area with a pseudo-element. Border width 1px, focus ring 2px with 2px offset. - Page measure. Content is capped at
--container-max(110rem / 1760px), applied once by the app shell. Page gutters use--gutter(24px, tightening to 16px at<= 48remso a tablet does not spend a quarter of its width on padding). Anything text-heavy - prose, single-column form panels - takes the inner cap--measure-max(60rem) instead: a label-control pair stretched to 1760px puts the label a screen away from its input. Tables and dashboards may use the full container width.
Motion
Motion is feedback, not decoration. MOTION_INTENSITY 2: nothing moves unless it communicates state.
| Token | Value | Use |
|---|---|---|
--motion-fast |
100ms | hover, focus, pressed feedback |
--motion-default |
150ms | menus, popovers, switches, tabs |
--motion-slow |
220ms | dialogs, sheets, command palette, page-level transitions |
--motion-spin |
800ms | continuous loading spinner rotation |
--motion-ease-out |
cubic-bezier(0.2, 0, 0, 1) |
enters, hovers (default easing) |
--motion-ease-in-out |
cubic-bezier(0.4, 0, 0.2, 1) |
exits, reversible toggles |
--motion-rise |
-2px | hover lift distance |
--motion-enter |
8px | enter-toward travel (overlays come toward rest) |
Rules: elements enter toward their resting position (translate 8px to 0, fade
in) and exit away, never back the way they came. Lists may stagger at 30ms per
row, capped at 8 rows. The only ambient animation permitted is the live-stream
pulse on the streaming badge, because it conveys a real connection state.
Under prefers-reduced-motion: reduce all duration and distance tokens collapse
to zero and keyframe animations are disabled; the loading spinner falls back to
a static ring.
Component rules
Everything consumes tokens; no component may introduce a raw color, radius, size, or duration. shadcn/ui is the one kit; customize its components with these tokens, never ship it in default zinc.
-
Buttons. Variants: primary (
--primaryfill, the near-white/near-black emphasis surface), secondary (--secondaryfill), ghost (transparent,--accentwash on hover), destructive (--destructivefill). Heights from control metrics; radius--radius-md; weight 500; focus-visible shows the 2px--ringwith offset; disabled drops to 50% opacity; loading swaps in a--motion-spinspinner and disables the control. One primary action per view region. -
Links and inline accents. Links, the active-nav inset bar, and the live dot consume the accent blue (
--ringin content,--sidebar-primaryin the sidebar). The accent blue is never used as a component fill. -
Badges. Two shapes: solid (semantic fill + its foreground) and soft (semantic color as text over a 16%
color-mixtint with a 32% tint border). Soft is the default for tables and lists; solid is reserved for the single most important state in a region. Radius--radius-sm, size xs, weight 500. -
Status vocabulary (locked by the D-CONTRACT strings). The app must render these literal badge texts with these semantics:
Badge text Meaning Style defaultdefault provider soft muted, icon-only: a Starglyph withtitle="Default provider"and the word insr-onlytextno keyprovider has no API key or cloud credentials soft destructive disabledprovider disabled soft destructive onlineprovider enabled, credentialed, last health probe clean soft success errorlast provider health probe failed, or a log-level error / failed request soft warning (provider rail) / soft destructive (logs) enabledresource enabled soft success http-sseMCP transport (default per D11) soft info streamable-http,autoMCP transports soft info read-onlytool is safe (readOnlyHint) soft info needs confirmationtool requires approval soft warning streamingSSE log stream live soft success + pulse dot disconnectedSSE log stream down soft destructive healthyMCP server healthy soft success unhealthyMCP server failing soft destructive okgateway healthz soft success ds-r2 note:
defaultwas specified as solid primary. Shipped code renders it as a soft muted icon-only chip - the star already reads as "default", and the word cost ~45px of the 288px provider rail, which is what tipped a long-named row into the overlap bug now locked bytwo-pane.overflow.test.tsx. The literal stringssetandmissingare not rendered anywhere; provider key presence is shown as a dotted marker in the detail table and as theno keystatus in the rail. The pulse dot is allowed only onstreamingand at most once per view. No other decorative dots. -
Chips and tags. Model chips and the
CUSTOMprovider tag are soft neutral (muted) pills at radius--radius-full, xs weight 500. Removable chips carry a trailing close glyph inside the 44px hit area. -
Left nav rail.
--sidebarsurface, 15rem expanded, 3rem icon rail collapsed. A top search box carries aCtrl K/Cmd Kaffordance glyph and opens the command palette. Groups are collapsible section headers: each group label is a button witharia-expandedand a rotating chevron that discloses its leaves; a search query force-opens every group with a match, and the collapsed icon rail force-opens all of them. Leaves do not nest, and there is no external-link glyph variant. Section labels are 2xs mono muted. Item: sm weight 500, radius--radius-md, hover =--sidebar-accentwash, active =--sidebar-accentfill +--sidebar-accent-foregroundtext + a 2px--sidebar-primaryinset bar on the left edge. Footer carries the admin-token button (with a status dot: muted / success / destructive) and the theme toggle. The mono version line (gateway v{semver} on Deno {deno}) lives on the Status page, not the rail. -
Command palette. A centered overlay on
--popoverwith--shadow-lg, radius--radius-xl, opened byCmd K/Ctrl K. A single search input, then grouped results with 2xs mono group labels; the highlighted row uses the--accentwash; each row may carry a trailing mono shortcut hint. Enters toward rest at--motion-slow; scrim is a token-derived translucent wash, never a blur. -
Tabbed configuration panel. Horizontal tabs (underline indicator in
--primary) over dense field grids. Rows mix inputs, native selects, switches, and a monospace PEM textarea (--font-family-mono,--inputborder). A sticky footer on--cardwith a top--borderhairline holds the Save (primary) and Remove (destructive ghost) actions. -
Filter and facet sidebar. A secondary panel of collapsible facet groups: a searchable model list, checkbox groups, and collapsible sections with a rotating chevron. Group headers are 2xs mono muted; counts are mono. Selected facets echo as removable soft-neutral chips above the results.
-
Analytics dashboard. A grid of chart cards (
--card,--radius-lg, 1px--border) each with a title, a bar/line toggle, a legend keyed to--chart-1..5, and a graceful "No data available" empty state on the muted surface. Above them, a 4-up stat-tile row: xs muted sans label, 3xl mono semibold value, optional xs mono muted delta. Charts use only--chart-*for series and--muted-foregroundfor axes and gridlines. -
Data tables (log and resource tables). Card surface, 1px
--borderoutline, radius--radius-lg, header row in 2xs mono muted on--mutedwith sortable columns (an up/down affordance on the active sort key). Body rows are 34px min height with--space-3cell padding and a bottom hairline only; hover row gets the--accentwash. Telemetry columns (time, latency, tokens, cost, model) are mono; numeric columns right-aligned. Request status renders as a status bar plus a soft badge. Rows expose inline actions: a secret reveal (eye) toggle and copy button for keys, and edit and delete icon buttons, each inside the 44px hit area. A footer shows pagination: "Showing X-Y of Z" (mono) with Previous and Next ghost buttons. Skeleton rows use--mutedwith the shimmer guarded by reduced motion. -
Custom-provider form. An inline form in the Providers detail pane presenting a toggle grid of request types (chat, embedding, image, audio, and so on) as a wrapped grid of switch rows, plus the provider id and base URL fields. Confirm is a primary button; the provider then carries the
CUSTOMtag. -
Forms. Inputs on
--background/--cardwith--inputborders, radius--radius-md, height--control-h; labels sm weight 500; helper text sm muted; error state swaps border and helper to--destructiveand setsaria-invalid. Focus is always the 2px ring with offset. Secrets are write-only fields; the UI never renders a stored key (ahasApiKeyflag drivesset/missing), and the reveal control only unmasks freshly entered input. -
Overlays. Dialogs, sheets, and the command palette on
--popoverwith--shadow-lgand radius--radius-xl; enter toward rest at--motion-slow; scrim is a token-derived translucent wash, never a blur (no glassmorphism). Toasts bottom-right on--popoverwith--shadow-mdand a 3px semantic left border.
Usage rules
- Do keep the neutral monochrome plus one accent blue; don't introduce a second accent hue or use the accent blue as a fill.
- Do render every telemetry value (latency, cost, tokens, model ids, versions, log lines) in the mono stack; don't put prose or headings in mono.
- Do use the semantic tokens for state (success, warning, destructive, info) exactly per the status vocabulary; don't color status by eye or invent new badge texts for locked contract strings.
- Do lock each page to one theme with dark as default; don't flip themes mid-page or mix light cards onto the dark canvas.
- Do derive every visual value from a token; don't hardcode hex, oklch, px sizes, or durations in component code.
- Do use borders and surface steps for elevation on dark; don't use glows, glassmorphism, or heavy shadows.
- Do keep motion at feedback level with the reduced-motion collapse; don't animate for decoration or exceed the slow tier for UI chrome.
- Do keep the one 6px radius family; don't mix pill buttons with square cards or vary radius per component.
- Do show empty ("No data available"), loading (skeleton), and error states for every data region and chart card; don't ship a table or chart that renders blank while fetching.
- Do meet AA contrast and the 44px hit-area minimum on every interactive element in both themes; an inaccessible control is a defect, not a style choice.
- Do keep Frosty's own identity and
sk-/vk-key prefixes; don't adopt any reference product's branding or key prefixes.
Accessibility
Contrast computed programmatically (WCAG 2.x relative luminance, sRGB) by converting each token from OKLCH to sRGB and applying the 2.x contrast formula. Verdicts: AA requires 4.5:1 for text, 3:1 for large text and non-text UI; AAA is 7:1. Soft-badge rows composite the semantic color at 16% alpha over the card surface (source-over in sRGB) and measure the full-strength color text against that composited background. Every value below is a freshly computed ds-r2 figure.
Dark theme (default)
| Foreground | Background | Ratio | Requires | Verdict | Role |
|---|---|---|---|---|---|
--foreground |
--background |
18.84:1 | 4.5:1 | AAA | body text on canvas |
--foreground |
--card |
17.85:1 | 4.5:1 | AAA | body text on card |
--foreground |
--popover |
17.16:1 | 4.5:1 | AAA | body text on popover/dialog |
--foreground |
--secondary |
15.10:1 | 4.5:1 | AAA | text on secondary button |
--muted-foreground |
--background |
7.70:1 | 4.5:1 | AAA | muted text on canvas |
--muted-foreground |
--card |
7.29:1 | 4.5:1 | AAA | muted text on card |
--muted-foreground |
--popover |
7.01:1 | 4.5:1 | AAA | muted text on popover |
--muted-foreground |
--muted |
6.52:1 | 4.5:1 | AA | muted fg on muted surface |
--primary-foreground |
--primary |
14.14:1 | 4.5:1 | AAA | primary button label, solid default chip |
--accent-foreground |
--accent |
15.11:1 | 4.5:1 | AAA | hover-surface label |
--destructive-foreground |
--destructive |
5.43:1 | 4.5:1 | AA | destructive button label |
--success-foreground |
--success |
8.01:1 | 4.5:1 | AAA | solid success badge label |
--warning-foreground |
--warning |
8.74:1 | 4.5:1 | AAA | solid warning badge label |
--info-foreground |
--info |
6.67:1 | 4.5:1 | AA | solid info badge label |
--sidebar-foreground |
--sidebar |
10.77:1 | 4.5:1 | AAA | nav item label |
--muted-foreground |
--sidebar |
7.87:1 | 4.5:1 | AAA | nav section label |
--sidebar-accent-foreground |
--sidebar-accent |
15.97:1 | 4.5:1 | AAA | active nav item label |
--ring |
--background |
5.41:1 | 4.5:1 | AA | link/accent text on canvas |
--ring |
--card |
5.13:1 | 4.5:1 | AA | link/accent text on card |
--sidebar-primary |
--sidebar |
5.53:1 | 3.0:1 | AA (UI) | active nav indicator |
--ring |
--background |
5.41:1 | 3.0:1 | AA (UI) | focus ring vs canvas |
--ring |
--card |
5.13:1 | 3.0:1 | AA (UI) | focus ring vs card |
Chart marks, dark (non-text, 3:1 as a mark on --card):
| Series | Ratio vs card | Requires | Verdict |
|---|---|---|---|
--chart-1 blue |
6.00:1 | 3.0:1 | pass |
--chart-2 green |
8.00:1 | 3.0:1 | pass |
--chart-3 amber |
9.88:1 | 3.0:1 | pass |
--chart-4 red |
5.59:1 | 3.0:1 | pass |
--chart-5 violet |
4.80:1 | 3.0:1 | pass |
Soft badges, dark (color text over 16% tint composited on card):
| Badge text color | Composited bg | Ratio | Verdict |
|---|---|---|---|
--success |
#1a2e25 |
6.15:1 | AA |
--warning |
#342d1e |
7.25:1 | AAA |
--destructive |
#361e1f |
4.64:1 | AA |
--info |
#1c2936 |
5.17:1 | AA |
--muted-foreground |
#282a2c |
5.65:1 | AA |
Light theme
| Foreground | Background | Ratio | Requires | Verdict | Role |
|---|---|---|---|---|---|
--foreground |
--background |
17.59:1 | 4.5:1 | AAA | body text on canvas |
--foreground |
--card |
18.10:1 | 4.5:1 | AAA | body text on card |
--foreground |
--popover |
18.10:1 | 4.5:1 | AAA | body text on popover/dialog |
--foreground |
--secondary |
16.36:1 | 4.5:1 | AAA | text on secondary button |
--muted-foreground |
--background |
6.49:1 | 4.5:1 | AA | muted text on canvas |
--muted-foreground |
--card |
6.68:1 | 4.5:1 | AA | muted text on card |
--muted-foreground |
--popover |
6.68:1 | 4.5:1 | AA | muted text on popover |
--muted-foreground |
--muted |
6.04:1 | 4.5:1 | AA | muted fg on muted surface |
--primary-foreground |
--primary |
15.76:1 | 4.5:1 | AAA | primary button label, solid default chip |
--accent-foreground |
--accent |
14.86:1 | 4.5:1 | AAA | hover-surface label |
--destructive-foreground |
--destructive |
5.85:1 | 4.5:1 | AA | destructive button label |
--success-foreground |
--success |
5.82:1 | 4.5:1 | AA | solid success badge label |
--warning-foreground |
--warning |
5.40:1 | 4.5:1 | AA | solid warning badge label |
--info-foreground |
--info |
5.81:1 | 4.5:1 | AA | solid info badge label |
--sidebar-foreground |
--sidebar |
15.31:1 | 4.5:1 | AAA | nav item label |
--muted-foreground |
--sidebar |
6.22:1 | 4.5:1 | AA | nav section label |
--sidebar-accent-foreground |
--sidebar-accent |
14.43:1 | 4.5:1 | AAA | active nav item label |
--ring |
--background |
4.76:1 | 4.5:1 | AA | link/accent text on canvas |
--ring |
--card |
4.89:1 | 4.5:1 | AA | link/accent text on card |
--sidebar-primary |
--sidebar |
5.18:1 | 3.0:1 | AA (UI) | active nav indicator |
--ring |
--background |
4.76:1 | 3.0:1 | AA (UI) | focus ring vs canvas |
--ring |
--card |
4.89:1 | 3.0:1 | AA (UI) | focus ring vs card |
Chart marks, light (non-text, 3:1 as a mark on --card):
| Series | Ratio vs card | Requires | Verdict |
|---|---|---|---|
--chart-1 blue |
5.56:1 | 3.0:1 | pass |
--chart-2 green |
5.09:1 | 3.0:1 | pass |
--chart-3 amber |
4.05:1 | 3.0:1 | pass |
--chart-4 red |
6.12:1 | 3.0:1 | pass |
--chart-5 violet |
6.55:1 | 3.0:1 | pass |
Soft badges, light (color text over 16% tint composited on card):
| Badge text color | Composited bg | Ratio | Verdict |
|---|---|---|---|
--success |
#d6e8e0 |
4.77:1 | AA |
--warning |
#ede5d7 |
4.51:1 | AA |
--destructive |
#f5dadc |
4.64:1 | AA |
--info |
#dae6f3 |
4.79:1 | AA |
--muted-foreground |
#e5e5e6 |
5.30:1 | AA |
Result: 0 failures across 64 checked pairs (both themes): 22 text/UI pairs, 5 chart marks, and 5 soft badges per theme. Weakest required margins are the dark soft-destructive badge at 4.64:1 (text, needs 4.5:1) and the light accent link at 4.76:1 (text, needs 4.5:1); both pass.
Decorative hairlines (--border, --input, --sidebar-border) are intentionally
low-contrast structure, not required 3:1 pairs under WCAG 1.4.11; field and card
identity is carried by fill, label, and the focus ring, which do pass 3:1.
Further requirements: visible focus (2px ring, 2px offset) on every interactive
element; 44px minimum hit areas; full keyboard path (including the command
palette and all row actions); labels tied to inputs; role=alert on error
banners; aria-live=polite on the log stream region; sortable headers exposed
with aria-sort; reduced-motion collapse as specified in Motion.
References and provenance
- Revision: ds-r2, which supersedes ds-r1 (glacier-blue). ds-r1 was a saturated cyan-leaning "glacier" identity; the owner found it too branded for an operator console and supplied reference screenshots of a modern neutral LLM-gateway console. ds-r2 re-anchors on that neutral register.
- Anchor: the modern near-neutral shadcn console register (shadcn
"new-york"/neutral), from the owner's reference screenshots: monochrome
cool near-grays, a light-on-dark primary, a single restrained accent blue for
focus/links/active-nav, and a functional semantic status vocabulary. Frosty
keeps its own identity and
sk-/vk-key prefixes; it does not adopt the reference's branding or key prefixes. - ds-r1 to ds-r2 delta: neutral hue moved from cool slate (about 230) to a
whisper-cool 265 at much lower chroma; the glacier brand accent was removed and
primary became the shadcn-neutral monochrome inversion (near-white in dark,
near-black in light); the accent blue is now reserved (focus/links/active-nav
only, never a fill); a
--chart-1..5family was added for the new analytics dashboard; density increased one step (13.5px body, 34px default control, motion default 150ms / slow 220ms); thedefaultbadge moved from soft primary to solid primary. Token NAMES are unchanged so the app re-themes by swapping values. - UI kit: shadcn/ui on Tailwind CSS v4 + React + Vite (locked by intake
D-STACK). Theming format OKLCH, dual theme, dark default. One kit for the
whole project; no second component system. Install:
npx shadcn@latest init(Vite flow), thennpx shadcn@latest add button badge input select checkbox switch table tabs dialog sheet command sonner alert skeleton sidebar label form tooltip pagination. - Icons: lucide-react, explicitly granted by the owner for shadcn parity (recorded intake grant). No hand-rolled SVG icons.
- Fonts: system stacks only; JetBrains Mono (SIL OFL 1.1) named as optional progressive enhancement, no font files shipped, no webfont fetches.
- Locked intake decisions honored: D-STACK (kit), D-IDENTITY (own Frosty
identity, dark-first, both themes tokenized), D-CONTRACT (status badge
vocabulary and literal strings), D-DELIVERABLE (this file plus tokens.css plus
design-system.html at
docs/design/). - Contrast method: each token converted OKLCH to sRGB and scored with the WCAG 2.x relative-luminance contrast formula; soft badges composited at 16% over card. 0 failures across 64 pairs in both themes.