aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/2026-07-03-panel-widget-gallery-prototype.html
blob: 8e642f47d94fb47000de424bc401e69f2ec85fa9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
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>