diff options
Diffstat (limited to 'docs/prototypes')
| -rw-r--r-- | docs/prototypes/widgets.js | 247 |
1 files changed, 170 insertions, 77 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index 6477046..dc37bd4 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -3273,17 +3273,26 @@ DUPRE.eqBars = function (host, opts = {}) { return { el, get: () => vals, set }; }; -/* N13 crossed-needle meter — one drive value, FWD and RFL needles cross */ +/* N13 crossed-needle meter — one drive value, FWD and RFL needles cross. + + Contract (everything a consumer needs; no page globals touched): + opts: value (0-100, default 55); onChange(value, 'FWD n · RFL n') fires + on every set — RFL derives from the drive value (0.68×), the way a + crossed-needle SWR face couples the two. + handle: el, get() (current value), set(v) — clamped to 0-100; drag on + the face sets by vertical delta. + CSS lives in the "crossed-needle" block of DUPRE_CSS; no other styles + involved. */ DUPRE.crossNeedle = function (host, opts = {}) { const onChange = opts.onChange || noop; let v = opts.value ?? 55; - const el = document.createElement('div'); el.className = 'crossm'; - el.innerHTML = `<div class="face"><div class="arc"></div><div class="nA"></div><div class="nB"></div></div> - <div class="lbl"><span>FWD</span><span>RFL</span></div>`; + const el = document.createElement('div'); el.className = 'dupre-crossm'; + el.innerHTML = `<div class="dupre-face"><div class="dupre-crossm-arc"></div><div class="dupre-fwd"></div><div class="dupre-rfl"></div></div> + <div class="dupre-lbl"><span>FWD</span><span>RFL</span></div>`; host.appendChild(el); - const face = el.querySelector('.face'), nA = el.querySelector('.nA'), nB = el.querySelector('.nB'); + const face = el.querySelector('.dupre-face'), nA = el.querySelector('.dupre-fwd'), nB = el.querySelector('.dupre-rfl'); function set(nv) { - v = nv; + v = Math.max(0, Math.min(100, nv)); const fwd = v, rfl = v * 0.68; nA.style.transform = `rotate(${-42 + fwd / 100 * 84}deg)`; nB.style.transform = `rotate(${42 - rfl / 100 * 84}deg)`; @@ -3294,17 +3303,25 @@ DUPRE.crossNeedle = function (host, opts = {}) { return { el, get: () => v, set }; }; -/* N14 thermometer column — mercury rises with the value */ +/* N14 thermometer column — mercury rises with the value. + + Contract (everything a consumer needs; no page globals touched): + opts: value (0-100, default 58); onChange(value, 'N°') fires on every + set — the readout maps 0-100 onto the printed 30-90° scale. + handle: el, get() (current value), set(v) — clamped to 0-100; drag + anywhere on the column sets by vertical delta. + CSS lives in the "thermometer" block of DUPRE_CSS; no other styles + involved. */ DUPRE.thermometer = function (host, opts = {}) { const onChange = opts.onChange || noop; let v = opts.value ?? 58; - const el = document.createElement('div'); el.className = 'thermo'; - el.innerHTML = `<div class="scale"><span>90</span><span>60</span><span>30</span></div> - <div class="wrapcol"><div class="tube"><div class="fill"></div></div><div class="bulb"></div></div>`; + const el = document.createElement('div'); el.className = 'dupre-thermo'; + el.innerHTML = `<div class="dupre-scale"><span>90</span><span>60</span><span>30</span></div> + <div class="dupre-wrapcol"><div class="dupre-thermo-tube"><div class="dupre-thermo-fill"></div></div><div class="dupre-bulb"></div></div>`; host.appendChild(el); - const fill = el.querySelector('.fill'); + const fill = el.querySelector('.dupre-thermo-fill'); function set(nv) { - v = nv; + v = Math.max(0, Math.min(100, nv)); fill.style.height = v + '%'; onChange(v, Math.round(30 + v / 100 * 60) + '°'); } @@ -3313,18 +3330,27 @@ DUPRE.thermometer = function (host, opts = {}) { return { el, get: () => v, set }; }; -/* N15 bourdon pressure gauge — needle over a printed arc with a redline */ +/* N15 bourdon pressure gauge — needle over a printed arc with a redline. + + Contract (everything a consumer needs; no page globals touched): + opts: value (0-100, default 45); onChange(value, 'N PSI') fires on every + set — the readout maps 0-100 onto a 0-160 PSI scale. + handle: el, get() (current value), set(v) — clamped to 0-100, the + needle sweeps -60°..+60°; drag anywhere on the dial sets by + vertical delta. The printed arc and redline are the builder's + own inline SVG. + CSS lives in the "bourdon" block of DUPRE_CSS; no other styles involved. */ DUPRE.bourdon = function (host, opts = {}) { const onChange = opts.onChange || noop; let v = opts.value ?? 45; - const el = document.createElement('div'); el.className = 'bourdon'; + const el = document.createElement('div'); el.className = 'dupre-bourdon'; el.innerHTML = `<svg viewBox="0 0 82 82"><path d="M14 62 A34 34 0 0 1 68 62" fill="none" stroke="#2c2f32" stroke-width="3"/> <path d="M52 22 A34 34 0 0 1 68 62" fill="none" stroke="#cb6b4d" stroke-width="3"/></svg> - <div class="ndl"></div><div class="hub"></div><div class="cap">PSI</div>`; + <div class="dupre-bourdon-ndl"></div><div class="dupre-bourdon-hub"></div><div class="dupre-bourdon-cap">PSI</div>`; host.appendChild(el); - const ndl = el.querySelector('.ndl'); + const ndl = el.querySelector('.dupre-bourdon-ndl'); function set(nv) { - v = nv; + v = Math.max(0, Math.min(100, nv)); ndl.style.transform = `rotate(${-60 + v / 100 * 120}deg)`; onChange(v, Math.round(v / 100 * 160) + ' PSI'); } @@ -3333,16 +3359,25 @@ DUPRE.bourdon = function (host, opts = {}) { return { el, get: () => v, set }; }; -/* N16 strip-chart recorder — scrolling history, pen rides the newest value; - push(v) appends, set(samples, current) replaces the trace (values 0..1) */ +/* N16 strip-chart recorder — scrolling history, pen rides the newest value. + + Contract (everything a consumer needs; no page globals touched): + opts: samples (history length, default 60, floor 2); value (initial + level 0-1, default 0.5); onChange(current, 'N') fires on every + paint (readout in percent). Values clamp to 0-1 at paint. + handle: el, get() (newest value), push(v) appends one sample and + scrolls, set(samples, current) replaces the whole trace (a + short or missing array back-fills 0.5). + CSS lives in the "strip-chart" block of DUPRE_CSS; no other styles + involved. */ DUPRE.stripChart = function (host, opts = {}) { const onChange = opts.onChange || noop; - const n = opts.samples || 60; + const n = Math.max(2, opts.samples || 60); const hist = Array.from({ length: n }, () => opts.value ?? 0.5); - const el = document.createElement('span'); el.className = 'strip'; - el.innerHTML = '<span class="rule"></span><svg viewBox="0 0 176 62" preserveAspectRatio="none"><polyline/></svg><span class="pen" style="top:31px"></span>'; + const el = document.createElement('span'); el.className = 'dupre-strip'; + el.innerHTML = '<span class="dupre-rule"></span><svg viewBox="0 0 176 62" preserveAspectRatio="none"><polyline/></svg><span class="dupre-pen" style="top:31px"></span>'; host.appendChild(el); - const line = el.querySelector('polyline'), pen = el.querySelector('.pen'); + const line = el.querySelector('polyline'), pen = el.querySelector('.dupre-pen'); const clamp = x => Math.max(0, Math.min(1, x)); const yPx = v => 59 - clamp(v) * 56; function paint(current) { @@ -3352,23 +3387,33 @@ DUPRE.stripChart = function (host, opts = {}) { } function push(v) { hist.push(v); hist.shift(); paint(v); } function set(samples, current) { + if (!Array.isArray(samples)) samples = []; for (let i = 0; i < n; i++) hist[i] = samples[i] ?? 0.5; paint(current ?? hist[n - 1]); } return { el, get: () => hist[n - 1], push, set }; }; -/* N17 correlation meter — needle rests at 0 and swings ±; drag left/right */ +/* N17 correlation meter — needle rests at 0 and swings ±; drag left/right. + + Contract (everything a consumer needs; no page globals touched): + opts: value (0-100, default 58 — 50 is the 0 rest point); onChange(corr, + '±x.xx') fires on every set with the CORRELATION (-1..+1 mapped + from the 0-100 position), not the raw position. + handle: el, get() (current 0-100 position), set(p) — clamped to 0-100; + drag left/right sets by pointer position. + CSS lives in the "correlation" block of DUPRE_CSS; no other styles + involved. */ DUPRE.corrMeter = function (host, opts = {}) { const onChange = opts.onChange || noop; let p = opts.value ?? 58; - const el = document.createElement('div'); el.className = 'corr'; - el.innerHTML = `<div class="face"><div class="arc"></div><div class="zero"></div><div class="ndl"></div></div> - <div class="lbl"><span>−1</span><span>0</span><span>+1</span></div>`; + const el = document.createElement('div'); el.className = 'dupre-corr'; + el.innerHTML = `<div class="dupre-corr-face"><div class="dupre-corr-arc"></div><div class="dupre-zero"></div><div class="dupre-corr-ndl"></div></div> + <div class="dupre-corr-lbl"><span>−1</span><span>0</span><span>+1</span></div>`; host.appendChild(el); - const ndl = el.querySelector('.ndl'); + const ndl = el.querySelector('.dupre-corr-ndl'); function set(np) { - p = np; + p = Math.max(0, Math.min(100, np)); const v = (p - 50) / 50; ndl.style.transform = `rotate(${v * 38}deg)`; onChange(v, (v >= 0 ? '+' : '') + v.toFixed(2)); @@ -3378,20 +3423,28 @@ DUPRE.corrMeter = function (host, opts = {}) { return { el, get: () => p, set }; }; -/* N18 battery-cell gauge — charge as discrete cells, the low end warns */ +/* N18 battery-cell gauge — charge as discrete cells, the low end warns. + + Contract (everything a consumer needs; no page globals touched): + opts: cells (count, default 8, floor 1); warnAt (threshold, default 25 — + at or below it lit cells tint dupre-warn); value (0-100, default + 62); onChange(value, 'N%') fires on every set. + handle: el, get() (current value), set(v) — clamped to 0-100; drag + left/right sets by pointer position. Lit cells carry dupre-on. + CSS lives in the "battery" block of DUPRE_CSS; no other styles involved. */ DUPRE.battCells = function (host, opts = {}) { const onChange = opts.onChange || noop; - const cells = opts.cells || 8, warnAt = opts.warnAt ?? 25; + const cells = Math.max(1, opts.cells || 8), warnAt = opts.warnAt ?? 25; let p = opts.value ?? 62; - const el = document.createElement('div'); el.className = 'batt'; - el.innerHTML = '<div class="cells"></div><span class="nub"></span>'; + const el = document.createElement('div'); el.className = 'dupre-batt'; + el.innerHTML = '<div class="dupre-cells"></div><span class="dupre-nub"></span>'; host.appendChild(el); - const cc = el.querySelector('.cells'); - for (let i = 0; i < cells; i++) { const c = document.createElement('span'); c.className = 'cell'; cc.appendChild(c); } + const cc = el.querySelector('.dupre-cells'); + for (let i = 0; i < cells; i++) { const c = document.createElement('span'); c.className = 'dupre-cell'; cc.appendChild(c); } function set(np) { - p = np; + p = Math.max(0, Math.min(100, np)); const lit = Math.round(p / 100 * cells); - for (let i = 0; i < cells; i++) cc.children[i].className = 'cell' + (i < lit ? ' on' : '') + ((p <= warnAt && i < lit) ? ' warn' : ''); + for (let i = 0; i < cells; i++) cc.children[i].className = 'dupre-cell' + (i < lit ? ' dupre-on' : '') + ((p <= warnAt && i < lit) ? ' dupre-warn' : ''); onChange(p, Math.round(p) + '%'); } dragX(el, set); @@ -3400,7 +3453,18 @@ DUPRE.battCells = function (host, opts = {}) { }; /* R01 moving-coil VU — pivot below the face, authentic nonlinear dB scale; - set(t) positions instantly (0..1.02); ballistics belong to the signal owner */ + ballistics belong to the signal owner. + + Contract (everything a consumer needs; no page globals touched): + opts: value (0-1.02, default .35 — clamped, the needle paints from it + at build); onChange(t, '+x.x VU') fires on every set, no initial + fire — the readout maps t through the shared VU law (VUDB/vuDb), + the needle caps at t=1. + handle: el, get() (current t), set(t) — clamped to 0-1.02; positions + instantly, no ballistics. No drag — display-only. + SVG-built: styling is inline attributes plus the shared .rsvg stage block + of DUPRE_CSS; the glass gradient and face clip are per-instance defs + entries. */ DUPRE.mcVu = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 150, 96); @@ -3436,13 +3500,13 @@ DUPRE.mcVu = function (host, opts = {}) { x: 75, y: 78, 'text-anchor': 'middle', 'font-size': 11, 'font-weight': 700, 'font-family': 'var(--mono)', fill: '#3a3128' }).textContent = 'VU'; + let t = Math.max(0, Math.min(1.02, opts.value ?? .35)); const needle = svgEl(face, 'line', { x1: cx, y1: cy, x2: cx, y2: cy - 62, stroke: '#1a1613', 'stroke-width': 1.6, - transform: `rotate(${sweep(.35)},${cx},${cy})` + transform: `rotate(${sweep(Math.min(1, t))},${cx},${cy})` }); svgEl(face, 'circle', { cx, cy: 88, r: 9, fill: '#16130f' }); svgEl(s, 'rect', { x: 8, y: 8, width: 134, height: 74, rx: 3, fill: `url(#${glassId})` }); - let t = opts.value ?? .35; function set(nt) { t = Math.max(0, Math.min(1.02, nt)); needle.setAttribute('transform', `rotate(${sweep(Math.min(1, t))},${cx},${cy})`); @@ -3513,7 +3577,18 @@ DUPRE.roundMeter = function (host, opts = {}) { return { el: s, get: () => v, set }; }; -/* R08 chrome MIN/MAX indicator — dark pointer over a brushed dome */ +/* R08 chrome MIN/MAX indicator — dark pointer over a brushed dome. + + Contract (everything a consumer needs; no page globals touched): + opts: value (0-100, default 50); onChange(value, readout) fires on + every set — the readout says 'MIN' at ≤2, 'MAX' at ≥98, 'N%' + between. + handle: el, get() (current value), set(v) — clamped to 0-100, the + pointer sweeps -55°..+55°; drag anywhere sets by vertical + delta. + SVG-built: styling is inline attributes plus the shared .rsvg stage block + of DUPRE_CSS; the ring and dome gradients register in the shared defs + plate. */ DUPRE.chromeMinMax = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg drag', 110, 104); @@ -3556,7 +3631,16 @@ DUPRE.chromeMinMax = function (host, opts = {}) { return { el: s, get: () => v, set }; }; -/* R09 black-face aviation gauge — zone arcs per the airspeed-indicator scheme */ +/* R09 black-face aviation gauge — zone arcs per the airspeed-indicator scheme. + + Contract (everything a consumer needs; no page globals touched): + opts: value (0-100, default 52); onChange(value, 'x.x ×100 rpm') fires + on every set — the readout maps 0-100 onto 0-8 ×100 RPM. + handle: el, get() (current value), set(v) — clamped to 0-100, the + needle sweeps -135°..+135°; drag anywhere sets by vertical + delta. + SVG-built: styling is inline attributes plus the shared .rsvg stage block + of DUPRE_CSS; the needle-glow filter registers in the shared defs plate. */ DUPRE.aviationGauge = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg drag', 110, 104); @@ -3610,7 +3694,16 @@ DUPRE.aviationGauge = function (host, opts = {}) { return { el: s, get: () => v, set }; }; -/* R13 edgewise strip meter — compressed log scale, the bar rides the edge */ +/* R13 edgewise strip meter — compressed log scale, the bar rides the edge. + + Contract (everything a consumer needs; no page globals touched): + opts: value (fraction of the window, .08-.94, default .62); onChange( + frac, '−x.x dB') fires on every set — the readout interpolates + the compressed 0..−40 dB scale printed on the face. + handle: el, get() (current fraction), set(f) — clamped to .08-.94 (the + printed scale's span); drag up/down sets by pointer position. + SVG-built: styling is inline attributes plus the shared .rsvg stage block + of DUPRE_CSS; the face gradient is a per-instance defs entry. */ DUPRE.edgeMeter = function (host, opts = {}) { const onChange = opts.onChange || noop; const SCALE = [[0, .08], [3, .26], [6, .44], [12, .62], [20, .78], [40, .94]]; @@ -5611,60 +5704,60 @@ const DUPRE_CSS = ` .dupre-eq .dupre-band i.dupre-clip{opacity:1;background:var(--fail)} /* crossed-needle */ -.crossm{width:120px} -.crossm .face{position:relative;height:56px;overflow:hidden;cursor:ns-resize;touch-action:none} -.crossm .arc{position:absolute;inset:2px 4px -56px;border:2px solid var(--wash);border-radius:50%} -.crossm .nA{position:absolute;left:14px;bottom:2px;width:2px;height:52px;background:var(--gold-hi);transform-origin:50% 100%; +.dupre-crossm{width:120px} +.dupre-crossm .dupre-face{position:relative;height:56px;overflow:hidden;cursor:ns-resize;touch-action:none} +.dupre-crossm .dupre-crossm-arc{position:absolute;inset:2px 4px -56px;border:2px solid var(--wash);border-radius:50%} +.dupre-crossm .dupre-fwd{position:absolute;left:14px;bottom:2px;width:2px;height:52px;background:var(--gold-hi);transform-origin:50% 100%; border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.5);transition:transform .12s cubic-bezier(.3,1.2,.5,1)} -.crossm .nB{position:absolute;right:14px;bottom:2px;width:2px;height:52px;background:var(--fail);transform-origin:50% 100%; +.dupre-crossm .dupre-rfl{position:absolute;right:14px;bottom:2px;width:2px;height:52px;background:var(--fail);transform-origin:50% 100%; border-radius:2px;box-shadow:0 0 5px rgba(203,107,77,.5);transition:transform .12s cubic-bezier(.3,1.2,.5,1)} -.crossm .lbl{display:flex;justify-content:space-between;color:var(--steel);font-size:9px;margin-top:2px} +.dupre-crossm .dupre-lbl{display:flex;justify-content:space-between;color:var(--steel);font-size:9px;margin-top:2px} /* thermometer */ -.thermo{display:flex;align-items:flex-end;gap:6px;height:74px;cursor:ns-resize;touch-action:none} -.thermo .tube{width:12px;height:64px;border-radius:6px 6px 0 0;position:relative;background:#0d0f10;border:1px solid #231f18;overflow:hidden} -.thermo .fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,#a35a3f,var(--fail))} -.thermo .bulb{width:20px;height:20px;border-radius:50%;background:var(--fail);position:absolute;left:-4px;bottom:-9px; +.dupre-thermo{display:flex;align-items:flex-end;gap:6px;height:74px;cursor:ns-resize;touch-action:none} +.dupre-thermo .dupre-thermo-tube{width:12px;height:64px;border-radius:6px 6px 0 0;position:relative;background:#0d0f10;border:1px solid #231f18;overflow:hidden} +.dupre-thermo .dupre-thermo-fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,#a35a3f,var(--fail))} +.dupre-thermo .dupre-bulb{width:20px;height:20px;border-radius:50%;background:var(--fail);position:absolute;left:-4px;bottom:-9px; box-shadow:0 0 6px rgba(203,107,77,.5)} -.thermo .scale{display:flex;flex-direction:column;justify-content:space-between;height:64px;font-size:9px;color:var(--steel)} -.thermo .wrapcol{position:relative;padding-bottom:9px} +.dupre-thermo .dupre-scale{display:flex;flex-direction:column;justify-content:space-between;height:64px;font-size:9px;color:var(--steel)} +.dupre-thermo .dupre-wrapcol{position:relative;padding-bottom:9px} /* bourdon */ -.bourdon{width:82px;height:82px;border-radius:50%;position:relative;cursor:ns-resize;touch-action:none; +.dupre-bourdon{width:82px;height:82px;border-radius:50%;position:relative;cursor:ns-resize;touch-action:none; background:radial-gradient(circle at 48% 42%,#1d1a16,#0c0b09);border:2px solid #2c261d; box-shadow:inset 0 2px 5px rgba(0,0,0,.6)} -.bourdon svg{position:absolute;inset:0} -.bourdon .ndl{position:absolute;left:50%;top:50%;width:2px;height:32px;margin:-32px 0 0 -1px;background:var(--gold-hi); +.dupre-bourdon svg{position:absolute;inset:0} +.dupre-bourdon-ndl{position:absolute;left:50%;top:50%;width:2px;height:32px;margin:-32px 0 0 -1px;background:var(--gold-hi); transform-origin:50% 100%;border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.55);transition:transform .12s cubic-bezier(.3,1.2,.5,1)} -.bourdon .hub{position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px;border-radius:50%;background:var(--gold)} -.bourdon .cap{position:absolute;left:0;right:0;bottom:12px;text-align:center;font-size:8px;letter-spacing:.16em;color:var(--steel)} +.dupre-bourdon-hub{position:absolute;left:50%;top:50%;width:8px;height:8px;margin:-4px;border-radius:50%;background:var(--gold)} +.dupre-bourdon-cap{position:absolute;left:0;right:0;bottom:12px;text-align:center;font-size:8px;letter-spacing:.16em;color:var(--steel)} /* strip-chart */ -.strip{width:176px;height:62px;border-radius:5px;position:relative;overflow:hidden; +.dupre-strip{width:176px;height:62px;border-radius:5px;position:relative;overflow:hidden; background:#0c0e0f;border:1px solid #231f18} -.strip .rule{position:absolute;inset:0;opacity:.4; +.dupre-strip .dupre-rule{position:absolute;inset:0;opacity:.4; background-image:linear-gradient(90deg,rgba(150,147,133,.14) 1px,transparent 1px);background-size:16px 100%} -.strip svg{position:absolute;inset:0;width:100%;height:100%} -.strip polyline{fill:none;stroke:var(--gold);stroke-width:1.4} -.strip .pen{position:absolute;right:2px;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--gold-hi); +.dupre-strip svg{position:absolute;inset:0;width:100%;height:100%} +.dupre-strip polyline{fill:none;stroke:var(--gold);stroke-width:1.4} +.dupre-strip .dupre-pen{position:absolute;right:2px;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--gold-hi); box-shadow:0 0 6px rgba(var(--glow-hi),.8);transition:top .08s linear} /* correlation */ -.corr{width:150px;cursor:pointer;touch-action:none} -.corr .face{position:relative;height:44px;overflow:hidden} -.corr .arc{position:absolute;inset:0 0 -44px;border:2px solid var(--wash);border-radius:50%} -.corr .zero{position:absolute;left:50%;top:2px;width:1px;height:10px;background:var(--gold);margin-left:-.5px} -.corr .ndl{position:absolute;left:50%;bottom:0;width:2px;height:38px;background:var(--gold-hi);transform-origin:50% 100%; +.dupre-corr{width:150px;cursor:pointer;touch-action:none} +.dupre-corr-face{position:relative;height:44px;overflow:hidden} +.dupre-corr-arc{position:absolute;inset:0 0 -44px;border:2px solid var(--wash);border-radius:50%} +.dupre-corr .dupre-zero{position:absolute;left:50%;top:2px;width:1px;height:10px;background:var(--gold);margin-left:-.5px} +.dupre-corr-ndl{position:absolute;left:50%;bottom:0;width:2px;height:38px;background:var(--gold-hi);transform-origin:50% 100%; border-radius:2px;box-shadow:0 0 5px rgba(var(--glow-hi),.5);transition:transform .1s ease-out} -.corr .lbl{display:flex;justify-content:space-between;color:var(--steel);font-size:9px;margin-top:2px} +.dupre-corr-lbl{display:flex;justify-content:space-between;color:var(--steel);font-size:9px;margin-top:2px} /* battery */ -.batt{display:flex;align-items:center;cursor:pointer;touch-action:none} -.batt .cells{display:flex;gap:2px;padding:3px;border:1px solid #34302a;border-radius:4px;background:#0d0f10} -.batt .cell{width:12px;height:26px;border-radius:1px;background:var(--wash);opacity:.35} -.batt .cell.on{opacity:1;background:linear-gradient(180deg,var(--pass),#5c7526)} -.batt .cell.warn.on{background:linear-gradient(180deg,var(--fail),#a04a34)} -.batt .nub{width:4px;height:12px;background:#34302a;border-radius:0 2px 2px 0} +.dupre-batt{display:flex;align-items:center;cursor:pointer;touch-action:none} +.dupre-batt .dupre-cells{display:flex;gap:2px;padding:3px;border:1px solid #34302a;border-radius:4px;background:#0d0f10} +.dupre-batt .dupre-cell{width:12px;height:26px;border-radius:1px;background:var(--wash);opacity:.35} +.dupre-batt .dupre-cell.dupre-on{opacity:1;background:linear-gradient(180deg,var(--pass),#5c7526)} +.dupre-batt .dupre-cell.dupre-warn.dupre-on{background:linear-gradient(180deg,var(--fail),#a04a34)} +.dupre-batt .dupre-nub{width:4px;height:12px;background:#34302a;border-radius:0 2px 2px 0} /* split-flap — four clipped half-panels per cell; the fold happens on .ftc (falls) and .fbn (lands), driven by the builder's WAAPI flips */ |
