{ "schemaVersion": 2, "generatedAt": "2026-08-22T00:00:00.000Z", "title": "Design System: arr — Master Control", "extensions": { "colorMeta": { "ground": { "role": "neutral", "displayName": "Console Charcoal", "canonical": "oklch(0.14 0.01 250)", "tonalRamp": [ "oklch(0.14 0.01 250)", "oklch(0.18 0.012 250)", "oklch(0.21 0.014 250)", "oklch(0.3 0.015 250)", "oklch(0.42 0.02 250)", "oklch(0.58 0.015 250)", "oklch(0.72 0.015 250)", "oklch(0.93 0.008 250)" ] }, "signal-ok": { "role": "secondary", "displayName": "Signal Green", "canonical": "oklch(0.8 0.17 150)", "tonalRamp": ["oklch(0.45 0.1 150)", "oklch(0.62 0.14 150)", "oklch(0.8 0.17 150)", "oklch(0.88 0.12 150)"] }, "signal-warn": { "role": "tertiary", "displayName": "Caution Amber", "canonical": "oklch(0.82 0.14 80)", "tonalRamp": ["oklch(0.5 0.1 80)", "oklch(0.66 0.13 80)", "oklch(0.82 0.14 80)", "oklch(0.9 0.1 80)"] }, "signal-fault": { "role": "secondary", "displayName": "Fault Red", "canonical": "oklch(0.68 0.21 25)", "tonalRamp": ["oklch(0.42 0.15 25)", "oklch(0.55 0.19 25)", "oklch(0.68 0.21 25)", "oklch(0.8 0.14 25)"] }, "accent": { "role": "primary", "displayName": "Phosphor Cyan", "canonical": "oklch(0.8 0.12 220)", "tonalRamp": ["oklch(0.5 0.1 220)", "oklch(0.65 0.11 220)", "oklch(0.8 0.12 220)", "oklch(0.9 0.08 220)"] }, "verdict-eligible": { "role": "secondary", "displayName": "Eligible Green", "canonical": "oklch(0.8 0.17 150)", "tonalRamp": ["oklch(0.45 0.1 150)", "oklch(0.62 0.14 150)", "oklch(0.8 0.17 150)", "oklch(0.88 0.12 150)"] }, "verdict-waived": { "role": "tertiary", "displayName": "Waiver Amber", "canonical": "oklch(0.82 0.14 80)", "tonalRamp": ["oklch(0.5 0.1 80)", "oklch(0.66 0.13 80)", "oklch(0.82 0.14 80)", "oklch(0.9 0.1 80)"] }, "verdict-rejected": { "role": "neutral", "displayName": "Rejected Slate", "canonical": "oklch(0.68 0.02 250)", "tonalRamp": ["oklch(0.42 0.02 250)", "oklch(0.55 0.02 250)", "oklch(0.68 0.02 250)", "oklch(0.8 0.015 250)"] }, "status-channel": { "role": "tertiary", "displayName": "Channel Violet", "canonical": "oklch(0.78 0.14 305)", "tonalRamp": ["oklch(0.64 0.045 305)", "oklch(0.72 0.09 305)", "oklch(0.78 0.14 305)", "oklch(0.86 0.09 305)"] } }, "typographyMeta": { "display": { "displayName": "Display (Rajdhani)", "purpose": "Rajdhani 700 — wordmark and module names only. Machined, condensed, uppercase with 0.1-0.12em tracking. Self-hosted woff2 in web/public/fonts (no CDN: the SPA is embedded in the binary and must work offline)." }, "label": { "displayName": "Label (system sans)", "purpose": "system-ui 600 uppercase, 0.6875rem, 0.12em tracking — engraved micro-labels (module roles). Never a kicker above a heading." }, "readout": { "displayName": "Readout (mono)", "purpose": "ui-monospace stack — every value the operator reads: status words, details, version, rtt. Mono is for measurement, never costume." }, "scale": { "displayName": "Type scale", "purpose": "Seven fixed steps as tokens: --text-2xs 0.6875rem (micro-labels), --text-xs 0.75rem (details, chips), --text-sm 0.8125rem (readouts, controls), --text-base 1rem (body), --text-md 1.0625rem (module names), --text-lg 1.3125rem (master module), --text-xl 1.625rem (wordmark). Components never carry a literal font-size." } }, "shadows": [ { "name": "panel-highlight", "value": "inset 0 1px oklch(1 0 0 / 4%)", "purpose": "The machined top edge of every panel and control. The only non-lamp shadow in the system." }, { "name": "lamp-glow", "value": "0 0 6px var(--lamp), 0 0 14px oklch(from var(--lamp) l c h / 35%)", "purpose": "A lamp's own light. Reserved for .lamp; never decoration on panels or text." } ], "motion": [ { "name": "ease-out", "value": "cubic-bezier(0.25, 1, 0.5, 1)", "purpose": "Default easing for state transitions." }, { "name": "duration-state", "value": "150-300ms", "purpose": "Hover/focus 150ms, lamp and trace state changes 300ms." }, { "name": "power-up strike", "value": "lamp-strike 420ms, staggered 120ms upstream→downstream, once", "purpose": "The one authored moment: the board lights in signal order on first verdict. Guarded by prefers-reduced-motion." } ], "breakpoints": [{ "name": "stack", "value": "46rem — the chain turns vertical, one junctioned trace" }], "themes": { "strategy": "committed dark only — a control room is lit by its lamps; the operator uses this on a homelab desk at night. color-scheme: dark is declared in the shell.", "dark": { "bg": "oklch(0.14 0.01 250)", "ink": "oklch(0.93 0.008 250)", "primary": "oklch(0.8 0.12 220)" } } }, "components": [ { "name": "Lamp", "kind": "custom", "refersTo": "signal-ok", "description": "Status lamp. Hue by state (ok/degraded/unconfigured/unreachable), glow is its own light. data-state=\"off\" is dead air: dark, no glow, no motion. Always paired with a status word — remove color and the state must survive.", "html": "", "css": ".lamp { --lamp: var(--signal-idle); width: 0.625rem; height: 0.625rem; border-radius: 50%; background: var(--lamp); box-shadow: 0 0 6px var(--lamp), 0 0 14px oklch(from var(--lamp) l c h / 35%); } .lamp[data-state=\"ok\"] { --lamp: var(--signal-ok); } .lamp[data-state=\"off\"] { --lamp: var(--lamp-off); box-shadow: none; }" }, { "name": "Chip", "kind": "custom", "refersTo": "ground", "description": "The §9.3 column unit: a small machined panel holding one mono value (score, resolution, source, HDR, audio, size, seeders). Fixed rhythm so columns align; never a soft pill.", "html": "2160p", "css": ".chip { display: inline-flex; align-items: center; gap: var(--space-1); min-height: 1.375rem; padding: 0 var(--space-2); background: var(--panel-raised); border: 1px solid var(--line); border-radius: var(--radius); font-family: var(--font-readout); font-size: var(--text-xs); color: var(--ink-muted); }" }, { "name": "Verdict chip", "kind": "custom", "refersTo": "verdict-eligible", "description": "Release classification (§9.3): eligible / waived / rejected. Hue + border treatment + the word, so the three buckets survive with colour removed — eligible is a solid tinted border, waived a dashed border (the visible mark of a relaxed rule), rejected is quiet slate and always carries the killing rule's name. Rejected is the policy working, never fault red.", "html": "waived · pt missing", "css": ".chip[data-verdict=\"eligible\"] { color: var(--verdict-eligible); border-color: oklch(from var(--verdict-eligible) l c h / 45%); } .chip[data-verdict=\"waived\"] { color: var(--verdict-waived); border-style: dashed; border-color: oklch(from var(--verdict-waived) l c h / 55%); } .chip[data-verdict=\"rejected\"] { color: var(--verdict-rejected); }" }, { "name": "Status chip", "kind": "custom", "refersTo": "status-channel", "description": "Library status (§4.2): airing / incomplete / waiting / complete / ended. Informational, not severity — colour marks activity in one channel-violet family (airing brightest, incomplete, waiting dimmest), satisfaction is neutral (complete bright ink, ended faint). The mono word is always present.", "html": "airing", "css": ".chip[data-status=\"airing\"] { color: var(--status-airing); border-color: oklch(from var(--status-airing) l c h / 45%); } .chip[data-status=\"incomplete\"] { color: var(--status-incomplete); } .chip[data-status=\"waiting\"] { color: var(--status-waiting); } .chip[data-status=\"complete\"] { color: var(--status-complete); } .chip[data-status=\"ended\"] { color: var(--status-ended); }" }, { "name": "Module (channel strip)", "kind": "card", "refersTo": "ground", "description": "One unit on the chain: lamp + Rajdhani name + mono status word, role label, mono detail. Panel with hairline border and 1px inset highlight; 4px machined radius, never a soft card.", "html": "

prowlarr

ok

indexers in

torznab search and rss

", "css": ".module { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: inset 0 1px var(--highlight); padding: var(--space-4) var(--space-4) var(--space-3); }" }, { "name": "Bus trace", "kind": "custom", "refersTo": "ground", "description": "Signal path between modules: hairline + arrowhead SVG, tethered to its source module's row. Lit with the source's signal hue via data-tone; inert gray when nothing flows.", "html": "", "css": ".bus-trace { --trace: var(--line-strong); } .bus-trace line { stroke: var(--trace); } .bus-arrow { fill: var(--trace); } .bus-trace[data-tone=\"ok\"] { --trace: var(--signal-ok); }" }, { "name": "Control", "kind": "button", "refersTo": "accent", "description": "The interactive element: mono uppercase, phosphor-cyan text, panel ground, ≥44px hit target. Cyan belongs to interaction only — never to status.", "html": "", "css": ".control { min-height: 2.75rem; padding: 0 var(--space-4); font-family: var(--font-readout); text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); background: var(--panel-raised); border: 1px solid var(--line-strong); border-radius: var(--radius); }" }, { "name": "Rail", "kind": "nav", "refersTo": "ground", "description": "Full-width panel strip framing the board top (identity + verdict + controls) and bottom (readouts). Hairline-ruled, panel ground, inset highlight.", "html": "

arr

all signals nominal

", "css": ".rail { display: flex; align-items: center; gap: var(--space-6); padding: var(--space-3) var(--space-6); border-bottom: 1px solid var(--line); background: var(--panel); box-shadow: inset 0 1px var(--highlight); }" } ], "narrative": { "northStar": "Master Control", "overview": "arr's UI is a broadcast master-control panel: the daemon is a signal chain (TMDB and Prowlarr feed in, Transmission is driven out) and every surface reads as lamps, readouts and traces on that chain. Near-black charcoal ground, machined panels with hairline borders and a 1px inset highlight, Rajdhani display names, monospace readouts, three semantic lamp hues plus one phosphor-cyan interactive hue. Committed dark. Built for the operator's one-glance question: can arr do its job, and if not, which upstream is down.", "keyCharacteristics": [ "Signal chain, not dashboard: topology mirrors DESIGN.md §3; no sidebar, no stat cards, no hero metric.", "Lamps carry state, words carry proof — every hue is paired with a mono status word.", "Rajdhani 700 uppercase for names, system sans for micro-labels, mono for every value.", "Panels are machined: hairline border, inset highlight, 4px radius; never soft shadows or glass.", "One authored motion: the power-up strike, upstream to downstream, once." ], "rules": [ { "name": "The Lamp-Hue Rule", "body": "Green/amber/red belong to state and to nothing else. Interaction is phosphor-cyan. A saturated color that is neither a state nor an interactive affordance is a bug.", "section": "colors" }, { "name": "The Word-Beside-The-Lamp Rule", "body": "Every lamp and lit trace is paired with a monospace status word. Remove all color and the board must still read.", "section": "colors" }, { "name": "The Readout Rule", "body": "Monospace is for values the operator reads — statuses, sizes, latencies, versions. Prose and labels are never mono; names are never sans.", "section": "typography" }, { "name": "The Machined-Panel Rule", "body": "Depth is one inset 1px highlight on a hairline-bordered panel. No drop shadows, no glass, no gradients. The only glow on the board is a lamp's own light.", "section": "elevation" }, { "name": "The Verdict Rule", "body": "Release verdicts are eligible green, waiver amber, rejected slate. Rejected is the policy engine doing its job, so it is quiet neutral — fault red stays reserved for the daemon's own failures. Each bucket also carries a non-colour mark: solid tinted border (eligible), dashed border (waived), the killing rule's name (rejected).", "section": "colors" }, { "name": "The Status-Is-Not-Severity Rule", "body": "The five library statuses are informational, never an error/warning/success ramp. Activity lives in one channel-violet family (airing > incomplete > waiting, by lightness and chroma); satisfaction is neutral (complete bright ink, ended faint). Complete is never green; incomplete is never amber.", "section": "colors" }, { "name": "The Token Rule", "body": "Components consume the custom properties declared in :root of web/src/style.css — never a literal colour, margin, padding or gap. web/scripts/check-tokens.mjs enforces this in CI; relative-colour derivation from a token (oklch(from var(--x) …)) counts as consuming the token.", "section": "colors" }, { "name": "The Dead-Air Rule", "body": "When nothing is flowing, nothing animates. Pulse means probing; data-state=\"off\" is dark and still. Motion that misreports activity is a truth bug.", "section": "motion" } ], "dos": [ "Do lay future surfaces (manual search buckets, queues) out as panels and readouts consuming these tokens; chips in §9.3 are small panels with mono values.", "Do keep every control ≥44px, focus rings cyan and visible, and reduced-motion honored.", "Do keep muted text at or above 4.5:1 on panel ground (--ink-muted floor).", "Do self-host every asset; the SPA is embedded in the binary and must work with no network.", "Do take every colour, spacing and font-size from the :root tokens; the token check (web/scripts/check-tokens.mjs) fails CI on literals." ], "donts": [ "Don't colour `complete` green or `incomplete` amber — library status is informational, not a severity ramp.", "Don't use fault red for a rejected release; red means the daemon itself is broken.", "Don't introduce a second interactive hue or use cyan for a status.", "Don't box content in soft-shadowed rounded cards or add glass/blur.", "Don't put a kicker/eyebrow label above a heading.", "Don't animate idle or dead elements; the strike runs once, on real data." ] } }