docs/adr/ADR-169-adam-mode-light-theme.md
| Field | Value |
|---|---|
| Status | Proposed |
| Date | 2026-06-02 |
| Deciders | ruv |
| Codename | adam-mode |
| Scope | examples/three.js/demos/05-skinned-realtime.html (primary), demos 01–04 (follow-on) |
| Relates to | ADR-019 (sensing-only UI), ADR-035 (live sensing UI accuracy) |
| Tracking issue | none yet |
examples/three.js/demos/05-skinned-realtime.html (build stamp 2026-05-15-fps-tune) is the live MediaPipe → Mixamo retargeting + ESP32 CSI overlay demo. It currently ships a single, opinionated dark theme:
--bg: #050507 (near-black), --text: #d8c69a (warm beige).--amber: #ffb840, --amber-hot: #ffe09f) on panels and controls..overlay-frame and a 50%-opacity CRT-style .scanlines layer.scene.background = new THREE.Color(0x050507) and scene.fog = new THREE.FogExp2(0x050507, 0.06) (lines 269–270).The dark/amber CRT aesthetic is intentional for screen-recording and "command-centre" feel, but it has real failure modes:
A toggle is the right answer because none of these reasons are universal — some sessions and some users want each mode.
Add a client-side theme toggle to 05-skinned-realtime.html that switches between the existing dark theme and a new light theme called adam-mode, driven by a data-theme="adam" attribute on <body> plus a sibling :root[data-theme="adam"] CSS block that re-defines the existing custom properties. A new toggle button in the existing #helpers panel switches between modes and persists the choice in localStorage under the key ruview.theme.
Add immediately after the existing :root { ... } block in <style>:
:root[data-theme="adam"] {
--bg: #f6f2ea;
--bg-panel: rgba(252, 250, 246, 0.92);
--amber: #b8741a; /* deeper amber, readable on cream */
--amber-hot: #8a5612; /* deepest amber for emphasis text */
--cyan: #1a6f8a; /* slate cyan */
--magenta: #a8348a; /* slate magenta */
--text: #2a241c; /* near-black warm */
--text-mute: #7a6f5d; /* warm grey */
--green: #1f7a32; /* forest green */
--red: #b03a1a; /* burnt sienna */
--border: rgba(184, 116, 26, 0.28);
}
Every existing element already reads from these custom properties, so the swap is automatic for panels, text, borders, and bar fills. No per-element CSS rewrites required.
The vignette and scanlines are dark-theme aesthetics. In adam-mode they would muddy the cream background. Two new rules:
:root[data-theme="adam"] .overlay-frame {
background:
radial-gradient(ellipse at center, transparent 70%, rgba(184,116,26,0.10) 100%),
linear-gradient(180deg, rgba(184,116,26,0.06) 0%, transparent 18%, transparent 82%, rgba(184,116,26,0.08) 100%);
}
:root[data-theme="adam"] .scanlines {
opacity: 0.15;
mix-blend-mode: multiply;
}
The vignette is preserved but inverted in colour and lightened; scanlines drop to 15 % opacity and switch from overlay to multiply blend so they read as faint paper texture rather than CRT lines.
Two scene colours are hard-coded at construction (lines 269–270). Replace them with a function call that reads the current theme:
function themeSceneColors(theme) {
return theme === 'adam'
? { bg: 0xf6f2ea, fogDensity: 0.025 }
: { bg: 0x050507, fogDensity: 0.06 };
}
function applySceneTheme(theme) {
const c = themeSceneColors(theme);
scene.background = new THREE.Color(c.bg);
scene.fog = new THREE.FogExp2(c.bg, c.fogDensity);
renderer.setClearColor(c.bg, 1.0);
}
Called once after renderer is constructed, then again from the toggle handler.
scene.fog density drops in adam-mode because exponential fog on a light background reads as "haze" much more strongly than on dark — 0.06 → 0.025 keeps the falloff visible without losing the figure into the background.
Add to the #helpers panel (top of its labels list):
<label class="theme-toggle">
<input type="checkbox" id="adam-mode-toggle">
<span>adam-mode (light)</span>
<span class="swatch" style="background: var(--amber)"></span>
</label>
Handler:
const THEME_KEY = 'ruview.theme';
const root = document.documentElement;
const toggle = document.getElementById('adam-mode-toggle');
function applyTheme(theme) {
if (theme === 'adam') {
root.setAttribute('data-theme', 'adam');
toggle.checked = true;
} else {
root.removeAttribute('data-theme');
toggle.checked = false;
}
applySceneTheme(theme);
try { localStorage.setItem(THEME_KEY, theme); } catch (_) {}
}
const initialTheme = (() => {
try { return localStorage.getItem(THEME_KEY) || 'dark'; }
catch (_) { return 'dark'; }
})();
applyTheme(initialTheme);
toggle.addEventListener('change', e => {
applyTheme(e.target.checked ? 'adam' : 'dark');
});
The user picked the name. It is a project-specific brand — distinct from the generic "light mode" terminology that other modes (--theme=high-contrast, --theme=print) may eventually need. Keeping a codename makes the toggle searchable in the codebase, the localStorage key portable across the demo set, and avoids ambiguity if dark itself is later renamed.
The string "adam" is the only literal value the data-theme attribute and the localStorage key ever take. "dark" is the implicit default (no attribute, no stored value).
| Alternative | Rejected because |
|---|---|
Use prefers-color-scheme: light only, no toggle | Operators frequently want the opposite of their OS preference for screen-recording or daylight desk use. Auto-only frustrates the actual use case. |
Ship two separate HTML files (05-…-dark.html, 05-…-light.html) | Doubles maintenance for every future demo edit. No path to per-session toggle. |
| Build a full multi-theme system with a runtime registry | Premature. Two themes don't need a registry; the data-theme="adam" attribute is the registry. |
| Use Tailwind / DaisyUI / a CSS framework | Demos are intentionally stand-alone single-file HTML for portability. No build step exists; adding one for theming is wrong shape. |
Adopt the cognitum-v0 / HOMECORE design tokens (--hc-* from examples/frontend/) | That design system is dark-only by intent (ADR-131). adam-mode is the light counterpart needed in demo contexts, not HA dashboard contexts. |
| Make adam-mode the default | Breaks the dark-aesthetic recording context this demo was originally built for. Default stays dark; toggle stays opt-in. |
localStorage — set once, sticks.<style> block addition, a single button, a ~25-line JS handler, and a swap of two scene-construction lines.:root blocks. Mitigated by keeping the existing custom-property names — adam-mode's values are the only edits.localStorage slot consumed per origin (ruview.theme).#b8741a) is visibly different from the dark-mode amber (#ffb840) — they share the same CSS variable name but a screenshot from each mode is not pixel-comparable. This is the correct call for accessibility (the bright amber is unreadable on cream) but does mean side-by-side comparisons need both screenshots labelled.| Risk | Likelihood | Mitigation |
|---|---|---|
Future demo edits update one :root block and forget the other | Medium | A lint script in scripts/ could grep both blocks for matching key sets; documented as P2 follow-up. |
localStorage blocked by privacy settings | Low | All accesses are wrapped in try/catch; falls back to dark. |
| Three.js fog density of 0.025 still washes out the model on adam-mode | Low | Empirically tuned during implementation; if it does, drop to 0.015 or remove fog entirely in adam-mode. |
| User on a high-DPI display sees scanlines as visible paper texture even at 15 % opacity | Low | If reported, drop to 8 % or hide scanlines entirely in adam-mode. |
Tiny scope — single file. No swarm needed.
:root[data-theme="adam"] CSS block and the two overlay overrides.themeSceneColors() and applySceneTheme().<label> markup and handler script.Acceptance criteria:
#helpers panel.view-source confirms the new CSS block, the toggle markup, and the handler are present.localStorage shows ruview.theme after a toggle.console.log(scene.background.getHexString())) confirms scene colour swap.<style> block; the same data-theme="adam" selector and the same JS handler can be copied.prefers-color-scheme: light on first load if localStorage has no stored choice. Trivial three-line addition.:root blocks declare the same custom-property names.