aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--assets/2026-07-03-instrument-console-panels-build-summary.org4
-rw-r--r--docs/prototypes/2026-07-03-instrument-console-panels-prototype.html (renamed from assets/2026-07-03-instrument-console-panels-prototype.html)0
-rw-r--r--docs/prototypes/2026-07-03-net-panel-rescan-prototype.html251
-rw-r--r--docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html338
-rw-r--r--docs/prototypes/2026-07-03-sound-panel-prototype.html417
-rw-r--r--docs/prototypes/2026-07-03-waybar-redesign-prototype.html321
-rw-r--r--docs/prototypes/README.org21
-rw-r--r--docs/specs/2026-07-03-audio-panel-spec.org4
-rw-r--r--docs/specs/2026-07-03-instrument-console-panels-spec.org12
-rw-r--r--todo.org4
10 files changed, 1365 insertions, 7 deletions
diff --git a/assets/2026-07-03-instrument-console-panels-build-summary.org b/assets/2026-07-03-instrument-console-panels-build-summary.org
index a7a3768..0602570 100644
--- a/assets/2026-07-03-instrument-console-panels-build-summary.org
+++ b/assets/2026-07-03-instrument-console-panels-build-summary.org
@@ -4,8 +4,8 @@
Findings summary for the no-approvals speedrun that rebuilt the net and
bluetooth waybar panels as single-screen instrument consoles. Spec:
-[[file:../docs/design/2026-07-03-instrument-console-panels-spec.org][2026-07-03-instrument-console-panels-spec.org]] (ID e73877f5, IMPLEMENTED).
-Normative design: [[file:2026-07-03-instrument-console-panels-prototype.html][2026-07-03-instrument-console-panels-prototype.html]]
+[[file:../docs/specs/2026-07-03-instrument-console-panels-spec.org][2026-07-03-instrument-console-panels-spec.org]] (ID e73877f5, IMPLEMENTED).
+Normative design: [[file:../docs/prototypes/2026-07-03-instrument-console-panels-prototype.html][docs/prototypes/2026-07-03-instrument-console-panels-prototype.html]]
(Craig approved through five prototype iterations).
* What shipped
diff --git a/assets/2026-07-03-instrument-console-panels-prototype.html b/docs/prototypes/2026-07-03-instrument-console-panels-prototype.html
index 0258f20..0258f20 100644
--- a/assets/2026-07-03-instrument-console-panels-prototype.html
+++ b/docs/prototypes/2026-07-03-instrument-console-panels-prototype.html
diff --git a/docs/prototypes/2026-07-03-net-panel-rescan-prototype.html b/docs/prototypes/2026-07-03-net-panel-rescan-prototype.html
new file mode 100644
index 0000000..3329cdb
--- /dev/null
+++ b/docs/prototypes/2026-07-03-net-panel-rescan-prototype.html
@@ -0,0 +1,251 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Net panel — rescan affordance</title>
+<style>
+:root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5;
+ --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d;
+ --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground)}
+body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 4rem;line-height:1.45;
+ background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)}
+.masthead{max-width:1200px;margin:0 auto 1.8rem}
+.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase}
+h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem}
+.masthead p{color:var(--dim);font-size:.86rem;max-width:82ch}
+.masthead p b{color:var(--silver)}
+
+.stage{display:flex;gap:2.2rem;flex-wrap:wrap;max-width:1200px;margin:0 auto;align-items:flex-start}
+.slot{width:400px}
+.slot-label{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:0 0 .55rem .2rem}
+.panel{background:var(--panel);border:2px solid var(--gold);border-radius:16px;padding:17px 19px;
+ box-shadow:0 18px 50px rgba(0,0,0,.55);font-size:13.5px;width:380px}
+
+.lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);flex:0 0 auto;box-shadow:0 0 6px 1px rgba(116,147,47,.55)}
+.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.55)}
+.lamp.off{background:var(--wash);box-shadow:none}
+.lamp.busy{background:var(--gold);animation:pulse .7s ease-in-out infinite}
+@keyframes pulse{50%{opacity:.25}}
+
+.b-face{background:var(--raise);border-radius:12px;border:1px solid #262320;padding:11px 14px}
+.b-id{display:flex;align-items:center;gap:9px}
+.b-id .state-word{color:var(--gold);font-weight:700;font-size:15px;letter-spacing:.12em}
+.b-id .unit{color:var(--steel);font-size:.68rem;letter-spacing:.3em;margin-left:auto}
+
+.chan{margin-top:12px}
+.chan .line1{display:flex;align-items:baseline;gap:9px}
+.chan .ssid{color:var(--cream);font-weight:700;font-size:14.5px}
+.chan .line2{color:var(--dim);font-size:11.5px;margin-top:2px}
+.ladder{display:inline-flex;gap:2px;align-items:flex-end;height:12px}
+.ladder i{width:4px;background:var(--wash);border-radius:1px}
+.ladder i:nth-child(1){height:4px}.ladder i:nth-child(2){height:7px}
+.ladder i:nth-child(3){height:10px}.ladder i:nth-child(4){height:12px}
+.ladder.l1 i:nth-child(-n+1){background:var(--gold)}.ladder.l2 i:nth-child(-n+2){background:var(--gold)}
+.ladder.l3 i:nth-child(-n+3){background:var(--gold)}.ladder.l4 i{background:var(--gold)}
+
+/* engrave header with the rescan action */
+.engrave{color:var(--steel);font-size:.64rem;letter-spacing:.24em;text-transform:uppercase;
+ display:flex;align-items:center;gap:8px;margin:14px 0 6px}
+.engrave::before{content:"";height:1px;background:var(--wash);width:10px;flex:0 0 auto}
+.engrave .cnt{color:var(--dim);letter-spacing:.08em;text-transform:none;cursor:pointer;
+ border-bottom:1px dotted transparent}
+.engrave .cnt:hover{color:var(--gold);border-bottom-color:var(--wash)}
+.engrave .cnt.scanning{color:var(--gold);animation:breathe 1.1s ease-in-out infinite;cursor:default;border-bottom-color:transparent}
+.engrave .spacer{flex:1;height:1px;background:var(--wash)}
+/* compact rescan glyph — sits right after the count, spins while scanning */
+.engrave .ricon{color:var(--dim);cursor:pointer;font-size:.82rem;display:inline-flex;line-height:1}
+.engrave .ricon:hover{color:var(--gold)}
+.engrave .ricon.spin{color:var(--gold);cursor:default;animation:spin .9s linear infinite}
+.engrave .act{color:var(--dim);letter-spacing:.06em;text-transform:none;font-size:.72rem;cursor:pointer}
+.engrave .act:hover{color:var(--gold)}
+@keyframes spin{to{transform:rotate(360deg)}}
+@keyframes breathe{0%,100%{opacity:1}50%{opacity:.35}}
+
+/* the list; section-breathe busy style pulses the whole well */
+#networks{border-radius:8px;transition:background .3s}
+#networks.breathe{animation:sectionbreathe 1.4s ease-in-out infinite}
+@keyframes sectionbreathe{0%,100%{background:transparent}50%{background:rgba(218,181,61,.06)}}
+
+.lamp-row{display:flex;align-items:center;gap:9px;padding:5px 6px;border-radius:7px;font-size:12.5px;cursor:pointer}
+.lamp-row:hover{background:var(--wash)}
+.lamp-row .who{color:var(--silver);white-space:nowrap}
+.lamp-row .who b{color:var(--cream)}
+.lamp-row .what{margin-left:auto;color:var(--dim);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.lamp-row.fresh{animation:fadein .5s ease}
+@keyframes fadein{from{opacity:0;transform:translateY(-3px);background:rgba(218,181,61,.12)}to{opacity:1}}
+
+.toast{margin-top:10px;font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px;
+ padding:5px 10px;opacity:0;transition:opacity .25s;min-height:1.4em}
+.toast.show{opacity:1}
+
+.aside{flex:1 1 320px;min-width:290px}
+.aside h3{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:1.1rem 0 .5rem}
+.aside h3:first-child{margin-top:.2rem}
+.aside ul{list-style:none}
+.aside li{font-size:.82rem;padding:.24rem 0 .24rem 1.1rem;position:relative}
+.aside li::before{content:"·";color:var(--gold);position:absolute;left:.25rem}
+.aside li b{color:var(--cream);font-weight:700}
+.aside li em{color:var(--dim);font-style:normal}
+.demo{border:1px dashed var(--wash);border-radius:10px;padding:.85rem 1rem;margin-top:1rem}
+.demo .lbl{color:var(--steel);font-size:.62rem;letter-spacing:.2em;text-transform:uppercase;margin-bottom:.5rem}
+.seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden;margin-bottom:.7rem}
+.seg button{flex:1;font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 6px;cursor:pointer}
+.seg button:last-child{border-right:0}
+.seg button.on{background:linear-gradient(180deg,#f0d879,var(--gold));color:var(--panel);font-weight:700}
+.demo .go{font:inherit;font-size:.8rem;color:var(--silver);background:transparent;border:1px solid var(--gold);
+ border-radius:8px;padding:.45rem 1rem;cursor:pointer}
+.demo .go:hover{background:rgba(218,181,61,.12)}
+.rec{border:1px dashed var(--wash);border-radius:10px;padding:.85rem 1rem;margin-top:.9rem;font-size:.82rem}
+.rec b{color:var(--gold)}
+@media (prefers-reduced-motion:reduce){*{animation:none!important}}
+</style>
+</head>
+<body>
+
+<header class="masthead">
+ <div class="eyebrow">archsetup · dupre panel family · net·01</div>
+ <h1>Networks — the rescan affordance</h1>
+ <p>Where does a WiFi rescan live, and how does it show it's working? The count "· N in range" is
+ really a live status field — the natural home for "scanning…". My lean: an explicit <b>⟳ rescan</b>
+ action in the engrave line (discoverable, same slot as "+ hidden"), with a <b>flash-and-fade</b>
+ busy state — the glyph spins, the count breathes, new rows fade in as found. The count is also
+ click-to-rescan as a shortcut. Use the selector on the right to feel each busy treatment.</p>
+</header>
+
+<div class="stage">
+ <div class="slot">
+ <div class="slot-label">net·01 — networks section</div>
+ <div class="panel">
+ <div class="b-face">
+ <div class="b-id">
+ <span class="lamp"></span>
+ <span class="state-word">ONLINE</span>
+ <span class="unit">NET·01</span>
+ </div>
+ </div>
+
+ <div class="engrave">channel</div>
+ <div class="chan">
+ <div class="line1"><span class="ssid">@Hyatt_WiFi</span>
+ <span class="ladder l3"><i></i><i></i><i></i><i></i></span>
+ <span class="dim" style="font-size:11px">-59 dBm · 44 ms</span></div>
+ <div class="line2">172.20.2.108/20 · gw 172.20.0.1 · route wlp170s0</div>
+ </div>
+
+ <div class="engrave">networks<span class="cnt" id="cnt" onclick="rescan()">· 5 in range</span>
+ <span class="ricon" id="rescan" onclick="rescan()" title="Rescan for networks">⟳</span>
+ <span class="spacer"></span>
+ <span class="act" onclick="toast('would open the hidden-SSID dialog')">+ hidden</span>
+ </div>
+ <div id="networks"></div>
+
+ <div class="toast" id="toast"></div>
+ </div>
+ </div>
+
+ <div class="aside">
+ <div class="demo">
+ <div class="lbl">busy feedback style</div>
+ <div class="seg" id="styleSeg">
+ <button class="on" data-s="all">all (rec)</button>
+ <button data-s="spin">spinner</button>
+ <button data-s="count">count</button>
+ <button data-s="section">section</button>
+ </div>
+ <button class="go" onclick="rescan()">▶ run a rescan</button>
+ </div>
+
+ <h3>The three busy signals</h3>
+ <ul>
+ <li><b>Spinner</b> — the ⟳ glyph rotates while scanning. The clearest "working" cue; universal.</li>
+ <li><b>Count breathe</b> — "· 5 in range" becomes "scanning…" and slow-pulses. Your idea: the status field animates in place, no extra chrome.</li>
+ <li><b>Section breathe</b> — the whole list gives a faint gold breath while the scan runs; found rows fade in gold. Ambient, ties the animation to what's changing.</li>
+ <li><b>All (recommended)</b> — spinner + count + fade-in together. The section breathe is optional; it can read as busy on a small panel, so it's off in "all" by default and its own option to try.</li>
+ </ul>
+
+ <h3>Why an explicit action, not only the count</h3>
+ <div class="rec">
+ Overloading the count as the sole trigger is elegant but a first look doesn't know it's clickable.
+ An explicit <b>⟳ rescan</b> in the engrave line is discoverable, sits in the same slot as "+ hidden"
+ (consistent), and doesn't cost a heavy console key. Keeping the count clickable too gives power users
+ the shortcut without hiding the affordance. A dedicated <b>RESCAN console key</b> (next to DOCTOR /
+ SPEED TEST) is the third option — heavier, and rescan is a list action, not a diagnostic, so it fits
+ the engrave line better.
+ </div>
+ </div>
+</div>
+
+<script>
+const $=id=>document.getElementById(id);
+let busy=false, style='all';
+let NETS=[
+ {ssid:'@Hyatt_WiFi', sig:3, sec:'WPA2', stored:true, active:true},
+ {ssid:'Hyatt_Meeting', sig:3, sec:'WPA2', stored:false, active:false},
+ {ssid:'DIRECT-roku-882',sig:2, sec:'WPA2', stored:false, active:false},
+ {ssid:'xfinitywifi', sig:1, sec:null, stored:false, active:false},
+ {ssid:'HomeNet', sig:0, sec:'WPA2', stored:true, active:false, oor:true},
+];
+const NEWFOUND=[
+ {ssid:'Hyatt_Guest', sig:2, sec:null, stored:false, active:false},
+ {ssid:'Marriott_CONF', sig:1, sec:'WPA2', stored:false, active:false},
+];
+const pctFor=[null,'22%','44%','61%','78%'];
+
+$('styleSeg').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;
+ style=b.dataset.s;[...$('styleSeg').children].forEach(x=>x.classList.toggle('on',x===b));});
+
+function rowEl(n,fresh){
+ const r=document.createElement('div'); r.className='lamp-row'+(fresh?' fresh':'');
+ const lamp=n.active?'lamp':(n.oor?'lamp off':'lamp gold');
+ const what=n.active?'active · '+(n.sec||'open')
+ :n.oor?'stored · out of range'
+ :(n.stored?'stored · ':'')+(n.sec||'open')+' · '+pctFor[n.sig];
+ r.innerHTML=`<span class="${lamp}"></span><span class="who">${n.active?'<b>'+n.ssid+'</b>':n.ssid}</span>`+
+ (!n.active&&!n.oor?`<span class="ladder l${n.sig}" style="margin-left:6px"><i></i><i></i><i></i><i></i></span>`:'')+
+ `<span class="what">${what}</span>`;
+ r.onclick=()=>{ if(busy)return; toast(n.active?'already on '+n.ssid:'would join '+n.ssid); };
+ return r;
+}
+function render(freshSet){
+ const host=$('networks'); host.innerHTML='';
+ const inRange=NETS.filter(n=>!n.oor).sort((a,b)=>(b.active-a.active)||(b.sig-a.sig));
+ const oor=NETS.filter(n=>n.oor);
+ [...inRange,...oor].forEach(n=>host.appendChild(rowEl(n,freshSet&&freshSet.has(n.ssid))));
+ if(!busy) $('cnt').textContent='· '+inRange.length+' in range';
+}
+function rescan(){
+ if(busy) return; busy=true;
+ const useSpin = style==='all'||style==='spin';
+ const useCount= style==='all'||style==='count';
+ const useSec = style==='section';
+ if(useSpin) $('rescan').classList.add('spin');
+ if(useCount){ $('cnt').classList.add('scanning'); $('cnt').textContent='scanning…'; }
+ if(useSec) $('networks').classList.add('breathe');
+ toast('scanning for networks…');
+ // networks trickle in as "found"
+ const fresh=new Set();
+ setTimeout(()=>{ NETS.splice(1,0,NEWFOUND[0]); fresh.add(NEWFOUND[0].ssid); render(fresh); },900);
+ setTimeout(()=>{ NETS.splice(3,0,NEWFOUND[1]); fresh.add(NEWFOUND[1].ssid); render(fresh); },1700);
+ setTimeout(()=>{
+ busy=false;
+ $('rescan').classList.remove('spin');
+ $('cnt').classList.remove('scanning');
+ $('networks').classList.remove('breathe');
+ const n=NETS.filter(x=>!x.oor).length;
+ $('cnt').textContent='· '+n+' in range';
+ toast('scan complete — '+n+' networks in range');
+ // reset for the next run so the demo is repeatable
+ NETS=NETS.filter(x=>x.ssid!=='Hyatt_Guest'&&x.ssid!=='Marriott_CONF');
+ },2600);
+}
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html b/docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html
new file mode 100644
index 0000000..8e642f4
--- /dev/null
+++ b/docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html
@@ -0,0 +1,338 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Panel widget gallery — dupre instrument console</title>
+<style>
+:root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5;
+ --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d;
+ --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground)}
+body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 5rem;line-height:1.45;
+ background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)}
+.wrap{max-width:1320px;margin:0 auto}
+.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase}
+h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem}
+.masthead p{color:var(--dim);font-size:.86rem;max-width:86ch}
+.masthead p b{color:var(--silver)}
+h2{color:var(--steel);font-size:.74rem;letter-spacing:.24em;text-transform:uppercase;
+ margin:2.2rem 0 .2rem;display:flex;align-items:center;gap:12px}
+h2::after{content:"";height:1px;background:var(--wash);flex:1}
+
+.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(232px,1fr));gap:14px;margin-top:1rem}
+.card{background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320;border-radius:12px;
+ padding:13px 14px 12px;display:flex;flex-direction:column;gap:9px;
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 6px 14px rgba(0,0,0,.4)}
+.wname{color:var(--cream);font-size:.82rem;font-weight:700;display:flex;align-items:center;gap:8px}
+.wname .no{color:var(--panel);background:var(--gold);border-radius:4px;font-size:.6rem;padding:0 5px;font-weight:400}
+.stagew{background:var(--well);border:1px solid #201d17;border-radius:9px;padding:14px 12px;
+ min-height:78px;display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap}
+.wnote{color:var(--dim);font-size:.72rem;line-height:1.4}
+.wnote b{color:var(--steel);font-weight:400}
+
+/* ---- shared primitives ---- */
+.lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);box-shadow:0 0 6px 1px rgba(116,147,47,.55)}
+.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.6)}
+.lamp.red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)}
+.lamp.off{background:var(--wash);box-shadow:none}
+.lamp.busy{background:var(--gold);animation:pulse .7s ease-in-out infinite}
+@keyframes pulse{50%{opacity:.25}}
+
+.switch{width:40px;height:21px;border-radius:11px;background:var(--wash);border:1px solid var(--slate);
+ position:relative;cursor:pointer}
+.switch::after{content:"";position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%;
+ background:var(--dim);transition:left .15s}
+.switch.on{background:var(--slate);border-color:var(--gold)}
+.switch.on::after{left:21px;background:var(--gold)}
+.switch.red{background:rgba(203,107,77,.2);border-color:var(--fail)}
+.switch.red::after{background:var(--fail);left:2px}
+
+.key{font:inherit;font-size:11.5px;letter-spacing:.06em;color:var(--silver);cursor:pointer;
+ background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-bottom-color:#0c0b0a;
+ border-radius:8px;padding:8px 12px;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)}
+.key:hover{color:var(--gold);border-color:var(--gold)}
+.key:active{transform:translateY(1px)}
+.key.on{color:var(--panel);background:linear-gradient(180deg,#f0d879,var(--gold));border-color:var(--gold-hi);font-weight:700}
+.key.red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)}
+.key.off{opacity:.4}
+
+.chip{color:var(--dim);cursor:pointer;border-bottom:1px dotted var(--wash);font-size:12px}
+.chip.on{color:var(--gold);border-color:var(--gold)}
+
+.badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px}
+.badge.red{background:var(--fail);color:var(--cream)}
+.badge.ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)}
+
+/* fader */
+.fader{width:150px;height:16px;position:relative;cursor:pointer}
+.fader .slot{position:absolute;top:6px;left:0;right:0;height:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden}
+.fader .fill{position:absolute;top:0;left:0;bottom:0;background:linear-gradient(90deg,#8a7524,var(--gold))}
+.fader .cap{position:absolute;top:1px;width:7px;height:14px;border-radius:2px;margin-left:-3.5px;
+ background:linear-gradient(180deg,#f0d879,#caa233);border:1px solid #7a6414;box-shadow:0 1px 2px rgba(0,0,0,.5)}
+/* vertical fader */
+.vfader{width:16px;height:64px;position:relative;cursor:pointer}
+.vfader .slot{position:absolute;left:6px;top:0;bottom:0;width:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden}
+.vfader .fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,#8a7524,var(--gold))}
+.vfader .cap{position:absolute;left:1px;height:7px;width:14px;border-radius:2px;margin-top:-3.5px;
+ background:linear-gradient(90deg,#caa233,#f0d879);border:1px solid #7a6414}
+
+/* rotary knob */
+.knob{width:52px;height:52px;border-radius:50%;position:relative;cursor:pointer;
+ background:radial-gradient(circle at 40% 35%,#2a2622,#141210);border:1px solid #3a352c;
+ box-shadow:inset 0 2px 3px rgba(255,255,255,.05),0 3px 6px rgba(0,0,0,.5)}
+.knob .ind{position:absolute;left:50%;top:5px;width:2px;height:16px;background:var(--gold-hi);
+ margin-left:-1px;transform-origin:50% 21px;border-radius:1px;box-shadow:0 0 5px rgba(255,215,95,.6)}
+.knob-scale{position:relative;width:64px;height:40px}
+.knob-scale .arc{position:absolute;inset:0 0 -24px 0;border:1.5px solid var(--wash);border-top-color:var(--gold);border-radius:50%;opacity:.5}
+
+/* needle gauge */
+.gauge{width:96px}
+.gauge .dial{position:relative;height:48px;overflow:hidden}
+.gauge .arc{position:absolute;inset:0 0 -48px 0;border:2px solid var(--wash);border-radius:50%}
+.gauge .tk{position:absolute;left:50%;bottom:0;width:1.5px;height:8px;background:var(--steel);transform-origin:50% 48px}
+.gauge .ndl{position:absolute;left:50%;bottom:0;width:2px;height:40px;background:var(--gold-hi);
+ transform-origin:50% 100%;transform:rotate(0deg);border-radius:2px;box-shadow:0 0 6px rgba(255,215,95,.5);
+ transition:transform .5s cubic-bezier(.3,1.3,.5,1)}
+.gauge .hub{position:absolute;left:50%;bottom:-4px;width:8px;height:8px;margin-left:-4px;border-radius:50%;background:var(--gold)}
+.gauge .gv{color:var(--cream);text-align:center;font-size:12px;font-weight:700;margin-top:5px;font-variant-numeric:tabular-nums}
+
+/* segmented VU / LED bar */
+.vu{width:170px;display:flex;flex-direction:column;gap:5px}
+.vurow{display:flex;align-items:center;gap:7px}
+.vurow .ch{color:var(--steel);font-size:.6rem;width:8px}
+.vubar{flex:1;display:flex;gap:2px;height:9px}
+.vubar i{flex:1;background:var(--wash);border-radius:1px;opacity:.3}
+.vubar i.on{opacity:1;background:var(--pass)}.vubar i.hot{opacity:1;background:var(--gold)}
+.vubar i.clip{opacity:1;background:var(--fail)}.vubar i.peak{outline:1px solid var(--gold-hi);outline-offset:-1px}
+
+/* mini 4-bar signal */
+.sig{display:flex;align-items:flex-end;gap:2px;height:18px}
+.sig i{width:4px;background:var(--wash);border-radius:1px}
+.sig i:nth-child(1){height:5px}.sig i:nth-child(2){height:9px}.sig i:nth-child(3){height:13px}.sig i:nth-child(4){height:17px}
+.sig i.on{background:var(--pass)}.sig i.hot{background:var(--gold)}.sig i.clip{background:var(--fail)}
+
+/* signal ladder (wifi bars) */
+.ladder{display:inline-flex;gap:3px;align-items:flex-end;height:18px}
+.ladder i{width:5px;background:var(--wash);border-radius:1px}
+.ladder i:nth-child(1){height:6px}.ladder i:nth-child(2){height:10px}
+.ladder i:nth-child(3){height:14px}.ladder i:nth-child(4){height:18px}
+.ladder.l3 i:nth-child(-n+3){background:var(--gold)}
+
+/* linear progress / fuel bar */
+.bar{width:160px;height:12px;background:#0d0f10;border:1px solid #231f18;border-radius:6px;overflow:hidden;position:relative}
+.bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,#8a7524,var(--gold));border-radius:6px}
+.bar.warn>span{background:linear-gradient(90deg,#a35a3f,var(--fail))}
+
+/* radial ring */
+.ring{width:60px;height:60px;border-radius:50%;
+ background:conic-gradient(var(--gold) calc(var(--p)*1%),var(--wash) 0);
+ display:grid;place-items:center;position:relative}
+.ring::before{content:"";position:absolute;inset:6px;border-radius:50%;background:var(--well)}
+.ring b{position:relative;color:var(--cream);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums}
+
+/* tabular readout */
+.readout{color:var(--cream);font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.04em}
+.readout small{color:var(--dim);font-size:12px;font-weight:400}
+.readout .u{color:var(--steel);font-size:.6rem;letter-spacing:.2em;display:block;text-align:center;margin-top:2px}
+
+/* sparkline */
+.spark{width:170px;height:44px}
+.spark svg{display:block;width:100%;height:100%}
+
+/* lamp row (list item) */
+.lrow{width:190px;display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:7px;background:#141210;cursor:pointer;font-size:12.5px}
+.lrow:hover{background:var(--wash)}
+.lrow .who{color:var(--silver)}.lrow .who b{color:var(--cream)}
+.lrow .what{margin-left:auto;color:var(--dim);font-size:11px}
+
+/* arm-to-fire */
+.arm{font:inherit;font-size:11.5px;color:var(--silver);cursor:pointer;background:#191715;border:1px solid #33302b;
+ border-radius:8px;padding:7px 12px}
+.arm.armed{background:rgba(203,107,77,.12);border-color:var(--fail);color:var(--fail)}
+
+/* stepper / segmented selector */
+.seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden}
+.seg button{font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 11px;cursor:pointer}
+.seg button:last-child{border-right:0}
+.seg button.on{background:linear-gradient(180deg,#f0d879,var(--gold));color:var(--panel);font-weight:700}
+
+/* engraved section label */
+.engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;
+ display:flex;align-items:center;gap:9px}
+.engrave::before,.engrave::after{content:"";height:1px;background:var(--wash);flex:1}
+.engrave::before{max-width:10px}
+.engrave .cnt{color:var(--dim);letter-spacing:.1em;text-transform:none}
+
+/* waveform strip */
+.wave{width:170px;height:38px}
+.wave svg{width:100%;height:100%;display:block}
+
+/* toast */
+.toastw{font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px;padding:5px 10px}
+
+/* output well (log step) */
+.owell{width:200px;background:var(--well);border:1px solid var(--wash);border-radius:8px;padding:7px 9px;font-size:11px}
+.ostep{display:flex;gap:7px;align-items:flex-start;padding:2px 0}
+.ostep .lamp{margin-top:3px;width:7px;height:7px}
+.ostep b{color:var(--cream);font-weight:700}.ostep .ev{color:var(--steel);display:block;font-size:10.5px}
+
+@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
+</style>
+</head>
+<body>
+<div class="wrap">
+<header class="masthead">
+ <div class="eyebrow">archsetup · dupre panel family</div>
+ <h1>Widget gallery — the instrument-console kit</h1>
+ <p>Every control + display idiom we can build in the dupre faceplate language, all rendering from
+ the same tokens the net / bt / sound panels use. <b>Controls</b> take input; <b>meters &amp;
+ gauges</b> show a live analog value; <b>indicators &amp; readouts</b> show state or a number.
+ Live ones animate. Pick what fits each job — most cost pure CSS; the two that need a real
+ drawing surface (needle gauge, waveform) are flagged in their notes.</p>
+</header>
+
+<h2>Controls — take input</h2>
+<div class="grid" id="controls"></div>
+
+<h2>Meters &amp; gauges — live analog value</h2>
+<div class="grid" id="meters"></div>
+
+<h2>Indicators &amp; readouts — state or number</h2>
+<div class="grid" id="indicators"></div>
+
+</div>
+<script>
+const $ = id => document.getElementById(id);
+const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
+function card(host, no, name, html, note){
+ const c=document.createElement('div'); c.className='card';
+ c.innerHTML=`<div class="wname"><span class="no">${no}</span>${name}</div>`+
+ `<div class="stagew">${html}</div><div class="wnote">${note}</div>`;
+ host.appendChild(c); return c;
+}
+function buildBars(el,n){el.innerHTML='';for(let k=0;k<n;k++)el.appendChild(document.createElement('i'));}
+
+/* ============ CONTROLS ============ */
+const C=$('controls');
+card(C,'01','Toggle switch',
+ `<span class="switch on" onclick="this.classList.toggle('on')"></span>
+ <span class="switch red"></span>`,
+ '<b>on / off / muted.</b> The faceplate master control — wifi radio, bt power, master-mute. Click to flip.');
+card(C,'02','Console key',
+ `<button class="key on">LIVE</button><button class="key">SCAN</button><button class="key red">MUTED</button>`,
+ '<b>physical push button.</b> DOCTOR / SPEED TEST / mic mode. Gold = engaged, terracotta = off.');
+card(C,'03','Horizontal fader',
+ `<div class="fader" id="f1"><div class="slot"><div class="fill" style="width:68%"></div></div><div class="cap" style="left:68%"></div></div>`,
+ '<b>continuous 0-100.</b> Per-device volume, brightness, kbd backlight. Drag; the gold cap tracks.');
+card(C,'04','Vertical fader',
+ `<div class="vfader"><div class="slot"><div class="fill" style="height:60%"></div></div><div class="cap" style="bottom:60%"></div></div>
+ <div class="vfader"><div class="slot"><div class="fill" style="height:35%"></div></div><div class="cap" style="bottom:35%"></div></div>`,
+ '<b>channel-strip style.</b> A mixer column per device if you want the classic board look.');
+card(C,'05','Rotary knob',
+ `<span class="knob" id="knob" onclick="bumpKnob()"><span class="ind" id="kind"></span></span>`,
+ '<b>dial in a value.</b> Volume/gain the analog way. Click to turn; drag in the real build. Pairs with a scale arc.');
+card(C,'06','Segmented selector',
+ `<div class="seg"><button class="on">TIMER</button><button>ALARM</button><button>POMO</button></div>`,
+ '<b>pick one of a few.</b> Timer type, layout mode, theme. One press-lit segment.');
+card(C,'07','Chip toggle',
+ `<span class="chip on" onclick="this.classList.toggle('on')">discoverable on</span>`,
+ '<b>inline binary.</b> A soft toggle inside a line of text — discoverable, auto-dim, DND. Gold when on.');
+card(C,'08','Arm-to-fire',
+ `<button class="arm" id="arm" onclick="armFire()">forget</button>`,
+ '<b>two-stage confirm.</b> Destructive/disruptive actions — forget network, disconnect. First click arms (red), second fires.');
+card(C,'09','Lamp row',
+ `<div class="lrow"><span class="lamp gold"></span><span class="who"><b>WH-1000XM4</b></span><span class="what">tap to connect</span></div>`,
+ '<b>actionable list item.</b> The net/bt/sound row: lamp + name + status, click acts. The workhorse.');
+
+/* ============ METERS & GAUGES ============ */
+const M=$('meters');
+card(M,'10','Needle gauge',
+ `<div class="gauge"><div class="dial"><div class="arc"></div>
+ <div class="tk" style="transform:rotate(-60deg)"></div><div class="tk" style="transform:rotate(0)"></div><div class="tk" style="transform:rotate(60deg)"></div>
+ <div class="ndl" id="g1"></div><div class="hub"></div></div><div class="gv"><span id="g1v">0</span>%</div></div>`,
+ '<b>analog dial.</b> Throughput, battery, volume level. <b>Needs a Cairo/GTK drawing area</b> — CSS can fake a fixed angle but not a smooth sweep in waybar.');
+card(M,'11','Stereo VU (LED bar)',
+ `<div class="vu"><div class="vurow"><span class="ch">L</span><span class="vubar" id="vuL"></span></div>
+ <div class="vurow"><span class="ch">R</span><span class="vubar" id="vuR"></span></div></div>`,
+ '<b>live signal level.</b> The sound panel\'s second meter row. Peak-hold outline. Pure CSS — pango/box segments.');
+card(M,'12','Mini signal (4-bar)',
+ `<span class="sig" id="mini"></span>`,
+ '<b>compact activity.</b> Per-row "is this device playing" indicator. Cheap enough to sit in every list row.');
+card(M,'13','Signal ladder',
+ `<span class="ladder l3"><i></i><i></i><i></i><i></i></span>`,
+ '<b>discrete strength.</b> Wifi bars, bt RSSI — a stepped 0-4. Already in the net panel.');
+card(M,'14','Linear fuel bar',
+ `<div class="bar"><span style="width:72%"></span></div><div class="bar warn"><span style="width:12%"></span></div>`,
+ '<b>a single 0-100.</b> Battery, disk, download progress. Warn tint under threshold. Trivial in CSS.');
+card(M,'15','Radial ring',
+ `<span class="ring" style="--p:68"><b>68</b></span>`,
+ '<b>percentage as a donut.</b> CPU, battery, a single meter where a needle is overkill. conic-gradient, pure CSS.');
+card(M,'16','Sparkline',
+ `<span class="spark" id="spark"><svg viewBox="0 0 170 44" preserveAspectRatio="none"><polyline id="sparkp" fill="none" stroke="var(--gold-hi)" stroke-width="1.5"/></svg></span>`,
+ '<b>recent history.</b> Throughput/CPU over the last minute. SVG here; a drawing area in GTK.');
+card(M,'17','Waveform strip',
+ `<span class="wave" id="wave"><svg viewBox="0 0 170 38" preserveAspectRatio="none"><path id="wavep" fill="none" stroke="var(--gold)" stroke-width="1.2"/></svg></span>`,
+ '<b>audio waveform / scope.</b> A richer signal view for the sound panel. <b>Needs a drawing surface.</b>');
+
+/* ============ INDICATORS & READOUTS ============ */
+const I=$('indicators');
+card(I,'18','Status lamp',
+ `<span class="lamp"></span><span class="lamp gold"></span><span class="lamp red"></span><span class="lamp off"></span><span class="lamp busy"></span>`,
+ '<b>one-glance health.</b> Green ok · gold engaged · red fail · dim off · pulsing busy. The family signature.');
+card(I,'19','Badge / tag',
+ `<span class="badge">TUNNEL</span> <span class="badge red">LOW BATT</span> <span class="badge ghost">2.4G</span>`,
+ '<b>a labelled flag.</b> On the faceplate or a row — MUTED, AIRPLANE, DEF, a band tag.');
+card(I,'20','Tabular readout',
+ `<div style="text-align:center"><div class="readout">24:10</div><span class="u">timer</span></div>
+ <div style="text-align:center"><div class="readout">68<small>%</small></div></div>`,
+ '<b>a precise number.</b> Clock, countdown, volume %. BerkeleyMono tabular-nums so digits don\'t jitter.');
+card(I,'21','Engraved label',
+ `<span class="engrave">outputs<span class="cnt">· 3</span></span>`,
+ '<b>section divider.</b> The hairline-flanked caps label with a count. Groups a panel into readable blocks.');
+card(I,'22','Output well',
+ `<div class="owell"><div class="ostep"><span class="lamp"></span><span><b>Link</b><span class="ev">wlp170s0 · @Hyatt</span></span></div>
+ <div class="ostep"><span class="lamp gold"></span><span><b>DNS</b><span class="ev">resolving…</span></span></div></div>`,
+ '<b>streaming step log.</b> The doctor/scan output — lamp-per-step with evidence. For any run-and-report action.');
+card(I,'23','Toast / status line',
+ `<span class="toastw">joined @Hyatt_WiFi — saved</span>`,
+ '<b>transient confirmation.</b> The one-line result after an action. Auto-dismiss; red variant for errors.');
+
+/* ---- live animation ---- */
+let ph=0, kang=140;
+function bumpKnob(){ kang=(kang+35)%300-0; $('kind').style.transform=`rotate(${kang-150}deg)`; }
+function armFire(){ const a=$('arm'); if(a.classList.contains('armed')){a.classList.remove('armed');a.textContent='forget';}
+ else{a.classList.add('armed');a.textContent='forget? again';} }
+buildBars($('vuL'),16); buildBars($('vuR'),16); buildBars($('mini'),0);
+$('mini').innerHTML='<i></i><i></i><i></i><i></i>';
+$('kind').style.transform=`rotate(${kang-150}deg)`;
+const hist=Array.from({length:40},()=>0.5);
+function paintVU(el,l,pk){const b=el.children,n=b.length,lit=Math.round(l*n);
+ pk.v=Math.max(lit,(pk.v||0)-0.4);const p=Math.round(pk.v);
+ for(let k=0;k<n;k++){let c=k<lit?(k>=n-2?'clip':k>=n-4?'hot':'on'):'';if(p>0&&k===p-1)c=(c?c+' ':'')+'peak';b[k].className=c;}}
+const pkL={v:0},pkR={v:0};
+function paintMini(el,l){const b=el.children,lit=Math.round(l*4);for(let k=0;k<4;k++)b[k].className=k<lit?(k>=3?'clip':k>=2?'hot':'on'):'';}
+function lvl(){return Math.max(0,Math.min(1,0.5+0.4*Math.sin(ph*1.3)+ (Math.random()<0.15?Math.random()*0.4:0) - Math.random()*0.08));}
+function tick(){
+ ph+=0.09;
+ const a=lvl(), b=lvl();
+ paintVU($('vuL'),a,pkL); paintVU($('vuR'),b,pkR); paintMini($('mini'),a);
+ // needle sweeps 0..100
+ const gv=Math.round(50+45*Math.sin(ph*0.7));
+ $('g1').style.transform=`rotate(${-60+gv/100*120}deg)`; $('g1v').textContent=gv;
+ // sparkline
+ hist.push(0.5+0.42*Math.sin(ph*0.9)+ (Math.random()-0.5)*0.25); hist.shift();
+ $('sparkp').setAttribute('points',hist.map((v,i)=>`${i/(hist.length-1)*170},${44-Math.max(0,Math.min(1,v))*40-2}`).join(' '));
+ // waveform
+ let d='M0 19'; for(let x=0;x<=170;x+=3){const y=19+Math.sin(x*0.18+ph*3)*Math.sin(x*0.05)*14; d+=` L${x} ${y.toFixed(1)}`;}
+ $('wavep').setAttribute('d',d);
+}
+if(!reduced) setInterval(tick,80); else tick();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-03-sound-panel-prototype.html b/docs/prototypes/2026-07-03-sound-panel-prototype.html
new file mode 100644
index 0000000..d75f566
--- /dev/null
+++ b/docs/prototypes/2026-07-03-sound-panel-prototype.html
@@ -0,0 +1,417 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Sound — instrument console (pulsemixer)</title>
+<style>
+:root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5;
+ --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d;
+ --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground)}
+body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 4rem;line-height:1.45;
+ background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)}
+.masthead{max-width:1280px;margin:0 auto 1.8rem}
+.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase}
+h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem}
+.masthead p{color:var(--dim);font-size:.86rem;max-width:82ch}
+.masthead p b{color:var(--silver);font-weight:700}
+
+.stage{display:flex;gap:2.2rem;flex-wrap:wrap;max-width:1280px;margin:0 auto;align-items:flex-start}
+.slot{width:400px}
+.slot-label{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:0 0 .55rem .2rem}
+
+.panel{background:var(--panel);border:2px solid var(--gold);border-radius:16px;padding:17px 19px;
+ box-shadow:0 18px 50px rgba(0,0,0,.55);font-size:13.5px;width:380px;position:relative;overflow:hidden}
+
+.lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);flex:0 0 auto;
+ box-shadow:0 0 6px 1px rgba(116,147,47,.55)}
+.lamp.red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)}
+
+.b-face{background:var(--raise);border-radius:12px;border:1px solid #262320;padding:11px 14px}
+.b-id{display:flex;align-items:center;gap:9px}
+.b-id .state-word{color:var(--gold);font-weight:700;font-size:15px;letter-spacing:.12em}
+.b-id .unit{color:var(--steel);font-size:.68rem;letter-spacing:.3em;margin-left:auto}
+.b-id .g{font-size:17px;color:var(--cream)}
+.badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);
+ border-radius:4px;padding:1px 6px;display:none}
+.badge.show{display:inline-block}
+.badge.red{background:var(--fail);color:var(--cream)}
+.x-btn{margin-left:6px;color:var(--dim);border:0;background:transparent;font:inherit;font-size:1rem;
+ cursor:pointer;border-radius:50%;width:26px;height:26px;line-height:1;flex:0 0 auto}
+.x-btn:hover{background:var(--wash);color:var(--silver)}
+/* faceplate master quick-mute — same switch idiom as net wifi / bt power */
+.switch{width:38px;height:20px;border-radius:10px;background:var(--wash);
+ border:1px solid var(--slate);position:relative;flex:0 0 auto;cursor:pointer}
+.switch::after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px;
+ border-radius:50%;background:var(--dim);transition:left .15s}
+.switch.on{background:var(--slate);border-color:var(--gold)}
+.switch.on::after{left:19px;background:var(--gold)}
+.switch.muted{background:rgba(203,107,77,.2);border-color:var(--fail)}
+.switch.muted::after{background:var(--fail);left:2px}
+
+.engrave{color:var(--steel);font-size:.64rem;letter-spacing:.32em;text-transform:uppercase;
+ display:flex;align-items:center;gap:10px;margin:13px 0 6px}
+.engrave::before,.engrave::after{content:"";height:1px;background:var(--wash);flex:1}
+.engrave::before{max-width:12px}
+.engrave .cnt{color:var(--dim);letter-spacing:.12em;margin-left:2px;text-transform:none;font-size:.62rem}
+
+/* device row — a FIXED grid so nothing can overflow the plate:
+ [signal] [name] [fader] [pct] [mute]. minmax(0,1fr) lets the name shrink
+ and ellipsis instead of forcing the row wider than the panel. */
+.dev{display:grid;grid-template-columns:15px minmax(0,1fr) 84px 32px 20px;
+ align-items:center;gap:8px;padding:6px 5px;border-radius:8px;cursor:default}
+.dev:hover{background:var(--wash)}
+.dev .who{min-width:0;display:flex;align-items:center;gap:6px;color:var(--silver)}
+.dev .who .g{font-size:14px;color:var(--dim);flex:0 0 auto}
+.dev.active .who .g{color:var(--gold)}
+.dev .who .nm{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.dev.active .who .nm{color:var(--cream);font-weight:700}
+.dev.muted .who .nm{color:var(--dim)}
+.dev .def{font-size:.5rem;letter-spacing:.14em;color:var(--panel);background:var(--gold);
+ border-radius:3px;padding:0 4px;flex:0 0 auto;display:none}
+.dev.active .def{display:inline-block}
+/* fader — machined slot + gold cap; width-based fill, bounded to its cell */
+.fader{height:16px;position:relative;cursor:pointer}
+.fader .trk{position:absolute;top:6px;left:0;right:0;height:4px;border-radius:2px;background:var(--well);
+ border:1px solid #231f18;overflow:hidden}
+.fader .fill{position:absolute;top:0;left:0;bottom:0;background:linear-gradient(90deg,#8a7524,var(--gold))}
+.fader .cap{position:absolute;top:1px;width:7px;height:14px;border-radius:2px;margin-left:-3.5px;
+ background:linear-gradient(180deg,#f0d879,#caa233);border:1px solid #7a6414;box-shadow:0 1px 2px rgba(0,0,0,.5)}
+.dev.muted .fill{background:var(--wash)}
+.dev.muted .cap{background:linear-gradient(180deg,#6a6a6a,#3f3f3f);border-color:#2a2a2a}
+.pct{color:var(--cream);font-size:11.5px;font-variant-numeric:tabular-nums;text-align:right}
+.dev.muted .pct{color:var(--fail)}
+.mute-b{color:var(--dim);border:0;background:transparent;font:inherit;font-size:.9rem;cursor:pointer;
+ border-radius:5px;padding:0;justify-self:center;line-height:1}
+.mute-b:hover{color:var(--silver)}
+.dev.muted .mute-b{color:var(--fail)}
+/* per-row signal mini-meter — 4 bars that light when THIS device has a live
+ stream, so the sink/source actually playing is visible before you pick it */
+.sig{display:flex;align-items:flex-end;gap:1.5px;height:14px;justify-self:center}
+.sig i{width:2.5px;background:var(--wash);border-radius:1px}
+.sig i:nth-child(1){height:4px}.sig i:nth-child(2){height:7px}
+.sig i:nth-child(3){height:10px}.sig i:nth-child(4){height:13px}
+.sig i.on{background:var(--pass)}.sig i.hot{background:var(--gold)}.sig i.clip{background:var(--fail)}
+
+/* mic mode — three console keys */
+.modes{display:flex;gap:8px;margin-top:2px}
+.mode{flex:1;text-align:center;cursor:pointer;font:inherit;font-size:11px;letter-spacing:.06em;
+ background:linear-gradient(180deg,#23211e,#191715);color:var(--silver);
+ border:1px solid #33302b;border-bottom-color:#0c0b0a;border-radius:8px;padding:8px 4px;
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)}
+.mode:hover{color:var(--gold);border-color:var(--gold)}
+.mode.on{color:var(--panel);background:linear-gradient(180deg,#f0d879,var(--gold));border-color:var(--gold-hi);font-weight:700}
+.mode.on.mute{background:linear-gradient(180deg,#d98a6f,var(--fail));color:var(--cream)}
+.mode .k{display:block;font-size:.54rem;letter-spacing:.14em;color:var(--dim);margin-top:2px}
+.mode.on .k{color:rgba(16,15,15,.7)}
+.ptt-hint{color:var(--dim);font-size:10.5px;text-align:center;margin-top:6px;min-height:1.2em}
+.ptt-hint.live{color:var(--gold)}
+
+/* meter row 1 — the volume dials you set (needles), OUT + IN */
+.meters{display:flex;gap:12px;margin-top:10px}
+.meter{flex:1;background:var(--well);border:1px solid var(--wash);border-radius:10px;padding:9px 10px 7px;position:relative}
+.meter .mode-tag{position:absolute;top:6px;left:8px;font-size:.56rem;letter-spacing:.2em;color:var(--pass)}
+.meter .mode-tag.mut{color:var(--fail)}
+.meter .dial{position:relative;height:50px;overflow:hidden;margin-top:13px}
+.meter .arc{position:absolute;inset:0 0 -50px 0;border:2px solid var(--wash);border-radius:50%}
+.meter .tick{position:absolute;left:50%;bottom:0;width:1.5px;height:9px;background:var(--steel);transform-origin:50% 50px}
+.meter .needle{position:absolute;left:50%;bottom:0;width:2px;height:42px;background:var(--gold-hi);
+ transform-origin:50% 100%;transform:rotate(20deg);border-radius:2px;
+ box-shadow:0 0 6px rgba(255,215,95,.5);transition:transform .3s cubic-bezier(.3,1.3,.5,1)}
+.meter .needle.mut{background:var(--fail);box-shadow:0 0 6px rgba(203,107,77,.5)}
+.meter .hub{position:absolute;left:50%;bottom:-4px;width:9px;height:9px;margin-left:-4.5px;border-radius:50%;background:var(--gold)}
+.meter .m-value{color:var(--cream);font-size:13px;text-align:center;font-weight:700;margin-top:6px;font-variant-numeric:tabular-nums}
+.meter .m-value small{color:var(--dim);font-weight:400}
+.meter .m-label{color:var(--steel);font-size:.6rem;letter-spacing:.2em;text-align:center;margin-top:2px;
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+
+/* meter row 2 — the stereo VU pair: the live SIGNAL through the selected
+ output. Confirms which device is actually carrying the audio. */
+.vupair{margin-top:10px;background:var(--well);border:1px solid var(--wash);border-radius:10px;padding:9px 11px 8px}
+.vuhead{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:7px}
+.vuhead .t{color:var(--steel);font-size:.56rem;letter-spacing:.2em;text-transform:uppercase}
+.vuhead .src{color:var(--cream);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60%}
+.vurow{display:flex;align-items:center;gap:8px;margin:4px 0}
+.vurow .ch{color:var(--steel);font-size:.62rem;width:9px;flex:0 0 auto}
+.vubar{flex:1;display:flex;gap:2px;height:9px}
+.vubar i{flex:1;background:var(--wash);border-radius:1px;opacity:.3}
+.vubar i.on{opacity:1;background:var(--pass)}
+.vubar i.hot{opacity:1;background:var(--gold)}
+.vubar i.clip{opacity:1;background:var(--fail)}
+.vubar i.peak{outline:1px solid var(--gold-hi);outline-offset:-1px}
+.vupair.mute .vubar i{background:var(--wash);opacity:.22}
+.vupair.mute .vuhead .src{color:var(--fail)}
+
+.toast{margin-top:10px;font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px;
+ padding:5px 10px;opacity:0;transition:opacity .25s;min-height:1.4em}
+.toast.show{opacity:1}
+.toast.err{background:transparent;border:1px solid var(--fail);color:var(--fail)}
+
+.aside{flex:1 1 320px;min-width:300px}
+.aside h3{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:1.1rem 0 .5rem}
+.aside h3:first-child{margin-top:.2rem}
+.aside ul{list-style:none}
+.aside li{font-size:.82rem;padding:.24rem 0 .24rem 1.1rem;position:relative}
+.aside li::before{content:"·";color:var(--gold);position:absolute;left:.25rem}
+.aside li b{color:var(--cream);font-weight:700}
+.aside li em{color:var(--dim);font-style:normal}
+.barbits{display:flex;gap:14px;margin:.4rem 0 .2rem;align-items:center;flex-wrap:wrap}
+.barbits span{display:flex;align-items:center;gap:7px;color:var(--silver);font-size:.82rem}
+.barbits .g{font-size:19px;color:var(--cream)}
+.barbits .g.mut{color:var(--fail)}
+.rec{border:1px dashed var(--wash);border-radius:10px;padding:.85rem 1rem;margin-top:.9rem;font-size:.82rem;color:var(--silver)}
+.rec b{color:var(--gold)}
+*{scrollbar-width:thin;scrollbar-color:var(--slate) transparent}
+::-webkit-scrollbar{width:6px;height:6px}
+::-webkit-scrollbar-thumb{background:var(--slate);border-radius:4px}
+@media (prefers-reduced-motion:reduce){.needle{transition:none}}
+</style>
+</head>
+<body>
+
+<header class="masthead">
+ <div class="eyebrow">archsetup · dupre panel family · instrument consoles</div>
+ <h1>Sound — the pulsemixer console</h1>
+ <p>Same faceplate as net + bluetooth. The bar's <b>sound glyph</b> opens this. Every sink and
+ source is a row: <b>click the row body to make it default</b>, drag the fader for its volume,
+ hit the glyph to mute just it. Each row has a <b>live-signal meter</b> — the device actually
+ carrying audio dances even when it isn't the default, so you can <b>find the one playing the
+ meeting</b> and click it. The faceplate switch is the <b>master quick-mute</b>; the mic carries
+ <b>live · muted · push-to-talk</b> (hold Space). Row 1 of gauges is the volume you set; row 2 is
+ the <b>stereo VU</b> of the selected output's live signal.</p>
+</header>
+
+<div class="stage">
+ <div class="slot">
+ <div class="slot-label">snd·01 — pulsemixer in console form</div>
+ <div class="panel">
+
+ <div class="b-face">
+ <div class="b-id">
+ <span class="lamp" id="lamp"></span>
+ <span class="g" id="face-g">󰕾</span>
+ <span class="state-word" id="state">PLAYBACK</span>
+ <span class="badge red" id="mute-badge">MUTED</span>
+ <span class="unit">SND·01</span>
+ <span class="switch on" id="master" onclick="masterMute()" title="Master quick-mute (Super+Shift+M)"></span>
+ <button class="x-btn" title="Close (Esc)">✕</button>
+ </div>
+ </div>
+
+ <div class="engrave">outputs<span class="cnt" id="out-cnt"></span></div>
+ <div id="outputs"></div>
+
+ <div class="engrave">inputs<span class="cnt" id="in-cnt"></span></div>
+ <div id="inputs"></div>
+
+ <div class="engrave">mic mode</div>
+ <div class="modes">
+ <button class="mode" id="md-toggle" onclick="micToggle()">LIVE<span class="k">Super+Shift+A</span></button>
+ <button class="mode" id="md-ptt" onclick="micPtt()">PUSH·TALK<span class="k">hold Space</span></button>
+ </div>
+ <div class="ptt-hint" id="ptt-hint"></div>
+
+ <!-- meter row 1 — volume you set -->
+ <div class="meters">
+ <div class="meter">
+ <span class="mode-tag" id="vt-out">OUT VOL</span>
+ <div class="dial"><div class="arc"></div>
+ <div class="tick" style="transform:rotate(-60deg)"></div><div class="tick" style="transform:rotate(-30deg)"></div>
+ <div class="tick" style="transform:rotate(0)"></div><div class="tick" style="transform:rotate(30deg)"></div>
+ <div class="tick" style="transform:rotate(60deg)"></div>
+ <div class="needle" id="n-out"></div><div class="hub"></div></div>
+ <div class="m-value"><span id="v-out">68</span> <small>%</small></div>
+ <div class="m-label" id="l-out">SPEAKERS</div>
+ </div>
+ <div class="meter">
+ <span class="mode-tag" id="vt-in">IN VOL</span>
+ <div class="dial"><div class="arc"></div>
+ <div class="tick" style="transform:rotate(-60deg)"></div><div class="tick" style="transform:rotate(-30deg)"></div>
+ <div class="tick" style="transform:rotate(0)"></div><div class="tick" style="transform:rotate(30deg)"></div>
+ <div class="tick" style="transform:rotate(60deg)"></div>
+ <div class="needle" id="n-in"></div><div class="hub"></div></div>
+ <div class="m-value"><span id="v-in">54</span> <small>%</small></div>
+ <div class="m-label" id="l-in">BUILT-IN MIC</div>
+ </div>
+ </div>
+
+ <!-- meter row 2 — stereo VU of the selected output's live signal -->
+ <div class="vupair" id="vupair">
+ <div class="vuhead"><span class="t">signal · VU peak</span><span class="src" id="vu-src">SPEAKERS</span></div>
+ <div class="vurow"><span class="ch">L</span><span class="vubar" id="vu-l"></span></div>
+ <div class="vurow"><span class="ch">R</span><span class="vubar" id="vu-r"></span></div>
+ </div>
+
+ <div class="toast" id="toast"></div>
+ </div>
+ </div>
+
+ <div class="aside">
+ <h3>The bar glyph</h3>
+ <div class="barbits">
+ <span><span class="g">󰕾</span> normal — speaker + arcs</span>
+ <span><span class="g mut">󰝟</span> muted — speaker ✕</span>
+ <span><span class="g" style="color:var(--gold)">󰢮</span> ptt armed</span>
+ </div>
+ <h3>Idiom map (same as net / bt)</h3>
+ <ul>
+ <li><b>Faceplate switch = master quick-mute</b> — the net wifi / bt power switch, here muting all output. Flip: state → MUTED, lamp red, bar glyph → speaker-✕.</li>
+ <li><b>Rows are devices</b> — every sink + source. <b>Click the row body</b> to set default (gold DEF moves); the fader sets that device's volume; the trailing glyph mutes just it.</li>
+ <li><b>Per-row signal meter</b> — the 4 bars at the left of each row read that device's <b>live</b> level. A sink can carry a stream without being default, so the one playing lights up — <em>demo: the music is on WH-1000XM4 while Speakers is still default. Click WH-1000XM4 to move to it.</em></li>
+ <li><b>Two meter rows</b> — row 1 = the volume you set (OUT + IN needles); row 2 = the <b>stereo VU</b> (L/R) of the selected output's live signal, red when muted.</li>
+ <li><b>Mic = two console keys</b> — one toggles LIVE↔MUTED (the label flips to show the state), the other is PUSH·TALK: the mic sits muted and un-mutes only while Space is held.</li>
+ <li><b>Verify-everything</b> — every action re-reads pactl/wpctl state after firing, like net/bt.</li>
+ </ul>
+ <h3>Push-to-talk — the one hard part</h3>
+ <div class="rec">
+ Hold-to-talk needs a global key grab under Wayland. Two routes to spec: <b>(a)</b> a Hyprland
+ <b>bind pair</b> — <em>bindp</em> Space press → unmute, release → re-mute, armed only while PTT
+ mode is active (so it doesn't steal Space everywhere); or <b>(b)</b> an <b>evdev/libinput
+ listener</b> reading the key directly. (a) is lighter; (b) survives focus changes but needs
+ input-group permissions. Feasibility research is phase 1 of the spec.
+ </div>
+ </div>
+</div>
+
+<script>
+const $ = id => document.getElementById(id);
+const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
+
+/* sig = base live level 0..1 (what stream is on this device right now).
+ Note the demo: music is on WH-1000XM4 (sig .72) while Speakers is default. */
+let OUT = [
+ {id:'spk', name:'Built-in Speakers', g:'󰓃', vol:68, mute:false, def:true, sig:0, ph:0.0},
+ {id:'xm4', name:'WH-1000XM4', g:'󰋋', vol:80, mute:false, def:false, sig:0.72,ph:1.7},
+ {id:'hdmi',name:'HDMI · Dell U2720', g:'󰡁', vol:100,mute:true, def:false, sig:0, ph:2.9},
+];
+let IN = [
+ {id:'bmic', name:'Built-in Mic', g:'󰍬', vol:54, mute:false, def:true, sig:0.16,ph:0.6},
+ {id:'yeti', name:'Blue Yeti USB', g:'󰦉', vol:70, mute:false, def:false, sig:0, ph:2.1},
+ {id:'xm4m', name:'WH-1000XM4 Headset',g:'󰋎', vol:60, mute:true, def:false, sig:0, ph:3.3},
+];
+let master = false, micMode = 'live', pttHeld = false;
+const pkL={v:0}, pkR={v:0};
+
+function outMutedOf(d){ return master || d.mute; }
+function inMutedOf(d){ return d.mute || (d.def && (micMode==='mute' || (micMode==='ptt' && !pttHeld))); }
+
+function fader(dev){
+ const w=document.createElement('div'); w.className='fader';
+ w.innerHTML=`<div class="trk"><div class="fill" style="width:${dev.vol}%"></div></div>`+
+ `<div class="cap" style="left:${dev.vol}%"></div>`;
+ w.onclick=(e)=>{e.stopPropagation();
+ const r=w.getBoundingClientRect();
+ dev.vol=Math.max(0,Math.min(100,Math.round((e.clientX-r.left)/r.width*100)));
+ if(dev.vol>0) dev.mute=false;
+ render(); toast(`${dev.name} → ${dev.vol}%`);
+ };
+ return w;
+}
+function row(dev,kind){
+ const muted = kind==='out'?outMutedOf(dev):inMutedOf(dev);
+ const r=document.createElement('div');
+ r.className='dev'+(dev.def?' active':'')+(muted?' muted':'');
+ r.title=dev.name;
+ const sig=document.createElement('span'); sig.className='sig'; sig.id='sig-'+dev.id;
+ sig.innerHTML='<i></i><i></i><i></i><i></i>';
+ const who=document.createElement('div'); who.className='who';
+ who.innerHTML=`<span class="g">${dev.g}</span><span class="nm">${dev.name}</span><span class="def">DEF</span>`;
+ const pct=document.createElement('span'); pct.className='pct'; pct.textContent=dev.mute?'mute':dev.vol+'%';
+ const mb=document.createElement('button'); mb.className='mute-b'; mb.textContent=dev.mute?'󰝟':'󰕾';
+ mb.title='mute '+dev.name;
+ mb.onclick=(e)=>{e.stopPropagation();dev.mute=!dev.mute;render();toast(`${dev.name} ${dev.mute?'muted':'unmuted'}`);};
+ r.append(sig,who,fader(dev),pct,mb);
+ r.onclick=()=>{ if(dev.def) return;
+ (kind==='out'?OUT:IN).forEach(d=>d.def=false); dev.def=true; render();
+ toast(`default ${kind==='out'?'output':'input'} → ${dev.name}`);
+ };
+ return r;
+}
+function render(){
+ const o=$('outputs'); o.innerHTML=''; OUT.forEach(d=>o.appendChild(row(d,'out')));
+ const i=$('inputs'); i.innerHTML=''; IN.forEach(d=>i.appendChild(row(d,'in')));
+ $('out-cnt').textContent='· '+OUT.length;
+ $('in-cnt').textContent='· '+IN.length;
+ const od=OUT.find(d=>d.def), id=IN.find(d=>d.def);
+ const oM=outMutedOf(od), iM=inMutedOf(id);
+ $('master').className='switch'+(master?' muted':' on');
+ $('state').textContent=oM?'MUTED':'PLAYBACK';
+ $('lamp').className='lamp'+(oM?' red':'');
+ $('face-g').textContent=oM?'󰝟':'󰕾';
+ $('mute-badge').classList.toggle('show',oM);
+ setNeedle('out',oM?0:od.vol,oM); $('v-out').textContent=oM?0:od.vol; $('l-out').textContent=od.name.toUpperCase();
+ setNeedle('in',iM?0:id.vol,iM); $('v-in').textContent=iM?0:id.vol; $('l-in').textContent=id.name.toUpperCase();
+ $('vt-out').textContent=oM?'OUT·MUTE':'OUT VOL'; $('vt-out').className='mode-tag'+(oM?' mut':'');
+ $('vt-in').textContent=iM?'IN·MUTE':'IN VOL'; $('vt-in').className='mode-tag'+(iM?' mut':'');
+ $('vu-src').textContent=od.name.toUpperCase();
+ $('vupair').classList.toggle('mute',oM);
+ // mic controls: one live/muted toggle + one push-to-talk key
+ const tg=$('md-toggle');
+ tg.textContent = micMode==='mute' ? 'MUTED' : 'LIVE';
+ tg.appendChild(Object.assign(document.createElement('span'),{className:'k',textContent:'Super+Shift+A'}));
+ tg.className = 'mode' + (micMode==='mute' ? ' on mute' : micMode==='live' ? ' on' : '');
+ $('md-ptt').className='mode'+(micMode==='ptt'?' on':'');
+ $('ptt-hint').textContent = micMode==='ptt'
+ ? (pttHeld?'▸ transmitting — Space held':'mic muted — hold Space to talk')
+ : micMode==='mute' ? 'mic muted' : '';
+ $('ptt-hint').className='ptt-hint'+(micMode==='ptt'&&pttHeld?' live':'');
+}
+function setNeedle(side,val,muted){
+ const deg=-60+Math.max(0,Math.min(1,val/100))*120;
+ const n=$('n-'+side); n.style.transform=`rotate(${deg}deg)`;
+ n.className='needle'+(muted?' mut':'');
+}
+
+/* live-signal animation — the per-row minis + the stereo VU pair */
+let phase=0;
+function level(dev,muted){
+ if(muted||!dev.sig) return 0;
+ const env=0.5+0.5*Math.abs(Math.sin(phase*1.25+dev.ph));
+ const trans=Math.random()<0.14?Math.random()*0.4:0;
+ return Math.max(0,Math.min(1,dev.sig*env+trans-Math.random()*0.07));
+}
+function paintMini(el,lvl){
+ const b=el.children, lit=Math.round(lvl*4);
+ for(let k=0;k<4;k++){ b[k].className = k<lit ? (k>=3?'clip':k>=2?'hot':'on') : ''; }
+}
+function paintVU(el,lvl,pk){
+ const b=el.children, n=b.length, lit=Math.round(lvl*n);
+ pk.v=Math.max(lit,pk.v-0.35); const p=Math.round(pk.v);
+ for(let k=0;k<n;k++){ let c = k<lit ? (k>=n-2?'clip':k>=n-4?'hot':'on') : '';
+ if(p>0 && k===p-1) c=(c?c+' ':'')+'peak'; b[k].className=c; }
+}
+function buildVU(el,n){ el.innerHTML=''; for(let k=0;k<n;k++) el.appendChild(document.createElement('i')); }
+function tick(){
+ phase+=0.09;
+ OUT.forEach(d=>{const el=$('sig-'+d.id); if(el) paintMini(el, d._l=level(d,outMutedOf(d)));});
+ IN.forEach(d=>{const el=$('sig-'+d.id); if(el) paintMini(el, d._l=level(d,inMutedOf(d)));});
+ const od=OUT.find(d=>d.def), oM=outMutedOf(od);
+ const base=oM?0:level(od,false);
+ paintVU($('vu-l'),Math.min(1,base*(0.92+Math.random()*0.16)),pkL);
+ paintVU($('vu-r'),Math.min(1,base*(0.92+Math.random()*0.16)),pkR);
+}
+
+function masterMute(){ master=!master; render(); toast(master?'ALL OUTPUT MUTED':'output unmuted'); }
+/* two mic buttons: toggle flips live<->muted (and leaves ptt); ptt arms/disarms */
+function micToggle(){ micMode = micMode==='mute' ? 'live' : 'mute'; pttHeld=false; render();
+ toast(micMode==='mute'?'mic muted':'mic live'); }
+function micPtt(){ micMode = micMode==='ptt' ? 'live' : 'ptt'; pttHeld=false; render();
+ toast(micMode==='ptt'?'push-to-talk armed — hold Space':'mic live'); }
+let tT;
+function toast(msg,err){ const t=$('toast'); t.textContent=msg;
+ t.className='toast show'+(err?' err':''); clearTimeout(tT); tT=setTimeout(()=>t.className='toast',2200); }
+addEventListener('keydown',e=>{ if(e.code==='Space'&&micMode==='ptt'&&!e.repeat){e.preventDefault();pttHeld=true;render();}});
+addEventListener('keyup', e=>{ if(e.code==='Space'&&micMode==='ptt'){e.preventDefault();pttHeld=false;render();}});
+
+buildVU($('vu-l'),16); buildVU($('vu-r'),16);
+render();
+if(!reduced) setInterval(tick,70); else tick();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-03-waybar-redesign-prototype.html b/docs/prototypes/2026-07-03-waybar-redesign-prototype.html
new file mode 100644
index 0000000..3f3e7c1
--- /dev/null
+++ b/docs/prototypes/2026-07-03-waybar-redesign-prototype.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>Waybar redesign — dupre instrument console</title>
+<style>
+:root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5;
+ --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d;
+ --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+}
+*{box-sizing:border-box;margin:0;padding:0}
+html{background:var(--ground)}
+body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 5rem;line-height:1.45;
+ background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)}
+.wrap{max-width:1400px;margin:0 auto}
+.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase}
+h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem}
+.masthead p{color:var(--dim);font-size:.86rem;max-width:88ch}
+.masthead p b{color:var(--silver);font-weight:700}
+
+/* each variation sits on a "desk" — a strip of desktop so the bar reads as a
+ real top bar floating over a window, exactly how it looks on-screen */
+.desk{margin:1.9rem 0 0;border-radius:14px;overflow:hidden;
+ border:1px solid #211e1a;
+ background:
+ linear-gradient(180deg,#0e0d0c 0 62px,transparent 62px),
+ repeating-linear-gradient(135deg,#191613 0 14px,#171512 14px 28px);
+ position:relative}
+.desk-label{display:flex;align-items:baseline;gap:.7rem;padding:.5rem .2rem .1rem}
+.desk-label .n{color:var(--gold);font-size:.82rem;letter-spacing:.08em}
+.desk-label .d{color:var(--dim);font-size:.76rem}
+.desk-window{position:absolute;inset:70px 26px 22px;border:1px solid #262320;border-radius:10px;
+ background:linear-gradient(180deg,#131110,#0e0d0c);opacity:.6}
+.desk-window::before{content:"emacs — instrument-console.el";position:absolute;top:8px;left:14px;
+ color:#3a3630;font-size:.72rem}
+
+/* the bar frame: matches waybar's -54 top strip. two clusters, gold-bordered,
+ floating with a gap between them (modules-center is empty in the real config) */
+.bar{position:relative;z-index:2;display:flex;justify-content:space-between;align-items:flex-start;
+ gap:1rem;padding:10px 12px;height:132px}
+.cluster{display:flex;align-items:center;gap:2px;
+ background:var(--panel);border:1.4px solid var(--gold);border-radius:15px;
+ padding:2px 9px;box-shadow:0 4px 9px rgba(0,0,0,.5)}
+.mod{display:flex;align-items:center;gap:7px;color:var(--silver);
+ padding:7px 9px;border-radius:11px;font-size:14px;cursor:default;white-space:nowrap;position:relative}
+.mod .g{font-size:16px;line-height:1}
+.mod .g.xl{font-size:19px}
+.mod:hover{background:var(--wash)}
+.val{font-variant-numeric:tabular-nums}
+.cream{color:var(--cream)}.gold{color:var(--gold)}.dim{color:var(--dim)}
+.fail{color:var(--fail)}.pass{color:var(--pass)}.steel{color:var(--steel)}
+
+/* workspaces — circular tokens like the real ws-icons */
+.ws{display:flex;gap:5px;padding:0 3px}
+.ws b{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;font-size:13px;
+ color:var(--silver);border:1.4px solid var(--slate);background:#141210}
+.ws b.on{color:var(--panel);background:var(--gold);border-color:var(--gold);font-weight:700;
+ box-shadow:0 0 8px 1px rgba(218,181,61,.4)}
+.ws b.busy{border-color:var(--steel);color:var(--cream)}
+.menu{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;color:var(--gold);
+ font-size:17px;background:linear-gradient(180deg,#211e19,#151210);border:1px solid #33302b}
+.title{color:var(--dim);font-size:13px;max-width:230px;overflow:hidden;text-overflow:ellipsis}
+
+/* collapse arrows — recessed dim wells, per the current design */
+.arrow{color:var(--dim);font-size:12px;background:rgba(0,0,0,.35);
+ box-shadow:inset 0 1px 2px rgba(0,0,0,.7);border-radius:7px;padding:8px 7px}
+
+/* lamp — the panel's signature status dot, glow and all */
+.lamp{width:8px;height:8px;border-radius:50%;background:var(--pass);flex:0 0 auto;
+ box-shadow:0 0 6px 1px rgba(116,147,47,.55)}
+.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.6)}
+.lamp.red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)}
+.lamp.off{background:var(--wash);box-shadow:none}
+
+/* engraved hairline divider between functional groups (echoes .engrave rules) */
+.sep{width:1px;align-self:stretch;margin:6px 3px;
+ background:linear-gradient(180deg,transparent,var(--wash) 22%,var(--wash) 78%,transparent)}
+
+.notes{margin-top:2.4rem;display:flex;gap:2rem;flex-wrap:wrap}
+.note{flex:1 1 300px;min-width:280px}
+.note h3{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:0 0 .5rem}
+.note ul{list-style:none}
+.note li{font-size:.82rem;padding:.24rem 0 .24rem 1.1rem;position:relative;color:var(--silver)}
+.note li::before{content:"·";color:var(--gold);position:absolute;left:.25rem}
+.note li b{color:var(--cream);font-weight:700}
+.note li em{color:var(--dim);font-style:normal}
+.rec{border:1px dashed var(--wash);border-radius:10px;padding:.9rem 1.1rem;margin-top:1.1rem;
+ font-size:.83rem;color:var(--silver)}
+.rec b{color:var(--gold)}
+
+/* ============ V1 · FACEPLATE ============ */
+/* machined faceplate: vertical gradient + a 1px top highlight + deeper shadow.
+ otherwise the current layout — lowest-risk, faithful to GTK CSS. */
+.v1 .cluster{background:linear-gradient(180deg,var(--raise),var(--panel));
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.05),0 6px 14px rgba(0,0,0,.55)}
+.v1 .val.clock{color:var(--cream)}
+
+/* ============ V2 · INSTRUMENT SEGMENTS ============ */
+/* each functional group is its own recessed sub-faceplate with an engraved unit
+ label underneath, lamps on status modules, gold-hi active values */
+.v2 .cluster{background:linear-gradient(180deg,var(--raise),var(--panel));padding:3px 6px;
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.05),0 6px 14px rgba(0,0,0,.55)}
+.v2 .seg{display:flex;flex-direction:column;align-items:center;gap:2px;
+ background:var(--well);border:1px solid #232019;border-radius:10px;padding:3px 5px 2px;margin:0 2px}
+.v2 .seg .row{display:flex;align-items:center;gap:6px}
+.v2 .seg .unit{color:var(--steel);font-size:.52rem;letter-spacing:.24em;text-transform:uppercase}
+.v2 .mod{padding:5px 7px}
+.v2 .mod:hover{background:var(--wash)}
+.v2 .menu{background:linear-gradient(180deg,#2a251d,#161310);border-color:var(--gold)}
+/* mini gauge for sysmon — a squat needle echoing the panel meters */
+.gauge{width:26px;height:15px;position:relative;overflow:hidden}
+.gauge .arc{position:absolute;inset:0 0 -26px 0;border:1.5px solid var(--wash);border-radius:50%}
+.gauge .ndl{position:absolute;left:50%;bottom:0;width:1.5px;height:13px;background:var(--gold-hi);
+ transform-origin:50% 100%;transform:rotate(18deg);border-radius:1px;
+ box-shadow:0 0 4px rgba(255,215,95,.5)}
+.gauge .hub{position:absolute;left:50%;bottom:-2px;width:5px;height:5px;margin-left:-2.5px;
+ border-radius:50%;background:var(--gold)}
+
+/* ============ V3 · FULL CONSOLE ============ */
+/* every module a recessed well with a lamp; console-key toggles with full
+ physical-key gradient + inset; sysmon as twin analog gauges; cream clock */
+.v3 .cluster{background:linear-gradient(180deg,#0d0c0b,#080807);border-width:1.8px;
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.05),0 8px 18px rgba(0,0,0,.6);padding:3px 8px}
+.v3 .mod{background:var(--well);border:1px solid #201d17;border-radius:9px;margin:0 2px;padding:6px 9px}
+.v3 .mod:hover{background:#141210;border-color:var(--slate)}
+/* console-key toggles — the physical key from the panels' .c-btn */
+.v3 .key{background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;
+ border-bottom-color:#0c0b0a;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)}
+.v3 .key.engaged{color:var(--gold);border-color:var(--gold)}
+.v3 .key.off{color:var(--fail)}
+.v3 .menu{background:linear-gradient(180deg,#2a251d,#141110);border:1px solid var(--gold);
+ box-shadow:0 0 8px rgba(218,181,61,.25)}
+.v3 .ws b{background:var(--well);border-color:#201d17}
+.v3 .ws b.on{background:var(--gold);border-color:var(--gold)}
+.v3 .val.clock{color:var(--cream);font-weight:700}
+.v3 .tz{color:var(--steel);font-size:.55rem;letter-spacing:.2em}
+.v3 .twin{display:flex;gap:6px}
+.v3 .gauge2{width:22px;height:14px;position:relative;overflow:hidden}
+.v3 .gauge2 .arc{position:absolute;inset:0 0 -22px 0;border:1.5px solid var(--wash);border-radius:50%}
+.v3 .gauge2 .ndl{position:absolute;left:50%;bottom:0;width:1.5px;height:12px;transform-origin:50% 100%;
+ border-radius:1px;background:var(--gold-hi);box-shadow:0 0 4px rgba(255,215,95,.5)}
+.v3 .gauge2 .ndl.warn{background:var(--gold)}
+.v3 .gauge2 .hub{position:absolute;left:50%;bottom:-2px;width:4px;height:4px;margin-left:-2px;
+ border-radius:50%;background:var(--gold)}
+</style>
+</head>
+<body>
+<div class="wrap">
+
+<header class="masthead">
+ <div class="eyebrow">archsetup · dupre panel family · waybar</div>
+ <h1>Waybar — three ways to spruce it</h1>
+ <p>The bar already runs the dupre palette and a gold border. These three push it toward the
+ <b>instrument-console faceplate</b> language of the net + bluetooth panels — machined
+ gradient plates, engraved unit labels, glowing status lamps, physical console keys, analog
+ gauges — dialing the intensity up from left-touch to full console. Same real module set in
+ each so you're comparing <b>treatment, not content</b>. All three stay inside what GTK3 CSS
+ (waybar's engine) can actually render.</p>
+</header>
+
+<!-- ============ CURRENT (reference) ============ -->
+<div class="desk">
+ <div class="bar" style="padding-top:14px">
+ <div class="cluster">
+ <span class="menu"></span>
+ <span class="ws"><b class="on">1</b><b class="busy">2</b><b>3</b></span>
+ <span class="mod"><span class="g">󰕰</span></span>
+ <span class="title">instrument-console.el</span>
+ <span class="mod arrow"></span>
+ </div>
+ <div class="cluster">
+ <span class="mod arrow"></span>
+ <span class="mod"><span class="g gold">󰤨</span></span>
+ <span class="mod"><span class="g xl">󰍬</span></span>
+ <span class="mod"><span class="g xl">󰕾</span> <span class="val">62%</span></span>
+ <span class="mod"><span class="g">󰹇</span></span>
+ <span class="mod"><span class="g">󰃟</span></span>
+ <span class="mod"><span class="g">󰒲</span></span>
+ <span class="mod"><span class="g">󰂯</span></span>
+ <span class="mod"><span class="g">󰻠</span> <span class="val">8%</span></span>
+ <span class="mod"><span class="g">󰔛</span> <span class="val">24:10</span></span>
+ <span class="mod"><span class="g">󰃭</span> <span class="val">Fri Jul 3</span></span>
+ </div>
+ </div>
+ <div class="desk-window"></div>
+</div>
+<div class="desk-label"><span class="n">current</span><span class="d">— flat pills, colour-only states. the baseline these three build on.</span></div>
+
+<!-- ============ V1 · FACEPLATE ============ -->
+<div class="desk v1">
+ <div class="bar" style="padding-top:14px">
+ <div class="cluster">
+ <span class="menu"></span>
+ <span class="ws"><b class="on">1</b><b class="busy">2</b><b>3</b></span>
+ <span class="sep"></span>
+ <span class="mod"><span class="g">󰕰</span></span>
+ <span class="title">instrument-console.el</span>
+ <span class="mod arrow"></span>
+ </div>
+ <div class="cluster">
+ <span class="mod arrow"></span>
+ <span class="mod"><span class="lamp gold"></span><span class="g gold">󰤨</span></span>
+ <span class="sep"></span>
+ <span class="mod"><span class="g xl">󰍬</span></span>
+ <span class="mod"><span class="g xl">󰕾</span> <span class="val cream">62%</span></span>
+ <span class="sep"></span>
+ <span class="mod"><span class="g">󰹇</span></span>
+ <span class="mod"><span class="g">󰃟</span></span>
+ <span class="mod"><span class="g gold">󰒲</span></span>
+ <span class="mod"><span class="lamp"></span><span class="g">󰂯</span></span>
+ <span class="sep"></span>
+ <span class="mod"><span class="g">󰻠</span> <span class="val">8%</span></span>
+ <span class="mod"><span class="g gold">󰔛</span> <span class="val gold">24:10</span></span>
+ <span class="sep"></span>
+ <span class="mod"><span class="g dim">󰃭</span> <span class="val clock">Fri Jul 3</span></span>
+ </div>
+ </div>
+ <div class="desk-window"></div>
+</div>
+<div class="desk-label"><span class="n">variation 1 · faceplate</span><span class="d">— machined gradient + top highlight, engraved hairline dividers, status lamps on net/bt, cream clock. Nearest to today; drop-in GTK CSS.</span></div>
+
+<!-- ============ V2 · INSTRUMENT SEGMENTS ============ -->
+<div class="desk v2">
+ <div class="bar" style="padding-top:12px">
+ <div class="cluster">
+ <span class="menu"></span>
+ <span class="seg"><span class="row ws"><b class="on">1</b><b class="busy">2</b><b>3</b></span><span class="unit">wksp</span></span>
+ <span class="seg"><span class="row"><span class="g">󰕰</span><span class="title" style="max-width:180px">instrument-console.el</span></span><span class="unit">layout · window</span></span>
+ <span class="mod arrow"></span>
+ </div>
+ <div class="cluster">
+ <span class="mod arrow"></span>
+ <span class="seg"><span class="row"><span class="lamp gold"></span><span class="g gold">󰤨</span><span class="gold">CAPTIVE</span></span><span class="unit">net</span></span>
+ <span class="seg"><span class="row"><span class="g xl">󰍬</span><span class="g xl">󰕾</span><span class="val cream">62%</span></span><span class="unit">sound</span></span>
+ <span class="seg"><span class="row"><span class="g">󰹇</span><span class="g">󰃟</span><span class="g gold">󰒲</span></span><span class="unit">toggles</span></span>
+ <span class="seg"><span class="row"><span class="lamp"></span><span class="g">󰂯</span><span class="dim">M650</span></span><span class="unit">bt</span></span>
+ <span class="seg"><span class="row"><span class="gauge"><span class="arc"></span><span class="ndl"></span><span class="hub"></span></span><span class="val">8%</span></span><span class="unit">cpu</span></span>
+ <span class="seg"><span class="row"><span class="g gold">󰔛</span><span class="val gold">24:10</span></span><span class="unit">timer</span></span>
+ <span class="seg"><span class="row"><span class="val cream">Fri Jul 3</span><span class="val">11:23</span></span><span class="unit">clock</span></span>
+ </div>
+ </div>
+ <div class="desk-window"></div>
+</div>
+<div class="desk-label"><span class="n">variation 2 · instrument segments</span><span class="d">— each group a recessed sub-plate with an engraved unit label; a squat needle gauge for cpu. Reads like a row of instruments. Taller; label row costs a few px.</span></div>
+
+<!-- ============ V3 · FULL CONSOLE ============ -->
+<div class="desk v3">
+ <div class="bar" style="padding-top:12px">
+ <div class="cluster">
+ <span class="menu"></span>
+ <span class="ws"><b class="on">1</b><b class="busy">2</b><b>3</b></span>
+ <span class="sep"></span>
+ <span class="mod key"><span class="g">󰕰</span></span>
+ <span class="title">instrument-console.el</span>
+ <span class="mod arrow"></span>
+ </div>
+ <div class="cluster">
+ <span class="mod arrow"></span>
+ <span class="mod"><span class="lamp gold"></span><span class="g gold">󰤨</span> <span class="gold">CAPTIVE</span></span>
+ <span class="sep"></span>
+ <span class="mod key"><span class="g xl">󰍬</span></span>
+ <span class="mod"><span class="g xl">󰕾</span> <span class="val cream">62%</span></span>
+ <span class="sep"></span>
+ <span class="mod key engaged"><span class="g">󰹇</span></span>
+ <span class="mod key"><span class="g">󰃟</span></span>
+ <span class="mod key engaged"><span class="g">󰒲</span></span>
+ <span class="mod"><span class="lamp"></span><span class="g">󰂯</span> <span class="dim val">72%</span></span>
+ <span class="sep"></span>
+ <span class="mod"><span class="twin">
+ <span class="gauge2"><span class="arc"></span><span class="ndl"></span><span class="hub"></span></span>
+ <span class="gauge2"><span class="arc"></span><span class="ndl warn" style="transform:rotate(38deg)"></span><span class="hub"></span></span>
+ </span></span>
+ <span class="mod key"><span class="g gold">󰔛</span> <span class="val gold">24:10</span></span>
+ <span class="sep"></span>
+ <span class="mod"><span class="g dim">󰃭</span> <span class="val clock">Fri Jul 3</span> <span class="val clock">11:23</span><span class="tz">&nbsp;EDT</span></span>
+ </div>
+ </div>
+ <div class="desk-window"></div>
+</div>
+<div class="desk-label"><span class="n">variation 3 · full console</span><span class="d">— every module a recessed well, physical console keys for toggles (gold when engaged, terracotta when off), twin cpu/mem gauges, cream tabular clock with engraved TZ. Furthest from today; closest to the panels.</span></div>
+
+<!-- ============ NOTES ============ -->
+<div class="notes">
+ <div class="note">
+ <h3>What carries over from the panels</h3>
+ <ul>
+ <li><b>Lamps</b> — the glowing status dot lands on net + bt so health reads at a glance, not just by glyph colour <em>(gold = captive/engaged, green = ok, red = fail, dim = off)</em>.</li>
+ <li><b>Machined faceplate</b> — the cluster gets the b-face vertical gradient + 1px top highlight + deeper shadow, so it looks milled rather than printed.</li>
+ <li><b>Engraved dividers</b> — hairline separators group the right cluster into net · sound · toggles · system · clock, echoing the panels' engraved section rules.</li>
+ <li><b>Console keys</b> — the toggles (touchpad, dim, caffeine) borrow .c-btn: gradient fill, inset highlight, gold border when engaged.</li>
+ <li><b>Gauges</b> — sysmon becomes a squat needle (or twin needles for cpu/mem), the same instrument the panels use for throughput and battery.</li>
+ <li><b>Cream + tabular</b> — the clock and live values shift to cream with tabular-nums, matching the panels' readouts.</li>
+ </ul>
+ </div>
+ <div class="note">
+ <h3>GTK3 translation caveats</h3>
+ <ul>
+ <li><b>Dividers</b> need real separator modules or per-module borders — waybar can't inject <em>::before</em> content between modules the way this HTML does.</li>
+ <li><b>Lamps</b> render as a small pango glyph (● with colour + text-shadow glow) prepended in each script, or a tiny bordered box widget — both are GTK-safe.</li>
+ <li><b>Gauges</b> are the real work: GTK CSS can't draw a rotating needle. Options — a Cairo/GTK drawing area in a custom module, or fake it with a unicode gauge glyph that steps by load band. V2's single gauge is cheaper than V3's twin.</li>
+ <li><b>V2's unit labels</b> raise the bar height (the label row). Fine at 54px reserved, but worth eyeballing against the -54 margin strip.</li>
+ <li>Gradients, inset box-shadow, border colour states, tabular-nums — all already proven in the current stylesheet.</li>
+ </ul>
+ </div>
+ <div class="note">
+ <h3>My read</h3>
+ <div class="rec">
+ <b>Variation 1 (faceplate)</b> is the one I'd ship first: it lands ~80% of the instrument-console feel — lamps, milled plates, engraved grouping, cream clock — for pure CSS plus a lamp glyph in the net/bt scripts. No custom drawing, no height risk.
+ <br><br>
+ <b>Variation 3</b> is the aspirational target once a gauge-drawing module exists (it'd also upgrade the sysmon popup). <b>Variation 2</b> is the middle path if you want the unit labels' legibility but not the full recessed-well density. They're not exclusive — 1 can grow into 3.
+ </div>
+ </div>
+</div>
+
+</div>
+</body>
+</html>
diff --git a/docs/prototypes/README.org b/docs/prototypes/README.org
new file mode 100644
index 0000000..9df85cb
--- /dev/null
+++ b/docs/prototypes/README.org
@@ -0,0 +1,21 @@
+#+TITLE: Panel & Waybar Design Prototypes
+#+AUTHOR: Craig Jennings
+
+Self-contained HTML/CSS design prototypes for the instrument-console panel
+family and the waybar redesign. Each opens standalone in a browser (no external
+assets). These are the normative visual references the specs in [[file:../specs/][docs/specs/]]
+point at.
+
+* Prototypes
+
+- [[file:2026-07-03-instrument-console-panels-prototype.html][2026-07-03-instrument-console-panels-prototype.html]] — the net + bluetooth
+ pair; the approved faceplate design that shipped. Normative reference for
+ [[file:../specs/2026-07-03-instrument-console-panels-spec.org][the instrument-console spec]].
+- [[file:2026-07-03-net-panel-rescan-prototype.html][2026-07-03-net-panel-rescan-prototype.html]] — the manual rescan/scan ⟳
+ affordance for the NETWORKS/NEARBY headers (busy-style throbber + list fade).
+- [[file:2026-07-03-sound-panel-prototype.html][2026-07-03-sound-panel-prototype.html]] — the audio/pulsemixer console; layout
+ reference for [[file:../specs/2026-07-03-audio-panel-spec.org][the audio-panel spec]].
+- [[file:2026-07-03-panel-widget-gallery-prototype.html][2026-07-03-panel-widget-gallery-prototype.html]] — the shared instrument-console
+ widget kit (lamps, engraved sections, console keys, needle gauges).
+- [[file:2026-07-03-waybar-redesign-prototype.html][2026-07-03-waybar-redesign-prototype.html]] — three directions for sprucing up
+ waybar in the dupre instrument-console aesthetic (future work).
diff --git a/docs/specs/2026-07-03-audio-panel-spec.org b/docs/specs/2026-07-03-audio-panel-spec.org
index 82041ed..5b678a8 100644
--- a/docs/specs/2026-07-03-audio-panel-spec.org
+++ b/docs/specs/2026-07-03-audio-panel-spec.org
@@ -24,7 +24,7 @@
- [2026-07-03 Fri] DRAFT — stub from the todo.org task "Audio panel spec"
(roam ask 2026-07-02) plus the 2026-07-03 waybar/sound design discussion.
Written to iterate alongside the prototype
- (=working/sound-panel/sound-panel-prototype.html=). Spine is present; the
+ (=docs/prototypes/2026-07-03-sound-panel-prototype.html=). Spine is present; the
Decisions and Design detail get filled in as we go.
* Metadata
@@ -70,7 +70,7 @@ this shape; audio is the third instrument in the family.
* Design sketch
-Prototype: =working/sound-panel/sound-panel-prototype.html= (the reference for
+Prototype: =docs/prototypes/2026-07-03-sound-panel-prototype.html= (the reference for
layout + idioms below).
** Surface (from the prototype)
diff --git a/docs/specs/2026-07-03-instrument-console-panels-spec.org b/docs/specs/2026-07-03-instrument-console-panels-spec.org
index c0a0c56..2c80aa9 100644
--- a/docs/specs/2026-07-03-instrument-console-panels-spec.org
+++ b/docs/specs/2026-07-03-instrument-console-panels-spec.org
@@ -13,6 +13,16 @@
:PROPERTIES:
:ID: e73877f5-4f5b-4f81-b946-dbaa6145e0d5
:END:
+- 2026-07-03 Fri @ 17:35 -0400 :: Post-impl increment (stays IMPLEMENTED): added
+ a manual rescan/scan ⟳ affordance to both panels — net NETWORKS header
+ (drives manage.rescan) and bt NEARBY header (drives on_scan / pair-mode
+ discovery), with the approved "all" busy style (Gtk.Spinner throbber + a
+ GLib breathe on "scanning…" + a one-shot list fade; no CSS keyframes exist in
+ this GTK setup). Prototype: archsetup docs/prototypes/2026-07-03-net-panel-rescan-prototype.html. Code shipped
+ UNCOMMITTED into the dotfiles repo from an archsetup session (cross-project);
+ handoff at ~/.dotfiles/inbox/2026-07-03-1733-from-archsetup-rescan-handoff.org.
+ Verified: net 584 + bt 223 unit OK, both AT-SPI smokes green (⟳ present);
+ live busy-animation feel pending Craig's eyeball.
- 2026-07-03 Fri @ 06:49 -0400 :: DOING → IMPLEMENTED: all six phases shipped
(net GTK-free layer 81ec9c3, net view 800ef60; bt GTK-free layer 5318b34, bt
view 66f03d9; phase-6 dead-code removal f4e688e). Both panels are single-screen
@@ -36,7 +46,7 @@
|---------------------+------------------------------------------------------------|
| Repos | dotfiles (net/, bluetooth/, themes), archsetup |
|---------------------+------------------------------------------------------------|
-| Normative reference | [[file:../../assets/2026-07-03-instrument-console-panels-prototype.html][assets/2026-07-03-instrument-console-panels-prototype.html]] |
+| Normative reference | [[file:../prototypes/2026-07-03-instrument-console-panels-prototype.html][docs/prototypes/2026-07-03-instrument-console-panels-prototype.html]] |
|---------------------+------------------------------------------------------------|
* Summary
diff --git a/todo.org b/todo.org
index 83e06f2..6265b31 100644
--- a/todo.org
+++ b/todo.org
@@ -1408,7 +1408,7 @@ CLOSED: [2026-07-03 Fri]
:PROPERTIES:
:SPEC_ID: e73877f5-4f5b-4f81-b946-dbaa6145e0d5
:END:
-The no-approvals speedrun build of the console design Craig approved through five prototype iterations (2026-07-02/03). Spec: [[file:docs/specs/2026-07-03-instrument-console-panels-spec.org]] — the interactive prototype [[file:assets/2026-07-03-instrument-console-panels-prototype.html][assets/2026-07-03-instrument-console-panels-prototype.html]] is the normative design reference. Folds three open tasks: network panel redesign, bt switch placement + title, bt rename devices. Code in ~/.dotfiles (net/, bluetooth/, themes/dupre/panel.css). Final step: flip the spec to IMPLEMENTED, write the findings summary to file, finalize session context.
+The no-approvals speedrun build of the console design Craig approved through five prototype iterations (2026-07-02/03). Spec: [[file:docs/specs/2026-07-03-instrument-console-panels-spec.org]] — the interactive prototype [[file:docs/prototypes/2026-07-03-instrument-console-panels-prototype.html][docs/prototypes/2026-07-03-instrument-console-panels-prototype.html]] is the normative design reference. Folds three open tasks: network panel redesign, bt switch placement + title, bt rename devices. Code in ~/.dotfiles (net/, bluetooth/, themes/dupre/panel.css). Final step: flip the spec to IMPLEMENTED, write the findings summary to file, finalize session context.
*** 2026-07-03 Fri @ 03:20:00 -0400 Phase 2 shipped: net GTK-free console layer + engine verbs
Dotfiles =81ec9c3= (TDD, 52 new tests, 581 net green). Pure presenter logic for the single-screen console, no view code touched: =viewmodel.net_faceplate= (state word + lamp + TUNNEL/AIRPLANE badges, wired-link-wins precedence), =network_console_rows= (ethernet pinned, radio-off note, active-then-signal sort, per-row lamp/caption/ladder/forget), =channel_headline= (wired device+speed / SSID+ladder+dBm / not-connected placeholder), =tunnel_console_rows=, dial-meter geometry (=meter_needle_deg= + =meter_scale= 100→1000 auto-relabel), =signal_bars=/=mbps_label=, and =panel.ArmState= (two-click arm-to-fire for forget/disconnect). Engine verbs: =manage.wifi_radio= (nmcli radio wifi on|off), =manage.device_up= (ethernet take-the-route), =sysio.link_speed_mbps= (/sys wired speed), =connections.ethernet_devices=, hidden flag on =manage.add=.
@@ -1707,4 +1707,4 @@ CLOSED: [2026-07-03 Fri]
:END:
Went past the spec to a full build in a no-approvals speedrun. Spec is now IMPLEMENTED ([[file:docs/specs/2026-07-03-audio-panel-spec.org]], all 5 Decisions resolved). The panel shipped in the dotfiles repo (branch panel-bugfixing, commits 65e5bb0..9601420): pactl engine, GTK-free presenter, GTK instrument-console panel (OUTPUTS/INPUTS device rows with faders + per-device mute, LIVE/MUTED/PUSH·TALK mic keys, twin VU gauges, master quick-mute), Hyprland-bind push-to-talk, bar indicator, and the bar/keybind wiring (Super+A → panel, XF86AudioMute → master quick-mute). 102 unit tests + a passing AT-SPI smoke on velox. Live-eyeball validation filed under Manual testing and validation. Apply steps + follow-ups handed to the dotfiles project inbox.
-Original ask (roam inbox, 2026-07-02): net/bt-panel kin — change default output/input, volume for both, push-to-talk mic mode for meetings, master quick-mute, bar sound-glyph state. Related bindings: Super+M audio-cycle ring, Super+Shift+A mic-toggle. Prototype: =working/sound-panel/sound-panel-prototype.html=.
+Original ask (roam inbox, 2026-07-02): net/bt-panel kin — change default output/input, volume for both, push-to-talk mic mode for meetings, master quick-mute, bar sound-glyph state. Related bindings: Super+M audio-cycle ring, Super+Shift+A mic-toggle. Prototype: =docs/prototypes/2026-07-03-sound-panel-prototype.html=.