# Frosty design system Canonical design source of truth for the Frosty Control Plane UI (`apps/control-ui`). Tokens are materialized in [`tokens.css`](./tokens.css) and documented live in [`design-system.html`](./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](#references-and-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](#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-1` through `--space-16`). Stay on the scale; when in doubt round down. - **Radius: one family from a single 6px base.** `--radius-sm` 4px (badges, checkboxes), `--radius-md` 6px (buttons, inputs), `--radius-lg` 8px (cards, panels, chart cards), `--radius-xl` 12px (dialogs, sheets), `--radius-full` for 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 `<= 48rem` so 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 (`--primary` fill, the near-white/near-black emphasis surface), secondary (`--secondary` fill), ghost (transparent, `--accent` wash on hover), destructive (`--destructive` fill). Heights from control metrics; radius `--radius-md`; weight 500; focus-visible shows the 2px `--ring` with offset; disabled drops to 50% opacity; loading swaps in a `--motion-spin` spinner 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 (`--ring` in content, `--sidebar-primary` in 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-mix` tint 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 | | --- | --- | --- | | `default` | default provider | soft muted, icon-only: a `Star` glyph with `title="Default provider"` and the word in `sr-only` text | | `no key` | provider has no API key or cloud credentials | soft destructive | | `disabled` | provider disabled | soft destructive | | `online` | provider enabled, credentialed, last health probe clean | soft success | | `error` | last provider health probe failed, or a log-level error / failed request | soft warning (provider rail) / soft destructive (logs) | | `enabled` | resource enabled | soft success | | `http-sse` | MCP transport (default per D11) | soft info | | `streamable-http`, `auto` | MCP transports | soft info | | `read-only` | tool is safe (readOnlyHint) | soft info | | `needs confirmation` | tool requires approval | soft warning | | `streaming` | SSE log stream live | soft success + pulse dot | | `disconnected` | SSE log stream down | soft destructive | | `healthy` | MCP server healthy | soft success | | `unhealthy` | MCP server failing | soft destructive | | `ok` | gateway healthz | soft success | ds-r2 note: `default` was 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 by `two-pane.overflow.test.tsx`. The literal strings `set` and `missing` are not rendered anywhere; provider key presence is shown as a dotted marker in the detail table and as the `no key` status in the rail. The pulse dot is allowed only on `streaming` and at most once per view. No other decorative dots. - **Chips and tags.** Model chips and the `CUSTOM` provider 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.** `--sidebar` surface, 15rem expanded, 3rem icon rail collapsed. A top **search box** carries a `Ctrl K` / `Cmd K` affordance glyph and opens the command palette. Groups are **collapsible section headers**: each group label is a button with `aria-expanded` and 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-accent` wash, active = `--sidebar-accent` fill + `--sidebar-accent-foreground` text + a 2px `--sidebar-primary` inset 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 `--popover` with `--shadow-lg`, radius `--radius-xl`, opened by `Cmd K` / `Ctrl K`. A single search input, then grouped results with 2xs mono group labels; the highlighted row uses the `--accent` wash; 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`, `--input` border). A sticky footer on `--card` with a top `--border` hairline 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-foreground` for axes and gridlines. - **Data tables (log and resource tables).** Card surface, 1px `--border` outline, radius `--radius-lg`, header row in 2xs mono muted on `--muted` with **sortable columns** (an up/down affordance on the active sort key). Body rows are 34px min height with `--space-3` cell padding and a bottom hairline only; hover row gets the `--accent` wash. 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 `--muted` with 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 `CUSTOM` tag. - **Forms.** Inputs on `--background`/`--card` with `--input` borders, radius `--radius-md`, height `--control-h`; labels sm weight 500; helper text sm muted; error state swaps border and helper to `--destructive` and sets `aria-invalid`. Focus is always the 2px ring with offset. Secrets are write-only fields; the UI never renders a stored key (a `hasApiKey` flag drives `set`/`missing`), and the reveal control only unmasks freshly entered input. - **Overlays.** Dialogs, sheets, and the command palette on `--popover` with `--shadow-lg` and 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 `--popover` with `--shadow-md` and a 3px semantic left border. ## Usage rules 1. **Do** keep the neutral monochrome plus one accent blue; **don't** introduce a second accent hue or use the accent blue as a fill. 2. **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. 3. **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. 4. **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. 5. **Do** derive every visual value from a token; **don't** hardcode hex, oklch, px sizes, or durations in component code. 6. **Do** use borders and surface steps for elevation on dark; **don't** use glows, glassmorphism, or heavy shadows. 7. **Do** keep motion at feedback level with the reduced-motion collapse; **don't** animate for decoration or exceed the slow tier for UI chrome. 8. **Do** keep the one 6px radius family; **don't** mix pill buttons with square cards or vary radius per component. 9. **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. 10. **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. 11. **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..5` family was added for the new analytics dashboard; density increased one step (13.5px body, 34px default control, motion default 150ms / slow 220ms); the `default` badge 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), then `npx 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.