# Cairn OBS Design System (Phase 5) Direction: **Signal** — cockpit/ICU-monitor instrumentation logic. Color is a rationed resource: the UI is near-neutral grayscale everywhere, so the four severity tiers land as genuinely the most saturated colors anywhere on screen, not one saturated color competing with a dozen decorative ones. See the Phase 5 design-direction review (three options were presented; Signal was the one picked) for the two rejected directions and the full rationale. This doc is the source of truth for the token system and component library `/web/src/lib/components/ui` implements — read it before adding a new color, spacing value, or component, rather than reaching for a literal hex or one-off markup the way Phase 0-3's pages did. ## Status **Status: Phase 5 shipped.** All ten tasks are built: the token system, the `ui/` component library, the persistent sidebar nav + command palette, a real ECharts-based charting layer (5 chart types, drill-down, zoom), the dashboard panel rebuild (GridStack drag-and-drop + live-preview panel editor), the query/search redesign (CodeMirror syntax highlighting + autocomplete, sortable/resizable/expandable results), the alerting UI redesign (severity-state pill + delivery timeline), and an accessibility pass driven by real axe-core runs against the live app (not static analysis) — see the Accessibility section below for what that actually caught. See `/docs/phase-5-runbook.md` for the full verification log. Verified in a real browser against a live docker-compose stack with real seeded data (not just fixtures): dark/light/system theme and comfortable/compact density switching persisting across navigation and reload; command palette open/filter/keyboard-navigate/go-to; every nav route including dashboard/alert detail pages with real panels and real alert history; all 6 panel viz types rendering real query results; a firing alert rule's full state-history timeline; keyboard-only operation of the query editor, results table (sort/expand), and dashboard grid. ## Fonts Self-hosted (not a Google Fonts CDN link — no runtime dependency on a third party for the app to render correctly): `web/static/fonts/`, licensed under the SIL Open Font License (see that directory's `LICENSE.txt`). | Role | Typeface | Why | |---|---|---| | UI (nav, labels, headings, body) | Overpass | Originally drawn for U.S. highway signage — engineered to be read correctly, fast, under bad conditions. A more honest reason to pick a typeface for an incident-response tool than "it looks modern." | | Data (log tables, code, query bar, numbers) | Overpass Mono | Same family as the UI face — one typeface end to end removes even the small cognitive cost of a font pairing, matching the direction's overall restraint. | Both are variable fonts (one file covers the full weight range), loaded via `@font-face` in `web/src/lib/styles/fonts.css`. ## Color tokens Defined in `web/src/lib/styles/tokens.css`. Dark is the literal default — `:root` defines the dark palette directly, light is the override (both `@media (prefers-color-scheme: light)` for an unset preference and `[data-theme="light"]` for an explicit one) — not a retrofit where light is `:root` and dark is bolted on. Component CSS must only ever read a token, never a literal hex; that's what makes the theme/density toggles a token swap instead of a per-component rewrite. | Token | Dark | Light | Use | |---|---|---|---| | `--color-bg` | `#0a0a0b` | `#f7f7f8` | Page background | | `--color-surface` | `#17181a` | `#ffffff` | Cards, tables, inputs | | `--color-surface-raised` | `#1e2023` | `#ffffff` | Hover states, popovers | | `--color-border` / `--color-border-strong` | `#2a2c2f` / `#3a3d41` | `#dfe0e2` / `#c7c9cc` | Dividers, input borders | | `--color-text` / `--color-text-muted` / `--color-text-faint` | `#f0f0f1` / `#85888d` / `#8a8d92` | `#101113` / `#6b6e73` / `#75787d` | Body text hierarchy | | `--color-accent` / `--color-accent-strong` | `#3fb6ff` | `#0b84d6` | Interactive elements only — links, primary buttons, focus rings, active nav. Never reused for severity (see below); a semantic color competing with the brand accent defeats the point of "color means something." | ### Severity tiers The schema carries seven OTel severities (`TRACE`/`DEBUG`/`INFO`/`WARN`/`ERROR`/`FATAL`/`UNSPECIFIED`, see `/storage/README.md`). Seven colors would be seven things to memorize at a glance; `web/src/lib/severity.ts`'s `severityTier()` collapses them to four accent tiers plus one "quiet" state: | OTel severity | Tier | Dark | Light | |---|---|---|---| | `TRACE`, `DEBUG`, `UNSPECIFIED` | quiet | `#85888d` | `#6b6e73` | | `INFO` | info | `#4c8dff` | `#1a63d6` | | `WARN` | warn | `#f5c242` | `#8c6800` | | `ERROR` | error | `#ff6a39` | `#c94b1e` | | `FATAL` | critical | `#ff2d78` | `#c21362` | Chosen as a blue → amber → orange → magenta progression — hue *and* lightness both shift at every step, so no two adjacent tiers rely on red-vs-green to be told apart, and the sequence should survive grayscale and protanopia/deuteranopia simulation. That reasoning hasn't been verified with an actual simulator yet — do that before treating it as confirmed accessible, not just plausible. Each tier also has a translucent `-bg` token (e.g. `--color-sev-warn-bg`) for chip/pill backgrounds. These are tuned independently of the solid foreground colors above, not derived from them by a fixed formula: a translucent color tints *toward* its own hue as alpha increases, which for these saturated, low-luminance hues (critical's magenta especially) *lowers* contrast against the foreground text the higher the alpha goes — the opposite of the intuitive "more opaque background is safer" assumption. `--color-sev-warn` itself was darkened in light mode (`#9c7300` → `#8c6800`) for the same underlying reason: axe-core caught the *plain* light-mode warn text failing AA (4.32:1) against white before any background was even involved. See the Accessibility section. Use `` (`ui/SeverityBadge.svelte`) to render one of these — it owns the OTel string → tier mapping so call sites can't invent a sixth color by hand. For non-log-severity status (success/danger/neutral/accent — e.g. a "saved" confirmation), use `` instead; the two are deliberately separate components so a form-validation color can never accidentally collide with a log-severity one. ## Type scale 14px base (dense-first — this is a tool for reading log tables, not marketing copy), 1.2 modular ratio: `--text-xs` 11px · `--text-sm` 13px · `--text-base` 14px · `--text-md` 16px · `--text-lg` 20px · `--text-xl` 28px · `--text-2xl` 40px `--font-weight-normal` 400, `--font-weight-medium` 600, `--font-weight-bold` 700. `font-variant-numeric: tabular-nums` is set globally on `body` so columns of numbers/timestamps align. ## Spacing, radius, shadow 8px-based spacing scale: `--space-1` through `--space-8` (4px, 8px, 12px, 16px, 24px, 32px, 48px, 64px). Radius: `--radius-sm` 4px, `--radius-md` 6px, `--radius-lg` 10px, `--radius-full` (pills). Shadow is used sparingly, matching Signal's restraint — only real overlays (modals, the command palette, tooltips) get one (`--shadow-sm/md/lg`); inline UI never does. ## Density Two presets, one token swap — `html.density-compact` overrides `--row-height`, `--row-padding-y/x`, `--panel-padding`, `--control-height`, and drops `--text-base` to `--text-sm`. Comfortable (the default) suits dashboards and forms; compact suits log tables and query results. Toggle via `$lib/density.svelte.ts`'s `setDensity()`/`toggleDensity()` — a global, persisted (`localStorage['cairnobs.density']`) preference, not a per-page setting, so switching it on one page carries to the next. `web/src/app.html` has a synchronous inline script that applies the stored value before first paint, so there's no flash of the wrong density on reload; keep that script's storage key/values in sync with `density.svelte.ts` if either changes. ## Theme `$lib/theme.svelte.ts`, same persisted/synchronous-apply shape as density. Three states — `'dark' | 'light' | 'system'` — but unlike most apps, the *unset* default is `'dark'`, not `'system'`. That's the actual point of "real dark mode as the default, not an afterthought": a first-time visitor on a light-OS machine still lands in dark. `'system'` is available as a deliberate opt-in for anyone who wants their OS setting to win instead. ## Components (`web/src/lib/components/ui/`) Import from the barrel: `import { Button, Input, ... } from '$lib/components/ui';` | Component | Notes | |---|---| | `Button` | `variant`: `primary`/`secondary`/`ghost`/`danger`. `size`: `sm`/`md`. Renders `` when `href` is passed, `