aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-18 16:33:31 -0500
committerCraig Jennings <c@cjennings.net>2026-07-18 16:33:31 -0500
commitda0e1fad217c33cc376004cc19b05fe7f95debd6 (patch)
treed9a1010fc78cfd6ae1f112423200c127ebe1286e /docs
parent12532dbf85964b44aa7f96c42a0d0350eae0f35e (diff)
downloadarchsetup-da0e1fad217c33cc376004cc19b05fe7f95debd6.tar.gz
archsetup-da0e1fad217c33cc376004cc19b05fe7f95debd6.zip
feat(gallery): bring the decade-through-EQ ten to the extraction bar
decadeBox, twoHandSafety, voiceLoop, miniSig, fuelBar, radialRing, sparkline, waveStrip, scope, and eqBars each get the contract comment. The eight DOM builders take dupre- prefixes across their markup and CSS blocks. voiceLoop's inner bar becomes dupre-vk-bar to stay clear of fuelBar's dupre-bar, and miniSig and eqBars move their cell modifiers to the shared dupre-on, dupre-hot, and dupre-clip. Domain gates land where a handle could wedge: decadeBox coerces and clamps its digits, twoHandSafety ignores sides other than L and R, voiceLoop keeps the demo state to the stock loop set (a caller's shorter list used to crash on it), the meters clamp set() to their documented ranges, and sparkline and scope guard the one-sample case that drew NaN coordinates. Two probe checks cover the gates and drive voiceLoop's grammar through the prefixed DOM. probe-fams follows scope's rename. Extraction audit: 74/111 contract comments.
Diffstat (limited to 'docs')
-rw-r--r--docs/prototypes/widgets.js278
1 files changed, 192 insertions, 86 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js
index f09bedc..6477046 100644
--- a/docs/prototypes/widgets.js
+++ b/docs/prototypes/widgets.js
@@ -2808,11 +2808,20 @@ DUPRE.knifeSwitch = function (host, opts = {}) {
return { el: s, get: () => closed, set };
};
-/* R49 decade box — four skirted knobs, one digit each; the value is their sum */
+/* R49 decade box — four skirted knobs, one digit each; the value is their sum.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: digits (array of four 0-9 digits, most-significant first, default
+ [3, 5, 0, 0] — missing entries read 0, values clamp to 0-9);
+ onChange(total, 'N,NNN Ω') fires on every redraw, including the
+ initial paint.
+ handle: el, get() (the summed value; digits weight x1000/x100/x10/x1).
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; no gradients registered. */
DUPRE.decadeBox = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const MUL = [1000, 100, 10, 1], LBL = ['x1000', 'x100', 'x10', 'x1'];
- const digs = (opts.digits || [3, 5, 0, 0]).slice();
+ const digs = MUL.map((_, i) => Math.round(Math.max(0, Math.min(9, +(opts.digits || [3, 5, 0, 0])[i] || 0))));
const s = stageSvg(host, 'rsvg', 160, 100);
svgEl(s, 'rect', { x: 2, y: 2, width: 156, height: 96, rx: 8, fill: '#17140f', stroke: '#060505', 'stroke-width': 1.5 });
const wins = [], knobs = [];
@@ -2844,7 +2853,16 @@ DUPRE.decadeBox = function (host, opts = {}) {
return { el: s, get: () => total() };
};
-/* R50 two-hand safety — one palm button arms a 500ms window; the other completes the cycle */
+/* R50 two-hand safety — one palm button arms a 500ms window; the other completes the cycle.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: onChange(state, text) fires on every transition — 'ready',
+ 'armed' (one palm down, 0.5s window), 'running' (1.5s cycle),
+ 'fault' (window expired or same hand twice); initial fire 'ready'.
+ handle: el, press(side) — side is 'L' or 'R' only, anything else is
+ ignored; presses during a running cycle are ignored.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; no gradients registered. */
DUPRE.twoHandSafety = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg press', 160, 100);
@@ -2864,6 +2882,7 @@ DUPRE.twoHandSafety = function (host, opts = {}) {
const flash = cap => { cap.setAttribute('fill', '#e0523a'); setTimeout(() => cap.setAttribute('fill', '#8f2416'), 200); };
const fault = () => { armed = null; clearTimeout(armT); onChange('fault', 'TIE-DOWN FAULT · release and retry'); };
const press = side => {
+ if (side !== 'L' && side !== 'R') return;
if (busy) return;
if (armed === null) {
armed = side; flash(side === 'L' ? L.cap : R.cap);
@@ -2887,19 +2906,33 @@ DUPRE.twoHandSafety = function (host, opts = {}) {
return { el: s, press };
};
-/* R51 voice-loop keyset — independent monitor states, exclusive talk, activity flicker */
+/* R51 voice-loop keyset — independent monitor states, exclusive talk, activity flicker.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: loops (array of key labels; the default 8-loop set also gets a
+ legible demo state — FD + A/G monitored, GNC talking — while a
+ caller's own set starts all-idle); onChange(states, text) fires
+ on every click and the initial paint — states is an array of
+ '0' idle / '1' monitored / '2' talking, one per loop.
+ handle: el, get() (the states array). Click cycles idle → monitored →
+ talking → idle; talk is exclusive across keys. Activity
+ flicker is display-side and honors prefers-reduced-motion.
+ CSS lives in the "voice-loop keyset" block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.voiceLoop = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const LOOPS = opts.loops || ['FD', 'GNC', 'ECOM', 'SURG', 'A/G', 'NET1', 'NET2', 'PAO'];
- const el = document.createElement('div'); el.className = 'vloop'; host.appendChild(el);
+ const el = document.createElement('div'); el.className = 'dupre-vloop'; host.appendChild(el);
const keys = LOOPS.map(l => {
- const k = document.createElement('div'); k.className = 'vk';
- k.innerHTML = l + '<span class="bar"></span>'; k.dataset.state = '0'; el.appendChild(k); return k;
+ const k = document.createElement('div'); k.className = 'dupre-vk';
+ k.innerHTML = l + '<span class="dupre-vk-bar"></span>'; k.dataset.state = '0'; el.appendChild(k); return k;
});
- /* a legible default: FD + A/G monitored, GNC talking */
- keys[0].dataset.state = '1'; keys[0].classList.add('mon');
- keys[4].dataset.state = '1'; keys[4].classList.add('mon');
- keys[1].dataset.state = '2'; keys[1].classList.add('mon', 'tlk');
+ if (!opts.loops) {
+ /* a legible default: FD + A/G monitored, GNC talking */
+ keys[0].dataset.state = '1'; keys[0].classList.add('dupre-mon');
+ keys[4].dataset.state = '1'; keys[4].classList.add('dupre-mon');
+ keys[1].dataset.state = '2'; keys[1].classList.add('dupre-mon', 'dupre-tlk');
+ }
const refresh = () => {
const mon = keys.filter(k => k.dataset.state !== '0').length;
const tlk = keys.findIndex(k => k.dataset.state === '2');
@@ -2907,20 +2940,20 @@ DUPRE.voiceLoop = function (host, opts = {}) {
};
keys.forEach(k => k.addEventListener('click', () => {
const st = k.dataset.state;
- if (st === '0') { k.dataset.state = '1'; k.classList.add('mon'); }
+ if (st === '0') { k.dataset.state = '1'; k.classList.add('dupre-mon'); }
else if (st === '1') {
- keys.forEach(o => { if (o.dataset.state === '2') { o.dataset.state = '1'; o.classList.remove('tlk'); } });
- k.dataset.state = '2'; k.classList.add('tlk');
+ keys.forEach(o => { if (o.dataset.state === '2') { o.dataset.state = '1'; o.classList.remove('dupre-tlk'); } });
+ k.dataset.state = '2'; k.classList.add('dupre-tlk');
}
- else { k.dataset.state = '0'; k.classList.remove('mon', 'tlk', 'act'); }
+ else { k.dataset.state = '0'; k.classList.remove('dupre-mon', 'dupre-tlk', 'dupre-act'); }
refresh();
}));
if (!matchMedia('(prefers-reduced-motion: reduce)').matches)
setInterval(() => {
const t = performance.now() / 1000;
keys.forEach((k, i) => {
- if (k.dataset.state === '0') { k.classList.remove('act'); return; }
- k.classList.toggle('act', Math.sin(t * (1.1 + i * 0.37) + i * 2.1) > 0.55);
+ if (k.dataset.state === '0') { k.classList.remove('dupre-act'); return; }
+ k.classList.toggle('dupre-act', Math.sin(t * (1.1 + i * 0.37) + i * 2.1) > 0.55);
});
}, 300);
refresh();
@@ -3000,18 +3033,27 @@ DUPRE.vuPair = function (host, opts = {}) {
return { el, get: () => [lv, rv], set };
};
-/* 12 mini 4-bar signal — compact activity meter; set(level) */
+/* 12 mini 4-bar signal — compact activity meter; set(level).
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: onChange(level, 'N%') fires on every set (level 0-1, clamped;
+ readout in percent). No initial fire — the page's tick drives it
+ (the tick contract — the page owns the clock and the signal).
+ handle: el, get() (last level set), set(level). Bars light bottom-up:
+ dupre-on, third bar dupre-hot, top bar dupre-clip.
+ CSS lives in the "mini 4-bar signal" block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.miniSig = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const el = document.createElement('span'); el.className = 'sig';
+ const el = document.createElement('span'); el.className = 'dupre-sig';
el.innerHTML = '<i></i><i></i><i></i><i></i>';
host.appendChild(el);
let v = 0;
function set(l) {
- v = 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') : '';
- onChange(l, Math.round(l * 100) + '%');
+ v = Math.max(0, Math.min(1, l));
+ const b = el.children, lit = Math.round(v * 4);
+ for (let k = 0; k < 4; k++) b[k].className = k < lit ? (k >= 3 ? 'dupre-clip' : k >= 2 ? 'dupre-hot' : 'dupre-on') : '';
+ onChange(v, Math.round(v * 100) + '%');
}
return { el, get: () => v, set };
};
@@ -3042,36 +3084,53 @@ DUPRE.signalLadder = function (host, opts = {}) {
return { el, get: () => v, set };
};
-/* 14 linear fuel bar — one 0-100 bar, warn tint under the threshold; drag to set */
+/* 14 linear fuel bar — one 0-100 bar, warn tint under the threshold; drag to set.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (0-100, default 50); warnAt (threshold, default 20 — below
+ it the fill tints dupre-warn); onChange(value, 'N%') fires on
+ every set, including the initial paint.
+ handle: el, get() (current value), set(v) — clamped to 0-100; drag
+ along the bar sets by pointer position.
+ CSS lives in the "linear progress / fuel bar" block of DUPRE_CSS; no
+ other styles involved. */
DUPRE.fuelBar = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const warnAt = opts.warnAt ?? 20;
let v = opts.value ?? 50;
- const el = document.createElement('div'); el.className = 'bar';
+ const el = document.createElement('div'); el.className = 'dupre-bar';
el.innerHTML = '<span></span>';
host.appendChild(el);
const fill = el.querySelector('span');
function set(p) {
- v = p;
- fill.style.width = p + '%';
- el.classList.toggle('warn', p < warnAt);
- onChange(p, Math.round(p) + '%');
+ v = Math.max(0, Math.min(100, p));
+ fill.style.width = v + '%';
+ el.classList.toggle('dupre-warn', v < warnAt);
+ onChange(v, Math.round(v) + '%');
}
dragX(el, set);
set(v);
return { el, get: () => v, set };
};
-/* 15 radial ring — percentage donut; drag up/down to set */
+/* 15 radial ring — percentage donut; drag up/down to set.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (0-100, default 68); onChange(value, 'N%') fires on every
+ set, including the initial paint.
+ handle: el, get() (current value), set(v) — clamped to 0-100; the fill
+ is a conic gradient driven by the element-scoped --p property.
+ CSS lives in the "radial ring" block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.radialRing = function (host, opts = {}) {
const onChange = opts.onChange || noop;
let v = opts.value ?? 68;
- const el = document.createElement('span'); el.className = 'ring';
+ const el = document.createElement('span'); el.className = 'dupre-ring';
el.innerHTML = '<b></b>';
host.appendChild(el);
const num = el.querySelector('b');
function set(nv) {
- v = nv;
+ v = Math.max(0, Math.min(100, nv));
el.style.setProperty('--p', v);
num.textContent = Math.round(v);
onChange(v, Math.round(v) + '%');
@@ -3081,39 +3140,63 @@ DUPRE.radialRing = function (host, opts = {}) {
return { el, get: () => v, set };
};
-/* 16 sparkline — rolling history trace; push(v) appends a sample, fill(v) levels it */
+/* 16 sparkline — rolling history trace; push(v) appends a sample, fill(v) levels it.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: samples (history length, default 40, floor 2 — the trace needs
+ two points); value (initial level, default 0.5); onChange(last,
+ 'N') fires on every repaint (samples 0-1, clamped on entry;
+ readout 0-100). No initial fire — the page's tick drives it (the
+ tick contract — the page owns the clock and the signal). The
+ history buffer is display-side state owned in here.
+ handle: el, get() (newest sample), push(v) (append, oldest drops),
+ fill(v) (level the whole buffer).
+ CSS lives in the "sparkline" block of DUPRE_CSS; the trace colour is an
+ inline stroke, no other styles involved. */
DUPRE.sparkline = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const n = opts.samples || 40;
- const hist = Array.from({ length: n }, () => opts.value ?? 0.5);
- const el = document.createElement('span'); el.className = 'spark';
+ const n = Math.max(2, opts.samples || 40);
+ const clamp = x => Math.max(0, Math.min(1, x));
+ const hist = Array.from({ length: n }, () => clamp(opts.value ?? 0.5));
+ const el = document.createElement('span'); el.className = 'dupre-spark';
el.innerHTML = '<svg viewBox="0 0 170 44" preserveAspectRatio="none"><polyline fill="none" stroke="var(--gold-hi)" stroke-width="1.5"/></svg>';
host.appendChild(el);
const line = el.querySelector('polyline');
- const clamp = x => Math.max(0, Math.min(1, x));
function paint() {
- line.setAttribute('points', hist.map((v, i) => `${i / (n - 1) * 170},${44 - clamp(v) * 40 - 2}`).join(' '));
- onChange(hist[n - 1], String(Math.round(clamp(hist[n - 1]) * 100)));
+ line.setAttribute('points', hist.map((v, i) => `${i / (n - 1) * 170},${44 - v * 40 - 2}`).join(' '));
+ onChange(hist[n - 1], String(Math.round(hist[n - 1] * 100)));
}
- function push(v) { hist.push(v); hist.shift(); paint(); }
- function fill(v) { hist.fill(v); paint(); }
+ function push(v) { hist.push(clamp(v)); hist.shift(); paint(); }
+ function fill(v) { hist.fill(clamp(v)); paint(); }
return { el, get: () => hist[n - 1], push, fill };
};
-/* 17 waveform strip — sampled trace; set(samples, amp) with samples in -1..1 */
+/* 17 waveform strip — sampled trace; set(samples, amp) with samples in -1..1.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: amp (initial amplitude for the readout, default 0.6);
+ onChange(amp, 'amp N%') fires on every set, including the
+ initial paint (a flat centre line).
+ handle: el, get() (last amp), set(samples, amp) — samples in -1..1,
+ clamped per sample; fewer than two samples draws the flat
+ centre line. Amp is readout-only; the trace height comes from
+ the samples themselves.
+ CSS lives in the "waveform strip" block of DUPRE_CSS; the trace colour
+ is an inline stroke, no other styles involved. */
DUPRE.waveStrip = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const el = document.createElement('span'); el.className = 'wave';
+ const el = document.createElement('span'); el.className = 'dupre-wave';
el.innerHTML = '<svg viewBox="0 0 170 38" preserveAspectRatio="none"><path fill="none" stroke="var(--gold)" stroke-width="1.2"/></svg>';
host.appendChild(el);
const path = el.querySelector('path');
+ const clamp = x => Math.max(-1, Math.min(1, x));
let amp = 0;
function set(samples, a) {
amp = a;
let d = 'M0 19';
if (samples.length < 2) d += ' L170 19';
else for (let i = 0; i < samples.length; i++)
- d += ` L${(i / (samples.length - 1) * 170).toFixed(1)} ${(19 + samples[i] * 14).toFixed(1)}`;
+ d += ` L${(i / (samples.length - 1) * 170).toFixed(1)} ${(19 + clamp(samples[i]) * 14).toFixed(1)}`;
path.setAttribute('d', d);
onChange(amp, 'amp ' + Math.round(amp * 100) + '%');
}
@@ -3121,31 +3204,54 @@ DUPRE.waveStrip = function (host, opts = {}) {
return { el, get: () => amp, set };
};
-/* N11 oscilloscope — sampled phosphor trace; set(samples, vpp) with samples in -1..1 */
+/* N11 oscilloscope — sampled phosphor trace; set(samples, vpp) with samples in -1..1.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: onChange(vpp, 'Vpp N') fires on every set (readout is vpp x100).
+ No initial fire — the page's tick drives it (the tick contract —
+ the page owns the clock and the signal).
+ handle: el, get() (last vpp), set(samples, vpp) — samples in -1..1,
+ clamped per sample; fewer than two samples clears the trace.
+ Vpp is readout-only; the trace comes from the samples.
+ CSS lives in the "oscilloscope" block of DUPRE_CSS; the screen-family
+ --scr-* vars retint it, with the original green as fallback. */
DUPRE.scope = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const el = document.createElement('span'); el.className = 'scope';
- el.innerHTML = '<span class="grat"></span><svg viewBox="0 0 176 78" preserveAspectRatio="none"><polyline/></svg>';
+ const el = document.createElement('span'); el.className = 'dupre-scope';
+ el.innerHTML = '<span class="dupre-grat"></span><svg viewBox="0 0 176 78" preserveAspectRatio="none"><polyline/></svg>';
host.appendChild(el);
const line = el.querySelector('polyline');
+ const clamp = x => Math.max(-1, Math.min(1, x));
let vpp = 0;
function set(samples, v) {
vpp = v;
const n = samples.length;
- line.setAttribute('points', samples.map((s, i) => `${(i / (n - 1) * 176).toFixed(1)},${(39 + s * 22).toFixed(1)}`).join(' '));
+ line.setAttribute('points', n < 2 ? '' :
+ samples.map((s, i) => `${(i / (n - 1) * 176).toFixed(1)},${(39 + clamp(s) * 22).toFixed(1)}`).join(' '));
onChange(vpp, 'Vpp ' + Math.round(vpp * 100));
}
return { el, get: () => vpp, set };
};
-/* N12 spectrum / EQ bars — set(values) paints one column per band */
+/* N12 spectrum / EQ bars — set(values) paints one column per band.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: bands (columns, default 11); cells (segments per column, default
+ 9); onChange(values, 'peak N%') fires on every set (values 0-1
+ per band, clamped; a missing band reads 0). No initial fire —
+ the page's tick drives it (the tick contract — the page owns the
+ clock and the signal).
+ handle: el, get() (last values array), set(values). Cells light
+ bottom-up: dupre-on, top three dupre-hot, topmost dupre-clip.
+ CSS lives in the "spectrum / EQ" block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.eqBars = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const bands = opts.bands || 11, cells = opts.cells || 9;
- const el = document.createElement('span'); el.className = 'eq';
+ const el = document.createElement('span'); el.className = 'dupre-eq';
host.appendChild(el);
for (let b = 0; b < bands; b++) {
- const band = document.createElement('span'); band.className = 'band';
+ const band = document.createElement('span'); band.className = 'dupre-band';
for (let s = 0; s < cells; s++) band.appendChild(document.createElement('i'));
el.appendChild(band);
}
@@ -3154,11 +3260,11 @@ DUPRE.eqBars = function (host, opts = {}) {
vals = values;
let peak = 0;
for (let b = 0; b < bands; b++) {
- const col = el.children[b].children, val = values[b], lit = Math.round(val * cells);
+ const col = el.children[b].children, val = Math.max(0, Math.min(1, values[b] || 0)), lit = Math.round(val * cells);
peak = Math.max(peak, val);
for (let k = 0; k < cells; k++) {
let c = '';
- if (k < lit) c = (k >= cells - 1 ? 'clip' : k >= cells - 3 ? 'hot' : 'on');
+ if (k < lit) c = (k >= cells - 1 ? 'dupre-clip' : k >= cells - 3 ? 'dupre-hot' : 'dupre-on');
col[k].className = c;
}
}
@@ -5266,10 +5372,10 @@ const DUPRE_CSS = `
.dupre-vubar i.dupre-clip{opacity:1;background:var(--fail)}.dupre-vubar i.dupre-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)}
+.dupre-sig{display:flex;align-items:flex-end;gap:2px;height:18px}
+.dupre-sig i{width:4px;background:var(--wash);border-radius:1px}
+.dupre-sig i:nth-child(1){height:5px}.dupre-sig i:nth-child(2){height:9px}.dupre-sig i:nth-child(3){height:13px}.dupre-sig i:nth-child(4){height:17px}
+.dupre-sig i.dupre-on{background:var(--pass)}.dupre-sig i.dupre-hot{background:var(--gold)}.dupre-sig i.dupre-clip{background:var(--fail)}
/* signal ladder (wifi bars) */
.dupre-ladder{display:inline-flex;gap:3px;align-items:flex-end;height:18px;cursor:pointer}
@@ -5278,16 +5384,16 @@ const DUPRE_CSS = `
.dupre-ladder i:nth-child(3){height:14px}.dupre-ladder i:nth-child(4){height:18px}
/* linear progress / fuel bar */
-.bar{width:160px;height:12px;background:#0d0f10;border:1px solid #231f18;border-radius:6px;overflow:hidden;position:relative;cursor:pointer;touch-action:none}
-.bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,var(--amber-grad-bot),var(--gold));border-radius:6px}
-.bar.warn>span{background:linear-gradient(90deg,#a35a3f,var(--fail))}
+.dupre-bar{width:160px;height:12px;background:#0d0f10;border:1px solid #231f18;border-radius:6px;overflow:hidden;position:relative;cursor:pointer;touch-action:none}
+.dupre-bar>span{position:absolute;left:0;top:0;bottom:0;background:linear-gradient(90deg,var(--amber-grad-bot),var(--gold));border-radius:6px}
+.dupre-bar.dupre-warn>span{background:linear-gradient(90deg,#a35a3f,var(--fail))}
/* radial ring */
-.ring{width:60px;height:60px;border-radius:50%;cursor:ns-resize;touch-action:none;
+.dupre-ring{width:60px;height:60px;border-radius:50%;cursor:ns-resize;touch-action:none;
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}
+.dupre-ring::before{content:"";position:absolute;inset:6px;border-radius:50%;background:var(--well)}
+.dupre-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;cursor:pointer}
@@ -5295,8 +5401,8 @@ const DUPRE_CSS = `
.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%}
+.dupre-spark{width:170px;height:44px}
+.dupre-spark svg{display:block;width:100%;height:100%}
/* lamp row (list item) */
.dupre-lrow{width:190px;display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:7px;background:#141210;cursor:pointer;font-size:12.5px}
@@ -5323,8 +5429,8 @@ const DUPRE_CSS = `
.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}
+.dupre-wave{width:170px;height:38px}
+.dupre-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;cursor:pointer}
@@ -5471,38 +5577,38 @@ const DUPRE_CSS = `
background:radial-gradient(circle at 40% 35%,#151210,#000);box-shadow:inset 0 1px 2px rgba(0,0,0,.9)}
/* oscilloscope — screen-family vars with the original green as fallback */
-.scope{width:176px;height:78px;border-radius:6px;position:relative;overflow:hidden;
+.dupre-scope{width:176px;height:78px;border-radius:6px;position:relative;overflow:hidden;
background:radial-gradient(circle at 50% 45%,var(--scr-bgc,#04140a),var(--scr-bge,#020a05));
border:1px solid var(--scr-brd,#123018);
box-shadow:inset 0 0 16px rgba(0,0,0,.7)}
-.scope .grat{position:absolute;inset:0;opacity:.5;
+.dupre-scope .dupre-grat{position:absolute;inset:0;opacity:.5;
background-image:linear-gradient(var(--scr-grat,rgba(111,206,51,.18)) 1px,transparent 1px),linear-gradient(90deg,var(--scr-grat,rgba(111,206,51,.18)) 1px,transparent 1px);
background-size:22px 19.5px}
-.scope .grat::after{content:"";position:absolute;left:0;right:0;top:50%;height:1px;background:var(--scr-gratc,rgba(111,206,51,.35))}
-.scope svg{position:absolute;inset:0;width:100%;height:100%}
-.scope polyline{fill:none;stroke:var(--scr-hi,var(--phos));stroke-width:1.6;filter:drop-shadow(0 0 3px var(--scr-glow,rgba(127,224,160,.8)))}
+.dupre-scope .dupre-grat::after{content:"";position:absolute;left:0;right:0;top:50%;height:1px;background:var(--scr-gratc,rgba(111,206,51,.35))}
+.dupre-scope svg{position:absolute;inset:0;width:100%;height:100%}
+.dupre-scope polyline{fill:none;stroke:var(--scr-hi,var(--phos));stroke-width:1.6;filter:drop-shadow(0 0 3px var(--scr-glow,rgba(127,224,160,.8)))}
/* voice-loop keyset */
-.vloop{display:grid;grid-template-columns:repeat(4,52px);gap:4px}
-.vk{display:flex;flex-direction:column;align-items:stretch;gap:4px;font-size:7px;letter-spacing:.05em;
+.dupre-vloop{display:grid;grid-template-columns:repeat(4,52px);gap:4px}
+.dupre-vk{display:flex;flex-direction:column;align-items:stretch;gap:4px;font-size:7px;letter-spacing:.05em;
text-align:center;padding:6px 4px 4px;cursor:pointer;
color:var(--dim);background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-radius:4px;
font-family:var(--mono)}
-.vk .bar{height:3px;width:40px;align-self:center;border-radius:2px;background:#16240f}
-.vk.mon{color:var(--silver)}
-.vk.mon .bar{background:var(--pass);box-shadow:0 0 4px rgba(116,147,47,.6)}
-.vk.mon.act .bar{background:#a8d84a;box-shadow:0 0 7px rgba(150,200,60,.9)}
-.vk.tlk{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));
+.dupre-vk .dupre-vk-bar{height:3px;width:40px;align-self:center;border-radius:2px;background:#16240f}
+.dupre-vk.dupre-mon{color:var(--silver)}
+.dupre-vk.dupre-mon .dupre-vk-bar{background:var(--pass);box-shadow:0 0 4px rgba(116,147,47,.6)}
+.dupre-vk.dupre-mon.dupre-act .dupre-vk-bar{background:#a8d84a;box-shadow:0 0 7px rgba(150,200,60,.9)}
+.dupre-vk.dupre-tlk{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));
border-color:var(--gold-hi);font-weight:700}
-.vk.tlk .bar{background:var(--pass);box-shadow:0 0 4px rgba(116,147,47,.6)}
+.dupre-vk.dupre-tlk .dupre-vk-bar{background:var(--pass);box-shadow:0 0 4px rgba(116,147,47,.6)}
/* spectrum / EQ */
-.eq{display:flex;align-items:flex-end;gap:3px;height:60px}
-.eq .band{width:9px;display:flex;flex-direction:column-reverse;gap:2px;height:100%}
-.eq .band i{height:5px;border-radius:1px;background:var(--wash);opacity:.3}
-.eq .band i.on{opacity:1;background:var(--pass)}
-.eq .band i.hot{opacity:1;background:var(--gold)}
-.eq .band i.clip{opacity:1;background:var(--fail)}
+.dupre-eq{display:flex;align-items:flex-end;gap:3px;height:60px}
+.dupre-eq .dupre-band{width:9px;display:flex;flex-direction:column-reverse;gap:2px;height:100%}
+.dupre-eq .dupre-band i{height:5px;border-radius:1px;background:var(--wash);opacity:.3}
+.dupre-eq .dupre-band i.dupre-on{opacity:1;background:var(--pass)}
+.dupre-eq .dupre-band i.dupre-hot{opacity:1;background:var(--gold)}
+.dupre-eq .dupre-band i.dupre-clip{opacity:1;background:var(--fail)}
/* crossed-needle */
.crossm{width:120px}