# Design — scan_web Locked design system for the unified scan/record control panel. Every future view (the M3 Scanning/LIO view, any later addition) reads this file before shipping new UI — extend it, don't invent a parallel system. ## Genre modern-minimal (Stripe / Linear / instrument-panel school — SaaS/dashboard trigger) ## Macrostructure family This is an **app**, not a marketing site — no macrostructure/nav-archetype/ footer-archetype apparatus applies (those are page-shape concepts for landing pages). The whole app is one page-type: - App pages (Setup/Control view, Scanning/Live view, any future view): hairline-bordered panel grid, left-biased control column + right-biased primary content, sticky toolbar with a segmented view switch. No enrichment — function carries the page (never add hero imagery/CSS art). ## Theme Custom — cool-cobalt, referenced against the "Tally" modern-minimal SaaS example the user pointed at, adapted toward Cobalt's instrument-panel discipline (hairlines over shadow, mono status readouts) since the app is an operational dashboard, not a marketing hero. - `--color-paper` oklch(97.8% 0.005 255) - `--color-paper-1` oklch(99.3% 0.003 255) - `--color-ink` oklch(22.0% 0.020 258) - `--color-ink-2` oklch(38.0% 0.016 257) - `--color-rule` oklch(85.0% 0.010 255) - `--color-accent` oklch(52.0% 0.185 256) - `--color-focus` oklch(48.0% 0.200 256) - Functional state colours (not brand accent — status semantics): `--color-success` (149°), `--color-warning` (68°), `--color-danger` (25°) Full token set: [`frontend/css/tokens.css`](frontend/css/tokens.css). ## Typography - Display: Geist, weight 650, normal style - Body: Geist, weight 400 (single-family discipline — the modern-minimal signature) - Mono/outlier: Geist Mono, weight 500 — carries exactly one role: status badges, tab labels, meta/hint text, and numeric-readout fields (camera params). Do not reach for it a third role. - Korean UI copy falls back per-glyph to "Noto Sans CJK KR" / "Noto Sans KR" automatically — Geist covers Latin only. - Fonts are self-hosted at `frontend/vendor/fonts/` (woff2, variable) — never a Google Fonts CDN link. The field PC/phone may have no internet. ## Spacing 4-point named scale in `tokens.css` (`--space-3xs` … `--space-2xl`). Always reference by name. ## Motion - Easings: `--ease-out` / `--ease-in` / `--ease-in-out`, no bounce/overshoot. - Modern-minimal default: reveals are OFF — this is a repeatedly-used tool, not a first-impression marketing page. The only motion is functional: button press, badge colour transition, tab crossfade, REC pulse dot. - `prefers-reduced-motion: reduce` collapses everything to ≤150ms opacity (see `tokens.css`). ## Microinteractions stance - Silent success (no toast for visible state changes). - Focus rings appear instantly, never animated in. - No `transition: all` anywhere — properties are named explicitly. ## Status/health vocabulary — must stay legible in direct sunlight Every status signal is **icon + text + colour**, never colour alone (outdoor/colour-blind requirement, not just a Hallmark default): - `badge-ok` — ✓, success green - `badge-stale` — !, warning amber - `badge-down` — ✕, danger red - `badge-rec` — pulsing dot, danger red - The ● / state-line idiom (`● 실행 중` / `● 정지`) is carried over from the original PyQt5 tools on purpose — operators already know it. ## CTA voice - Primary actions (시동/녹화 시작): pill radius, solid semantic fill (`--color-success` / `--color-danger`), 44px min-height (touch target floor). - Secondary/utility actions: `.btn-gray`, `.btn-blue` — same pill shape, neutral or accent fill. - E-STOP: outlined danger pill that fills solid on hover/press — visually distinct from the recording Stop button, since it's a different severity of action (kills every subsystem at once). ## What pages MUST share - The token set in `tokens.css` — never an inline OKLCH/hex value. - The badge/status vocabulary above. - 44px minimum touch target on every interactive control (phone access is a hard requirement, not a nice-to-have). - Hairline-bordered panels, whisper shadow at most — no drop-shadow cards. ## What pages MAY differ on - Panel layout within a view (the Scanning/LIO view is full-viewport canvas + status strip, not the Setup view's panel grid — that's fine, it's a different *content* need, not a different *system*). ## Related tool: the calibration GUI (`~/dvlc_gui/dvlc_calib_gui.py`) Native PyQt5, out of scope for a web port (Phase 2, if ever). Its Qt stylesheet was updated to reuse this same palette (sRGB conversions of the tokens above) purely for visual consistency across the two tools the operator switches between — no functional change. See the `STYLESHEET` constant and `C_*` colour constants near the top of that file.