aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes')
-rw-r--r--docs/prototypes/widgets.js304
1 files changed, 208 insertions, 96 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js
index 2766353..e6f02ce 100644
--- a/docs/prototypes/widgets.js
+++ b/docs/prototypes/widgets.js
@@ -801,13 +801,13 @@ DUPRE.lampRow = function (host, opts = {}) {
const name = opts.name || 'WH-1000XM4';
const states = opts.states || [['gold', 'tap to connect'], ['busy', 'connecting…'], ['', 'connected']];
const row = document.createElement('div'); row.className = 'lrow';
- row.innerHTML = `<span class="lamp"></span><span class="who"><b></b></span><span class="what"></span>`;
+ row.innerHTML = `<span class="dupre-lamp"></span><span class="who"><b></b></span><span class="what"></span>`;
row.querySelector('b').textContent = name;
host.appendChild(row);
let idx;
const set = i => {
idx = i;
- row.querySelector('.lamp').className = 'lamp ' + states[i][0];
+ row.querySelector('.dupre-lamp').className = 'dupre-lamp' + (states[i][0] ? ' dupre-' + states[i][0] : '');
row.querySelector('.what').textContent = states[i][1];
onChange(i, states[i][1]);
};
@@ -1666,7 +1666,17 @@ DUPRE.waveRegion = function (host, opts = {}) {
return { el: s, get: () => ({ s: S, e: E }), set };
};
-/* R20 drum roller selector — numbered paper drum in a window, center chip reads it */
+/* R20 drum roller selector — numbered paper drum in a window, center chip reads it.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: title (engraved header, default 'EQUALIZER'); channels (array of
+ { name, v } drums, v 1-10, default HIGH/LOW pair — the layout is
+ sized for two); onChange(values array, 'NAME v · NAME v') fires on
+ every set, including the initial paint.
+ handle: el, get() (values array), set(i, v) — v clamps 1-10; each drum
+ drags vertically on its own hit strip.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.drumRoller = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const chans = (opts.channels || [{ name: 'HIGH', v: 6 }, { name: 'LOW', v: 8 }]).map(c => ({ name: c.name, v: c.v }));
@@ -2507,16 +2517,23 @@ DUPRE.voiceLoop = function (host, opts = {}) {
onChange(value, text) like every other builder. Display-side state that
belongs to the instrument (peak-hold, history buffers) lives in here. */
-/* 10 needle gauge — drag up/down sweeps the needle over a 120° arc */
+/* 10 needle gauge — drag up/down sweeps the needle over a 120° arc.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (0-100, default 50); onChange(value, 'value N%') fires on
+ every set, including the initial paint.
+ handle: el, get() (current value), set(v) — no clamping beyond the
+ drag helper's 0-100; the needle maps value linearly to ±60°.
+ CSS lives in the "needle gauge" block of DUPRE_CSS; no other styles involved. */
DUPRE.needleGauge = function (host, opts = {}) {
const onChange = opts.onChange || noop;
let v = opts.value ?? 50;
- const el = document.createElement('div'); el.className = 'gauge';
- el.innerHTML = `<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"></div><div class="hub"></div></div><div class="gv"><span>0</span>%</div>`;
+ const el = document.createElement('div'); el.className = 'dupre-gauge';
+ el.innerHTML = `<div class="dupre-dial"><div class="dupre-arc"></div>
+ <div class="dupre-tk" style="transform:rotate(-60deg)"></div><div class="dupre-tk" style="transform:rotate(0)"></div><div class="dupre-tk" style="transform:rotate(60deg)"></div>
+ <div class="dupre-ndl"></div><div class="dupre-hub"></div></div><div class="dupre-gv"><span>0</span>%</div>`;
host.appendChild(el);
- const ndl = el.querySelector('.ndl'), num = el.querySelector('.gv span');
+ const ndl = el.querySelector('.dupre-ndl'), num = el.querySelector('.dupre-gv span');
function set(nv) {
v = nv;
ndl.style.transform = `rotate(${-60 + v / 100 * 120}deg)`;
@@ -2528,23 +2545,33 @@ DUPRE.needleGauge = function (host, opts = {}) {
return { el, get: () => v, set };
};
-/* 11 stereo VU — two LED bars with peak-hold; set(l, r) drives both */
+/* 11 stereo VU — two LED bars with peak-hold; set(l, r) drives both.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: bars (cells per channel, default 16); onChange([l, r], 'L n · R n')
+ fires on every set (values 0-1, readout in percent).
+ handle: el, get() ([l, r] last set), set(l, r). Peak-hold is display-side
+ state owned in here: each channel's peak cell decays 0.4 cells
+ per set() call, so the page's tick cadence is the decay clock
+ (the tick contract — the page owns the clock and the signal).
+ CSS lives in the "segmented VU / LED bar" block of DUPRE_CSS; no other
+ styles involved. */
DUPRE.vuPair = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const n = opts.bars || 16;
- const el = document.createElement('div'); el.className = 'vu';
- el.innerHTML = `<div class="vurow"><span class="ch">L</span><span class="vubar"></span></div>
- <div class="vurow"><span class="ch">R</span><span class="vubar"></span></div>`;
+ const el = document.createElement('div'); el.className = 'dupre-vu';
+ el.innerHTML = `<div class="dupre-vurow"><span class="dupre-ch">L</span><span class="dupre-vubar"></span></div>
+ <div class="dupre-vurow"><span class="dupre-ch">R</span><span class="dupre-vubar"></span></div>`;
host.appendChild(el);
- const bars = el.querySelectorAll('.vubar');
+ const bars = el.querySelectorAll('.dupre-vubar');
bars.forEach(b => buildBars(b, n));
const pkL = { v: 0 }, pkR = { v: 0 };
function paint(bar, l, pk) {
const b = bar.children, 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';
+ let c = k < lit ? (k >= n - 2 ? 'dupre-clip' : k >= n - 4 ? 'dupre-hot' : 'dupre-on') : '';
+ if (p > 0 && k === p - 1) c = (c ? c + ' ' : '') + 'dupre-peak';
b[k].className = c;
}
}
@@ -2573,11 +2600,19 @@ DUPRE.miniSig = function (host, opts = {}) {
return { el, get: () => v, set };
};
-/* 13 signal ladder — stepped 0-4 strength; click cycles */
+/* 13 signal ladder — stepped 0-4 strength; click cycles.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (0-4, default 3); onChange(v, 'v/4') fires on every set,
+ including the initial paint.
+ handle: el, get() (current step), set(v) — bars at or below v light
+ gold; click advances (v + 1) % 5.
+ CSS lives in the "signal ladder" block of DUPRE_CSS; the lit/unlit bar
+ colours are inline (--gold / --wash), no other styles involved. */
DUPRE.signalLadder = function (host, opts = {}) {
const onChange = opts.onChange || noop;
let v = opts.value ?? 3;
- const el = document.createElement('span'); el.className = 'ladder';
+ const el = document.createElement('span'); el.className = 'dupre-ladder';
el.innerHTML = '<i></i><i></i><i></i><i></i>';
host.appendChild(el);
function set(nv) {
@@ -2895,7 +2930,16 @@ DUPRE.mcVu = function (host, opts = {}) {
return { el: s, get: () => t, set };
};
-/* R07 round panel meter — porthole bezel, same VU law as R01; drag up/down */
+/* R07 round panel meter — porthole bezel, same VU law as R01; drag up/down.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (0-100, default 50); onChange(value, '+x.x dB') fires on
+ every set — the readout maps the linear value through the shared
+ VU law (VUDB/vuDb), like every VU-family meter.
+ handle: el, get() (current value), set(v) — the needle sweeps -40°..+40°
+ over the clamped 0-1 position.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; the face clip is a per-instance defs entry. */
DUPRE.roundMeter = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg drag', 110, 104);
@@ -3501,10 +3545,19 @@ DUPRE.comfortMeter = function (host, opts = {}) {
/* ================= indicators & readouts ================= */
-/* 18 status lamps — one lamp per health state; click any lamp to cycle it */
+/* 18 status lamps — one lamp per health state; click any lamp to cycle it.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: states (array of state indices, default [0,1,2,3,4] — one lamp
+ per entry; 0 ok · 1 engaged · 2 fault · 3 off · 4 busy);
+ onChange(states array copy, state name) fires per change and once
+ at build with a count summary.
+ handle: el, get() (states copy), set(i, st) — st wraps modulo 5.
+ CSS lives in the "shared primitives" .dupre-lamp block of DUPRE_CSS
+ (shared with the output well's step lamps); no other styles involved. */
DUPRE.statusLamps = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const CLS = ['lamp', 'lamp gold', 'lamp red', 'lamp off', 'lamp busy'];
+ const CLS = ['dupre-lamp', 'dupre-lamp dupre-gold', 'dupre-lamp dupre-red', 'dupre-lamp dupre-off', 'dupre-lamp dupre-busy'];
const NAMES = ['ok', 'engaged', 'fault', 'off', 'busy'];
const states = (opts.states || [0, 1, 2, 3, 4]).slice();
const wrap = document.createElement('span');
@@ -3592,7 +3645,7 @@ DUPRE.outputWell = function (host, opts = {}) {
const w = document.createElement('div'); w.className = 'owell'; host.appendChild(w);
const add = s => {
const d = document.createElement('div'); d.className = 'ostep';
- d.innerHTML = `<span class="lamp ${s[0]}"></span><span><b>${s[1]}</b><span class="ev">${s[2]}</span></span>`;
+ d.innerHTML = `<span class="dupre-lamp${s[0] ? ' dupre-' + s[0] : ''}"></span><span><b>${s[1]}</b><span class="ev">${s[2]}</span></span>`;
w.appendChild(d); while (w.children.length > keep) w.removeChild(w.firstChild);
};
seed.forEach(add);
@@ -3621,14 +3674,22 @@ DUPRE.toast = function (host, opts = {}) {
return { el: t, fire };
};
-/* 26 nixie tubes — one lit numeral per tube, leading zeros dark; click increments */
+/* 26 nixie tubes — one lit numeral per tube, leading zeros dark; click increments.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: digits (tube count, default 2); value (initial, default 8);
+ onChange(value, zero-padded string) fires on every set.
+ handle: el, get() (current value), set(v) — wraps modulo 10^digits;
+ leading-zero tubes go dark rather than showing 0, like the
+ hardware.
+ CSS lives in the "nixie tube" block of DUPRE_CSS; no other styles involved. */
DUPRE.nixie = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const digits = opts.digits || 2;
const max = Math.pow(10, digits);
- const nx = document.createElement('span'); nx.className = 'nixie';
+ const nx = document.createElement('span'); nx.className = 'dupre-nixie';
for (let i = 0; i < digits; i++) {
- const tu = document.createElement('span'); tu.className = 'tube'; tu.innerHTML = '<b></b>'; nx.appendChild(tu);
+ const tu = document.createElement('span'); tu.className = 'dupre-tube'; tu.innerHTML = '<b></b>'; nx.appendChild(tu);
}
host.appendChild(nx);
let v;
@@ -3636,7 +3697,7 @@ DUPRE.nixie = function (host, opts = {}) {
v = ((x % max) + max) % max;
const s = String(v).padStart(digits, '0');
[...nx.children].forEach((tu, i) => {
- tu.classList.toggle('off', i < digits - 1 && v < Math.pow(10, digits - 1 - i));
+ tu.classList.toggle('dupre-off', i < digits - 1 && v < Math.pow(10, digits - 1 - i));
tu.querySelector('b').textContent = s[i];
});
onChange(v, s);
@@ -3837,15 +3898,24 @@ DUPRE.splitFlap.STYLES = {
};
/* N21 seven-segment countdown — mm:ss in lit segments; the page drives tick();
- click adds a minute */
+ click adds a minute.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: secs (initial seconds, default 24:10); onChange(secs, 'mm:ss')
+ fires on every set.
+ handle: el, get() (current seconds), set(v) — wraps modulo 3600;
+ tick() steps one second down (the tick contract — the page owns
+ the clock); click adds 60.
+ CSS lives in the "seven-segment" block of DUPRE_CSS; the digit glyphs come
+ from the shared seg7() engine helper (.seg7, shared with the DSKY). */
DUPRE.sevenSeg = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const sv = document.createElement('span'); sv.className = 'seven'; host.appendChild(sv);
+ const sv = document.createElement('span'); sv.className = 'dupre-seven'; host.appendChild(sv);
let secs;
const set = v => {
secs = ((v % 3600) + 3600) % 3600;
const mm = String(Math.floor(secs / 60)).padStart(2, '0'), ss = String(secs % 60).padStart(2, '0');
- sv.innerHTML = seg7(mm[0]) + seg7(mm[1]) + '<span class="colon"><i></i><i></i></span>' + seg7(ss[0]) + seg7(ss[1]);
+ sv.innerHTML = seg7(mm[0]) + seg7(mm[1]) + '<span class="dupre-colon"><i></i><i></i></span>' + seg7(ss[0]) + seg7(ss[1]);
onChange(secs, mm + ':' + ss);
};
sv.addEventListener('click', () => set(secs + 60));
@@ -3853,14 +3923,23 @@ DUPRE.sevenSeg = function (host, opts = {}) {
return { el: sv, get: () => secs, set, tick: () => set(secs - 1) };
};
-/* N22 VFD marquee — teal dot-matrix scroll; the page drives tick(); click cycles the message */
+/* N22 VFD marquee — teal dot-matrix scroll; the page drives tick(); click cycles the message.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: msgs (message pool, default demo set); width (window px, 176);
+ onChange(msg index, 'msg i/n') fires on click-cycle and once at
+ build.
+ handle: el, get() (current message index), tick() — advances the scroll
+ 1.1px and wraps when the text clears the window (the tick
+ contract — the page owns the clock).
+ CSS lives in the "VFD marquee" block of DUPRE_CSS; no other styles involved. */
DUPRE.vfdMarquee = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const msgs = opts.msgs || ['ARCHSETUP · NET OK · BT 2 · SND 62%', 'WIFI @Hyatt · 300 Mbps · VPN UP', 'BATTERY 84% · DISK 61% · TEMP 47C'];
const W = opts.width || 176;
- const m = document.createElement('span'); m.className = 'vfdm';
- m.innerHTML = '<span class="txt"></span><span class="mesh"></span>';
- const t = m.querySelector('.txt'); t.textContent = msgs[0];
+ const m = document.createElement('span'); m.className = 'dupre-vfdm';
+ m.innerHTML = '<span class="dupre-txt"></span><span class="dupre-mesh"></span>';
+ const t = m.querySelector('.dupre-txt'); t.textContent = msgs[0];
host.appendChild(m);
let mi = 0, x = W;
m.addEventListener('click', () => {
@@ -3922,28 +4001,43 @@ DUPRE.annunciator = function (host, opts = {}) {
};
};
-/* N24 jewel pilot lamps — faceted bezel indicators; click cycles red · amber · green · dark */
+/* N24 jewel pilot lamps — faceted bezel indicators; click cycles red · amber · green · dark.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: states (array, one jewel per entry: 0 red · 1 amber · 2 green ·
+ -1 dark; default [0,1,2,-1]); onChange(state, name) fires per
+ change and once at build with a hint.
+ handle: el, get() (states copy), set(i, st) — st clamps to the lens
+ range, negatives dark the jewel. State lives in here; the DOM
+ is a paint of it (was the reverse — the old builder kept state
+ only in classList/--jc, so nothing could read or drive it).
+ CSS lives in the "jewel" block of DUPRE_CSS; no other styles involved. */
DUPRE.jewels = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const cols = ['var(--jewel-r)', 'var(--jewel-a)', 'var(--jewel-g)'];
const NAMES = ['red', 'amber', 'green'];
- const init = opts.states || [0, 1, 2, -1]; /* index into cols; -1 = dark */
+ const states = (opts.states || [0, 1, 2, -1]).slice(); /* index into cols; -1 = dark */
const wrap = document.createElement('span');
wrap.style.cssText = 'display:inline-flex;gap:10px;align-items:center';
- init.forEach(st => {
- const j = document.createElement('span'); j.className = 'jewel' + (st < 0 ? ' dim' : '');
- if (st >= 0) j.style.setProperty('--jc', cols[st]);
- j.addEventListener('click', () => {
- if (j.classList.contains('dim')) { j.classList.remove('dim'); j.style.setProperty('--jc', cols[0]); onChange(0, 'red'); return; }
- const cur = j.style.getPropertyValue('--jc').trim(); const i = cols.indexOf(cur);
- if (i >= cols.length - 1 || i < 0) { j.classList.add('dim'); onChange(-1, 'dark'); }
- else { j.style.setProperty('--jc', cols[i + 1]); onChange(i + 1, NAMES[i + 1]); }
- });
- wrap.appendChild(j);
+ const paint = i => {
+ const st = states[i], j = els[i];
+ j.className = 'dupre-jewel' + (st < 0 ? ' dupre-dim' : '');
+ if (st >= 0) j.style.setProperty('--jc', cols[st]); else j.style.removeProperty('--jc');
+ };
+ function set(i, st) {
+ states[i] = st < 0 ? -1 : Math.min(st, cols.length - 1);
+ paint(i);
+ onChange(states[i], states[i] < 0 ? 'dark' : NAMES[states[i]]);
+ }
+ const els = states.map((_, i) => {
+ const j = document.createElement('span');
+ j.addEventListener('click', () => set(i, states[i] >= cols.length - 1 ? -1 : states[i] + 1));
+ wrap.appendChild(j); return j;
});
host.appendChild(wrap);
+ states.forEach((_, i) => paint(i));
onChange(null, 'click to cycle');
- return { el: wrap };
+ return { el: wrap, get: () => states.slice(), set };
};
/* N25 tape counter — odometer wheels; set(total) rolls each wheel to its digit */
@@ -3970,14 +4064,22 @@ DUPRE.tapeCounter = function (host, opts = {}) {
};
/* N26 analog clock — engraved ticks + three hands; the page owns the time source
- and drives set(h, m, s); silent until the first set, like the live original */
+ and drives set(h, m, s); silent until the first set, like the live original.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: onChange([h, m, s], 'hh:mm:ss') fires on every set.
+ handle: el, set(h, m, s) — the minute hand carries a seconds fraction
+ and the hour hand a minutes fraction, so the face reads like a
+ real movement. Display-only: no state to get, no clock of its
+ own (the tick contract — the page owns the time source).
+ CSS lives in the "analog clock" block of DUPRE_CSS; no other styles involved. */
DUPRE.analogClock = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const cl = document.createElement('span'); cl.className = 'clock';
- for (let i = 0; i < 12; i++) { const t = document.createElement('span'); t.className = 'tk'; t.style.transform = `rotate(${i * 30}deg)`; cl.appendChild(t); }
- cl.insertAdjacentHTML('beforeend', '<span class="hh"></span><span class="mh"></span><span class="sh"></span><span class="pin"></span>');
+ const cl = document.createElement('span'); cl.className = 'dupre-clock';
+ for (let i = 0; i < 12; i++) { const t = document.createElement('span'); t.className = 'dupre-tk'; t.style.transform = `rotate(${i * 30}deg)`; cl.appendChild(t); }
+ cl.insertAdjacentHTML('beforeend', '<span class="dupre-hh"></span><span class="dupre-mh"></span><span class="dupre-sh"></span><span class="dupre-pin"></span>');
host.appendChild(cl);
- const hh = cl.querySelector('.hh'), mh = cl.querySelector('.mh'), sh = cl.querySelector('.sh');
+ const hh = cl.querySelector('.dupre-hh'), mh = cl.querySelector('.dupre-mh'), sh = cl.querySelector('.dupre-sh');
const set = (h, m, s) => {
sh.style.transform = `rotate(${s * 6}deg)`;
mh.style.transform = `rotate(${m * 6 + s * 0.1}deg)`;
@@ -4378,7 +4480,17 @@ DUPRE.radarSweep = function (host, opts = {}) {
};
/* R35 day-date disc calendar — coaxial printed discs rotate so today reads
- under the fixed hand; starts on today; click rolls midnight forward */
+ under the fixed hand; starts on today; click rolls midnight forward.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: date (1-31) and day (0-6, SUN=0), both defaulting to today;
+ onChange([date, day], 'DAY d') fires on every set, including the
+ initial paint.
+ handle: el, get() ([date, day]), set(d, w) — rotates both discs so the
+ commanded pair reads under the fixed hand; click advances date
+ and weekday together like a midnight rollover.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.dayDateCal = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const DAYS = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
@@ -4641,11 +4753,11 @@ DUPRE.blinkenlights = function (host, opts = {}) {
/* instrument-internal CSS (moved from the gallery <style> block; gallery keeps page furniture) */
const DUPRE_CSS = `
/* ---- 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(var(--glow-lo),.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 var(--pulse-rate) ease-in-out infinite}
+.dupre-lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);box-shadow:0 0 6px 1px rgba(116,147,47,.55)}
+.dupre-lamp.dupre-gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(var(--glow-lo),.6)}
+.dupre-lamp.dupre-red{background:var(--fail);box-shadow:0 0 6px 1px rgba(203,107,77,.55)}
+.dupre-lamp.dupre-off{background:var(--wash);box-shadow:none}
+.dupre-lamp.dupre-busy{background:var(--gold);animation:pulse var(--pulse-rate) ease-in-out infinite}
/* standard pulse: 1s ease-in-out — the norm for every pulsing / flashing element */
@keyframes pulse{50%{opacity:.25}}
@@ -4715,24 +4827,24 @@ const DUPRE_CSS = `
margin-left:-1px;transform-origin:50% 21px;border-radius:1px;box-shadow:0 0 5px rgba(var(--glow-hi),.6)}
/* needle gauge */
-.gauge{width:96px;cursor:ns-resize;touch-action:none}
-.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%;top:1px;width:1.5px;height:8px;margin-left:-.75px;background:var(--steel);transform-origin:50% 47px}
-.gauge .ndl{position:absolute;left:50%;bottom:0;width:2px;height:40px;background:var(--gold-hi);
+.dupre-gauge{width:96px;cursor:ns-resize;touch-action:none}
+.dupre-gauge .dupre-dial{position:relative;height:48px;overflow:hidden}
+.dupre-gauge .dupre-arc{position:absolute;inset:0 0 -48px 0;border:2px solid var(--wash);border-radius:50%}
+.dupre-gauge .dupre-tk{position:absolute;left:50%;top:1px;width:1.5px;height:8px;margin-left:-.75px;background:var(--steel);transform-origin:50% 47px}
+.dupre-gauge .dupre-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(var(--glow-hi),.5);
transition:transform .12s 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}
+.dupre-gauge .dupre-hub{position:absolute;left:50%;bottom:-4px;width:8px;height:8px;margin-left:-4px;border-radius:50%;background:var(--gold)}
+.dupre-gauge .dupre-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}
+.dupre-vu{width:170px;display:flex;flex-direction:column;gap:5px}
+.dupre-vurow{display:flex;align-items:center;gap:7px}
+.dupre-vurow .dupre-ch{color:var(--steel);font-size:.6rem;width:8px}
+.dupre-vubar{flex:1;display:flex;gap:2px;height:9px}
+.dupre-vubar i{flex:1;background:var(--wash);border-radius:1px;opacity:.3}
+.dupre-vubar i.dupre-on{opacity:1;background:var(--pass)}.dupre-vubar i.dupre-hot{opacity:1;background:var(--gold)}
+.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}
@@ -4741,10 +4853,10 @@ const DUPRE_CSS = `
.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;cursor:pointer}
-.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}
+.dupre-ladder{display:inline-flex;gap:3px;align-items:flex-end;height:18px;cursor:pointer}
+.dupre-ladder i{width:5px;background:var(--wash);border-radius:1px}
+.dupre-ladder i:nth-child(1){height:6px}.dupre-ladder i:nth-child(2){height:10px}
+.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}
@@ -4801,7 +4913,7 @@ const DUPRE_CSS = `
/* output well (log step) */
.owell{width:200px;background:var(--well);border:1px solid var(--wash);border-radius:8px;padding:7px 9px;font-size:11px;cursor:pointer}
.ostep{display:flex;gap:7px;align-items:flex-start;padding:2px 0}
-.ostep .lamp{margin-top:3px;width:7px;height:7px}
+.ostep .dupre-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}
/* rotary selector */
@@ -4824,14 +4936,14 @@ const DUPRE_CSS = `
background:var(--tn-ndl,var(--fail));box-shadow:0 0 7px var(--tn-ndlglow,rgba(203,107,77,.85));transition:left .25s}
/* nixie tube */
-.nixie{display:inline-flex;gap:5px;cursor:pointer}
-.nixie .tube{width:30px;height:44px;border-radius:5px;position:relative;overflow:hidden;
+.dupre-nixie{display:inline-flex;gap:5px;cursor:pointer}
+.dupre-nixie .dupre-tube{width:30px;height:44px;border-radius:5px;position:relative;overflow:hidden;
background:radial-gradient(circle at 50% 38%,#241a12,#0b0807);border:1px solid #2c261d;
display:grid;place-items:center;box-shadow:inset 0 0 12px rgba(0,0,0,.6)}
-.nixie .tube b{font-size:26px;font-weight:400;color:#ff9a3c;
+.dupre-nixie .dupre-tube b{font-size:26px;font-weight:400;color:#ff9a3c;
text-shadow:0 0 6px rgba(255,140,50,.85),0 0 15px rgba(255,120,40,.45)}
-.nixie .tube.off b{color:#3a2a1c;text-shadow:none}
-.nixie .tube::after{content:"";position:absolute;inset:0;pointer-events:none;
+.dupre-nixie .dupre-tube.dupre-off b{color:#3a2a1c;text-shadow:none}
+.dupre-nixie .dupre-tube::after{content:"";position:absolute;inset:0;pointer-events:none;
background:linear-gradient(180deg,rgba(255,255,255,.06),transparent 32%)}
/* ===== candidate-unique CSS ===== */
@@ -5059,12 +5171,12 @@ const DUPRE_CSS = `
.flap-light .flapd::after{background:rgba(0,0,0,.3);box-shadow:0 1px 0 rgba(255,255,255,.3)}
/* seven-segment */
-.seven{display:inline-flex;gap:6px;padding:6px 9px;border-radius:6px;background:#0a0806;border:1px solid #231f18;cursor:pointer;
+.dupre-seven{display:inline-flex;gap:6px;padding:6px 9px;border-radius:6px;background:#0a0806;border:1px solid #231f18;cursor:pointer;
box-shadow:inset 0 0 10px rgba(0,0,0,.6)}
.seg7{width:22px;height:40px;filter:drop-shadow(0 0 3px rgba(87,211,87,.55))}
.seg7.red{filter:drop-shadow(0 0 3px rgba(226,84,63,.55))}
-.seven .colon{align-self:center;display:flex;flex-direction:column;gap:9px}
-.seven .colon i{width:4px;height:4px;border-radius:50%;background:var(--sevgrn);box-shadow:0 0 4px rgba(87,211,87,.7)}
+.dupre-seven .dupre-colon{align-self:center;display:flex;flex-direction:column;gap:9px}
+.dupre-seven .dupre-colon i{width:4px;height:4px;border-radius:50%;background:var(--sevgrn);box-shadow:0 0 4px rgba(87,211,87,.7)}
/* DSKY verb/noun panel */
.dsky{display:flex;gap:9px;align-items:stretch;background:#1c1916;border:1px solid #060505;border-radius:8px;padding:8px}
@@ -5084,11 +5196,11 @@ const DUPRE_CSS = `
.dsky .pad .key{padding:4px 0;font-size:8.5px;border-radius:5px;text-align:center;letter-spacing:.03em}
/* VFD marquee */
-.vfdm{width:176px;height:34px;border-radius:5px;overflow:hidden;position:relative;cursor:pointer;
+.dupre-vfdm{width:176px;height:34px;border-radius:5px;overflow:hidden;position:relative;cursor:pointer;
background:linear-gradient(180deg,#04100e,#020807);border:1px solid #123028;box-shadow:inset 0 0 12px rgba(0,0,0,.6)}
-.vfdm .txt{position:absolute;top:50%;transform:translateY(-50%);white-space:nowrap;font-size:15px;letter-spacing:.22em;
+.dupre-vfdm .dupre-txt{position:absolute;top:50%;transform:translateY(-50%);white-space:nowrap;font-size:15px;letter-spacing:.22em;
color:var(--vfd);text-shadow:0 0 6px rgba(99,230,200,.65)}
-.vfdm .mesh{position:absolute;inset:0;pointer-events:none;opacity:.35;
+.dupre-vfdm .dupre-mesh{position:absolute;inset:0;pointer-events:none;opacity:.35;
background-image:radial-gradient(rgba(0,0,0,.6) 40%,transparent 41%);background-size:3px 3px}
/* annunciator */
@@ -5107,13 +5219,13 @@ const DUPRE_CSS = `
.acell.fault{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));font-weight:700;box-shadow:0 0 8px rgba(203,107,77,.4);animation:pulse var(--pulse-rate) ease-in-out infinite}
/* jewel */
-.jewel{width:24px;height:24px;border-radius:50%;position:relative;cursor:pointer;
+.dupre-jewel{width:24px;height:24px;border-radius:50%;position:relative;cursor:pointer;
background:radial-gradient(circle at 36% 30%,rgba(255,255,255,.75),var(--jc) 42%,#3a0d08 100%);
box-shadow:0 0 10px 1px var(--jc),inset 0 -2px 3px rgba(0,0,0,.5)}
-.jewel::after{content:"";position:absolute;inset:0;border-radius:50%;
+.dupre-jewel::after{content:"";position:absolute;inset:0;border-radius:50%;
background:conic-gradient(from 0deg,rgba(255,255,255,.12) 0 22deg,transparent 22deg 45deg,rgba(0,0,0,.18) 45deg 67deg,transparent 67deg 90deg);
background-repeat:repeat}
-.jewel.dim{background:radial-gradient(circle at 36% 30%,rgba(120,120,120,.3),#241f1b 55%,#0e0c0a);box-shadow:inset 0 -2px 3px rgba(0,0,0,.5)}
+.dupre-jewel.dupre-dim{background:radial-gradient(circle at 36% 30%,rgba(120,120,120,.3),#241f1b 55%,#0e0c0a);box-shadow:inset 0 -2px 3px rgba(0,0,0,.5)}
/* tape counter */
.counter{display:inline-flex;gap:2px;padding:4px;background:#0c0b0a;border:1px solid #2c261d;border-radius:4px;
@@ -5129,16 +5241,16 @@ const DUPRE_CSS = `
.counter .redw{background:linear-gradient(180deg,#e7b46a,#cf9440)}
/* analog clock */
-.clock{width:78px;height:78px;border-radius:50%;position:relative;
+.dupre-clock{width:78px;height:78px;border-radius:50%;position:relative;
background:radial-gradient(circle at 46% 40%,#1c1915,#0c0b09);border:2px solid #2c261d;box-shadow:inset 0 2px 5px rgba(0,0,0,.6)}
-.clock .tk{position:absolute;left:50%;top:4px;width:1.5px;height:6px;background:var(--steel);margin-left:-.75px;transform-origin:50% 35px}
-.clock .hh{position:absolute;left:50%;top:50%;width:3px;height:20px;margin:-20px 0 0 -1.5px;background:var(--cream);
+.dupre-clock .dupre-tk{position:absolute;left:50%;top:4px;width:1.5px;height:6px;background:var(--steel);margin-left:-.75px;transform-origin:50% 35px}
+.dupre-clock .dupre-hh{position:absolute;left:50%;top:50%;width:3px;height:20px;margin:-20px 0 0 -1.5px;background:var(--cream);
transform-origin:50% 100%;border-radius:2px}
-.clock .mh{position:absolute;left:50%;top:50%;width:2px;height:28px;margin:-28px 0 0 -1px;background:var(--silver);
+.dupre-clock .dupre-mh{position:absolute;left:50%;top:50%;width:2px;height:28px;margin:-28px 0 0 -1px;background:var(--silver);
transform-origin:50% 100%;border-radius:2px}
-.clock .sh{position:absolute;left:50%;top:50%;width:1px;height:30px;margin:-30px 0 0 -.5px;background:var(--gold-hi);
+.dupre-clock .dupre-sh{position:absolute;left:50%;top:50%;width:1px;height:30px;margin:-30px 0 0 -.5px;background:var(--gold-hi);
transform-origin:50% 100%;box-shadow:0 0 4px rgba(var(--glow-hi),.5)}
-.clock .pin{position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--gold)}
+.dupre-clock .dupre-pin{position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--gold)}
/* frequency-dial scale */
.freqscale{width:184px;height:44px;position:relative;border-radius:5px;overflow:hidden;cursor:pointer;touch-action:none;