aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-22 15:15:02 -0500
committerCraig Jennings <c@cjennings.net>2026-07-22 15:15:02 -0500
commitf1c661f50d6af2a115fc1430ae0478f2acef2dec (patch)
treeb894ea2e14327794bb75671b962f37185cff22ea /docs/prototypes
parentf594068bed75302aba50793ccf5e6ce9cf53c7f8 (diff)
downloadarchsetup-f1c661f50d6af2a115fc1430ae0478f2acef2dec.tar.gz
archsetup-f1c661f50d6af2a115fc1430ae0478f2acef2dec.zip
docs(settings-panel): seal the prototype arc and open the build
Prototypes P12-P37 land, along with the night-watch and levels-compare pages plus the shared-engine updates and the kit-candidate file. The spec flips from DRAFT through READY to DOING with rewritten implementation phases and seven recorded build findings. The build tasks are filed in todo.org, and phase 1 is closed against the dotfiles commits.
Diffstat (limited to 'docs/prototypes')
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-levels-compare.html107
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-1.html2
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-10.html194
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-11.html205
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-12.html283
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-13.html304
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-14.html308
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-15.html299
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-16.html312
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-17.html311
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-18.html321
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-19.html336
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-2.html3
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-20.html360
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-21.html287
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-22.html291
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-23.html305
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-24.html304
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-25.html292
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-26.html295
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-27.html313
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-28.html296
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-29.html297
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-3.html2
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-30.html310
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-31.html319
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-32.html548
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-33.html613
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-34.html720
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html717
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-36.html901
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-37.html915
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-4.html1
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-5.html2
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-6.html343
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-7.html322
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-8.html360
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-9.html211
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-scenes-compare.html177
-rw-r--r--docs/prototypes/2026-07-21-night-watch-screensaver-prototype-1.html165
-rw-r--r--docs/prototypes/2026-07-22-watch-preview.pngbin0 -> 88870 bytes
-rw-r--r--docs/prototypes/desktop-settings-shared.css12
-rw-r--r--docs/prototypes/desktop-settings-shared.js16
-rw-r--r--docs/prototypes/dupre-kit-additions.js1256
44 files changed, 13928 insertions, 7 deletions
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-levels-compare.html b/docs/prototypes/2026-07-02-desktop-settings-panel-levels-compare.html
new file mode 100644
index 0000000..6f2427d
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-levels-compare.html
@@ -0,0 +1,107 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — levels casting: detent fader + single drum</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .page-cap { width: 100%; text-align: center; color: var(--dim); font-size: 12px; letter-spacing: 0.04em; max-width: 560px; }
+ .page-cap b { color: var(--gold); }
+ .row { display: flex; justify-content: center; align-items: center; gap: 14px; flex-wrap: wrap; }
+ .readout { text-align: center; color: var(--steel); font-size: 11px; letter-spacing: 0.1em; min-height: 15px; }
+ .readout b { color: var(--gold-hi); font-weight: 400; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="page-cap">
+ <b>Levels casting.</b> Brightness: the new multi-detent slide attenuator —
+ drag slowly across 25 / 50 / 75 and feel the thumb park until you push
+ through (the tick lights while parked). Keyboard: the generalized drum
+ roller running a single drum, 0-10.
+ </div>
+
+ <div class="ds-panel" style="width:320px">
+ <div class="ds-face"><span class="glyph">⚙</span><span class="title">Levels</span></div>
+
+ <div class="ds-engrave">Brightness — detent fader (new)</div>
+ <div class="row" id="fader-well"></div>
+ <div class="readout" id="ro-b"></div>
+
+ <div class="ds-engrave">Keyboard — drum roller ×10 (upgraded)</div>
+ <div class="row" id="drum-well"></div>
+ <div class="readout" id="ro-k"></div>
+
+ <div class="ds-toast" id="toast"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+DS.bindToast(document.getElementById("toast"));
+let booting = true;
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function render() {
+ booting = true;
+
+ const fw = document.getElementById("fader-well");
+ fw.innerHTML = "";
+ const roB = document.getElementById("ro-b");
+ const commitB = debounce((v) => {
+ const pct = Math.max(DS.SLIDERS.brightness.floor, v);
+ if (pct !== DS.state.brightness) DS.setSlider("brightness", pct, fw);
+ }, 350);
+ DUPRE.detentFader(fw, {
+ value: DS.state.brightness,
+ detents: [25, 50, 75],
+ onChange: (v, label) => {
+ roB.innerHTML = "brightness: <b>" + v + "%</b>" + (label.includes("detent") ? " · <b>parked</b>" : "");
+ if (!booting) commitB(v);
+ },
+ });
+
+ const dw = document.getElementById("drum-well");
+ dw.innerHTML = "";
+ const roK = document.getElementById("ro-k");
+ const commitK = debounce((vals) => {
+ const pct = Math.round(vals[0]) * 10;
+ if (pct !== DS.state.kbd) DS.setSlider("kbd", pct, dw);
+ }, 350);
+ DUPRE.drumRoller(dw, {
+ title: "KBD BACKLIGHT ×10",
+ channels: [{ name: "KBD", v: Math.round(DS.state.kbd / 10) }],
+ min: 0,
+ max: 10,
+ onChange: (vals) => {
+ roK.innerHTML = "keyboard: <b>" + Math.round(vals[0]) * 10 + "%</b>";
+ if (!booting) commitK(vals);
+ },
+ });
+
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-1.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-1.html
index e57d13e..1671eda 100644
--- a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-1.html
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-1.html
@@ -52,7 +52,7 @@ function render() {
// Toggle tiles
col.appendChild(DS.engrave("Toggles"));
const grid = DS.h("div", "ds-grid2");
- ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd"].forEach((k) =>
+ ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd", "weathergeo"].forEach((k) =>
grid.appendChild(DS.toggleTile(k)));
if (DS.state.laptop) grid.appendChild(DS.toggleTile("airplane"));
col.appendChild(grid);
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-10.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-10.html
new file mode 100644
index 0000000..ea26651
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-10.html
@@ -0,0 +1,194 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 10 (spacecraft DSKY, Dupre Kit)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .row { display: flex; justify-content: center; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
+ .dupre-rotsel, .dupre-keylock { display: inline-block; }
+ .cellcap { color: var(--steel); font-size: 8.5px; letter-spacing: 0.1em; text-align: center; margin-top: 3px; text-transform: uppercase; }
+ .swgrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
+ .swcell { display: flex; flex-direction: column; align-items: center; gap: 2px; }
+ .caution-well { pointer-events: none; }
+ .verbcard { color: var(--dim); font-size: 9px; line-height: 1.5; text-align: center; }
+ .verbcard b { color: var(--gold); font-weight: 400; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Direction 10 — Spacecraft DSKY (Dupre Kit).</b> The THUMB SLIDE
+ attenuator (drag the tabs) sets both levels. Toggles are slide switches
+ under a live caution board (amber cell = engaged). Scenes are commanded on
+ the DSKY: key VERB, two digits, ENTR. Power is the keyed mode switch.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const TOGGLES = [
+ ["autodim", "DIM"], ["caffeine", "CAFF"], ["touchpad", "TPAD"], ["mouse", "MOUSE"],
+ ["nightlight", "NIGHT"], ["dnd", "DND"], ["weathergeo", "WXGEO"], ["airplane", "AIR"],
+];
+let booting = true;
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: off", true);
+ render();
+}
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+ const rows = TOGGLES.filter(([k]) => k !== "airplane" || DS.state.laptop);
+
+ // Caution board: display-only mirror of the toggle states (amber = on).
+ col.appendChild(DS.engrave("Caution board"));
+ const annWell = h("div", "caution-well");
+ DUPRE.annunciator(annWell, {
+ cells: rows.map(([k, label]) => [label, DS.state[k] ? 1 : 0]),
+ });
+ const bar = annWell.querySelector(".dupre-annbar");
+ if (bar) bar.style.display = "none";
+ col.appendChild(annWell);
+
+ // Toggle switches: one slide switch per system.
+ col.appendChild(DS.engrave("Systems"));
+ const grid = h("div", "swgrid");
+ rows.forEach(([k, label]) => {
+ const cell = h("div", "swcell");
+ const host = h("div");
+ DUPRE.slideToggle(host, {
+ on: DS.state[k],
+ preset: k === "airplane" ? "armed" : "panel",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, cell); },
+ });
+ cell.appendChild(host);
+ cell.appendChild(h("div", "cellcap", label));
+ grid.appendChild(cell);
+ });
+ col.appendChild(grid);
+
+ // Levels: the thumb-slide attenuator (Craig favorite #2).
+ col.appendChild(DS.engrave("Levels"));
+ const lvRow = h("div", "row");
+ const tsHost = h("div");
+ const commit = debounce((vals) => {
+ if (vals[0] !== DS.state.brightness) DS.setSlider("brightness", vals[0], tsHost);
+ if (vals[1] !== DS.state.kbd) DS.setSlider("kbd", vals[1], tsHost);
+ }, 350);
+ DUPRE.thumbSlide(tsHost, {
+ channels: [
+ { name: "BRIGHT", v: DS.state.brightness },
+ { name: "KBD", v: DS.state.kbd },
+ ],
+ onChange: (vals) => { if (!booting) commit(vals.slice()); },
+ });
+ lvRow.appendChild(tsHost);
+ col.appendChild(lvRow);
+
+ // Scenes: commanded through the DSKY verb grammar.
+ col.appendChild(DS.engrave("Command — scenes"));
+ const dHost = h("div");
+ DUPRE.dsky(dHost, {
+ onChange: (vals, label) => {
+ if (booting) return;
+ const m = /^V(\d\d) ENTR$/.exec(label);
+ if (!m) return;
+ const code = +m[1];
+ if (code === 0) sceneOff();
+ else if (code >= 1 && code <= 4) DS.applyScene(SCENES[code - 1], dHost);
+ else DS.toast("V" + m[1] + ": no such program", false);
+ },
+ });
+ col.appendChild(dHost);
+ const card = h("div", "verbcard");
+ card.innerHTML =
+ "<b>V01</b> FOCUS · <b>V02</b> PRESENTATION · <b>V03</b> BATTERY · <b>V04</b> NIGHT<br>" +
+ "<b>V00</b> scene off · <b>V35</b> lamp test";
+ col.appendChild(card);
+
+ // Power: keyed mode switch.
+ col.appendChild(DS.engrave("Power mode"));
+ const pRow = h("div", "row");
+ const kHost = h("div");
+ DUPRE.keySwitch(kHost, {
+ items: ["PERF", "BAL", "SAVER"],
+ index: DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile),
+ onChange: (i) => { if (!booting) DS.setProfile(DS.PROFILES[i].id, kHost); },
+ });
+ pRow.appendChild(kHost);
+ col.appendChild(pRow);
+
+ // Actions: arm-to-fire.
+ col.appendChild(DS.engrave("Actions"));
+ const aRow = h("div", "row");
+ [["LOCK", () => DS.momentary("Lock")],
+ ["SUSPEND", () => DS.momentary("Suspend")],
+ ["WALLPAPER", openWallpaper]].forEach(([label, fn]) => {
+ const host = h("div");
+ DUPRE.armButton(host, {
+ label,
+ armLabel: label + "?",
+ onChange: (st) => { if (!booting && st === "fired") fn(); },
+ });
+ aRow.appendChild(host);
+ });
+ col.appendChild(aRow);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-11.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-11.html
new file mode 100644
index 0000000..d58fbfc
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-11.html
@@ -0,0 +1,205 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 11 (mixing desk, Dupre Kit)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .row { display: flex; justify-content: center; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
+ .dupre-rotsel, .dupre-keylock { display: inline-block; }
+ .cellcap { color: var(--steel); font-size: 8.5px; letter-spacing: 0.12em; text-align: center; margin-top: 2px; text-transform: uppercase; }
+ .fader-cell { display: flex; flex-direction: column; align-items: center; }
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
+ .vu-well { display: flex; justify-content: center; padding: 2px 0 6px; }
+ .stomp-cell { display: flex; flex-direction: column; align-items: center; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Direction 11 — Mixing desk (Dupre Kit).</b> Channel strips: slot faders
+ ride the two levels in dB (the VU bridge follows), scenes punch in on the
+ radio preset bank, toggles are lit chips with DND on a stomp pedal, power
+ is a rotary selector. Lock/suspend/wallpaper are arm-to-fire.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const BANK = ["OFF", "FOCUS", "PRESNT", "BATT", "NIGHT"];
+const CHIPS = [
+ ["autodim", "DIM"], ["caffeine", "CAFF"], ["touchpad", "TPAD"], ["mouse", "MOUSE"],
+ ["nightlight", "NIGHT"], ["weathergeo", "WXGEO"], ["airplane", "AIR"],
+];
+const dbOf = (pct) => -24 + (pct / 100) * 36;
+const pctOf = (db) => Math.round(((db + 24) / 36) * 100);
+let booting = true;
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: off", true);
+ render();
+}
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // VU bridge + channel faders.
+ col.appendChild(DS.engrave("Channel strips — levels"));
+ const vuWell = h("div", "vu-well");
+ const vu = DUPRE.vuPair(vuWell, {});
+ vu.set(DS.state.brightness / 100, DS.state.kbd / 100);
+ col.appendChild(vuWell);
+
+ const fRow = h("div", "row");
+ [["brightness", "BRIGHT", 0], ["kbd", "KBD", 1]].forEach(([key, label, ch]) => {
+ const cell = h("div", "fader-cell");
+ const host = h("div");
+ const commit = debounce((db) => {
+ const pct = Math.max(DS.SLIDERS[key].floor, pctOf(db));
+ if (pct !== DS.state[key]) DS.setSlider(key, pct, cell);
+ }, 350);
+ DUPRE.slotFader(host, {
+ value: dbOf(DS.state[key]),
+ onChange: (db) => {
+ if (booting) return;
+ const l = ch === 0 ? Math.max(0, Math.min(1, pctOf(db) / 100)) : DS.state.brightness / 100;
+ const r = ch === 1 ? Math.max(0, Math.min(1, pctOf(db) / 100)) : DS.state.kbd / 100;
+ vu.set(l, r);
+ commit(db);
+ },
+ });
+ cell.appendChild(host);
+ cell.appendChild(h("div", "cellcap", label));
+ fRow.appendChild(cell);
+ });
+ col.appendChild(fRow);
+
+ // Scenes: punch a preset.
+ col.appendChild(DS.engrave("Presets — scenes"));
+ const pbHost = h("div");
+ DUPRE.presetBank(pbHost, {
+ items: BANK,
+ active: DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0,
+ onChange: (i) => {
+ if (booting) return;
+ if (i === 0) sceneOff();
+ else DS.applyScene(SCENES[i - 1], pbHost);
+ },
+ });
+ col.appendChild(pbHost);
+
+ // Toggles: lit chips; DND gets the stomp pedal.
+ col.appendChild(DS.engrave("Mutes — toggles"));
+ const chips = h("div", "chips");
+ CHIPS.filter(([k]) => k !== "airplane" || DS.state.laptop).forEach(([k, label]) => {
+ const host = h("span");
+ DUPRE.chipToggle(host, {
+ label,
+ on: DS.state[k],
+ accent: k === "airplane" ? "red" : "amber",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ chips.appendChild(host);
+ });
+ col.appendChild(chips);
+
+ const sRow = h("div", "row");
+ const sCell = h("div", "stomp-cell");
+ const sHost = h("div");
+ DUPRE.stompSwitch(sHost, {
+ on: DS.state.dnd,
+ onChange: (on) => { if (!booting && on !== DS.state.dnd) DS.setToggle("dnd", sCell); },
+ });
+ sCell.appendChild(sHost);
+ sCell.appendChild(h("div", "cellcap", "DND — stomp to silence"));
+ sRow.appendChild(sCell);
+
+ // Power beside the pedal: rotary selector.
+ const pCell = h("div", "stomp-cell");
+ const rHost = h("div");
+ DUPRE.rotarySelector(rHost, {
+ values: ["PERF", "BAL", "SAVER"],
+ index: DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile),
+ onChange: (v) => {
+ if (booting) return;
+ const i = ["PERF", "BAL", "SAVER"].indexOf(v);
+ if (i >= 0) DS.setProfile(DS.PROFILES[i].id, pCell);
+ },
+ });
+ pCell.appendChild(rHost);
+ pCell.appendChild(h("div", "cellcap", "POWER"));
+ sRow.appendChild(pCell);
+ col.appendChild(sRow);
+
+ // Actions: arm-to-fire.
+ col.appendChild(DS.engrave("Actions"));
+ const aRow = h("div", "row");
+ [["LOCK", () => DS.momentary("Lock")],
+ ["SUSPEND", () => DS.momentary("Suspend")],
+ ["WALLPAPER", openWallpaper]].forEach(([label, fn]) => {
+ const host = h("div");
+ DUPRE.armButton(host, {
+ label,
+ armLabel: label + "?",
+ onChange: (st) => { if (!booting && st === "fired") fn(); },
+ });
+ aRow.appendChild(host);
+ });
+ col.appendChild(aRow);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-12.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-12.html
new file mode 100644
index 0000000..866fa80
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-12.html
@@ -0,0 +1,283 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 12 (the casting composite)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ /* receiver plate: brushed texture behind each section (direction 6 chrome) */
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .row { display: flex; justify-content: center; align-items: center; gap: 12px; flex-wrap: wrap; }
+ .row-split { display: flex; justify-content: space-around; align-items: flex-start; }
+ .cellcap { color: var(--steel); font-size: 8.5px; letter-spacing: 0.14em; text-align: center; margin-top: 2px; text-transform: uppercase; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .dupre-rotsel, .dupre-keylock { display: inline-block; }
+ /* kit spans need a blockifying host outside the gallery */
+ .khost { display: flex; justify-content: center; }
+
+ /* tuner well + presets (direction 6, over the kit slideRule) */
+ .tuner-well { display: flex; justify-content: center; padding: 4px 0 6px; }
+ .presets { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 6px; }
+ .preset {
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-bottom-width: 3px; border-radius: 6px;
+ padding: 5px 2px; text-align: center; cursor: pointer; color: var(--silver);
+ }
+ .preset:hover { border-color: var(--gold); color: var(--gold); }
+ .preset .num { font-size: 8px; color: var(--dim); display: block; }
+ .preset .nm { font-size: 9.5px; letter-spacing: 0.04em; }
+ .preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px; transform: translateY(2px);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12); color: var(--cream);
+ }
+ .preset.cur .num { color: var(--gold); }
+
+ /* fader line: label left, fader flexing, readout right */
+ .fline { display: flex; align-items: center; gap: 8px; }
+ .fline .fname { color: var(--silver); font-size: 11px; width: 58px; }
+ .fline .fpct { color: var(--cream); font-size: 12px; width: 38px; text-align: right; font-variant-numeric: tabular-nums; }
+ .fline .fhost { flex: 1; display: flex; }
+ .fline .fhost .dupre-dfader { width: 100%; }
+
+ /* rocker bank */
+ .rockers { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px 6px; justify-items: center; }
+
+ /* wallpaper row (console key) */
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 12 — the casting composite.</b> Every control as cast
+ 2026-07-21: tuner-dial scenes + presets, detent fader, single drum,
+ chicken-head power, rocker bank, guarded airplane, wind-up caffeine
+ timer, wallpaper sub-view. Warm-gold receiver chrome. Lock/suspend
+ live on wlogout, not here.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const STOPS = ["OFF", "FOCUS", "PRES", "BATT", "NIGHT"];
+const ROCKERS = [
+ ["autodim", "Auto-dim"], ["touchpad", "Touchpad"], ["mouse", "Mouse"],
+ ["nightlight", "Night light"], ["dnd", "DND"], ["weathergeo", "Weather geo"],
+];
+let booting = true;
+let caffMinutes = 0; // survives re-renders
+
+const sceneIdx = () => (DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0);
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: off", true);
+ render();
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- Scenes: the hero — tuner dial + preset keys ------------------------
+ const scenesPlate = h("div", "plate");
+ const tw = h("div", "tuner-well");
+ DUPRE.slideRule(tw, {
+ values: STOPS,
+ value: STOPS[sceneIdx()],
+ skin: "warm",
+ fmt: (v) => v,
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "OFF") sceneOff();
+ else DS.applyScene(SCENES[STOPS.indexOf(v) - 1], tw);
+ },
+ });
+ // string stops: the builder's lit-mark compare is numeric-only; patch it
+ tw.querySelectorAll(".dupre-mk").forEach((m) =>
+ m.classList.toggle("dupre-on", m.textContent === STOPS[sceneIdx()]));
+ scenesPlate.appendChild(tw);
+ const pr = h("div", "presets");
+ SCENES.forEach((n, i) => {
+ const k = h("div", "preset" + (DS.state.scene === n ? " cur" : ""));
+ k.innerHTML = `<span class="num">PRESET ${i + 1}</span><span class="nm">${n}</span>`;
+ k.onclick = () => DS.applyScene(n, k);
+ pr.appendChild(k);
+ });
+ scenesPlate.appendChild(pr);
+ col.appendChild(scenesPlate);
+
+ // ---- Brightness: full-width detent fader --------------------------------
+ col.appendChild(DS.engrave("Brightness"));
+ const fl = h("div", "fline");
+ fl.appendChild(h("span", "fname", "Screen"));
+ const fhost = h("span", "fhost");
+ const fpct = h("span", "fpct", DS.state.brightness + "%");
+ const commitB = debounce((v) => {
+ const pct = Math.max(DS.SLIDERS.brightness.floor, v);
+ if (pct !== DS.state.brightness) DS.setSlider("brightness", pct, fl);
+ }, 350);
+ DUPRE.detentFader(fhost, {
+ value: DS.state.brightness,
+ detents: [25, 50, 75],
+ onChange: (v) => {
+ fpct.textContent = v + "%";
+ if (!booting) commitB(v);
+ },
+ });
+ fl.appendChild(fhost);
+ fl.appendChild(fpct);
+ col.appendChild(fl);
+
+ // ---- Keyboard drum + power chicken-head ---------------------------------
+ col.appendChild(DS.engrave("Keyboard · Power"));
+ const duo = h("div", "row-split");
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitK = debounce((vals) => {
+ const pct = Math.round(vals[0]) * 10;
+ if (pct !== DS.state.kbd) DS.setSlider("kbd", pct, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "BACKLIGHT ×10",
+ channels: [{ name: "KBD", v: Math.round(DS.state.kbd / 10) }],
+ min: 0, max: 10,
+ onChange: (vals) => { if (!booting) commitK(vals); },
+ });
+ dcell.appendChild(dhost);
+ duo.appendChild(dcell);
+
+ const pcell = h("div", "vcell");
+ const phost = h("div", "khost");
+ const pIdx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ DUPRE.chickenHead(phost, {
+ items: [["PERF", -60], ["BAL", 0], ["SAVER", 60]],
+ index: pIdx,
+ onChange: (i) => { if (!booting && i !== pIdx) DS.setProfile(DS.PROFILES[i].id, pcell); },
+ });
+ pcell.appendChild(phost);
+ pcell.appendChild(h("div", "cellcap", "power"));
+ duo.appendChild(pcell);
+ col.appendChild(duo);
+
+ // ---- Caffeine: the wind-up timer ---------------------------------------
+ col.appendChild(DS.engrave("Caffeine — wind to stay awake"));
+ const trow = h("div", "row");
+ const thost = h("div", "khost");
+ DUPRE.timerDial(thost, {
+ minutes: caffMinutes,
+ onChange: (min) => {
+ caffMinutes = min;
+ const on = min > 0;
+ if (!booting && on !== DS.state.caffeine) DS.setToggle("caffeine", thost);
+ },
+ });
+ trow.appendChild(thost);
+ col.appendChild(trow);
+
+ // ---- Toggle bank: rockers ----------------------------------------------
+ col.appendChild(DS.engrave("Switches"));
+ const bank = h("div", "rockers");
+ ROCKERS.forEach(([k, name]) => {
+ const cell = h("div", "vcell");
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, cell); },
+ });
+ cell.appendChild(host);
+ cell.appendChild(h("div", "cellcap", name));
+ bank.appendChild(cell);
+ });
+ col.appendChild(bank);
+
+ // ---- Airplane: the guarded lever (laptop-only) --------------------------
+ if (DS.state.laptop) {
+ const grow = h("div", "row");
+ const gcell = h("div", "vcell");
+ const ghost = h("div", "khost");
+ DUPRE.guardedToggle(ghost, {
+ on: DS.state.airplane,
+ onLabel: "AIRPLANE",
+ offLabel: "RADIO OK",
+ onChange: (on) => { if (!booting && on !== DS.state.airplane) DS.setToggle("airplane", gcell); },
+ });
+ gcell.appendChild(ghost);
+ grow.appendChild(gcell);
+ col.appendChild(grow);
+ }
+
+ // ---- Wallpaper: sub-view row -------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-13.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-13.html
new file mode 100644
index 0000000..401644f
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-13.html
@@ -0,0 +1,304 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 13 (P12 + Craig's iteration)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .row { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; }
+ .cellcap { color: var(--steel); font-size: 8.5px; letter-spacing: 0.14em; text-align: center; margin-top: 2px; text-transform: uppercase; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+
+ /* top block: presets over the tuner, compact chicken-head upper right */
+ .topgrid { display: flex; gap: 8px; align-items: stretch; }
+ .topleft { flex: 1; display: flex; flex-direction: column; gap: 8px; }
+ .topright { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; }
+ .tuner-well { display: flex; justify-content: center; }
+
+ .presets { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 5px; }
+ .preset {
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-bottom-width: 3px; border-radius: 6px;
+ padding: 4px 1px; text-align: center; cursor: pointer; color: var(--silver);
+ overflow: hidden;
+ }
+ .preset:hover { border-color: var(--gold); color: var(--gold); }
+ .preset .num { font-size: 7.5px; color: var(--dim); display: block; letter-spacing: 0; }
+ .preset .nm { font-size: 8.5px; letter-spacing: 0; display: block; }
+ .preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px; transform: translateY(2px);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12); color: var(--cream);
+ }
+ .preset.cur .num { color: var(--gold); }
+
+ /* levels + caffeine block: drums left, big timer right */
+ .midgrid { display: flex; gap: 8px; align-items: flex-start; justify-content: center; }
+
+ /* switch wall: six cells, airplane's guarded lever among them */
+ .wall { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px 6px; justify-items: center; align-items: end; }
+
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 13 — P12 + Craig's iteration.</b> Presets moved above the
+ dial (Preset 2 contains its text); screen + keyboard are drum partners
+ left of a larger caffeine timer with an always-on rocker; compact
+ chicken-head upper right; switch wall of six with airplane's guarded
+ lever in the mix; weather-geo dropped from the panel; wallpaper where
+ it was.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const STOPS = ["OFF", "FOCUS", "PRES", "BATT", "NIGHT"];
+const ROCKERS = [
+ ["autodim", "Auto-dim"], ["touchpad", "Touchpad"], ["mouse", "Mouse"],
+ ["nightlight", "Night light"], ["dnd", "DND"],
+];
+let booting = true;
+let caffMinutes = 0; // survives re-renders
+let caffHold = false; // the always-on rocker
+let timerHost = null; // the timerDial is PERSISTENT: it owns a live
+ // countdown interval, so re-renders reparent it
+ // instead of rebuilding (rebuilds leak ghost
+ // intervals and kill an in-progress wind drag)
+
+const sceneIdx = () => (DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0);
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: off", true);
+ render();
+}
+
+// caffeine is on when held-on OR wound; keep DS in sync with that truth.
+// Debounced: the sync triggers a panel re-render, which must not happen
+// mid-wind (it would yank the dial out from under the drag).
+function syncCaffeine(el) {
+ const on = caffHold || caffMinutes > 0;
+ if (!booting && on !== DS.state.caffeine) DS.setToggle("caffeine", el);
+}
+const syncCaffeineSoon = debounce(() => syncCaffeine(timerHost), 450);
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- top: presets over the tuner; compact chicken-head upper right -----
+ const top = h("div", "plate");
+ const pr = h("div", "presets");
+ SCENES.forEach((n, i) => {
+ const k = h("div", "preset" + (DS.state.scene === n ? " cur" : ""));
+ k.innerHTML = `<span class="num">PRESET ${i + 1}</span><span class="nm">${n}</span>`;
+ k.onclick = () => DS.applyScene(n, k);
+ pr.appendChild(k);
+ });
+ top.appendChild(pr);
+ const grid = h("div", "topgrid");
+ grid.style.marginTop = "8px";
+ const left = h("div", "topleft");
+
+ const tw = h("div", "tuner-well");
+ DUPRE.slideRule(tw, {
+ values: STOPS,
+ value: STOPS[sceneIdx()],
+ skin: "warm",
+ fmt: (v) => v,
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "OFF") sceneOff();
+ else DS.applyScene(SCENES[STOPS.indexOf(v) - 1], tw);
+ },
+ });
+ tw.querySelectorAll(".dupre-mk").forEach((m) =>
+ m.classList.toggle("dupre-on", m.textContent === STOPS[sceneIdx()]));
+ // the kit tuner's fixed width clips the NIGHT stop's label; give it room
+ tw.querySelector(".dupre-tuner").style.width = "198px";
+ left.appendChild(tw);
+ grid.appendChild(left);
+
+ const right = h("div", "topright");
+ const phost = h("div", "khost");
+ const pIdx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ DUPRE.chickenHead(phost, {
+ items: [["PERF", -60], ["BAL", 0], ["SAVER", 60]],
+ index: pIdx,
+ onChange: (i) => { if (!booting && i !== pIdx) DS.setProfile(DS.PROFILES[i].id, right); },
+ });
+ scaleSvg(phost, 78);
+ right.appendChild(phost);
+ right.appendChild(h("div", "cellcap", "power"));
+ grid.appendChild(right);
+ top.appendChild(grid);
+ col.appendChild(top);
+
+ // ---- middle: drum partners left, larger caffeine timer right ------------
+ col.appendChild(DS.engrave("Levels · Caffeine"));
+ const mid = h("div", "midgrid");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 189, // drum top flush with the timer dial, bottom with the HOLD rocker
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ mid.appendChild(dcell);
+
+ const tcell = h("div", "vcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: caffMinutes,
+ onChange: (min) => {
+ caffMinutes = min;
+ syncCaffeineSoon();
+ },
+ });
+ scaleSvg(timerHost, 178);
+ }
+ tcell.appendChild(timerHost);
+ const holdHost = h("div", "khost");
+ DUPRE.rocker(holdHost, {
+ on: caffHold,
+ onLabel: "HOLD",
+ offLabel: "TIMED",
+ onChange: (on) => {
+ if (booting) return;
+ caffHold = on;
+ syncCaffeine(holdHost);
+ },
+ });
+ tcell.appendChild(holdHost);
+ tcell.appendChild(h("div", "cellcap", "always on"));
+ mid.appendChild(tcell);
+ col.appendChild(mid);
+
+ // ---- switch wall: five rockers + the guarded airplane lever -------------
+ col.appendChild(DS.engrave("Switches"));
+ const wall = h("div", "wall");
+ ROCKERS.forEach(([k, name]) => {
+ const cell = h("div", "vcell");
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, cell); },
+ });
+ cell.appendChild(host);
+ cell.appendChild(h("div", "cellcap", name));
+ wall.appendChild(cell);
+ });
+ if (DS.state.laptop) {
+ const cell = h("div", "vcell");
+ const host = h("div", "khost");
+ DUPRE.guardedToggle(host, {
+ on: DS.state.airplane,
+ onLabel: "AIRPLANE",
+ offLabel: "RADIO OK",
+ onChange: (on) => { if (!booting && on !== DS.state.airplane) DS.setToggle("airplane", cell); },
+ });
+ scaleSvg(host, 72);
+ cell.appendChild(host);
+ wall.appendChild(cell);
+ }
+ col.appendChild(wall);
+
+ // ---- wallpaper: where it was -------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-14.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-14.html
new file mode 100644
index 0000000..af02eab
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-14.html
@@ -0,0 +1,308 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 14 (timer bat, centered dial)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .row { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; }
+ .cellcap { color: var(--steel); font-size: 8.5px; letter-spacing: 0.14em; text-align: center; margin-top: 2px; text-transform: uppercase; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .tcell { position: relative; }
+ .bat-oly { position: absolute; top: -8px; right: -10px; }
+ .bat-oly svg { width: 54px; height: 69px; }
+
+ /* top block: presets over the tuner, compact chicken-head upper right */
+ .topgrid { display: flex; gap: 8px; align-items: stretch; }
+ .topleft { flex: 1; display: flex; flex-direction: column; gap: 8px; }
+ .topright { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; }
+ .tuner-well { display: flex; justify-content: center; }
+
+ .presets { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 5px; }
+ .preset {
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-bottom-width: 3px; border-radius: 6px;
+ padding: 4px 1px; text-align: center; cursor: pointer; color: var(--silver);
+ overflow: hidden;
+ }
+ .preset:hover { border-color: var(--gold); color: var(--gold); }
+ .preset .num { font-size: 7.5px; color: var(--dim); display: block; letter-spacing: 0; }
+ .preset .nm { font-size: 8.5px; letter-spacing: 0; display: block; }
+ .preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px; transform: translateY(2px);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12); color: var(--cream);
+ }
+ .preset.cur .num { color: var(--gold); }
+
+ /* levels + caffeine block: drums left, big timer right */
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+
+ /* switch wall: six cells, airplane's guarded lever among them */
+ .wall { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px 6px; justify-items: center; align-items: end; }
+
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 14 — P13 + timer refinements.</b> The HOLD rocker is gone:
+ a bat toggle sits at the timer's upper right where the winding arrow
+ was — up TIMED, down HOLD (always on). The dial is vertically centered
+ against the drums. Everything else as P13.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const STOPS = ["OFF", "FOCUS", "PRES", "BATT", "NIGHT"];
+const ROCKERS = [
+ ["autodim", "Auto-dim"], ["touchpad", "Touchpad"], ["mouse", "Mouse"],
+ ["nightlight", "Night light"], ["dnd", "DND"],
+];
+let booting = true;
+let caffMinutes = 0; // survives re-renders
+let caffHold = false; // the always-on rocker
+let timerHost = null; // the timerDial is PERSISTENT: it owns a live
+ // countdown interval, so re-renders reparent it
+ // instead of rebuilding (rebuilds leak ghost
+ // intervals and kill an in-progress wind drag)
+
+const sceneIdx = () => (DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0);
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: off", true);
+ render();
+}
+
+// caffeine is on when held-on OR wound; keep DS in sync with that truth.
+// Debounced: the sync triggers a panel re-render, which must not happen
+// mid-wind (it would yank the dial out from under the drag).
+function syncCaffeine(el) {
+ const on = caffHold || caffMinutes > 0;
+ if (!booting && on !== DS.state.caffeine) DS.setToggle("caffeine", el);
+}
+const syncCaffeineSoon = debounce(() => syncCaffeine(timerHost), 450);
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- top: presets over the tuner; compact chicken-head upper right -----
+ const top = h("div", "plate");
+ const pr = h("div", "presets");
+ SCENES.forEach((n, i) => {
+ const k = h("div", "preset" + (DS.state.scene === n ? " cur" : ""));
+ k.innerHTML = `<span class="num">PRESET ${i + 1}</span><span class="nm">${n}</span>`;
+ k.onclick = () => DS.applyScene(n, k);
+ pr.appendChild(k);
+ });
+ top.appendChild(pr);
+ const grid = h("div", "topgrid");
+ grid.style.marginTop = "8px";
+ const left = h("div", "topleft");
+
+ const tw = h("div", "tuner-well");
+ DUPRE.slideRule(tw, {
+ values: STOPS,
+ value: STOPS[sceneIdx()],
+ skin: "warm",
+ fmt: (v) => v,
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "OFF") sceneOff();
+ else DS.applyScene(SCENES[STOPS.indexOf(v) - 1], tw);
+ },
+ });
+ tw.querySelectorAll(".dupre-mk").forEach((m) =>
+ m.classList.toggle("dupre-on", m.textContent === STOPS[sceneIdx()]));
+ // the kit tuner's fixed width clips the NIGHT stop's label; give it room
+ tw.querySelector(".dupre-tuner").style.width = "198px";
+ left.appendChild(tw);
+ grid.appendChild(left);
+
+ const right = h("div", "topright");
+ const phost = h("div", "khost");
+ const pIdx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ DUPRE.chickenHead(phost, {
+ items: [["PERF", -60], ["BAL", 0], ["SAVER", 60]],
+ index: pIdx,
+ onChange: (i) => { if (!booting && i !== pIdx) DS.setProfile(DS.PROFILES[i].id, right); },
+ });
+ scaleSvg(phost, 78);
+ right.appendChild(phost);
+ right.appendChild(h("div", "cellcap", "power"));
+ grid.appendChild(right);
+ top.appendChild(grid);
+ col.appendChild(top);
+
+ // ---- middle: drum partners left, larger caffeine timer right ------------
+ col.appendChild(DS.engrave("Levels · Caffeine"));
+ const mid = h("div", "midgrid");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 189, // tall drums; the timer centers against them
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ mid.appendChild(dcell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: caffMinutes,
+ onChange: (min) => {
+ caffMinutes = min;
+ syncCaffeineSoon();
+ },
+ });
+ scaleSvg(timerHost, 178);
+ // the bat toggle takes the winding arrow's corner
+ const svg = timerHost.querySelector("svg");
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+ }
+ tcell.appendChild(timerHost);
+ const batHost = h("div", "bat-oly");
+ DUPRE.batToggle(batHost, {
+ on: !caffHold, // up = TIMED, down = HOLD
+ onLabel: "TIMED",
+ offLabel: "HOLD",
+ onChange: (on) => {
+ if (booting) return;
+ caffHold = !on;
+ syncCaffeine(batHost);
+ },
+ });
+ tcell.appendChild(batHost);
+ mid.appendChild(tcell);
+ col.appendChild(mid);
+
+ // ---- switch wall: five rockers + the guarded airplane lever -------------
+ col.appendChild(DS.engrave("Switches"));
+ const wall = h("div", "wall");
+ ROCKERS.forEach(([k, name]) => {
+ const cell = h("div", "vcell");
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, cell); },
+ });
+ cell.appendChild(host);
+ cell.appendChild(h("div", "cellcap", name));
+ wall.appendChild(cell);
+ });
+ if (DS.state.laptop) {
+ const cell = h("div", "vcell");
+ const host = h("div", "khost");
+ DUPRE.guardedToggle(host, {
+ on: DS.state.airplane,
+ onLabel: "AIRPLANE",
+ offLabel: "RADIO OK",
+ onChange: (on) => { if (!booting && on !== DS.state.airplane) DS.setToggle("airplane", cell); },
+ });
+ scaleSvg(host, 72);
+ cell.appendChild(host);
+ wall.appendChild(cell);
+ }
+ col.appendChild(wall);
+
+ // ---- wallpaper: where it was -------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-15.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-15.html
new file mode 100644
index 0000000..7bdc336
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-15.html
@@ -0,0 +1,299 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 15 (power slide, full-width tuner)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .row { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; }
+ .cellcap { color: var(--steel); font-size: 8.5px; letter-spacing: 0.14em; text-align: center; margin-top: 2px; text-transform: uppercase; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .tcell { position: relative; }
+ .bat-oly { position: absolute; top: -8px; right: -10px; }
+ .bat-oly svg { width: 54px; height: 69px; }
+
+ /* top block: presets over the tuner, compact chicken-head upper right */
+ .topgrid { display: flex; gap: 8px; align-items: stretch; }
+ .topleft { flex: 1; display: flex; flex-direction: column; gap: 8px; }
+ .topright { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; }
+ .tuner-well { display: flex; justify-content: center; }
+
+ .presets { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 5px; }
+ .preset {
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-bottom-width: 3px; border-radius: 6px;
+ padding: 4px 1px; text-align: center; cursor: pointer; color: var(--silver);
+ overflow: hidden;
+ }
+ .preset:hover { border-color: var(--gold); color: var(--gold); }
+ .preset .num { font-size: 7.5px; color: var(--dim); display: block; letter-spacing: 0; }
+ .preset .nm { font-size: 8.5px; letter-spacing: 0; display: block; }
+ .preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px; transform: translateY(2px);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12); color: var(--cream);
+ }
+ .preset.cur .num { color: var(--gold); }
+
+ /* levels + caffeine block: drums left, big timer right */
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+
+ /* switch wall: six cells, airplane's guarded lever among them */
+ .wall { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px 6px; justify-items: center; align-items: end; }
+
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 15 — P14 + power slide.</b> The chicken-head is out: the
+ tuner dial is restored to full width, and beneath it a three-position
+ slide switch of equal length carries power — a status lamp per position,
+ labeled PERFORMANCE / BALANCE / POWER SAVE. Everything else as P14.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const STOPS = ["OFF", "FOCUS", "PRES", "BATT", "NIGHT"];
+const ROCKERS = [
+ ["autodim", "Auto-dim"], ["touchpad", "Touchpad"], ["mouse", "Mouse"],
+ ["nightlight", "Night light"], ["dnd", "DND"],
+];
+let booting = true;
+let caffMinutes = 0; // survives re-renders
+let caffHold = false; // the always-on rocker
+let timerHost = null; // the timerDial is PERSISTENT: it owns a live
+ // countdown interval, so re-renders reparent it
+ // instead of rebuilding (rebuilds leak ghost
+ // intervals and kill an in-progress wind drag)
+
+const sceneIdx = () => (DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0);
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: off", true);
+ render();
+}
+
+// caffeine is on when held-on OR wound; keep DS in sync with that truth.
+// Debounced: the sync triggers a panel re-render, which must not happen
+// mid-wind (it would yank the dial out from under the drag).
+function syncCaffeine(el) {
+ const on = caffHold || caffMinutes > 0;
+ if (!booting && on !== DS.state.caffeine) DS.setToggle("caffeine", el);
+}
+const syncCaffeineSoon = debounce(() => syncCaffeine(timerHost), 450);
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- top: presets over the tuner; compact chicken-head upper right -----
+ const top = h("div", "plate");
+ const pr = h("div", "presets");
+ SCENES.forEach((n, i) => {
+ const k = h("div", "preset" + (DS.state.scene === n ? " cur" : ""));
+ k.innerHTML = `<span class="num">PRESET ${i + 1}</span><span class="nm">${n}</span>`;
+ k.onclick = () => DS.applyScene(n, k);
+ pr.appendChild(k);
+ });
+ top.appendChild(pr);
+
+ const tw = h("div", "tuner-well");
+ tw.style.marginTop = "8px";
+ DUPRE.slideRule(tw, {
+ values: STOPS,
+ value: STOPS[sceneIdx()],
+ skin: "warm",
+ width: 306, // restored to the plate's full width
+ fmt: (v) => v,
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "OFF") sceneOff();
+ else DS.applyScene(SCENES[STOPS.indexOf(v) - 1], tw);
+ },
+ });
+ top.appendChild(tw);
+
+ const pw = h("div", "khost");
+ pw.style.marginTop = "8px";
+ const pIdx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ DUPRE.slideSelector(pw, {
+ items: ["PERFORMANCE", "BALANCE", "POWER SAVE"],
+ index: pIdx,
+ width: 306, // equal to the tuner's length
+ onChange: (i) => { if (!booting && i !== pIdx) DS.setProfile(DS.PROFILES[i].id, pw); },
+ });
+ top.appendChild(pw);
+ col.appendChild(top);
+
+ // ---- middle: drum partners left, larger caffeine timer right ------------
+ col.appendChild(DS.engrave("Levels · Caffeine"));
+ const mid = h("div", "midgrid");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 189, // tall drums; the timer centers against them
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ mid.appendChild(dcell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: caffMinutes,
+ onChange: (min) => {
+ caffMinutes = min;
+ syncCaffeineSoon();
+ },
+ });
+ scaleSvg(timerHost, 178);
+ // the bat toggle takes the winding arrow's corner
+ const svg = timerHost.querySelector("svg");
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+ }
+ tcell.appendChild(timerHost);
+ const batHost = h("div", "bat-oly");
+ DUPRE.batToggle(batHost, {
+ on: !caffHold, // up = TIMED, down = HOLD
+ onLabel: "TIMED",
+ offLabel: "HOLD",
+ onChange: (on) => {
+ if (booting) return;
+ caffHold = !on;
+ syncCaffeine(batHost);
+ },
+ });
+ tcell.appendChild(batHost);
+ mid.appendChild(tcell);
+ col.appendChild(mid);
+
+ // ---- switch wall: five rockers + the guarded airplane lever -------------
+ col.appendChild(DS.engrave("Switches"));
+ const wall = h("div", "wall");
+ ROCKERS.forEach(([k, name]) => {
+ const cell = h("div", "vcell");
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, cell); },
+ });
+ cell.appendChild(host);
+ cell.appendChild(h("div", "cellcap", name));
+ wall.appendChild(cell);
+ });
+ if (DS.state.laptop) {
+ const cell = h("div", "vcell");
+ const host = h("div", "khost");
+ DUPRE.guardedToggle(host, {
+ on: DS.state.airplane,
+ onLabel: "AIRPLANE",
+ offLabel: "RADIO OK",
+ onChange: (on) => { if (!booting && on !== DS.state.airplane) DS.setToggle("airplane", cell); },
+ });
+ scaleSvg(host, 72);
+ cell.appendChild(host);
+ wall.appendChild(cell);
+ }
+ col.appendChild(wall);
+
+ // ---- wallpaper: where it was -------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-16.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-16.html
new file mode 100644
index 0000000..a2d76e0
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-16.html
@@ -0,0 +1,312 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 16 (rebalanced: quiet chassis)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ /* one chassis grid: every section on an identical plate, identical gaps */
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .row { display: flex; justify-content: center; align-items: center; gap: 10px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .tuner-well { display: flex; justify-content: center; margin-top: 8px; }
+
+ .presets { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 5px; }
+ .preset {
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-bottom-width: 3px; border-radius: 6px;
+ padding: 4px 1px; text-align: center; cursor: pointer; color: var(--silver);
+ overflow: hidden;
+ }
+ .preset:hover { border-color: var(--gold); color: var(--gold); }
+ .preset .num { font-size: 7.5px; color: var(--dim); display: block; letter-spacing: 0; }
+ .preset .nm { font-size: 8.5px; letter-spacing: 0; display: block; }
+ .preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px; transform: translateY(2px);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12); color: var(--cream);
+ }
+ .preset.cur .num { color: var(--gold); }
+
+ /* levels + caffeine: slides left, tamed timer right, bat docked on stage */
+ .midgrid { display: flex; gap: 6px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .bat-oly { position: absolute; top: -2px; right: 0; }
+ .bat-oly svg { width: 52px; height: 67px; }
+
+ /* switch wall: name-on-switch rockers + the airplane lever, one baseline */
+ .wall { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 6px; justify-items: center; align-items: center; }
+
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 16 — rebalanced.</b> Same instruments, honest loudness:
+ three uniform plates on one grid; thumb-slide pair replaces the drums;
+ the timer is de-throned (steel knob, one engraving, amber index) with
+ the bat docked on its stage; rockers carry their function name on the
+ lit half (quiet dark when off); red survives only on the tuner needle
+ and the airplane disc.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const STOPS = ["OFF", "FOCUS", "PRES", "BATT", "NIGHT"];
+const ROCKERS = [
+ ["autodim", "DIM"], ["touchpad", "TPAD"], ["mouse", "MOUSE"],
+ ["nightlight", "NIGHT"], ["dnd", "DND"],
+];
+let booting = true;
+let caffMinutes = 0;
+let caffHold = false;
+let timerHost = null; // persistent: the timer owns a live countdown
+
+const sceneIdx = () => (DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0);
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: off", true);
+ render();
+}
+
+function syncCaffeine(el) {
+ const on = caffHold || caffMinutes > 0;
+ if (!booting && on !== DS.state.caffeine) DS.setToggle("caffeine", el);
+}
+const syncCaffeineSoon = debounce(() => syncCaffeine(timerHost), 450);
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+// de-throne the stock timer: steel stop knob, amber index/OFF, one engraving
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p16Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p16Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "WIND · PUSH STOPS";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: head unit — presets, tuner, power slide -------------------
+ const head = h("div", "plate");
+ head.appendChild(DS.engrave("Scenes · Power"));
+ const pr = h("div", "presets");
+ SCENES.forEach((n, i) => {
+ const k = h("div", "preset" + (DS.state.scene === n ? " cur" : ""));
+ k.innerHTML = `<span class="num">PRESET ${i + 1}</span><span class="nm">${n}</span>`;
+ k.onclick = () => DS.applyScene(n, k);
+ pr.appendChild(k);
+ });
+ head.appendChild(pr);
+
+ const tw = h("div", "tuner-well");
+ DUPRE.slideRule(tw, {
+ values: STOPS,
+ value: STOPS[sceneIdx()],
+ skin: "warm",
+ width: 306,
+ fmt: (v) => v,
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "OFF") sceneOff();
+ else DS.applyScene(SCENES[STOPS.indexOf(v) - 1], tw);
+ },
+ });
+ head.appendChild(tw);
+
+ const pw = h("div", "khost");
+ pw.style.marginTop = "8px";
+ const pIdx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ DUPRE.slideSelector(pw, {
+ items: ["PERFORMANCE", "BALANCE", "POWER SAVE"],
+ index: pIdx,
+ width: 306,
+ onChange: (i) => { if (!booting && i !== pIdx) DS.setProfile(DS.PROFILES[i].id, pw); },
+ });
+ head.appendChild(pw);
+ col.appendChild(head);
+
+ // ---- plate 2: levels + caffeine -----------------------------------------
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Caffeine"));
+ const mid = h("div", "midgrid");
+
+ const scell = h("div", "vcell");
+ const shost = h("div", "khost");
+ const commitLv = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]));
+ const k = Math.round(vals[1]);
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, scell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, scell);
+ }, 350);
+ DUPRE.thumbSlide(shost, {
+ channels: [
+ { name: "SCREEN", v: DS.state.brightness },
+ { name: "KBD", v: DS.state.kbd },
+ ],
+ onChange: (vals) => { if (!booting) commitLv(vals.slice()); },
+ });
+ scell.appendChild(shost);
+ mid.appendChild(scell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: caffMinutes,
+ onChange: (min) => {
+ caffMinutes = min;
+ syncCaffeineSoon();
+ },
+ });
+ scaleSvg(timerHost, 158);
+ tameTimer(timerHost);
+ // the bat docks where the winding arrow was
+ const svg = timerHost.querySelector("svg");
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+ }
+ tcell.appendChild(timerHost);
+ const batHost = h("div", "bat-oly");
+ DUPRE.batToggle(batHost, {
+ on: !caffHold,
+ onLabel: "TIMED",
+ offLabel: "HOLD",
+ onChange: (on) => {
+ if (booting) return;
+ caffHold = !on;
+ syncCaffeine(batHost);
+ },
+ });
+ tcell.appendChild(batHost);
+ mid.appendChild(tcell);
+ lv.appendChild(mid);
+ col.appendChild(lv);
+
+ // ---- plate 3: switch wall -----------------------------------------------
+ const sw = h("div", "plate");
+ sw.appendChild(DS.engrave("Switches"));
+ const wall = h("div", "wall");
+ ROCKERS.forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ wall.appendChild(host);
+ });
+ if (DS.state.laptop) {
+ const host = h("div", "khost");
+ DUPRE.guardedToggle(host, {
+ on: DS.state.airplane,
+ onLabel: "AIR",
+ offLabel: "SAFE",
+ onChange: (on) => { if (!booting && on !== DS.state.airplane) DS.setToggle("airplane", host); },
+ });
+ scaleSvg(host, 64);
+ wall.appendChild(host);
+ }
+ sw.appendChild(wall);
+ col.appendChild(sw);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-17.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-17.html
new file mode 100644
index 0000000..3372694
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-17.html
@@ -0,0 +1,311 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 17 (switches under their presets)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ /* one chassis grid: every section on an identical plate, identical gaps */
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .row { display: flex; justify-content: center; align-items: center; gap: 10px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .tuner-well { display: flex; justify-content: center; margin-top: 8px; }
+
+ .presets { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 5px; }
+ .preset {
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-bottom-width: 3px; border-radius: 6px;
+ padding: 4px 1px; text-align: center; cursor: pointer; color: var(--silver);
+ overflow: hidden;
+ }
+ .preset:hover { border-color: var(--gold); color: var(--gold); }
+ .preset .num { font-size: 7.5px; color: var(--dim); display: block; letter-spacing: 0; }
+ .preset .nm { font-size: 8.5px; letter-spacing: 0; display: block; }
+ .preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px; transform: translateY(2px);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12); color: var(--cream);
+ }
+ .preset.cur .num { color: var(--gold); }
+
+ /* levels + caffeine: slides left, tamed timer right, bat docked on stage */
+ .midgrid { display: flex; gap: 6px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .bat-oly { position: absolute; top: -2px; right: 0; }
+ .bat-oly svg { width: 52px; height: 67px; }
+
+ /* switch wall: name-on-switch rockers + the airplane lever, one baseline */
+ .wall { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 6px; justify-items: center; align-items: center; }
+
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 17 — switches under their presets.</b> The switch wall
+ moves up beside its cause: scenes on top, the switches they program
+ directly beneath, the power slide just below the wall — with a dark
+ knob now. Levels · caffeine follow. Everything else as P16.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const STOPS = ["OFF", "FOCUS", "PRES", "BATT", "NIGHT"];
+const ROCKERS = [
+ ["autodim", "DIM"], ["touchpad", "TPAD"], ["mouse", "MOUSE"],
+ ["nightlight", "NIGHT"], ["dnd", "DND"],
+];
+let booting = true;
+let caffMinutes = 0;
+let caffHold = false;
+let timerHost = null; // persistent: the timer owns a live countdown
+
+const sceneIdx = () => (DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0);
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: off", true);
+ render();
+}
+
+function syncCaffeine(el) {
+ const on = caffHold || caffMinutes > 0;
+ if (!booting && on !== DS.state.caffeine) DS.setToggle("caffeine", el);
+}
+const syncCaffeineSoon = debounce(() => syncCaffeine(timerHost), 450);
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+// de-throne the stock timer: steel stop knob, amber index/OFF, one engraving
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p16Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p16Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "WIND · PUSH STOPS";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: head unit — presets, tuner, power slide -------------------
+ const head = h("div", "plate");
+ head.appendChild(DS.engrave("Scenes"));
+ const pr = h("div", "presets");
+ SCENES.forEach((n, i) => {
+ const k = h("div", "preset" + (DS.state.scene === n ? " cur" : ""));
+ k.innerHTML = `<span class="num">PRESET ${i + 1}</span><span class="nm">${n}</span>`;
+ k.onclick = () => DS.applyScene(n, k);
+ pr.appendChild(k);
+ });
+ head.appendChild(pr);
+
+ const tw = h("div", "tuner-well");
+ DUPRE.slideRule(tw, {
+ values: STOPS,
+ value: STOPS[sceneIdx()],
+ skin: "warm",
+ width: 306,
+ fmt: (v) => v,
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "OFF") sceneOff();
+ else DS.applyScene(SCENES[STOPS.indexOf(v) - 1], tw);
+ },
+ });
+ head.appendChild(tw);
+ col.appendChild(head);
+
+ // ---- plate 2: the switches those presets program, power slide beneath ---
+ const sw = h("div", "plate");
+ sw.appendChild(DS.engrave("Switches · Power"));
+ const wall = h("div", "wall");
+ ROCKERS.forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ wall.appendChild(host);
+ });
+ if (DS.state.laptop) {
+ const host = h("div", "khost");
+ DUPRE.guardedToggle(host, {
+ on: DS.state.airplane,
+ onLabel: "AIR",
+ offLabel: "SAFE",
+ onChange: (on) => { if (!booting && on !== DS.state.airplane) DS.setToggle("airplane", host); },
+ });
+ scaleSvg(host, 64);
+ wall.appendChild(host);
+ }
+ sw.appendChild(wall);
+
+ const pw = h("div", "khost");
+ pw.style.marginTop = "10px";
+ const pIdx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ DUPRE.slideSelector(pw, {
+ items: ["PERFORMANCE", "BALANCE", "POWER SAVE"],
+ index: pIdx,
+ width: 306,
+ knob: "dark",
+ onChange: (i) => { if (!booting && i !== pIdx) DS.setProfile(DS.PROFILES[i].id, pw); },
+ });
+ sw.appendChild(pw);
+ col.appendChild(sw);
+
+ // ---- plate 2: levels + caffeine -----------------------------------------
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Caffeine"));
+ const mid = h("div", "midgrid");
+
+ const scell = h("div", "vcell");
+ const shost = h("div", "khost");
+ const commitLv = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]));
+ const k = Math.round(vals[1]);
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, scell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, scell);
+ }, 350);
+ DUPRE.thumbSlide(shost, {
+ channels: [
+ { name: "SCREEN", v: DS.state.brightness },
+ { name: "KBD", v: DS.state.kbd },
+ ],
+ onChange: (vals) => { if (!booting) commitLv(vals.slice()); },
+ });
+ scell.appendChild(shost);
+ mid.appendChild(scell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: caffMinutes,
+ onChange: (min) => {
+ caffMinutes = min;
+ syncCaffeineSoon();
+ },
+ });
+ scaleSvg(timerHost, 158);
+ tameTimer(timerHost);
+ // the bat docks where the winding arrow was
+ const svg = timerHost.querySelector("svg");
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+ }
+ tcell.appendChild(timerHost);
+ const batHost = h("div", "bat-oly");
+ DUPRE.batToggle(batHost, {
+ on: !caffHold,
+ onLabel: "TIMED",
+ offLabel: "HOLD",
+ onChange: (on) => {
+ if (booting) return;
+ caffHold = !on;
+ syncCaffeine(batHost);
+ },
+ });
+ tcell.appendChild(batHost);
+ mid.appendChild(tcell);
+ lv.appendChild(mid);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-18.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-18.html
new file mode 100644
index 0000000..c0ff06c
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-18.html
@@ -0,0 +1,321 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 18 (program chart, real proportions)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ /* one chassis grid: every section on an identical plate, identical gaps */
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .row { display: flex; justify-content: center; align-items: center; gap: 10px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .tuner-well { display: flex; justify-content: center; margin-top: 8px; }
+
+ .presets { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 5px; }
+ .preset {
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-bottom-width: 3px; border-radius: 6px;
+ padding: 4px 1px; text-align: center; cursor: pointer; color: var(--silver);
+ overflow: hidden;
+ }
+ .preset:hover { border-color: var(--gold); color: var(--gold); }
+ .preset .num { font-size: 7.5px; color: var(--dim); display: block; letter-spacing: 0; }
+ .preset .nm { font-size: 8.5px; letter-spacing: 0; display: block; }
+ .preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px; transform: translateY(2px);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12); color: var(--cream);
+ }
+ .preset.cur .num { color: var(--gold); }
+
+ /* levels + caffeine: slides left, tamed timer right, bat docked on stage */
+ .midgrid { display: flex; gap: 6px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .bat-oly { position: absolute; top: -2px; right: -4px; }
+ .bat-oly svg { width: 46px; height: 59px; }
+
+ /* switch wall: name-on-switch rockers + the airplane lever, one baseline */
+ .wall { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 6px; justify-items: center; align-items: center; }
+
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 18 — the program chart.</b> The missing analog component:
+ a faceplate program chart (washing-machine style) printed between the
+ scenes and their switches — filled dot = the program closes it, ring =
+ opens it, dash = leaves it alone, letter = power; the active program's
+ row lights. Airplane is gone (the network panel owns its master
+ switch). OFF on the dial now reads CUSTOM. Slides and timer re-scaled
+ toward their real-life sizes.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const STOPS = ["CUSTOM", "FOCUS", "PRES", "BATT", "NIGHT"];
+const ROCKERS = [
+ ["autodim", "DIM"], ["touchpad", "TPAD"], ["mouse", "MOUSE"],
+ ["nightlight", "NIGHT"], ["dnd", "DND"],
+];
+let booting = true;
+let caffMinutes = 0;
+let caffHold = false;
+let timerHost = null; // persistent: the timer owns a live countdown
+
+const sceneIdx = () => (DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0);
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: custom", true);
+ render();
+}
+
+function syncCaffeine(el) {
+ const on = caffHold || caffMinutes > 0;
+ if (!booting && on !== DS.state.caffeine) DS.setToggle("caffeine", el);
+}
+const syncCaffeineSoon = debounce(() => syncCaffeine(timerHost), 450);
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+// de-throne the stock timer: steel stop knob, amber index/OFF, one engraving
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p16Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p16Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "WIND · PUSH STOPS";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: head unit — presets, tuner, power slide -------------------
+ const head = h("div", "plate");
+ head.appendChild(DS.engrave("Scenes"));
+ const pr = h("div", "presets");
+ SCENES.forEach((n, i) => {
+ const k = h("div", "preset" + (DS.state.scene === n ? " cur" : ""));
+ k.innerHTML = `<span class="num">PRESET ${i + 1}</span><span class="nm">${n}</span>`;
+ k.onclick = () => DS.applyScene(n, k);
+ pr.appendChild(k);
+ });
+ head.appendChild(pr);
+
+ const tw = h("div", "tuner-well");
+ DUPRE.slideRule(tw, {
+ values: STOPS,
+ value: STOPS[sceneIdx()],
+ skin: "warm",
+ width: 306,
+ fmt: (v) => v,
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "CUSTOM") sceneOff();
+ else DS.applyScene(SCENES[STOPS.indexOf(v) - 1], tw);
+ },
+ });
+ head.appendChild(tw);
+
+ // the program chart: what each preset recalls, printed on the plate
+ const chartHost = h("div", "khost");
+ chartHost.style.marginTop = "8px";
+ const CHART_COLS = ["DIM", "NIGHT", "DND", "CAFF", "PWR"];
+ const PWR_LETTER = { performance: "P", balanced: "B", saver: "S" };
+ DUPRE.programChart(chartHost, {
+ cols: CHART_COLS,
+ width: 296,
+ rows: SCENES.map((n) => {
+ const sets = DS.SCENES[n].sets;
+ const dot = (k) => (k in sets ? !!sets[k] : null);
+ return { name: n, cells: [dot("autodim"), dot("nightlight"), dot("dnd"), dot("caffeine"), PWR_LETTER[sets.powerprofile] || null] };
+ }),
+ active: sceneIdx() - 1,
+ });
+ head.appendChild(chartHost);
+ col.appendChild(head);
+
+ // ---- plate 2: the switches those presets program, power slide beneath ---
+ const sw = h("div", "plate");
+ sw.appendChild(DS.engrave("Switches · Power"));
+ const wall = h("div", "wall");
+ ROCKERS.forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ wall.appendChild(host);
+ });
+ sw.appendChild(wall);
+
+ const pw = h("div", "khost");
+ pw.style.marginTop = "10px";
+ const pIdx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ DUPRE.slideSelector(pw, {
+ items: ["PERFORMANCE", "BALANCE", "POWER SAVE"],
+ index: pIdx,
+ width: 306,
+ knob: "dark",
+ onChange: (i) => { if (!booting && i !== pIdx) DS.setProfile(DS.PROFILES[i].id, pw); },
+ });
+ sw.appendChild(pw);
+ col.appendChild(sw);
+
+ // ---- plate 2: levels + caffeine -----------------------------------------
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Caffeine"));
+ const mid = h("div", "midgrid");
+
+ const scell = h("div", "vcell");
+ const shost = h("div", "khost");
+ const commitLv = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]));
+ const k = Math.round(vals[1]);
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, scell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, scell);
+ }, 350);
+ DUPRE.thumbSlide(shost, {
+ channels: [
+ { name: "SCREEN", v: DS.state.brightness },
+ { name: "KBD", v: DS.state.kbd },
+ ],
+ onChange: (vals) => { if (!booting) commitLv(vals.slice()); },
+ });
+ scaleSvg(shost, 165);
+ scell.appendChild(shost);
+ mid.appendChild(scell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: caffMinutes,
+ onChange: (min) => {
+ caffMinutes = min;
+ syncCaffeineSoon();
+ },
+ });
+ scaleSvg(timerHost, 137);
+ tameTimer(timerHost);
+ // the bat docks where the winding arrow was
+ const svg = timerHost.querySelector("svg");
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+ }
+ tcell.appendChild(timerHost);
+ const batHost = h("div", "bat-oly");
+ DUPRE.batToggle(batHost, {
+ on: !caffHold,
+ onLabel: "TIMED",
+ offLabel: "HOLD",
+ onChange: (on) => {
+ if (booting) return;
+ caffHold = !on;
+ syncCaffeine(batHost);
+ },
+ });
+ tcell.appendChild(batHost);
+ mid.appendChild(tcell);
+ lv.appendChild(mid);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-19.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-19.html
new file mode 100644
index 0000000..442e39e
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-19.html
@@ -0,0 +1,336 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 19 (punch-card chart, green switches)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ /* one chassis grid: every section on an identical plate, identical gaps */
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .row { display: flex; justify-content: center; align-items: center; gap: 10px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .tuner-well { display: flex; justify-content: center; margin-top: 8px; }
+
+ .presets { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 5px; }
+ .preset {
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-bottom-width: 3px; border-radius: 6px;
+ padding: 4px 1px; text-align: center; cursor: pointer; color: var(--silver);
+ overflow: hidden;
+ }
+ .preset:hover { border-color: var(--gold); color: var(--gold); }
+ .preset .num { font-size: 7.5px; color: var(--dim); display: block; letter-spacing: 0; }
+ .preset .nm { font-size: 8.5px; letter-spacing: 0; display: block; }
+ .preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px; transform: translateY(2px);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12); color: var(--cream);
+ }
+ .preset.cur .num { color: var(--gold); }
+
+ /* levels + caffeine: slides left, tamed timer right, bat docked on stage */
+ .midgrid { display: flex; gap: 6px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .bat-oly { position: absolute; top: -2px; right: -4px; }
+ .bat-oly svg { width: 46px; height: 59px; }
+
+ /* switch wall: name-on-switch rockers at real rocker size, green engage */
+ .wall { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 6px; justify-items: center; align-items: center; }
+ .wall .dupre-rocker { width: 50px; height: 32px; }
+ .wall .dupre-rocker .dupre-half { font-size: 8px; }
+
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 19 — the chart becomes a punch card.</b> Program-chart
+ lights are white (future state, not live amber) and clickable: a click
+ reprograms the preset — dots cycle closes/opens/untouched, the power
+ letter cycles P/B/S/–. Switches engage GREEN and shrink to real rocker
+ size; the slides grow. Less amber everywhere.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const STOPS = ["CUSTOM", "FOCUS", "PRES", "BATT", "NIGHT"];
+const ROCKERS = [
+ ["autodim", "DIM"], ["touchpad", "TPAD"], ["mouse", "MOUSE"],
+ ["nightlight", "NIGHT"], ["dnd", "DND"],
+];
+let booting = true;
+let caffMinutes = 0;
+let caffHold = false;
+let timerHost = null; // persistent: the timer owns a live countdown
+
+const sceneIdx = () => (DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0);
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: custom", true);
+ render();
+}
+
+function syncCaffeine(el) {
+ const on = caffHold || caffMinutes > 0;
+ if (!booting && on !== DS.state.caffeine) DS.setToggle("caffeine", el);
+}
+const syncCaffeineSoon = debounce(() => syncCaffeine(timerHost), 450);
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+// de-throne the stock timer: steel stop knob, amber index/OFF, one engraving
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p16Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p16Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "WIND · PUSH STOPS";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: head unit — presets, tuner, power slide -------------------
+ const head = h("div", "plate");
+ head.appendChild(DS.engrave("Scenes"));
+ const pr = h("div", "presets");
+ SCENES.forEach((n, i) => {
+ const k = h("div", "preset" + (DS.state.scene === n ? " cur" : ""));
+ k.innerHTML = `<span class="num">PRESET ${i + 1}</span><span class="nm">${n}</span>`;
+ k.onclick = () => DS.applyScene(n, k);
+ pr.appendChild(k);
+ });
+ head.appendChild(pr);
+
+ const tw = h("div", "tuner-well");
+ DUPRE.slideRule(tw, {
+ values: STOPS,
+ value: STOPS[sceneIdx()],
+ skin: "warm",
+ width: 306,
+ fmt: (v) => v,
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "CUSTOM") sceneOff();
+ else DS.applyScene(SCENES[STOPS.indexOf(v) - 1], tw);
+ },
+ });
+ head.appendChild(tw);
+
+ // the program chart: what each preset recalls, printed on the plate
+ const chartHost = h("div", "khost");
+ chartHost.style.marginTop = "8px";
+ const CHART_COLS = ["DIM", "NIGHT", "DND", "CAFF", "PWR"];
+ const PWR_LETTER = { performance: "P", balanced: "B", saver: "S" };
+ const CHART_KEYS = ["autodim", "nightlight", "dnd", "caffeine"];
+ const LETTER_PWR = { P: "performance", B: "balanced", S: "saver" };
+ DUPRE.programChart(chartHost, {
+ cols: CHART_COLS,
+ width: 296,
+ editable: true,
+ rows: SCENES.map((n) => {
+ const sets = DS.SCENES[n].sets;
+ const dot = (k) => (k in sets ? !!sets[k] : null);
+ return { name: n, cells: [dot("autodim"), dot("nightlight"), dot("dnd"), dot("caffeine"), PWR_LETTER[sets.powerprofile] || null] };
+ }),
+ active: sceneIdx() - 1,
+ onEdit: (ri, ci, v) => {
+ const sets = DS.SCENES[SCENES[ri]].sets;
+ if (ci < 4) {
+ if (v === null) delete sets[CHART_KEYS[ci]];
+ else sets[CHART_KEYS[ci]] = v;
+ } else {
+ if (v === null) delete sets.powerprofile;
+ else sets.powerprofile = LETTER_PWR[v];
+ }
+ DS.toast(SCENES[ri] + " program updated", true);
+ },
+ });
+ head.appendChild(chartHost);
+ col.appendChild(head);
+
+ // ---- plate 2: the switches those presets program, power slide beneath ---
+ const sw = h("div", "plate");
+ sw.appendChild(DS.engrave("Switches · Power"));
+ const wall = h("div", "wall");
+ ROCKERS.forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ wall.appendChild(host);
+ });
+ sw.appendChild(wall);
+
+ const pw = h("div", "khost");
+ pw.style.marginTop = "10px";
+ const pIdx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ DUPRE.slideSelector(pw, {
+ items: ["PERFORMANCE", "BALANCE", "POWER SAVE"],
+ index: pIdx,
+ width: 306,
+ knob: "dark",
+ onChange: (i) => { if (!booting && i !== pIdx) DS.setProfile(DS.PROFILES[i].id, pw); },
+ });
+ sw.appendChild(pw);
+ col.appendChild(sw);
+
+ // ---- plate 2: levels + caffeine -----------------------------------------
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Caffeine"));
+ const mid = h("div", "midgrid");
+
+ const scell = h("div", "vcell");
+ const shost = h("div", "khost");
+ const commitLv = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]));
+ const k = Math.round(vals[1]);
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, scell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, scell);
+ }, 350);
+ DUPRE.thumbSlide(shost, {
+ channels: [
+ { name: "SCREEN", v: DS.state.brightness },
+ { name: "KBD", v: DS.state.kbd },
+ ],
+ onChange: (vals) => { if (!booting) commitLv(vals.slice()); },
+ });
+ scaleSvg(shost, 172);
+ scell.appendChild(shost);
+ mid.appendChild(scell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: caffMinutes,
+ onChange: (min) => {
+ caffMinutes = min;
+ syncCaffeineSoon();
+ },
+ });
+ scaleSvg(timerHost, 130);
+ tameTimer(timerHost);
+ // the bat docks where the winding arrow was
+ const svg = timerHost.querySelector("svg");
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+ }
+ tcell.appendChild(timerHost);
+ const batHost = h("div", "bat-oly");
+ DUPRE.batToggle(batHost, {
+ on: !caffHold,
+ onLabel: "TIMED",
+ offLabel: "HOLD",
+ onChange: (on) => {
+ if (booting) return;
+ caffHold = !on;
+ syncCaffeine(batHost);
+ },
+ });
+ tcell.appendChild(batHost);
+ mid.appendChild(tcell);
+ lv.appendChild(mid);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-2.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-2.html
index af2160c..c215f85 100644
--- a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-2.html
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-2.html
@@ -72,6 +72,7 @@ function render() {
]));
const powerNodes = [];
+ powerNodes.push(DS.toggleKey("caffeine"));
if (DS.state.laptop) powerNodes.push(DS.toggleKey("airplane"));
powerNodes.push(DS.profileSeg());
const acts = DS.h("div", "ds-grid2");
@@ -82,6 +83,8 @@ function render() {
col.appendChild(section("Notifications", [DS.toggleKey("dnd")]));
+ col.appendChild(section("Weather", [DS.toggleKey("weathergeo")]));
+
const scenes = DS.h("div", "ds-grid4");
Object.keys(DS.SCENES).forEach((n) => scenes.appendChild(DS.sceneKey(n)));
col.appendChild(section("Scenes", [scenes]));
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-20.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-20.html
new file mode 100644
index 0000000..9f01322
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-20.html
@@ -0,0 +1,360 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 20 (the switch matrix)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+
+ /* the matrix: switches down, presets across */
+ .mx { display: grid; grid-template-columns: 96px repeat(4, 1fr); row-gap: 6px; align-items: center; }
+ .mx .mx-cell { display: flex; align-items: center; justify-content: center; min-height: 22px; }
+ .mx .mx-live { justify-content: flex-start; }
+ .mx .dupre-rocker { width: 50px; height: 32px; }
+ .mx .dupre-rocker .dupre-half { font-size: 8px; }
+ .mx-preset {
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-bottom-width: 3px; border-radius: 6px;
+ padding: 5px 2px; text-align: center; cursor: pointer; color: var(--silver);
+ font-size: 8.5px; letter-spacing: 0.02em; width: 100%; margin: 0 2px;
+ overflow: hidden; font-family: var(--mono);
+ }
+ .mx-preset:hover { border-color: var(--gold); color: var(--gold); }
+ .mx-preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px; transform: translateY(2px);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12); color: var(--cream);
+ }
+ .mx-name { color: var(--steel); font-size: 8px; letter-spacing: 0.1em; text-transform: uppercase; margin-left: 6px; }
+ .mx-lamp { width: 11px; height: 11px; border-radius: 50%;
+ background: radial-gradient(circle at 38% 32%, #2e2a24, #171310); border: 1px solid #060505; }
+ .mx-lamp.on { background: radial-gradient(circle at 38% 32%, #a9c95f, var(--pass) 60%, #3c4d18);
+ box-shadow: 0 0 7px rgba(116,147,47,.7); }
+ /* punch cells: the chart's dot vocabulary, scoped for the matrix */
+ .mx .pc-dot, .mx .pc-dash { pointer-events: none; }
+ .mx .pc-dot { width: 6px; height: 6px; border-radius: 50%; }
+ .mx .pc-dot.pc-set { background: #cfd3cf; }
+ .mx .pc-dot.pc-clear { border: 1px solid var(--steel); opacity: .6; }
+ .mx .pc-dash { width: 6px; height: 1px; background: var(--wash); }
+ .mx .mx-punch { cursor: pointer; border-radius: 4px; height: 22px; }
+ .mx .mx-punch:hover { background: #ffffff10; }
+ .mx .mx-pwr { font-size: 7.5px; color: var(--steel); font-family: var(--mono); }
+ .mx-row-live .mx-pwr { color: #f2f4f2; }
+
+ .lvrow { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; }
+ .lvrow .lv-name { color: var(--silver); font-size: 10px; width: 48px; letter-spacing: 0.06em; }
+ .timer-row { display: flex; justify-content: center; margin-top: 4px; }
+ .tcell { position: relative; }
+ .bat-oly { position: absolute; top: -2px; right: -4px; }
+ .bat-oly svg { width: 46px; height: 59px; }
+
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 20 — the switch matrix.</b> The panel's axis is the
+ switches: each row is a live switch, the four preset buttons run across
+ the top (names only), and the white punch lights under each preset
+ program what it recalls. TPAD and MOUSE (never scene-touched) drop to
+ their own strip below the levels. Screen, keyboard, and power are
+ horizontal thumb slides. The slide-rule dial is retired.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES); // Focus Presentation Battery Night
+const MX_ROWS = [
+ { key: "autodim", name: "DIM", kind: "rocker" },
+ { key: "nightlight", name: "NIGHT", kind: "rocker" },
+ { key: "dnd", name: "DND", kind: "rocker" },
+ { key: "caffeine", name: "CAFF", kind: "lamp" },
+ { key: "powerprofile", name: "PWR", kind: "power" },
+];
+const PWR_LETTER = { performance: "P", balanced: "B", saver: "S" };
+const LETTER_PWR = { P: "performance", B: "balanced", S: "saver" };
+const PWR_CYCLE = ["P", "B", "S", null];
+let booting = true;
+let caffMinutes = 0;
+let caffHold = false;
+let timerHost = null;
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function syncCaffeine(el) {
+ const on = caffHold || caffMinutes > 0;
+ if (!booting && on !== DS.state.caffeine) DS.setToggle("caffeine", el);
+}
+const syncCaffeineSoon = debounce(() => syncCaffeine(timerHost), 450);
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p20Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p20Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "WIND · PUSH STOPS";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+}
+
+// program cell value for scene n, row r
+function cellOf(n, r) {
+ const sets = DS.SCENES[n].sets;
+ if (r.kind === "power") return PWR_LETTER[sets.powerprofile] || null;
+ return r.key in sets ? !!sets[r.key] : null;
+}
+function paintPunch(c, v) {
+ if (v === true) c.innerHTML = '<span class="pc-dot pc-set"></span>';
+ else if (v === false) c.innerHTML = '<span class="pc-dot pc-clear"></span>';
+ else if (typeof v === "string") { c.innerHTML = ""; c.textContent = v; c.classList.add("mx-pwr"); return; }
+ else c.innerHTML = '<span class="pc-dash"></span>';
+ c.classList.remove("mx-pwr");
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the matrix ------------------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Programs"));
+ const mx = h("div", "mx");
+
+ // header row: empty corner + preset buttons (names only)
+ mx.appendChild(h("div", "mx-cell"));
+ SCENES.forEach((n) => {
+ const b = h("button", "mx-preset" + (DS.state.scene === n ? " cur" : ""), n);
+ b.onclick = () => DS.applyScene(n, b);
+ const wrap = h("div", "mx-cell");
+ wrap.appendChild(b);
+ mx.appendChild(wrap);
+ });
+
+ // switch rows
+ MX_ROWS.forEach((r) => {
+ const live = h("div", "mx-cell mx-live");
+ if (r.kind === "rocker") {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[r.key],
+ onLabel: r.name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[r.key]) DS.setToggle(r.key, host); },
+ });
+ live.appendChild(host);
+ } else if (r.kind === "lamp") {
+ const lamp = h("span", "mx-lamp" + (DS.state.caffeine ? " on" : ""));
+ live.appendChild(lamp);
+ live.appendChild(h("span", "mx-name", r.name));
+ } else {
+ const host = h("div", "khost");
+ DUPRE.thumbSlideH(host, {
+ width: 86,
+ positions: ["P", "B", "S"],
+ index: DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile),
+ onChange: (i) => {
+ if (booting) return;
+ if (DS.PROFILES[i].id !== DS.state.powerprofile) DS.setProfile(DS.PROFILES[i].id, host);
+ },
+ });
+ live.appendChild(host);
+ }
+ mx.appendChild(live);
+
+ SCENES.forEach((n) => {
+ const c = h("div", "mx-cell mx-punch");
+ paintPunch(c, cellOf(n, r));
+ c.onclick = () => {
+ const sets = DS.SCENES[n].sets;
+ const v = cellOf(n, r);
+ let nv;
+ if (r.kind === "power") {
+ nv = PWR_CYCLE[(PWR_CYCLE.indexOf(v) + 1) % PWR_CYCLE.length];
+ if (nv === null) delete sets.powerprofile; else sets.powerprofile = LETTER_PWR[nv];
+ } else {
+ nv = v === true ? false : v === false ? null : true;
+ if (nv === null) delete sets[r.key]; else sets[r.key] = nv;
+ }
+ paintPunch(c, nv);
+ DS.toast(n + " program updated", true);
+ };
+ mx.appendChild(c);
+ });
+ });
+
+ plate.appendChild(mx);
+ col.appendChild(plate);
+
+ // ---- plate 2: levels (horizontal) + caffeine ----------------------------
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Caffeine"));
+ [["brightness", "SCREEN"], ["kbd", "KBD"]].forEach(([key, name]) => {
+ const row = h("div", "lvrow");
+ row.appendChild(h("span", "lv-name", name));
+ const host = h("div", "khost");
+ host.style.flex = "1";
+ const commit = debounce((v) => {
+ const pct = Math.max(DS.SLIDERS[key].floor, v);
+ if (pct !== DS.state[key]) DS.setSlider(key, pct, row);
+ }, 350);
+ DUPRE.thumbSlideH(host, {
+ width: 250,
+ value: DS.state[key],
+ onChange: (v) => { if (!booting) commit(v); },
+ });
+ row.appendChild(host);
+ lv.appendChild(row);
+ });
+
+ const trow = h("div", "timer-row");
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: caffMinutes,
+ onChange: (min) => {
+ caffMinutes = min;
+ syncCaffeineSoon();
+ },
+ });
+ scaleSvg(timerHost, 130);
+ tameTimer(timerHost);
+ }
+ tcell.appendChild(timerHost);
+ const batHost = h("div", "bat-oly");
+ DUPRE.batToggle(batHost, {
+ on: !caffHold,
+ onLabel: "TIMED",
+ offLabel: "HOLD",
+ onChange: (on) => {
+ if (booting) return;
+ caffHold = !on;
+ syncCaffeine(batHost);
+ },
+ });
+ tcell.appendChild(batHost);
+ trow.appendChild(tcell);
+ lv.appendChild(trow);
+ col.appendChild(lv);
+
+ // ---- plate 3: pointing devices (never scene-touched) --------------------
+ const pt = h("div", "plate");
+ pt.appendChild(DS.engrave("Pointing"));
+ const strip = h("div", "pointing");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ strip.appendChild(host);
+ });
+ pt.appendChild(strip);
+ col.appendChild(pt);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-21.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-21.html
new file mode 100644
index 0000000..274ae78
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-21.html
@@ -0,0 +1,287 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 21 (pin routing matrix)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 21 — the pin routing matrix.</b> The interface is the
+ matrix: switch names label the rows, the program buttons head the
+ columns, pins route settings into programs. The NOW column is live —
+ green pins are current state, click to toggle. Power is a radio row
+ group (one pin per column). CAFF is an ordinary switch; the dial is
+ now a SLEEP TIMER (wind minutes until suspend). The drums are back.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["DIM", "autodim"], ["NIGHT", "nightlight"], ["DND", "dnd"], ["CAFF", "caffeine"]];
+const PWR_ROWS = [["PERF", "performance"], ["BAL", "balanced"], ["SAVER", "saver"]];
+const ROW_LABELS = [...SWITCH_ROWS.map(r => r[0]), ...PWR_ROWS.map(r => r[0])];
+const COLS = ["NOW", ...SCENES];
+let booting = true;
+let sleepMinutes = 0;
+let timerHost = null; // persistent: the timer owns a live countdown
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each)
+SCENES.forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+// the dial is a SLEEP TIMER now: wind minutes; zero = suspend (mocked)
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p21Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p21Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "SLEEP TIMER · PUSH STOPS";
+ if (t.textContent === "MINUTES") t.textContent = "SLEEP IN";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+}
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Program routing"));
+ const mxHost = h("div", "khost");
+ const mx = DUPRE.pinMatrix(mxHost, {
+ rows: ROW_LABELS,
+ cols: COLS,
+ pins: currentPins(),
+ width: 306,
+ x0: 58,
+ dy: 17,
+ colHeads: "button",
+ activeCol: DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : -1,
+ colColors: ["#a9c95f", "#cfd3cf", "#cfd3cf", "#cfd3cf", "#cfd3cf"],
+ groups: [SWITCH_ROWS.length, PWR_ROWS.length],
+ onColHead: (j) => {
+ if (booting) return;
+ if (j === 0) return; // NOW isn't a program
+ DS.applyScene(SCENES[j - 1], mxHost);
+ },
+ onPin: (rowLbl, colLbl, seated) => {
+ if (booting) return;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ const pw = PWR_ROWS.find(([l]) => l === rowLbl);
+ if (colLbl === "NOW") {
+ if (sw) {
+ if (seated !== DS.state[sw[1]]) DS.setToggle(sw[1], mxHost);
+ else render();
+ } else if (pw) {
+ if (!seated) { mx.set(rowLbl + ">NOW", true); return; } // can't pull the live profile
+ DS.setProfile(pw[1], mxHost);
+ }
+ return;
+ }
+ const sets = DS.SCENES[colLbl].sets;
+ if (sw) {
+ sets[sw[1]] = seated;
+ DS.toast(colLbl + " program updated", true);
+ } else if (pw) {
+ if (!seated) { mx.set(rowLbl + ">" + colLbl, true); return; } // one power pin per program
+ sets.powerprofile = pw[1];
+ PWR_ROWS.forEach(([l, id]) => { if (id !== pw[1]) mx.set(l + ">" + colLbl, false); });
+ DS.toast(colLbl + " program updated", true);
+ }
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: levels (the drums are back) + sleep timer -----------------
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Sleep"));
+ const mid = h("div", "midgrid");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 132,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ mid.appendChild(dcell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: 0,
+ onChange: (min) => {
+ const was = sleepMinutes;
+ sleepMinutes = min;
+ if (booting) return;
+ if (was > 0.5 && min === 0) DS.toast("Sleep timer expired — SUSPEND (mock)", true);
+ },
+ });
+ scaleSvg(timerHost, 132);
+ tameTimer(timerHost);
+ }
+ tcell.appendChild(timerHost);
+ mid.appendChild(tcell);
+ lv.appendChild(mid);
+ col.appendChild(lv);
+
+ // ---- plate 3: pointing (never program-routed) ---------------------------
+ const pt = h("div", "plate");
+ pt.appendChild(DS.engrave("Pointing"));
+ const strip = h("div", "pointing");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ strip.appendChild(host);
+ });
+ pt.appendChild(strip);
+ col.appendChild(pt);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-22.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-22.html
new file mode 100644
index 0000000..f67e990
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-22.html
@@ -0,0 +1,291 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 22 (the programmable matrix)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 22 — the programmable matrix.</b> No NOW column: press a
+ program head (FOCUS · PRESENT · BATTERY · NIGHT) and its column's pins
+ go white → green, because the active program IS the live state.
+ Editing the green column edits the present; editing a white column
+ edits a future. Power rows are radio (a pin moves, never pulls).
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROWS = [["PERF", "performance"], ["BAL", "balanced"], ["SAVER", "saver"]];
+const ROW_LABELS = [...SWITCH_ROWS.map(r => r[0]), ...PWR_ROWS.map(r => r[0])];
+const COLS = ["NOW", ...SCENES];
+let booting = true;
+let sleepMinutes = 0;
+let timerHost = null; // persistent: the timer owns a live countdown
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each)
+SCENES.forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+// the dial is a SLEEP TIMER now: wind minutes; zero = suspend (mocked)
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p21Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p21Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "SLEEP TIMER · PUSH STOPS";
+ if (t.textContent === "MINUTES") t.textContent = "SLEEP IN";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+}
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Program routing"));
+ const mxHost = h("div", "khost");
+ const PROG_LABEL = { Focus: "FOCUS", Presentation: "PRESENT", Battery: "BATTERY", Night: "NIGHT" };
+ const LABEL_PROG = Object.fromEntries(Object.entries(PROG_LABEL).map(([k, v]) => [v, k]));
+ const PROGS = SCENES.map((n) => PROG_LABEL[n]);
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + PROG_LABEL[n]); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + PROG_LABEL[n]); });
+ });
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: ROW_LABELS,
+ programs: PROGS,
+ pins,
+ radios: [PWR_ROWS.map(([l]) => l)],
+ groups: [SWITCH_ROWS.length, PWR_ROWS.length],
+ active: DS.state.scene ? SCENES.indexOf(DS.state.scene) : -1,
+ width: 306,
+ x0: 104,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(SCENES[j], mxHost);
+ },
+ onPin: (rowLbl, progLbl, seated, isActive) => {
+ if (booting) return;
+ const scene = LABEL_PROG[progLbl];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ const pw = PWR_ROWS.find(([l]) => l === rowLbl);
+ if (sw) {
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ DS.state.scene = scene; // stay in the program: its column is the truth
+ render();
+ } else DS.toast(progLbl + " program updated", true);
+ } else if (pw && seated) {
+ sets.powerprofile = pw[1];
+ if (isActive && DS.state.powerprofile !== pw[1]) {
+ DS.setProfile(pw[1], mxHost);
+ DS.state.scene = scene;
+ render();
+ } else DS.toast(progLbl + " program updated", true);
+ }
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: levels (the drums are back) + sleep timer -----------------
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Sleep"));
+ const mid = h("div", "midgrid");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 132,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ mid.appendChild(dcell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: 0,
+ onChange: (min) => {
+ const was = sleepMinutes;
+ sleepMinutes = min;
+ if (booting) return;
+ if (was > 0.5 && min === 0) DS.toast("Sleep timer expired — SUSPEND (mock)", true);
+ },
+ });
+ scaleSvg(timerHost, 132);
+ tameTimer(timerHost);
+ }
+ tcell.appendChild(timerHost);
+ mid.appendChild(tcell);
+ lv.appendChild(mid);
+ col.appendChild(lv);
+
+ // ---- plate 3: pointing (never program-routed) ---------------------------
+ const pt = h("div", "plate");
+ pt.appendChild(DS.engrave("Pointing"));
+ const strip = h("div", "pointing");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ strip.appendChild(host);
+ });
+ pt.appendChild(strip);
+ col.appendChild(pt);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-23.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-23.html
new file mode 100644
index 0000000..d0ddf85
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-23.html
@@ -0,0 +1,305 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 23 (pushwheel CPU row)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 23 — the pushwheel CPU row.</b> CPU power collapses to a
+ single letter row: each program carries P / B / S (click cycles), and
+ the row ends in a push-button thumbwheel — tape-counter window showing
+ the LIVE letter, triangle steppers to walk it (up toward P). Row
+ labels right-align to the buttons' left edge. Active column still
+ floods green, letters included.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["P", "B", "S"]; // ordered with DS.PROFILES
+const LETTER_OF = { performance: "P", balanced: "B", saver: "S" };
+const PWR_OF = { P: "performance", B: "balanced", S: "saver" };
+let booting = true;
+let sleepMinutes = 0;
+let timerHost = null; // persistent: the timer owns a live countdown
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each)
+SCENES.forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+// the dial is a SLEEP TIMER now: wind minutes; zero = suspend (mocked)
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p21Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p21Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "SLEEP TIMER · PUSH STOPS";
+ if (t.textContent === "MINUTES") t.textContent = "SLEEP IN";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+}
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Program routing"));
+ const mxHost = h("div", "khost");
+ const PROG_LABEL = { Focus: "FOCUS", Presentation: "PRESENT", Battery: "BATTERY", Night: "NIGHT" };
+ const LABEL_PROG = Object.fromEntries(Object.entries(PROG_LABEL).map(([k, v]) => [v, k]));
+ const PROGS = SCENES.map((n) => PROG_LABEL[n]);
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + PROG_LABEL[n]); });
+ });
+ const letters = {};
+ SCENES.forEach((n) => { letters[PWR_ROW + ">" + PROG_LABEL[n]] = LETTER_OF[DS.SCENES[n].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES, stepper: true }],
+ programs: PROGS,
+ pins,
+ letters,
+ groups: [SWITCH_ROWS.length, 1],
+ active: DS.state.scene ? SCENES.indexOf(DS.state.scene) : -1,
+ width: 306,
+ x0: 104,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(SCENES[j], mxHost);
+ },
+ onPin: (rowLbl, progLbl, seated, isActive) => {
+ if (booting) return;
+ const scene = LABEL_PROG[progLbl];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast(progLbl + " program updated", true);
+ },
+ onLetter: (rowLbl, progLbl, v, isActive) => {
+ if (booting) return;
+ const scene = LABEL_PROG[progLbl];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast(progLbl + " program updated", true);
+ },
+ onStep: (rowLbl, dir) => {
+ if (booting) return;
+ const idx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ const n = Math.max(0, Math.min(DS.PROFILES.length - 1, idx + dir));
+ if (n === idx) return;
+ const scene = DS.state.scene;
+ DS.setProfile(DS.PROFILES[n].id, mxHost);
+ if (scene) {
+ DS.SCENES[scene].sets.powerprofile = DS.PROFILES[n].id; // active-is-live
+ stayActive(scene);
+ }
+ },
+ });
+ mx.setStepper(PWR_ROW, LETTER_OF[DS.state.powerprofile]);
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: levels (the drums are back) + sleep timer -----------------
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Sleep"));
+ const mid = h("div", "midgrid");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 132,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ mid.appendChild(dcell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: 0,
+ onChange: (min) => {
+ const was = sleepMinutes;
+ sleepMinutes = min;
+ if (booting) return;
+ if (was > 0.5 && min === 0) DS.toast("Sleep timer expired — SUSPEND (mock)", true);
+ },
+ });
+ scaleSvg(timerHost, 132);
+ tameTimer(timerHost);
+ }
+ tcell.appendChild(timerHost);
+ mid.appendChild(tcell);
+ lv.appendChild(mid);
+ col.appendChild(lv);
+
+ // ---- plate 3: pointing (never program-routed) ---------------------------
+ const pt = h("div", "plate");
+ pt.appendChild(DS.engrave("Pointing"));
+ const strip = h("div", "pointing");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ strip.appendChild(host);
+ });
+ pt.appendChild(strip);
+ col.appendChild(pt);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-24.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-24.html
new file mode 100644
index 0000000..c473ce0
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-24.html
@@ -0,0 +1,304 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 24 (tape wheel, jewel verdicts)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 24 — tape wheel + jewel verdicts.</b> The triangles are
+ gone: click the tape-counter wheel itself to cycle the live CPU letter
+ (P → B → S, wrapping). In the active column every switch row shows a
+ verdict as a small jewel pilot lamp — green seated, red for a socket
+ the program holds off. The wheel cell is bigger, cream paper and
+ cylinder shade like the kit tape counter.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["P", "B", "S"]; // ordered with DS.PROFILES
+const LETTER_OF = { performance: "P", balanced: "B", saver: "S" };
+const PWR_OF = { P: "performance", B: "balanced", S: "saver" };
+let booting = true;
+let sleepMinutes = 0;
+let timerHost = null; // persistent: the timer owns a live countdown
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each)
+SCENES.forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+// the dial is a SLEEP TIMER now: wind minutes; zero = suspend (mocked)
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p21Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p21Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "SLEEP TIMER · PUSH STOPS";
+ if (t.textContent === "MINUTES") t.textContent = "SLEEP IN";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+}
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Program routing"));
+ const mxHost = h("div", "khost");
+ const PROG_LABEL = { Focus: "FOCUS", Presentation: "PRESENT", Battery: "BATTERY", Night: "NIGHT" };
+ const LABEL_PROG = Object.fromEntries(Object.entries(PROG_LABEL).map(([k, v]) => [v, k]));
+ const PROGS = SCENES.map((n) => PROG_LABEL[n]);
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + PROG_LABEL[n]); });
+ });
+ const letters = {};
+ SCENES.forEach((n) => { letters[PWR_ROW + ">" + PROG_LABEL[n]] = LETTER_OF[DS.SCENES[n].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES, stepper: true }],
+ programs: PROGS,
+ pins,
+ letters,
+ groups: [SWITCH_ROWS.length, 1],
+ active: DS.state.scene ? SCENES.indexOf(DS.state.scene) : -1,
+ width: 306,
+ x0: 104,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(SCENES[j], mxHost);
+ },
+ onPin: (rowLbl, progLbl, seated, isActive) => {
+ if (booting) return;
+ const scene = LABEL_PROG[progLbl];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast(progLbl + " program updated", true);
+ },
+ onLetter: (rowLbl, progLbl, v, isActive) => {
+ if (booting) return;
+ const scene = LABEL_PROG[progLbl];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast(progLbl + " program updated", true);
+ },
+ onStep: (rowLbl) => {
+ if (booting) return;
+ const idx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ const n = (idx + 1) % DS.PROFILES.length; // the wheel is the button: cycle, wrapping
+ const scene = DS.state.scene;
+ DS.setProfile(DS.PROFILES[n].id, mxHost);
+ if (scene) {
+ DS.SCENES[scene].sets.powerprofile = DS.PROFILES[n].id; // active-is-live
+ stayActive(scene);
+ }
+ },
+ });
+ mx.setStepper(PWR_ROW, LETTER_OF[DS.state.powerprofile]);
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: levels (the drums are back) + sleep timer -----------------
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Sleep"));
+ const mid = h("div", "midgrid");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 132,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ mid.appendChild(dcell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: 0,
+ onChange: (min) => {
+ const was = sleepMinutes;
+ sleepMinutes = min;
+ if (booting) return;
+ if (was > 0.5 && min === 0) DS.toast("Sleep timer expired — SUSPEND (mock)", true);
+ },
+ });
+ scaleSvg(timerHost, 132);
+ tameTimer(timerHost);
+ }
+ tcell.appendChild(timerHost);
+ mid.appendChild(tcell);
+ lv.appendChild(mid);
+ col.appendChild(lv);
+
+ // ---- plate 3: pointing (never program-routed) ---------------------------
+ const pt = h("div", "plate");
+ pt.appendChild(DS.engrave("Pointing"));
+ const strip = h("div", "pointing");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ strip.appendChild(host);
+ });
+ pt.appendChild(strip);
+ col.appendChild(pt);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-25.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-25.html
new file mode 100644
index 0000000..e0bbb1d
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-25.html
@@ -0,0 +1,292 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 25 (wheels in every column)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 25 — wheels in every column.</b> The side wheel was a
+ NOW-column vestige: every program column now carries its own tape
+ wheel in the CPU row — click any wheel to cycle that program's letter;
+ the active column's wheel wears the green bezel and IS the live value.
+ Jewel pilot lamps sized up to match the taller rows.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["P", "B", "S"]; // ordered with DS.PROFILES
+const LETTER_OF = { performance: "P", balanced: "B", saver: "S" };
+const PWR_OF = { P: "performance", B: "balanced", S: "saver" };
+let booting = true;
+let sleepMinutes = 0;
+let timerHost = null; // persistent: the timer owns a live countdown
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each)
+SCENES.forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+// the dial is a SLEEP TIMER now: wind minutes; zero = suspend (mocked)
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p21Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p21Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "SLEEP TIMER · PUSH STOPS";
+ if (t.textContent === "MINUTES") t.textContent = "SLEEP IN";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+}
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Program routing"));
+ const mxHost = h("div", "khost");
+ const PROG_LABEL = { Focus: "FOCUS", Presentation: "PRESENT", Battery: "BATTERY", Night: "NIGHT" };
+ const LABEL_PROG = Object.fromEntries(Object.entries(PROG_LABEL).map(([k, v]) => [v, k]));
+ const PROGS = SCENES.map((n) => PROG_LABEL[n]);
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + PROG_LABEL[n]); });
+ });
+ const letters = {};
+ SCENES.forEach((n) => { letters[PWR_ROW + ">" + PROG_LABEL[n]] = LETTER_OF[DS.SCENES[n].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ groups: [SWITCH_ROWS.length, 1],
+ active: DS.state.scene ? SCENES.indexOf(DS.state.scene) : -1,
+ width: 306,
+ x0: 104,
+ dy: 21,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(SCENES[j], mxHost);
+ },
+ onPin: (rowLbl, progLbl, seated, isActive) => {
+ if (booting) return;
+ const scene = LABEL_PROG[progLbl];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast(progLbl + " program updated", true);
+ },
+ onLetter: (rowLbl, progLbl, v, isActive) => {
+ if (booting) return;
+ const scene = LABEL_PROG[progLbl];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast(progLbl + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: levels (the drums are back) + sleep timer -----------------
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Sleep"));
+ const mid = h("div", "midgrid");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 132,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ mid.appendChild(dcell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: 0,
+ onChange: (min) => {
+ const was = sleepMinutes;
+ sleepMinutes = min;
+ if (booting) return;
+ if (was > 0.5 && min === 0) DS.toast("Sleep timer expired — SUSPEND (mock)", true);
+ },
+ });
+ scaleSvg(timerHost, 132);
+ tameTimer(timerHost);
+ }
+ tcell.appendChild(timerHost);
+ mid.appendChild(tcell);
+ lv.appendChild(mid);
+ col.appendChild(lv);
+
+ // ---- plate 3: pointing (never program-routed) ---------------------------
+ const pt = h("div", "plate");
+ pt.appendChild(DS.engrave("Pointing"));
+ const strip = h("div", "pointing");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ strip.appendChild(host);
+ });
+ pt.appendChild(strip);
+ col.appendChild(pt);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-26.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-26.html
new file mode 100644
index 0000000..c0d0cb4
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-26.html
@@ -0,0 +1,295 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 26 (physical keys, arrow wheels)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .ds-panel { width: 434px; }
+ .ds-caption { max-width: 430px; }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 26 — physical keys, arrow wheels.</b> Program heads are
+ real keys now: raised bevel at rest, depressed when active, green
+ lighting only the legend. Row labels engraved bigger on the faceplate.
+ The CPU wheels print ↑ – ↓ instead of letters, no live highlight on
+ the wheel — the depressed key and the jewels say what's live. The
+ whole panel is wider to give it room.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "\u2013", "\u2193"]; // up / dash / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "\u2013", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "\u2013": "balanced", "\u2193": "saver" };
+let booting = true;
+let sleepMinutes = 0;
+let timerHost = null; // persistent: the timer owns a live countdown
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each)
+SCENES.forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+// the dial is a SLEEP TIMER now: wind minutes; zero = suspend (mocked)
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p21Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p21Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "SLEEP TIMER · PUSH STOPS";
+ if (t.textContent === "MINUTES") t.textContent = "SLEEP IN";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+}
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Program routing"));
+ const mxHost = h("div", "khost");
+ const PROG_LABEL = { Focus: "FOCUS", Presentation: "PRESENT", Battery: "BATTERY", Night: "NIGHT" };
+ const LABEL_PROG = Object.fromEntries(Object.entries(PROG_LABEL).map(([k, v]) => [v, k]));
+ const PROGS = SCENES.map((n) => PROG_LABEL[n]);
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + PROG_LABEL[n]); });
+ });
+ const letters = {};
+ SCENES.forEach((n) => { letters[PWR_ROW + ">" + PROG_LABEL[n]] = LETTER_OF[DS.SCENES[n].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ groups: [SWITCH_ROWS.length, 1],
+ active: DS.state.scene ? SCENES.indexOf(DS.state.scene) : -1,
+ width: 382,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(SCENES[j], mxHost);
+ },
+ onPin: (rowLbl, progLbl, seated, isActive) => {
+ if (booting) return;
+ const scene = LABEL_PROG[progLbl];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast(progLbl + " program updated", true);
+ },
+ onLetter: (rowLbl, progLbl, v, isActive) => {
+ if (booting) return;
+ const scene = LABEL_PROG[progLbl];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast(progLbl + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: levels (the drums are back) + sleep timer -----------------
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Sleep"));
+ const mid = h("div", "midgrid");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 132,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ mid.appendChild(dcell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: 0,
+ onChange: (min) => {
+ const was = sleepMinutes;
+ sleepMinutes = min;
+ if (booting) return;
+ if (was > 0.5 && min === 0) DS.toast("Sleep timer expired — SUSPEND (mock)", true);
+ },
+ });
+ scaleSvg(timerHost, 132);
+ tameTimer(timerHost);
+ }
+ tcell.appendChild(timerHost);
+ mid.appendChild(tcell);
+ lv.appendChild(mid);
+ col.appendChild(lv);
+
+ // ---- plate 3: pointing (never program-routed) ---------------------------
+ const pt = h("div", "plate");
+ pt.appendChild(DS.engrave("Pointing"));
+ const strip = h("div", "pointing");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ strip.appendChild(host);
+ });
+ pt.appendChild(strip);
+ col.appendChild(pt);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-27.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-27.html
new file mode 100644
index 0000000..617ee5a
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-27.html
@@ -0,0 +1,313 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 27 (console keys, six slots, one bench)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .ds-panel { width: 560px; }
+ .ds-caption { max-width: 540px; }
+ .bench { display: flex; align-items: center; justify-content: center; gap: 14px; }
+ .groove { width: 2px; align-self: stretch; margin: 4px 0;
+ background: linear-gradient(180deg, transparent, #060505 15%, #060505 85%, transparent);
+ box-shadow: 1px 0 0 #2a2724; }
+ .rockstack { display: flex; flex-direction: column; gap: 10px; align-items: center; }
+ .rockstack .dupre-rocker { width: 56px; height: 34px; }
+ .rockstack .dupre-rocker .dupre-half { font-size: 8px; }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 27 — console keys, six slots, one bench.</b> Heads are
+ kit console keys lighting green; two blank program slots grow the
+ matrix rightward. The lower level is one bench: drums · groove · TPAD
+ over MOUSE · groove · sleep dial. Next conversation: what the dial
+ fires when it runs out.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "\u2013", "\u2193"]; // up / dash / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "\u2013", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "\u2013": "balanced", "\u2193": "saver" };
+let booting = true;
+let sleepMinutes = 0;
+let timerHost = null; // persistent: the timer owns a live countdown
+
+// two unassigned program slots (blank keys, all-off programs)
+if (!DS.SCENES.Slot5) DS.SCENES.Slot5 = { glyph: "", sets: {} };
+if (!DS.SCENES.Slot6) DS.SCENES.Slot6 = { glyph: "", sets: {} };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+// the dial is a SLEEP TIMER now: wind minutes; zero = suspend (mocked)
+function tameTimer(host) {
+ const svg = host.querySelector("svg");
+ const NS = "http://www.w3.org/2000/svg";
+ const defs = document.createElementNS(NS, "defs");
+ defs.innerHTML = `<radialGradient id="p21Steel" cx="38%" cy="32%" r="80%">
+ <stop offset="0" stop-color="#c9c4b8"/><stop offset="1" stop-color="#6e685c"/>
+ </radialGradient>`;
+ svg.insertBefore(defs, svg.firstChild);
+ svg.querySelectorAll('[fill="url(#mtRed)"]').forEach((n) => n.setAttribute("fill", "url(#p21Steel)"));
+ [...svg.querySelectorAll("text")].forEach((t) => {
+ if (t.textContent === "TURN TO START") t.remove();
+ if (t.textContent === "PUSH TO STOP") t.textContent = "SLEEP TIMER · PUSH STOPS";
+ if (t.textContent === "MINUTES") t.textContent = "SLEEP IN";
+ if (t.textContent === "OFF") t.setAttribute("fill", "var(--gold-hi)");
+ });
+ [...svg.querySelectorAll("line")].forEach((l) => {
+ if (l.getAttribute("stroke") === "#e0523a") l.setAttribute("stroke", "var(--gold)");
+ });
+ svg.querySelectorAll('[stroke="#5c150c"]').forEach((n) => n.setAttribute("stroke", "#3c382f"));
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+}
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Program routing"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", scene: "Focus" },
+ { id: "PRESENT", label: "PRESENT", scene: "Presentation" },
+ { id: "BATTERY", label: "BATTERY", scene: "Battery" },
+ { id: "NIGHT", label: "NIGHT", scene: "Night" },
+ { id: "S5", label: "", scene: "Slot5" },
+ { id: "S6", label: "", scene: "Slot6" },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label }) => ({ id, label }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ groups: [SWITCH_ROWS.length, 1],
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 508,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — drums · groove · pointing · groove · dial ----
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Pointing · Sleep"));
+ const bench = h("div", "bench");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 132,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ bench.appendChild(dcell);
+
+ bench.appendChild(h("div", "groove"));
+
+ const rocks = h("div", "rockstack");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ rocks.appendChild(host);
+ });
+ bench.appendChild(rocks);
+
+ bench.appendChild(h("div", "groove"));
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: 0,
+ onChange: (min) => {
+ const was = sleepMinutes;
+ sleepMinutes = min;
+ if (booting) return;
+ if (was > 0.5 && min === 0) DS.toast("Sleep timer expired — SUSPEND (mock)", true);
+ },
+ });
+ scaleSvg(timerHost, 132);
+ tameTimer(timerHost);
+ }
+ tcell.appendChild(timerHost);
+ bench.appendChild(tcell);
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-28.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-28.html
new file mode 100644
index 0000000..769066e
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-28.html
@@ -0,0 +1,296 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 28 (the idle tripper rail)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .ds-panel { width: 560px; }
+ .ds-caption { max-width: 540px; }
+ .bench { display: flex; align-items: center; justify-content: center; gap: 14px; }
+ .groove { width: 2px; align-self: stretch; margin: 4px 0;
+ background: linear-gradient(180deg, transparent, #060505 15%, #060505 85%, transparent);
+ box-shadow: 1px 0 0 #2a2724; }
+ .rockstack { display: flex; flex-direction: column; gap: 10px; align-items: center; }
+ .rockstack .dupre-rocker { width: 56px; height: 34px; }
+ .rockstack .dupre-rocker .dupre-half { font-size: 8px; }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 28 — the idle tripper rail.</b> The dial is retired: the
+ panel programs futures, so idleness gets a time-switch tripper rail —
+ drag the DIM / LOCK / WATCH / SCREEN OFF / SUSPEND tabs along the
+ idle-minutes scale; slide one into the OFF siding to disable it.
+ Engage CAFFEINE and the whole policy dims — bypassed.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "\u2013", "\u2193"]; // up / dash / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "\u2013", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "\u2013": "balanced", "\u2193": "saver" };
+let booting = true;
+// the idle policy the rail programs (mock persistence across renders)
+const IDLE_STAGES = [
+ { id: "dim", label: "DIM", minutes: 5 },
+ { id: "lock", label: "LOCK", minutes: 7 },
+ { id: "watch", label: "WATCH", minutes: 8 },
+ { id: "dpms", label: "SCREEN OFF", minutes: 10 },
+ { id: "suspend", label: "SUSPEND", minutes: 30 },
+];
+
+// two unassigned program slots (blank keys, all-off programs)
+if (!DS.SCENES.Slot5) DS.SCENES.Slot5 = { glyph: "", sets: {} };
+if (!DS.SCENES.Slot6) DS.SCENES.Slot6 = { glyph: "", sets: {} };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Program routing"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", scene: "Focus" },
+ { id: "PRESENT", label: "PRESENT", scene: "Presentation" },
+ { id: "BATTERY", label: "BATTERY", scene: "Battery" },
+ { id: "NIGHT", label: "NIGHT", scene: "Night" },
+ { id: "S5", label: "", scene: "Slot5" },
+ { id: "S6", label: "", scene: "Slot6" },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label }) => ({ id, label }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ groups: [SWITCH_ROWS.length, 1],
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 508,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — drums · groove · pointing · groove · dial ----
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Pointing"));
+ const bench = h("div", "bench");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 132,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ bench.appendChild(dcell);
+
+ bench.appendChild(h("div", "groove"));
+
+ const rocks = h("div", "rockstack");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ rocks.appendChild(host);
+ });
+ bench.appendChild(rocks);
+
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- plate 3: the idle program — the tripper rail -----------------------
+ const ip = h("div", "plate");
+ ip.appendChild(DS.engrave("Idle program"));
+ const railHost = h("div", "khost");
+ const rail = DUPRE.tripRail(railHost, {
+ stages: IDLE_STAGES.map((st) => ({ ...st })),
+ max: 60,
+ width: 508,
+ onChange: (id, minutes, snapshot) => {
+ snapshot.forEach((st) => { const m = IDLE_STAGES.find((o) => o.id === st.id); if (m) m.minutes = st.minutes; });
+ if (booting || !id) return;
+ const st = IDLE_STAGES.find((o) => o.id === id);
+ DS.toast(st.label + (minutes == null ? " stage disabled" : " → idle " + Math.round(minutes) + "m"), true);
+ },
+ });
+ rail.setBypassed(DS.state.caffeine);
+ ip.appendChild(railHost);
+ col.appendChild(ip);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-29.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-29.html
new file mode 100644
index 0000000..9a05b81
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-29.html
@@ -0,0 +1,297 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 29 (the idle tripper dial)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .ds-panel { width: 560px; }
+ .ds-caption { max-width: 540px; }
+ .bench { display: flex; align-items: center; justify-content: center; gap: 14px; }
+ .groove { width: 2px; align-self: stretch; margin: 4px 0;
+ background: linear-gradient(180deg, transparent, #060505 15%, #060505 85%, transparent);
+ box-shadow: 1px 0 0 #2a2724; }
+ .rockstack { display: flex; flex-direction: column; gap: 10px; align-items: center; }
+ .rockstack .dupre-rocker { width: 56px; height: 34px; }
+ .rockstack .dupre-rocker .dupre-half { font-size: 8px; }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 29 — the idle tripper dial.</b> Craig's actual mental
+ image: the round segment-timer with four COLORED tripper tabs on the
+ rim — DIM · LOCK · SCREEN OFF · SUSPEND (the night watch is the lock
+ screen's face, not its own stage). Drag a tab around the idle scale;
+ drop it through the bottom OFF notch to disable. Side legend maps
+ color → stage → minutes. CAFFEINE still bypasses the lot.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "\u2013", "\u2193"]; // up / dash / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "\u2013", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "\u2013": "balanced", "\u2193": "saver" };
+let booting = true;
+// the idle policy the rail programs (mock persistence across renders)
+const IDLE_STAGES = [
+ { id: "dim", label: "DIM", color: "#d29638", minutes: 5 },
+ { id: "lock", label: "LOCK · WATCH", color: "#54677d", minutes: 7 },
+ { id: "dpms", label: "SCREEN OFF", color: "#969385", minutes: 10 },
+ { id: "suspend", label: "SUSPEND", color: "#cb6b4d", minutes: 30 },
+];
+
+// two unassigned program slots (blank keys, all-off programs)
+if (!DS.SCENES.Slot5) DS.SCENES.Slot5 = { glyph: "", sets: {} };
+if (!DS.SCENES.Slot6) DS.SCENES.Slot6 = { glyph: "", sets: {} };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Program routing"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", scene: "Focus" },
+ { id: "PRESENT", label: "PRESENT", scene: "Presentation" },
+ { id: "BATTERY", label: "BATTERY", scene: "Battery" },
+ { id: "NIGHT", label: "NIGHT", scene: "Night" },
+ { id: "S5", label: "", scene: "Slot5" },
+ { id: "S6", label: "", scene: "Slot6" },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label }) => ({ id, label }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ groups: [SWITCH_ROWS.length, 1],
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 508,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — drums · groove · pointing · groove · dial ----
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Pointing · Idle"));
+ const bench = h("div", "bench");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 132,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ bench.appendChild(dcell);
+
+ bench.appendChild(h("div", "groove"));
+
+ const rocks = h("div", "rockstack");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ rocks.appendChild(host);
+ });
+ bench.appendChild(rocks);
+
+ bench.appendChild(h("div", "groove"));
+
+ const icell = h("div", "vcell");
+ const railHost = h("div", "khost");
+ const rail = DUPRE.tripDial(railHost, {
+ stages: IDLE_STAGES.map((st) => ({ ...st })),
+ max: 60,
+ size: 138,
+ legend: true,
+ onChange: (id, minutes, snapshot) => {
+ snapshot.forEach((st) => { const m = IDLE_STAGES.find((o) => o.id === st.id); if (m) m.minutes = st.minutes; });
+ if (booting || !id) return;
+ const st = IDLE_STAGES.find((o) => o.id === id);
+ DS.toast(st.label + (minutes == null ? " stage disabled" : " → idle " + Math.round(minutes) + "m"), true);
+ },
+ });
+ rail.setBypassed(DS.state.caffeine);
+ icell.appendChild(railHost);
+ bench.appendChild(icell);
+
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-3.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-3.html
index 8c383f1..49661e4 100644
--- a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-3.html
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-3.html
@@ -93,7 +93,7 @@ function render() {
col.appendChild(mh);
const grid2 = DS.h("div", "ds-grid2");
- ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd"].forEach((key) =>
+ ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd", "weathergeo"].forEach((key) =>
grid2.appendChild(DS.toggleTile(key)));
if (DS.state.laptop) grid2.appendChild(DS.toggleTile("airplane"));
col.appendChild(grid2);
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-30.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-30.html
new file mode 100644
index 0000000..5081165
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-30.html
@@ -0,0 +1,310 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 30 (family chrome)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#dab53d; --gold-hi:#ffd75f;
+ --amber-grad-top:#f0d879; --amber-grad-mid:#dab53d; --amber-grad-bot:#8f7a1f;
+ --amber-edge:#7d6a16; --amber-warn:#edc95d;
+ --glow-hi:255,215,95; --glow-lo:218,181,61;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ body.ds-stage {
+ background: radial-gradient(1200px 600px at 70% -10%, #1c1915 0%, transparent 60%), var(--ground);
+ }
+ .ds-panel { width: 540px; background-color: var(--panel); border-width: 1.6px; }
+ .ds-face {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #262320;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
+ }
+ .ds-face .title { color: var(--cream); letter-spacing: .22em; }
+ .ds-caption { max-width: 520px; }
+ .bench { display: flex; align-items: center; justify-content: center; gap: 8px; }
+ .groove { width: 2px; align-self: stretch; margin: 4px 0;
+ background: linear-gradient(180deg, transparent, #060505 15%, #060505 85%, transparent);
+ box-shadow: 1px 0 0 #2a2724; }
+ .rockstack { display: flex; flex-direction: column; gap: 10px; align-items: center; }
+ .rockstack .dupre-rocker { width: 50px; height: 32px; }
+ .rockstack .dupre-rocker .dupre-half { font-size: 8px; }
+ .plate {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #2c2823;
+ border-radius: 11px;
+ padding: 10px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 2px 6px rgba(0,0,0,.35);
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 30 — family chrome.</b> Aligned with the maintenance /
+ net / bt family: theme gold (#dab53d), raised gradient faceplates with
+ inset highlights, maint-style header. Slots labeled 1 · 2 (slim keys);
+ factory presets programmed; PROGRAMMABLE PRESETS on its own
+ high-contrast plate, no divider; dial + drums enlarged, dial numerals
+ lighter.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "\u2013", "\u2193"]; // up / dash / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "\u2013", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "\u2013": "balanced", "\u2193": "saver" };
+let booting = true;
+// the idle policy the rail programs (mock persistence across renders)
+const IDLE_STAGES = [
+ { id: "dim", label: "DIM", color: "#d29638", minutes: 5 },
+ { id: "lock", label: "LOCK · WATCH", color: "#54677d", minutes: 7 },
+ { id: "dpms", label: "SCREEN OFF", color: "#969385", minutes: 10 },
+ { id: "suspend", label: "SUSPEND", color: "#cb6b4d", minutes: 30 },
+];
+
+// two unassigned program slots (blank keys, all-off programs)
+if (!DS.SCENES.Slot5) DS.SCENES.Slot5 = { glyph: "", sets: {} };
+if (!DS.SCENES.Slot6) DS.SCENES.Slot6 = { glyph: "", sets: {} };
+
+// factory presets (Craig, 2026-07-22) — rows: AUTO-DIM, NIGHT LIGHT, DND, CAFFEINE + CPU
+DS.SCENES.Focus.sets = { autodim: true, nightlight: false, dnd: true, caffeine: true, powerprofile: "performance" };
+DS.SCENES.Presentation.sets = { autodim: false, nightlight: false, dnd: true, caffeine: true, powerprofile: "balanced" };
+DS.SCENES.Battery.sets = { autodim: true, nightlight: false, dnd: false, caffeine: false, powerprofile: "saver" };
+DS.SCENES.Night.sets = { autodim: true, nightlight: true, dnd: false, caffeine: true, powerprofile: "saver" };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Programmable presets"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", scene: "Focus" },
+ { id: "PRESENT", label: "PRESENT", scene: "Presentation" },
+ { id: "BATTERY", label: "BATTERY", scene: "Battery" },
+ { id: "NIGHT", label: "NIGHT", scene: "Night" },
+ { id: "S5", label: "1", scene: "Slot5", slim: true },
+ { id: "S6", label: "2", scene: "Slot6", slim: true },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label, slim }) => ({ id, label, slim }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 486,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — drums · groove · pointing · groove · dial ----
+ const lv = h("div", "plate");
+ lv.appendChild(DS.engrave("Levels · Pointing · Idle"));
+ const bench = h("div", "bench");
+
+ const dcell = h("div", "vcell");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 176,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ bench.appendChild(dcell);
+
+ bench.appendChild(h("div", "groove"));
+
+ const rocks = h("div", "rockstack");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onLabel: name,
+ offLabel: "",
+ quiet: true,
+ accent: "green",
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, host); },
+ });
+ rocks.appendChild(host);
+ });
+ bench.appendChild(rocks);
+
+ bench.appendChild(h("div", "groove"));
+
+ const icell = h("div", "vcell");
+ const railHost = h("div", "khost");
+ const rail = DUPRE.tripDial(railHost, {
+ stages: IDLE_STAGES.map((st) => ({ ...st })),
+ max: 60,
+ size: 148,
+ legend: true,
+ onChange: (id, minutes, snapshot) => {
+ snapshot.forEach((st) => { const m = IDLE_STAGES.find((o) => o.id === st.id); if (m) m.minutes = st.minutes; });
+ if (booting || !id) return;
+ const st = IDLE_STAGES.find((o) => o.id === id);
+ DS.toast(st.label + (minutes == null ? " stage disabled" : " → idle " + Math.round(minutes) + "m"), true);
+ },
+ });
+ rail.setBypassed(DS.state.caffeine);
+ icell.appendChild(railHost);
+ bench.appendChild(icell);
+
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-31.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-31.html
new file mode 100644
index 0000000..9d9a0c1
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-31.html
@@ -0,0 +1,319 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 31 (encircled groups)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#dab53d; --gold-hi:#ffd75f;
+ --amber-grad-top:#f0d879; --amber-grad-mid:#dab53d; --amber-grad-bot:#8f7a1f;
+ --amber-edge:#7d6a16; --amber-warn:#edc95d;
+ --glow-hi:255,215,95; --glow-lo:218,181,61;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ body.ds-stage {
+ background: radial-gradient(1200px 600px at 70% -10%, #1c1915 0%, transparent 60%), var(--ground);
+ }
+ .ds-panel { width: 540px; background-color: var(--panel); border-width: 1.6px; }
+ .ds-face {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #262320;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
+ }
+ .ds-face .title { color: var(--cream); letter-spacing: .22em; }
+ .ds-caption { max-width: 520px; }
+ .bench { display: flex; align-items: stretch; justify-content: center; gap: 12px; }
+ .fcol { display: flex; flex-direction: column; gap: 12px; }
+ .fbox {
+ position: relative;
+ border: 1px solid #35302a;
+ border-radius: 10px;
+ padding: 14px 12px 10px;
+ display: flex; align-items: center; justify-content: center; gap: 10px;
+ }
+ .fbox .flbl {
+ position: absolute; top: -7px; left: 12px;
+ background: var(--raise);
+ padding: 0 7px;
+ color: var(--steel); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
+ }
+ .fbox .dupre-key.ptr { width: 78px; height: 24px; font-size: 8px; letter-spacing: .08em; padding: 0; }
+ .plate {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #2c2823;
+ border-radius: 11px;
+ padding: 10px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 2px 6px rgba(0,0,0,.35);
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 31 — encircled groups.</b> TPAD and MOUSE become console
+ keys (lighting green), horizontal above the dial; the bench reorganizes
+ into three bordered instrument groups with their labels set into the
+ border: BRIGHTNESS · POINTERS · IDLE. Dial unchanged, drums grown to
+ proportion.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "\u2013", "\u2193"]; // up / dash / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "\u2013", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "\u2013": "balanced", "\u2193": "saver" };
+let booting = true;
+// the idle policy the rail programs (mock persistence across renders)
+const IDLE_STAGES = [
+ { id: "dim", label: "DIM", color: "#d29638", minutes: 5 },
+ { id: "lock", label: "LOCK · WATCH", color: "#54677d", minutes: 7 },
+ { id: "dpms", label: "SCREEN OFF", color: "#969385", minutes: 10 },
+ { id: "suspend", label: "SUSPEND", color: "#cb6b4d", minutes: 30 },
+];
+
+// two unassigned program slots (blank keys, all-off programs)
+if (!DS.SCENES.Slot5) DS.SCENES.Slot5 = { glyph: "", sets: {} };
+if (!DS.SCENES.Slot6) DS.SCENES.Slot6 = { glyph: "", sets: {} };
+
+// factory presets (Craig, 2026-07-22) — rows: AUTO-DIM, NIGHT LIGHT, DND, CAFFEINE + CPU
+DS.SCENES.Focus.sets = { autodim: true, nightlight: false, dnd: true, caffeine: true, powerprofile: "performance" };
+DS.SCENES.Presentation.sets = { autodim: false, nightlight: false, dnd: true, caffeine: true, powerprofile: "balanced" };
+DS.SCENES.Battery.sets = { autodim: true, nightlight: false, dnd: false, caffeine: false, powerprofile: "saver" };
+DS.SCENES.Night.sets = { autodim: true, nightlight: true, dnd: false, caffeine: true, powerprofile: "saver" };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Programmable presets"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", scene: "Focus" },
+ { id: "PRESENT", label: "PRESENT", scene: "Presentation" },
+ { id: "BATTERY", label: "BATTERY", scene: "Battery" },
+ { id: "NIGHT", label: "NIGHT", scene: "Night" },
+ { id: "S5", label: "1", scene: "Slot5", slim: true },
+ { id: "S6", label: "2", scene: "Slot6", slim: true },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label, slim }) => ({ id, label, slim }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 486,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — three encircled instrument groups -------------
+ const lv = h("div", "plate");
+ const bench = h("div", "bench");
+
+ const fbox = (label) => {
+ const b = h("div", "fbox");
+ b.appendChild(h("span", "flbl", label));
+ return b;
+ };
+
+ // BRIGHTNESS — the drum pair
+ const bright = fbox("Brightness");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, bright);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, bright);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 204,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ bright.appendChild(dhost);
+ bench.appendChild(bright);
+
+ const rcol = h("div", "fcol");
+
+ // POINTERS — console keys, horizontal
+ const ptr = fbox("Pointers");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const b = h("button", "dupre-key ptr", name);
+ if (DS.state[k]) b.classList.add("dupre-green");
+ b.onclick = () => { b.blur(); DS.setToggle(k, ptr); };
+ ptr.appendChild(b);
+ });
+ rcol.appendChild(ptr);
+
+ // IDLE — the tripper dial (size unchanged)
+ const idle = fbox("Idle");
+ const railHost = h("div", "khost");
+ const rail = DUPRE.tripDial(railHost, {
+ stages: IDLE_STAGES.map((st) => ({ ...st })),
+ max: 60,
+ size: 148,
+ legend: true,
+ onChange: (id, minutes, snapshot) => {
+ snapshot.forEach((st) => { const m = IDLE_STAGES.find((o) => o.id === st.id); if (m) m.minutes = st.minutes; });
+ if (booting || !id) return;
+ const st = IDLE_STAGES.find((o) => o.id === id);
+ DS.toast(st.label + (minutes == null ? " stage disabled" : " → idle " + Math.round(minutes) + "m"), true);
+ },
+ });
+ rail.setBypassed(DS.state.caffeine);
+ idle.appendChild(railHost);
+ rcol.appendChild(idle);
+
+ bench.appendChild(rcol);
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-32.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-32.html
new file mode 100644
index 0000000..bc241de
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-32.html
@@ -0,0 +1,548 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 32 (the wallpaper channels)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#dab53d; --gold-hi:#ffd75f;
+ --amber-grad-top:#f0d879; --amber-grad-mid:#dab53d; --amber-grad-bot:#8f7a1f;
+ --amber-edge:#7d6a16; --amber-warn:#edc95d;
+ --glow-hi:255,215,95; --glow-lo:218,181,61;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ body.ds-stage {
+ background: radial-gradient(1200px 600px at 70% -10%, #1c1915 0%, transparent 60%), var(--ground);
+ }
+ .ds-panel { width: 540px; background-color: var(--panel); border-width: 1.6px; }
+ .ds-face {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #262320;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
+ }
+ .ds-face .title { color: var(--cream); letter-spacing: .22em; }
+ .ds-caption { max-width: 520px; }
+ .bench { display: flex; align-items: stretch; justify-content: center; gap: 12px; }
+ .fcol { display: flex; flex-direction: column; gap: 12px; }
+ .fbox {
+ position: relative;
+ border: 1px solid #35302a;
+ border-radius: 10px;
+ padding: 14px 12px 10px;
+ display: flex; align-items: center; justify-content: center; gap: 10px;
+ }
+ .fbox .flbl {
+ position: absolute; top: -7px; left: 12px;
+ background: var(--raise);
+ padding: 0 7px;
+ color: var(--steel); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
+ }
+ .fbox .dupre-key.ptr { width: 78px; height: 24px; font-size: 8px; letter-spacing: .08em; padding: 0; }
+ .plate {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #2c2823;
+ border-radius: 11px;
+ padding: 10px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 2px 6px rgba(0,0,0,.35);
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ /* ---- wallpaper channel bank ---- */
+ .wp-chans { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
+ .wp-chan {
+ border: 1px solid #33302b; border-radius: 8px; padding: 4px;
+ background: linear-gradient(180deg, var(--key-top, #23211e), var(--key-bot, #191715));
+ cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 3px;
+ }
+ .wp-chan:hover { border-color: var(--gold); }
+ .wp-chan.on { border-color: #a9c95f; box-shadow: 0 0 8px rgba(169,201,95,.25); }
+ .wp-chan .face { width: 100%; aspect-ratio: 16/10; border-radius: 4px; overflow: hidden; position: relative; background: #0c0a08; }
+ .wp-chan .nm { font-size: 7px; letter-spacing: .08em; color: var(--dim); text-transform: uppercase; }
+ .wp-chan.on .nm { color: #a9c95f; }
+ .half { position: absolute; inset: 0; }
+ .half.b { clip-path: polygon(100% 0, 100% 100%, 0 100%); }
+ .deck { position: absolute; inset: 0; }
+ .deck i { position: absolute; width: 68%; height: 68%; border-radius: 3px; border: 1px solid #0009; }
+ .mini-clocks { position: absolute; inset: 2px; display: grid; grid-template-columns: repeat(3,1fr); gap: 1px; }
+ .mini-clocks i { border-radius: 50%; border: 1px solid #4a4438; position: relative; background: #14110e; }
+ .mini-clocks i::after { content: ""; position: absolute; left: 50%; top: 18%; width: 1px; height: 34%; background: var(--gold); transform-origin: bottom; }
+ .mini-clocks i:nth-child(2)::after { transform: rotate(65deg); }
+ .mini-clocks i:nth-child(3)::after { transform: rotate(140deg); }
+ .mini-clocks i:nth-child(4)::after { transform: rotate(-50deg); }
+ .mini-clocks i:nth-child(5)::after { transform: rotate(110deg); }
+ .mini-clocks i:nth-child(6)::after { transform: rotate(-120deg); }
+ .clock-face { position: absolute; inset: 8% 25%; border-radius: 50%; border: 1.5px solid var(--cream); background: #14110e; }
+ .clock-face::before { content: ""; position: absolute; left: 50%; top: 12%; width: 1.5px; height: 40%; background: var(--cream); transform: rotate(40deg); transform-origin: bottom; }
+ .clock-face::after { content: ""; position: absolute; left: 50%; top: 22%; width: 1.5px; height: 30%; background: var(--gold); transform: rotate(-70deg); transform-origin: bottom; }
+ .watch-face { position: absolute; inset: 6% 22%; border-radius: 50%; border: 1px solid #3a5c2a; background: radial-gradient(circle, #0f1a0c, #060905); }
+ .watch-face::after { content: ""; position: absolute; left: 50%; top: 6%; width: 1px; height: 44%; background: #a9c95f; transform: rotate(55deg); transform-origin: bottom; box-shadow: 0 0 4px #a9c95f; }
+ .vid-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #fffc; font-size: 14px; text-shadow: 0 1px 3px #000; }
+
+ .wp-conf { margin-top: 10px; }
+ .wp-lib { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
+ .wp-th { aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .wp-th:hover { border-color: var(--gold); }
+ .wp-th.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .pair-bench { display: flex; align-items: center; gap: 10px; justify-content: center; }
+ .pair-slot { width: 104px; aspect-ratio: 16/10; border: 1.5px dashed #4a443a; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-slot.filled { border-style: solid; }
+ .pair-slot.arm { border-color: var(--gold); }
+ .pair-slot .cap { position: absolute; bottom: 2px; left: 4px; font-size: 7px; letter-spacing: .12em; color: #fff9; text-shadow: 0 1px 2px #000; }
+ .pair-tray { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
+ .pair-chip { width: 64px; aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-chip.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .wp-note { color: var(--dim); font-size: 10px; margin-top: 6px; }
+ .wp-row2 { display: flex; align-items: center; gap: 10px; justify-content: center; margin-top: 8px; }
+ .int-wheel {
+ width: 46px; height: 30px; border-radius: 4px; background: #0a0908; border: 1px solid #3a352c;
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
+ color: #2b2418; font-weight: 700; font-size: 12px; font-family: var(--mono);
+ background: linear-gradient(90deg,#cfc6a8,#ece4c8 50%,#c6bd9e);
+ box-shadow: inset 0 4px 5px rgba(0,0,0,.35), inset 0 -4px 5px rgba(0,0,0,.35);
+ }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 32 — the wallpaper channels.</b> The sub-view is a channel
+ bank: seven mode cards (night watch · sun pair · single · random ·
+ video · clock · world), each wearing its own face — slashed thumbs,
+ deck fans, mini clock matrices. The active channel lights green; the
+ config surface below switches per channel. Pairs are MINTED: fill the
+ day and night frames, confirm, and the slashed chip joins the tray
+ forever.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "\u2013", "\u2193"]; // up / dash / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "\u2013", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "\u2013": "balanced", "\u2193": "saver" };
+let booting = true;
+// the idle policy the rail programs (mock persistence across renders)
+const IDLE_STAGES = [
+ { id: "dim", label: "DIM", color: "#d29638", minutes: 5 },
+ { id: "lock", label: "LOCK · WATCH", color: "#54677d", minutes: 7 },
+ { id: "dpms", label: "SCREEN OFF", color: "#969385", minutes: 10 },
+ { id: "suspend", label: "SUSPEND", color: "#cb6b4d", minutes: 30 },
+];
+
+// two unassigned program slots (blank keys, all-off programs)
+if (!DS.SCENES.Slot5) DS.SCENES.Slot5 = { glyph: "", sets: {} };
+if (!DS.SCENES.Slot6) DS.SCENES.Slot6 = { glyph: "", sets: {} };
+
+// factory presets (Craig, 2026-07-22) — rows: AUTO-DIM, NIGHT LIGHT, DND, CAFFEINE + CPU
+DS.SCENES.Focus.sets = { autodim: true, nightlight: false, dnd: true, caffeine: true, powerprofile: "performance" };
+DS.SCENES.Presentation.sets = { autodim: false, nightlight: false, dnd: true, caffeine: true, powerprofile: "balanced" };
+DS.SCENES.Battery.sets = { autodim: true, nightlight: false, dnd: false, caffeine: false, powerprofile: "saver" };
+DS.SCENES.Night.sets = { autodim: true, nightlight: true, dnd: false, caffeine: true, powerprofile: "saver" };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+// ---- wallpaper: channels state (mock persistence) -------------------------
+const SWATCH = [
+ "linear-gradient(135deg,#2a3d5c,#101b2e)", "linear-gradient(135deg,#5c3a2a,#2e1810)",
+ "linear-gradient(135deg,#3a5c2a,#16240f)", "linear-gradient(135deg,#40364f,#1b1626)",
+ "linear-gradient(135deg,#1a1e2e,#05070d)", "linear-gradient(135deg,#5c5030,#2e2810)",
+ "linear-gradient(135deg,#6b4a5c,#2e1420)", "linear-gradient(135deg,#2a5c55,#0f2420)",
+ "linear-gradient(135deg,#7a6248,#3a2c1a)", "linear-gradient(135deg,#23364a,#0a1018)",
+];
+const VIDEOS = ["linear-gradient(160deg,#38506b,#0d1520)", "linear-gradient(160deg,#5c4a2a,#241a08)", "linear-gradient(160deg,#2c4a3e,#0a1a12)"];
+const WP = {
+ channel: "single",
+ single: 2,
+ pairs: [{ day: 1, night: 4 }],
+ pairSel: 0,
+ video: 0,
+ interval: 30,
+ building: { day: null, night: null, arm: null },
+};
+const INTERVALS = [5, 10, 15, 30, 60];
+const CHANNELS = [
+ { id: "watch", nm: "Watch" },
+ { id: "pair", nm: "Sun pair" },
+ { id: "single", nm: "Single" },
+ { id: "random", nm: "Random" },
+ { id: "video", nm: "Video" },
+ { id: "clock", nm: "Clock" },
+ { id: "world", nm: "World" },
+];
+
+function chanFace(id) {
+ const f = h("div", "face");
+ if (id === "single") { const i = h("div", "half"); i.style.background = SWATCH[WP.single]; f.appendChild(i); }
+ else if (id === "pair") {
+ const p = WP.pairs[WP.pairSel] || WP.pairs[0];
+ const a = h("div", "half"); a.style.background = p ? SWATCH[p.day] : "#222";
+ const b = h("div", "half b"); b.style.background = p ? SWATCH[p.night] : "#111";
+ f.appendChild(a); f.appendChild(b);
+ } else if (id === "random") {
+ const d = h("div", "deck");
+ [[8, "18%", "4%", "-6deg"], [4, "10%", "16%", "3deg"], [0, "2%", "28%", "-2deg"]].forEach(([si, top, left, rot]) => {
+ const c = h("i"); c.style.cssText = `top:${top};left:${left};transform:rotate(${rot});background:${SWATCH[si]}`;
+ d.appendChild(c);
+ });
+ f.appendChild(d);
+ } else if (id === "video") {
+ const i = h("div", "half"); i.style.background = VIDEOS[WP.video]; f.appendChild(i);
+ f.appendChild(h("div", "vid-play", "\u25B6"));
+ } else if (id === "clock") f.appendChild(h("div", "clock-face"));
+ else if (id === "world") {
+ const m = h("div", "mini-clocks");
+ for (let i = 0; i < 6; i++) { const c = h("i"); c.style.setProperty("--r", (i * 55) + "deg"); c.querySelector; m.appendChild(c); }
+ f.appendChild(m);
+ } else if (id === "watch") f.appendChild(h("div", "watch-face"));
+ return f;
+}
+
+function libGrid(sel, onPick) {
+ const g = h("div", "wp-lib");
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th" + (i === sel ? " sel" : ""));
+ t.style.background = sw;
+ t.onclick = () => onPick(i);
+ g.appendChild(t);
+ });
+ return g;
+}
+
+function wallpaperView() {
+ const sv = h("div", "ds-col");
+ const head = h("div", "ds-sub-head");
+ head.innerHTML = `<span class="back">\u2039 Back</span><span class="title">Wallpaper</span>`;
+ head.querySelector(".back").onclick = () => subview.classList.remove("open");
+ sv.appendChild(head);
+
+ const paint = () => {
+ body2.innerHTML = "";
+
+ // channel bank
+ const bank = h("div", "wp-chans");
+ CHANNELS.forEach((c) => {
+ const card = h("div", "wp-chan" + (WP.channel === c.id ? " on" : ""));
+ card.appendChild(chanFace(c.id));
+ card.appendChild(h("div", "nm", c.nm));
+ card.onclick = () => { WP.channel = c.id; DS.toast("Wallpaper: " + c.nm, true); paint(); };
+ bank.appendChild(card);
+ });
+ body2.appendChild(bank);
+
+ const conf = h("div", "wp-conf");
+ if (WP.channel === "single") {
+ conf.appendChild(DS.engrave("Library"));
+ conf.appendChild(libGrid(WP.single, (i) => { WP.single = i; DS.toast("Wallpaper set", true); paint(); }));
+ } else if (WP.channel === "pair") {
+ conf.appendChild(DS.engrave("Pair press — fill both, confirm to mint"));
+ const bench = h("div", "pair-bench");
+ ["day", "night"].forEach((k) => {
+ const slot = h("div", "pair-slot" +
+ (WP.building[k] !== null ? " filled" : "") + (WP.building.arm === k ? " arm" : ""));
+ if (WP.building[k] !== null) slot.style.background = SWATCH[WP.building[k]];
+ slot.appendChild(h("span", "cap", k === "day" ? "\u2600 DAY" : "\u263E NIGHT"));
+ slot.onclick = () => { WP.building.arm = WP.building.arm === k ? null : k; paint(); };
+ bench.appendChild(slot);
+ });
+ const mint = h("button", "dupre-key", "MINT PAIR");
+ mint.style.cssText = "height:26px;font-size:8px;padding:0 10px";
+ mint.onclick = () => {
+ if (WP.building.day === null || WP.building.night === null) { DS.toast("fill both frames first", false); return; }
+ WP.pairs.push({ day: WP.building.day, night: WP.building.night });
+ WP.pairSel = WP.pairs.length - 1;
+ WP.building = { day: null, night: null, arm: null };
+ DS.toast("Pair minted", true);
+ paint();
+ };
+ bench.appendChild(mint);
+ conf.appendChild(bench);
+ if (WP.building.arm) {
+ conf.appendChild(h("div", "wp-note", "pick the " + WP.building.arm.toUpperCase() + " picture:"));
+ conf.appendChild(libGrid(WP.building[WP.building.arm], (i) => {
+ WP.building[WP.building.arm] = i;
+ WP.building.arm = WP.building.arm === "day" && WP.building.night === null ? "night" : null;
+ paint();
+ }));
+ }
+ conf.appendChild(DS.engrave("Minted pairs"));
+ const tray = h("div", "pair-tray");
+ WP.pairs.forEach((p, i) => {
+ const chip = h("div", "pair-chip" + (i === WP.pairSel ? " sel" : ""));
+ const a = h("div", "half"); a.style.background = SWATCH[p.day];
+ const b = h("div", "half b"); b.style.background = SWATCH[p.night];
+ chip.appendChild(a); chip.appendChild(b);
+ chip.onclick = () => { WP.pairSel = i; DS.toast("Pair selected", true); paint(); };
+ tray.appendChild(chip);
+ });
+ conf.appendChild(tray);
+ conf.appendChild(h("div", "wp-note", "Swaps at local sunrise / sundown (sun-time from lat/long)."));
+ } else if (WP.channel === "random") {
+ conf.appendChild(DS.engrave("Random — draws from the whole library"));
+ const row = h("div", "wp-row2");
+ row.appendChild(h("span", "wp-note", "next draw every"));
+ const wheel = h("div", "int-wheel", WP.interval);
+ wheel.onclick = () => {
+ WP.interval = INTERVALS[(INTERVALS.indexOf(WP.interval) + 1) % INTERVALS.length];
+ DS.toast("Random interval: " + WP.interval + "m", true);
+ paint();
+ };
+ row.appendChild(wheel);
+ row.appendChild(h("span", "wp-note", "minutes"));
+ conf.appendChild(row);
+ } else if (WP.channel === "video") {
+ conf.appendChild(DS.engrave("Videos"));
+ const g = h("div", "wp-lib");
+ VIDEOS.forEach((v, i) => {
+ const t = h("div", "wp-th" + (i === WP.video ? " sel" : ""));
+ t.style.background = v;
+ t.appendChild(h("div", "vid-play", "\u25B6"));
+ t.onclick = () => { WP.video = i; DS.toast("Video set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(g);
+ } else {
+ conf.appendChild(h("div", "wp-note",
+ WP.channel === "watch" ? "The night watch plays as the wallpaper. No options."
+ : WP.channel === "clock" ? "The custom clock fills the screen. No options."
+ : "A matrix of world clocks fills the screen. No options."));
+ }
+ body2.appendChild(conf);
+ };
+
+ const body2 = h("div", "ds-col");
+ sv.appendChild(body2);
+ paint();
+ return sv;
+}
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(wallpaperView());
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Programmable presets"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", scene: "Focus" },
+ { id: "PRESENT", label: "PRESENT", scene: "Presentation" },
+ { id: "BATTERY", label: "BATTERY", scene: "Battery" },
+ { id: "NIGHT", label: "NIGHT", scene: "Night" },
+ { id: "S5", label: "1", scene: "Slot5", slim: true },
+ { id: "S6", label: "2", scene: "Slot6", slim: true },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label, slim }) => ({ id, label, slim }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 486,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — three encircled instrument groups -------------
+ const lv = h("div", "plate");
+ const bench = h("div", "bench");
+
+ const fbox = (label) => {
+ const b = h("div", "fbox");
+ b.appendChild(h("span", "flbl", label));
+ return b;
+ };
+
+ // BRIGHTNESS — the drum pair
+ const bright = fbox("Brightness");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, bright);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, bright);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 204,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ bright.appendChild(dhost);
+ bench.appendChild(bright);
+
+ const rcol = h("div", "fcol");
+
+ // POINTERS — console keys, horizontal
+ const ptr = fbox("Pointers");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const b = h("button", "dupre-key ptr", name);
+ if (DS.state[k]) b.classList.add("dupre-green");
+ b.onclick = () => { b.blur(); DS.setToggle(k, ptr); };
+ ptr.appendChild(b);
+ });
+ rcol.appendChild(ptr);
+
+ // IDLE — the tripper dial (size unchanged)
+ const idle = fbox("Idle");
+ const railHost = h("div", "khost");
+ const rail = DUPRE.tripDial(railHost, {
+ stages: IDLE_STAGES.map((st) => ({ ...st })),
+ max: 60,
+ size: 148,
+ legend: true,
+ onChange: (id, minutes, snapshot) => {
+ snapshot.forEach((st) => { const m = IDLE_STAGES.find((o) => o.id === st.id); if (m) m.minutes = st.minutes; });
+ if (booting || !id) return;
+ const st = IDLE_STAGES.find((o) => o.id === id);
+ DS.toast(st.label + (minutes == null ? " stage disabled" : " → idle " + Math.round(minutes) + "m"), true);
+ },
+ });
+ rail.setBypassed(DS.state.caffeine);
+ idle.appendChild(railHost);
+ rcol.appendChild(idle);
+
+ bench.appendChild(rcol);
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-33.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-33.html
new file mode 100644
index 0000000..7dc67c8
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-33.html
@@ -0,0 +1,613 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 33 (sources + pair scrap)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#dab53d; --gold-hi:#ffd75f;
+ --amber-grad-top:#f0d879; --amber-grad-mid:#dab53d; --amber-grad-bot:#8f7a1f;
+ --amber-edge:#7d6a16; --amber-warn:#edc95d;
+ --glow-hi:255,215,95; --glow-lo:218,181,61;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ body.ds-stage {
+ background: radial-gradient(1200px 600px at 70% -10%, #1c1915 0%, transparent 60%), var(--ground);
+ }
+ .ds-panel { width: 540px; background-color: var(--panel); border-width: 1.6px; }
+ .ds-face {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #262320;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
+ }
+ .ds-face .title { color: var(--cream); letter-spacing: .22em; }
+ .ds-caption { max-width: 520px; }
+ .bench { display: flex; align-items: stretch; justify-content: center; gap: 12px; }
+ .fcol { display: flex; flex-direction: column; gap: 12px; }
+ .fbox {
+ position: relative;
+ border: 1px solid #35302a;
+ border-radius: 10px;
+ padding: 14px 12px 10px;
+ display: flex; align-items: center; justify-content: center; gap: 10px;
+ }
+ .fbox .flbl {
+ position: absolute; top: -7px; left: 12px;
+ background: var(--raise);
+ padding: 0 7px;
+ color: var(--steel); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
+ }
+ .fbox .dupre-key.ptr { width: 78px; height: 24px; font-size: 8px; letter-spacing: .08em; padding: 0; }
+ .plate {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #2c2823;
+ border-radius: 11px;
+ padding: 10px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 2px 6px rgba(0,0,0,.35);
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ /* ---- wallpaper channel bank ---- */
+ .wp-chans { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
+ .wp-chan {
+ border: 1px solid #33302b; border-radius: 8px; padding: 4px;
+ background: linear-gradient(180deg, var(--key-top, #23211e), var(--key-bot, #191715));
+ cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 3px;
+ }
+ .wp-chan:hover { border-color: var(--gold); }
+ .wp-chan.on { border-color: #a9c95f; box-shadow: 0 0 8px rgba(169,201,95,.25); }
+ .wp-chan .face { width: 100%; aspect-ratio: 16/10; border-radius: 4px; overflow: hidden; position: relative; background: #0c0a08; }
+ .wp-chan .nm { font-size: 7px; letter-spacing: .08em; color: var(--dim); text-transform: uppercase; }
+ .wp-chan.on .nm { color: #a9c95f; }
+ .half { position: absolute; inset: 0; }
+ .half.b { clip-path: polygon(100% 0, 100% 100%, 0 100%); }
+ .deck { position: absolute; inset: 0; }
+ .deck i { position: absolute; width: 68%; height: 68%; border-radius: 3px; border: 1px solid #0009; }
+ .mini-clocks { position: absolute; inset: 2px; display: grid; grid-template-columns: repeat(3,1fr); gap: 1px; }
+ .mini-clocks i { border-radius: 50%; border: 1px solid #4a4438; position: relative; background: #14110e; }
+ .mini-clocks i::after { content: ""; position: absolute; left: 50%; top: 18%; width: 1px; height: 34%; background: var(--gold); transform-origin: bottom; }
+ .mini-clocks i:nth-child(2)::after { transform: rotate(65deg); }
+ .mini-clocks i:nth-child(3)::after { transform: rotate(140deg); }
+ .mini-clocks i:nth-child(4)::after { transform: rotate(-50deg); }
+ .mini-clocks i:nth-child(5)::after { transform: rotate(110deg); }
+ .mini-clocks i:nth-child(6)::after { transform: rotate(-120deg); }
+ .clock-face { position: absolute; inset: 8% 25%; border-radius: 50%; border: 1.5px solid var(--cream); background: #14110e; }
+ .clock-face::before { content: ""; position: absolute; left: 50%; top: 12%; width: 1.5px; height: 40%; background: var(--cream); transform: rotate(40deg); transform-origin: bottom; }
+ .clock-face::after { content: ""; position: absolute; left: 50%; top: 22%; width: 1.5px; height: 30%; background: var(--gold); transform: rotate(-70deg); transform-origin: bottom; }
+ .watch-face { position: absolute; inset: 6% 22%; border-radius: 50%; border: 1px solid #3a5c2a; background: radial-gradient(circle, #0f1a0c, #060905); }
+ .watch-face::after { content: ""; position: absolute; left: 50%; top: 6%; width: 1px; height: 44%; background: #a9c95f; transform: rotate(55deg); transform-origin: bottom; box-shadow: 0 0 4px #a9c95f; }
+ .vid-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #fffc; font-size: 14px; text-shadow: 0 1px 3px #000; }
+
+ .wp-conf { margin-top: 10px; }
+ .wp-lib { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
+ .wp-th { aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .wp-th:hover { border-color: var(--gold); }
+ .wp-th.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .pair-bench { display: flex; align-items: center; gap: 10px; justify-content: center; }
+ .pair-slot { width: 104px; aspect-ratio: 16/10; border: 1.5px dashed #4a443a; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-slot.filled { border-style: solid; }
+ .pair-slot.arm { border-color: var(--gold); }
+ .pair-slot .cap { position: absolute; bottom: 2px; left: 4px; font-size: 7px; letter-spacing: .12em; color: #fff9; text-shadow: 0 1px 2px #000; }
+ .pair-tray { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
+ .pair-chip { width: 64px; aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-chip.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .wp-note { color: var(--dim); font-size: 10px; margin-top: 6px; }
+ .wp-row2 { display: flex; align-items: center; gap: 10px; justify-content: center; margin-top: 8px; }
+ .int-wheel {
+ width: 46px; height: 30px; border-radius: 4px; background: #0a0908; border: 1px solid #3a352c;
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
+ color: #2b2418; font-weight: 700; font-size: 12px; font-family: var(--mono);
+ background: linear-gradient(90deg,#cfc6a8,#ece4c8 50%,#c6bd9e);
+ box-shadow: inset 0 4px 5px rgba(0,0,0,.35), inset 0 -4px 5px rgba(0,0,0,.35);
+ }
+ .wp-dirs { display: flex; flex-direction: column; gap: 4px; }
+ .wp-dir {
+ display: flex; align-items: center; gap: 8px;
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 6px;
+ padding: 5px 8px; font-size: 11px; color: var(--silver);
+ }
+ .wp-dir .path { color: var(--dim); flex: 1; }
+ .wp-dir .rm { color: var(--dim); cursor: pointer; font-size: 12px; padding: 0 3px; }
+ .wp-dir .rm:hover { color: var(--fail); }
+ .pair-chip .rm {
+ position: absolute; top: 1px; right: 2px; color: #fffa; cursor: pointer;
+ font-size: 10px; text-shadow: 0 1px 2px #000; display: none; padding: 0 2px;
+ }
+ .pair-chip:hover .rm { display: block; }
+ .pair-chip.armed { border-color: var(--fail); box-shadow: 0 0 6px rgba(203,107,77,.5); }
+ .pair-chip.armed .rm { display: block; color: var(--fail); }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 32 — the wallpaper channels.</b> The sub-view is a channel
+ bank: seven mode cards (night watch · sun pair · single · random ·
+ video · clock · world), each wearing its own face — slashed thumbs,
+ deck fans, mini clock matrices. The active channel lights green; the
+ config surface below switches per channel. Pairs are MINTED: fill the
+ day and night frames, confirm, and the slashed chip joins the tray
+ forever.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "\u2013", "\u2193"]; // up / dash / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "\u2013", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "\u2013": "balanced", "\u2193": "saver" };
+let booting = true;
+// the idle policy the rail programs (mock persistence across renders)
+const IDLE_STAGES = [
+ { id: "dim", label: "DIM", color: "#d29638", minutes: 5 },
+ { id: "lock", label: "LOCK · WATCH", color: "#54677d", minutes: 7 },
+ { id: "dpms", label: "SCREEN OFF", color: "#969385", minutes: 10 },
+ { id: "suspend", label: "SUSPEND", color: "#cb6b4d", minutes: 30 },
+];
+
+// two unassigned program slots (blank keys, all-off programs)
+if (!DS.SCENES.Slot5) DS.SCENES.Slot5 = { glyph: "", sets: {} };
+if (!DS.SCENES.Slot6) DS.SCENES.Slot6 = { glyph: "", sets: {} };
+
+// factory presets (Craig, 2026-07-22) — rows: AUTO-DIM, NIGHT LIGHT, DND, CAFFEINE + CPU
+DS.SCENES.Focus.sets = { autodim: true, nightlight: false, dnd: true, caffeine: true, powerprofile: "performance" };
+DS.SCENES.Presentation.sets = { autodim: false, nightlight: false, dnd: true, caffeine: true, powerprofile: "balanced" };
+DS.SCENES.Battery.sets = { autodim: true, nightlight: false, dnd: false, caffeine: false, powerprofile: "saver" };
+DS.SCENES.Night.sets = { autodim: true, nightlight: true, dnd: false, caffeine: true, powerprofile: "saver" };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+// ---- wallpaper: channels state (mock persistence) -------------------------
+const SWATCH = [
+ "linear-gradient(135deg,#2a3d5c,#101b2e)", "linear-gradient(135deg,#5c3a2a,#2e1810)",
+ "linear-gradient(135deg,#3a5c2a,#16240f)", "linear-gradient(135deg,#40364f,#1b1626)",
+ "linear-gradient(135deg,#1a1e2e,#05070d)", "linear-gradient(135deg,#5c5030,#2e2810)",
+ "linear-gradient(135deg,#6b4a5c,#2e1420)", "linear-gradient(135deg,#2a5c55,#0f2420)",
+ "linear-gradient(135deg,#7a6248,#3a2c1a)", "linear-gradient(135deg,#23364a,#0a1018)",
+];
+const VIDEOS = ["linear-gradient(160deg,#38506b,#0d1520)", "linear-gradient(160deg,#5c4a2a,#241a08)", "linear-gradient(160deg,#2c4a3e,#0a1a12)"];
+const WP = {
+ channel: "single",
+ single: 2,
+ pairs: [{ day: 1, night: 4 }],
+ pairSel: 0,
+ video: 0,
+ interval: 30,
+ building: { day: null, night: null, arm: null },
+ dirs: ["~/pictures/wallpaper", "~/pictures/wallpaper/day-night"],
+ scrapArm: null,
+};
+const DIR_CANDIDATES = ["~/pictures/travel", "~/pictures/astro", "~/downloads/walls", "~/pictures/scans"];
+const INTERVALS = [5, 10, 15, 30, 60];
+const CHANNELS = [
+ { id: "watch", nm: "Watch" },
+ { id: "pair", nm: "Sun pair" },
+ { id: "single", nm: "Single" },
+ { id: "random", nm: "Random" },
+ { id: "video", nm: "Video" },
+ { id: "clock", nm: "Clock" },
+ { id: "world", nm: "World" },
+];
+
+function chanFace(id) {
+ const f = h("div", "face");
+ if (id === "single") { const i = h("div", "half"); i.style.background = SWATCH[WP.single]; f.appendChild(i); }
+ else if (id === "pair") {
+ const p = WP.pairs[WP.pairSel] || WP.pairs[0];
+ const a = h("div", "half"); a.style.background = p ? SWATCH[p.day] : "#222";
+ const b = h("div", "half b"); b.style.background = p ? SWATCH[p.night] : "#111";
+ f.appendChild(a); f.appendChild(b);
+ } else if (id === "random") {
+ const d = h("div", "deck");
+ [[8, "18%", "4%", "-6deg"], [4, "10%", "16%", "3deg"], [0, "2%", "28%", "-2deg"]].forEach(([si, top, left, rot]) => {
+ const c = h("i"); c.style.cssText = `top:${top};left:${left};transform:rotate(${rot});background:${SWATCH[si]}`;
+ d.appendChild(c);
+ });
+ f.appendChild(d);
+ } else if (id === "video") {
+ const i = h("div", "half"); i.style.background = VIDEOS[WP.video]; f.appendChild(i);
+ f.appendChild(h("div", "vid-play", "\u25B6"));
+ } else if (id === "clock") f.appendChild(h("div", "clock-face"));
+ else if (id === "world") {
+ const m = h("div", "mini-clocks");
+ for (let i = 0; i < 6; i++) { const c = h("i"); c.style.setProperty("--r", (i * 55) + "deg"); c.querySelector; m.appendChild(c); }
+ f.appendChild(m);
+ } else if (id === "watch") f.appendChild(h("div", "watch-face"));
+ return f;
+}
+
+function libGrid(sel, onPick) {
+ const g = h("div", "wp-lib");
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th" + (i === sel ? " sel" : ""));
+ t.style.background = sw;
+ t.onclick = () => onPick(i);
+ g.appendChild(t);
+ });
+ return g;
+}
+
+function wallpaperView() {
+ const sv = h("div", "ds-col");
+ const head = h("div", "ds-sub-head");
+ head.innerHTML = `<span class="back">\u2039 Back</span><span class="title">Wallpaper</span>`;
+ head.querySelector(".back").onclick = () => subview.classList.remove("open");
+ sv.appendChild(head);
+
+ const paint = () => {
+ body2.innerHTML = "";
+
+ // channel bank
+ const bank = h("div", "wp-chans");
+ CHANNELS.forEach((c) => {
+ const card = h("div", "wp-chan" + (WP.channel === c.id ? " on" : ""));
+ card.appendChild(chanFace(c.id));
+ card.appendChild(h("div", "nm", c.nm));
+ card.onclick = () => { WP.channel = c.id; DS.toast("Wallpaper: " + c.nm, true); paint(); };
+ bank.appendChild(card);
+ });
+ body2.appendChild(bank);
+
+ const conf = h("div", "wp-conf");
+ if (WP.channel === "single") {
+ conf.appendChild(DS.engrave("Library"));
+ conf.appendChild(libGrid(WP.single, (i) => { WP.single = i; DS.toast("Wallpaper set", true); paint(); }));
+ } else if (WP.channel === "pair") {
+ conf.appendChild(DS.engrave("Pair press — fill both, confirm to mint"));
+ const bench = h("div", "pair-bench");
+ ["day", "night"].forEach((k) => {
+ const slot = h("div", "pair-slot" +
+ (WP.building[k] !== null ? " filled" : "") + (WP.building.arm === k ? " arm" : ""));
+ if (WP.building[k] !== null) slot.style.background = SWATCH[WP.building[k]];
+ slot.appendChild(h("span", "cap", k === "day" ? "\u2600 DAY" : "\u263E NIGHT"));
+ slot.onclick = () => { WP.building.arm = WP.building.arm === k ? null : k; paint(); };
+ bench.appendChild(slot);
+ });
+ const mint = h("button", "dupre-key", "MINT PAIR");
+ mint.style.cssText = "height:26px;font-size:8px;padding:0 10px";
+ mint.onclick = () => {
+ if (WP.building.day === null || WP.building.night === null) { DS.toast("fill both frames first", false); return; }
+ WP.pairs.push({ day: WP.building.day, night: WP.building.night });
+ WP.pairSel = WP.pairs.length - 1;
+ WP.building = { day: null, night: null, arm: null };
+ DS.toast("Pair minted", true);
+ paint();
+ };
+ bench.appendChild(mint);
+ conf.appendChild(bench);
+ if (WP.building.arm) {
+ conf.appendChild(h("div", "wp-note", "pick the " + WP.building.arm.toUpperCase() + " picture:"));
+ conf.appendChild(libGrid(WP.building[WP.building.arm], (i) => {
+ WP.building[WP.building.arm] = i;
+ WP.building.arm = WP.building.arm === "day" && WP.building.night === null ? "night" : null;
+ paint();
+ }));
+ }
+ conf.appendChild(DS.engrave("Minted pairs"));
+ const tray = h("div", "pair-tray");
+ WP.pairs.forEach((p, i) => {
+ const chip = h("div", "pair-chip" + (i === WP.pairSel ? " sel" : "") + (WP.scrapArm === i ? " armed" : ""));
+ const a = h("div", "half"); a.style.background = SWATCH[p.day];
+ const b = h("div", "half b"); b.style.background = SWATCH[p.night];
+ chip.appendChild(a); chip.appendChild(b);
+ const rm = h("span", "rm", "\u2715");
+ rm.onclick = (ev) => {
+ ev.stopPropagation();
+ if (WP.scrapArm === i) {
+ WP.pairs.splice(i, 1);
+ WP.scrapArm = null;
+ if (WP.pairSel >= WP.pairs.length) WP.pairSel = WP.pairs.length - 1;
+ DS.toast("Pair scrapped", true);
+ } else {
+ WP.scrapArm = i;
+ DS.toast("strike \u2715 again to scrap", false);
+ }
+ paint();
+ };
+ chip.appendChild(rm);
+ chip.onclick = () => { WP.scrapArm = null; WP.pairSel = i; DS.toast("Pair selected", true); paint(); };
+ tray.appendChild(chip);
+ });
+ conf.appendChild(tray);
+ conf.appendChild(h("div", "wp-note", "Swaps at local sunrise / sundown (sun-time from lat/long)."));
+ } else if (WP.channel === "random") {
+ conf.appendChild(DS.engrave("Random — draws from the whole library"));
+ const row = h("div", "wp-row2");
+ row.appendChild(h("span", "wp-note", "next draw every"));
+ const wheel = h("div", "int-wheel", WP.interval);
+ wheel.onclick = () => {
+ WP.interval = INTERVALS[(INTERVALS.indexOf(WP.interval) + 1) % INTERVALS.length];
+ DS.toast("Random interval: " + WP.interval + "m", true);
+ paint();
+ };
+ row.appendChild(wheel);
+ row.appendChild(h("span", "wp-note", "minutes"));
+ conf.appendChild(row);
+ } else if (WP.channel === "video") {
+ conf.appendChild(DS.engrave("Videos"));
+ const g = h("div", "wp-lib");
+ VIDEOS.forEach((v, i) => {
+ const t = h("div", "wp-th" + (i === WP.video ? " sel" : ""));
+ t.style.background = v;
+ t.appendChild(h("div", "vid-play", "\u25B6"));
+ t.onclick = () => { WP.video = i; DS.toast("Video set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(g);
+ } else {
+ conf.appendChild(h("div", "wp-note",
+ WP.channel === "watch" ? "The night watch plays as the wallpaper. No options."
+ : WP.channel === "clock" ? "The custom clock fills the screen. No options."
+ : "A matrix of world clocks fills the screen. No options."));
+ }
+ body2.appendChild(conf);
+
+ // sources: the directories the library draws from (all channels)
+ const src = h("div", "wp-conf");
+ src.appendChild(DS.engrave("Sources"));
+ const dirs = h("div", "wp-dirs");
+ WP.dirs.forEach((d, i) => {
+ const row = h("div", "wp-dir");
+ row.innerHTML = `<span>\u{f024b}</span>`;
+ row.appendChild(h("span", "path", d));
+ const rm = h("span", "rm", "\u2715");
+ rm.onclick = () => {
+ if (WP.dirs.length === 1) { DS.toast("keep at least one source", false); return; }
+ WP.dirs.splice(i, 1);
+ DS.toast("Source removed", true);
+ paint();
+ };
+ row.appendChild(rm);
+ dirs.appendChild(row);
+ });
+ src.appendChild(dirs);
+ const add = h("button", "dupre-key", "ADD DIRECTORY");
+ add.style.cssText = "height:24px;font-size:8px;padding:0 10px;margin-top:6px";
+ add.onclick = () => {
+ const next = DIR_CANDIDATES.find((c) => !WP.dirs.includes(c));
+ if (!next) { DS.toast("no more candidates in the mock", false); return; }
+ WP.dirs.push(next);
+ DS.toast("Source added: " + next, true);
+ paint();
+ };
+ src.appendChild(add);
+ body2.appendChild(src);
+ };
+
+ const body2 = h("div", "ds-col");
+ sv.appendChild(body2);
+ paint();
+ return sv;
+}
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(wallpaperView());
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Programmable presets"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", scene: "Focus" },
+ { id: "PRESENT", label: "PRESENT", scene: "Presentation" },
+ { id: "BATTERY", label: "BATTERY", scene: "Battery" },
+ { id: "NIGHT", label: "NIGHT", scene: "Night" },
+ { id: "S5", label: "1", scene: "Slot5", slim: true },
+ { id: "S6", label: "2", scene: "Slot6", slim: true },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label, slim }) => ({ id, label, slim }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 486,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — three encircled instrument groups -------------
+ const lv = h("div", "plate");
+ const bench = h("div", "bench");
+
+ const fbox = (label) => {
+ const b = h("div", "fbox");
+ b.appendChild(h("span", "flbl", label));
+ return b;
+ };
+
+ // BRIGHTNESS — the drum pair
+ const bright = fbox("Brightness");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, bright);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, bright);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 204,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ bright.appendChild(dhost);
+ bench.appendChild(bright);
+
+ const rcol = h("div", "fcol");
+
+ // POINTERS — console keys, horizontal
+ const ptr = fbox("Pointers");
+ [["touchpad", "TPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const b = h("button", "dupre-key ptr", name);
+ if (DS.state[k]) b.classList.add("dupre-green");
+ b.onclick = () => { b.blur(); DS.setToggle(k, ptr); };
+ ptr.appendChild(b);
+ });
+ rcol.appendChild(ptr);
+
+ // IDLE — the tripper dial (size unchanged)
+ const idle = fbox("Idle");
+ const railHost = h("div", "khost");
+ const rail = DUPRE.tripDial(railHost, {
+ stages: IDLE_STAGES.map((st) => ({ ...st })),
+ max: 60,
+ size: 148,
+ legend: true,
+ onChange: (id, minutes, snapshot) => {
+ snapshot.forEach((st) => { const m = IDLE_STAGES.find((o) => o.id === st.id); if (m) m.minutes = st.minutes; });
+ if (booting || !id) return;
+ const st = IDLE_STAGES.find((o) => o.id === id);
+ DS.toast(st.label + (minutes == null ? " stage disabled" : " → idle " + Math.round(minutes) + "m"), true);
+ },
+ });
+ rail.setBypassed(DS.state.caffeine);
+ idle.appendChild(railHost);
+ rcol.appendChild(idle);
+
+ bench.appendChild(rcol);
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-34.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-34.html
new file mode 100644
index 0000000..3692ada
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-34.html
@@ -0,0 +1,720 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 34 (annotation batch one)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#dab53d; --gold-hi:#ffd75f;
+ --amber-grad-top:#f0d879; --amber-grad-mid:#dab53d; --amber-grad-bot:#8f7a1f;
+ --amber-edge:#7d6a16; --amber-warn:#edc95d;
+ --glow-hi:255,215,95; --glow-lo:218,181,61;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ body.ds-stage {
+ background: radial-gradient(1200px 600px at 70% -10%, #1c1915 0%, transparent 60%), var(--ground);
+ }
+ .ds-panel { width: 540px; background-color: var(--panel); border-width: 1.6px; }
+ .ds-face {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #262320;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
+ }
+ .ds-face .title { color: var(--gold); letter-spacing: .12em; font-size: 15px; }
+ .ds-caption { max-width: 520px; }
+ .bench { display: flex; align-items: stretch; justify-content: center; gap: 12px; }
+ .fcol { display: flex; flex-direction: column; gap: 12px; }
+ .fbox {
+ position: relative;
+ border: 1px solid #35302a;
+ border-radius: 10px;
+ padding: 14px 12px 10px;
+ display: flex; align-items: center; justify-content: center; gap: 10px;
+ }
+ .fbox .flbl {
+ position: absolute; top: -7px; left: 12px;
+ background: var(--raise);
+ padding: 0 7px;
+ color: var(--steel); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
+ }
+ .fbox .dupre-key.ptr { width: 96px; height: 24px; font-size: 8px; letter-spacing: .08em; padding: 0; }
+ .plate {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #2c2823;
+ border-radius: 11px;
+ padding: 10px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 2px 6px rgba(0,0,0,.35);
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ /* ---- wallpaper channel bank ---- */
+ .wp-chans { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
+ .wp-chan {
+ border: 1px solid #33302b; border-radius: 8px; padding: 4px;
+ background: linear-gradient(180deg, var(--key-top, #23211e), var(--key-bot, #191715));
+ cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 3px;
+ }
+ .wp-chan:hover { border-color: var(--gold); }
+ .wp-chan.on { border-color: #a9c95f; box-shadow: 0 0 8px rgba(169,201,95,.25); }
+ .wp-chan .face { width: 100%; aspect-ratio: 16/10; border-radius: 4px; overflow: hidden; position: relative; background: #0c0a08; }
+ .wp-chan .nm { font-size: 7px; letter-spacing: .08em; color: var(--dim); text-transform: uppercase; }
+ .wp-chan.on .nm { color: #a9c95f; }
+ .half { position: absolute; inset: 0; }
+ .half.b { clip-path: polygon(100% 0, 100% 100%, 0 100%); }
+ .deck { position: absolute; inset: 0; }
+ .deck i { position: absolute; width: 68%; height: 68%; border-radius: 3px; border: 1px solid #0009; }
+ .mini-clocks { position: absolute; inset: 2px; display: grid; grid-template-columns: repeat(3,1fr); gap: 1px; }
+ .mini-clocks i { border-radius: 50%; border: 1px solid #4a4438; position: relative; background: #14110e; }
+ .mini-clocks i::after { content: ""; position: absolute; left: 50%; top: 18%; width: 1px; height: 34%; background: var(--gold); transform-origin: bottom; }
+ .mini-clocks i:nth-child(2)::after { transform: rotate(65deg); }
+ .mini-clocks i:nth-child(3)::after { transform: rotate(140deg); }
+ .mini-clocks i:nth-child(4)::after { transform: rotate(-50deg); }
+ .mini-clocks i:nth-child(5)::after { transform: rotate(110deg); }
+ .mini-clocks i:nth-child(6)::after { transform: rotate(-120deg); }
+ .clock-face { position: absolute; inset: 8% 25%; border-radius: 50%; border: 1.5px solid var(--cream); background: #14110e; }
+ .clock-face::before { content: ""; position: absolute; left: 50%; top: 12%; width: 1.5px; height: 40%; background: var(--cream); transform: rotate(40deg); transform-origin: bottom; }
+ .clock-face::after { content: ""; position: absolute; left: 50%; top: 22%; width: 1.5px; height: 30%; background: var(--gold); transform: rotate(-70deg); transform-origin: bottom; }
+ .watch-face { position: absolute; inset: 6% 22%; border-radius: 50%; border: 1px solid #3a5c2a; background: radial-gradient(circle, #0f1a0c, #060905); }
+ .watch-face::after { content: ""; position: absolute; left: 50%; top: 6%; width: 1px; height: 44%; background: #a9c95f; transform: rotate(55deg); transform-origin: bottom; box-shadow: 0 0 4px #a9c95f; }
+ .vid-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #fffc; font-size: 14px; text-shadow: 0 1px 3px #000; }
+
+ .wp-conf { margin-top: 10px; }
+ .wp-lib { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
+ .wp-th { aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .wp-th:hover { border-color: var(--gold); }
+ .wp-th.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .pair-bench { display: flex; align-items: center; gap: 10px; justify-content: center; }
+ .pair-slot { width: 104px; aspect-ratio: 16/10; border: 1.5px dashed #4a443a; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-slot.filled { border-style: solid; }
+ .pair-slot.arm { border-color: var(--gold); }
+ .pair-slot .cap { position: absolute; bottom: 2px; left: 4px; font-size: 7px; letter-spacing: .12em; color: #fff9; text-shadow: 0 1px 2px #000; }
+ .pair-tray { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
+ .pair-chip { width: 64px; aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-chip.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .wp-note { color: var(--dim); font-size: 10px; margin-top: 6px; }
+ .wp-row2 { display: flex; align-items: center; gap: 10px; justify-content: center; margin-top: 8px; }
+ .int-wheel {
+ width: 46px; height: 30px; border-radius: 4px; background: #0a0908; border: 1px solid #3a352c;
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
+ color: #2b2418; font-weight: 700; font-size: 12px; font-family: var(--mono);
+ background: linear-gradient(90deg,#cfc6a8,#ece4c8 50%,#c6bd9e);
+ box-shadow: inset 0 4px 5px rgba(0,0,0,.35), inset 0 -4px 5px rgba(0,0,0,.35);
+ }
+ .ds-toast.empty { visibility: hidden; }
+ .wp-dirs { display: flex; flex-direction: column; gap: 4px; }
+ .wp-dir {
+ display: flex; align-items: center; gap: 8px;
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 6px;
+ padding: 5px 8px; font-size: 11px; color: var(--silver);
+ }
+ .wp-dir .path { color: var(--dim); flex: 1; }
+ .wp-dir .rm { color: var(--dim); cursor: pointer; font-size: 12px; padding: 0 3px; }
+ .wp-dir .rm:hover { color: var(--fail); }
+ .pair-chip .rm {
+ position: absolute; top: 1px; right: 2px; color: #fffa; cursor: pointer;
+ font-size: 10px; text-shadow: 0 1px 2px #000; display: none; padding: 0 2px;
+ }
+ .pair-chip:hover .rm { display: block; }
+ .pair-chip.armed { border-color: var(--fail); box-shadow: 0 0 6px rgba(203,107,77,.5); }
+ .pair-chip.armed .rm { display: block; color: var(--fail); }
+ .wp-scroll { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 5px; }
+ .wp-scroll .wp-th { flex: 0 0 62px; }
+ .wp-desc {
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 8px;
+ color: var(--steel); font-size: 11px; line-height: 1.5; padding: 8px 10px; margin-top: 8px;
+ }
+ .wp-prev {
+ width: 100%; aspect-ratio: 16/9; border-radius: 8px; border: 1px solid #2c2823;
+ overflow: hidden; position: relative; background: #0c0a08;
+ }
+ .wp-prev img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .sortkeys { display: flex; gap: 6px; margin-bottom: 6px; }
+ .sortkeys .dupre-key.srt { width: 30px; height: 20px; font-size: 9px; padding: 0; }
+ .pair-scroll .pair-cell { flex: 0 0 66px; display: flex; flex-direction: column; }
+ .pair-cell .pair-chip { width: 66px; }
+ .pair-foot { display: flex; align-items: center; justify-content: space-between; height: 14px; }
+ .pair-lbl { font-size: 7.5px; color: var(--steel); letter-spacing: .04em; overflow: hidden; white-space: nowrap; }
+ .pbtn { color: var(--dim); cursor: pointer; font-size: 10px; padding: 0 2px; }
+ .pbtn:hover { color: var(--gold); }
+ .press-side { display: flex; flex-direction: column; gap: 6px; }
+ .wp-lblin {
+ background: #0a0908; border: 1px solid #3a352c; border-radius: 5px;
+ color: var(--cream); font-family: var(--mono); font-size: 10px; padding: 4px 7px; width: 120px;
+ }
+ .wp-lblin::placeholder { color: var(--dim); }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 32 — the wallpaper channels.</b> The sub-view is a channel
+ bank: seven mode cards (night watch · sun pair · single · random ·
+ video · clock · world), each wearing its own face — slashed thumbs,
+ deck fans, mini clock matrices. The active channel lights green; the
+ config surface below switches per channel. Pairs are MINTED: fill the
+ day and night frames, confirm, and the slashed chip joins the tray
+ forever.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">SETTINGS</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "=", "\u2193"]; // up / equals / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "=", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "=": "balanced", "\u2193": "saver" };
+let booting = true;
+// the idle policy the rail programs (mock persistence across renders)
+const IDLE_STAGES = [
+ { id: "dim", label: "DIM", color: "#d29638", minutes: 5 },
+ { id: "lock", label: "LOCK · WATCH", color: "#54677d", minutes: 7 },
+ { id: "dpms", label: "SCREEN OFF", color: "#969385", minutes: 10 },
+ { id: "suspend", label: "SUSPEND", color: "#cb6b4d", minutes: 30 },
+];
+
+// two unassigned program slots (blank keys, all-off programs)
+["Slot5", "Slot6", "Slot7", "Slot8"].forEach((n) => { if (!DS.SCENES[n]) DS.SCENES[n] = { glyph: "", sets: {} }; });
+
+// factory presets (Craig, 2026-07-22) — rows: AUTO-DIM, NIGHT LIGHT, DND, CAFFEINE + CPU
+DS.SCENES.Focus.sets = { autodim: true, nightlight: false, dnd: true, caffeine: true, powerprofile: "performance" };
+DS.SCENES.Presentation.sets = { autodim: false, nightlight: false, dnd: true, caffeine: true, powerprofile: "balanced" };
+DS.SCENES.Battery.sets = { autodim: true, nightlight: false, dnd: false, caffeine: false, powerprofile: "saver" };
+DS.SCENES.Night.sets = { autodim: true, nightlight: true, dnd: false, caffeine: true, powerprofile: "saver" };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+// ---- wallpaper: channels state (mock persistence) -------------------------
+const SWATCH = [
+ "linear-gradient(135deg,#2a3d5c,#101b2e)", "linear-gradient(135deg,#5c3a2a,#2e1810)",
+ "linear-gradient(135deg,#3a5c2a,#16240f)", "linear-gradient(135deg,#40364f,#1b1626)",
+ "linear-gradient(135deg,#1a1e2e,#05070d)", "linear-gradient(135deg,#5c5030,#2e2810)",
+ "linear-gradient(135deg,#6b4a5c,#2e1420)", "linear-gradient(135deg,#2a5c55,#0f2420)",
+ "linear-gradient(135deg,#7a6248,#3a2c1a)", "linear-gradient(135deg,#23364a,#0a1018)",
+ "linear-gradient(135deg,#4a5a3a,#1c2410)", "linear-gradient(135deg,#5a3a4a,#240f1c)",
+];
+const VIDEOS = ["linear-gradient(160deg,#38506b,#0d1520)", "linear-gradient(160deg,#5c4a2a,#241a08)", "linear-gradient(160deg,#2c4a3e,#0a1a12)"];
+const WP = {
+ channel: "single",
+ single: 2,
+ pairs: [{ day: 1, night: 4, label: "harbor", ts: 1 }, { day: 8, night: 9, label: "", ts: 2 }],
+ pairSel: 0,
+ video: 0,
+ interval: 30,
+ building: { day: null, night: null, label: "", arm: null },
+ dirs: ["~/pictures/wallpaper", "~/pictures/wallpaper/day-night"],
+ sort: "time",
+ mintSeq: 3,
+};
+const INTERVALS = [5, 10, 15, 30, 60];
+const CHANNELS = [
+ { id: "watch", nm: "Watch" },
+ { id: "pair", nm: "Sun pair" },
+ { id: "single", nm: "Single" },
+ { id: "random", nm: "Random" },
+ { id: "video", nm: "Video" },
+ { id: "clock", nm: "Clock" },
+ { id: "world", nm: "World" },
+];
+const SOURCED = ["single", "pair", "random", "video"]; // generator modes hide sources
+const DESC = {
+ watch: "Plays the Dupre night watch: the instrument board keeps real time while the machine idles. The same board is the lock screen's face.",
+ pair: "Two pictures, one day. The day picture shows from sunrise, the night picture from sundown — sun-times from your location. Pick a minted pair from the gallery, or press a new one below: fill both frames from the pictures, name it if you like, and mint.",
+ single: "One picture, always. Pick it from the gallery; the preview shows what ships.",
+ random: "Draws a fresh picture from your sources on the wheel's interval. Click the wheel to change the minutes.",
+ video: "Plays a video loop as the wallpaper, aerial style. Pick the loop from the gallery.",
+ clock: "Fills the screen with the big clock — the same clock the time module opens when clicked.",
+ world: "Fills the screen with a matrix of world clocks — the cities and timezones from the time module's hover.",
+};
+const DIR_CANDIDATES = ["~/pictures/travel", "~/pictures/astro", "~/downloads/walls", "~/pictures/scans"];
+
+function chanFace(id) {
+ const f = h("div", "face");
+ if (id === "single") { const i = h("div", "half"); i.style.background = SWATCH[WP.single]; f.appendChild(i); }
+ else if (id === "pair") {
+ const p = WP.pairs[WP.pairSel] || WP.pairs[0];
+ const a = h("div", "half"); a.style.background = p ? SWATCH[p.day] : "#222";
+ const b = h("div", "half b"); b.style.background = p ? SWATCH[p.night] : "#111";
+ f.appendChild(a); f.appendChild(b);
+ } else if (id === "random") {
+ const d = h("div", "deck");
+ [[8, "18%", "4%", "-6deg"], [4, "10%", "16%", "3deg"], [0, "2%", "28%", "-2deg"]].forEach(([si, top, left, rot]) => {
+ const c = h("i"); c.style.cssText = `top:${top};left:${left};transform:rotate(${rot});background:${SWATCH[si]}`;
+ d.appendChild(c);
+ });
+ f.appendChild(d);
+ } else if (id === "video") {
+ const i = h("div", "half"); i.style.background = VIDEOS[WP.video]; f.appendChild(i);
+ f.appendChild(h("div", "vid-play", "\u25B6"));
+ } else if (id === "clock") f.appendChild(h("div", "clock-face"));
+ else if (id === "world") {
+ const m = h("div", "mini-clocks");
+ for (let i = 0; i < 6; i++) m.appendChild(h("i"));
+ f.appendChild(m);
+ } else if (id === "watch") f.appendChild(h("div", "watch-face"));
+ return f;
+}
+
+function scrollGallery(cls) {
+ return h("div", "wp-scroll" + (cls ? " " + cls : ""));
+}
+
+function wallpaperView() {
+ const sv = h("div", "ds-col");
+ const head = h("div", "ds-sub-head");
+ head.innerHTML = `<span class="back">\u2039 Back</span><span class="title">Wallpaper</span>`;
+ head.querySelector(".back").onclick = () => subview.classList.remove("open");
+ sv.appendChild(head);
+ const body2 = h("div", "ds-col");
+ sv.appendChild(body2);
+
+ const paint = () => {
+ body2.innerHTML = "";
+
+ // modes — labeled and divided like every family section
+ body2.appendChild(DS.engrave("Modes"));
+ const bank = h("div", "wp-chans");
+ CHANNELS.forEach((c) => {
+ const card = h("div", "wp-chan" + (WP.channel === c.id ? " on" : ""));
+ card.appendChild(chanFace(c.id));
+ card.appendChild(h("div", "nm", c.nm));
+ card.onclick = () => { WP.channel = c.id; DS.toast("Wallpaper: " + c.nm, true); paint(); };
+ bank.appendChild(card);
+ });
+ body2.appendChild(bank);
+
+ // description — helpful text for every mode, never "no options"
+ body2.appendChild(h("div", "wp-desc", DESC[WP.channel]));
+
+ const conf = h("div", "wp-conf");
+ const previewBox = (bg, imgSrc) => {
+ const pv = h("div", "wp-prev");
+ if (imgSrc) { const img = h("img"); img.src = imgSrc; pv.appendChild(img); }
+ else if (bg) pv.style.background = bg;
+ return pv;
+ };
+
+ if (WP.channel === "single") {
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th" + (i === WP.single ? " sel" : ""));
+ t.style.background = sw;
+ t.onclick = () => { WP.single = i; DS.toast("Wallpaper set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(g);
+ conf.appendChild(DS.engrave("Preview"));
+ conf.appendChild(previewBox(SWATCH[WP.single]));
+ } else if (WP.channel === "pair") {
+ // minted pair gallery: side-scroll, sortable, labeled, editable
+ conf.appendChild(DS.engrave("Minted pairs"));
+ const sortRow = h("div", "sortkeys");
+ [["alpha", "a"], ["time", "\u{f0150}"]].forEach(([mode, glyph]) => {
+ const k = h("button", "dupre-key srt" + (WP.sort === mode ? " dupre-on" : ""), glyph);
+ k.onclick = () => { k.blur(); WP.sort = mode; paint(); };
+ sortRow.appendChild(k);
+ });
+ conf.appendChild(sortRow);
+ const tray = scrollGallery("pair-scroll");
+ const order = WP.pairs.map((p, i) => ({ p, i }));
+ if (WP.sort === "alpha") order.sort((x, y) => (x.p.label || "\uffff").localeCompare(y.p.label || "\uffff"));
+ else order.sort((x, y) => y.p.ts - x.p.ts);
+ order.forEach(({ p, i }) => {
+ const cell = h("div", "pair-cell");
+ const chip = h("div", "pair-chip" + (i === WP.pairSel ? " sel" : ""));
+ const a = h("div", "half"); a.style.background = SWATCH[p.day];
+ const b = h("div", "half b"); b.style.background = SWATCH[p.night];
+ chip.appendChild(a); chip.appendChild(b);
+ chip.onclick = () => { WP.pairSel = i; DS.toast("Pair selected", true); paint(); };
+ cell.appendChild(chip);
+ const foot = h("div", "pair-foot");
+ const ed = h("span", "pbtn", "~");
+ ed.title = "edit — reopens in the press";
+ ed.onclick = () => {
+ WP.building = { day: p.day, night: p.night, label: p.label, arm: null };
+ WP.pairs.splice(i, 1);
+ if (WP.pairSel >= WP.pairs.length) WP.pairSel = WP.pairs.length - 1;
+ DS.toast("Pair opened in the press", true);
+ paint();
+ };
+ foot.appendChild(ed);
+ foot.appendChild(h("span", "pair-lbl", p.label || ""));
+ const del = h("span", "pbtn", "\u2212");
+ del.title = "delete — recreate to get it back";
+ del.onclick = () => {
+ WP.pairs.splice(i, 1);
+ if (WP.pairSel >= WP.pairs.length) WP.pairSel = WP.pairs.length - 1;
+ DS.toast("Pair deleted", true);
+ paint();
+ };
+ foot.appendChild(del);
+ cell.appendChild(foot);
+ tray.appendChild(cell);
+ });
+ conf.appendChild(tray);
+
+ // the pair press, with its label field
+ conf.appendChild(DS.engrave("Pair press"));
+ const bench = h("div", "pair-bench");
+ ["day", "night"].forEach((k) => {
+ const slot = h("div", "pair-slot" +
+ (WP.building[k] !== null ? " filled" : "") + (WP.building.arm === k ? " arm" : ""));
+ if (WP.building[k] !== null) slot.style.background = SWATCH[WP.building[k]];
+ slot.appendChild(h("span", "cap", k === "day" ? "\u2600 DAY" : "\u263E NIGHT"));
+ slot.onclick = () => { WP.building.arm = WP.building.arm === k ? null : k; paint(); };
+ bench.appendChild(slot);
+ });
+ const side = h("div", "press-side");
+ const lbl = h("input", "wp-lblin");
+ lbl.placeholder = "label (optional)";
+ lbl.value = WP.building.label || "";
+ lbl.oninput = () => { WP.building.label = lbl.value; };
+ side.appendChild(lbl);
+ const mint = h("button", "dupre-key", "MINT PAIR");
+ mint.style.cssText = "height:26px;font-size:8px;padding:0 10px";
+ mint.onclick = () => {
+ mint.blur();
+ if (WP.building.day === null || WP.building.night === null) { DS.toast("fill both frames first", false); return; }
+ WP.pairs.push({ day: WP.building.day, night: WP.building.night, label: WP.building.label || "", ts: WP.mintSeq++ });
+ WP.pairSel = WP.pairs.length - 1;
+ WP.building = { day: null, night: null, label: "", arm: null };
+ DS.toast("Pair minted", true);
+ paint();
+ };
+ side.appendChild(mint);
+ bench.appendChild(side);
+ conf.appendChild(bench);
+ if (WP.building.arm) conf.appendChild(h("div", "wp-note", "click a picture below to fill the " + WP.building.arm.toUpperCase() + " frame"));
+
+ // the picture gallery that feeds the press
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th");
+ t.style.background = sw;
+ t.onclick = () => {
+ if (!WP.building.arm) { DS.toast("click a DAY or NIGHT frame first", false); return; }
+ WP.building[WP.building.arm] = i;
+ WP.building.arm = WP.building.arm === "day" && WP.building.night === null ? "night" : null;
+ paint();
+ };
+ g.appendChild(t);
+ });
+ conf.appendChild(g);
+ conf.appendChild(h("div", "wp-note", "Only picture types the lock screen can display appear here."));
+ } else if (WP.channel === "random") {
+ const row = h("div", "wp-row2");
+ row.appendChild(h("span", "wp-note", "next draw every"));
+ const wheel = h("div", "int-wheel", WP.interval);
+ wheel.onclick = () => {
+ WP.interval = INTERVALS[(INTERVALS.indexOf(WP.interval) + 1) % INTERVALS.length];
+ DS.toast("Random interval: " + WP.interval + "m", true);
+ paint();
+ };
+ row.appendChild(wheel);
+ row.appendChild(h("span", "wp-note", "minutes"));
+ conf.appendChild(row);
+ } else if (WP.channel === "video") {
+ conf.appendChild(DS.engrave("Videos"));
+ const g = scrollGallery();
+ VIDEOS.forEach((v, i) => {
+ const t = h("div", "wp-th" + (i === WP.video ? " sel" : ""));
+ t.style.background = v;
+ t.appendChild(h("div", "vid-play", "\u25B6"));
+ t.onclick = () => { WP.video = i; DS.toast("Video set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(g);
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox(VIDEOS[WP.video]);
+ pv.appendChild(h("div", "vid-play", "\u25B6"));
+ conf.appendChild(pv);
+ } else if (WP.channel === "watch") {
+ conf.appendChild(DS.engrave("Preview"));
+ conf.appendChild(previewBox(null, "2026-07-22-watch-preview.png"));
+ } else if (WP.channel === "clock") {
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox("#0c0a08");
+ const cf = h("div", "clock-face"); cf.style.inset = "6% 34%";
+ pv.appendChild(cf);
+ conf.appendChild(pv);
+ } else if (WP.channel === "world") {
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox("#0c0a08");
+ const m = h("div", "mini-clocks"); m.style.inset = "8%";
+ for (let i = 0; i < 6; i++) m.appendChild(h("i"));
+ pv.appendChild(m);
+ conf.appendChild(pv);
+ }
+ body2.appendChild(conf);
+
+ // sources: only for the modes that draw from directories
+ if (SOURCED.includes(WP.channel)) {
+ const src = h("div", "wp-conf");
+ src.appendChild(DS.engrave("Sources"));
+ const dirs = h("div", "wp-dirs");
+ WP.dirs.forEach((d, i) => {
+ const row = h("div", "wp-dir");
+ row.innerHTML = `<span>\u{f024b}</span>`;
+ row.appendChild(h("span", "path", d));
+ const rm = h("span", "rm", "\u2715");
+ rm.onclick = () => {
+ if (WP.dirs.length === 1) { DS.toast("keep at least one source", false); return; }
+ WP.dirs.splice(i, 1);
+ DS.toast("Source removed", true);
+ paint();
+ };
+ row.appendChild(rm);
+ dirs.appendChild(row);
+ });
+ src.appendChild(dirs);
+ const add = h("button", "dupre-key", "ADD DIRECTORY");
+ add.style.cssText = "height:24px;font-size:8px;padding:0 10px;margin-top:6px";
+ add.onclick = () => {
+ add.blur();
+ const next = DIR_CANDIDATES.find((c) => !WP.dirs.includes(c));
+ if (!next) { DS.toast("no more candidates in the mock", false); return; }
+ WP.dirs.push(next);
+ DS.toast("Source added: " + next, true);
+ paint();
+ };
+ src.appendChild(add);
+ body2.appendChild(src);
+ }
+ };
+ paint();
+ return sv;
+}
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(wallpaperView());
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Programmable presets"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", glyph: "\u{f0210}", scene: "Focus" }, // crosshairs — focus
+ { id: "PRESENT", label: "PRESENT", glyph: "\u{f0379}", scene: "Presentation" }, // projector screen
+ { id: "BATTERY", label: "BATTERY", glyph: "\u{f007e}", scene: "Battery" }, // half battery
+ { id: "NIGHT", label: "NIGHT", glyph: "\u{f0594}", scene: "Night" }, // crescent moon
+ { id: "S5", label: "1", scene: "Slot5", slim: true },
+ { id: "S6", label: "2", scene: "Slot6", slim: true },
+ { id: "S7", label: "3", scene: "Slot7", slim: true },
+ { id: "S8", label: "4", scene: "Slot8", slim: true },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label, glyph, slim }) => ({ id, label, glyph, slim }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 452,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — three encircled instrument groups -------------
+ const lv = h("div", "plate");
+ const bench = h("div", "bench");
+
+ const fbox = (label) => {
+ const b = h("div", "fbox");
+ b.appendChild(h("span", "flbl", label));
+ return b;
+ };
+
+ // BRIGHTNESS — the drum pair
+ const bright = fbox("Brightness");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, bright);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, bright);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 210,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ bright.appendChild(dhost);
+ bench.appendChild(bright);
+
+ const rcol = h("div", "fcol");
+
+ // POINTERS — console keys, horizontal
+ const ptr = fbox("Pointers");
+ [["touchpad", "TOUCHPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const b = h("button", "dupre-key ptr", name);
+ if (DS.state[k]) b.classList.add("dupre-green");
+ b.onclick = () => { b.blur(); DS.setToggle(k, ptr); };
+ ptr.appendChild(b);
+ });
+ rcol.appendChild(ptr);
+
+ // IDLE — the tripper dial (size unchanged)
+ const idle = fbox("Idle");
+ const railHost = h("div", "khost");
+ const rail = DUPRE.tripDial(railHost, {
+ stages: IDLE_STAGES.map((st) => ({ ...st })),
+ max: 60,
+ size: 148,
+ legend: true,
+ onChange: (id, minutes, snapshot) => {
+ snapshot.forEach((st) => { const m = IDLE_STAGES.find((o) => o.id === st.id); if (m) m.minutes = st.minutes; });
+ if (booting || !id) return;
+ const st = IDLE_STAGES.find((o) => o.id === id);
+ DS.toast(st.label + (minutes == null ? " stage disabled" : " → idle " + Math.round(minutes) + "m"), true);
+ },
+ });
+ rail.setBypassed(DS.state.caffeine);
+ idle.appendChild(railHost);
+ rcol.appendChild(idle);
+
+ bench.appendChild(rcol);
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html
new file mode 100644
index 0000000..9296af2
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html
@@ -0,0 +1,717 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 35 (FINAL candidate)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#dab53d; --gold-hi:#ffd75f;
+ --amber-grad-top:#f0d879; --amber-grad-mid:#dab53d; --amber-grad-bot:#8f7a1f;
+ --amber-edge:#7d6a16; --amber-warn:#edc95d;
+ --glow-hi:255,215,95; --glow-lo:218,181,61;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ body.ds-stage {
+ background: radial-gradient(1200px 600px at 70% -10%, #1c1915 0%, transparent 60%), var(--ground);
+ }
+ .ds-panel { width: 540px; background-color: var(--panel); border-width: 1.6px; }
+ .ds-face {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #262320;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
+ }
+ .ds-face .title { color: var(--gold); letter-spacing: .12em; font-size: 15px; }
+ .ds-caption { max-width: 520px; }
+ .bench { display: flex; align-items: stretch; justify-content: center; gap: 12px; }
+ .fcol { display: flex; flex-direction: column; gap: 12px; }
+ .fbox {
+ position: relative;
+ border: 1px solid #35302a;
+ border-radius: 10px;
+ padding: 14px 12px 10px;
+ display: flex; align-items: center; justify-content: center; gap: 10px;
+ }
+ .fbox .flbl {
+ position: absolute; top: -7px; left: 12px;
+ background: var(--raise);
+ padding: 0 7px;
+ color: var(--steel); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
+ }
+ .fbox .dupre-key.ptr { width: 96px; height: 24px; font-size: 8px; letter-spacing: .08em; padding: 0; }
+ .plate {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #2c2823;
+ border-radius: 11px;
+ padding: 10px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 2px 6px rgba(0,0,0,.35);
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ /* ---- wallpaper channel bank ---- */
+ .wp-chans { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
+ .wp-chan {
+ border: 1px solid #33302b; border-radius: 8px; padding: 4px;
+ background: linear-gradient(180deg, var(--key-top, #23211e), var(--key-bot, #191715));
+ cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 3px;
+ }
+ .wp-chan:hover { border-color: var(--gold); }
+ .wp-chan.on { border-color: #a9c95f; box-shadow: 0 0 8px rgba(169,201,95,.25); }
+ .wp-chan .face { width: 100%; aspect-ratio: 16/10; border-radius: 4px; overflow: hidden; position: relative; background: #0c0a08; }
+ .wp-chan .nm { font-size: 7px; letter-spacing: .08em; color: var(--dim); text-transform: uppercase; }
+ .wp-chan.on .nm { color: #a9c95f; }
+ .half { position: absolute; inset: 0; }
+ .half.b { clip-path: polygon(100% 0, 100% 100%, 0 100%); }
+ .deck { position: absolute; inset: 0; }
+ .deck i { position: absolute; width: 68%; height: 68%; border-radius: 3px; border: 1px solid #0009; }
+ .mini-clocks { position: absolute; inset: 2px; display: grid; grid-template-columns: repeat(3,1fr); gap: 1px; }
+ .mini-clocks i { border-radius: 50%; border: 1px solid #4a4438; position: relative; background: #14110e; }
+ .mini-clocks i::after { content: ""; position: absolute; left: 50%; top: 18%; width: 1px; height: 34%; background: var(--gold); transform-origin: bottom; }
+ .mini-clocks i:nth-child(2)::after { transform: rotate(65deg); }
+ .mini-clocks i:nth-child(3)::after { transform: rotate(140deg); }
+ .mini-clocks i:nth-child(4)::after { transform: rotate(-50deg); }
+ .mini-clocks i:nth-child(5)::after { transform: rotate(110deg); }
+ .mini-clocks i:nth-child(6)::after { transform: rotate(-120deg); }
+ .clock-face { position: absolute; inset: 8% 25%; border-radius: 50%; border: 1.5px solid var(--cream); background: #14110e; }
+ .clock-face::before { content: ""; position: absolute; left: 50%; top: 12%; width: 1.5px; height: 40%; background: var(--cream); transform: rotate(40deg); transform-origin: bottom; }
+ .clock-face::after { content: ""; position: absolute; left: 50%; top: 22%; width: 1.5px; height: 30%; background: var(--gold); transform: rotate(-70deg); transform-origin: bottom; }
+ .watch-face { position: absolute; inset: 6% 22%; border-radius: 50%; border: 1px solid #3a5c2a; background: radial-gradient(circle, #0f1a0c, #060905); }
+ .watch-face::after { content: ""; position: absolute; left: 50%; top: 6%; width: 1px; height: 44%; background: #a9c95f; transform: rotate(55deg); transform-origin: bottom; box-shadow: 0 0 4px #a9c95f; }
+ .vid-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #fffc; font-size: 14px; text-shadow: 0 1px 3px #000; }
+
+ .wp-conf { margin-top: 10px; }
+ .wp-lib { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
+ .wp-th { aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .wp-th:hover { border-color: var(--gold); }
+ .wp-th.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .pair-bench { display: flex; align-items: center; gap: 10px; justify-content: center; }
+ .pair-slot { width: 104px; aspect-ratio: 16/10; border: 1.5px dashed #4a443a; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-slot.filled { border-style: solid; }
+ .pair-slot.arm { border-color: var(--gold); }
+ .pair-slot .cap { position: absolute; bottom: 2px; left: 4px; font-size: 7px; letter-spacing: .12em; color: #fff9; text-shadow: 0 1px 2px #000; }
+ .pair-tray { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
+ .pair-chip { width: 64px; aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-chip.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .wp-note { color: var(--dim); font-size: 10px; margin-top: 6px; }
+ .wp-row2 { display: flex; align-items: center; gap: 10px; justify-content: center; margin-top: 8px; }
+ .int-wheel {
+ width: 46px; height: 30px; border-radius: 4px; background: #0a0908; border: 1px solid #3a352c;
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
+ color: #2b2418; font-weight: 700; font-size: 12px; font-family: var(--mono);
+ background: linear-gradient(90deg,#cfc6a8,#ece4c8 50%,#c6bd9e);
+ box-shadow: inset 0 4px 5px rgba(0,0,0,.35), inset 0 -4px 5px rgba(0,0,0,.35);
+ }
+ .ds-toast.empty { visibility: hidden; }
+ .wp-dirs { display: flex; flex-direction: column; gap: 4px; }
+ .wp-dir {
+ display: flex; align-items: center; gap: 8px;
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 6px;
+ padding: 5px 8px; font-size: 11px; color: var(--silver);
+ }
+ .wp-dir .path { color: var(--dim); flex: 1; }
+ .wp-dir .rm { color: var(--dim); cursor: pointer; font-size: 12px; padding: 0 3px; }
+ .wp-dir .rm:hover { color: var(--fail); }
+ .pair-chip .rm {
+ position: absolute; top: 1px; right: 2px; color: #fffa; cursor: pointer;
+ font-size: 10px; text-shadow: 0 1px 2px #000; display: none; padding: 0 2px;
+ }
+ .pair-chip:hover .rm { display: block; }
+ .pair-chip.armed { border-color: var(--fail); box-shadow: 0 0 6px rgba(203,107,77,.5); }
+ .pair-chip.armed .rm { display: block; color: var(--fail); }
+ .wp-scroll { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 5px; }
+ .wp-scroll .wp-th { flex: 0 0 62px; }
+ .wp-desc {
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 8px;
+ color: var(--steel); font-size: 11px; line-height: 1.5; padding: 8px 10px; margin-top: 8px;
+ }
+ .wp-prev {
+ width: 100%; aspect-ratio: 16/9; border-radius: 8px; border: 1px solid #2c2823;
+ overflow: hidden; position: relative; background: #0c0a08;
+ }
+ .wp-prev img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .sortkeys { display: flex; gap: 6px; margin-bottom: 6px; }
+ .sortkeys .dupre-key.srt { width: 30px; height: 20px; font-size: 9px; padding: 0; }
+ .pair-scroll .pair-cell { flex: 0 0 66px; display: flex; flex-direction: column; }
+ .pair-cell .pair-chip { width: 66px; }
+ .pair-foot { display: flex; align-items: center; justify-content: space-between; height: 14px; }
+ .pair-lbl { font-size: 7.5px; color: var(--steel); letter-spacing: .04em; overflow: hidden; white-space: nowrap; }
+ .pbtn { color: var(--dim); cursor: pointer; font-size: 10px; padding: 0 2px; }
+ .pbtn:hover { color: var(--gold); }
+ .press-side { display: flex; flex-direction: column; gap: 6px; }
+ .wp-lblin {
+ background: #0a0908; border: 1px solid #3a352c; border-radius: 5px;
+ color: var(--cream); font-family: var(--mono); font-size: 10px; padding: 4px 7px; width: 120px;
+ }
+ .wp-lblin::placeholder { color: var(--dim); }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 35 — the final candidate.</b> The consolidated machine:
+ the programmable presets matrix (glyph keys, four numbered slots,
+ punch pins, arrow-and-equals CPU wheels), the instrument bench
+ (drums · pointer keys · idle tripper dial), and the wallpaper
+ channels with descriptions, galleries, previews, the pair press, and
+ sources. Normative reference for the spec.
+ </div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">SETTINGS</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "=", "\u2193"]; // up / equals / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "=", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "=": "balanced", "\u2193": "saver" };
+let booting = true;
+// the idle policy the rail programs (mock persistence across renders)
+const IDLE_STAGES = [
+ { id: "dim", label: "DIM", color: "#d29638", minutes: 5 },
+ { id: "lock", label: "LOCK · WATCH", color: "#54677d", minutes: 7 },
+ { id: "dpms", label: "SCREEN OFF", color: "#969385", minutes: 10 },
+ { id: "suspend", label: "SUSPEND", color: "#cb6b4d", minutes: 30 },
+];
+
+// two unassigned program slots (blank keys, all-off programs)
+["Slot5", "Slot6", "Slot7", "Slot8"].forEach((n) => { if (!DS.SCENES[n]) DS.SCENES[n] = { glyph: "", sets: {} }; });
+
+// factory presets (Craig, 2026-07-22) — rows: AUTO-DIM, NIGHT LIGHT, DND, CAFFEINE + CPU
+DS.SCENES.Focus.sets = { autodim: true, nightlight: false, dnd: true, caffeine: true, powerprofile: "performance" };
+DS.SCENES.Presentation.sets = { autodim: false, nightlight: false, dnd: true, caffeine: true, powerprofile: "balanced" };
+DS.SCENES.Battery.sets = { autodim: true, nightlight: false, dnd: false, caffeine: false, powerprofile: "saver" };
+DS.SCENES.Night.sets = { autodim: true, nightlight: true, dnd: false, caffeine: true, powerprofile: "saver" };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+// ---- wallpaper: channels state (mock persistence) -------------------------
+const SWATCH = [
+ "linear-gradient(135deg,#2a3d5c,#101b2e)", "linear-gradient(135deg,#5c3a2a,#2e1810)",
+ "linear-gradient(135deg,#3a5c2a,#16240f)", "linear-gradient(135deg,#40364f,#1b1626)",
+ "linear-gradient(135deg,#1a1e2e,#05070d)", "linear-gradient(135deg,#5c5030,#2e2810)",
+ "linear-gradient(135deg,#6b4a5c,#2e1420)", "linear-gradient(135deg,#2a5c55,#0f2420)",
+ "linear-gradient(135deg,#7a6248,#3a2c1a)", "linear-gradient(135deg,#23364a,#0a1018)",
+ "linear-gradient(135deg,#4a5a3a,#1c2410)", "linear-gradient(135deg,#5a3a4a,#240f1c)",
+];
+const VIDEOS = ["linear-gradient(160deg,#38506b,#0d1520)", "linear-gradient(160deg,#5c4a2a,#241a08)", "linear-gradient(160deg,#2c4a3e,#0a1a12)"];
+const WP = {
+ channel: "single",
+ single: 2,
+ pairs: [{ day: 1, night: 4, label: "harbor", ts: 1 }, { day: 8, night: 9, label: "", ts: 2 }],
+ pairSel: 0,
+ video: 0,
+ interval: 30,
+ building: { day: null, night: null, label: "", arm: null },
+ dirs: ["~/pictures/wallpaper", "~/pictures/wallpaper/day-night"],
+ sort: "time",
+ mintSeq: 3,
+};
+const INTERVALS = [5, 10, 15, 30, 60];
+const CHANNELS = [
+ { id: "watch", nm: "Watch" },
+ { id: "pair", nm: "Sun pair" },
+ { id: "single", nm: "Single" },
+ { id: "random", nm: "Random" },
+ { id: "video", nm: "Video" },
+ { id: "clock", nm: "Clock" },
+ { id: "world", nm: "World" },
+];
+const SOURCED = ["single", "pair", "random", "video"]; // generator modes hide sources
+const DESC = {
+ watch: "Plays the Dupre night watch: the instrument board keeps real time while the machine idles. The same board is the lock screen's face.",
+ pair: "Two pictures, one day. The day picture shows from sunrise, the night picture from sundown — sun-times from your location. Pick a minted pair from the gallery, or press a new one below: fill both frames from the pictures, name it if you like, and mint.",
+ single: "One picture, always. Pick it from the gallery; the preview shows what ships.",
+ random: "Draws a fresh picture from your sources on the wheel's interval. Click the wheel to change the minutes.",
+ video: "Plays a video loop as the wallpaper, aerial style. Pick the loop from the gallery.",
+ clock: "Fills the screen with the big clock — the same clock the time module opens when clicked.",
+ world: "Fills the screen with a matrix of world clocks — the cities and timezones from the time module's hover.",
+};
+const DIR_CANDIDATES = ["~/pictures/travel", "~/pictures/astro", "~/downloads/walls", "~/pictures/scans"];
+
+function chanFace(id) {
+ const f = h("div", "face");
+ if (id === "single") { const i = h("div", "half"); i.style.background = SWATCH[WP.single]; f.appendChild(i); }
+ else if (id === "pair") {
+ const p = WP.pairs[WP.pairSel] || WP.pairs[0];
+ const a = h("div", "half"); a.style.background = p ? SWATCH[p.day] : "#222";
+ const b = h("div", "half b"); b.style.background = p ? SWATCH[p.night] : "#111";
+ f.appendChild(a); f.appendChild(b);
+ } else if (id === "random") {
+ const d = h("div", "deck");
+ [[8, "18%", "4%", "-6deg"], [4, "10%", "16%", "3deg"], [0, "2%", "28%", "-2deg"]].forEach(([si, top, left, rot]) => {
+ const c = h("i"); c.style.cssText = `top:${top};left:${left};transform:rotate(${rot});background:${SWATCH[si]}`;
+ d.appendChild(c);
+ });
+ f.appendChild(d);
+ } else if (id === "video") {
+ const i = h("div", "half"); i.style.background = VIDEOS[WP.video]; f.appendChild(i);
+ f.appendChild(h("div", "vid-play", "\u25B6"));
+ } else if (id === "clock") f.appendChild(h("div", "clock-face"));
+ else if (id === "world") {
+ const m = h("div", "mini-clocks");
+ for (let i = 0; i < 6; i++) m.appendChild(h("i"));
+ f.appendChild(m);
+ } else if (id === "watch") f.appendChild(h("div", "watch-face"));
+ return f;
+}
+
+function scrollGallery(cls) {
+ return h("div", "wp-scroll" + (cls ? " " + cls : ""));
+}
+
+function wallpaperView() {
+ const sv = h("div", "ds-col");
+ const head = h("div", "ds-sub-head");
+ head.innerHTML = `<span class="back">\u2039 Back</span><span class="title">Wallpaper</span>`;
+ head.querySelector(".back").onclick = () => subview.classList.remove("open");
+ sv.appendChild(head);
+ const body2 = h("div", "ds-col");
+ sv.appendChild(body2);
+
+ const paint = () => {
+ body2.innerHTML = "";
+
+ // modes — labeled and divided like every family section
+ body2.appendChild(DS.engrave("Modes"));
+ const bank = h("div", "wp-chans");
+ CHANNELS.forEach((c) => {
+ const card = h("div", "wp-chan" + (WP.channel === c.id ? " on" : ""));
+ card.appendChild(chanFace(c.id));
+ card.appendChild(h("div", "nm", c.nm));
+ card.onclick = () => { WP.channel = c.id; DS.toast("Wallpaper: " + c.nm, true); paint(); };
+ bank.appendChild(card);
+ });
+ body2.appendChild(bank);
+
+ // description — helpful text for every mode, never "no options"
+ body2.appendChild(h("div", "wp-desc", DESC[WP.channel]));
+
+ const conf = h("div", "wp-conf");
+ const previewBox = (bg, imgSrc) => {
+ const pv = h("div", "wp-prev");
+ if (imgSrc) { const img = h("img"); img.src = imgSrc; pv.appendChild(img); }
+ else if (bg) pv.style.background = bg;
+ return pv;
+ };
+
+ if (WP.channel === "single") {
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th" + (i === WP.single ? " sel" : ""));
+ t.style.background = sw;
+ t.onclick = () => { WP.single = i; DS.toast("Wallpaper set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(g);
+ conf.appendChild(DS.engrave("Preview"));
+ conf.appendChild(previewBox(SWATCH[WP.single]));
+ } else if (WP.channel === "pair") {
+ // minted pair gallery: side-scroll, sortable, labeled, editable
+ conf.appendChild(DS.engrave("Minted pairs"));
+ const sortRow = h("div", "sortkeys");
+ [["alpha", "a"], ["time", "\u{f0150}"]].forEach(([mode, glyph]) => {
+ const k = h("button", "dupre-key srt" + (WP.sort === mode ? " dupre-on" : ""), glyph);
+ k.onclick = () => { k.blur(); WP.sort = mode; paint(); };
+ sortRow.appendChild(k);
+ });
+ conf.appendChild(sortRow);
+ const tray = scrollGallery("pair-scroll");
+ const order = WP.pairs.map((p, i) => ({ p, i }));
+ if (WP.sort === "alpha") order.sort((x, y) => (x.p.label || "\uffff").localeCompare(y.p.label || "\uffff"));
+ else order.sort((x, y) => y.p.ts - x.p.ts);
+ order.forEach(({ p, i }) => {
+ const cell = h("div", "pair-cell");
+ const chip = h("div", "pair-chip" + (i === WP.pairSel ? " sel" : ""));
+ const a = h("div", "half"); a.style.background = SWATCH[p.day];
+ const b = h("div", "half b"); b.style.background = SWATCH[p.night];
+ chip.appendChild(a); chip.appendChild(b);
+ chip.onclick = () => { WP.pairSel = i; DS.toast("Pair selected", true); paint(); };
+ cell.appendChild(chip);
+ const foot = h("div", "pair-foot");
+ const ed = h("span", "pbtn", "~");
+ ed.title = "edit — reopens in the press";
+ ed.onclick = () => {
+ WP.building = { day: p.day, night: p.night, label: p.label, arm: null };
+ WP.pairs.splice(i, 1);
+ if (WP.pairSel >= WP.pairs.length) WP.pairSel = WP.pairs.length - 1;
+ DS.toast("Pair opened in the press", true);
+ paint();
+ };
+ foot.appendChild(ed);
+ foot.appendChild(h("span", "pair-lbl", p.label || ""));
+ const del = h("span", "pbtn", "\u2212");
+ del.title = "delete — recreate to get it back";
+ del.onclick = () => {
+ WP.pairs.splice(i, 1);
+ if (WP.pairSel >= WP.pairs.length) WP.pairSel = WP.pairs.length - 1;
+ DS.toast("Pair deleted", true);
+ paint();
+ };
+ foot.appendChild(del);
+ cell.appendChild(foot);
+ tray.appendChild(cell);
+ });
+ conf.appendChild(tray);
+
+ // the pair press, with its label field
+ conf.appendChild(DS.engrave("Pair press"));
+ const bench = h("div", "pair-bench");
+ ["day", "night"].forEach((k) => {
+ const slot = h("div", "pair-slot" +
+ (WP.building[k] !== null ? " filled" : "") + (WP.building.arm === k ? " arm" : ""));
+ if (WP.building[k] !== null) slot.style.background = SWATCH[WP.building[k]];
+ slot.appendChild(h("span", "cap", k === "day" ? "\u2600 DAY" : "\u263E NIGHT"));
+ slot.onclick = () => { WP.building.arm = WP.building.arm === k ? null : k; paint(); };
+ bench.appendChild(slot);
+ });
+ const side = h("div", "press-side");
+ const lbl = h("input", "wp-lblin");
+ lbl.placeholder = "label (optional)";
+ lbl.value = WP.building.label || "";
+ lbl.oninput = () => { WP.building.label = lbl.value; };
+ side.appendChild(lbl);
+ const mint = h("button", "dupre-key", "MINT PAIR");
+ mint.style.cssText = "height:26px;font-size:8px;padding:0 10px";
+ mint.onclick = () => {
+ mint.blur();
+ if (WP.building.day === null || WP.building.night === null) { DS.toast("fill both frames first", false); return; }
+ WP.pairs.push({ day: WP.building.day, night: WP.building.night, label: WP.building.label || "", ts: WP.mintSeq++ });
+ WP.pairSel = WP.pairs.length - 1;
+ WP.building = { day: null, night: null, label: "", arm: null };
+ DS.toast("Pair minted", true);
+ paint();
+ };
+ side.appendChild(mint);
+ bench.appendChild(side);
+ conf.appendChild(bench);
+ if (WP.building.arm) conf.appendChild(h("div", "wp-note", "click a picture below to fill the " + WP.building.arm.toUpperCase() + " frame"));
+
+ // the picture gallery that feeds the press
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th");
+ t.style.background = sw;
+ t.onclick = () => {
+ if (!WP.building.arm) { DS.toast("click a DAY or NIGHT frame first", false); return; }
+ WP.building[WP.building.arm] = i;
+ WP.building.arm = WP.building.arm === "day" && WP.building.night === null ? "night" : null;
+ paint();
+ };
+ g.appendChild(t);
+ });
+ conf.appendChild(g);
+ conf.appendChild(h("div", "wp-note", "Only picture types the lock screen can display appear here."));
+ } else if (WP.channel === "random") {
+ const row = h("div", "wp-row2");
+ row.appendChild(h("span", "wp-note", "next draw every"));
+ const wheel = h("div", "int-wheel", WP.interval);
+ wheel.onclick = () => {
+ WP.interval = INTERVALS[(INTERVALS.indexOf(WP.interval) + 1) % INTERVALS.length];
+ DS.toast("Random interval: " + WP.interval + "m", true);
+ paint();
+ };
+ row.appendChild(wheel);
+ row.appendChild(h("span", "wp-note", "minutes"));
+ conf.appendChild(row);
+ } else if (WP.channel === "video") {
+ conf.appendChild(DS.engrave("Videos"));
+ const g = scrollGallery();
+ VIDEOS.forEach((v, i) => {
+ const t = h("div", "wp-th" + (i === WP.video ? " sel" : ""));
+ t.style.background = v;
+ t.appendChild(h("div", "vid-play", "\u25B6"));
+ t.onclick = () => { WP.video = i; DS.toast("Video set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(g);
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox(VIDEOS[WP.video]);
+ pv.appendChild(h("div", "vid-play", "\u25B6"));
+ conf.appendChild(pv);
+ } else if (WP.channel === "watch") {
+ conf.appendChild(DS.engrave("Preview"));
+ conf.appendChild(previewBox(null, "2026-07-22-watch-preview.png"));
+ } else if (WP.channel === "clock") {
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox("#0c0a08");
+ const cf = h("div", "clock-face"); cf.style.inset = "6% 34%";
+ pv.appendChild(cf);
+ conf.appendChild(pv);
+ } else if (WP.channel === "world") {
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox("#0c0a08");
+ const m = h("div", "mini-clocks"); m.style.inset = "8%";
+ for (let i = 0; i < 6; i++) m.appendChild(h("i"));
+ pv.appendChild(m);
+ conf.appendChild(pv);
+ }
+ body2.appendChild(conf);
+
+ // sources: only for the modes that draw from directories
+ if (SOURCED.includes(WP.channel)) {
+ const src = h("div", "wp-conf");
+ src.appendChild(DS.engrave("Sources"));
+ const dirs = h("div", "wp-dirs");
+ WP.dirs.forEach((d, i) => {
+ const row = h("div", "wp-dir");
+ row.innerHTML = `<span>\u{f024b}</span>`;
+ row.appendChild(h("span", "path", d));
+ const rm = h("span", "rm", "\u2715");
+ rm.onclick = () => {
+ if (WP.dirs.length === 1) { DS.toast("keep at least one source", false); return; }
+ WP.dirs.splice(i, 1);
+ DS.toast("Source removed", true);
+ paint();
+ };
+ row.appendChild(rm);
+ dirs.appendChild(row);
+ });
+ src.appendChild(dirs);
+ const add = h("button", "dupre-key", "ADD DIRECTORY");
+ add.style.cssText = "height:24px;font-size:8px;padding:0 10px;margin-top:6px";
+ add.onclick = () => {
+ add.blur();
+ const next = DIR_CANDIDATES.find((c) => !WP.dirs.includes(c));
+ if (!next) { DS.toast("no more candidates in the mock", false); return; }
+ WP.dirs.push(next);
+ DS.toast("Source added: " + next, true);
+ paint();
+ };
+ src.appendChild(add);
+ body2.appendChild(src);
+ }
+ };
+ paint();
+ return sv;
+}
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(wallpaperView());
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Programmable presets"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", glyph: "\u{f0210}", scene: "Focus" }, // crosshairs — focus
+ { id: "PRESENT", label: "PRESENT", glyph: "\u{f0379}", scene: "Presentation" }, // projector screen
+ { id: "BATTERY", label: "BATTERY", glyph: "\u{f007e}", scene: "Battery" }, // half battery
+ { id: "NIGHT", label: "NIGHT", glyph: "\u{f0594}", scene: "Night" }, // crescent moon
+ { id: "S5", label: "1", scene: "Slot5", slim: true },
+ { id: "S6", label: "2", scene: "Slot6", slim: true },
+ { id: "S7", label: "3", scene: "Slot7", slim: true },
+ { id: "S8", label: "4", scene: "Slot8", slim: true },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label, glyph, slim }) => ({ id, label, glyph, slim }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 452,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — three encircled instrument groups -------------
+ const lv = h("div", "plate");
+ const bench = h("div", "bench");
+
+ const fbox = (label) => {
+ const b = h("div", "fbox");
+ b.appendChild(h("span", "flbl", label));
+ return b;
+ };
+
+ // BRIGHTNESS — the drum pair
+ const bright = fbox("Brightness");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, bright);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, bright);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 210,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ bright.appendChild(dhost);
+ bench.appendChild(bright);
+
+ const rcol = h("div", "fcol");
+
+ // POINTERS — console keys, horizontal
+ const ptr = fbox("Pointers");
+ [["touchpad", "TOUCHPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const b = h("button", "dupre-key ptr", name);
+ if (DS.state[k]) b.classList.add("dupre-green");
+ b.onclick = () => { b.blur(); DS.setToggle(k, ptr); };
+ ptr.appendChild(b);
+ });
+ rcol.appendChild(ptr);
+
+ // IDLE — the tripper dial (size unchanged)
+ const idle = fbox("Idle");
+ const railHost = h("div", "khost");
+ const rail = DUPRE.tripDial(railHost, {
+ stages: IDLE_STAGES.map((st) => ({ ...st })),
+ max: 60,
+ size: 148,
+ legend: true,
+ onChange: (id, minutes, snapshot) => {
+ snapshot.forEach((st) => { const m = IDLE_STAGES.find((o) => o.id === st.id); if (m) m.minutes = st.minutes; });
+ if (booting || !id) return;
+ const st = IDLE_STAGES.find((o) => o.id === id);
+ DS.toast(st.label + (minutes == null ? " stage disabled" : " → idle " + Math.round(minutes) + "m"), true);
+ },
+ });
+ rail.setBypassed(DS.state.caffeine);
+ idle.appendChild(railHost);
+ rcol.appendChild(idle);
+
+ bench.appendChild(rcol);
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-36.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-36.html
new file mode 100644
index 0000000..ac39b16
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-36.html
@@ -0,0 +1,901 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 36 (annotation batch two)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#dab53d; --gold-hi:#ffd75f;
+ --amber-grad-top:#f0d879; --amber-grad-mid:#dab53d; --amber-grad-bot:#8f7a1f;
+ --amber-edge:#7d6a16; --amber-warn:#edc95d;
+ --glow-hi:255,215,95; --glow-lo:218,181,61;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ body.ds-stage {
+ background: radial-gradient(1200px 600px at 70% -10%, #1c1915 0%, transparent 60%), var(--ground);
+ }
+ .ds-panel { width: 540px; background-color: var(--panel); border-width: 1.6px; }
+ .ds-face {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #262320;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
+ }
+ .ds-face .title { color: var(--gold); letter-spacing: .12em; font-size: 15px; }
+ .ds-caption { max-width: 520px; }
+ .bench { display: flex; align-items: stretch; justify-content: center; gap: 12px; }
+ .fcol { display: flex; flex-direction: column; gap: 12px; }
+ .fbox {
+ position: relative;
+ border: 1px solid #35302a;
+ border-radius: 10px;
+ padding: 14px 12px 10px;
+ display: flex; align-items: center; justify-content: center; gap: 10px;
+ }
+ .fbox .flbl {
+ position: absolute; top: -7px; left: 12px;
+ background: var(--raise);
+ padding: 0 7px;
+ color: var(--steel); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
+ }
+ .fbox .dupre-key.ptr { width: 96px; height: 24px; font-size: 8px; letter-spacing: .08em; padding: 0; }
+ .plate {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #2c2823;
+ border-radius: 11px;
+ padding: 10px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 2px 6px rgba(0,0,0,.35);
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ /* ---- wallpaper channel bank ---- */
+ .wp-chans { display: grid; grid-template-columns: repeat(8, 1fr); gap: 5px; }
+ .wp-chan {
+ border: 1px solid #33302b; border-radius: 8px; padding: 4px;
+ background: linear-gradient(180deg, var(--key-top, #23211e), var(--key-bot, #191715));
+ cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 3px;
+ }
+ .wp-chan:hover { border-color: var(--gold); }
+ .wp-chan.on { border-color: #a9c95f; box-shadow: 0 0 8px rgba(169,201,95,.25); }
+ .wp-chan .face { width: 100%; aspect-ratio: 16/10; border-radius: 4px; overflow: hidden; position: relative; background: #0c0a08; }
+ .wp-chan .nm { font-size: 7px; letter-spacing: .08em; color: var(--dim); text-transform: uppercase; }
+ .wp-chan.on .nm { color: #a9c95f; }
+ .half { position: absolute; inset: 0; }
+ .half.b { clip-path: polygon(100% 0, 100% 100%, 0 100%); }
+ .deck { position: absolute; inset: 0; }
+ .deck i { position: absolute; width: 68%; height: 68%; border-radius: 3px; border: 1px solid #0009; }
+ .mini-clocks { position: absolute; inset: 2px; display: grid; grid-template-columns: repeat(3,1fr); gap: 1px; }
+ .mini-clocks i { border-radius: 50%; border: 1px solid #4a4438; position: relative; background: #14110e; }
+ .mini-clocks i::after { content: ""; position: absolute; left: 50%; top: 18%; width: 1px; height: 34%; background: var(--gold); transform-origin: bottom; }
+ .mini-clocks i:nth-child(2)::after { transform: rotate(65deg); }
+ .mini-clocks i:nth-child(3)::after { transform: rotate(140deg); }
+ .mini-clocks i:nth-child(4)::after { transform: rotate(-50deg); }
+ .mini-clocks i:nth-child(5)::after { transform: rotate(110deg); }
+ .mini-clocks i:nth-child(6)::after { transform: rotate(-120deg); }
+ .clock-face { position: absolute; inset: 8% 25%; border-radius: 50%; border: 1.5px solid var(--cream); background: #14110e; }
+ .clock-face::before { content: ""; position: absolute; left: 50%; top: 12%; width: 1.5px; height: 40%; background: var(--cream); transform: rotate(40deg); transform-origin: bottom; }
+ .clock-face::after { content: ""; position: absolute; left: 50%; top: 22%; width: 1.5px; height: 30%; background: var(--gold); transform: rotate(-70deg); transform-origin: bottom; }
+ .watch-face { position: absolute; inset: 6% 22%; border-radius: 50%; border: 1px solid #3a5c2a; background: radial-gradient(circle, #0f1a0c, #060905); }
+ .watch-face::after { content: ""; position: absolute; left: 50%; top: 6%; width: 1px; height: 44%; background: #a9c95f; transform: rotate(55deg); transform-origin: bottom; box-shadow: 0 0 4px #a9c95f; }
+ .vid-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #fffc; font-size: 14px; text-shadow: 0 1px 3px #000; }
+
+ .wp-conf { margin-top: 10px; }
+ .wp-lib { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
+ .wp-th { aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .wp-th:hover { border-color: var(--gold); }
+ .wp-th.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .pair-bench { display: flex; align-items: center; gap: 10px; justify-content: center; }
+ .pair-slot { width: 104px; aspect-ratio: 16/10; border: 1.5px dashed #4a443a; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-slot.filled { border-style: solid; }
+ .pair-slot.arm { border-color: var(--gold); }
+ .pair-slot .cap { position: absolute; bottom: 2px; left: 4px; font-size: 7px; letter-spacing: .12em; color: #fff9; text-shadow: 0 1px 2px #000; }
+ .pair-tray { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
+ .pair-chip { width: 64px; aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-chip.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .wp-note { color: var(--dim); font-size: 10px; margin-top: 6px; }
+ .wp-row2 { display: flex; align-items: center; gap: 10px; justify-content: center; margin-top: 8px; }
+ .int-wheel {
+ width: 46px; height: 30px; border-radius: 4px; background: #0a0908; border: 1px solid #3a352c;
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
+ color: #2b2418; font-weight: 700; font-size: 12px; font-family: var(--mono);
+ background: linear-gradient(90deg,#cfc6a8,#ece4c8 50%,#c6bd9e);
+ box-shadow: inset 0 4px 5px rgba(0,0,0,.35), inset 0 -4px 5px rgba(0,0,0,.35);
+ }
+ .ds-toast.empty { visibility: hidden; }
+ .wp-dirs { display: flex; flex-direction: column; gap: 4px; max-height: 100px; overflow-y: auto; padding-right: 4px; }
+ .wp-dirs::-webkit-scrollbar { width: 7px; }
+ .wp-dirs::-webkit-scrollbar-track { background: #0a0908; border-radius: 4px; }
+ .wp-dirs::-webkit-scrollbar-thumb { background: #3a352c; border-radius: 4px; }
+ .wp-dir {
+ display: flex; align-items: center; gap: 8px;
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 6px;
+ padding: 5px 8px; font-size: 11px; color: var(--silver);
+ }
+ .wp-dir .path { color: var(--dim); flex: 1; }
+ .wp-dir .rm { color: var(--dim); cursor: pointer; font-size: 12px; padding: 0 3px; }
+ .wp-dir .rm:hover { color: var(--fail); }
+ .pair-chip .rm {
+ position: absolute; top: 1px; right: 2px; color: #fffa; cursor: pointer;
+ font-size: 10px; text-shadow: 0 1px 2px #000; display: none; padding: 0 2px;
+ }
+ .pair-chip:hover .rm { display: block; }
+ .pair-chip.armed { border-color: var(--fail); box-shadow: 0 0 6px rgba(203,107,77,.5); }
+ .pair-chip.armed .rm { display: block; color: var(--fail); }
+ .wp-well {
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 8px;
+ padding: 10px;
+ }
+ .wp-scroll { display: flex; gap: 8px; overflow-x: auto; padding: 0 0 12px; }
+ .wp-scroll::-webkit-scrollbar { height: 7px; }
+ .wp-scroll::-webkit-scrollbar-track { background: #0a0908; border-radius: 4px; }
+ .wp-scroll::-webkit-scrollbar-thumb { background: #3a352c; border-radius: 4px; }
+ .wp-scroll::-webkit-scrollbar-thumb:hover { background: var(--gold); }
+ .wp-scroll .wp-th { flex: 0 0 62px; }
+ .sorted-row { display: flex; gap: 8px; align-items: stretch; }
+ .sorted-row .sortcol { display: flex; flex-direction: column; gap: 6px; justify-content: flex-start; padding-top: 10px; }
+ .sorted-row .wp-well { flex: 1; min-width: 0; }
+ .count-badge {
+ position: absolute; top: 2px; right: 3px; background: rgba(0,0,0,.65);
+ color: var(--cream); font-size: 8px; font-family: var(--mono);
+ border-radius: 4px; padding: 0 4px;
+ }
+ .color-chip { border: 1.5px solid #33302b; }
+ input[type="color"].wp-picker {
+ width: 54px; height: 32px; border: 1px solid #3a352c; border-radius: 6px;
+ background: #0a0908; padding: 2px; cursor: pointer;
+ }
+ .wp-desc {
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 8px;
+ color: var(--steel); font-size: 11px; line-height: 1.5; padding: 8px 10px; margin-top: 8px;
+ }
+ .wp-prev {
+ width: 100%; aspect-ratio: 16/9; border-radius: 8px; border: 1px solid #2c2823;
+ overflow: hidden; position: relative; background: #0c0a08;
+ }
+ .wp-prev img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .sortkeys { display: flex; flex-direction: column; gap: 6px; }
+ .sortkeys .dupre-key.srt { width: 30px; height: 20px; font-size: 9px; padding: 0; }
+ .pair-scroll .pair-cell { flex: 0 0 66px; display: flex; flex-direction: column; }
+ .pair-cell .pair-chip { width: 66px; }
+ .pair-foot { display: flex; align-items: center; justify-content: space-between; height: 14px; }
+ .pair-lbl { font-size: 7.5px; color: var(--steel); letter-spacing: .04em; overflow: hidden; white-space: nowrap; }
+ .pbtn { color: var(--dim); cursor: pointer; font-size: 10px; padding: 0 2px; }
+ .pbtn:hover { color: var(--gold); }
+ .press-side { display: flex; flex-direction: column; gap: 6px; }
+ .wp-lblin {
+ background: #0a0908; border: 1px solid #3a352c; border-radius: 5px;
+ color: var(--cream); font-family: var(--mono); font-size: 10px; padding: 4px 7px; width: 120px;
+ }
+ .wp-lblin::placeholder { color: var(--dim); }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 36 — annotation batch two.</b> Uniform slim preset keys;
+ pointers above brightness with the dial enlarged, legend below;
+ SOLID COLOR joins the channels; RANDOM gains labeled, count-badged
+ SETS; sort keys stack left of sortable galleries; custom scrollbars
+ with even framing; scrollable sources; a dozen seeded pairs so the
+ scroller shows itself.
+ </div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">SETTINGS</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "=", "\u2193"]; // up / equals / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "=", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "=": "balanced", "\u2193": "saver" };
+let booting = true;
+// the idle policy the rail programs (mock persistence across renders)
+const IDLE_STAGES = [
+ { id: "dim", label: "DIM", color: "#d29638", minutes: 5 },
+ { id: "lock", label: "LOCK · WATCH", color: "#54677d", minutes: 7 },
+ { id: "dpms", label: "SCREEN OFF", color: "#969385", minutes: 10 },
+ { id: "suspend", label: "SUSPEND", color: "#cb6b4d", minutes: 30 },
+];
+
+// two unassigned program slots (blank keys, all-off programs)
+["Slot5", "Slot6", "Slot7", "Slot8"].forEach((n) => { if (!DS.SCENES[n]) DS.SCENES[n] = { glyph: "", sets: {} }; });
+
+// factory presets (Craig, 2026-07-22) — rows: AUTO-DIM, NIGHT LIGHT, DND, CAFFEINE + CPU
+DS.SCENES.Focus.sets = { autodim: true, nightlight: false, dnd: true, caffeine: true, powerprofile: "performance" };
+DS.SCENES.Presentation.sets = { autodim: false, nightlight: false, dnd: true, caffeine: true, powerprofile: "balanced" };
+DS.SCENES.Battery.sets = { autodim: true, nightlight: false, dnd: false, caffeine: false, powerprofile: "saver" };
+DS.SCENES.Night.sets = { autodim: true, nightlight: true, dnd: false, caffeine: true, powerprofile: "saver" };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+// ---- wallpaper: channels state (mock persistence) -------------------------
+const SWATCH = [
+ "linear-gradient(135deg,#2a3d5c,#101b2e)", "linear-gradient(135deg,#5c3a2a,#2e1810)",
+ "linear-gradient(135deg,#3a5c2a,#16240f)", "linear-gradient(135deg,#40364f,#1b1626)",
+ "linear-gradient(135deg,#1a1e2e,#05070d)", "linear-gradient(135deg,#5c5030,#2e2810)",
+ "linear-gradient(135deg,#6b4a5c,#2e1420)", "linear-gradient(135deg,#2a5c55,#0f2420)",
+ "linear-gradient(135deg,#7a6248,#3a2c1a)", "linear-gradient(135deg,#23364a,#0a1018)",
+ "linear-gradient(135deg,#4a5a3a,#1c2410)", "linear-gradient(135deg,#5a3a4a,#240f1c)",
+];
+const VIDEOS = ["linear-gradient(160deg,#38506b,#0d1520)", "linear-gradient(160deg,#5c4a2a,#241a08)", "linear-gradient(160deg,#2c4a3e,#0a1a12)"];
+const WP = {
+ channel: "single",
+ single: 2,
+ pairs: [
+ { day: 1, night: 4, label: "harbor", ts: 1 }, { day: 8, night: 9, label: "", ts: 2 },
+ { day: 0, night: 4, label: "coast", ts: 3 }, { day: 2, night: 10, label: "forest", ts: 4 },
+ { day: 5, night: 11, label: "amber", ts: 5 }, { day: 6, night: 3, label: "", ts: 6 },
+ { day: 7, night: 9, label: "reef", ts: 7 }, { day: 8, night: 4, label: "dune", ts: 8 },
+ { day: 3, night: 11, label: "violet", ts: 9 }, { day: 10, night: 2, label: "", ts: 10 },
+ { day: 9, night: 0, label: "slate", ts: 11 }, { day: 11, night: 5, label: "plum", ts: 12 },
+ ],
+ pairSel: 0,
+ sets: [{ pics: [0, 2, 5, 8], label: "earthtones", ts: 1 }],
+ setSel: 0,
+ setBuild: { pics: [], label: "" },
+ colors: [{ c: "#3a5c2a", label: "moss", ts: 1 }, { c: "#54677d", label: "slate", ts: 2 }],
+ colorSel: 0,
+ colorBuild: { c: "#dab53d", label: "" },
+ video: 0,
+ interval: 30,
+ building: { day: null, night: null, label: "", arm: null },
+ dirs: ["~/pictures/wallpaper", "~/pictures/wallpaper/day-night"],
+ sort: "time",
+ mintSeq: 13,
+};
+const INTERVALS = [5, 10, 15, 30, 60];
+const CHANNELS = [
+ { id: "watch", nm: "Watch" },
+ { id: "pair", nm: "Sun pair" },
+ { id: "single", nm: "Single" },
+ { id: "solid", nm: "Solid" },
+ { id: "random", nm: "Random" },
+ { id: "video", nm: "Video" },
+ { id: "clock", nm: "Clock" },
+ { id: "world", nm: "World" },
+];
+const SOURCED = ["single", "pair", "random", "video"]; // solid + generators hide sources
+const DESC = {
+ solid: "A single flat color fills the screen. Mint colors from the picker below — they persist like pairs — and select one from the shelf.",
+ watch: "Plays the Dupre night watch: the instrument board keeps real time while the machine idles. The same board is the lock screen's face.",
+ pair: "Two pictures, one day. The day picture shows from sunrise, the night picture from sundown — sun-times from your location. Pick a minted pair from the gallery, or press a new one below: fill both frames from the pictures, name it if you like, and mint.",
+ single: "One picture, always. Pick it from the gallery; the preview shows what ships.",
+ random: "Draws a fresh picture from your sources on the wheel's interval. Click the wheel to change the minutes.",
+ video: "Plays a video loop as the wallpaper, aerial style. Pick the loop from the gallery.",
+ clock: "Fills the screen with the big clock — the same clock the time module opens when clicked.",
+ world: "Fills the screen with a matrix of world clocks — the cities and timezones from the time module's hover.",
+};
+const DIR_CANDIDATES = ["~/pictures/travel", "~/pictures/astro", "~/downloads/walls", "~/pictures/scans"];
+
+function chanFace(id) {
+ const f = h("div", "face");
+ if (id === "single") { const i = h("div", "half"); i.style.background = SWATCH[WP.single]; f.appendChild(i); }
+ else if (id === "pair") {
+ const p = WP.pairs[WP.pairSel] || WP.pairs[0];
+ const a = h("div", "half"); a.style.background = p ? SWATCH[p.day] : "#222";
+ const b = h("div", "half b"); b.style.background = p ? SWATCH[p.night] : "#111";
+ f.appendChild(a); f.appendChild(b);
+ } else if (id === "random") {
+ const set = WP.sets[WP.setSel];
+ const i = h("div", "half"); i.style.background = set ? SWATCH[set.pics[0]] : "#222";
+ f.appendChild(i);
+ if (set) f.appendChild(h("span", "count-badge", set.pics.length));
+ } else if (id === "solid") {
+ const c = WP.colors[WP.colorSel];
+ const i = h("div", "half"); i.style.background = c ? c.c : "#444";
+ f.appendChild(i);
+ } else if (id === "video") {
+ const i = h("div", "half"); i.style.background = VIDEOS[WP.video]; f.appendChild(i);
+ f.appendChild(h("div", "vid-play", "\u25B6"));
+ } else if (id === "clock") f.appendChild(h("div", "clock-face"));
+ else if (id === "world") {
+ const m = h("div", "mini-clocks");
+ for (let i = 0; i < 6; i++) m.appendChild(h("i"));
+ f.appendChild(m);
+ } else if (id === "watch") f.appendChild(h("div", "watch-face"));
+ return f;
+}
+
+function scrollGallery(cls) {
+ return h("div", "wp-scroll" + (cls ? " " + cls : ""));
+}
+
+function wallpaperView() {
+ const sv = h("div", "ds-col");
+ const head = h("div", "ds-sub-head");
+ head.innerHTML = `<span class="back">\u2039 Back</span><span class="title">Wallpaper</span>`;
+ head.querySelector(".back").onclick = () => subview.classList.remove("open");
+ sv.appendChild(head);
+ const body2 = h("div", "ds-col");
+ sv.appendChild(body2);
+
+ const paint = () => {
+ body2.innerHTML = "";
+
+ // modes — labeled and divided like every family section
+ body2.appendChild(DS.engrave("Modes"));
+ const bank = h("div", "wp-chans");
+ CHANNELS.forEach((c) => {
+ const card = h("div", "wp-chan" + (WP.channel === c.id ? " on" : ""));
+ card.appendChild(chanFace(c.id));
+ card.appendChild(h("div", "nm", c.nm));
+ card.onclick = () => { WP.channel = c.id; DS.toast("Wallpaper: " + c.nm, true); paint(); };
+ bank.appendChild(card);
+ });
+ body2.appendChild(bank);
+
+ // description — helpful text for every mode, never "no options"
+ body2.appendChild(h("div", "wp-desc", DESC[WP.channel]));
+
+ const conf = h("div", "wp-conf");
+ const previewBox = (bg, imgSrc) => {
+ const pv = h("div", "wp-prev");
+ if (imgSrc) { const img = h("img"); img.src = imgSrc; pv.appendChild(img); }
+ else if (bg) pv.style.background = bg;
+ return pv;
+ };
+
+ const sortCol = () => {
+ const c = h("div", "sortcol");
+ [["alpha", "a"], ["time", "\u{f0150}"]].forEach(([mode, glyph]) => {
+ const k = h("button", "dupre-key srt" + (WP.sort === mode ? " dupre-on" : ""), glyph);
+ k.onclick = () => { k.blur(); WP.sort = mode; paint(); };
+ c.appendChild(k);
+ });
+ return c;
+ };
+ const sortedRow = (wellContent) => {
+ const row = h("div", "sorted-row");
+ row.appendChild(sortCol());
+ const well = h("div", "wp-well");
+ well.appendChild(wellContent);
+ row.appendChild(well);
+ return row;
+ };
+ const sortView = (arr) => {
+ const order = arr.map((o, i) => ({ o, i }));
+ if (WP.sort === "alpha") order.sort((x, y) => (x.o.label || "\uffff").localeCompare(y.o.label || "\uffff"));
+ else order.sort((x, y) => y.o.ts - x.o.ts);
+ return order;
+ };
+ const chipFoot = (label, onEdit, onDel) => {
+ const foot = h("div", "pair-foot");
+ const ed = h("span", "pbtn", "~"); ed.title = "edit — reopens below"; ed.onclick = onEdit;
+ foot.appendChild(ed);
+ foot.appendChild(h("span", "pair-lbl", label || ""));
+ const del = h("span", "pbtn", "\u2212"); del.title = "delete — recreate to get it back"; del.onclick = onDel;
+ foot.appendChild(del);
+ return foot;
+ };
+ const galleryWell = (content) => {
+ const well = h("div", "wp-well");
+ well.appendChild(content);
+ return well;
+ };
+
+ if (WP.channel === "single") {
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th" + (i === WP.single ? " sel" : ""));
+ t.style.background = sw;
+ t.onclick = () => { WP.single = i; DS.toast("Wallpaper set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(galleryWell(g));
+ conf.appendChild(DS.engrave("Preview"));
+ conf.appendChild(previewBox(SWATCH[WP.single]));
+ } else if (WP.channel === "pair") {
+ conf.appendChild(DS.engrave("Minted pairs"));
+ const tray = scrollGallery("pair-scroll");
+ sortView(WP.pairs).forEach(({ o: p, i }) => {
+ const cell = h("div", "pair-cell");
+ const chip = h("div", "pair-chip" + (i === WP.pairSel ? " sel" : ""));
+ const a = h("div", "half"); a.style.background = SWATCH[p.day];
+ const b = h("div", "half b"); b.style.background = SWATCH[p.night];
+ chip.appendChild(a); chip.appendChild(b);
+ chip.onclick = () => { WP.pairSel = i; DS.toast("Pair selected", true); paint(); };
+ cell.appendChild(chip);
+ cell.appendChild(chipFoot(p.label,
+ () => {
+ WP.building = { day: p.day, night: p.night, label: p.label, arm: null };
+ WP.pairs.splice(i, 1);
+ if (WP.pairSel >= WP.pairs.length) WP.pairSel = WP.pairs.length - 1;
+ DS.toast("Pair opened in the press", true);
+ paint();
+ },
+ () => {
+ WP.pairs.splice(i, 1);
+ if (WP.pairSel >= WP.pairs.length) WP.pairSel = WP.pairs.length - 1;
+ DS.toast("Pair deleted", true);
+ paint();
+ }));
+ tray.appendChild(cell);
+ });
+ conf.appendChild(sortedRow(tray));
+
+ conf.appendChild(DS.engrave("Pair press"));
+ const bench = h("div", "pair-bench");
+ ["day", "night"].forEach((k) => {
+ const slot = h("div", "pair-slot" +
+ (WP.building[k] !== null ? " filled" : "") + (WP.building.arm === k ? " arm" : ""));
+ if (WP.building[k] !== null) slot.style.background = SWATCH[WP.building[k]];
+ slot.appendChild(h("span", "cap", k === "day" ? "\u2600 DAY" : "\u263E NIGHT"));
+ slot.onclick = () => { WP.building.arm = WP.building.arm === k ? null : k; paint(); };
+ bench.appendChild(slot);
+ });
+ const side = h("div", "press-side");
+ const lbl = h("input", "wp-lblin");
+ lbl.placeholder = "label (optional)";
+ lbl.value = WP.building.label || "";
+ lbl.oninput = () => { WP.building.label = lbl.value; };
+ side.appendChild(lbl);
+ const mint = h("button", "dupre-key", "MINT PAIR");
+ mint.style.cssText = "height:26px;font-size:8px;padding:0 10px";
+ mint.onclick = () => {
+ mint.blur();
+ if (WP.building.day === null || WP.building.night === null) { DS.toast("fill both frames first", false); return; }
+ WP.pairs.push({ day: WP.building.day, night: WP.building.night, label: WP.building.label || "", ts: WP.mintSeq++ });
+ WP.pairSel = WP.pairs.length - 1;
+ WP.building = { day: null, night: null, label: "", arm: null };
+ DS.toast("Pair minted", true);
+ paint();
+ };
+ side.appendChild(mint);
+ bench.appendChild(side);
+ conf.appendChild(bench);
+ if (WP.building.arm) conf.appendChild(h("div", "wp-note", "click a picture below to fill the " + WP.building.arm.toUpperCase() + " frame"));
+
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th");
+ t.style.background = sw;
+ t.onclick = () => {
+ if (!WP.building.arm) { DS.toast("click a DAY or NIGHT frame first", false); return; }
+ WP.building[WP.building.arm] = i;
+ WP.building.arm = WP.building.arm === "day" && WP.building.night === null ? "night" : null;
+ paint();
+ };
+ g.appendChild(t);
+ });
+ conf.appendChild(galleryWell(g));
+ conf.appendChild(h("div", "wp-note", "Only picture types the lock screen can display appear here."));
+ } else if (WP.channel === "solid") {
+ conf.appendChild(DS.engrave("Minted colors"));
+ const tray = scrollGallery("pair-scroll");
+ sortView(WP.colors).forEach(({ o: c, i }) => {
+ const cell = h("div", "pair-cell");
+ const chip = h("div", "pair-chip color-chip" + (i === WP.colorSel ? " sel" : ""));
+ chip.style.background = c.c;
+ chip.onclick = () => { WP.colorSel = i; DS.toast("Color selected", true); paint(); };
+ cell.appendChild(chip);
+ cell.appendChild(chipFoot(c.label,
+ () => {
+ WP.colorBuild = { c: c.c, label: c.label };
+ WP.colors.splice(i, 1);
+ if (WP.colorSel >= WP.colors.length) WP.colorSel = WP.colors.length - 1;
+ DS.toast("Color opened in the picker", true);
+ paint();
+ },
+ () => {
+ WP.colors.splice(i, 1);
+ if (WP.colorSel >= WP.colors.length) WP.colorSel = WP.colors.length - 1;
+ DS.toast("Color deleted", true);
+ paint();
+ }));
+ tray.appendChild(cell);
+ });
+ conf.appendChild(sortedRow(tray));
+
+ conf.appendChild(DS.engrave("Color picker"));
+ const row = h("div", "pair-bench");
+ const pick = h("input", "wp-picker");
+ pick.type = "color";
+ pick.value = WP.colorBuild.c;
+ pick.oninput = () => { WP.colorBuild.c = pick.value; };
+ row.appendChild(pick);
+ const side = h("div", "press-side");
+ const lbl = h("input", "wp-lblin");
+ lbl.placeholder = "label (optional)";
+ lbl.value = WP.colorBuild.label || "";
+ lbl.oninput = () => { WP.colorBuild.label = lbl.value; };
+ side.appendChild(lbl);
+ const mint = h("button", "dupre-key", "MINT COLOR");
+ mint.style.cssText = "height:26px;font-size:8px;padding:0 10px";
+ mint.onclick = () => {
+ mint.blur();
+ WP.colors.push({ c: WP.colorBuild.c, label: WP.colorBuild.label || "", ts: WP.mintSeq++ });
+ WP.colorSel = WP.colors.length - 1;
+ WP.colorBuild = { c: WP.colorBuild.c, label: "" };
+ DS.toast("Color minted", true);
+ paint();
+ };
+ side.appendChild(mint);
+ row.appendChild(side);
+ conf.appendChild(row);
+ } else if (WP.channel === "random") {
+ conf.appendChild(DS.engrave("Minted sets"));
+ const tray = scrollGallery("pair-scroll");
+ sortView(WP.sets).forEach(({ o: st, i }) => {
+ const cell = h("div", "pair-cell");
+ const chip = h("div", "pair-chip" + (i === WP.setSel ? " sel" : ""));
+ chip.style.background = SWATCH[st.pics[0]];
+ chip.appendChild(h("span", "count-badge", st.pics.length));
+ chip.onclick = () => { WP.setSel = i; DS.toast("Set selected", true); paint(); };
+ cell.appendChild(chip);
+ cell.appendChild(chipFoot(st.label,
+ () => {
+ WP.setBuild = { pics: st.pics.slice(), label: st.label };
+ WP.sets.splice(i, 1);
+ if (WP.setSel >= WP.sets.length) WP.setSel = WP.sets.length - 1;
+ DS.toast("Set opened below", true);
+ paint();
+ },
+ () => {
+ WP.sets.splice(i, 1);
+ if (WP.setSel >= WP.sets.length) WP.setSel = WP.sets.length - 1;
+ DS.toast("Set deleted", true);
+ paint();
+ }));
+ tray.appendChild(cell);
+ });
+ conf.appendChild(sortedRow(tray));
+
+ conf.appendChild(DS.engrave("Set press — click pictures to add"));
+ const bRow = h("div", "wp-well");
+ const bTray = scrollGallery();
+ if (WP.setBuild.pics.length === 0) bTray.appendChild(h("div", "wp-note", "empty — click pictures below"));
+ WP.setBuild.pics.forEach((pi, k) => {
+ const t = h("div", "wp-th");
+ t.style.background = SWATCH[pi];
+ t.title = "click to remove from the set";
+ t.onclick = () => { WP.setBuild.pics.splice(k, 1); paint(); };
+ bTray.appendChild(t);
+ });
+ bRow.appendChild(bTray);
+ conf.appendChild(bRow);
+ const side = h("div", "pair-bench");
+ const lbl = h("input", "wp-lblin");
+ lbl.placeholder = "label (optional)";
+ lbl.value = WP.setBuild.label || "";
+ lbl.oninput = () => { WP.setBuild.label = lbl.value; };
+ side.appendChild(lbl);
+ const mint = h("button", "dupre-key", "MINT SET");
+ mint.style.cssText = "height:26px;font-size:8px;padding:0 10px";
+ mint.onclick = () => {
+ mint.blur();
+ if (WP.setBuild.pics.length < 2) { DS.toast("a set needs at least two pictures", false); return; }
+ WP.sets.push({ pics: WP.setBuild.pics.slice(), label: WP.setBuild.label || "", ts: WP.mintSeq++ });
+ WP.setSel = WP.sets.length - 1;
+ WP.setBuild = { pics: [], label: "" };
+ DS.toast("Set minted", true);
+ paint();
+ };
+ side.appendChild(mint);
+ const row2 = h("div", "wp-row2");
+ row2.appendChild(h("span", "wp-note", "next draw every"));
+ const wheel = h("div", "int-wheel", WP.interval);
+ wheel.onclick = () => {
+ WP.interval = INTERVALS[(INTERVALS.indexOf(WP.interval) + 1) % INTERVALS.length];
+ DS.toast("Random interval: " + WP.interval + "m", true);
+ paint();
+ };
+ row2.appendChild(wheel);
+ row2.appendChild(h("span", "wp-note", "minutes"));
+ side.appendChild(row2);
+ conf.appendChild(side);
+
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th");
+ t.style.background = sw;
+ t.onclick = () => {
+ if (WP.setBuild.pics.includes(i)) { DS.toast("already in the set", false); return; }
+ WP.setBuild.pics.push(i);
+ paint();
+ };
+ g.appendChild(t);
+ });
+ conf.appendChild(galleryWell(g));
+ } else if (WP.channel === "video") {
+ conf.appendChild(DS.engrave("Videos"));
+ const g = scrollGallery();
+ VIDEOS.forEach((v, i) => {
+ const t = h("div", "wp-th" + (i === WP.video ? " sel" : ""));
+ t.style.background = v;
+ t.appendChild(h("div", "vid-play", "\u25B6"));
+ t.onclick = () => { WP.video = i; DS.toast("Video set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(galleryWell(g));
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox(VIDEOS[WP.video]);
+ pv.appendChild(h("div", "vid-play", "\u25B6"));
+ conf.appendChild(pv);
+ } else if (WP.channel === "watch") {
+ conf.appendChild(DS.engrave("Preview"));
+ conf.appendChild(previewBox(null, "2026-07-22-watch-preview.png"));
+ } else if (WP.channel === "clock") {
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox("#0c0a08");
+ const cf = h("div", "clock-face"); cf.style.inset = "6% 34%";
+ pv.appendChild(cf);
+ conf.appendChild(pv);
+ } else if (WP.channel === "world") {
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox("#0c0a08");
+ const m = h("div", "mini-clocks"); m.style.inset = "8%";
+ for (let i = 0; i < 6; i++) m.appendChild(h("i"));
+ pv.appendChild(m);
+ conf.appendChild(pv);
+ }
+ body2.appendChild(conf);
+
+ // sources: only for the modes that draw from directories
+ if (SOURCED.includes(WP.channel)) {
+ const src = h("div", "wp-conf");
+ src.appendChild(DS.engrave("Sources"));
+ const dirs = h("div", "wp-dirs");
+ WP.dirs.forEach((d, i) => {
+ const row = h("div", "wp-dir");
+ row.innerHTML = `<span>\u{f024b}</span>`;
+ row.appendChild(h("span", "path", d));
+ const rm = h("span", "rm", "\u2715");
+ rm.onclick = () => {
+ if (WP.dirs.length === 1) { DS.toast("keep at least one source", false); return; }
+ WP.dirs.splice(i, 1);
+ DS.toast("Source removed", true);
+ paint();
+ };
+ row.appendChild(rm);
+ dirs.appendChild(row);
+ });
+ src.appendChild(dirs);
+ const add = h("button", "dupre-key", "ADD DIRECTORY");
+ add.style.cssText = "height:24px;font-size:8px;padding:0 10px;margin-top:6px";
+ add.onclick = () => {
+ add.blur();
+ const next = DIR_CANDIDATES.find((c) => !WP.dirs.includes(c));
+ if (!next) { DS.toast("no more candidates in the mock", false); return; }
+ WP.dirs.push(next);
+ DS.toast("Source added: " + next, true);
+ paint();
+ };
+ src.appendChild(add);
+ body2.appendChild(src);
+ }
+ };
+ paint();
+ return sv;
+}
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(wallpaperView());
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Programmable presets"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", glyph: "\u{f0210}", scene: "Focus" }, // crosshairs — focus
+ { id: "PRESENT", label: "PRESENT", glyph: "\u{f0379}", scene: "Presentation" }, // projector screen
+ { id: "BATTERY", label: "BATTERY", glyph: "\u{f007e}", scene: "Battery" }, // half battery
+ { id: "NIGHT", label: "NIGHT", glyph: "\u{f0594}", scene: "Night" }, // crescent moon
+ { id: "S5", label: "1", scene: "Slot5" },
+ { id: "S6", label: "2", scene: "Slot6" },
+ { id: "S7", label: "3", scene: "Slot7" },
+ { id: "S8", label: "4", scene: "Slot8" },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label, glyph, slim }) => ({ id, label, glyph, slim }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 436,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — pointers over brightness; the idle dial alone -
+ const lv = h("div", "plate");
+ const bench = h("div", "bench");
+
+ const fbox = (label) => {
+ const b = h("div", "fbox");
+ b.appendChild(h("span", "flbl", label));
+ return b;
+ };
+
+ const lcol = h("div", "fcol");
+
+ // POINTERS — console keys, horizontal (above brightness per batch two)
+ const ptr = fbox("Pointers");
+ [["touchpad", "TOUCHPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const b = h("button", "dupre-key ptr", name);
+ if (DS.state[k]) b.classList.add("dupre-green");
+ b.onclick = () => { b.blur(); DS.setToggle(k, ptr); };
+ ptr.appendChild(b);
+ });
+ lcol.appendChild(ptr);
+
+ // BRIGHTNESS — the drum pair, centered in its frame
+ const bright = fbox("Brightness");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, bright);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, bright);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 196,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ bright.appendChild(dhost);
+ lcol.appendChild(bright);
+ bench.appendChild(lcol);
+
+ // IDLE — enlarged dial, legend beneath, alone in its frame
+ const idle = fbox("Idle");
+ const railHost = h("div", "khost");
+ const rail = DUPRE.tripDial(railHost, {
+ stages: IDLE_STAGES.map((st) => ({ ...st })),
+ max: 60,
+ size: 184,
+ legend: true,
+ legendPos: "bottom",
+ onChange: (id, minutes, snapshot) => {
+ snapshot.forEach((st) => { const m = IDLE_STAGES.find((o) => o.id === st.id); if (m) m.minutes = st.minutes; });
+ if (booting || !id) return;
+ const st = IDLE_STAGES.find((o) => o.id === id);
+ DS.toast(st.label + (minutes == null ? " stage disabled" : " → idle " + Math.round(minutes) + "m"), true);
+ },
+ });
+ rail.setBypassed(DS.state.caffeine);
+ idle.appendChild(railHost);
+ bench.appendChild(idle);
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-37.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-37.html
new file mode 100644
index 0000000..952b9d1
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-37.html
@@ -0,0 +1,915 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 37 (annotation batch three)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#dab53d; --gold-hi:#ffd75f;
+ --amber-grad-top:#f0d879; --amber-grad-mid:#dab53d; --amber-grad-bot:#8f7a1f;
+ --amber-edge:#7d6a16; --amber-warn:#edc95d;
+ --glow-hi:255,215,95; --glow-lo:218,181,61;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ body.ds-stage {
+ background: radial-gradient(1200px 600px at 70% -10%, #1c1915 0%, transparent 60%), var(--ground);
+ }
+ .ds-panel { width: 540px; background-color: var(--panel); border-width: 1.6px; }
+ .ds-face {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #262320;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
+ }
+ .ds-face .title { color: var(--gold); letter-spacing: .12em; font-size: 15px; }
+ .ds-caption { max-width: 520px; }
+ .bench { display: flex; align-items: stretch; justify-content: center; gap: 12px; }
+ .fcol { display: flex; flex-direction: column; gap: 12px; }
+ .fbox {
+ position: relative;
+ border: 1px solid #35302a;
+ border-radius: 10px;
+ padding: 14px 12px 10px;
+ display: flex; align-items: center; justify-content: center; gap: 10px;
+ }
+ .fbox .flbl {
+ position: absolute; top: -7px; left: 12px;
+ background: var(--raise);
+ padding: 0 7px;
+ color: var(--steel); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
+ }
+ .fbox .dupre-key.ptr { width: 96px; height: 24px; font-size: 8px; letter-spacing: .08em; padding: 0; }
+ .plate {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #2c2823;
+ border-radius: 11px;
+ padding: 10px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 2px 6px rgba(0,0,0,.35);
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ /* ---- wallpaper channel bank ---- */
+ .wp-chans { display: grid; grid-template-columns: repeat(8, 1fr); gap: 5px; }
+ .wp-chan {
+ border: 1px solid #33302b; border-radius: 8px; padding: 4px;
+ background: linear-gradient(180deg, var(--key-top, #23211e), var(--key-bot, #191715));
+ cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 3px;
+ }
+ .wp-chan:hover { border-color: var(--gold); }
+ .wp-chan.on { border-color: #a9c95f; box-shadow: 0 0 8px rgba(169,201,95,.25); }
+ .wp-chan .face { width: 100%; aspect-ratio: 16/10; border-radius: 4px; overflow: hidden; position: relative; background: #0c0a08; }
+ .wp-chan .nm { font-size: 7px; letter-spacing: .08em; color: var(--dim); text-transform: uppercase; }
+ .wp-chan.on .nm { color: #a9c95f; }
+ .half { position: absolute; inset: 0; }
+ .half.b { clip-path: polygon(100% 0, 100% 100%, 0 100%); }
+ .deck { position: absolute; inset: 0; }
+ .deck i { position: absolute; width: 68%; height: 68%; border-radius: 3px; border: 1px solid #0009; }
+ .mini-clocks { position: absolute; inset: 3px; display: grid; grid-template-columns: repeat(3,1fr); gap: 2px; justify-items: center; align-items: center; }
+ .mini-clocks i {
+ width: 86%; aspect-ratio: 1; border-radius: 50%; position: relative;
+ background: radial-gradient(circle at 40% 32%, #f2ecd8, #cfc8b6);
+ border: 1px solid #4a4438;
+ }
+ .mini-clocks i::before, .mini-clocks i::after {
+ content: ""; position: absolute; left: 50%; background: #2b2418; transform-origin: bottom;
+ }
+ .mini-clocks i::before { top: 26%; width: 1.5px; height: 24%; } /* hour */
+ .mini-clocks i::after { top: 14%; width: 1px; height: 36%; } /* minute */
+ .mini-clocks i:nth-child(1)::before { transform: rotate(40deg); }
+ .mini-clocks i:nth-child(2)::before { transform: rotate(105deg); } .mini-clocks i:nth-child(2)::after { transform: rotate(-60deg); }
+ .mini-clocks i:nth-child(3)::before { transform: rotate(160deg); } .mini-clocks i:nth-child(3)::after { transform: rotate(80deg); }
+ .mini-clocks i:nth-child(4)::before { transform: rotate(-75deg); } .mini-clocks i:nth-child(4)::after { transform: rotate(150deg); }
+ .mini-clocks i:nth-child(5)::before { transform: rotate(15deg); } .mini-clocks i:nth-child(5)::after { transform: rotate(-130deg); }
+ .mini-clocks i:nth-child(6)::before { transform: rotate(-140deg); } .mini-clocks i:nth-child(6)::after { transform: rotate(30deg); }
+ .face-glyph { position: absolute; font-size: 11px; }
+ .face-glyph.sun { left: 14%; top: 10%; color: #6b4e08; }
+ .face-glyph.moon { right: 14%; bottom: 8%; color: #e8e2cc; }
+ .wp-prev.sm { width: 55%; margin: 0 auto; }
+ .clock-face { position: absolute; inset: 8% 25%; border-radius: 50%; border: 1.5px solid var(--cream); background: #14110e; }
+ .clock-face::before { content: ""; position: absolute; left: 50%; top: 12%; width: 1.5px; height: 40%; background: var(--cream); transform: rotate(40deg); transform-origin: bottom; }
+ .clock-face::after { content: ""; position: absolute; left: 50%; top: 22%; width: 1.5px; height: 30%; background: var(--gold); transform: rotate(-70deg); transform-origin: bottom; }
+ .watch-face { position: absolute; inset: 6% 22%; border-radius: 50%; border: 1px solid #3a5c2a; background: radial-gradient(circle, #0f1a0c, #060905); }
+ .watch-face::after { content: ""; position: absolute; left: 50%; top: 6%; width: 1px; height: 44%; background: #a9c95f; transform: rotate(55deg); transform-origin: bottom; box-shadow: 0 0 4px #a9c95f; }
+ .vid-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #fffc; font-size: 14px; text-shadow: 0 1px 3px #000; }
+
+ .wp-conf { margin-top: 10px; }
+ .wp-lib { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
+ .wp-th { aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .wp-th:hover { border-color: var(--gold); }
+ .wp-th.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .pair-bench { display: flex; align-items: center; gap: 10px; justify-content: center; }
+ .pair-slot { width: 104px; aspect-ratio: 16/10; border: 1.5px dashed #4a443a; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-slot.filled { border-style: solid; }
+ .pair-slot.arm { border-color: var(--gold); }
+ .pair-slot .cap { position: absolute; bottom: 2px; left: 4px; font-size: 7px; letter-spacing: .12em; color: #fff9; text-shadow: 0 1px 2px #000; }
+ .pair-tray { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
+ .pair-chip { width: 64px; aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-chip.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .wp-note { color: var(--dim); font-size: 10px; margin-top: 6px; }
+ .wp-row2 { display: flex; align-items: center; gap: 10px; justify-content: center; margin-top: 8px; }
+ .int-wheel {
+ width: 46px; height: 30px; border-radius: 4px; background: #0a0908; border: 1px solid #3a352c;
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
+ color: #2b2418; font-weight: 700; font-size: 12px; font-family: var(--mono);
+ background: linear-gradient(90deg,#cfc6a8,#ece4c8 50%,#c6bd9e);
+ box-shadow: inset 0 4px 5px rgba(0,0,0,.35), inset 0 -4px 5px rgba(0,0,0,.35);
+ }
+ .ds-toast.empty { visibility: hidden; }
+ .wp-dirs { display: flex; flex-direction: column; gap: 4px; max-height: 100px; overflow-y: auto; padding-right: 4px; }
+ .wp-dirs::-webkit-scrollbar { width: 7px; }
+ .wp-dirs::-webkit-scrollbar-track { background: #0a0908; border-radius: 4px; }
+ .wp-dirs::-webkit-scrollbar-thumb { background: #3a352c; border-radius: 4px; }
+ .wp-dir {
+ display: flex; align-items: center; gap: 8px;
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 6px;
+ padding: 5px 8px; font-size: 11px; color: var(--silver);
+ }
+ .wp-dir .path { color: var(--dim); flex: 1; }
+ .wp-dir .rm { color: var(--dim); cursor: pointer; font-size: 12px; padding: 0 3px; }
+ .wp-dir .rm:hover { color: var(--fail); }
+ .pair-chip .rm {
+ position: absolute; top: 1px; right: 2px; color: #fffa; cursor: pointer;
+ font-size: 10px; text-shadow: 0 1px 2px #000; display: none; padding: 0 2px;
+ }
+ .pair-chip:hover .rm { display: block; }
+ .pair-chip.armed { border-color: var(--fail); box-shadow: 0 0 6px rgba(203,107,77,.5); }
+ .pair-chip.armed .rm { display: block; color: var(--fail); }
+ .wp-well {
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 8px;
+ padding: 10px;
+ }
+ .wp-scroll { display: flex; gap: 8px; overflow-x: auto; padding: 0 0 12px; }
+ .wp-scroll::-webkit-scrollbar { height: 7px; }
+ .wp-scroll::-webkit-scrollbar-track { background: #0a0908; border-radius: 4px; }
+ .wp-scroll::-webkit-scrollbar-thumb { background: #3a352c; border-radius: 4px; }
+ .wp-scroll::-webkit-scrollbar-thumb:hover { background: var(--gold); }
+ .wp-scroll .wp-th { flex: 0 0 62px; }
+ .sorted-row { display: flex; gap: 8px; align-items: stretch; }
+ .sorted-row .sortcol { display: flex; flex-direction: column; gap: 6px; justify-content: flex-start; padding-top: 10px; }
+ .sorted-row .wp-well { flex: 1; min-width: 0; }
+ .count-badge {
+ position: absolute; top: 2px; right: 3px; background: rgba(0,0,0,.65);
+ color: var(--cream); font-size: 8px; font-family: var(--mono);
+ border-radius: 4px; padding: 0 4px;
+ }
+ .color-chip { border: 1.5px solid #33302b; }
+ input[type="color"].wp-picker {
+ width: 54px; height: 32px; border: 1px solid #3a352c; border-radius: 6px;
+ background: #0a0908; padding: 2px; cursor: pointer;
+ }
+ .wp-desc {
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 8px;
+ color: var(--steel); font-size: 11px; line-height: 1.5; padding: 8px 10px; margin-top: 8px;
+ }
+ .wp-prev {
+ width: 100%; aspect-ratio: 16/9; border-radius: 8px; border: 1px solid #2c2823;
+ overflow: hidden; position: relative; background: #0c0a08;
+ }
+ .wp-prev img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .sortkeys { display: flex; flex-direction: column; gap: 6px; }
+ .sortkeys .dupre-key.srt { width: 30px; height: 20px; font-size: 9px; padding: 0; }
+ .pair-scroll .pair-cell { flex: 0 0 66px; display: flex; flex-direction: column; }
+ .pair-cell .pair-chip { width: 66px; }
+ .pair-foot { display: flex; align-items: center; justify-content: space-between; height: 14px; }
+ .pair-lbl { font-size: 7.5px; color: var(--steel); letter-spacing: .04em; overflow: hidden; white-space: nowrap; }
+ .pbtn { color: var(--dim); cursor: pointer; font-size: 10px; padding: 0 2px; }
+ .pbtn:hover { color: var(--gold); }
+ .press-side { display: flex; flex-direction: column; gap: 6px; }
+ .wp-lblin {
+ background: #0a0908; border: 1px solid #3a352c; border-radius: 5px;
+ color: var(--cream); font-family: var(--mono); font-size: 10px; padding: 4px 7px; width: 120px;
+ }
+ .wp-lblin::placeholder { color: var(--dim); }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 37 — annotation batch three.</b> Iconic channel faces
+ (sun/crescent pair card, dupre-blue solid, honest mini clocks); slots
+ announce as 1-4; the idle dial reads exact minutes while dragging and
+ IDLE stands visible; smaller video preview; WALLPAPER capitalized.
+ FOCUS tries the headphones glyph — confirm or veto.
+ </div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">SETTINGS</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "=", "\u2193"]; // up / equals / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "=", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "=": "balanced", "\u2193": "saver" };
+let booting = true;
+// the idle policy the rail programs (mock persistence across renders)
+const IDLE_STAGES = [
+ { id: "dim", label: "DIM", color: "#d29638", minutes: 5 },
+ { id: "lock", label: "LOCK · WATCH", color: "#54677d", minutes: 7 },
+ { id: "dpms", label: "SCREEN OFF", color: "#969385", minutes: 10 },
+ { id: "suspend", label: "SUSPEND", color: "#cb6b4d", minutes: 30 },
+];
+
+// two unassigned program slots (blank keys, all-off programs)
+["1", "2", "3", "4"].forEach((n) => { if (!DS.SCENES[n]) DS.SCENES[n] = { glyph: "", sets: {} }; });
+
+// factory presets (Craig, 2026-07-22) — rows: AUTO-DIM, NIGHT LIGHT, DND, CAFFEINE + CPU
+DS.SCENES.Focus.sets = { autodim: true, nightlight: false, dnd: true, caffeine: true, powerprofile: "performance" };
+DS.SCENES.Presentation.sets = { autodim: false, nightlight: false, dnd: true, caffeine: true, powerprofile: "balanced" };
+DS.SCENES.Battery.sets = { autodim: true, nightlight: false, dnd: false, caffeine: false, powerprofile: "saver" };
+DS.SCENES.Night.sets = { autodim: true, nightlight: true, dnd: false, caffeine: true, powerprofile: "saver" };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+// ---- wallpaper: channels state (mock persistence) -------------------------
+const SWATCH = [
+ "linear-gradient(135deg,#2a3d5c,#101b2e)", "linear-gradient(135deg,#5c3a2a,#2e1810)",
+ "linear-gradient(135deg,#3a5c2a,#16240f)", "linear-gradient(135deg,#40364f,#1b1626)",
+ "linear-gradient(135deg,#1a1e2e,#05070d)", "linear-gradient(135deg,#5c5030,#2e2810)",
+ "linear-gradient(135deg,#6b4a5c,#2e1420)", "linear-gradient(135deg,#2a5c55,#0f2420)",
+ "linear-gradient(135deg,#7a6248,#3a2c1a)", "linear-gradient(135deg,#23364a,#0a1018)",
+ "linear-gradient(135deg,#4a5a3a,#1c2410)", "linear-gradient(135deg,#5a3a4a,#240f1c)",
+];
+const VIDEOS = ["linear-gradient(160deg,#38506b,#0d1520)", "linear-gradient(160deg,#5c4a2a,#241a08)", "linear-gradient(160deg,#2c4a3e,#0a1a12)"];
+const WP = {
+ channel: "single",
+ single: 2,
+ pairs: [
+ { day: 1, night: 4, label: "harbor", ts: 1 }, { day: 8, night: 9, label: "", ts: 2 },
+ { day: 0, night: 4, label: "coast", ts: 3 }, { day: 2, night: 10, label: "forest", ts: 4 },
+ { day: 5, night: 11, label: "amber", ts: 5 }, { day: 6, night: 3, label: "", ts: 6 },
+ { day: 7, night: 9, label: "reef", ts: 7 }, { day: 8, night: 4, label: "dune", ts: 8 },
+ { day: 3, night: 11, label: "violet", ts: 9 }, { day: 10, night: 2, label: "", ts: 10 },
+ { day: 9, night: 0, label: "slate", ts: 11 }, { day: 11, night: 5, label: "plum", ts: 12 },
+ ],
+ pairSel: 0,
+ sets: [{ pics: [0, 2, 5, 8], label: "earthtones", ts: 1 }],
+ setSel: 0,
+ setBuild: { pics: [], label: "" },
+ colors: [{ c: "#3a5c2a", label: "moss", ts: 1 }, { c: "#54677d", label: "slate", ts: 2 }],
+ colorSel: 0,
+ colorBuild: { c: "#dab53d", label: "" },
+ video: 0,
+ interval: 30,
+ building: { day: null, night: null, label: "", arm: null },
+ dirs: ["~/pictures/wallpaper", "~/pictures/wallpaper/day-night"],
+ sort: "time",
+ mintSeq: 13,
+};
+const INTERVALS = [5, 10, 15, 30, 60];
+const CHANNELS = [
+ { id: "watch", nm: "Watch" },
+ { id: "pair", nm: "Sun pair" },
+ { id: "single", nm: "Single" },
+ { id: "solid", nm: "Solid" },
+ { id: "random", nm: "Random" },
+ { id: "video", nm: "Video" },
+ { id: "clock", nm: "Clock" },
+ { id: "world", nm: "World" },
+];
+const SOURCED = ["single", "pair", "random", "video"]; // solid + generators hide sources
+const DESC = {
+ solid: "A single flat color fills the screen. Mint colors from the picker below — they persist like pairs — and select one from the shelf.",
+ watch: "Plays the Dupre night watch: the instrument board keeps real time while the machine idles. The same board is the lock screen's face.",
+ pair: "Two pictures, one day. The day picture shows from sunrise, the night picture from sundown — sun-times from your location. Pick a minted pair from the gallery, or press a new one below: fill both frames from the pictures, name it if you like, and mint.",
+ single: "One picture, always. Pick it from the gallery; the preview shows what ships.",
+ random: "Draws a fresh picture from your sources on the wheel's interval. Click the wheel to change the minutes.",
+ video: "Plays a video loop as the wallpaper, aerial style. Pick the loop from the gallery.",
+ clock: "Fills the screen with the big clock — the same clock the time module opens when clicked.",
+ world: "Fills the screen with a matrix of world clocks — the cities and timezones from the time module's hover.",
+};
+const DIR_CANDIDATES = ["~/pictures/travel", "~/pictures/astro", "~/downloads/walls", "~/pictures/scans"];
+
+function chanFace(id) {
+ const f = h("div", "face");
+ if (id === "single") { const i = h("div", "half"); i.style.background = SWATCH[WP.single]; f.appendChild(i); }
+ else if (id === "pair") {
+ const a = h("div", "half"); a.style.background = "#dab53d";
+ const b = h("div", "half b"); b.style.background = "#1a2438";
+ f.appendChild(a); f.appendChild(b);
+ const sun = h("span", "face-glyph sun", "\u2600");
+ const moon = h("span", "face-glyph moon", "\u263E");
+ f.appendChild(sun); f.appendChild(moon);
+ } else if (id === "random") {
+ const set = WP.sets[WP.setSel];
+ const i = h("div", "half"); i.style.background = set ? SWATCH[set.pics[0]] : "#222";
+ f.appendChild(i);
+ if (set) f.appendChild(h("span", "count-badge", set.pics.length));
+ } else if (id === "solid") {
+ const i = h("div", "half"); i.style.background = "#54677d"; /* dupre blue */
+ f.appendChild(i);
+ } else if (id === "video") {
+ const i = h("div", "half"); i.style.background = VIDEOS[WP.video]; f.appendChild(i);
+ f.appendChild(h("div", "vid-play", "\u25B6"));
+ } else if (id === "clock") f.appendChild(h("div", "clock-face"));
+ else if (id === "world") {
+ const m = h("div", "mini-clocks");
+ for (let i = 0; i < 6; i++) m.appendChild(h("i"));
+ f.appendChild(m);
+ } else if (id === "watch") f.appendChild(h("div", "watch-face"));
+ return f;
+}
+
+function scrollGallery(cls) {
+ return h("div", "wp-scroll" + (cls ? " " + cls : ""));
+}
+
+function wallpaperView() {
+ const sv = h("div", "ds-col");
+ const head = h("div", "ds-sub-head");
+ head.innerHTML = `<span class="back">\u2039 Back</span><span class="title">WALLPAPER</span>`;
+ head.querySelector(".back").onclick = () => subview.classList.remove("open");
+ sv.appendChild(head);
+ const body2 = h("div", "ds-col");
+ sv.appendChild(body2);
+
+ const paint = () => {
+ body2.innerHTML = "";
+
+ // modes — labeled and divided like every family section
+ body2.appendChild(DS.engrave("Modes"));
+ const bank = h("div", "wp-chans");
+ CHANNELS.forEach((c) => {
+ const card = h("div", "wp-chan" + (WP.channel === c.id ? " on" : ""));
+ card.appendChild(chanFace(c.id));
+ card.appendChild(h("div", "nm", c.nm));
+ card.onclick = () => { WP.channel = c.id; DS.toast("Wallpaper: " + c.nm, true); paint(); };
+ bank.appendChild(card);
+ });
+ body2.appendChild(bank);
+
+ // description — helpful text for every mode, never "no options"
+ body2.appendChild(h("div", "wp-desc", DESC[WP.channel]));
+
+ const conf = h("div", "wp-conf");
+ const previewBox = (bg, imgSrc) => {
+ const pv = h("div", "wp-prev");
+ if (imgSrc) { const img = h("img"); img.src = imgSrc; pv.appendChild(img); }
+ else if (bg) pv.style.background = bg;
+ return pv;
+ };
+
+ const sortCol = () => {
+ const c = h("div", "sortcol");
+ [["alpha", "a"], ["time", "\u{f0150}"]].forEach(([mode, glyph]) => {
+ const k = h("button", "dupre-key srt" + (WP.sort === mode ? " dupre-on" : ""), glyph);
+ k.onclick = () => { k.blur(); WP.sort = mode; paint(); };
+ c.appendChild(k);
+ });
+ return c;
+ };
+ const sortedRow = (wellContent) => {
+ const row = h("div", "sorted-row");
+ row.appendChild(sortCol());
+ const well = h("div", "wp-well");
+ well.appendChild(wellContent);
+ row.appendChild(well);
+ return row;
+ };
+ const sortView = (arr) => {
+ const order = arr.map((o, i) => ({ o, i }));
+ if (WP.sort === "alpha") order.sort((x, y) => (x.o.label || "\uffff").localeCompare(y.o.label || "\uffff"));
+ else order.sort((x, y) => y.o.ts - x.o.ts);
+ return order;
+ };
+ const chipFoot = (label, onEdit, onDel) => {
+ const foot = h("div", "pair-foot");
+ const ed = h("span", "pbtn", "~"); ed.title = "edit — reopens below"; ed.onclick = onEdit;
+ foot.appendChild(ed);
+ foot.appendChild(h("span", "pair-lbl", label || ""));
+ const del = h("span", "pbtn", "\u2212"); del.title = "delete — recreate to get it back"; del.onclick = onDel;
+ foot.appendChild(del);
+ return foot;
+ };
+ const galleryWell = (content) => {
+ const well = h("div", "wp-well");
+ well.appendChild(content);
+ return well;
+ };
+
+ if (WP.channel === "single") {
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th" + (i === WP.single ? " sel" : ""));
+ t.style.background = sw;
+ t.onclick = () => { WP.single = i; DS.toast("Wallpaper set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(galleryWell(g));
+ conf.appendChild(DS.engrave("Preview"));
+ conf.appendChild(previewBox(SWATCH[WP.single]));
+ } else if (WP.channel === "pair") {
+ conf.appendChild(DS.engrave("Minted pairs"));
+ const tray = scrollGallery("pair-scroll");
+ sortView(WP.pairs).forEach(({ o: p, i }) => {
+ const cell = h("div", "pair-cell");
+ const chip = h("div", "pair-chip" + (i === WP.pairSel ? " sel" : ""));
+ const a = h("div", "half"); a.style.background = SWATCH[p.day];
+ const b = h("div", "half b"); b.style.background = SWATCH[p.night];
+ chip.appendChild(a); chip.appendChild(b);
+ chip.onclick = () => { WP.pairSel = i; DS.toast("Pair selected", true); paint(); };
+ cell.appendChild(chip);
+ cell.appendChild(chipFoot(p.label,
+ () => {
+ WP.building = { day: p.day, night: p.night, label: p.label, arm: null };
+ WP.pairs.splice(i, 1);
+ if (WP.pairSel >= WP.pairs.length) WP.pairSel = WP.pairs.length - 1;
+ DS.toast("Pair opened in the press", true);
+ paint();
+ },
+ () => {
+ WP.pairs.splice(i, 1);
+ if (WP.pairSel >= WP.pairs.length) WP.pairSel = WP.pairs.length - 1;
+ DS.toast("Pair deleted", true);
+ paint();
+ }));
+ tray.appendChild(cell);
+ });
+ conf.appendChild(sortedRow(tray));
+
+ conf.appendChild(DS.engrave("Pair press"));
+ const bench = h("div", "pair-bench");
+ ["day", "night"].forEach((k) => {
+ const slot = h("div", "pair-slot" +
+ (WP.building[k] !== null ? " filled" : "") + (WP.building.arm === k ? " arm" : ""));
+ if (WP.building[k] !== null) slot.style.background = SWATCH[WP.building[k]];
+ slot.appendChild(h("span", "cap", k === "day" ? "\u2600 DAY" : "\u263E NIGHT"));
+ slot.onclick = () => { WP.building.arm = WP.building.arm === k ? null : k; paint(); };
+ bench.appendChild(slot);
+ });
+ const side = h("div", "press-side");
+ const lbl = h("input", "wp-lblin");
+ lbl.placeholder = "label (optional)";
+ lbl.value = WP.building.label || "";
+ lbl.oninput = () => { WP.building.label = lbl.value; };
+ side.appendChild(lbl);
+ const mint = h("button", "dupre-key", "MINT PAIR");
+ mint.style.cssText = "height:26px;font-size:8px;padding:0 10px";
+ mint.onclick = () => {
+ mint.blur();
+ if (WP.building.day === null || WP.building.night === null) { DS.toast("fill both frames first", false); return; }
+ WP.pairs.push({ day: WP.building.day, night: WP.building.night, label: WP.building.label || "", ts: WP.mintSeq++ });
+ WP.pairSel = WP.pairs.length - 1;
+ WP.building = { day: null, night: null, label: "", arm: null };
+ DS.toast("Pair minted", true);
+ paint();
+ };
+ side.appendChild(mint);
+ bench.appendChild(side);
+ conf.appendChild(bench);
+ if (WP.building.arm) conf.appendChild(h("div", "wp-note", "click a picture below to fill the " + WP.building.arm.toUpperCase() + " frame"));
+
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th");
+ t.style.background = sw;
+ t.onclick = () => {
+ if (!WP.building.arm) { DS.toast("click a DAY or NIGHT frame first", false); return; }
+ WP.building[WP.building.arm] = i;
+ WP.building.arm = WP.building.arm === "day" && WP.building.night === null ? "night" : null;
+ paint();
+ };
+ g.appendChild(t);
+ });
+ conf.appendChild(galleryWell(g));
+ conf.appendChild(h("div", "wp-note", "Only picture types the lock screen can display appear here."));
+ } else if (WP.channel === "solid") {
+ conf.appendChild(DS.engrave("Minted colors"));
+ const tray = scrollGallery("pair-scroll");
+ sortView(WP.colors).forEach(({ o: c, i }) => {
+ const cell = h("div", "pair-cell");
+ const chip = h("div", "pair-chip color-chip" + (i === WP.colorSel ? " sel" : ""));
+ chip.style.background = c.c;
+ chip.onclick = () => { WP.colorSel = i; DS.toast("Color selected", true); paint(); };
+ cell.appendChild(chip);
+ cell.appendChild(chipFoot(c.label,
+ () => {
+ WP.colorBuild = { c: c.c, label: c.label };
+ WP.colors.splice(i, 1);
+ if (WP.colorSel >= WP.colors.length) WP.colorSel = WP.colors.length - 1;
+ DS.toast("Color opened in the picker", true);
+ paint();
+ },
+ () => {
+ WP.colors.splice(i, 1);
+ if (WP.colorSel >= WP.colors.length) WP.colorSel = WP.colors.length - 1;
+ DS.toast("Color deleted", true);
+ paint();
+ }));
+ tray.appendChild(cell);
+ });
+ conf.appendChild(sortedRow(tray));
+
+ conf.appendChild(DS.engrave("Color picker"));
+ const row = h("div", "pair-bench");
+ const pick = h("input", "wp-picker");
+ pick.type = "color";
+ pick.value = WP.colorBuild.c;
+ pick.oninput = () => { WP.colorBuild.c = pick.value; };
+ row.appendChild(pick);
+ const side = h("div", "press-side");
+ const lbl = h("input", "wp-lblin");
+ lbl.placeholder = "label (optional)";
+ lbl.value = WP.colorBuild.label || "";
+ lbl.oninput = () => { WP.colorBuild.label = lbl.value; };
+ side.appendChild(lbl);
+ const mint = h("button", "dupre-key", "MINT COLOR");
+ mint.style.cssText = "height:26px;font-size:8px;padding:0 10px";
+ mint.onclick = () => {
+ mint.blur();
+ WP.colors.push({ c: WP.colorBuild.c, label: WP.colorBuild.label || "", ts: WP.mintSeq++ });
+ WP.colorSel = WP.colors.length - 1;
+ WP.colorBuild = { c: WP.colorBuild.c, label: "" };
+ DS.toast("Color minted", true);
+ paint();
+ };
+ side.appendChild(mint);
+ row.appendChild(side);
+ conf.appendChild(row);
+ } else if (WP.channel === "random") {
+ conf.appendChild(DS.engrave("Minted sets"));
+ const tray = scrollGallery("pair-scroll");
+ sortView(WP.sets).forEach(({ o: st, i }) => {
+ const cell = h("div", "pair-cell");
+ const chip = h("div", "pair-chip" + (i === WP.setSel ? " sel" : ""));
+ chip.style.background = SWATCH[st.pics[0]];
+ chip.appendChild(h("span", "count-badge", st.pics.length));
+ chip.onclick = () => { WP.setSel = i; DS.toast("Set selected", true); paint(); };
+ cell.appendChild(chip);
+ cell.appendChild(chipFoot(st.label,
+ () => {
+ WP.setBuild = { pics: st.pics.slice(), label: st.label };
+ WP.sets.splice(i, 1);
+ if (WP.setSel >= WP.sets.length) WP.setSel = WP.sets.length - 1;
+ DS.toast("Set opened below", true);
+ paint();
+ },
+ () => {
+ WP.sets.splice(i, 1);
+ if (WP.setSel >= WP.sets.length) WP.setSel = WP.sets.length - 1;
+ DS.toast("Set deleted", true);
+ paint();
+ }));
+ tray.appendChild(cell);
+ });
+ conf.appendChild(sortedRow(tray));
+
+ conf.appendChild(DS.engrave("Set press — click pictures to add"));
+ const bRow = h("div", "wp-well");
+ const bTray = scrollGallery();
+ if (WP.setBuild.pics.length === 0) bTray.appendChild(h("div", "wp-note", "empty — click pictures below"));
+ WP.setBuild.pics.forEach((pi, k) => {
+ const t = h("div", "wp-th");
+ t.style.background = SWATCH[pi];
+ t.title = "click to remove from the set";
+ t.onclick = () => { WP.setBuild.pics.splice(k, 1); paint(); };
+ bTray.appendChild(t);
+ });
+ bRow.appendChild(bTray);
+ conf.appendChild(bRow);
+ const side = h("div", "pair-bench");
+ const lbl = h("input", "wp-lblin");
+ lbl.placeholder = "label (optional)";
+ lbl.value = WP.setBuild.label || "";
+ lbl.oninput = () => { WP.setBuild.label = lbl.value; };
+ side.appendChild(lbl);
+ const mint = h("button", "dupre-key", "MINT SET");
+ mint.style.cssText = "height:26px;font-size:8px;padding:0 10px";
+ mint.onclick = () => {
+ mint.blur();
+ if (WP.setBuild.pics.length < 2) { DS.toast("a set needs at least two pictures", false); return; }
+ WP.sets.push({ pics: WP.setBuild.pics.slice(), label: WP.setBuild.label || "", ts: WP.mintSeq++ });
+ WP.setSel = WP.sets.length - 1;
+ WP.setBuild = { pics: [], label: "" };
+ DS.toast("Set minted", true);
+ paint();
+ };
+ side.appendChild(mint);
+ const row2 = h("div", "wp-row2");
+ row2.appendChild(h("span", "wp-note", "next draw every"));
+ const wheel = h("div", "int-wheel", WP.interval);
+ wheel.onclick = () => {
+ WP.interval = INTERVALS[(INTERVALS.indexOf(WP.interval) + 1) % INTERVALS.length];
+ DS.toast("Random interval: " + WP.interval + "m", true);
+ paint();
+ };
+ row2.appendChild(wheel);
+ row2.appendChild(h("span", "wp-note", "minutes"));
+ side.appendChild(row2);
+ conf.appendChild(side);
+
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th");
+ t.style.background = sw;
+ t.onclick = () => {
+ if (WP.setBuild.pics.includes(i)) { DS.toast("already in the set", false); return; }
+ WP.setBuild.pics.push(i);
+ paint();
+ };
+ g.appendChild(t);
+ });
+ conf.appendChild(galleryWell(g));
+ } else if (WP.channel === "video") {
+ conf.appendChild(DS.engrave("Videos"));
+ const g = scrollGallery();
+ VIDEOS.forEach((v, i) => {
+ const t = h("div", "wp-th" + (i === WP.video ? " sel" : ""));
+ t.style.background = v;
+ t.appendChild(h("div", "vid-play", "\u25B6"));
+ t.onclick = () => { WP.video = i; DS.toast("Video set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(galleryWell(g));
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox(VIDEOS[WP.video]);
+ pv.classList.add("sm");
+ pv.appendChild(h("div", "vid-play", "\u25B6"));
+ conf.appendChild(pv);
+ } else if (WP.channel === "watch") {
+ conf.appendChild(DS.engrave("Preview"));
+ conf.appendChild(previewBox(null, "2026-07-22-watch-preview.png"));
+ } else if (WP.channel === "clock") {
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox("#0c0a08");
+ const cf = h("div", "clock-face"); cf.style.inset = "6% 34%";
+ pv.appendChild(cf);
+ conf.appendChild(pv);
+ } else if (WP.channel === "world") {
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox("#0c0a08");
+ const m = h("div", "mini-clocks"); m.style.inset = "8%";
+ for (let i = 0; i < 6; i++) m.appendChild(h("i"));
+ pv.appendChild(m);
+ conf.appendChild(pv);
+ }
+ body2.appendChild(conf);
+
+ // sources: only for the modes that draw from directories
+ if (SOURCED.includes(WP.channel)) {
+ const src = h("div", "wp-conf");
+ src.appendChild(DS.engrave("Sources"));
+ const dirs = h("div", "wp-dirs");
+ WP.dirs.forEach((d, i) => {
+ const row = h("div", "wp-dir");
+ row.innerHTML = `<span>\u{f024b}</span>`;
+ row.appendChild(h("span", "path", d));
+ const rm = h("span", "rm", "\u2715");
+ rm.onclick = () => {
+ if (WP.dirs.length === 1) { DS.toast("keep at least one source", false); return; }
+ WP.dirs.splice(i, 1);
+ DS.toast("Source removed", true);
+ paint();
+ };
+ row.appendChild(rm);
+ dirs.appendChild(row);
+ });
+ src.appendChild(dirs);
+ const add = h("button", "dupre-key", "ADD DIRECTORY");
+ add.style.cssText = "height:24px;font-size:8px;padding:0 10px;margin-top:6px";
+ add.onclick = () => {
+ add.blur();
+ const next = DIR_CANDIDATES.find((c) => !WP.dirs.includes(c));
+ if (!next) { DS.toast("no more candidates in the mock", false); return; }
+ WP.dirs.push(next);
+ DS.toast("Source added: " + next, true);
+ paint();
+ };
+ src.appendChild(add);
+ body2.appendChild(src);
+ }
+ };
+ paint();
+ return sv;
+}
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(wallpaperView());
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Programmable presets"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", glyph: "\u{f02cb}", scene: "Focus" }, // headphones — deep work (Craig to confirm)
+ { id: "PRESENT", label: "PRESENT", glyph: "\u{f0379}", scene: "Presentation" }, // projector screen
+ { id: "BATTERY", label: "BATTERY", glyph: "\u{f007e}", scene: "Battery" }, // half battery
+ { id: "NIGHT", label: "NIGHT", glyph: "\u{f0594}", scene: "Night" }, // crescent moon
+ { id: "S5", label: "1", scene: "1" },
+ { id: "S6", label: "2", scene: "2" },
+ { id: "S7", label: "3", scene: "3" },
+ { id: "S8", label: "4", scene: "4" },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label, glyph, slim }) => ({ id, label, glyph, slim }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 436,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — pointers over brightness; the idle dial alone -
+ const lv = h("div", "plate");
+ const bench = h("div", "bench");
+
+ const fbox = (label) => {
+ const b = h("div", "fbox");
+ b.appendChild(h("span", "flbl", label));
+ return b;
+ };
+
+ const lcol = h("div", "fcol");
+
+ // POINTERS — console keys, horizontal (above brightness per batch two)
+ const ptr = fbox("Pointers");
+ [["touchpad", "TOUCHPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const b = h("button", "dupre-key ptr", name);
+ if (DS.state[k]) b.classList.add("dupre-green");
+ b.onclick = () => { b.blur(); DS.setToggle(k, ptr); };
+ ptr.appendChild(b);
+ });
+ lcol.appendChild(ptr);
+
+ // BRIGHTNESS — the drum pair, centered in its frame
+ const bright = fbox("Brightness");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, bright);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, bright);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 196,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ bright.appendChild(dhost);
+ lcol.appendChild(bright);
+ bench.appendChild(lcol);
+
+ // IDLE — enlarged dial, legend beneath, alone in its frame
+ const idle = fbox("Idle");
+ const railHost = h("div", "khost");
+ const rail = DUPRE.tripDial(railHost, {
+ stages: IDLE_STAGES.map((st) => ({ ...st })),
+ max: 60,
+ size: 184,
+ legend: true,
+ legendPos: "bottom",
+ onChange: (id, minutes, snapshot) => {
+ snapshot.forEach((st) => { const m = IDLE_STAGES.find((o) => o.id === st.id); if (m) m.minutes = st.minutes; });
+ if (booting || !id) return;
+ const st = IDLE_STAGES.find((o) => o.id === id);
+ DS.toast(st.label + (minutes == null ? " stage disabled" : " → idle " + Math.round(minutes) + "m"), true);
+ },
+ });
+ rail.setBypassed(DS.state.caffeine);
+ idle.appendChild(railHost);
+ bench.appendChild(idle);
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-4.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-4.html
index 23d72fb..67b9f56 100644
--- a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-4.html
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-4.html
@@ -141,6 +141,7 @@ function render() {
["autodim", "nightlight", "touchpad", "mouse", "dnd"].forEach((k) => well.appendChild(lampRow(k)));
if (DS.state.laptop) well.appendChild(lampRow("airplane"));
well.appendChild(lampRow("caffeine"));
+ well.appendChild(lampRow("weathergeo"));
col.appendChild(well);
col.appendChild(DS.engrave("Scenes"));
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-5.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-5.html
index 662e9f6..8cbaa45 100644
--- a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-5.html
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-5.html
@@ -116,8 +116,10 @@ function render() {
col.appendChild(h("div", "sep"));
["touchpad", "mouse"].forEach((k) => col.appendChild(toggleRow(k)));
+ col.appendChild(toggleRow("caffeine"));
if (DS.state.laptop) col.appendChild(toggleRow("airplane"));
col.appendChild(toggleRow("dnd"));
+ col.appendChild(toggleRow("weathergeo"));
col.appendChild(h("div", "sep"));
col.appendChild(expandRow("\u{f0241}", "Power profile", DS.state.powerprofile, () => DS.profileSeg()));
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-6.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-6.html
new file mode 100644
index 0000000..952e4d9
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-6.html
@@ -0,0 +1,343 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 6 (hi-fi receiver)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ /* Hi-fi receiver faceplate: brushed plate, backlit tuner dial with scene
+ "stations", preset keys, VU meters over rotary knobs, switch bank. */
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #201e1a, #171512);
+ border: 1px solid var(--plate-edge);
+ border-radius: 10px;
+ padding: 10px;
+ }
+
+ /* Tuner dial: warm backlit window, station labels, sliding needle. */
+ .tuner {
+ position: relative;
+ height: 64px;
+ border-radius: 8px;
+ background: linear-gradient(180deg, #2b2414, #1c1809);
+ border: 1px solid #3a321c;
+ box-shadow: inset 0 0 18px #dab53d22;
+ overflow: hidden;
+ }
+ .tuner .scale {
+ position: absolute; left: 10px; right: 10px; top: 12px; height: 8px;
+ background:
+ repeating-linear-gradient(90deg, #dab53d55 0 1px, transparent 1px 8.5%);
+ border-bottom: 1px solid #dab53d44;
+ }
+ .tuner .station {
+ position: absolute; top: 28px;
+ transform: translateX(-50%);
+ color: #9a8a55; font-size: 10px; letter-spacing: 0.1em;
+ cursor: pointer; padding: 2px 4px;
+ }
+ .tuner .station:hover { color: var(--gold-bright); }
+ .tuner .station.cur { color: var(--gold-bright); text-shadow: 0 0 8px #dab53daa; }
+ .tuner .freq {
+ position: absolute; bottom: 3px; left: 10px;
+ color: #6a5f3a; font-size: 9px; letter-spacing: 0.14em;
+ }
+ .tuner .needle {
+ position: absolute; top: 6px; bottom: 6px; width: 2px;
+ background: linear-gradient(180deg, var(--fail), #8a4830);
+ box-shadow: 0 0 6px #cb6b4d88;
+ transition: left .35s cubic-bezier(.3,1.2,.4,1);
+ }
+
+ /* Preset keys: chunky mechanical station buttons under the dial. */
+ .presets { display: grid; grid-template-columns: repeat(4,1fr); gap: 6px; margin-top: 8px; }
+ .preset {
+ background-image: linear-gradient(180deg, var(--key-top), var(--key-bot));
+ border: 1px solid var(--key-edge);
+ border-bottom-width: 3px;
+ border-radius: 6px;
+ padding: 6px 4px;
+ text-align: center;
+ cursor: pointer;
+ color: var(--silver);
+ }
+ .preset:hover { border-color: var(--gold); color: var(--gold); }
+ .preset .num { font-size: 9px; color: var(--dim); display: block; }
+ .preset .nm { font-size: 10.5px; letter-spacing: 0.06em; }
+ .preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px;
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12);
+ transform: translateY(2px);
+ color: var(--cream);
+ }
+ .preset.cur .num { color: var(--gold); }
+
+ /* VU meter: cream arc window with a swinging needle. */
+ .duo { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
+ .vu {
+ position: relative; height: 58px; border-radius: 8px 8px 4px 4px;
+ background: linear-gradient(180deg, #efe3c2, #d9cba2);
+ border: 1px solid #3a3226;
+ overflow: hidden;
+ }
+ .vu .arc {
+ position: absolute; left: 8px; right: 8px; top: 10px; height: 5px;
+ border-radius: 4px;
+ background: linear-gradient(90deg, #6b6352 70%, #b5432e 70%);
+ opacity: .8;
+ }
+ .vu .vu-lbl {
+ position: absolute; bottom: 3px; width: 100%; text-align: center;
+ color: #5a5140; font-size: 8.5px; letter-spacing: 0.18em;
+ }
+ .vu .nd {
+ position: absolute; left: 50%; bottom: -6px; width: 2px; height: 52px;
+ background: #1d1a14;
+ transform-origin: 50% 100%;
+ transition: transform .25s ease-out;
+ }
+ .knob-cell { display: flex; flex-direction: column; align-items: center; gap: 5px; margin-top: 6px; }
+ .knob {
+ width: 52px; height: 52px; border-radius: 50%;
+ background:
+ radial-gradient(circle at 35% 30%, #35322c, #191715 70%),
+ var(--key-bot);
+ border: 2px solid var(--key-edge);
+ box-shadow: 0 3px 8px #000a, inset 0 1px 0 #ffffff14;
+ position: relative;
+ cursor: ew-resize;
+ touch-action: none;
+ }
+ .knob .ptr {
+ position: absolute; left: 50%; top: 4px; width: 2px; height: 14px;
+ margin-left: -1px;
+ background: var(--gold);
+ transform-origin: 50% 22px;
+ }
+ .knob-lbl { color: var(--steel); font-size: 9.5px; letter-spacing: 0.14em; }
+ .knob-val { color: var(--cream); font-size: 11px; font-variant-numeric: tabular-nums; }
+
+ /* Switch bank: square backlit-legend switches, two rows of four. */
+ .bank { display: grid; grid-template-columns: repeat(4,1fr); gap: 6px; }
+ .sw {
+ background-image: linear-gradient(180deg, var(--key-top), var(--key-bot));
+ border: 1px solid var(--key-edge);
+ border-radius: 6px;
+ padding: 7px 3px 5px;
+ display: flex; flex-direction: column; align-items: center; gap: 4px;
+ cursor: pointer;
+ }
+ .sw:hover { border-color: var(--gold); }
+ .sw .g { font-size: 15px; color: var(--steel); }
+ .sw .lgd { font-size: 8px; letter-spacing: 0.08em; color: var(--dim); text-transform: uppercase; }
+ .sw.on { border-color: var(--gold); background-image: linear-gradient(180deg, #2a2618, #1d1a12); }
+ .sw.on .g { color: var(--gold); }
+ .sw.on .lgd { color: var(--gold-bright); text-shadow: 0 0 7px #dab53d99; }
+ .sw.verified { animation: dsVerify 0.9s ease-out; }
+
+ /* Input selector: one rotary with three engraved detents. */
+ .selector { display: flex; align-items: center; gap: 14px; padding: 2px 6px; }
+ .selector .labels { display: flex; flex-direction: column; gap: 3px; }
+ .selector .det { color: var(--dim); font-size: 10px; letter-spacing: 0.12em; cursor: pointer; }
+ .selector .det:hover { color: var(--silver); }
+ .selector .det.cur { color: var(--gold-bright); text-shadow: 0 0 7px #dab53d77; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Direction 6 — Hi-fi receiver.</b> Scenes are station presets on a backlit
+ tuner dial; the needle slides to the tuned scene. Rotary knobs + VU meters
+ for the levels, a backlit switch bank for toggles, an input selector for
+ power. Drag knobs sideways; click stations or preset keys.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENE_ORDER = Object.keys(DS.SCENES); // 4 stations
+const STATION_POS = [14, 38, 62, 86]; // % across the dial
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function tuner() {
+ const t = h("div", "tuner");
+ t.appendChild(h("div", "scale"));
+ SCENE_ORDER.forEach((n, i) => {
+ const s = h("span", "station" + (DS.state.scene === n ? " cur" : ""), n.toUpperCase());
+ s.style.left = STATION_POS[i] + "%";
+ s.onclick = () => DS.applyScene(n, t);
+ t.appendChild(s);
+ });
+ t.appendChild(h("span", "freq", "SCENE TUNING"));
+ const nd = h("div", "needle");
+ const idx = SCENE_ORDER.indexOf(DS.state.scene);
+ nd.style.left = (idx < 0 ? 3 : STATION_POS[idx]) + "%";
+ t.appendChild(nd);
+ return t;
+}
+
+function presetKeys() {
+ const wrap = h("div", "presets");
+ SCENE_ORDER.forEach((n, i) => {
+ const k = h("div", "preset" + (DS.state.scene === n ? " cur" : ""));
+ k.innerHTML = `<span class="num">PRESET ${i + 1}</span><span class="nm">${n}</span>`;
+ k.onclick = () => DS.applyScene(n, k);
+ wrap.appendChild(k);
+ });
+ return wrap;
+}
+
+// VU needle: value 0-100 → -42°..42° swing.
+function vuMeter(key, label) {
+ const v = h("div", "vu");
+ v.appendChild(h("div", "arc"));
+ const nd = h("div", "nd");
+ nd.style.transform = `rotate(${(DS.state[key] / 100) * 84 - 42}deg)`;
+ nd.dataset.vu = key;
+ v.appendChild(nd);
+ v.appendChild(h("div", "vu-lbl", label));
+ return v;
+}
+
+// Rotary knob: drag sideways (or wheel) to set; pointer sweeps -135°..135°.
+function knob(key, label) {
+ const meta = DS.SLIDERS[key];
+ const cell = h("div", "knob-cell");
+ const kn = h("div", "knob");
+ const ptr = h("div", "ptr");
+ const val = h("div", "knob-val");
+ const angle = (v) => ((v - meta.floor) / (100 - meta.floor)) * 270 - 135;
+ const paint = (v) => {
+ ptr.style.transform = `rotate(${angle(v)}deg)`;
+ val.textContent = v + "%";
+ const vu = document.querySelector(`[data-vu="${key}"]`);
+ if (vu) vu.style.transform = `rotate(${(v / 100) * 84 - 42}deg)`;
+ };
+ kn.appendChild(ptr);
+ paint(DS.state[key]);
+ let live = DS.state[key];
+ kn.onpointerdown = (e) => {
+ kn.setPointerCapture(e.pointerId);
+ const startX = e.clientX, startV = live;
+ kn.onpointermove = (m) => {
+ live = Math.max(meta.floor, Math.min(100, Math.round(startV + (m.clientX - startX) / 2)));
+ paint(live);
+ };
+ kn.onpointerup = () => { kn.onpointermove = null; DS.setSlider(key, live, kn); };
+ };
+ kn.onwheel = (e) => {
+ e.preventDefault();
+ live = Math.max(meta.floor, Math.min(100, live + (e.deltaY < 0 ? 5 : -5)));
+ paint(live);
+ clearTimeout(kn._w);
+ kn._w = setTimeout(() => DS.setSlider(key, live, kn), 350);
+ };
+ cell.appendChild(kn);
+ cell.appendChild(h("div", "knob-lbl", label));
+ cell.appendChild(val);
+ return cell;
+}
+
+function switchBank() {
+ const bank = h("div", "bank");
+ const keys = ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd", "weathergeo"];
+ if (DS.state.laptop) keys.push("airplane");
+ keys.forEach((k) => {
+ const meta = DS.TOGGLES[k];
+ const el = h("div", "sw" + (DS.state[k] ? " on" : ""));
+ el.innerHTML = `<span class="g">${meta.glyph}</span><span class="lgd">${meta.name}</span>`;
+ el.onclick = () => DS.setToggle(k, el);
+ bank.appendChild(el);
+ });
+ return bank;
+}
+
+function inputSelector() {
+ const sel = h("div", "selector");
+ const kn = h("div", "knob");
+ kn.style.cursor = "pointer";
+ const ptr = h("div", "ptr");
+ const idx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ ptr.style.transform = `rotate(${idx * 45 - 45}deg)`;
+ kn.appendChild(ptr);
+ kn.onclick = () => {
+ const next = DS.PROFILES[(idx + 1) % DS.PROFILES.length];
+ DS.setProfile(next.id, sel);
+ };
+ sel.appendChild(kn);
+ const labels = h("div", "labels");
+ DS.PROFILES.forEach((p) => {
+ const d = h("span", "det" + (p.id === DS.state.powerprofile ? " cur" : ""), p.label.toUpperCase());
+ d.onclick = () => DS.setProfile(p.id, sel);
+ labels.appendChild(d);
+ });
+ sel.appendChild(labels);
+ sel.appendChild(h("span", "knob-lbl", "POWER"));
+ return sel;
+}
+
+function render() {
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ const dialPlate = h("div", "plate");
+ dialPlate.appendChild(tuner());
+ dialPlate.appendChild(presetKeys());
+ col.appendChild(dialPlate);
+
+ col.appendChild(DS.engrave("Levels"));
+ const lv = h("div", "plate");
+ const duo = h("div", "duo");
+ const c1 = h("div"); c1.appendChild(vuMeter("brightness", "BRIGHTNESS")); c1.appendChild(knob("brightness", "BRIGHTNESS"));
+ const c2 = h("div"); c2.appendChild(vuMeter("kbd", "KEYBOARD")); c2.appendChild(knob("kbd", "KEYBOARD"));
+ duo.appendChild(c1); duo.appendChild(c2);
+ lv.appendChild(duo);
+ col.appendChild(lv);
+
+ col.appendChild(DS.engrave("Switches"));
+ col.appendChild(switchBank());
+
+ col.appendChild(DS.engrave("Power"));
+ col.appendChild(inputSelector());
+
+ col.appendChild(DS.engrave("Actions"));
+ const acts = h("div", "ds-grid3");
+ acts.appendChild(DS.actionBtn("Lock", "\u{f033e}", false));
+ acts.appendChild(DS.actionBtn("Suspend", "\u{f04b2}", false));
+ acts.appendChild(DS.actionBtn("Wallpaper", "\u{f021c}", false, openWallpaper));
+ col.appendChild(acts);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-7.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-7.html
new file mode 100644
index 0000000..dc7ac0a
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-7.html
@@ -0,0 +1,322 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 7 (chronograph)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ /* Chronograph: one big watch face. Brightness is the main dial (drag the
+ bezel), chrono subdials carry keyboard / power / scene, toggles are the
+ bracelet links below. */
+ .case {
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
+ }
+ .face-wrap { position: relative; width: 300px; height: 300px; }
+ .bezel {
+ position: absolute; inset: 0; border-radius: 50%;
+ background:
+ radial-gradient(circle at 32% 28%, #3a362e, #191715 72%);
+ border: 2px solid var(--key-edge);
+ box-shadow: 0 8px 26px #000c, inset 0 1px 0 #ffffff14;
+ cursor: pointer;
+ touch-action: none;
+ }
+ .ring {
+ position: absolute; inset: 12px; border-radius: 50%;
+ }
+ .dial {
+ position: absolute; inset: 34px; border-radius: 50%;
+ background: radial-gradient(circle at 40% 32%, #14130f, #0b0a08 75%);
+ border: 1px solid #2c2820;
+ pointer-events: none;
+ }
+ .tick {
+ position: absolute; left: 50%; top: 14px; width: 1px; height: 7px;
+ margin-left: -0.5px;
+ background: #6a6250;
+ transform-origin: 50% 136px;
+ pointer-events: none;
+ }
+ .tick.major { width: 2px; height: 11px; margin-left: -1px; background: var(--gold); }
+ .hand {
+ position: absolute; left: 50%; top: 44px; width: 3px; height: 106px;
+ margin-left: -1.5px;
+ background: linear-gradient(180deg, var(--gold-bright), var(--gold) 80%, transparent);
+ transform-origin: 50% 106px;
+ border-radius: 3px;
+ box-shadow: 0 0 6px #dab53d55;
+ transition: transform .2s ease-out;
+ pointer-events: none;
+ }
+ .cap {
+ position: absolute; left: 50%; top: 50%; width: 12px; height: 12px;
+ margin: -6px 0 0 -6px; border-radius: 50%;
+ background: radial-gradient(circle at 35% 30%, var(--cream), #8a7d55);
+ pointer-events: none;
+ }
+ .digital {
+ position: absolute; left: 50%; top: 33%; transform: translateX(-50%);
+ text-align: center; pointer-events: none;
+ }
+ .digital .big { color: var(--cream); font-size: 21px; font-weight: 700; font-variant-numeric: tabular-nums; }
+ .digital .big .u { color: var(--dim); font-size: 11px; }
+ .digital .lbl { color: var(--steel); font-size: 8.5px; letter-spacing: 0.22em; }
+
+ /* Subdials */
+ .subdial {
+ position: absolute; width: 74px; height: 74px; border-radius: 50%;
+ background: radial-gradient(circle at 40% 32%, #1b1914, #100e0a 75%);
+ border: 1px solid #322d24;
+ cursor: pointer;
+ }
+ .subdial .s-hand {
+ position: absolute; left: 50%; top: 9px; width: 2px; height: 28px;
+ margin-left: -1px;
+ background: var(--fail);
+ transform-origin: 50% 28px;
+ transition: transform .25s cubic-bezier(.3,1.4,.4,1);
+ pointer-events: none;
+ }
+ .subdial .s-cap {
+ position: absolute; left: 50%; top: 50%; width: 6px; height: 6px;
+ margin: -3px 0 0 -3px; border-radius: 50%; background: var(--cream);
+ pointer-events: none;
+ }
+ .subdial .s-lbl {
+ position: absolute; width: 100%; text-align: center; bottom: 13px;
+ color: var(--steel); font-size: 7.5px; letter-spacing: 0.14em;
+ pointer-events: none;
+ }
+ .subdial .s-val {
+ position: absolute; width: 100%; text-align: center; bottom: 4px;
+ color: var(--gold); font-size: 8.5px;
+ pointer-events: none;
+ }
+ .subdial.left { left: 42px; top: 113px; }
+ .subdial.right { right: 42px; top: 113px; }
+ .subdial.bottom{ left: 113px; bottom: 34px; }
+ .subdial:hover { border-color: var(--gold); }
+
+ /* Crown + pushers: momentary actions on the right case edge. */
+ .crown {
+ position: absolute; right: -12px; width: 16px; height: 26px;
+ border-radius: 4px;
+ background: linear-gradient(90deg, #2c2922, #191715);
+ border: 1px solid var(--key-edge);
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ color: var(--steel); font-size: 9px;
+ }
+ .crown:hover { border-color: var(--gold); color: var(--gold); }
+ .crown.c1 { top: 84px; transform: rotate(20deg); }
+ .crown.c2 { top: 140px; }
+ .crown.c3 { top: 196px; transform: rotate(-20deg); }
+ .crown-lbl {
+ position: absolute; right: -66px; width: 50px;
+ color: var(--dim); font-size: 8.5px; letter-spacing: 0.08em;
+ }
+
+ /* Bracelet: toggle links under the case. */
+ .bracelet { display: grid; grid-template-columns: repeat(4,1fr); gap: 5px; width: 100%; }
+ .link {
+ background: linear-gradient(180deg, #24211c, #171512);
+ border: 1px solid var(--key-edge);
+ border-radius: 5px;
+ padding: 6px 2px 5px;
+ display: flex; flex-direction: column; align-items: center; gap: 3px;
+ cursor: pointer;
+ }
+ .link:hover { border-color: var(--gold); }
+ .link .g { font-size: 14px; color: var(--steel); }
+ .link .n { font-size: 7.5px; letter-spacing: 0.06em; color: var(--dim); text-transform: uppercase; }
+ .link.on { border-color: var(--gold); background: linear-gradient(180deg, #2a2618, #1d1a12); }
+ .link.on .g { color: var(--gold); }
+ .link.on .n { color: var(--gold-bright); }
+ .link.verified { animation: dsVerify 0.9s ease-out; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Direction 7 — Chronograph.</b> The panel is a watch. Drag around the
+ bezel to set brightness (the gold hand + digital window follow). Subdials:
+ keyboard backlight (drag or click), power profile and scene (click to step).
+ Crowns on the case are lock / suspend / wallpaper; the bracelet links are
+ the toggles.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENE_ORDER = Object.keys(DS.SCENES);
+const B_FLOOR = DS.SLIDERS.brightness.floor;
+const SWEEP = 270, START = -135; // main dial sweep
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+const angleOf = (v, floor) => START + ((v - floor) / (100 - floor)) * SWEEP;
+
+// pointer position → dial value (angle from 12 o'clock, clamped to the sweep)
+function valueAt(e, el, floor) {
+ const r = el.getBoundingClientRect();
+ const dx = e.clientX - (r.left + r.width / 2);
+ const dy = e.clientY - (r.top + r.height / 2);
+ let a = Math.atan2(dx, -dy) * 180 / Math.PI; // 0 at top, cw positive
+ a = Math.max(START, Math.min(START + SWEEP, a));
+ return Math.round(floor + ((a - START) / SWEEP) * (100 - floor));
+}
+
+function mainDial() {
+ const wrap = h("div", "face-wrap");
+ const bez = h("div", "bezel");
+
+ const ring = h("div", "ring");
+ const paintRing = (v) => {
+ const deg = ((v - B_FLOOR) / (100 - B_FLOOR)) * SWEEP;
+ ring.style.background =
+ `conic-gradient(from ${START}deg, var(--gold) 0 ${deg}deg, #21201b ${deg}deg ${SWEEP}deg, transparent ${SWEEP}deg 360deg)`;
+ ring.style.borderRadius = "50%";
+ };
+ paintRing(DS.state.brightness);
+ bez.appendChild(ring);
+ bez.appendChild(h("div", "dial"));
+
+ for (let i = 0; i <= 20; i++) {
+ const t = h("div", "tick" + (i % 5 === 0 ? " major" : ""));
+ t.style.transform = `rotate(${START + (i / 20) * SWEEP}deg)`;
+ bez.appendChild(t);
+ }
+
+ const hand = h("div", "hand");
+ const dig = h("div", "digital");
+ const paintHand = (v) => {
+ hand.style.transform = `rotate(${angleOf(v, B_FLOOR)}deg)`;
+ dig.innerHTML = `<div class="big">${v}<span class="u">%</span></div><div class="lbl">BRIGHTNESS</div>`;
+ };
+ paintHand(DS.state.brightness);
+ bez.appendChild(hand);
+ bez.appendChild(dig);
+ bez.appendChild(h("div", "cap"));
+
+ let live = DS.state.brightness;
+ bez.onpointerdown = (e) => {
+ if (e.target.closest(".subdial") || e.target.closest(".crown")) return;
+ bez.setPointerCapture(e.pointerId);
+ live = valueAt(e, bez, B_FLOOR); paintRing(live); paintHand(live);
+ bez.onpointermove = (m) => { live = valueAt(m, bez, B_FLOOR); paintRing(live); paintHand(live); };
+ bez.onpointerup = () => { bez.onpointermove = null; DS.setSlider("brightness", live, bez); };
+ };
+
+ // Subdial: keyboard backlight (drag same as main, smaller sweep display).
+ const kbd = h("div", "subdial left");
+ const kh = h("div", "s-hand");
+ const kFloor = DS.SLIDERS.kbd.floor;
+ const paintK = (v) => kh.style.transform = `rotate(${angleOf(v, kFloor)}deg)`;
+ paintK(DS.state.kbd);
+ kbd.appendChild(kh); kbd.appendChild(h("div", "s-cap"));
+ kbd.appendChild(h("div", "s-lbl", "KEYBOARD"));
+ const kv = h("div", "s-val", DS.state.kbd + "%");
+ kbd.appendChild(kv);
+ let kLive = DS.state.kbd;
+ kbd.onpointerdown = (e) => {
+ e.stopPropagation();
+ kbd.setPointerCapture(e.pointerId);
+ kLive = valueAt(e, kbd, kFloor); paintK(kLive); kv.textContent = kLive + "%";
+ kbd.onpointermove = (m) => { kLive = valueAt(m, kbd, kFloor); paintK(kLive); kv.textContent = kLive + "%"; };
+ kbd.onpointerup = () => { kbd.onpointermove = null; DS.setSlider("kbd", kLive, kbd); };
+ };
+
+ // Subdial: power profile — three detents, click steps.
+ const pw = h("div", "subdial right");
+ const pIdx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ const ph = h("div", "s-hand");
+ ph.style.transform = `rotate(${pIdx * 90 - 90}deg)`;
+ pw.appendChild(ph); pw.appendChild(h("div", "s-cap"));
+ pw.appendChild(h("div", "s-lbl", "POWER"));
+ pw.appendChild(h("div", "s-val", DS.PROFILES[pIdx].label.toUpperCase()));
+ pw.onclick = (e) => {
+ e.stopPropagation();
+ DS.setProfile(DS.PROFILES[(pIdx + 1) % DS.PROFILES.length].id, pw);
+ };
+
+ // Subdial: scene — off + four stations, click steps.
+ const sc = h("div", "subdial bottom");
+ const sIdx = SCENE_ORDER.indexOf(DS.state.scene); // -1 = off
+ const sh = h("div", "s-hand");
+ sh.style.transform = `rotate(${(sIdx + 1) * 72 - 144}deg)`;
+ sc.appendChild(sh); sc.appendChild(h("div", "s-cap"));
+ sc.appendChild(h("div", "s-lbl", "SCENE"));
+ sc.appendChild(h("div", "s-val", sIdx < 0 ? "OFF" : SCENE_ORDER[sIdx].toUpperCase()));
+ sc.onclick = (e) => {
+ e.stopPropagation();
+ const next = SCENE_ORDER[(sIdx + 1) % SCENE_ORDER.length];
+ DS.applyScene(next, sc);
+ };
+
+ bez.appendChild(kbd); bez.appendChild(pw); bez.appendChild(sc);
+ wrap.appendChild(bez);
+
+ // Crowns: momentary actions.
+ const mk = (cls, glyph, label, fn) => {
+ const c = h("div", "crown " + cls, glyph);
+ c.title = label;
+ c.onclick = (e) => { e.stopPropagation(); fn ? fn() : DS.momentary(label); };
+ return c;
+ };
+ wrap.appendChild(mk("c1", "\u{f033e}", "Lock"));
+ wrap.appendChild(mk("c2", "\u{f04b2}", "Suspend"));
+ wrap.appendChild(mk("c3", "\u{f021c}", "Wallpaper", openWallpaper));
+
+ return wrap;
+}
+
+function bracelet() {
+ const br = h("div", "bracelet");
+ const keys = ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd", "weathergeo"];
+ if (DS.state.laptop) keys.push("airplane");
+ keys.forEach((k) => {
+ const meta = DS.TOGGLES[k];
+ const el = h("div", "link" + (DS.state[k] ? " on" : ""));
+ el.innerHTML = `<span class="g">${meta.glyph}</span><span class="n">${meta.name}</span>`;
+ el.onclick = () => DS.setToggle(k, el);
+ br.appendChild(el);
+ });
+ return br;
+}
+
+function render() {
+ body.innerHTML = "";
+ const cs = h("div", "case");
+ cs.appendChild(mainDial());
+ cs.appendChild(bracelet());
+ body.appendChild(cs);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-8.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-8.html
new file mode 100644
index 0000000..13fed0b
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-8.html
@@ -0,0 +1,360 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 8 (gauge cluster)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ /* 60s dashboard: twin needle gauges in a binnacle, a rocker-switch bank,
+ push-button drive modes for scenes, an ignition rotary for power. */
+ .binnacle {
+ background:
+ radial-gradient(140px 90px at 25% 20%, #ffffff07, transparent),
+ linear-gradient(180deg, #1c1a17, #121110);
+ border: 1px solid var(--plate-edge);
+ border-radius: 14px;
+ padding: 12px 10px 8px;
+ display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
+ }
+ .gauge {
+ position: relative;
+ width: 100%; aspect-ratio: 1;
+ border-radius: 50%;
+ background: radial-gradient(circle at 38% 30%, #16140f, #0a0908 78%);
+ border: 3px solid #2c2820;
+ box-shadow: inset 0 0 16px #000c, 0 4px 12px #0009;
+ cursor: pointer;
+ touch-action: none;
+ }
+ .gauge .tk {
+ position: absolute; left: 50%; top: 8px; width: 1.5px; height: 7px;
+ margin-left: -0.75px;
+ background: #7a7057;
+ transform-origin: 50% calc(var(--gr) * 1px);
+ pointer-events: none;
+ }
+ .gauge .tk.mj { width: 2.5px; height: 11px; background: var(--cream); }
+ .gauge .num {
+ position: absolute; left: 50%; top: 50%;
+ color: var(--steel); font-size: 9px;
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ }
+ .gauge .needle {
+ position: absolute; left: 50%; bottom: 50%; width: 3px; height: 42%;
+ margin-left: -1.5px;
+ background: linear-gradient(180deg, var(--fail) 60%, #7e4530);
+ transform-origin: 50% 100%;
+ border-radius: 3px 3px 0 0;
+ box-shadow: 0 0 6px #cb6b4d66;
+ transition: transform .18s ease-out;
+ pointer-events: none;
+ }
+ .gauge .hub {
+ position: absolute; left: 50%; top: 50%; width: 14px; height: 14px;
+ margin: -7px 0 0 -7px; border-radius: 50%;
+ background: radial-gradient(circle at 35% 30%, #4a453b, #211e19);
+ border: 1px solid #383328;
+ pointer-events: none;
+ }
+ .gauge .g-lbl {
+ position: absolute; width: 100%; text-align: center; top: 27%;
+ color: var(--steel); font-size: 8px; letter-spacing: 0.2em;
+ pointer-events: none;
+ }
+ .odo {
+ position: absolute; width: 100%; text-align: center; bottom: 17%;
+ pointer-events: none;
+ }
+ .odo span {
+ display: inline-block;
+ background: #060505; border: 1px solid #2a2620; border-radius: 3px;
+ color: var(--cream); font-size: 11px; font-variant-numeric: tabular-nums;
+ padding: 1px 6px; letter-spacing: 0.12em;
+ }
+
+ /* Rocker bank: tilting switches with engraved legends. */
+ .rockers { display: grid; grid-template-columns: repeat(4,1fr); gap: 6px; }
+ .rocker {
+ display: flex; flex-direction: column; align-items: center; gap: 4px;
+ cursor: pointer; padding: 5px 2px 4px;
+ border-radius: 6px; border: 1px solid transparent;
+ }
+ .rocker:hover { border-color: var(--key-edge); }
+ .rk {
+ width: 26px; height: 38px; border-radius: 5px;
+ background: linear-gradient(180deg, var(--key-top), var(--key-bot));
+ border: 1px solid var(--key-edge);
+ position: relative;
+ perspective: 60px;
+ }
+ .rk .cap {
+ position: absolute; inset: 2px; border-radius: 3px;
+ background: linear-gradient(180deg, #2e2b25 48%, #191712 52%);
+ transform: rotateX(14deg);
+ transition: transform .12s, background .12s;
+ }
+ .rocker.on .rk { border-color: var(--gold); }
+ .rocker.on .rk .cap {
+ transform: rotateX(-14deg);
+ background: linear-gradient(180deg, #191712 48%, #4a3d16 52%);
+ }
+ .rocker .lgd {
+ font-size: 7.5px; letter-spacing: 0.06em; color: var(--dim);
+ text-transform: uppercase; text-align: center;
+ }
+ .rocker.on .lgd { color: var(--gold-bright); text-shadow: 0 0 6px #dab53d88; }
+ .rocker .g { font-size: 13px; color: var(--steel); }
+ .rocker.on .g { color: var(--gold); }
+ .rocker.verified { animation: dsVerify 0.9s ease-out; }
+
+ /* Drive-mode push buttons (scenes): the push-button transmission column. */
+ .modes { display: grid; grid-template-columns: repeat(4,1fr); gap: 6px; }
+ .mode {
+ background: linear-gradient(180deg, var(--key-top), var(--key-bot));
+ border: 1px solid var(--key-edge);
+ border-bottom-width: 4px;
+ border-radius: 6px;
+ padding: 7px 2px;
+ text-align: center; cursor: pointer;
+ }
+ .mode .ltr { display: block; color: var(--cream); font-size: 15px; font-weight: 700; }
+ .mode .nm { display: block; color: var(--dim); font-size: 7.5px; letter-spacing: 0.08em; text-transform: uppercase; margin-top: 2px; }
+ .mode:hover { border-color: var(--gold); }
+ .mode.cur {
+ border-color: var(--gold); border-bottom-width: 1px;
+ transform: translateY(3px);
+ background: linear-gradient(180deg, #2a2618, #1d1a12);
+ }
+ .mode.cur .ltr { color: var(--gold-bright); text-shadow: 0 0 8px #dab53d99; }
+ .mode.cur .nm { color: var(--gold); }
+
+ /* Ignition rotary (power profile). */
+ .ignition { display: flex; align-items: center; gap: 14px; padding: 2px 6px; }
+ .ig-knob {
+ width: 54px; height: 54px; border-radius: 50%;
+ background: radial-gradient(circle at 35% 30%, #35322c, #191715 70%);
+ border: 2px solid var(--key-edge);
+ box-shadow: 0 3px 8px #000a;
+ position: relative; cursor: pointer;
+ }
+ .ig-knob .slot {
+ position: absolute; left: 50%; top: 50%; width: 5px; height: 26px;
+ margin: -13px 0 0 -2.5px;
+ background: #0a0908; border-radius: 3px; border: 1px solid #3a352c;
+ transform-origin: 50% 50%;
+ transition: transform .2s;
+ }
+ .ig-labels { display: flex; flex-direction: column; gap: 3px; }
+ .ig-labels .det { color: var(--dim); font-size: 10px; letter-spacing: 0.12em; cursor: pointer; }
+ .ig-labels .det:hover { color: var(--silver); }
+ .ig-labels .det.cur { color: var(--gold-bright); text-shadow: 0 0 7px #dab53d77; }
+ .ig-lbl { margin-left: auto; color: var(--steel); font-size: 9px; letter-spacing: 0.16em; }
+
+ /* Dash pull-knob actions. */
+ .pulls { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; }
+ .pull {
+ display: flex; flex-direction: column; align-items: center; gap: 4px;
+ cursor: pointer; padding: 5px 2px;
+ border-radius: 6px; border: 1px solid transparent;
+ }
+ .pull:hover { border-color: var(--key-edge); }
+ .pull .cap {
+ width: 30px; height: 30px; border-radius: 50%;
+ background: radial-gradient(circle at 35% 30%, #3a362e, #1c1a15);
+ border: 2px solid var(--key-edge);
+ box-shadow: 0 3px 6px #000a;
+ display: flex; align-items: center; justify-content: center;
+ color: var(--steel); font-size: 13px;
+ }
+ .pull:hover .cap { border-color: var(--gold); color: var(--gold); }
+ .pull .lgd { font-size: 8px; letter-spacing: 0.1em; color: var(--dim); text-transform: uppercase; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Direction 8 — Gauge cluster.</b> A dashboard binnacle: brightness and
+ keyboard are needle gauges (drag across the dial), toggles are a rocker
+ bank with backlit legends, scenes are push-button drive modes, power is an
+ ignition rotary, actions are dash pull-knobs.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENE_ORDER = Object.keys(DS.SCENES);
+const SWEEP = 240, START = -120; // gauge sweep
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function valueAt(e, el, floor) {
+ const r = el.getBoundingClientRect();
+ const dx = e.clientX - (r.left + r.width / 2);
+ const dy = e.clientY - (r.top + r.height / 2);
+ let a = Math.atan2(dx, -dy) * 180 / Math.PI;
+ a = Math.max(START, Math.min(START + SWEEP, a));
+ return Math.round(floor + ((a - START) / SWEEP) * (100 - floor));
+}
+
+function gauge(key, label) {
+ const meta = DS.SLIDERS[key];
+ const g = h("div", "gauge");
+
+ // ticks + numerals — radius derives from the rendered size after mount
+ requestAnimationFrame(() => {
+ const R = g.clientWidth / 2;
+ for (let i = 0; i <= 10; i++) {
+ const t = h("div", "tk" + (i % 2 === 0 ? " mj" : ""));
+ t.style.setProperty("--gr", R - 8);
+ t.style.transform = `rotate(${START + (i / 10) * SWEEP}deg)`;
+ g.appendChild(t);
+ if (i % 2 === 0) {
+ const a = (START + (i / 10) * SWEEP - 90) * Math.PI / 180;
+ const nr = R - 26;
+ const n = h("div", "num", i * 10);
+ n.style.left = 50 + (Math.cos(a) * nr / R) * 50 + "%";
+ n.style.top = 50 + (Math.sin(a) * nr / R) * 50 + "%";
+ g.appendChild(n);
+ }
+ }
+ });
+
+ g.appendChild(h("div", "g-lbl", label));
+ const nd = h("div", "needle");
+ const odo = h("div", "odo");
+ const odoSpan = h("span");
+ odo.appendChild(odoSpan);
+ const paint = (v) => {
+ nd.style.transform = `rotate(${START + ((v - meta.floor) / (100 - meta.floor)) * SWEEP}deg)`;
+ odoSpan.textContent = String(v).padStart(3, "0");
+ };
+ paint(DS.state[key]);
+ g.appendChild(nd);
+ g.appendChild(h("div", "hub"));
+ g.appendChild(odo);
+
+ let live = DS.state[key];
+ g.onpointerdown = (e) => {
+ g.setPointerCapture(e.pointerId);
+ live = valueAt(e, g, meta.floor); paint(live);
+ g.onpointermove = (m) => { live = valueAt(m, g, meta.floor); paint(live); };
+ g.onpointerup = () => { g.onpointermove = null; DS.setSlider(key, live, g); };
+ };
+ return g;
+}
+
+function rockerBank() {
+ const bank = h("div", "rockers");
+ const keys = ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd", "weathergeo"];
+ if (DS.state.laptop) keys.push("airplane");
+ keys.forEach((k) => {
+ const meta = DS.TOGGLES[k];
+ const el = h("div", "rocker" + (DS.state[k] ? " on" : ""));
+ el.innerHTML =
+ `<span class="g">${meta.glyph}</span>` +
+ `<span class="rk"><span class="cap"></span></span>` +
+ `<span class="lgd">${meta.name}</span>`;
+ el.onclick = () => DS.setToggle(k, el);
+ bank.appendChild(el);
+ });
+ return bank;
+}
+
+function driveModes() {
+ const m = h("div", "modes");
+ SCENE_ORDER.forEach((n) => {
+ const el = h("div", "mode" + (DS.state.scene === n ? " cur" : ""));
+ el.innerHTML = `<span class="ltr">${n[0]}</span><span class="nm">${n}</span>`;
+ el.onclick = () => DS.applyScene(n, el);
+ m.appendChild(el);
+ });
+ return m;
+}
+
+function ignition() {
+ const ig = h("div", "ignition");
+ const kn = h("div", "ig-knob");
+ const slot = h("div", "slot");
+ const idx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ slot.style.transform = `rotate(${idx * 45 - 45}deg)`;
+ kn.appendChild(slot);
+ kn.onclick = () => DS.setProfile(DS.PROFILES[(idx + 1) % DS.PROFILES.length].id, ig);
+ ig.appendChild(kn);
+ const labels = h("div", "ig-labels");
+ DS.PROFILES.forEach((p) => {
+ const d = h("span", "det" + (p.id === DS.state.powerprofile ? " cur" : ""), p.label.toUpperCase());
+ d.onclick = () => DS.setProfile(p.id, ig);
+ labels.appendChild(d);
+ });
+ ig.appendChild(labels);
+ ig.appendChild(h("span", "ig-lbl", "IGNITION"));
+ return ig;
+}
+
+function pulls() {
+ const p = h("div", "pulls");
+ const mk = (glyph, label, fn) => {
+ const el = h("div", "pull");
+ el.innerHTML = `<span class="cap">${glyph}</span><span class="lgd">${label}</span>`;
+ el.onclick = fn || (() => DS.momentary(label));
+ return el;
+ };
+ p.appendChild(mk("\u{f033e}", "Lock"));
+ p.appendChild(mk("\u{f04b2}", "Suspend"));
+ p.appendChild(mk("\u{f021c}", "Wallpaper", openWallpaper));
+ return p;
+}
+
+function render() {
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ const bn = h("div", "binnacle");
+ bn.appendChild(gauge("brightness", "BRIGHTNESS"));
+ bn.appendChild(gauge("kbd", "KEYBOARD"));
+ col.appendChild(bn);
+
+ col.appendChild(DS.engrave("Switches"));
+ col.appendChild(rockerBank());
+
+ col.appendChild(DS.engrave("Drive modes"));
+ col.appendChild(driveModes());
+
+ col.appendChild(DS.engrave("Ignition"));
+ col.appendChild(ignition());
+
+ col.appendChild(DS.engrave("Actions"));
+ col.appendChild(pulls());
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-9.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-9.html
new file mode 100644
index 0000000..bc15e94
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-9.html
@@ -0,0 +1,211 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 9 (engine room, Dupre Kit)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ /* Dupre Kit token set (from the gallery :root / tokens.json) — the kit
+ builders read these; they override the shared prototype gold. */
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .row { display: flex; justify-content: center; align-items: flex-start; gap: 8px; flex-wrap: wrap; }
+ /* kit spans that carry width/height need block layout outside the gallery */
+ .dupre-rotsel, .dupre-keylock { display: inline-block; }
+ .cellcap { color: var(--steel); font-size: 9px; letter-spacing: 0.14em; text-align: center; margin-top: 2px; }
+ .flap-well { display: flex; justify-content: center; padding: 6px 0 2px; pointer-events: none; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Direction 9 — Engine room (Dupre Kit).</b> Real kit instruments: the
+ DRUM ROLLER sets both levels (drag the drums), a split-flap board reads the
+ scene tuned on the rotary selector, breaker panels carry the toggles,
+ airplane is a guarded lever, power is an engine-order telegraph (click to
+ ring the next order). Lock/suspend/wallpaper are arm-to-fire.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">Settings</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES); // 4 names
+const SEL = ["OFF", "FOCUS", "PRESENT", "BATTERY", "NIGHT"]; // rotary detents
+let booting = true;
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: off", true);
+ render();
+}
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+// breakerPanel has no set(): sync its hardcoded initial states to DS by
+// clicking the transparent hit rects while booting swallows the onChange.
+function breakers(names, keys) {
+ const host = h("div");
+ const bp = DUPRE.breakerPanel(host, {
+ names,
+ onChange: (states) => {
+ if (booting) return;
+ states.forEach((st, i) => {
+ const on = st === "on";
+ if (on !== DS.state[keys[i]]) DS.setToggle(keys[i], host);
+ });
+ },
+ });
+ const hits = host.querySelectorAll('rect[fill="transparent"]');
+ bp.get().forEach((st, i) => {
+ if ((st === "on") !== DS.state[keys[i]])
+ hits[i].dispatchEvent(new MouseEvent("click", { bubbles: true }));
+ });
+ return host;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // Scenes: split-flap reads the tuned scene, rotary selector picks it.
+ col.appendChild(DS.engrave("Scene board"));
+ const flapWell = h("div", "flap-well");
+ const flap = DUPRE.splitFlap(flapWell, { cells: 8, animate: true, flapMs: 60 });
+ const BOARD = { Focus: "FOCUS", Presentation: "PRESENT", Battery: "BATTERY", Night: "NIGHT" };
+ flap.setText(DS.state.scene ? BOARD[DS.state.scene] : "STANDBY");
+ col.appendChild(flapWell);
+ const selRow = h("div", "row");
+ const selHost = h("div");
+ DUPRE.rotarySelector(selHost, {
+ values: SEL,
+ index: DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0,
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "OFF") sceneOff();
+ else DS.applyScene(SCENES[SEL.indexOf(v) - 1], selHost);
+ },
+ });
+ selRow.appendChild(selHost);
+ selRow.appendChild(Object.assign(h("div", "cellcap", "SCENE SELECT"), { style: "align-self:center" }));
+ col.appendChild(selRow);
+
+ // Levels: the drum roller (Craig favorite #1) — drums are 1-10, ×10 = percent.
+ col.appendChild(DS.engrave("Levels"));
+ const drumRow = h("div", "row");
+ const drumHost = h("div");
+ const commitDrums = debounce((vals) => {
+ const b = Math.round(vals[0]) * 10, k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, drumHost);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, drumHost);
+ }, 350);
+ DUPRE.drumRoller(drumHost, {
+ title: "LEVELS ×10",
+ channels: [
+ { name: "BRIGHT", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.max(1, Math.round(DS.state.kbd / 10)) },
+ ],
+ onChange: (vals) => { if (!booting) commitDrums(vals); },
+ });
+ drumRow.appendChild(drumHost);
+ drumRow.appendChild(h("div"));
+ drumRow.lastChild.appendChild(h("div", "cellcap", "DRAG DRUM"));
+ col.appendChild(drumRow);
+
+ // Toggles: breaker panels + a guarded lever for airplane.
+ col.appendChild(DS.engrave("Breakers"));
+ const brRow = h("div", "row");
+ brRow.appendChild(breakers(["DIM", "CAFF", "TPAD", "MOUSE"], ["autodim", "caffeine", "touchpad", "mouse"]));
+ brRow.appendChild(breakers(["NIGHT", "DND", "WXGEO"], ["nightlight", "dnd", "weathergeo"]));
+ col.appendChild(brRow);
+
+ if (DS.state.laptop) {
+ const gRow = h("div", "row");
+ const gHost = h("div");
+ DUPRE.guardedToggle(gHost, {
+ on: DS.state.airplane,
+ onLabel: "AIRPLANE",
+ offLabel: "RADIO OK",
+ onChange: (on) => { if (!booting && on !== DS.state.airplane) DS.setToggle("airplane", gHost); },
+ });
+ gRow.appendChild(gHost);
+ col.appendChild(gRow);
+ }
+
+ // Power: engine-order telegraph — ring the next order.
+ col.appendChild(DS.engrave("Engine order — power"));
+ const tRow = h("div", "row");
+ const tHost = h("div");
+ DUPRE.telegraphIndicator(tHost, {
+ states: ["PERF", "BAL", "SAVER"],
+ index: DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile),
+ onChange: (i) => { if (!booting) DS.setProfile(DS.PROFILES[i].id, tHost); },
+ });
+ tRow.appendChild(tHost);
+ col.appendChild(tRow);
+
+ // Actions: arm-to-fire.
+ col.appendChild(DS.engrave("Actions"));
+ const aRow = h("div", "row");
+ [["LOCK", () => DS.momentary("Lock")],
+ ["SUSPEND", () => DS.momentary("Suspend")],
+ ["WALLPAPER", openWallpaper]].forEach(([label, fn]) => {
+ const host = h("div");
+ DUPRE.armButton(host, {
+ label,
+ armLabel: label + "?",
+ onChange: (st) => { if (!booting && st === "fired") fn(); },
+ });
+ aRow.appendChild(host);
+ });
+ col.appendChild(aRow);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-scenes-compare.html b/docs/prototypes/2026-07-02-desktop-settings-panel-scenes-compare.html
new file mode 100644
index 0000000..9efc1dc
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-scenes-compare.html
@@ -0,0 +1,177 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — scene control casting: tuner dial vs rotary + split-flap</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ body.ds-stage { flex-direction: row; flex-wrap: wrap; align-items: flex-start; justify-content: center; gap: 24px; padding-top: 40px; }
+ .page-cap { width: 100%; text-align: center; color: var(--dim); font-size: 12px; letter-spacing: 0.04em; }
+ .page-cap b { color: var(--gold); }
+ .dupre-rotsel { display: inline-block; }
+ .row { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; }
+ .flap-well { display: flex; justify-content: center; padding: 4px 0 2px; pointer-events: none; }
+ .tuner-well { display: flex; justify-content: center; padding: 6px 0; }
+ .readout { text-align: center; color: var(--steel); font-size: 11px; letter-spacing: 0.1em; min-height: 15px; }
+ .readout b { color: var(--gold-hi); font-weight: 400; }
+
+ /* preset keys under the dial (the direction-6 keys, Dupre Kit tokens) */
+ .presets { display: grid; grid-template-columns: repeat(4,1fr); gap: 6px; }
+ .preset {
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-bottom-width: 3px; border-radius: 6px;
+ padding: 6px 4px; text-align: center; cursor: pointer; color: var(--silver);
+ }
+ .preset:hover { border-color: var(--gold); color: var(--gold); }
+ .preset .num { font-size: 9px; color: var(--dim); display: block; }
+ .preset .nm { font-size: 10.5px; letter-spacing: 0.06em; }
+ .preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px; transform: translateY(2px);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12); color: var(--cream);
+ }
+ .preset.cur .num { color: var(--gold); }
+
+ /* skin chips for the tuner */
+ .skins { display: flex; gap: 8px; justify-content: center; }
+ .skinchip {
+ color: var(--dim); font-size: 9.5px; letter-spacing: 0.1em; cursor: pointer;
+ border-bottom: 1px dotted var(--wash); text-transform: uppercase;
+ }
+ .skinchip:hover { color: var(--silver); }
+ .skinchip.cur { color: var(--gold-hi); border-color: var(--gold); }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="page-cap">
+ <b>Scene-control casting.</b> Both candidates drive the same scene state —
+ tune one, the other follows. Left: tuner dial (kit slideRule) + preset
+ keys, with skin chips. Right: rotary selector + split-flap board.
+ </div>
+
+ <div class="ds-panel" style="width:300px">
+ <div class="ds-face"><span class="glyph">⚙</span><span class="title">Candidate 1</span></div>
+ <div class="ds-engrave">Tuner dial + presets</div>
+ <div class="tuner-well" id="tuner-well"></div>
+ <div class="skins" id="skins"></div>
+ <div class="presets" id="presets"></div>
+ <div class="readout" id="ro1"></div>
+ </div>
+
+ <div class="ds-panel" style="width:300px">
+ <div class="ds-face"><span class="glyph">⚙</span><span class="title">Candidate 3</span></div>
+ <div class="ds-engrave">Rotary + split-flap</div>
+ <div class="flap-well" id="flap-well"></div>
+ <div class="row" id="rot-well"></div>
+ <div class="readout" id="ro2"></div>
+ </div>
+
+ <div class="ds-toast" id="toast" style="width:300px"></div>
+
+<script src="widgets.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES); // Focus Presentation Battery Night
+const STOPS = ["OFF", "FOCUS", "PRES", "BATT", "NIGHT"];
+const BOARD = { Focus: "FOCUS", Presentation: "PRESENT", Battery: "BATTERY", Night: "NIGHT" };
+let booting = true;
+
+function sceneOff(el) {
+ DS.state.scene = null;
+ DS.toast("Scene: off", true);
+ render();
+}
+const sceneIdx = () => (DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0);
+
+let skin = "warm";
+
+function render() {
+ booting = true;
+
+ // ---- Candidate 1: slideRule tuner + preset keys -------------------------
+ const tw = document.getElementById("tuner-well");
+ tw.innerHTML = "";
+ const dial = DUPRE.slideRule(tw, {
+ values: STOPS,
+ value: STOPS[sceneIdx()],
+ skin,
+ fmt: (v) => v,
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "OFF") sceneOff(tw);
+ else DS.applyScene(SCENES[STOPS.indexOf(v) - 1], tw);
+ },
+ });
+ // string stops: the builder's lit-mark compare is numeric-only; patch it.
+ const glow = () => tw.querySelectorAll(".dupre-mk").forEach((m) =>
+ m.classList.toggle("dupre-on", m.textContent === STOPS[sceneIdx()]));
+ glow();
+
+ const sk = document.getElementById("skins");
+ sk.innerHTML = "";
+ ["warm", "chrome", "black", "blue"].forEach((name) => {
+ const c = h("span", "skinchip" + (name === skin ? " cur" : ""), name);
+ c.onclick = () => { skin = name; dial.setStyle("skin", name); sk.querySelectorAll(".skinchip").forEach((x) => x.classList.toggle("cur", x.textContent === name)); };
+ sk.appendChild(c);
+ });
+
+ const pr = document.getElementById("presets");
+ pr.innerHTML = "";
+ SCENES.forEach((n, i) => {
+ const k = h("div", "preset" + (DS.state.scene === n ? " cur" : ""));
+ k.innerHTML = `<span class="num">PRESET ${i + 1}</span><span class="nm">${n}</span>`;
+ k.onclick = () => DS.applyScene(n, k);
+ pr.appendChild(k);
+ });
+
+ // ---- Candidate 3: rotary selector + split-flap --------------------------
+ const fw = document.getElementById("flap-well");
+ fw.innerHTML = "";
+ const flap = DUPRE.splitFlap(fw, { cells: 8, animate: true, flapMs: 60 });
+ flap.setText(DS.state.scene ? BOARD[DS.state.scene] : "STANDBY");
+
+ const rw = document.getElementById("rot-well");
+ rw.innerHTML = "";
+ const rHost = h("div");
+ DUPRE.rotarySelector(rHost, {
+ values: ["OFF", "FOCUS", "PRESENT", "BATTERY", "NIGHT"],
+ index: sceneIdx(),
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "OFF") sceneOff(rHost);
+ else DS.applyScene(SCENES[["OFF", "FOCUS", "PRESENT", "BATTERY", "NIGHT"].indexOf(v) - 1], rHost);
+ },
+ });
+ rw.appendChild(rHost);
+
+ // ---- readouts -----------------------------------------------------------
+ const cur = DS.state.scene ? DS.state.scene.toUpperCase() : "no scene";
+ document.getElementById("ro1").innerHTML = "tuned: <b>" + cur + "</b>";
+ document.getElementById("ro2").innerHTML = "board: <b>" + cur + "</b>";
+
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-21-night-watch-screensaver-prototype-1.html b/docs/prototypes/2026-07-21-night-watch-screensaver-prototype-1.html
new file mode 100644
index 0000000..7ccc071
--- /dev/null
+++ b/docs/prototypes/2026-07-21-night-watch-screensaver-prototype-1.html
@@ -0,0 +1,165 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Dupre Night Watch — idle-stage screensaver prototype 1</title>
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ * { box-sizing: border-box; }
+ body {
+ margin: 0; min-height: 100vh; background: #050505;
+ color: var(--silver); font-family: var(--mono);
+ display: flex; flex-direction: column; align-items: center;
+ gap: 10px; padding: 24px;
+ zoom: 1.7;
+ cursor: none; /* it's a screensaver */
+ }
+ .board {
+ display: grid;
+ grid-template-columns: repeat(4, auto);
+ gap: 14px 18px;
+ align-items: center;
+ justify-items: center;
+ }
+ .cell {
+ background: linear-gradient(180deg, #16140f, #0e0d0b);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 8px 10px 6px;
+ display: flex; flex-direction: column; align-items: center; gap: 4px;
+ }
+ /* kit instruments are spans with fixed CSS sizes; a flex host blockifies
+ them (the gallery's card grid does this implicitly) */
+ .cell > div:first-child { display: flex; justify-content: center; }
+ .cell .lbl {
+ color: var(--steel); font-size: 8px; letter-spacing: 0.22em;
+ text-transform: uppercase;
+ }
+ .wide { grid-column: span 2; }
+ .masthead {
+ color: var(--steel); font-size: 10px; letter-spacing: 0.35em;
+ text-transform: uppercase; padding-bottom: 2px;
+ }
+ .masthead b { color: var(--gold); font-weight: 400; }
+</style>
+</head>
+<body>
+ <div class="masthead"><b>DUPRE</b> · NIGHT WATCH</div>
+ <div class="board" id="board"></div>
+
+<script src="widgets.js"></script>
+<script>
+const cell = (label, span) => {
+ const c = document.createElement("div");
+ c.className = "cell" + (span ? " wide" : "");
+ const host = document.createElement("div");
+ c.appendChild(host);
+ const l = document.createElement("div");
+ l.className = "lbl"; l.textContent = label;
+ c.appendChild(l);
+ document.getElementById("board").appendChild(c);
+ return host;
+};
+
+/* ---- top row: timekeeping ---------------------------------------------- */
+const flap = DUPRE.splitFlap(cell("local time", true), {
+ cells: 5, chars: " 0123456789:", flapMs: 70, animate: true,
+});
+const clock = DUPRE.analogClock(cell("movement"));
+const seg = DUPRE.sevenSeg(cell("min : sec"));
+
+/* ---- second row: watchstanding ----------------------------------------- */
+DUPRE.radarSweep(cell("scan"), { contacts: [[40, .5], [150, .72], [265, .38], [330, .6]] });
+const scope = DUPRE.scope(cell("trace"));
+const strip = DUPRE.stripChart(cell("drift"), { samples: 60, value: 0.5 });
+const eq = DUPRE.eqBars(cell("spectrum"), { bands: 11, cells: 9 });
+
+/* ---- third row: machine room ------------------------------------------- */
+const vu = DUPRE.vuPair(cell("programme"));
+const blinken = DUPRE.blinkenlights(cell("core"));
+const tape = DUPRE.tapeCounter(cell("elapsed"), { value: 0, wheels: 6, redFrom: 4 });
+const lamps = DUPRE.statusLamps(cell("systems"), { states: [0, 0, 1, 0, 4] });
+
+/* ---- bottom row: comms + orders ---------------------------------------- */
+DUPRE.voiceLoop(cell("loops"));
+const marquee = DUPRE.vfdMarquee(cell("wire", true), {
+ width: 200,
+ msgs: [
+ "DUPRE NIGHT WATCH · ALL SYSTEMS NOMINAL",
+ "IDLE PIPELINE ARMED · DIM 5 · LOCK 7 · DPMS 10",
+ "NEXT ORDER ON THE TELEGRAPH · STAND EASY",
+ ],
+});
+const tele = DUPRE.telegraphIndicator(cell("engine order"), {
+ states: ["AHEAD", "HALF", "SLOW", "STOP", "STAND BY", "FULL"], index: 3,
+});
+
+/* ---- the page owns the clock (tick contract) ---------------------------- */
+let last = { m: -1 };
+const timeTick = () => {
+ const d = new Date();
+ clock.set(d.getHours(), d.getMinutes(), d.getSeconds());
+ seg.set(d.getMinutes() * 60 + d.getSeconds());
+ if (d.getMinutes() !== last.m) {
+ last.m = d.getMinutes();
+ const hh = String(d.getHours()).padStart(2, "0");
+ const mm = String(d.getMinutes()).padStart(2, "0");
+ flap.setText(hh + ":" + mm);
+ }
+};
+timeTick();
+setInterval(timeTick, 1000);
+
+/* signals: slow random walks + a breathing sine */
+let vl = 0.4, vr = 0.5, ph = 0, driftV = 0.5;
+setInterval(() => {
+ vl = Math.max(0.05, Math.min(0.95, vl + (Math.random() - 0.5) * 0.16));
+ vr = Math.max(0.05, Math.min(0.95, vr + (Math.random() - 0.5) * 0.16));
+ vu.set(vl, vr);
+ eq.set(Array.from({ length: 11 }, (_, i) =>
+ Math.max(0, Math.min(1, (vl + vr) / 2 + Math.sin(ph + i * 0.7) * 0.3 + (Math.random() - 0.5) * 0.2))));
+}, 110);
+
+setInterval(() => {
+ ph += 0.25;
+ const n = 64;
+ scope.set(Array.from({ length: n }, (_, i) =>
+ Math.sin(ph + (i / n) * Math.PI * 4) * (0.5 + 0.3 * Math.sin(ph * 0.13))), 6.2);
+}, 90);
+
+setInterval(() => {
+ driftV = Math.max(0.08, Math.min(0.92, driftV + (Math.random() - 0.5) * 0.09));
+ strip.push(driftV);
+}, 450);
+
+setInterval(() => blinken.tick(), 260);
+setInterval(() => marquee.tick(), 40);
+
+let elapsed = 0;
+setInterval(() => tape.set(++elapsed), 1000);
+
+/* the watch changes: a lamp flickers its state every so often, and every
+ couple of minutes the telegraph rings a new order */
+setInterval(() => {
+ const i = Math.floor(Math.random() * 5);
+ lamps.set(i, Math.random() < 0.7 ? 0 : Math.random() < 0.5 ? 1 : 4);
+}, 4200);
+setInterval(() => tele.set(Math.floor(Math.random() * 6)), 120000);
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-22-watch-preview.png b/docs/prototypes/2026-07-22-watch-preview.png
new file mode 100644
index 0000000..9b8419b
--- /dev/null
+++ b/docs/prototypes/2026-07-22-watch-preview.png
Binary files differ
diff --git a/docs/prototypes/desktop-settings-shared.css b/docs/prototypes/desktop-settings-shared.css
index 95aec22..ec677c7 100644
--- a/docs/prototypes/desktop-settings-shared.css
+++ b/docs/prototypes/desktop-settings-shared.css
@@ -34,6 +34,7 @@
body.ds-stage {
margin: 0;
min-height: 100vh;
+ zoom: 1.6; /* the 360px capsule reads small on a large monitor (Craig, 2026-07-21) */
background:
radial-gradient(1200px 600px at 80% -10%, #17151322, transparent),
#050505;
@@ -126,8 +127,11 @@ body.ds-stage {
background: linear-gradient(90deg, var(--line), transparent);
}
-/* Status lamp: a colored dot with a soft glow; off is a dim wash. */
-.ds-lamp {
+/* Status lamp: retired 2026-07-21 (Craig: the glowing dot pulls the eye).
+ State reads from the control's own gold glyph/text instead. The spans
+ still render; this hides them everywhere at once. */
+.ds-lamp { display: none; }
+.ds-lamp-unused {
width: 9px; height: 9px; border-radius: 50%;
background-color: var(--pass);
box-shadow: 0 0 6px 1px rgba(116,147,47,.55);
@@ -228,7 +232,9 @@ body.ds-stage {
background: var(--cream);
border: 1px solid var(--gold);
cursor: pointer;
- box-shadow: -200px 0 0 194px var(--gold); /* gold fill to the left of the knob */
+ /* The old box-shadow fill hack bled a huge gold disc outside the 6px
+ track (the thumb is taller than the input, so overflow can't clip it).
+ The gold fill is now painted by slider() as an inline gradient. */
}
.ds-slider input[type=range]::-moz-range-thumb {
width: 14px; height: 14px; border-radius: 50%;
diff --git a/docs/prototypes/desktop-settings-shared.js b/docs/prototypes/desktop-settings-shared.js
index 39bc76a..f96a03a 100644
--- a/docs/prototypes/desktop-settings-shared.js
+++ b/docs/prototypes/desktop-settings-shared.js
@@ -16,6 +16,7 @@ const DS = (() => {
airplane: false,
nightlight: false,
dnd: false,
+ weathergeo: false,
brightness: 70,
kbd: 40,
powerprofile: "balanced", // performance | balanced | saver
@@ -38,6 +39,7 @@ const DS = (() => {
airplane: { glyph: "\u{f001d}", name: "Airplane", on: "on", off: "off" },
nightlight:{ glyph: "\u{f0594}", name: "Night light", on: "warm", off: "off" },
dnd: { glyph: "\u{f009b}", name: "Do Not Disturb",on:"silenced",off: "notifying" },
+ weathergeo:{ glyph: "\u{f034e}", name: "Weather geo", on: "whereami",off: "pinned" },
};
const SLIDERS = {
brightness: { glyph: "\u{f00df}", name: "Brightness", floor: 5 },
@@ -161,7 +163,13 @@ const DS = (() => {
const inp = h("input");
inp.type = "range"; inp.min = meta.floor; inp.max = 100; inp.value = state[key];
const pct = h("span", "s-pct", state[key] + "%");
- inp.oninput = () => { pct.textContent = inp.value + "%"; };
+ // Gold fill left of the knob, painted as a gradient (no shadow hack).
+ const paint = () => {
+ const p = ((inp.value - meta.floor) / (100 - meta.floor)) * 100;
+ inp.style.background = `linear-gradient(90deg, var(--gold) ${p}%, var(--well) ${p}%)`;
+ };
+ paint();
+ inp.oninput = () => { pct.textContent = inp.value + "%"; paint(); };
inp.onchange = () => setSlider(key, +inp.value, row);
row.appendChild(inp); row.appendChild(pct);
return row;
@@ -202,7 +210,11 @@ const DS = (() => {
"linear-gradient(135deg,#5c5030,#2e2810)",
];
function wallpaperSubview(closeCb) {
- const sv = h("div", "ds-subview");
+ // plain flow container: the CALLER's #subview owns the ds-subview
+ // overlay styling — carrying the class here left the content
+ // display:none inside an open overlay (empty wallpaper view, found
+ // testing prototype 12)
+ const sv = h("div", "ds-col");
const head = h("div", "ds-sub-head");
head.innerHTML = `<span class="back">‹ Back</span><span class="title">Wallpaper</span>`;
head.querySelector(".back").onclick = closeCb;
diff --git a/docs/prototypes/dupre-kit-additions.js b/docs/prototypes/dupre-kit-additions.js
new file mode 100644
index 0000000..005a577
--- /dev/null
+++ b/docs/prototypes/dupre-kit-additions.js
@@ -0,0 +1,1256 @@
+/* dupre-kit-additions.js — kit-candidate builders developed in the
+ * desktop-settings casting work, written to the widgets.js conventions
+ * (DUPRE.name(host, opts) → {el, get, set, ...}; onChange(value, text) on
+ * every state change including the initial paint; no page globals touched)
+ * so they can merge back into the kit with gallery cards.
+ *
+ * Load AFTER widgets.js. Contents:
+ * - DUPRE.detentFader (NEW) — multi-detent slide attenuator with
+ * speedbump drag physics.
+ * - DUPRE.drumRoller (UPGRADE) — backward-compatible redefinition:
+ * 1..N channels (stock hardcodes exactly two), configurable min/max
+ * range and stage height. Defaults reproduce the stock instrument.
+ * - DUPRE.guardedToggle (UPGRADE) — rotateX throw (no planar spin).
+ * - DUPRE.batToggle (UPGRADE) — same throw fix.
+ * - DUPRE.slideRule (UPGRADE) — width option; the printed scale
+ * spreads its stops proportionally. Default keeps stock 180px.
+ * - DUPRE.slideSelector (NEW) — stretchable n-position slide switch
+ * with a status lamp per position (pillSlide's full-size descendant).
+ * - DUPRE.programChart (NEW) — faceplate program matrix / punch card.
+ * - DUPRE.thumbSlideH (NEW) — horizontal thumb-slide attenuator;
+ * continuous 0-100 or detented positions (thumbSlide turned on its
+ * side, one channel, stretchable).
+ * - DUPRE.pinMatrix (UPGRADE) — configurable geometry, per-column
+ * pin colors, clickable button column-heads, group dividers, set().
+ * Defaults reproduce the stock 5×6 synth patch exactly.
+ * - DUPRE.programMatrix (NEW) — the programmable matrix: programs as
+ * column heads, settings as rows, white pins program, the ACTIVE
+ * column's pins run green because the recalled program IS the live
+ * state. No separate live column needed.
+ * - DUPRE.tripRail (NEW) — idle tripper rail: a time-switch tripper
+ * scale for "when idle reaches here, throw this" — draggable stage
+ * tabs on a sqrt-spaced minutes rail, with a parking siding to
+ * disable a stage and a bypass state (caffeine dims the policy).
+ * - DUPRE.tripDial (NEW) — the round form Craig actually imagined:
+ * the Intermatic segment-timer dial with colored tripper tabs on the
+ * rim, an OFF notch at the bottom, and a side legend. Same policy
+ * semantics as the rail.
+ */
+(() => {
+ const noop = () => {};
+ const SVGNS = 'http://www.w3.org/2000/svg';
+
+ /* local mini-helpers (widgets.js keeps its own private; additions carry
+ theirs so the file is self-contained until merged) */
+ const svgEl = (parent, tag, attrs) => {
+ const e = document.createElementNS(SVGNS, tag);
+ for (const k in (attrs || {})) e.setAttribute(k, attrs[k]);
+ parent.appendChild(e); return e;
+ };
+ const stageSvg = (host, cls, vw, vh) => {
+ const s = document.createElementNS(SVGNS, 'svg');
+ s.setAttribute('class', cls); s.setAttribute('viewBox', `0 0 ${vw} ${vh}`);
+ s.setAttribute('width', vw); s.setAttribute('height', vh);
+ host.appendChild(s); return s;
+ };
+ let uidN = 0;
+ const uid = p => p + '-a' + (++uidN);
+
+ /* additions-scoped gradients (ids prefixed so they never collide with the
+ kit's shared defs plate; on merge these fold into gradDef) */
+ let defsRoot = null;
+ const grad = (id, kind, geom, stops) => {
+ if (!defsRoot) {
+ const svg = document.createElementNS(SVGNS, 'svg');
+ svg.setAttribute('width', 0); svg.setAttribute('height', 0);
+ svg.setAttribute('aria-hidden', 'true');
+ svg.style.position = 'absolute';
+ defsRoot = document.createElementNS(SVGNS, 'defs');
+ svg.appendChild(defsRoot);
+ document.body.appendChild(svg);
+ }
+ if (defsRoot.querySelector('#' + id)) return;
+ const g = document.createElementNS(SVGNS, kind);
+ g.setAttribute('id', id);
+ for (const k in geom) g.setAttribute(k, geom[k]);
+ stops.forEach(([o, c]) => {
+ const st = document.createElementNS(SVGNS, 'stop');
+ st.setAttribute('offset', o); st.setAttribute('stop-color', c);
+ g.appendChild(st);
+ });
+ defsRoot.appendChild(g);
+ };
+
+ /* vertical drag with delta accumulation (the kit's dragDelta shape) */
+ const dragDelta = (el, getV, setV, o) => {
+ el.style.touchAction = 'none';
+ el.addEventListener('pointerdown', e => {
+ el.setPointerCapture(e.pointerId);
+ const y0 = e.clientY, v0 = getV();
+ const move = m => setV(Math.max(o.min, Math.min(o.max, v0 + (y0 - m.clientY) * o.sens)));
+ const up = () => { el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', up); };
+ el.addEventListener('pointermove', move);
+ el.addEventListener('pointerup', up);
+ e.preventDefault();
+ });
+ };
+
+ /* additions CSS, injected once (the kit pattern: DUPRE_CSS ships inside
+ widgets.js; these rules move into it on merge) */
+ if (!document.getElementById('dupre-additions-css')) {
+ const st = document.createElement('style');
+ st.id = 'dupre-additions-css';
+ st.textContent = `
+.dupre-dfader{position:relative;display:inline-block;width:230px;height:36px;cursor:ew-resize;touch-action:none}
+.dupre-dfader .df-slot{position:absolute;left:8px;right:8px;top:15px;height:6px;border-radius:4px;
+ background:var(--well,#0a0c0d);border:1px solid #0a0908;box-shadow:inset 0 1px 2px #000}
+.dupre-dfader .df-fill{position:absolute;left:9px;top:16px;height:4px;border-radius:3px;
+ background:linear-gradient(180deg,var(--amber-grad-top,#f2c76a),var(--gold,#e2a038));pointer-events:none}
+.dupre-dfader .df-tick{position:absolute;top:9px;width:2px;height:18px;border-radius:1px;
+ background:var(--steel,#969385);opacity:.55;transform:translateX(-50%);pointer-events:none}
+.dupre-dfader .df-tick.df-park{background:var(--gold-hi,#ffbe54);opacity:1;
+ box-shadow:0 0 5px rgba(var(--glow-hi,255,190,84),.7)}
+.dupre-dfader .df-cap{position:absolute;top:5px;width:12px;height:26px;border-radius:3px;transform:translateX(-50%);
+ background:linear-gradient(90deg,#7e7a70,#e8e5db 45%,#8a867c);border:1px solid #4e4a42;
+ box-shadow:0 2px 4px #000a;pointer-events:none}
+.dupre-dfader .df-cap::after{content:"";position:absolute;left:50%;top:3px;bottom:3px;width:2px;
+ transform:translateX(-50%);background:rgba(0,0,0,.35);border-radius:1px}
+.dupre-dfader.df-parked .df-cap{box-shadow:0 2px 4px #000a,0 0 7px rgba(var(--glow-hi,255,190,84),.55)}
+.dupre-slsel{display:block;width:220px;cursor:pointer;touch-action:none;user-select:none}
+.dupre-slsel .sl-cols{display:grid}
+.dupre-slsel .sl-col{display:flex;flex-direction:column;align-items:center;gap:3px;padding-bottom:5px}
+.dupre-slsel .sl-lamp{width:11px;height:11px;border-radius:50%;
+ background:radial-gradient(circle at 38% 32%,#2e2a24,#171310);border:1px solid #060505}
+.dupre-slsel .sl-col.sl-on .sl-lamp{background:radial-gradient(circle at 38% 32%,var(--gold-hi,#ffbe54),var(--gold,#e2a038) 60%,#8f671f);
+ box-shadow:0 0 7px rgba(var(--glow-hi,255,190,84),.75)}
+.dupre-slsel .sl-lbl{font-size:8px;letter-spacing:.1em;color:var(--dim,#7c838a);text-transform:uppercase;font-family:var(--mono)}
+.dupre-slsel .sl-col.sl-on .sl-lbl{color:var(--gold-hi,#ffbe54)}
+.dupre-slsel .sl-track{position:relative;height:20px;border-radius:10px;
+ background:var(--well,#0a0c0d);border:1px solid #0a0908;box-shadow:inset 0 2px 4px #000}
+.dupre-slsel .sl-knob{position:absolute;top:2px;bottom:2px;border-radius:8px;transform:translateX(-50%);
+ background:linear-gradient(180deg,#f0efec,#c4c1b9 45%,#75726a);border:1px solid #4e4a42;
+ box-shadow:0 2px 4px #000a;transition:left .16s ease}
+.dupre-slsel .sl-knob::after{content:"";position:absolute;left:50%;top:4px;bottom:4px;width:2px;
+ transform:translateX(-50%);background:rgba(0,0,0,.3);border-radius:1px}
+.dupre-rocker.dupre-quiet:not(.dupre-on) .dupre-bot{background:linear-gradient(180deg,#141210,#100e0c);
+ color:var(--dim,#7c838a);box-shadow:inset 0 3px 5px rgba(0,0,0,.55)}
+.dupre-rocker.dupre-green.dupre-on .dupre-top{background:linear-gradient(180deg,#a9c95f,var(--pass,#74932f));
+ color:var(--panel,#100f0f);box-shadow:0 0 10px rgba(116,147,47,.35)}
+.dupre-slsel.sl-dark .sl-knob{background:linear-gradient(180deg,#33302a,#1c1a16);border-color:#3f3b33}
+.dupre-slsel.sl-dark .sl-knob::after{background:rgba(255,255,255,.16)}
+.dupre-pchart{display:block;font-family:var(--mono);pointer-events:none}
+.dupre-pchart .pc-grid{display:grid;row-gap:2px}
+.dupre-pchart .pc-cell{display:flex;align-items:center;justify-content:center;height:14px}
+.dupre-pchart .pc-h{font-size:6.5px;letter-spacing:.08em;color:var(--steel,#969385);text-transform:uppercase}
+.dupre-pchart .pc-name{font-size:7.5px;letter-spacing:.06em;color:var(--dim,#7c838a);
+ text-transform:uppercase;justify-content:flex-end;padding-right:6px}
+.dupre-pchart .pc-dot{width:6px;height:6px;border-radius:50%}
+.dupre-pchart .pc-dot.pc-set{background:#cfd3cf}
+.dupre-pchart .pc-dot.pc-clear{border:1px solid var(--steel,#969385);opacity:.6}
+.dupre-pchart .pc-dash{width:6px;height:1px;background:var(--wash,#2c2f32)}
+.dupre-pchart .pc-pwr{font-size:7.5px;color:var(--steel,#969385)}
+.dupre-pchart .pc-row-on .pc-name{color:#f2f4f2}
+.dupre-pchart .pc-row-on .pc-dot.pc-set{background:#f2f4f2;
+ box-shadow:0 0 5px rgba(242,244,242,.6)}
+.dupre-pchart .pc-row-on .pc-pwr{color:#f2f4f2}
+.dupre-pchart.pc-edit{pointer-events:auto}
+.dupre-pchart.pc-edit .pc-cell:not(.pc-h):not(.pc-name){cursor:pointer;border-radius:4px}
+.dupre-pchart.pc-edit .pc-cell:not(.pc-h):not(.pc-name):hover{background:#ffffff10}
+`;
+ document.head.appendChild(st);
+ }
+
+ /* NEW — multi-detent slide attenuator: a horizontal fader whose thumb
+ parks at each detent; the pointer must push an escape margin past the
+ detent to break free (a speedbump, not a snap). Crossfader's center
+ detent generalized to a set.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (0-100, default 68); detents (values 0-100, default [50] —
+ the crossfader case); magnet (± units that capture the thumb,
+ default 3); escape (extra units past the magnet edge needed to
+ break out, default 3); onChange(v, label) on every change,
+ label 'N' or 'N · detent' while parked, including the initial
+ paint.
+ handle: el, get(), set(v) (clamped 0-100; set() obeys detent capture
+ so a programmatic sweep feels the same bumps), parked()
+ (the detent value or null).
+ CSS lives in the additions block (.dupre-dfader); no other styles
+ involved. */
+ DUPRE.detentFader = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const detents = (opts.detents || [50]).slice().sort((a, b) => a - b);
+ const MAGNET = opts.magnet !== undefined ? opts.magnet : 3;
+ const ESCAPE = opts.escape !== undefined ? opts.escape : 3;
+ const el = document.createElement('span'); el.className = 'dupre-dfader';
+ el.innerHTML = '<span class="df-slot"></span><span class="df-fill"></span>';
+ const ticks = detents.map(d => {
+ const t = document.createElement('span'); t.className = 'df-tick';
+ t.style.left = `calc(8px + ${d / 100} * (100% - 16px))`;
+ el.appendChild(t); return t;
+ });
+ const cap = document.createElement('span'); cap.className = 'df-cap'; el.appendChild(cap);
+ host.appendChild(el);
+
+ let v, park = null;
+ const paint = () => {
+ const left = `calc(8px + ${v / 100} * (100% - 16px))`;
+ cap.style.left = left;
+ el.querySelector('.df-fill').style.width = `calc(${v / 100} * (100% - 18px))`;
+ el.classList.toggle('df-parked', park !== null);
+ ticks.forEach((t, i) => t.classList.toggle('df-park', detents[i] === park));
+ };
+ /* speedbump core: raw is where the pointer says the thumb should be;
+ the thumb only goes there if it isn't captured, and capture only
+ breaks when raw clears the magnet+escape band. */
+ const apply = raw => {
+ raw = Math.max(0, Math.min(100, raw));
+ if (park !== null) {
+ if (Math.abs(raw - park) <= MAGNET + ESCAPE) raw = park; /* still parked */
+ else park = null; /* broke through */
+ }
+ if (park === null) {
+ const near = detents.find(d => Math.abs(raw - d) <= MAGNET);
+ if (near !== undefined) { park = near; raw = near; }
+ }
+ const nv = Math.round(raw);
+ if (nv === v && el.dataset.painted) return;
+ v = nv; el.dataset.painted = 1;
+ paint();
+ onChange(v, park !== null ? v + ' · detent' : String(v));
+ };
+
+ el.addEventListener('pointerdown', e => {
+ el.setPointerCapture(e.pointerId);
+ const rect = () => el.getBoundingClientRect();
+ const pct = m => ((m.clientX - rect().left - 8) / (rect().width - 16)) * 100;
+ apply(pct(e));
+ const move = m => apply(pct(m));
+ const up = () => { el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', up); };
+ el.addEventListener('pointermove', move);
+ el.addEventListener('pointerup', up);
+ e.preventDefault();
+ });
+
+ v = -1; park = null;
+ apply(opts.value !== undefined ? opts.value : 68);
+ return { el, get: () => v, set: apply, parked: () => park };
+ };
+
+ /* UPGRADE — drum roller selector, generalized. Stock hardcodes exactly two
+ drums (the onChange label indexes chans[1] unconditionally, so one
+ channel throws) and a fixed 1-10 range. This redefinition accepts 1..N
+ channels and a min/max range; every default reproduces the stock
+ instrument, so existing callers are unaffected.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: title (engraved header, default 'EQUALIZER'); channels (array of
+ { name, v } drums, any count ≥ 1, default HIGH/LOW pair);
+ min / max (drum range, default 1 / 10); height (stage height,
+ default 140 — a taller stage opens a longer paper window and
+ shows more of the scale); onChange(values array,
+ 'NAME v · NAME v · …') fires on every set, including the
+ initial paint.
+ handle: el, get() (values array), set(i, v) — v clamps to min..max;
+ each drum drags vertically on its own hit strip.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage
+ block of DUPRE_CSS; gradients are additions-scoped until merge. */
+ DUPRE.drumRoller = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const MIN = opts.min !== undefined ? opts.min : 1;
+ const MAX = opts.max !== undefined ? opts.max : 10;
+ const chans = (opts.channels || [{ name: 'HIGH', v: 6 }, { name: 'LOW', v: 8 }])
+ .map(c => ({ name: c.name, v: c.v }));
+ const N = chans.length;
+ const vw = Math.max(130, N * 50 + 40);
+ const vh = Math.max(100, opts.height !== undefined ? opts.height : 140);
+ const railY = 24, railH = vh - 36, winY = railY + 4, winH = railH - 8;
+ const s = stageSvg(host, 'rsvg', vw, vh), cy = winY + winH / 2, step = 17;
+ grad('dkaRail', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#3a3733'], ['.5', '#211f1c'], ['1', '#161412']]);
+ grad('dkaPaper', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#cfc6a8'], ['.5', '#ece4c8'], ['1', '#c6bd9e']]);
+ grad('dkaCurve', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', 'rgba(0,0,0,.4)'], ['.28', 'rgba(0,0,0,0)'], ['.72', 'rgba(0,0,0,0)'], ['1', 'rgba(0,0,0,.4)']]);
+ const localDefs = svgEl(s, 'defs', {});
+ if (opts.title !== '') svgEl(s, 'text', { x: vw / 2, y: 12, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.18em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = opts.title || 'EQUALIZER';
+ const grps = [];
+ const label = () => chans.map(c => `${c.name} ${Math.round(c.v)}`).join(' · ');
+ const set = (i, v) => {
+ v = Math.max(MIN, Math.min(MAX, v)); chans[i].v = v;
+ const p = grps[i];
+ p.nums.style.transform = `translateY(${(v - p.base) * step}px)`;
+ p.chip.textContent = Math.round(v);
+ onChange(chans.map(c => c.v), label());
+ };
+ const x0 = (vw - (N - 1) * 50) / 2; /* drums centered whatever the count */
+ chans.forEach((ch, i) => {
+ const x = x0 + i * 50, cid = uid('dkaClip');
+ svgEl(s, 'text', { x: x + 18, y: winY + 8, 'text-anchor': 'middle', 'font-size': 8, fill: 'var(--steel)' }).textContent = '+';
+ svgEl(s, 'text', { x: x + 18, y: winY + winH - 2, 'text-anchor': 'middle', 'font-size': 8, fill: 'var(--steel)' }).textContent = '−';
+ svgEl(s, 'rect', { x: x - 12, y: railY, width: 24, height: railH, rx: 4, fill: 'url(#dkaRail)', stroke: '#0a0908', 'stroke-width': 1.2 });
+ const clip = svgEl(localDefs, 'clipPath', { id: cid });
+ svgEl(clip, 'rect', { x: x - 9, y: winY, width: 18, height: winH });
+ svgEl(s, 'rect', { x: x - 9, y: winY, width: 18, height: winH, fill: 'url(#dkaPaper)' });
+ const g = svgEl(s, 'g', { 'clip-path': `url(#${cid})` });
+ const nums = svgEl(g, 'g', {}); nums.style.transition = 'transform .12s';
+ for (let n = MIN; n <= MAX; n++)
+ svgEl(nums, 'text', { x, y: cy + 3 + (ch.v - n) * step, 'text-anchor': 'middle', 'font-size': 9, 'font-weight': 700, 'font-family': 'var(--mono)', fill: '#2b2418' }).textContent = n;
+ svgEl(g, 'rect', { x: x - 9, y: winY, width: 18, height: winH, fill: 'url(#dkaCurve)', 'pointer-events': 'none' });
+ svgEl(s, 'rect', { x: x - 8, y: cy - 6.5, width: 16, height: 13, rx: 1.5, fill: '#1a1613', opacity: .92 });
+ const chip = svgEl(s, 'text', { x, y: cy + 3, 'text-anchor': 'middle', 'font-size': 9, 'font-weight': 700, 'font-family': 'var(--mono)', fill: 'var(--cream)' });
+ svgEl(s, 'text', { x, y: vh - 4, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.12em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = ch.name;
+ const hit = svgEl(s, 'rect', { x: x - 14, y: railY, width: 28, height: railH, fill: 'transparent' });
+ hit.style.cursor = 'ns-resize';
+ grps.push({ nums, chip, base: ch.v });
+ dragDelta(hit, () => chans[i].v, v => set(i, v), { min: MIN, max: MAX, sens: (MAX - MIN) / (winH * 1.1) });
+ });
+ chans.forEach((c, i) => set(i, c.v));
+ return { el: s, get: () => chans.map(c => c.v), set };
+ };
+
+ /* UPGRADE — guarded toggle, realistic throw. Stock animates the lever with
+ a 180° planar rotate, so mid-transition the handle points sideways (the
+ Linda Blair problem — Craig, 2026-07-21). A real switch bat flips toward
+ the viewer: this redefinition throws with rotateX, so the lever
+ foreshortens through the pivot and re-extends downward, never sweeping
+ sideways. Contract unchanged from stock: opts onLabel/offLabel/on/
+ onChange(on, 'ON'|'OFF'); handle {el, get, set}. */
+ DUPRE.guardedToggle = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const s = stageSvg(host, 'rsvg press', 90, 100), cx = 45, cy = 52;
+ grad('dkaDiscRed', 'radialGradient', { cx: '42%', cy: '34%', r: '85%' }, [['0', '#d98a6f'], ['.65', 'var(--fail)'], ['1', '#7a2a1a']]);
+ grad('dkaNut', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#8a8578'], ['1', '#4e4a42']]);
+ grad('dkaChrome', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#7e7a70'], ['.45', '#e8e5db'], ['1', '#8a867c']]);
+ grad('dkaTip', 'radialGradient', { cx: '40%', cy: '32%', r: '75%' }, [['0', '#f2efe8'], ['1', '#7e7a70']]);
+ const lblOn = svgEl(s, 'text', { x: cx, y: 12, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.1em', 'font-family': 'var(--mono)', fill: 'var(--gold-hi)' });
+ lblOn.textContent = opts.onLabel || 'ON';
+ const lblOff = svgEl(s, 'text', { x: cx, y: 96, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.1em', 'font-family': 'var(--mono)', fill: 'var(--dim)' });
+ lblOff.textContent = opts.offLabel || 'OFF';
+ svgEl(s, 'circle', { cx, cy, r: 13, fill: 'url(#dkaDiscRed)', stroke: '#4e150a', 'stroke-width': 1 });
+ const pts = []; for (let k = 0; k < 6; k++) { const a = (k * 60 + 30) * Math.PI / 180; pts.push((cx + 11 * Math.cos(a)) + ',' + (cy + 11 * Math.sin(a))); }
+ svgEl(s, 'polygon', { points: pts.join(' '), fill: 'url(#dkaNut)', stroke: '#2b2822' });
+ svgEl(s, 'circle', { cx, cy, r: 6, fill: '#14110e', stroke: '#000' });
+ for (const gx of [16, 74]) {
+ svgEl(s, 'rect', { x: gx - 6, y: cy - 24, width: 12, height: 48, rx: 6, fill: 'url(#dkaNut)', stroke: '#2b2822', 'stroke-width': 1 });
+ svgEl(s, 'rect', { x: gx - 4, y: cy - 21, width: 3.5, height: 42, rx: 2, fill: 'rgba(255,255,255,.14)' });
+ }
+ const lever = svgEl(s, 'g', {});
+ svgEl(lever, 'path', { d: `M ${cx - 3} ${cy} L ${cx - 2} 26 L ${cx + 2} 26 L ${cx + 3} ${cy} Z`, fill: 'url(#dkaChrome)', stroke: '#4e4a42', 'stroke-width': .5 });
+ svgEl(lever, 'rect', { x: cx - 4.5, y: 16, width: 9, height: 12, rx: 2, fill: 'url(#dkaTip)', stroke: '#5e5a52', 'stroke-width': .6 });
+ for (let r = 0; r < 4; r++) svgEl(lever, 'line', { x1: cx - 3.5, y1: 18.5 + r * 2.4, x2: cx + 3.5, y2: 18.5 + r * 2.4, stroke: 'rgba(0,0,0,.25)', 'stroke-width': .8 });
+ lever.style.transformBox = 'view-box';
+ lever.style.transformOrigin = `${cx}px ${cy}px`;
+ lever.style.transition = 'transform .16s ease-in-out';
+ let on;
+ const set = v => {
+ on = !!v;
+ /* rotateX: the bat flips through the pivot plane (foreshortens, then
+ re-extends downward) — the head-on view of a real throw. */
+ lever.style.transform = on ? 'rotateX(0deg)' : 'rotateX(180deg)';
+ lblOn.setAttribute('fill', on ? 'var(--gold-hi)' : 'var(--dim)');
+ lblOff.setAttribute('fill', on ? 'var(--dim)' : 'var(--gold-hi)');
+ onChange(on, on ? 'ON' : 'OFF');
+ };
+ s.addEventListener('click', () => set(!on));
+ set(opts.on !== undefined ? opts.on : true);
+ return { el: s, get: () => on, set };
+ };
+
+ /* UPGRADE — bat toggle, realistic throw. Same disease and same cure as
+ guardedToggle: stock spins the bat 180° in the plane (sideways at the
+ midpoint); this flips it with rotateX like a real switch bat. Contract
+ unchanged: opts onLabel/offLabel/on/onChange(on, 'ON'|'OFF'); handle
+ {el, get, set}. */
+ DUPRE.batToggle = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const s = stageSvg(host, 'rsvg press', 70, 90), cx = 35, cy = 44;
+ grad('dkaNut', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#8a8578'], ['1', '#4e4a42']]);
+ grad('dkaChrome', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#7e7a70'], ['.45', '#e8e5db'], ['1', '#8a867c']]);
+ grad('dkaTip', 'radialGradient', { cx: '40%', cy: '32%', r: '75%' }, [['0', '#f2efe8'], ['1', '#7e7a70']]);
+ const lblOn = svgEl(s, 'text', { x: cx, y: 9, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.1em', 'font-family': 'var(--mono)', fill: 'var(--gold-hi)' });
+ lblOn.textContent = opts.onLabel || 'ON';
+ const lblOff = svgEl(s, 'text', { x: cx, y: 87, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.1em', 'font-family': 'var(--mono)', fill: 'var(--dim)' });
+ lblOff.textContent = opts.offLabel || 'OFF';
+ const pts = []; for (let k = 0; k < 6; k++) { const a = (k * 60 + 30) * Math.PI / 180; pts.push((cx + 17 * Math.cos(a)) + ',' + (cy + 17 * Math.sin(a))); }
+ svgEl(s, 'polygon', { points: pts.join(' '), fill: 'url(#dkaNut)', stroke: '#2b2822' });
+ svgEl(s, 'circle', { cx, cy, r: 9, fill: '#14110e', stroke: '#000' });
+ const lever = svgEl(s, 'g', {});
+ svgEl(lever, 'path', { d: `M ${cx - 3.2} ${cy} L ${cx - 1.8} 21 L ${cx + 1.8} 21 L ${cx + 3.2} ${cy} Z`, fill: 'url(#dkaChrome)', stroke: '#4e4a42', 'stroke-width': .5 });
+ svgEl(lever, 'ellipse', { cx, cy: 17, rx: 6.5, ry: 8, fill: 'url(#dkaTip)', stroke: '#5e5a52', 'stroke-width': .6 });
+ lever.style.transformBox = 'view-box';
+ lever.style.transformOrigin = `${cx}px ${cy}px`;
+ lever.style.transition = 'transform .16s ease-in-out';
+ let on;
+ const set = v => {
+ on = !!v;
+ lever.style.transform = on ? 'rotateX(0deg)' : 'rotateX(180deg)';
+ lblOn.setAttribute('fill', on ? 'var(--gold-hi)' : 'var(--dim)');
+ lblOff.setAttribute('fill', on ? 'var(--dim)' : 'var(--gold-hi)');
+ onChange(on, on ? 'ON' : 'OFF');
+ };
+ s.addEventListener('click', () => set(!on));
+ set(opts.on !== undefined ? opts.on : true);
+ return { el: s, get: () => on, set };
+ };
+
+ /* NEW — n-position slide switch with a status lamp per position.
+ pillSlide's full-size descendant: the pill slide is a fixed 110px SVG
+ with mix LEDs; this one is DOM-built so it stretches to any width, and
+ each position carries its own lamp + label (only the active one lit).
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: items (position labels, default ['A','B','C']); index (initial
+ position, default 0); width (px, default 220 — set it to match
+ a neighboring instrument); knob ('chrome' default, or 'dark'
+ for a machined-black cap); onChange(index, label) fires on
+ every set, including the initial paint.
+ handle: el, get() (the index), set(i) (clamped); click or drag lands
+ on the nearest position.
+ CSS lives in the additions block (.dupre-slsel); the lamp is
+ self-styled (the shared dupre-lamp is display-managed by consumers). */
+ DUPRE.slideSelector = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const items = opts.items || ['A', 'B', 'C'];
+ const N = items.length;
+ const el = document.createElement('div'); el.className = 'dupre-slsel' + (opts.knob === 'dark' ? ' sl-dark' : '');
+ if (opts.width) el.style.width = opts.width + 'px';
+ const cols = document.createElement('div'); cols.className = 'sl-cols';
+ cols.style.gridTemplateColumns = `repeat(${N},1fr)`;
+ const colEls = items.map(lbl => {
+ const c = document.createElement('div'); c.className = 'sl-col';
+ c.innerHTML = `<span class="sl-lamp"></span><span class="sl-lbl">${lbl}</span>`;
+ cols.appendChild(c); return c;
+ });
+ el.appendChild(cols);
+ const track = document.createElement('div'); track.className = 'sl-track';
+ const knob = document.createElement('div'); knob.className = 'sl-knob';
+ track.appendChild(knob); el.appendChild(track);
+ host.appendChild(el);
+ knob.style.width = `calc(${100 / N}% - 8px)`;
+ let idx;
+ const set = i => {
+ idx = Math.max(0, Math.min(N - 1, i | 0));
+ knob.style.left = ((idx + 0.5) / N * 100) + '%';
+ colEls.forEach((c, k) => c.classList.toggle('sl-on', k === idx));
+ onChange(idx, items[idx]);
+ };
+ const pick = e => {
+ const r = track.getBoundingClientRect();
+ set(Math.floor(((e.clientX - r.left) / r.width) * N));
+ };
+ el.addEventListener('pointerdown', e => {
+ el.setPointerCapture(e.pointerId);
+ pick(e);
+ const move = m => pick(m);
+ const up = () => { el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', up); };
+ el.addEventListener('pointermove', move);
+ el.addEventListener('pointerup', up);
+ e.preventDefault();
+ });
+ set(opts.index !== undefined ? opts.index : 0);
+ return { el, get: () => idx, set };
+ };
+
+ /* UPGRADE — slide-rule dial with a width option. Stock prints its stops
+ at hardcoded x positions inside a fixed 180px window; this redefinition
+ spreads majors evenly across opts.width (default keeps stock geometry).
+ Contract otherwise unchanged: values/value/index/fmt/skin,
+ onChange(v, label); handle {el, get, set, setStyle}; unit stops still
+ interpolate between adjacent numeric majors; ←/→ step. */
+ (() => {
+ const OLD_STYLES = DUPRE.slideRule.STYLES;
+ DUPRE.slideRule = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const majors = opts.values || [4, 6, 8, 10, 12];
+ const fmt = opts.fmt || (v => 'pos ' + v);
+ const W = opts.width || 180;
+ /* custom widths get a wider edge margin so end-stop labels stay inside */
+ const M = opts.width ? 20 : 12;
+ const X = majors.map((_, i) => M + i * (W - 2 * M) / (majors.length - 1));
+ const stops = [];
+ majors.forEach((v, i) => {
+ stops.push({ v, x: X[i], major: true });
+ const b = majors[i + 1];
+ if (typeof v === 'number' && typeof b === 'number')
+ for (let u = v + 1; u < b; u++)
+ stops.push({ v: u, x: X[i] + (X[i + 1] - X[i]) * (u - v) / (b - v), major: false });
+ });
+ const t = document.createElement('span'); t.className = 'dupre-tuner';
+ if (opts.width) t.style.width = W + 'px';
+ t.tabIndex = 0; t.setAttribute('role', 'slider'); t.setAttribute('aria-label', 'slide-rule value');
+ const setStyle = (axis, name) => {
+ const o = (DUPRE.slideRule.STYLES[axis] || {})[name];
+ if (!o) return;
+ for (const [k, v] of Object.entries(o.vars)) t.style.setProperty(k, v);
+ };
+ setStyle('skin', opts.skin || 'warm');
+ stops.forEach(sp => {
+ t.insertAdjacentHTML('beforeend', sp.major
+ ? `<span class="dupre-tick" style="left:${sp.x}px"></span><span class="dupre-mk" style="left:${sp.x}px">${sp.v}</span>`
+ : `<span class="dupre-tick dupre-mn" style="left:${sp.x}px"></span>`);
+ });
+ const ndl = document.createElement('span'); ndl.className = 'dupre-ndl'; t.appendChild(ndl);
+ host.appendChild(t);
+ let idx;
+ const set = i => {
+ idx = Math.max(0, Math.min(stops.length - 1, i));
+ ndl.style.left = stops[idx].x + 'px';
+ t.querySelectorAll('.dupre-mk').forEach(m =>
+ m.classList.toggle('dupre-on', m.textContent === String(stops[idx].v)));
+ onChange(stops[idx].v, fmt(stops[idx].v));
+ };
+ t.addEventListener('click', e => {
+ const r = t.getBoundingClientRect();
+ const x = (e.clientX - r.left) * (t.offsetWidth / r.width);
+ let best = 0, bd = 1e9; stops.forEach((sp, j) => { const d = Math.abs(sp.x - x); if (d < bd) { bd = d; best = j; } });
+ set(best); t.focus();
+ });
+ t.addEventListener('keydown', e => {
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); set(idx - 1); }
+ else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); set(idx + 1); }
+ });
+ const initV = opts.value !== undefined ? opts.value : majors[opts.index !== undefined ? opts.index : 2];
+ const init = stops.findIndex(sp => sp.v === initV);
+ set(init < 0 ? 0 : init);
+ return { el: t, get: () => stops[idx].v, set, setStyle };
+ };
+ DUPRE.slideRule.STYLES = OLD_STYLES;
+ })();
+
+ /* UPGRADE — rocker with a `quiet` option. Stock paints the off state's
+ bottom half warning-red, which reads as a fault for switches whose off
+ state is unremarkable (auto-dim off is not an alarm). quiet: true keeps
+ the off half plain dark. Red-off stays the default for switches where
+ off IS the notable state. Contract otherwise unchanged.
+ Intended pairing: onLabel carries the switch's FUNCTION name (lit when
+ engaged), offLabel is empty — state reads from light and position, not
+ from ON/OFF prose. opts.accent: 'amber' (default) or 'green' — green
+ for run/engaged semantics per the kit accent family. */
+ DUPRE.rocker = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const r = document.createElement('span'); r.className = 'dupre-rocker'
+ + (opts.quiet ? ' dupre-quiet' : '') + (opts.accent === 'green' ? ' dupre-green' : '');
+ r.innerHTML = `<span class="dupre-half dupre-top">${opts.onLabel !== undefined ? opts.onLabel : 'ON'}</span><span class="dupre-half dupre-bot">${opts.offLabel !== undefined ? opts.offLabel : 'OFF'}</span>`;
+ host.appendChild(r);
+ let on;
+ const set = v => { on = !!v; r.classList.toggle('dupre-on', on); onChange(on, on ? 'ON' : 'OFF'); };
+ r.addEventListener('click', () => set(!on));
+ set(opts.on !== undefined ? opts.on : true);
+ return { el: r, get: () => on, set };
+ };
+
+ /* NEW — program chart: the washing-machine faceplate matrix, now a punch
+ card. One row per program, one column per contact; a filled WHITE dot
+ means the program will close it on recall, a ring means it will open
+ it, a dash means it leaves it alone; letter cells carry enums. The dots
+ are future state, so they light white — deliberately outside the amber
+ live-state family. With editable: true, clicking a cell reprograms the
+ preset: dot cells cycle set → clear → untouched, letter cells cycle
+ the `letters` list; onEdit reports the change and the consumer owns
+ the stored program.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: cols (column labels); rows ({ name, cells } aligned with cols:
+ true dot / false ring / null dash / string letter); active
+ (lit row, -1 none); width (px, default 260); editable (false);
+ letters (cycle for letter columns, default ['P','B','S',null]);
+ onEdit(rowIdx, colIdx, newValue) on every cell edit;
+ onChange(index, row name | 'custom') on every set, including
+ the initial paint.
+ handle: el, get() (active index), set(i) (-1..rows-1).
+ CSS lives in the additions block (.dupre-pchart); no other styles
+ involved. */
+ DUPRE.programChart = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const onEdit = opts.onEdit || noop;
+ const cols = opts.cols || ['A', 'B', 'C'];
+ const rows = (opts.rows || []).map(r => ({ name: r.name, cells: r.cells.slice() }));
+ const LETTERS = opts.letters || ['P', 'B', 'S', null];
+ /* a column is a letter column if any row carries a string there */
+ const letterCol = ci => rows.some(r => typeof r.cells[ci] === 'string');
+ const el = document.createElement('div');
+ el.className = 'dupre-pchart' + (opts.editable ? ' pc-edit' : '');
+ el.style.width = (opts.width || 260) + 'px';
+ const grid = document.createElement('div'); grid.className = 'pc-grid';
+ grid.style.gridTemplateColumns = `minmax(52px,auto) repeat(${cols.length},1fr)`;
+ const cell = cls => { const c = document.createElement('div'); c.className = 'pc-cell' + (cls ? ' ' + cls : ''); grid.appendChild(c); return c; };
+ cell('pc-h');
+ cols.forEach(c => { cell('pc-h').textContent = c; });
+ const paintCell = (c, v) => {
+ c.classList.remove('pc-pwr');
+ if (v === true) c.innerHTML = '<span class="pc-dot pc-set"></span>';
+ else if (v === false) c.innerHTML = '<span class="pc-dot pc-clear"></span>';
+ else if (typeof v === 'string') { c.classList.add('pc-pwr'); c.textContent = v; }
+ else c.innerHTML = '<span class="pc-dash"></span>';
+ };
+ const rowEls = rows.map((r, ri) => {
+ const els = [cell('pc-name')];
+ els[0].textContent = r.name;
+ cols.forEach((_, ci) => {
+ const c = cell();
+ paintCell(c, r.cells[ci]);
+ if (opts.editable) c.addEventListener('click', () => {
+ const v = rows[ri].cells[ci];
+ let nv;
+ if (letterCol(ci)) {
+ const cur = LETTERS.indexOf(v === undefined ? null : v);
+ nv = LETTERS[(cur + 1) % LETTERS.length];
+ } else if (v === true) nv = false;
+ else if (v === false) nv = null;
+ else nv = true;
+ rows[ri].cells[ci] = nv;
+ paintCell(c, nv);
+ onEdit(ri, ci, nv);
+ });
+ els.push(c);
+ });
+ return els;
+ });
+ el.appendChild(grid); host.appendChild(el);
+ let idx;
+ const set = i => {
+ idx = Math.max(-1, Math.min(rows.length - 1, i));
+ rowEls.forEach((els, ri) => els.forEach(c => c.classList.toggle('pc-row-on', ri === idx)));
+ onChange(idx, idx < 0 ? 'custom' : rows[idx].name);
+ };
+ set(opts.active !== undefined ? opts.active : -1);
+ return { el, get: () => idx, set };
+ };
+
+ /* NEW — horizontal thumb-slide attenuator. The vertical thumbSlide turned
+ on its side: one channel, a dark rail with a chrome thumb tab, printed
+ scale beneath. Continuous by default; pass positions for a detented
+ selector (the three-position switch case) — the tab snaps and the
+ active position label lights.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: width (px, default 220); value (0-100, default 50 — continuous
+ mode); positions (label array — switches to detent mode);
+ index (initial detent, default 0); onChange — continuous:
+ (value, 'N'); detented: (index, label). Fires on every set,
+ including the initial paint.
+ handle: el, get() (value or index), set(v|i). Drag or click anywhere
+ on the rail.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage
+ block of DUPRE_CSS; gradients are additions-scoped until merge. */
+ DUPRE.thumbSlideH = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const W = opts.width || 220;
+ const POS = opts.positions || null;
+ const H = 36, x0 = 14, x1 = W - 14, railY = 14;
+ grad('dkaThRail', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#3a3733'], ['.5', '#211f1c'], ['1', '#161412']]);
+ grad('dkaThTab', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#f2efe6'], ['1', '#c6c1b3']]);
+ const s = stageSvg(host, 'rsvg', W, H);
+ svgEl(s, 'rect', { x: x0 - 8, y: railY - 7, width: x1 - x0 + 16, height: 14, rx: 3, fill: 'url(#dkaThRail)', stroke: '#0a0908', 'stroke-width': 1 });
+ svgEl(s, 'rect', { x: x0 - 3, y: railY - 3, width: x1 - x0 + 6, height: 6, fill: '#0d0a08', stroke: '#050404' });
+ const labels = [];
+ const xAt = f => x0 + f * (x1 - x0);
+ if (POS) {
+ POS.forEach((lbl, i) => {
+ const x = xAt(POS.length === 1 ? 0.5 : i / (POS.length - 1));
+ svgEl(s, 'line', { x1: x, y1: railY + 5, x2: x, y2: railY + 8, stroke: 'var(--steel)', 'stroke-width': 1 });
+ const t = svgEl(s, 'text', { x, y: H - 3, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.08em', 'font-family': 'var(--mono)', fill: 'var(--dim)' });
+ t.textContent = lbl; labels.push(t);
+ });
+ } else {
+ for (let n = 0; n <= 10; n++) {
+ const x = xAt(n / 10);
+ svgEl(s, 'line', { x1: x, y1: railY + 5, x2: x, y2: railY + (n % 2 ? 6.5 : 8), stroke: 'var(--steel)', 'stroke-width': n % 2 ? .5 : .9, opacity: n % 2 ? .5 : .8 });
+ if (n % 2 === 0) {
+ const t = svgEl(s, 'text', { x, y: H - 3, 'text-anchor': 'middle', 'font-size': 5.5, 'font-family': 'var(--mono)', fill: 'var(--gold)', opacity: .55 });
+ t.textContent = n * 10; labels.push(t);
+ }
+ }
+ }
+ const tab = svgEl(s, 'g', {});
+ svgEl(tab, 'rect', { x: -6, y: railY - 9, width: 12, height: 18, rx: 2.5, fill: 'url(#dkaThTab)', stroke: '#7a766a', 'stroke-width': .6 });
+ svgEl(tab, 'rect', { x: -1, y: railY - 6.5, width: 2, height: 13, fill: 'rgba(0,0,0,.25)' });
+ let val;
+ const paint = () => {
+ const f = POS ? (POS.length === 1 ? 0.5 : val / (POS.length - 1)) : val / 100;
+ tab.setAttribute('transform', `translate(${xAt(f)},0)`);
+ if (POS) labels.forEach((t, i) => {
+ t.setAttribute('fill', i === val ? 'var(--gold-hi)' : 'var(--dim)');
+ });
+ else labels.forEach((t, i) => {
+ const on = Math.abs(i * 20 - val) <= 10;
+ t.setAttribute('fill', on ? 'var(--gold-hi)' : 'var(--gold)'); t.setAttribute('opacity', on ? '1' : '.55');
+ });
+ };
+ const set = v => {
+ val = POS ? Math.max(0, Math.min(POS.length - 1, Math.round(v)))
+ : Math.max(0, Math.min(100, Math.round(v)));
+ paint();
+ if (POS) onChange(val, POS[val]);
+ else onChange(val, String(val));
+ };
+ const fromPointer = e => {
+ const r = s.getBoundingClientRect();
+ const f = Math.max(0, Math.min(1, ((e.clientX - r.left) * (W / r.width) - x0) / (x1 - x0)));
+ set(POS ? f * (POS.length - 1) : f * 100);
+ };
+ s.style.cursor = 'ew-resize'; s.style.touchAction = 'none';
+ s.addEventListener('pointerdown', e => {
+ s.setPointerCapture(e.pointerId);
+ fromPointer(e);
+ const move = m => fromPointer(m);
+ const up = () => { s.removeEventListener('pointermove', move); s.removeEventListener('pointerup', up); };
+ s.addEventListener('pointermove', move);
+ s.addEventListener('pointerup', up);
+ e.preventDefault();
+ });
+ set(POS ? (opts.index !== undefined ? opts.index : 0) : (opts.value !== undefined ? opts.value : 50));
+ return { el: s, get: () => val, set };
+ };
+
+ /* UPGRADE — pin routing matrix, generalized. Stock is a fixed 160×110
+ five-by-six with gold pins, text heads, and no set(). This
+ redefinition keeps those defaults exactly and adds: width/dy geometry,
+ colColors (pin fill per column), colHeads: 'button' (column labels
+ become clickable key plates; setActiveCol lights one), groups (row
+ counts drawing hairline dividers between row groups), set(key, on),
+ and an onPin convenience callback.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: rows / cols (labels); pins ('ROW>COL' keys); width (px —
+ custom geometry mode; omit for stock); dy (row pitch, default
+ 16 in custom mode); x0 (label gutter, default 56 in custom
+ mode); colColors (css fill per column index, default gold);
+ colHeads ('text' default | 'button'); activeCol (initial lit
+ head, -1); groups (e.g. [4,3] → divider after row 4);
+ onChange(pins array, label) on every seat/pull including the
+ initial paint; onPin(rowLabel, colLabel, seated) per click;
+ onColHead(colIndex) when a button head is clicked.
+ handle: el, get() (pin keys), set(key, on), setActiveCol(i).
+ SVG-built: styling is inline attributes plus the shared .rsvg stage
+ block of DUPRE_CSS. */
+ DUPRE.pinMatrix = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const onPin = opts.onPin || noop;
+ const onColHead = opts.onColHead || noop;
+ const ROWS = opts.rows || ['OSC1', 'OSC2', 'LFO', 'NOIS', 'ENV'];
+ const COLS = opts.cols || ['VCF', 'VCA', 'PAN', 'DLY', 'OUT', 'MOD'];
+ const custom = !!opts.width;
+ const W = opts.width || 160;
+ const X0 = custom ? (opts.x0 || 56) : 42;
+ const DY = custom ? (opts.dy || 16) : 15.5;
+ const Y0 = custom ? (opts.colHeads === 'button' ? 40 : 28) : 30;
+ /* button heads are wider than a pin column; leave them room at the edge */
+ const DX = custom ? (W - X0 - (opts.colHeads === 'button' ? 36 : 16)) / (COLS.length - 1) : 19;
+ const H = custom ? Y0 + (ROWS.length - 1) * DY + 14 : 110;
+ const COLC = opts.colColors || [];
+ const groups = opts.groups || null;
+ const pins = new Set((opts.pins || (custom ? [] : ['OSC1>VCF', 'LFO>PAN', 'ENV>VCA']))
+ .filter(k => { const [r, c] = k.split('>'); return ROWS.includes(r) && COLS.includes(c) && k === r + '>' + c; }));
+ const s = stageSvg(host, 'rsvg press', W, H);
+ svgEl(s, 'rect', { x: 2, y: 2, width: W - 4, height: H - 4, rx: 8, fill: '#1c1916', stroke: '#060505', 'stroke-width': 1.5 });
+ let activeCol = opts.activeCol !== undefined ? opts.activeCol : -1;
+ const headEls = [];
+ COLS.forEach((c, j) => {
+ const cx = X0 + j * DX;
+ if (opts.colHeads === 'button') {
+ const bw = Math.min(DX - 4, 58);
+ const g = svgEl(s, 'g', {});
+ const rect = svgEl(g, 'rect', { x: cx - bw / 2, y: 8, width: bw, height: 20, rx: 5,
+ fill: '#131110', stroke: '#33302b', 'stroke-width': 1 });
+ const txt = svgEl(g, 'text', { x: cx, y: 21, 'text-anchor': 'middle', 'font-size': 6,
+ 'letter-spacing': '.04em', 'font-family': 'var(--mono)', fill: 'var(--silver)' });
+ txt.textContent = c;
+ g.style.cursor = 'pointer';
+ g.addEventListener('click', () => onColHead(j));
+ headEls.push({ rect, txt });
+ } else {
+ svgEl(s, 'text', { x: cx, y: custom ? 18 : 16, 'text-anchor': 'middle', 'font-size': custom ? 6 : 5.4,
+ 'letter-spacing': '.05em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = c;
+ headEls.push(null);
+ }
+ });
+ const paintHeads = () => headEls.forEach((he, j) => {
+ if (!he) return;
+ he.rect.setAttribute('stroke', j === activeCol ? 'var(--gold)' : '#33302b');
+ he.rect.setAttribute('fill', j === activeCol ? '#2a2618' : '#131110');
+ he.txt.setAttribute('fill', j === activeCol ? 'var(--cream)' : 'var(--silver)');
+ });
+ paintHeads();
+ ROWS.forEach((r, i) => svgEl(s, 'text', { x: X0 - 12, y: Y0 + i * DY + 2, 'text-anchor': 'end',
+ 'font-size': custom ? 6 : 5.4, 'letter-spacing': '.05em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = r);
+ if (groups) {
+ let acc = 0;
+ groups.slice(0, -1).forEach(n => {
+ acc += n;
+ const y = Y0 + (acc - 0.5) * DY;
+ svgEl(s, 'line', { x1: 10, y1: y, x2: W - 10, y2: y, stroke: '#2c2820', 'stroke-width': 1 });
+ });
+ }
+ const caps = {};
+ ROWS.forEach((r, i) => COLS.forEach((c, j) => {
+ const cx = X0 + j * DX, cy = Y0 + i * DY, key = r + '>' + c;
+ svgEl(s, 'circle', { cx, cy, r: 3.4, fill: '#0a0908', stroke: '#2c2820', 'stroke-width': 1 });
+ const fill = COLC[j] || 'var(--gold)';
+ const cap = svgEl(s, 'circle', { cx, cy, r: 4.4, fill, stroke: 'rgba(0,0,0,.45)', 'stroke-width': 1,
+ opacity: pins.has(key) ? 1 : 0, filter: 'drop-shadow(0 1px 2px rgba(0,0,0,.6))' });
+ caps[key] = cap;
+ const hit = svgEl(s, 'circle', { cx, cy, r: Math.min(8, DX / 2 - 1), fill: 'transparent' });
+ hit.style.cursor = 'pointer';
+ hit.addEventListener('click', () => {
+ const on = !pins.has(key);
+ if (on) pins.add(key); else pins.delete(key);
+ cap.setAttribute('opacity', on ? 1 : 0);
+ onPin(r, c, on);
+ onChange([...pins], (on ? r + ' → ' + c : 'pulled ' + r + ' → ' + c) + ' · ' + pins.size + ' routes');
+ });
+ }));
+ onChange([...pins], pins.size + ' routes');
+ return {
+ el: s,
+ get: () => [...pins],
+ set: (key, on) => {
+ if (!caps[key]) return;
+ if (on) pins.add(key); else pins.delete(key);
+ caps[key].setAttribute('opacity', on ? 1 : 0);
+ },
+ setActiveCol: i => { activeCol = i; paintHeads(); },
+ };
+ };
+
+ /* NEW — programmable matrix. The pin routing matrix specialized into a
+ program store: each column is a stored program, each row a setting; a
+ WHITE jewel pin means the program includes that setting. Pressing a
+ head key activates the program and the active column renders its
+ verdicts — active-is-live: a seated pin becomes a green jewel, an
+ empty socket lights a smaller red one (the program holds it off).
+ Editing the active column edits the present; a white column, a
+ future.
+
+ Head keys are the kit console-key primitive (.dupre-key), lighting
+ GREEN when their program is active. Programs may be strings or
+ { id, label, glyph, slim } — a glyph renders large on the key in
+ place of the label (label still names the program in callbacks);
+ blank labels make unassigned slots; ids keep pin keys unique. Rows are strings (pin rows) or { label, values }
+ — a LETTER ROW: every program cell is a tape-counter wheel (cream
+ paper, bezeled window, printed glyph from `values`); clicking a wheel
+ cycles that program's value; the active column's wheel IS the live
+ value and carries no highlight (paper doesn't glow).
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: rows (strings | {label, values}); programs (strings |
+ {id, label}); pins ('ROW>PROGID' keys); letters
+ ({'ROW>PROGID': glyph}); radios (row-label arrays: one pin
+ per program in the group; a radio pin moves, never pulls);
+ groups (row counts → hairline dividers); active (program
+ index, -1); width / x0 / dy (508/128/23-ish); engraved (row
+ labels as infilled relief); onActivate(index, programId) on
+ head press; onPin(rowLabel, programId, seated, isActive);
+ onLetter(rowLabel, programId, glyph, isActive) on wheel click.
+ handle: el (the wrap), get() (pins), set(row, programId, on),
+ setLetter(row, programId, glyph), setActive(i), getActive().
+ Composite-built: DOM console keys over an SVG pin field. */
+ DUPRE.programMatrix = function (host, opts = {}) {
+ const onActivate = opts.onActivate || noop;
+ const onPin = opts.onPin || noop;
+ const onLetter = opts.onLetter || noop;
+ const ROWS = (opts.rows || []).map(r => typeof r === 'string' ? { label: r } : r);
+ const PROGS = (opts.programs || ['P1', 'P2']).map(p => typeof p === 'string' ? { id: p, label: p } : p);
+ /* slim columns (e.g. unassigned slots) take a narrower band */
+ const weights = PROGS.map(p => p.slim ? 0.55 : 1);
+ const wSum = weights.reduce((a, b) => a + b, 0);
+ const radios = (opts.radios || []).map(g => g.slice());
+ const W = opts.width || 306;
+ const X0 = opts.x0 || 104;
+ const DY = opts.dy || 21;
+ const Y0 = 52;
+ const span = W - X0 - 30;
+ const colX = []; /* column centers by cumulative weight bands */
+ { let acc = 0; weights.forEach(w => { colX.push(X0 - 14 + (acc + w / 2) / wSum * span); acc += w; }); }
+ const colW = weights.map(w => w / wSum * span);
+ const DX = colW.length > 1 ? Math.min(...colW) : span; /* hit-radius basis */
+ const H = Y0 + (ROWS.length - 1) * DY + 18;
+ const WHITE = '#cfd3cf', GREEN = '#a9c95f';
+ grad('dkaJewelG', 'radialGradient', { cx: '38%', cy: '30%', r: '80%' }, [['0', '#dcebae'], ['.5', '#a9c95f'], ['1', '#55702a']]);
+ grad('dkaJewelR', 'radialGradient', { cx: '38%', cy: '30%', r: '80%' }, [['0', '#e0937e'], ['.5', '#c0503a'], ['1', '#6e2517']]);
+ grad('dkaJewelW', 'radialGradient', { cx: '38%', cy: '30%', r: '80%' }, [['0', '#f2f4f2'], ['.55', '#cfd3cf'], ['1', '#8e948e']]);
+ grad('dkaPaper2', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#cfc6a8'], ['.5', '#ece4c8'], ['1', '#c6bd9e']]);
+ grad('dkaCurve2', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', 'rgba(0,0,0,.45)'], ['.3', 'rgba(0,0,0,0)'], ['.7', 'rgba(0,0,0,0)'], ['1', 'rgba(0,0,0,.45)']]);
+ const pins = new Set(opts.pins || []);
+ const letters = Object.assign({}, opts.letters || {});
+ let active = opts.active !== undefined ? opts.active : -1;
+ /* container: a DOM head row of kit console keys over the SVG pin field */
+ const wrap = document.createElement('div');
+ wrap.style.cssText = 'position:relative;width:' + W + 'px';
+ host.appendChild(wrap);
+ const s = stageSvg(wrap, 'rsvg press', W, H);
+ svgEl(s, 'rect', { x: 2, y: 2, width: W - 4, height: H - 4, rx: 8, fill: '#0e0c0a', stroke: '#060505', 'stroke-width': 1.5 });
+ const heads = PROGS.map((pgm, j) => {
+ const cx = colX[j];
+ const headBw = Math.min(colW[j] - 8, 82);
+ const b = document.createElement('button');
+ b.className = 'dupre-key';
+ b.textContent = pgm.glyph || pgm.label;
+ if (pgm.glyph) b.style.fontSize = '15px';
+ b.style.cssText = 'position:absolute;top:8px;left:' + (cx - headBw / 2) + 'px;width:' + headBw +
+ 'px;height:24px;padding:0;text-align:center;font-size:7.5px;letter-spacing:.08em;box-sizing:border-box';
+ b.addEventListener('click', () => { b.blur(); onActivate(j, pgm.id); });
+ wrap.appendChild(b);
+ return b;
+ });
+ /* row labels right-align to the first head's LEFT edge; engraved-and-
+ infilled at faceplate size */
+ const labelX = colX[0] - Math.min(colW[0] - 8, 82) / 2 - 4;
+ ROWS.forEach((r, i) => {
+ const y = Y0 + i * DY + 2;
+ if (opts.engraved) {
+ svgEl(s, 'text', { x: labelX, y: y - 1, 'text-anchor': 'end', 'font-size': 7.6,
+ 'letter-spacing': '.1em', 'font-weight': 600, 'font-family': 'var(--mono)', fill: 'rgba(0,0,0,.8)' }).textContent = r.label;
+ svgEl(s, 'text', { x: labelX, y, 'text-anchor': 'end', 'font-size': 7.6,
+ 'letter-spacing': '.1em', 'font-weight': 600, 'font-family': 'var(--mono)', fill: '#a59d8c' }).textContent = r.label;
+ } else {
+ svgEl(s, 'text', { x: labelX, y, 'text-anchor': 'end', 'font-size': 6,
+ 'letter-spacing': '.05em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = r.label;
+ }
+ });
+ if (opts.groups) {
+ let acc = 0;
+ opts.groups.slice(0, -1).forEach(n => {
+ acc += n;
+ const y = Y0 + (acc - 0.5) * DY;
+ svgEl(s, 'line', { x1: 10, y1: y, x2: W - 10, y2: y, stroke: '#2c2820', 'stroke-width': 1 });
+ });
+ }
+ const caps = {}, letterEls = {};
+ const paint = () => {
+ heads.forEach((b, j) => {
+ b.classList.remove('dupre-on', 'dupre-green', 'dupre-red');
+ if (j === active) b.classList.add('dupre-green');
+ });
+ ROWS.forEach(r => PROGS.forEach((pgm, j) => {
+ const key = r.label + '>' + pgm.id;
+ if (r.values) { /* wheels carry no live highlight — paper doesn't glow */ }
+ else if (caps[key]) {
+ const seated = pins.has(key);
+ if (j === active) {
+ caps[key].setAttribute('fill', seated ? 'url(#dkaJewelG)' : 'url(#dkaJewelR)');
+ caps[key].setAttribute('opacity', 1);
+ caps[key].setAttribute('r', seated ? 5.6 : 4.8);
+ } else {
+ caps[key].setAttribute('fill', 'url(#dkaJewelW)');
+ caps[key].setAttribute('opacity', seated ? 1 : 0);
+ caps[key].setAttribute('r', 5.6);
+ }
+ }
+ }));
+ };
+ ROWS.forEach((r, i) => {
+ const cy = Y0 + i * DY;
+ PROGS.forEach((pgm, j) => {
+ const cx = colX[j], key = r.label + '>' + pgm.id;
+ if (r.values) {
+ svgEl(s, 'rect', { x: cx - 13, y: cy - 10.5, width: 26, height: 21, rx: 3,
+ fill: '#0a0908', stroke: '#3a352c', 'stroke-width': 1.2 });
+ svgEl(s, 'rect', { x: cx - 10, y: cy - 7.5, width: 20, height: 15, fill: 'url(#dkaPaper2)' });
+ const t = svgEl(s, 'text', { x: cx, y: cy + 3.8, 'text-anchor': 'middle', 'font-size': 11,
+ 'font-weight': 700, 'font-family': 'var(--mono)', fill: '#2b2418' });
+ t.textContent = letters[key] !== undefined ? letters[key] : r.values[0];
+ svgEl(s, 'rect', { x: cx - 10, y: cy - 7.5, width: 20, height: 15, fill: 'url(#dkaCurve2)', 'pointer-events': 'none' });
+ letterEls[key] = { t };
+ const hit = svgEl(s, 'rect', { x: cx - 12, y: cy - 10, width: 24, height: 20, fill: 'transparent' });
+ hit.style.cursor = 'pointer';
+ hit.addEventListener('click', () => {
+ const cur = r.values.indexOf(t.textContent);
+ const nv = r.values[(cur + 1) % r.values.length];
+ t.textContent = nv; letters[key] = nv;
+ onLetter(r.label, pgm.id, nv, j === active);
+ });
+ } else {
+ svgEl(s, 'circle', { cx, cy, r: 3.4, fill: '#0a0908', stroke: '#2c2820', 'stroke-width': 1 });
+ const cap = svgEl(s, 'circle', { cx, cy, r: 5.6, fill: 'url(#dkaJewelW)', stroke: 'rgba(0,0,0,.45)', 'stroke-width': 1,
+ opacity: pins.has(key) ? 1 : 0, filter: 'drop-shadow(0 1px 2px rgba(0,0,0,.6))' });
+ caps[key] = cap;
+ const hit = svgEl(s, 'circle', { cx, cy, r: Math.min(9, DX / 2 - 1), fill: 'transparent' });
+ hit.style.cursor = 'pointer';
+ hit.addEventListener('click', () => {
+ const group = radios.find(g => g.includes(r.label));
+ const seated = !pins.has(key);
+ if (group) {
+ if (!seated) return; /* a radio pin moves, never pulls */
+ group.forEach(gr => pins.delete(gr + '>' + pgm.id));
+ pins.add(key);
+ } else {
+ if (seated) pins.add(key); else pins.delete(key);
+ }
+ paint();
+ onPin(r.label, pgm.id, seated, j === active);
+ });
+ }
+ });
+ });
+ paint();
+ return {
+ el: wrap,
+ get: () => [...pins],
+ set: (r, c, on) => { const k = r + '>' + c; if (!caps[k]) return; if (on) pins.add(k); else pins.delete(k); paint(); },
+ setLetter: (r, c, v) => { const k = r + '>' + c; if (letterEls[k]) { letterEls[k].t.textContent = v; letters[k] = v; } },
+ setActive: i => { active = Math.max(-1, Math.min(PROGS.length - 1, i)); paint(); },
+ getActive: () => active,
+ };
+ };
+
+ /* NEW — idle tripper rail. The mechanical time-switch tripper translated
+ from clock time to idle time: a horizontal engraved scale of idle
+ minutes with clamp tabs along it, each engraved with the action it
+ throws when idleness reaches its mark. Nothing moves by itself — the
+ rail is a standing policy, readable left to right. Tabs keep their
+ stage order (they clamp between neighbors), and the siding past the
+ scale end parks a tab OFF (stage disabled). setBypassed(true) dims the
+ whole policy — the caffeine-engaged look.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: stages ([{ id, label, minutes }] in pipeline order; minutes
+ null = parked/disabled); max (scale end, default 60);
+ width (px, default 460); onChange(id, minutes|null, stages
+ snapshot) on every drag commit, including the initial paint
+ (id null on the initial paint).
+ handle: el, get() (stages snapshot), set(id, minutes|null),
+ setBypassed(bool).
+ SVG-built: styling is inline attributes plus the shared .rsvg stage
+ block of DUPRE_CSS; sqrt scale spreads the crowded early minutes. */
+ DUPRE.tripRail = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const MAX = opts.max || 60;
+ const W = opts.width || 460;
+ const H = 86;
+ const stages = (opts.stages || []).map(st => ({ id: st.id, label: st.label, minutes: st.minutes }));
+ const x0 = 16, x1 = W - 64, railY = 44;
+ const sideX = x1 + 14;
+ const xOf = m => x0 + Math.sqrt(Math.max(0, m) / MAX) * (x1 - x0);
+ const mOf = x => Math.pow(Math.max(0, x - x0) / (x1 - x0), 2) * MAX;
+ grad('dkaTabM', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#f0efec'], ['.45', '#c4c1b9'], ['1', '#75726a']]);
+ const s = stageSvg(host, 'rsvg', W, H);
+ const all = svgEl(s, 'g', {});
+ /* rail groove + sqrt tick scale */
+ svgEl(all, 'rect', { x: x0 - 8, y: railY - 4, width: x1 - x0 + 16, height: 8, rx: 3, fill: '#0d0a08', stroke: '#050404' });
+ [0, 1, 2, 5, 10, 15, 20, 30, 45, 60].filter(m => m <= MAX).forEach(m => {
+ const x = xOf(m);
+ svgEl(all, 'line', { x1: x, y1: railY + 6, x2: x, y2: railY + 10, stroke: 'var(--steel)', 'stroke-width': .9, opacity: .8 });
+ svgEl(all, 'text', { x, y: railY + 18, 'text-anchor': 'middle', 'font-size': 5.5, 'font-family': 'var(--mono)', fill: 'var(--steel)', opacity: .8 }).textContent = m;
+ });
+ svgEl(all, 'text', { x: (x0 + x1) / 2, y: railY + 28, 'text-anchor': 'middle', 'font-size': 5.5,
+ 'letter-spacing': '.18em', 'font-family': 'var(--mono)', fill: 'var(--dim)' }).textContent = 'MINUTES IDLE';
+ /* parking siding */
+ svgEl(all, 'rect', { x: sideX - 4, y: railY - 7, width: 46, height: 14, rx: 4, fill: 'none', stroke: '#2c2820', 'stroke-width': 1, 'stroke-dasharray': '3 2' });
+ svgEl(all, 'text', { x: sideX + 19, y: railY + 20, 'text-anchor': 'middle', 'font-size': 5.5,
+ 'letter-spacing': '.14em', 'font-family': 'var(--mono)', fill: 'var(--dim)' }).textContent = 'OFF';
+ const tabs = stages.map((st) => {
+ const g = svgEl(s, 'g', {});
+ const line = svgEl(g, 'line', { x1: 0, y1: 0, x2: 0, y2: 0, stroke: '#3a352c', 'stroke-width': 1 });
+ const lbl = svgEl(g, 'text', { x: 0, y: 0, 'text-anchor': 'middle', 'font-size': 6,
+ 'letter-spacing': '.08em', 'font-family': 'var(--mono)', fill: '#a59d8c' });
+ lbl.textContent = st.label;
+ const min = svgEl(g, 'text', { x: 0, y: 0, 'text-anchor': 'middle', 'font-size': 5.5,
+ 'font-family': 'var(--mono)', fill: 'var(--gold)' });
+ svgEl(g, 'rect', { x: -5, y: railY - 8, width: 10, height: 16, rx: 2, fill: 'url(#dkaTabM)', stroke: '#5e5a52', 'stroke-width': .7 });
+ svgEl(g, 'rect', { x: -1, y: railY - 5, width: 2, height: 10, fill: 'rgba(0,0,0,.3)' });
+ g.style.cursor = 'ew-resize';
+ return { g, line, lbl, min, st };
+ });
+ const paint = () => {
+ /* flag sides alternate among ACTIVE tabs so neighbors never share a
+ line even when parking breaks the built order */
+ let rank = 0;
+ tabs.forEach(t => {
+ const parked = t.st.minutes === null || t.st.minutes === undefined;
+ const up = parked ? true : (rank++ % 2 === 0);
+ const flagY = up ? 14 : railY + 34;
+ t.line.setAttribute('y1', up ? flagY + 3 : railY + 6);
+ t.line.setAttribute('y2', up ? railY - 6 : flagY - 8);
+ t.lbl.setAttribute('y', flagY);
+ t.min.setAttribute('y', up ? flagY + 8 : flagY - 12);
+ const x = parked ? sideX + 6 + tabs.filter(o => (o.st.minutes == null) && stages.indexOf(o.st) < stages.indexOf(t.st)).length * 12 : xOf(t.st.minutes);
+ t.g.setAttribute('transform', 'translate(' + x + ',0)');
+ t.min.textContent = parked ? 'off' : Math.round(t.st.minutes) + 'm';
+ t.g.setAttribute('opacity', parked ? .55 : 1);
+ });
+ };
+ /* drag: clamp between neighboring active stages; past the scale end parks */
+ tabs.forEach((t, i) => {
+ t.g.addEventListener('pointerdown', e => {
+ t.g.setPointerCapture(e.pointerId);
+ const move = m => {
+ const r = s.getBoundingClientRect();
+ const px = (m.clientX - r.left) * (W / r.width);
+ if (px > x1 + 8) { t.st.minutes = null; paint(); return; }
+ let mm = Math.round(mOf(Math.min(px, x1)) * 2) / 2;
+ const prev = tabs.slice(0, i).reverse().find(o => o.st.minutes != null);
+ const next = tabs.slice(i + 1).find(o => o.st.minutes != null);
+ if (prev) mm = Math.max(mm, prev.st.minutes + 0.5);
+ if (next) mm = Math.min(mm, next.st.minutes - 0.5);
+ t.st.minutes = Math.max(0.5, Math.min(MAX, mm));
+ /* exact-number counter: the dial's center reads the dragged value */
+ centerTxt.textContent = (t.st.minutes % 1 ? t.st.minutes.toFixed(1) : Math.round(t.st.minutes)) + 'm';
+ paint();
+ };
+ const up = () => {
+ t.g.removeEventListener('pointermove', move);
+ t.g.removeEventListener('pointerup', up);
+ centerTxt.textContent = 'IDLE';
+ onChange(t.st.id, t.st.minutes, stages.map(o => ({ ...o })));
+ };
+ t.g.addEventListener('pointermove', move);
+ t.g.addEventListener('pointerup', up);
+ e.preventDefault();
+ });
+ });
+ const stamp = svgEl(s, 'text', { x: (x0 + x1) / 2, y: 12, 'text-anchor': 'middle', 'font-size': 6.5,
+ 'letter-spacing': '.22em', 'font-family': 'var(--mono)', fill: 'var(--gold)', opacity: 0 });
+ stamp.textContent = 'BYPASSED · CAFFEINE';
+ let bypassed = false;
+ paint();
+ onChange(null, null, stages.map(o => ({ ...o })));
+ return {
+ el: s,
+ get: () => stages.map(o => ({ ...o })),
+ set: (id, m) => { const t = stages.find(o => o.id === id); if (t) { t.minutes = m; paint(); } },
+ setBypassed: b => {
+ bypassed = !!b;
+ all.setAttribute('opacity', bypassed ? .3 : 1);
+ tabs.forEach(t => t.g.setAttribute('opacity', bypassed ? .3 : (t.st.minutes == null ? .55 : 1)));
+ stamp.setAttribute('opacity', bypassed ? 1 : 0);
+ },
+ };
+ };
+
+ /* NEW — idle tripper dial. The mechanical segment-timer dial (Intermatic
+ idiom) translated from clock time to idle time: a circular sqrt-spaced
+ minutes scale sweeping 300° with COLORED tripper tabs clamped on the
+ rim — drag a tab around the rim to retune when its stage throws; drag
+ it into the bottom OFF notch to disable the stage. A side legend maps
+ color → stage → minutes. Nothing self-moves: the dial is a standing
+ policy. setBypassed(true) dims the face under a BYPASSED · CAFFEINE
+ stamp.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: stages ([{ id, label, color, minutes }] in pipeline order;
+ minutes null = parked in the OFF notch); max (scale end,
+ default 60); size (dial diameter px, default 132); legend
+ (true draws the color/label/minutes legend — legendPos
+ 'right' default, or 'bottom' beneath the dial);
+ onChange(id, minutes|null, stages snapshot) on every drag
+ commit, plus once at build (id null).
+ handle: el, get() (snapshot), set(id, minutes|null),
+ setBypassed(bool).
+ SVG-built: styling is inline attributes plus the shared .rsvg stage
+ block of DUPRE_CSS. */
+ DUPRE.tripDial = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const MAX = opts.max || 60;
+ const SIZE = opts.size || 132;
+ const stages = (opts.stages || []).map(st => ({ ...st }));
+ const LEG_BOTTOM = opts.legend && opts.legendPos === 'bottom';
+ const LEG_W = opts.legend && !LEG_BOTTOM ? 96 : 0;
+ const W = SIZE + LEG_W + 8;
+ const H = SIZE + 8 + (LEG_BOTTOM ? stages.length * 13 + 8 : 0);
+ const cx = SIZE / 2 + 4, cy = SIZE / 2 + 4;
+ const R = SIZE / 2 - 4; /* bezel radius */
+ const TR = R - 7; /* tab track radius */
+ const START = -150, SWEEP = 300; /* bottom 60° = the OFF notch */
+ const aOf = m => START + Math.sqrt(Math.max(0, m) / MAX) * SWEEP;
+ const mOf = a => Math.pow((a - START) / SWEEP, 2) * MAX;
+ const rad = a => (a - 90) * Math.PI / 180;
+ const at = (r, a) => [cx + r * Math.cos(rad(a)), cy + r * Math.sin(rad(a))];
+ const s = stageSvg(host, 'rsvg', W, H);
+ const all = svgEl(s, 'g', {});
+ svgEl(all, 'circle', { cx, cy, r: R, fill: '#17140f', stroke: '#060505', 'stroke-width': 2 });
+ svgEl(all, 'circle', { cx, cy, r: R - 14, fill: '#100e0a', stroke: '#2a231c', 'stroke-width': 1 });
+ /* sqrt scale ticks + numerals */
+ [0, 1, 2, 5, 10, 15, 20, 30, 45, 60].filter(m => m <= MAX).forEach(m => {
+ const a = aOf(m);
+ const [x1, y1] = at(R - 2, a), [x2, y2] = at(R - 8, a);
+ svgEl(all, 'line', { x1, y1, x2, y2, stroke: 'var(--cream)', 'stroke-width': .9, opacity: .75 });
+ const [tx, ty] = at(R - 20, a);
+ svgEl(all, 'text', { x: tx, y: ty + 2, 'text-anchor': 'middle', 'font-size': 6.2,
+ 'font-family': 'var(--mono)', fill: 'var(--cream)', opacity: .85 }).textContent = m;
+ });
+ /* OFF notch at the bottom gap */
+ const [nx, ny] = at(R - 20, 180);
+ svgEl(all, 'text', { x: nx, y: ny + 2, 'text-anchor': 'middle', 'font-size': 5.5,
+ 'letter-spacing': '.12em', 'font-family': 'var(--mono)', fill: 'var(--dim)' }).textContent = 'OFF';
+ const centerTxt = svgEl(all, 'text', { x: cx, y: cy + 3, 'text-anchor': 'middle', 'font-size': 9,
+ 'letter-spacing': '.2em', 'font-family': 'var(--mono)', fill: 'var(--cream)', opacity: .85 });
+ centerTxt.textContent = 'IDLE';
+ /* legend */
+ const legendEls = {};
+ if (opts.legend) {
+ stages.forEach((st, i) => {
+ const lx = LEG_BOTTOM ? 14 : SIZE + 12;
+ const ly = LEG_BOTTOM ? SIZE + 12 + i * 13 : 18 + i * 18;
+ const lw = LEG_BOTTOM ? W - 14 : W;
+ svgEl(s, 'rect', { x: lx, y: ly - 6, width: 8, height: 8, rx: 2, fill: st.color, stroke: 'rgba(0,0,0,.5)' });
+ svgEl(s, 'text', { x: lx + 13, y: ly + 1, 'font-size': 6, 'letter-spacing': '.06em',
+ 'font-family': 'var(--mono)', fill: '#a59d8c' }).textContent = st.label;
+ legendEls[st.id] = svgEl(s, 'text', { x: lw - 4, y: ly + 1, 'text-anchor': 'end', 'font-size': 6,
+ 'font-family': 'var(--mono)', fill: 'var(--gold)' });
+ });
+ }
+ /* tabs: colored clamps riding the rim */
+ const tabs = stages.map(st => {
+ const g = svgEl(s, 'g', {});
+ svgEl(g, 'rect', { x: -4.5, y: -TR - 8, width: 9, height: 15, rx: 2, fill: st.color, stroke: 'rgba(0,0,0,.55)', 'stroke-width': 1 });
+ svgEl(g, 'rect', { x: -1, y: -TR - 5, width: 2, height: 9, fill: 'rgba(0,0,0,.3)' });
+ g.style.cursor = 'grab';
+ return { g, st };
+ });
+ const paint = () => {
+ const parkedIds = stages.filter(o => o.minutes == null).map(o => o.id);
+ tabs.forEach(t => {
+ const parked = t.st.minutes == null;
+ const a = parked ? 172 + parkedIds.indexOf(t.st.id) * 9 : aOf(t.st.minutes);
+ t.g.setAttribute('transform', 'translate(' + cx + ',' + cy + ') rotate(' + a + ')');
+ t.g.setAttribute('opacity', parked ? .55 : 1);
+ if (legendEls[t.st.id]) legendEls[t.st.id].textContent = parked ? 'off'
+ : (t.st.minutes % 1 ? t.st.minutes.toFixed(1) : Math.round(t.st.minutes)) + 'm';
+ });
+ };
+ tabs.forEach((t, i) => {
+ t.g.addEventListener('pointerdown', e => {
+ t.g.setPointerCapture(e.pointerId);
+ const move = m => {
+ const r = s.getBoundingClientRect();
+ const px = (m.clientX - r.left) * (W / r.width) - cx;
+ const py = (m.clientY - r.top) * (H / r.height) - cy;
+ let a = Math.atan2(px, -py) * 180 / Math.PI; /* 0 up, cw */
+ if (a > 150 || a < -150) { t.st.minutes = null; paint(); return; }
+ let mm = Math.round(mOf(Math.max(START, Math.min(START + SWEEP, a))) * 2) / 2;
+ const prev = tabs.slice(0, i).reverse().find(o => o.st.minutes != null);
+ const next = tabs.slice(i + 1).find(o => o.st.minutes != null);
+ if (prev) mm = Math.max(mm, prev.st.minutes + 0.5);
+ if (next) mm = Math.min(mm, next.st.minutes - 0.5);
+ t.st.minutes = Math.max(0.5, Math.min(MAX, mm));
+ /* exact-number counter: the dial's center reads the dragged value */
+ centerTxt.textContent = (t.st.minutes % 1 ? t.st.minutes.toFixed(1) : Math.round(t.st.minutes)) + 'm';
+ paint();
+ };
+ const up = () => {
+ t.g.removeEventListener('pointermove', move);
+ t.g.removeEventListener('pointerup', up);
+ centerTxt.textContent = 'IDLE';
+ onChange(t.st.id, t.st.minutes, stages.map(o => ({ ...o })));
+ };
+ t.g.addEventListener('pointermove', move);
+ t.g.addEventListener('pointerup', up);
+ e.preventDefault();
+ });
+ });
+ const stamp = svgEl(s, 'text', { x: cx, y: cy - 14, 'text-anchor': 'middle', 'font-size': 5.5,
+ 'letter-spacing': '.14em', 'font-family': 'var(--mono)', fill: 'var(--gold)', opacity: 0 });
+ stamp.textContent = 'BYPASSED';
+ paint();
+ onChange(null, null, stages.map(o => ({ ...o })));
+ return {
+ el: s,
+ get: () => stages.map(o => ({ ...o })),
+ set: (id, m) => { const t = stages.find(o => o.id === id); if (t) { t.minutes = m; paint(); } },
+ setBypassed: b => {
+ all.setAttribute('opacity', b ? .3 : 1);
+ tabs.forEach(t => t.g.setAttribute('opacity', b ? .3 : (t.st.minutes == null ? .55 : 1)));
+ stamp.setAttribute('opacity', b ? 1 : 0);
+ },
+ };
+ };
+})();