aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/panel-widget-gallery.html
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes/panel-widget-gallery.html')
-rw-r--r--docs/prototypes/panel-widget-gallery.html2788
1 files changed, 1477 insertions, 1311 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index f8330bb..2d7bcb9 100644
--- a/docs/prototypes/panel-widget-gallery.html
+++ b/docs/prototypes/panel-widget-gallery.html
@@ -44,7 +44,7 @@
/* @tokens:end */
}
*{box-sizing:border-box;margin:0;padding:0}
-html{background:var(--ground)}
+html{background:var(--ground);font-size:130%}
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}
@@ -52,6 +52,46 @@ body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 5rem;line-h
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)}
+.szbar{display:flex;align-items:center;gap:8px;margin-top:.8rem}
+.szbar .lab{color:var(--steel);font-size:.68rem;letter-spacing:.22em;text-transform:uppercase;margin-right:4px}
+
+/* masthead row: description left, index panel right */
+.mastrow{display:flex;gap:28px;align-items:flex-start;justify-content:space-between;flex-wrap:wrap}
+.toc{position:relative;flex:0 0 auto;min-width:220px;padding:14px 20px 13px;border-radius:12px;
+ background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320;
+ box-shadow:inset 0 1px 0 rgba(255,255,255,.05),0 6px 14px rgba(0,0,0,.4)}
+.toc .tt{color:var(--steel);font-size:.66rem;letter-spacing:.26em;text-transform:uppercase;
+ margin-bottom:7px;display:flex;align-items:center;gap:10px}
+.toc .tt::after{content:"";height:1px;background:var(--wash);flex:1;min-width:24px}
+.toc a{display:block;color:var(--dim);text-decoration:none;font-size:.8rem;letter-spacing:.04em;padding:3.5px 0}
+.toc a::before{content:"▸ ";color:var(--steel);font-size:.72em}
+.toc a:hover{color:var(--gold-hi)}
+.toc .scr{position:absolute;width:5px;height:5px;border-radius:50%;
+ background:radial-gradient(circle at 35% 30%,#524d43,#161310);box-shadow:0 .5px 0 rgba(255,255,255,.08)}
+html{scroll-behavior:smooth}
+/* size toggle scales the widget stage only; card text stays at its own (slightly raised) size */
+body[data-size="2"] .stagew{zoom:1.7}
+body[data-size="3"] .stagew{zoom:2.4}
+/* intrinsically tiny widgets get an extra bump so they do not float in an empty stage */
+.stagew.boost{zoom:1.5}
+body[data-size="2"] .stagew.boost{zoom:2.55}
+body[data-size="3"] .stagew.boost{zoom:3.6}
+body[data-size="2"] .grid{grid-template-columns:repeat(auto-fill,minmax(380px,1fr))}
+body[data-size="3"] .grid{grid-template-columns:repeat(auto-fill,minmax(520px,1fr))}
+body[data-size="2"] .wrap,body[data-size="3"] .wrap{max-width:none}
+
+.secnote{color:var(--dim);font-size:.74rem;margin-top:.5rem;max-width:86ch}
+.palgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(176px,1fr));gap:10px;margin-top:1rem}
+.swatch{background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320;border-radius:9px;
+ padding:9px;display:flex;flex-direction:column;gap:6px}
+.swatch .sw{height:44px;border-radius:6px;border:1px solid rgba(255,255,255,.08)}
+.swatch .sw.txt{display:flex;align-items:center;justify-content:center;color:var(--steel);font-size:.62rem;
+ background:var(--well);padding:0 6px;text-align:center;overflow:hidden}
+.swatch .swn{color:var(--cream);font-size:.8rem}
+.swatch .swv{color:var(--dim);font-size:.72rem;line-height:1.35}
+.palhead{grid-column:1/-1;color:var(--steel);font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;
+ margin-top:.8rem;display:flex;align-items:center;gap:12px}
+.palhead::after{content:"";height:1px;background:var(--wash);flex:1}
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}
@@ -60,16 +100,42 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.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}
+.wname{color:var(--cream);font-size:.95rem;font-weight:700;display:flex;align-items:center;gap:8px}
+.wname .no{color:var(--panel);background:var(--gold);border-radius:4px;font-size:.7rem;padding:0 5px;font-weight:400}
+/* validation lamp: off = not done, amber = in progress, green = done; click cycles, state persists */
+.vlamp{margin-left:auto;flex:0 0 auto;width:10px;height:10px;border-radius:50%;cursor:pointer;
+ background:#221f1b;border:1px solid #33302b}
+.vlamp[data-v="amber"]{background:var(--gold);border-color:#7d5c16;box-shadow:0 0 6px 1px rgba(var(--glow-lo),.6)}
+.vlamp[data-v="green"]{background:var(--pass);border-color:#4a5c22;box-shadow:0 0 6px 1px rgba(116,147,47,.6)}
.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}
-.wrd{color:var(--gold-hi);font-size:.68rem;letter-spacing:.06em;font-variant-numeric:tabular-nums;
- min-height:.95rem;display:flex;align-items:center;gap:6px}
-.wrd::before{content:"▸";color:var(--steel);font-size:.6rem}
-.wnote{color:var(--dim);font-size:.72rem;line-height:1.4}
+.wrd{color:var(--gold-hi);font-size:.8rem;letter-spacing:.06em;font-variant-numeric:tabular-nums;
+ min-height:.95rem;display:flex;align-items:center;gap:6px;
+ border-top:1px solid var(--wash);padding-top:9px;margin-top:2px}
+.wrd::before{content:"▸";color:var(--steel);font-size:.7rem}
+.opts{display:flex;flex-direction:column;gap:5px}
+.opts:empty{display:none}
+.wnote{color:var(--dim);font-size:.85rem;line-height:1.45;
+ border-top:1px solid var(--wash);padding-top:9px;margin-top:2px}
.wnote b{color:var(--steel);font-weight:400}
+/* per-card spec sheet (collapsible) */
+.winfo{border-top:1px dashed var(--wash);padding-top:6px;margin-top:2px}
+.winfo summary{color:var(--steel);font-size:.74rem;letter-spacing:.18em;text-transform:uppercase;
+ cursor:pointer;list-style:none;user-select:none}
+.winfo summary::-webkit-details-marker{display:none}
+.winfo summary::before{content:"▸ "}
+.winfo[open] summary::before{content:"▾ "}
+.igrid{display:grid;grid-template-columns:max-content 1fr;gap:4px 10px;margin-top:7px}
+.igrid .ik{color:var(--steel);font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;padding-top:1px}
+.igrid .iv{color:var(--dim);font-size:.82rem;line-height:1.45}
+.igrid .iv b{color:var(--silver);font-weight:400}
+.igrid .iv a{color:var(--gold);text-decoration:none;border-bottom:1px dotted var(--gold)}
+.igrid .iv a:hover{color:var(--gold-hi);border-color:var(--gold-hi)}
+.card a.xref{color:var(--gold);text-decoration:none;border-bottom:1px dotted var(--gold)}
+.card a.xref:hover{color:var(--gold-hi);border-color:var(--gold-hi)}
+.card:target{border-color:var(--gold);box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 0 0 1px var(--gold),0 6px 14px rgba(0,0,0,.4)}
+
/* ---- 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)}
@@ -79,14 +145,26 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
/* standard pulse: 1s ease-in-out — the norm for every pulsing / flashing element */
@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}
+/* slide toggle — after the iPod-hold-switch pill: dome thumb, revealed state engraving.
+ Flex layout with fixed px only: absolute insets and stretched widths misrender under stage zoom. */
+.switch{display:inline-flex;vertical-align:middle;align-items:center;justify-content:space-between;
+ width:52px;height:23px;border-radius:12px;padding:2px 7px 2px 2px;cursor:pointer;
+ background:var(--sw-off-bg,linear-gradient(180deg,#15130f,#242019));
+ border:1px solid var(--sw-off-brd,var(--slate));
+ box-shadow:inset 0 1.5px 3px rgba(0,0,0,.55)}
+.switch::before{content:"OFF";order:2;font-size:7px;letter-spacing:.06em;font-weight:700;
+ font-family:var(--mono);color:var(--sw-off-ink,var(--cream))}
+.switch::after{content:"";order:1;width:17px;height:17px;border-radius:50%;
+ background:var(--sw-thumb,radial-gradient(circle at 35% 28%,#f6f2e8,#b0aa9a));
+ box-shadow:0 1px 2.5px rgba(0,0,0,.65),inset 0 1px 0 rgba(255,255,255,.25)}
+.switch.on{background:var(--sw-on-bg,linear-gradient(180deg,var(--amber-grad-top),var(--gold)));
+ border-color:var(--sw-on-brd,var(--gold-hi));
+ padding:2px 2px 2px 7px;box-shadow:inset 0 1.5px 3px rgba(0,0,0,.3)}
+.switch.on::before{content:"ON";order:1;color:var(--sw-on-ink,var(--panel))}
+.switch.on::after{order:2}
+.switch.red{background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail);padding:2px 2px 2px 7px}
+.switch.red::before{content:"OFF";order:1;color:var(--cream)}
+.switch.red::after{order:2}
.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;
@@ -347,16 +425,40 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.jog .dimple{position:absolute;left:50%;top:7px;width:8px;height:8px;margin-left:-4px;border-radius:50%;
background:radial-gradient(circle at 40% 35%,#151210,#000);box-shadow:inset 0 1px 2px rgba(0,0,0,.9)}
-/* oscilloscope */
+/* oscilloscope — screen-family vars with the original green as fallback */
.scope{width:176px;height:78px;border-radius:6px;position:relative;overflow:hidden;
- background:radial-gradient(circle at 50% 45%,#04140a,#020a05);border:1px solid #123018;
+ 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;
- background-image:linear-gradient(rgba(111,206,51,.18) 1px,transparent 1px),linear-gradient(90deg,rgba(111,206,51,.18) 1px,transparent 1px);
+ 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:rgba(111,206,51,.35)}
+.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(--phos);stroke-width:1.6;filter:drop-shadow(0 0 3px rgba(127,224,160,.8))}
+.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;
+ 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));
+ border-color:var(--gold-hi);font-weight:700}
+.vk.tlk .bar{background:var(--pass);box-shadow:0 0 4px rgba(116,147,47,.6)}
+
+/* option chips: each group is an encircled capsule (label + choices); groups flow with spacing */
+.famchips{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:1px}
+.fgroup{display:inline-flex;align-items:center;gap:6px;padding:3px 9px 3px 8px;
+ border:1px solid #2e2b26;border-radius:10px;background:rgba(255,255,255,.015)}
+.famchips .lab{color:var(--steel);font-size:.6rem;letter-spacing:.14em;text-transform:uppercase;margin-right:2px}
+.famchips .fc{width:13px;height:13px;border-radius:50%;cursor:pointer;
+ border:1px solid rgba(255,255,255,.22);box-shadow:0 1px 3px rgba(0,0,0,.5)}
+.famchips .fc.on{outline:2px solid var(--silver);outline-offset:1px}
/* spectrum / EQ */
.eq{display:flex;align-items:flex-end;gap:3px;height:60px}
@@ -440,6 +542,23 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
.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)}
+/* DSKY verb/noun panel */
+.dsky{display:flex;gap:9px;align-items:stretch;background:#1c1916;border:1px solid #060505;border-radius:8px;padding:8px}
+.dsky .lampcol{display:grid;grid-template-rows:repeat(6,1fr);gap:3px;width:52px}
+.dsky .sl{font-size:6.5px;letter-spacing:.04em;display:flex;align-items:center;justify-content:center;text-align:center;
+ background:#141210;color:#5c574c;border-radius:2px;border:1px solid #26221c}
+.dsky .sl.on{background:linear-gradient(180deg,var(--amber-warn),var(--gold));color:var(--panel);font-weight:700;
+ box-shadow:0 0 6px rgba(var(--glow-lo),.5)}
+.dsky .right{display:flex;flex-direction:column;gap:6px}
+.dsky .wins{display:flex;gap:8px}
+.dsky .win{display:flex;flex-direction:column;align-items:center;gap:2px}
+.dsky .win .wl{font-size:6px;letter-spacing:.14em;color:var(--steel)}
+.dsky .win .wd{display:flex;gap:2px;background:#0a0806;border:1px solid #231f18;border-radius:4px;padding:3px 4px}
+.dsky .win .wd .seg7{width:13px;height:24px}
+.dsky .win.hot .wd{border-color:var(--gold);box-shadow:0 0 5px rgba(var(--glow-lo),.4)}
+.dsky .pad{display:grid;grid-template-columns:repeat(5,1fr);gap:4px}
+.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;
background:linear-gradient(180deg,#04100e,#020807);border:1px solid #123028;box-shadow:inset 0 0 12px rgba(0,0,0,.6)}
@@ -449,6 +568,14 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
background-image:radial-gradient(rgba(0,0,0,.6) 40%,transparent 41%);background-size:3px 3px}
/* annunciator */
+.annwrap{display:flex;flex-direction:column;gap:6px}
+.annbar{display:flex;gap:5px;align-items:center}
+.mc{font-size:7px;letter-spacing:.08em;padding:4px 8px;border-radius:3px;background:#141210;color:#5c574c;
+ border:1px solid #26221c;text-align:center}
+.mc.on{background:linear-gradient(180deg,#d98a6f,var(--fail));color:var(--cream);font-weight:700;
+ box-shadow:0 0 8px rgba(203,107,77,.5)}
+.mc.on.fl{animation:pulse var(--pulse-rate) ease-in-out infinite}
+.annbar .key{padding:3px 7px;font-size:7.5px;border-radius:4px}
.annun{display:grid;grid-template-columns:repeat(3,1fr);gap:3px}
.acell{font-size:8.5px;letter-spacing:.08em;text-align:center;color:var(--dim);padding:6px 4px;border-radius:3px;cursor:pointer;
background:#141210;border:1px solid #262320;line-height:1.2}
@@ -518,11 +645,13 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
</style>
</head>
-<body>
+<body data-size="2">
<div class="wrap">
<header class="masthead">
<div class="eyebrow">archsetup · dupre panel family · live kit</div>
<h1>Widget gallery — the instrument-console kit, fully driveable</h1>
+ <div class="mastrow">
+ <div>
<p>Every control + display idiom in the dupre faceplate language, merged into one live console — the base kit,
the candidate batch, and the growing reference batch (R: modeled on period hardware), all rendering from
the same tokens the net / bt / sound panels use.
@@ -530,354 +659,1195 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
readouts</b> show state or a number. Every widget is operable: drag the faders, knobs and gauges; click the
switches, keys and lamps. Each card carries a live readout that tracks what you do. The trace displays run
a live signal; the pointer types drive from your drag.</p>
+ <div class="szbar"><span class="lab">widget size</span>
+ <button class="key" data-sz="1">S</button>
+ <button class="key on" data-sz="2">M</button>
+ <button class="key" data-sz="3">L</button>
+ </div>
+ </div>
+ <nav class="toc">
+ <span class="scr" style="top:6px;left:6px"></span><span class="scr" style="top:6px;right:6px"></span>
+ <span class="scr" style="bottom:6px;left:6px"></span><span class="scr" style="bottom:6px;right:6px"></span>
+ <div class="tt">Index</div>
+ <a href="#sec-controls">Controls</a>
+ <a href="#sec-meters">Meters &amp; gauges</a>
+ <a href="#sec-indicators">Indicators &amp; readouts</a>
+ <a href="#sec-palette">Palette</a>
+ </nav>
+ </div>
</header>
-<h2>Controls — take input</h2>
+<h2 id="sec-controls">Controls — take input</h2>
<div class="grid" id="controls"></div>
-<h2>Meters &amp; gauges — live analog value</h2>
+<h2 id="sec-meters">Meters &amp; gauges — live analog value</h2>
<div class="grid" id="meters"></div>
-<h2>Indicators &amp; readouts — state or number</h2>
+<h2 id="sec-indicators">Indicators &amp; readouts — state or number</h2>
<div class="grid" id="indicators"></div>
+<h2 id="sec-palette">Palette — the instrument colors</h2>
+<p class="secnote">Every named color the instruments are built from — markers, hands, faces, lamps, phosphors,
+ materials — with where it appears. Page chrome (background, body text) is deliberately absent.</p>
+<div class="palgrid" id="palette"></div>
+
</div>
+<script src="widgets.js"></script>
<script>
const $ = id => document.getElementById(id);
+const {svgEl,polar,dragX,dragY,dragDelta,seg7,buildBars,VUDB,vuDb,SCREEN_FAMS}=GW;
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
+document.querySelectorAll('.szbar .key').forEach(b=>b.addEventListener('click',()=>{
+ document.body.dataset.size=b.dataset.sz;
+ localStorage.setItem('gv-size',b.dataset.sz);
+ document.querySelectorAll('.szbar .key').forEach(k=>k.classList.toggle('on',k===b));
+}));
+(function(){const s=localStorage.getItem('gv-size');
+ if(!s||!/^[123]$/.test(s))return;
+ document.body.dataset.size=s;
+ document.querySelectorAll('.szbar .key').forEach(k=>k.classList.toggle('on',k.dataset.sz===s));})();
+/* boost the intrinsically tiny widgets (audit: content under 10% of the stage) */
+const BOOST=['01','07','08','12','13','N14','18'];
+
+/* ---- palette: the named instrument colors, grouped by role. No codes shown — names and uses. ---- */
+const PALETTE=[
+ ['Materials',[
+ ['Brass','#b8944a','comfort-meter bezel and knurl'],
+ ['Copper','#ad7a43','knife-switch blades, jaws and hinge'],
+ ['Chrome silver','var(--silver)','needles, ball lever, fader caps, chrome handles'],
+ ['Machined steel','var(--steel)','engraved captions, scale furniture, printed panel lines'],
+ ['Aluminum','#c9c4b8','timer hub, lever shafts, counter bezels'],
+ ['Rubber & bakelite','#2c2824','knob bodies, rocker pad, stomp switch'],
+ ]],
+ ['Faces & inks',[
+ ['Cream dial','var(--cream)','tick marks, scale numerals, dial faces, flag windows'],
+ ['Chart paper','#efe9da','day-date disc, circular chart, comfort face, telegraph dial'],
+ ['Face ink','#14110e','numerals and printing on the paper faces'],
+ ['CRT face green','#b9d8c0','round-scope face tint'],
+ ['Sky slate','var(--slate-hi)','attitude-indicator sky'],
+ ['Earth brown','#5c4630','attitude-indicator ground'],
+ ['Instrument black','#17140f','bezels, plates and housings'],
+ ]],
+ ['Lamps, LEDs & jewels',[
+ ['Lit amber','var(--gold-hi)','lit keys, sweep line, fixed indexes, blinkenlight lamps'],
+ ['Panel amber','var(--gold)','key gradients, bearing rings, seated pins, heading bug'],
+ ['Warning amber','var(--amber-warn)','warn cells, tripped collars, gear-transit lamps'],
+ ['Run green','var(--pass)','run lamps, gear greens, monitor bars, LIVE lamps'],
+ ['Fail terracotta','var(--fail)','muted keys, fault cells, TRIP button'],
+ ['LED green','var(--sevgrn)','seven-segment digits, battery cells'],
+ ['LED red','var(--sevred)','red seven-segment variant'],
+ ['Matrix red','#ff4a30','dot-matrix LEDs'],
+ ['Jewel red','var(--jewel-r)','pilot-lamp lens'],
+ ['Jewel amber','var(--jewel-a)','pilot-lamp lens, four-way corner lamps'],
+ ['Jewel green','var(--jewel-g)','pilot-lamp lens'],
+ ['Neon orange','#ff9a4c','nixie digits, dekatron glow, red screen family'],
+ ['Flip-disc yellow','#e3d44f','flip-dot faces'],
+ ['PDP magenta','#3a1420','blinkenlights panel skin'],
+ ]],
+ ['Screens & phosphors',[
+ ['Phosphor green','var(--phos)','oscilloscope traces, green screen family'],
+ ['Phosphor dim','var(--phos-dim)','phosphor afterglow and dark screen grounds'],
+ ['VFD cyan','var(--vfd)','marquee, filter-bank arrows, vfd screen family'],
+ ['Graticule green','#3d5c46','CRT graticule rulings'],
+ ['LCD white','#f2f4f2','waveform-editor ink, white screen family'],
+ ['P4 blue-white','#cfe4ff','blue screen family'],
+ ]],
+ ['Needles & controls',[
+ ['Needle red','#c23a28','comfort-meter needles, red hands'],
+ ['Control red','#e0523a','stop knobs, telegraph pointer, palm buttons, OFF marks'],
+ ['Pointer yellow','#e8cf4a','day-date read hand'],
+ ]],
+];
+(function buildPalette(){
+ const host=$('palette'); if(!host)return;
+ for(const [group,colors] of PALETTE){
+ const h=document.createElement('div'); h.className='palhead'; h.textContent=group;
+ host.appendChild(h);
+ for(const [name,val,used] of colors){
+ const tile=document.createElement('div'); tile.className='swatch';
+ tile.innerHTML=`<div class="sw" style="background:${val}"></div>`+
+ `<div class="swn">${name}</div><div class="swv">${used}</div>`;
+ host.appendChild(tile);}}
+})();
+
+/* ---- screen-color families: period-authentic alternates for the screen widgets ----
+ green = P1 CRT phosphor (radar/scopes; hi reuses --phos, graticule keeps the kit's scope green)
+ amber = P3 phosphor / backlit amber (the gold family the kit already speaks)
+ red = neon orange-red, the nixie's color pulled out as a family
+ blue = P4 blue-white (early TV / precision scopes)
+ vfd = blue-green vacuum-fluorescent (the marquee's --vfd pulled out as a family)
+ white = monochrome LCD/CRT white
+ Exploration data, deliberately NOT in tokens.json yet — families get tokenized once picked,
+ the same way amber earned its tokens. Values keyed by the CSS custom property they drive. */
+/* SCREEN_FAMS lives in widgets.js (GW.SCREEN_FAMS) */
+function screenChips(no,el,fams,def){
+ const cardEl=el.closest('.card'); if(!cardEl)return;
+ const row=document.createElement('div'); row.className='famchips';
+ const g=document.createElement('span'); g.className='fgroup'; row.appendChild(g);
+ const lab=document.createElement('span'); lab.className='lab'; lab.textContent='screen'; g.appendChild(lab);
+ fams.forEach(f=>{const b=document.createElement('span');
+ b.className='fc'+(f===def?' on':''); b.style.background=SCREEN_FAMS[f]['--scr-hi']; b.title=f;
+ b.addEventListener('click',()=>{
+ for(const [k,v] of Object.entries(SCREEN_FAMS[f])) el.style.setProperty(k,v);
+ g.querySelectorAll('.fc').forEach(x=>x.classList.toggle('on',x===b));
+ setRd(no,'screen '+f);});
+ g.appendChild(b);});
+ cardEl.querySelector('.opts').appendChild(row);
+}
+/* ---- per-card spec sheets ----
+ input how you drive it, incl. the model: click widgets port to every target (Emacs click-regions
+ included); drag widgets have no Emacs drag equivalent and need a click/key idiom there;
+ display widgets take no input in real use (gallery interaction is demo only).
+ period present only when the component is clearly not timeless — use it to keep a console
+ period-consistent (swap a modern part for its historical counterpart or vice versa). */
+const INFO={
+'01':{input:'Click to flip. Click-only, ports everywhere incl. Emacs.',
+ solves:'One persistent on/off state, visible at a glance without reading text.',
+ use:'Common in every modern UI. Shines as a master control: power, mute, enable.',
+ limits:'Two states only. Ambiguous if the on-direction is not marked.',
+ origin:'Slide switches (transistor radios, Walkman hold) → the iPod hold switch with its orange reveal → the iOS toggle (2007). This card wears the hardware skin of that lineage.',difficulty:'Intuitive.',
+ prefer:'The state persists and gets glanced at more than changed. For period consoles use R03 or N01 instead.',
+ period:'The pill rendering is modern (iPod/iOS era); the slide-switch mechanism underneath is 1950s+.',
+ ref:'reference/slide-toggle.jpg'},
+'02':{input:'Click one key to engage it; the others release. Click-only.',
+ solves:'One-of-few mode selection where the active mode must glow.',
+ use:'Common on consoles. Shines for 2-4 modes switched often.',
+ limits:'Wide per option; past ~5 keys a selector reads better.',
+ origin:'Broadcast and studio consoles.',difficulty:'Intuitive.',
+ prefer:'Each mode deserves a lit, labeled, pressable identity.'},
+'03':{input:'Drag along the track. Drag-only — needs a click/key idiom for Emacs.',
+ solves:'Continuous level with position as the value readout.',
+ use:'Common in audio and settings. Shines when relative position matters.',
+ limits:'Precision bounded by track length; needs horizontal room.',
+ origin:'Mixing desks.',difficulty:'Intuitive.',
+ prefer:'The value is a level the eye should compare across channels.'},
+'04':{input:'Drag along the track. Drag-only — needs a click/key idiom for Emacs.',
+ solves:'Same as the horizontal fader in a column footprint.',
+ use:'Common. Shines in channel strips where many sit side by side.',
+ limits:'Needs vertical room; thumb occludes the scale mid-drag.',
+ origin:'Mixing desks.',difficulty:'Intuitive.',
+ prefer:'Levels stack in parallel columns (mixer, EQ bank).'},
+'05':{input:'Drag vertically to rotate. Drag-only — Emacs wants +/- keys or click-step.',
+ solves:'Continuous control in the smallest footprint of any analog input.',
+ use:'Common everywhere. Shines for gain, tune, adjust anywhere space is tight.',
+ limits:'Drag direction is a learned convention; fine setting needs a readout.',
+ origin:'Radio and audio gear.',difficulty:'Easy — the vertical-drag convention must be learned.',
+ prefer:'Space is tight and the exact value matters less than the gesture.'},
+'06':{input:'Click a segment. Click-only, ports everywhere.',
+ solves:'One-of-N selection with every option visible at once.',
+ use:'Common. Shines for 3-6 named, equally likely options.',
+ limits:'Options must fit the bar; long labels break it.',
+ origin:'Test equipment range switches, modern segmented controls.',difficulty:'Intuitive.',
+ prefer:'All options should stay readable without opening anything.'},
+'07':{input:'Click to toggle. Click-only.',
+ solves:'A lightweight boolean that reads as a tag, not a switch.',
+ use:'Common in filters and option sets. Shines in rows of independent flags.',
+ limits:'Low visual weight — poor for states that must shout.',
+ origin:'Modern web UI, filter chips.',difficulty:'Intuitive.',
+ prefer:'Many independent booleans sit together and density wins.'},
+'08':{input:'Two clicks: arm, then fire; disarms on timeout. Click-only.',
+ solves:'Destructive actions need friction sized to their consequence.',
+ use:'Specialty. Shines guarding resets, wipes, irreversible sends.',
+ limits:'Friction annoys if the action is actually routine.',
+ origin:'Missile and launch panels, industrial interlocks.',difficulty:'Easy.',
+ prefer:'A mis-click would cost real work — make intent two-step.'},
+'09':{input:'Click a lamp to toggle it. Click-only.',
+ solves:'A bank of independent booleans with lit state per item.',
+ use:'Common. Shines for channel enables, feature flags.',
+ limits:'Unlabeled lamps need a legend; color alone excludes colorblind users.',
+ origin:'Console channel strips.',difficulty:'Intuitive.',
+ prefer:'The set is small and each member needs its own light.'},
+'24':{input:'Click to step to the next position. Click-only.',
+ solves:'One-of-N selection with a physical pointer you can read across the room.',
+ use:'Common on period gear. Shines for mode/range knobs.',
+ limits:'Stepping is sequential — jumping to a far position takes several clicks.',
+ origin:'Rotary switches on radios and instruments.',difficulty:'Intuitive.',
+ prefer:'The selection should read like hardware, not a menu.'},
+'25':{input:'Drag to slide the cursor. Drag-only — needs a key idiom for Emacs.',
+ solves:'Reading a value against a long calibrated scale.',
+ use:'Specialty. Shines where the scale itself carries meaning.',
+ limits:'Scale literacy required; poor for quick setting.',
+ origin:'Slide rules, tuning scales.',difficulty:'Moderate.',
+ prefer:'The scale markings are the point, not just the value.',
+ period:'1930s-60s.'},
+'N01':{input:'Click to rock. Click-only.',
+ solves:'Mains-grade on/off with an unmistakable thrown state.',
+ use:'Common. Shines as the one switch that powers the whole unit.',
+ limits:'Visually heavy — wrong for minor options.',
+ origin:'Appliance and amplifier mains switches.',difficulty:'Intuitive.',
+ prefer:'The action is THE power decision for the device.'},
+'N02':{input:'Click a transport key; keys are mutually exclusive. Click-only.',
+ solves:'Media transport state with one lit verb at a time.',
+ use:'Common wherever playback exists. Shines with a moving mechanism to confirm state.',
+ limits:'The verb set is fixed by convention — do not repurpose.',
+ origin:'Tape decks.',difficulty:'Intuitive.',
+ prefer:'Anything plays, records, or rewinds.',
+ period:'1960s-80s tape era styling.'},
+'N03':{input:'Click a numbered preset. Click-only.',
+ solves:'Instant recall of a small set of saved destinations.',
+ use:'Common. Shines for stations, scenes, memory slots.',
+ limits:'Anonymous numbers — the user must remember the mapping.',
+ origin:'Car radio mechanical presets.',difficulty:'Intuitive.',
+ prefer:'A handful of favorites deserve one-press recall.',
+ period:'1950s-70s mechanical-preset styling.'},
+'N04':{input:'Drag either ring: inner and outer rotate independently. Drag-only, and the two targets need care in Emacs.',
+ solves:'Two related parameters in one knob footprint.',
+ use:'Specialty. Shines for coarse/fine or volume/tone pairs.',
+ limits:'Which ring is which must be learned; easy to grab the wrong one.',
+ origin:'Radio concentric controls.',difficulty:'Moderate.',
+ prefer:'Two parameters are so coupled they should live on one axis.',
+ period:'1940s-70s radio styling.'},
+'N05':{input:'Drag to spin; the LED ring shows position. Drag-only.',
+ solves:'Endless rotation where the value can also be set by the program.',
+ use:'Common on modern gear. Shines when presets move the value under the knob.',
+ limits:'No physical end stops — the ring is the only position cue.',
+ origin:'Digital synths and controllers.',difficulty:'Easy.',
+ prefer:'Software also writes the value the knob edits.',
+ period:'1980s onward.'},
+'N06':{input:'Click to turn the key through its positions. Click-only.',
+ solves:'Mode changes that should require deliberate, almost credentialed intent.',
+ use:'Specialty. Shines for maintenance/normal/locked modes.',
+ limits:'Slow by design; wrong for anything frequent.',
+ origin:'Machine and alarm key switches.',difficulty:'Easy.',
+ prefer:'Casual switching must be discouraged by the control itself.'},
+'N07':{input:'Drag along the track; a detent holds center. Drag-only.',
+ solves:'Balancing two sources with an honest neutral point.',
+ use:'Specialty. Shines for A/B blend, pan, crossfade.',
+ limits:'Only meaningful with exactly two poles.',
+ origin:'DJ mixers.',difficulty:'Intuitive.',
+ prefer:'The midpoint is a real state, not just halfway.'},
+'N08':{input:'Drag a wheel vertically to step its digit. Drag-only — Emacs wants per-digit +/-.',
+ solves:'Exact multi-digit numeric entry without a keyboard.',
+ use:'Specialty. Shines for frequencies, counts, codes set digit by digit.',
+ limits:'Slow for big changes; each digit is its own control.',
+ origin:'Avionics and test equipment.',difficulty:'Expert — digit-wise thinking.',
+ prefer:'Exactness beats speed and a keypad would be overkill.',
+ period:'1960s-80s.'},
+'N09':{input:'Click a switch to flip it. Click-only.',
+ solves:'A persistent configuration word set once and read rarely.',
+ use:'Specialty. Shines for boot-time or install-time options.',
+ limits:'Cryptic without its legend; not for live settings.',
+ origin:'Circuit-board DIP switches.',difficulty:'Expert — meaningless without the manual.',
+ prefer:'Config should be physical, deliberate, and survive resets.',
+ period:'1975-95 computing.'},
+'N10':{input:'Drag the inner jog for fine steps, the outer shuttle for velocity. Drag-only, two coupled gestures.',
+ solves:'Navigating a timeline at both frame and sweep speeds.',
+ use:'Specialty. Shines for scrubbing media or logs.',
+ limits:'Shuttle velocity mapping must be learned; hard to discover.',
+ origin:'Video edit decks.',difficulty:'Expert.',
+ prefer:'One control must cover both precision and distance.',
+ period:'1980s-90s edit suites.'},
+'R02':{input:'Drag to rotate the disc under the fixed hairline. Drag-only.',
+ solves:'Reading a continuous setting to a tenth of a division.',
+ use:'Specialty. Shines for calibration and lab-grade settings.',
+ limits:'Vernier reading is a skill; overkill for coarse values.',
+ origin:'Laboratory instruments.',difficulty:'Expert — vernier literacy.',
+ prefer:'The precision of the readback is the whole point.',
+ period:'1930s-60s lab styling.'},
+'R03':{input:'Click to throw the lever. Click-only.',
+ solves:'A binary with a big, satisfying, unmistakable throw.',
+ use:'Common on period gear. Shines where the thrown angle must be visible.',
+ limits:'Larger than a rocker for the same bit of state.',
+ origin:'Aviation and lab toggles.',difficulty:'Intuitive.',
+ prefer:'State must read from across the panel.'},
+'R04':{input:'Drag vertically to rotate. Drag-only.',
+ solves:'Same job as the rotary knob in a period skin.',
+ use:'Skin variant. Shines in 30s-50s compositions.',
+ limits:'As the rotary knob.',
+ origin:'Bakelite radio knobs.',difficulty:'Easy.',
+ prefer:'The console is dressed to that era.',
+ period:'1930s-50s.'},
+'R05':{input:'Drag each band vertically. Drag-only, one gesture per band.',
+ solves:'Shaping a curve by its bands, position = the curve itself.',
+ use:'Common as graphic EQ. Shines when the fader tops draw the response.',
+ limits:'Fixed bands; inter-band moves need many gestures.',
+ origin:'Filter banks, then graphic equalizers.',difficulty:'Easy.',
+ prefer:'The overall shape matters more than any single value.',
+ period:'1950s-60s studio filter styling.'},
+'R06':{input:'Click to snap to the next position. Click-only.',
+ solves:'One-of-few selection with an emphatic pointer.',
+ use:'Common on amps and lab gear. Shines for 2-4 coarse modes.',
+ limits:'Sequential stepping; the blade hides labels under it.',
+ origin:'Amplifier and instrument selectors.',difficulty:'Intuitive.',
+ prefer:'The selector should look like it belongs on a tube amp.',
+ period:'1940s-60s.'},
+'R12':{input:'Drag the handle along the slot. Drag-only.',
+ solves:'A signed level (boost/cut) around a marked zero.',
+ use:'Common on period hi-fi. Shines for tone and balance trims.',
+ limits:'Chrome furniture is wide for what it holds.',
+ origin:'Hi-fi tone controls.',difficulty:'Intuitive.',
+ prefer:'Zero is the default and deviation should be obvious.',
+ period:'1960s-70s hi-fi.'},
+'R14':{input:'Drag to rotate; the knurl ring turns with it. Drag-only.',
+ solves:'Slow, geared tuning across an engraved scale.',
+ use:'Skin variant for tuning. Shines in early-radio compositions.',
+ limits:'As the rotary knob; the engraved scale needs light to read.',
+ origin:'1920s receivers.',difficulty:'Easy.',
+ prefer:'The console leans into the earliest radio era.',
+ period:'1920s-30s.'},
+'R15':{input:'Drag the bandspread dial; click the band ring to switch bands. Mixed — the click half ports to Emacs, the drag half needs an idiom.',
+ solves:'One dial covering several ranges without losing resolution.',
+ use:'Specialty. Shines for shortwave-style band plus fine-tune.',
+ limits:'Band/bandspread interplay confuses newcomers.',
+ origin:'Multi-band receivers.',difficulty:'Expert.',
+ prefer:'The domain genuinely has bands (ranges, scopes, zoom levels).',
+ period:'1930s-50s.'},
+'R16':{input:'Click digits to buffer, OK to commit, C to clear. Click-only, fully Emacs-portable.',
+ solves:'Deliberate numeric entry with an explicit confirm step.',
+ use:'Common industrial. Shines for codes, channels, quantities.',
+ limits:'Slower than typing on a real keyboard when one exists.',
+ origin:'Industrial and security keypads.',difficulty:'Intuitive.',
+ prefer:'Entry must be explicit and cancellable, not live.'},
+'R18':{input:'Drag each lit column vertically; channels are independent. Drag-only.',
+ solves:'Two adjacent levels set by feel with lit numeric feedback.',
+ use:'Specialty. Shines for blend/mix pairs on processors.',
+ limits:'Compact but dense; the lit figure is the only precise cue.',
+ origin:'Compressor blend controls.',difficulty:'Easy.',
+ prefer:'Two coupled levels should sit shoulder to shoulder.',
+ period:'1970s-80s rack gear.'},
+'R19':{input:'Drag horizontally; the nearer flag follows, flags cannot cross. Drag-only.',
+ solves:'Marking a start/end region inside a visible signal.',
+ use:'Specialty. Shines for trims, loops, selections over a waveform.',
+ limits:'Flag semantics (S/E, no-cross) must be learned; small targets.',
+ origin:'Hardware samplers.',difficulty:'Expert.',
+ prefer:'The region IS the value and the data must stay visible.',
+ period:'1980s sampler styling.'},
+'R20':{input:'Drag each drum vertically to roll it. Drag-only.',
+ solves:'Stepped selection with a satisfying mechanical roll.',
+ use:'Specialty. Shines for paired coarse selections (EQ turnovers).',
+ limits:'Values hidden off-drum; sequential like all steppers.',
+ origin:'Channel-strip EQ rollers.',difficulty:'Easy.',
+ prefer:'A stepped pair should feel like machinery, not menus.',
+ period:'1960s console styling.'},
+'R21':{input:'Click a key; its LED takes the selection. Click-only.',
+ solves:'One-of-N program recall where the LED, not the key, carries state.',
+ use:'Common on rack gear. Shines for 4-8 named programs; maps directly onto a waybar workspace selector.',
+ limits:'One LED of state per key — no per-key value.',
+ origin:'Lexicon 224 program row.',difficulty:'Intuitive.',
+ prefer:'Selection state should read as a dot row even from an angle.',
+ period:'Late 1970s-80s digital rack.'},
+'R22':{input:'Click a zone to select it directly (A / AB / B). Click-only.',
+ solves:'Three-way routing with direct selection, no cycling.',
+ use:'Specialty. Shines for source/both/monitor selects.',
+ limits:'Three positions only; AB lighting both LEDs must be understood.',
+ origin:'Hi-fi speaker selectors.',difficulty:'Intuitive.',
+ prefer:'The middle state is a real combination, not a midpoint.'},
+'R23':{input:'Drag vertically to rotate. Drag-only.',
+ solves:'Same job as the rotary knob in a machined-metal skin.',
+ use:'Skin variant. Shines on 70s hi-fi faceplates.',
+ limits:'As the rotary knob.',
+ origin:'Spun-aluminum hi-fi knobs.',difficulty:'Easy.',
+ prefer:'The console is dressed silver-on-black.',
+ period:'1970s hi-fi.'},
+'R24':{input:'Click the stomp to latch; the jewel lights when engaged. Click-only.',
+ solves:'A rugged latch whose state glows at foot distance.',
+ use:'Common on pedals. Shines for engage/bypass.',
+ limits:'One bit; the jewel is the only state cue.',
+ origin:'Guitar stompboxes.',difficulty:'Intuitive.',
+ prefer:'The toggle should survive abuse and read from far away.'},
+'R27':{input:'Drag to step through the detents. Drag with detents — Emacs wants +/- steps.',
+ solves:'Stepped gain where each position is a calibrated, repeatable value.',
+ use:'Specialty. Shines for preamp gain in fixed dB steps.',
+ limits:'No values between detents by design.',
+ origin:'Neve-style console gain selectors.',difficulty:'Easy.',
+ prefer:'Repeatability beats continuous freedom.',
+ period:'1970s console styling.'},
+'R28':{input:'Click to step the disc to the next position, wrapping. Click-only.',
+ solves:'Heavy-duty source selection with an unmistakable handle.',
+ use:'Specialty. Shines for OFF/ON/COMBINE-style power routing.',
+ limits:'Sequential stepping; big footprint.',
+ origin:'Marine battery selectors.',difficulty:'Intuitive.',
+ prefer:'The selection routes something heavy and should look like it.'},
+'R29':{input:'Click the guard open, then click to throw; closing re-guards. Click-only, two-step.',
+ solves:'A switch that cannot be thrown by accident, period.',
+ use:'Specialty. Shines over anything irreversible.',
+ limits:'Two actions every time; reserve for genuine hazards.',
+ origin:'Aviation guarded switches.',difficulty:'Easy.',
+ prefer:'Like arm-to-fire but the guard state itself must be visible.'},
+'R32':{input:'Drag the dial to wind minutes on (turn to start); click the red knob to zero it (push to stop). Mixed — the stop is click-portable, the wind needs a key idiom in Emacs.',
+ solves:'Setting a duration and watching it run down, with OFF as a real position.',
+ use:'Common in kitchens and labs. Shines for timeboxes, pomodoros, sleep timers.',
+ limits:'One duration, coarse resolution; the demo runs a minute per second.',
+ origin:'Spring-driven interval timers.',difficulty:'Intuitive.',
+ prefer:'The remaining time should read as a dial position, not digits.'},
+'R33':{input:'Click an arrow or anywhere in its quadrant to step that direction. Click-only — the most Emacs-portable control in the kit, since it is literally arrow keys.',
+ solves:'Discrete navigation in four directions without a pointer position.',
+ use:'Common on remotes, cameras, avionics MFDs. Shines for menu and grid focus.',
+ limits:'Steps, not analog rate; diagonals need two presses; no held-repeat here.',
+ origin:'Remote-control and camera four-way pads.',difficulty:'Intuitive.',
+ prefer:'Focus moves through a grid or menu and each step is deliberate.'},
+'R34':{input:'Click a quadrant; the lever throws there and stays. Click-only, fully portable.',
+ solves:'One-of-four stateful selection where the selector itself shows the state.',
+ use:'Specialty. Shines for quadrant modes (channel pairs, axis pairs, routing).',
+ limits:'Exactly four states; diagonal labels need the printed legend.',
+ origin:'Test-panel quadrant switches with ball levers.',difficulty:'Intuitive.',
+ prefer:'The rocker steps a cursor; this holds a position. Reach for it when the four options are states, not directions.'},
+'R37':{input:'Click a row/column intersection to seat or pull a pin. Click-only, fully portable.',
+ solves:'Many-to-many routing where every active route stays simultaneously readable.',
+ use:'Specialty. Shines for source-to-destination mapping: audio routing, signal paths, rule wiring.',
+ limits:'Grid size is the ceiling; dense matrices need hover labels to stay legible.',
+ origin:'EMS VCS3 synthesizer pin matrix.',difficulty:'Easy — rows and columns must be read.',
+ prefer:'The patch bay shows one cable per route; the matrix shows the whole routing state at once.',
+ period:'Late 1960s-70s synth and routing gear.'},
+'R38':{input:'Press and hold; release anywhere drops to SAFE. Hold-based — pointer capture on web, a held key in Emacs; the only held-state control in the kit.',
+ solves:'Actions that must stop the moment the operator lets go.',
+ use:'Specialty. Shines for jog/feed, test firing, anything unsafe to walk away from.',
+ limits:'Fatigue is the design point — do not use it for long operations.',
+ origin:'Dead-man controls on trains, cranes, machine tools.',difficulty:'Intuitive.',
+ prefer:'Release-to-safe matters more than convenience.'},
+'R39':{input:'Click a hole; the wheel winds to the stop and returns, then the digit registers. Click-only, though the return animation is the point.',
+ solves:'Numeric entry with built-in pacing — larger digits genuinely take longer.',
+ use:'Specialty and nostalgic. Shines when entry should feel deliberate, or in period sets.',
+ limits:'Slow by design; ten digits only; no correction but a redial.',
+ origin:'Rotary telephone dials (Strowger pulse dialing).',difficulty:'Intuitive to those who have seen one.',
+ prefer:'The keypad enters numbers; the dial performs them.',
+ period:'1920s-70s telephony; hard-clashes with anything digital.'},
+'R40':{input:'Click a breaker to open or close it; TRIP pops one out under fault. A tripped breaker resets in two clicks: pull to off, then close. Click-only.',
+ solves:'Protective state the SYSTEM can change — no other control in the kit throws itself.',
+ use:'Common industrially. Shines for services/circuits that can fail independently: a tripped breaker is a crashed daemon.',
+ limits:'The tripped mid-position must be visually distinct or the story is lost.',
+ origin:'Aircraft and distribution breaker rows.',difficulty:'Easy — the two-step reset must be learned.',
+ prefer:'Faults should be visible at the switch, not in a separate alarm list.'},
+'R41':{input:'Press VERB or NOUN, type two digits, ENTR commits; V35 runs the lamp test, bad grammar lights OPR ERR. Click-only, fully portable.',
+ solves:'Reaching many functions from few keys via a verb-noun command grammar.',
+ use:'Specialty. Shines when the function space outgrows the panel space.',
+ limits:'The grammar must be memorized — the tradeoff is capability for discoverability.',
+ origin:'Apollo Guidance Computer DSKY.',difficulty:'Expert — by design.',
+ prefer:'Dozens of rare commands beat dozens of rare buttons.',
+ period:'1960s-70s spaceflight; the aesthetic survives in every command palette.'},
+'R42':{input:'Click to advance a step (or start from OFF); it then walks itself through the program. Click-only.',
+ solves:'A whole procedure shown as one rotating position — where you are, what came before, what is next.',
+ use:'Common on appliances and process gear. Shines for fixed multi-step sequences.',
+ limits:'The program is fixed at manufacture; skipping steps means clicking through them.',
+ origin:'Washing-machine and dishwasher cam timers.',difficulty:'Intuitive.',
+ prefer:'The sequence matters more than any single state — R30 shows a state, this shows a program.',
+ period:'1950s-80s appliance controls.'},
+'R48':{input:'Click to throw the blade open or closed. Click-only.',
+ solves:'A disconnect whose open state is physically visible — no lamp to trust, the gap IS the proof.',
+ use:'Specialty. Shines as a master isolate where "is it really off" matters.',
+ limits:'Semantically just a toggle; the value is entirely in the visible gap.',
+ origin:'Early power-station switchboards.',difficulty:'Intuitive.',
+ prefer:'A lamp says off; an air gap proves it. Use where proof beats economy of space.',
+ period:'1890s-1930s power switching; pure theater after that, and worth it.'},
+'R49':{input:'Drag each knob to its digit; the windows read the digits and the readout sums them. Drag with detents — Emacs wants per-digit +/-.',
+ solves:'Composing one precise value from independent per-decade digits.',
+ use:'Specialty. Shines for calibration values, resistances, counts where each digit is a deliberate choice.',
+ limits:'Range fixed by the decade count; slow for sweeping a value.',
+ origin:'General Radio decade resistance boxes.',difficulty:'Easy.',
+ prefer:'The thumbwheel sets a digit; this composes a NUMBER from digits with the arithmetic visible.',
+ period:'1930s-70s lab bench.'},
+'R50':{input:'Click one palm button, then the other within 0.5s; same hand twice or too slow faults. Click-only — the hardware holds both, the screen keeps the two-target-within-window grammar.',
+ solves:'Commands that must cost both hands — presence at two separated points before anything moves.',
+ use:'Specialty. Shines for genuinely destructive cycles where arm-to-fire is not enough.',
+ limits:'Deliberately annoying; the anti-tie-down rule must fault same-hand repeats or the guarantee is void.',
+ origin:'OSHA press and cutter controls.',difficulty:'Easy — the window must be learned.',
+ prefer:'Arm-to-fire proves intent; two-hand proves position. The strongest friction in the kit.'},
+'R51':{input:'Click a loop key: off → monitor → talk → off. Talk is exclusive; monitors are independent. Click-only, fully portable.',
+ solves:'Attending many channels at once while speaking on exactly one.',
+ use:'Specialty. Shines for channels, feeds, log streams — anything monitored in parallel.',
+ limits:'Flicker communicates activity, not content; past a dozen loops even controllers tiered them.',
+ origin:'NASA mission-control comm keysets.',difficulty:'Easy.',
+ prefer:'R21 selects one program; this holds many half-attended and one active.',
+ period:'1960s-90s console era; the pattern lives on in every notification mute matrix.'},
+'R52':{input:'Display plus the switch register: click a toggle to flip its bit and the activity pattern folds it in. The lamps take no input.',
+ solves:'Showing computation happening — activity, not just state.',
+ use:'Specialty. Shines for load, traffic, progress-with-texture; honest busy-ness.',
+ limits:'Patterns are read as rhythm and density, not decoded bit by bit.',
+ origin:'PDP-11/70 and IMSAI front panels.',difficulty:'Intuitive to watch, expert to read.',
+ prefer:'A spinner says busy; blinkenlights say HOW busy and with what rhythm.',
+ period:'1965-80 minicomputer era; the icon of computing at work.'},
+'R56':{input:'Drag the left half for temperature, the right for humidity. Drag-only, two surfaces.',
+ solves:'A categorical verdict from two values — the crossing point falls into a printed judgment.',
+ use:'Common on weather stations and comfort meters. Shines when the pair only matters as a combined condition: comfort, safe-operating region, duty envelope.',
+ limits:'The zone thresholds are opinions baked into the face; five zones is about the ceiling.',
+ origin:'Brass household comfort meters.',difficulty:'Intuitive — the verdict is literally written down.',
+ prefer:'N13 derives a number from the crossing, R55 reads two values separately; this one answers "is the combination OK" without asking you to do the math.'},
+'R54':{input:'Display in real use; drag vertically here to spin the tape. Drag-only for the demo.',
+ solves:'A long scale in a narrow window — the value sits at a fixed eye position with its neighborhood visible above and below.',
+ use:'Common in modern cockpits (airspeed, altitude) and rack meters. Shines when panel width is scarce and trend context matters.',
+ limits:'No at-a-glance needle angle; rate reads as scroll speed, which takes acclimation.',
+ origin:'Ki-57 remote tachometer; universal in glass cockpits since.',difficulty:'Easy.',
+ prefer:'The needle gauge gives angle-at-a-glance; the tape gives context-around-the-value in a tenth the width.'},
+'R55':{input:'Drag the left half for the left needle, right half for the right. Drag-only, two independent surfaces.',
+ solves:'Two related values in one instrument hole, each on its own mirrored scale.',
+ use:'Common on aircraft engine panels. Shines for pairs the eye checks together: fuel/oil, left/right, in/out.',
+ limits:'Both scales must share a range family or the mirroring misleads.',
+ origin:'Ki-57 fuel/oil pressure gauge; standard twin-engine instrument form.',difficulty:'Intuitive.',
+ prefer:'N13 crosses two needles to read their intersection; this houses two needles to read separately. Symmetry here means "both healthy looks symmetric".'},
+'R53':{input:'Display; click loads fresh paper. The real recorder takes no input.',
+ solves:'Cyclic time — the same hour lands at the same angle, so daily rhythm becomes shape.',
+ use:'Specialty. Shines for temperatures, loads, and rates with a daily or weekly cycle.',
+ limits:'One revolution of history; overlapping days need pen colors, like the hardware.',
+ origin:'Partlow and Honeywell circular chart recorders.',difficulty:'Easy.',
+ prefer:'The strip chart (N16) answers what just happened; this answers what this time of day usually looks like.',
+ period:'1900s-80s process recording.'},
+'10':{input:'Display; drag the gallery card to drive the demo value. Real use: none.',
+ solves:'A magnitude plus its rate of change, read peripherally.',
+ use:'Common. Shines where trend and zone matter more than digits.',
+ limits:'Coarse precision; needs face room.',
+ origin:'Moving-coil panel meters.',difficulty:'Intuitive.',
+ prefer:'The eye should catch movement, not read a number.'},
+'11':{input:'Display only; runs from the live demo signal.',
+ solves:'Two channels of level with peak behavior, at a glance.',
+ use:'Common in audio. Shines for stereo program level.',
+ limits:'Segment resolution; no history.',
+ origin:'LED meter bridges.',difficulty:'Intuitive.',
+ prefer:'Level must be watchable continuously without fatigue.'},
+'12':{input:'Display only.',
+ solves:'Signal strength in the smallest possible footprint.',
+ use:'Common. Shines in status bars and corners.',
+ limits:'Four levels of resolution, no more.',
+ origin:'Phone signal bars.',difficulty:'Intuitive.',
+ prefer:'One glance must answer roughly-how-strong.'},
+'13':{input:'Click a rung to set the demo level. Real use: display.',
+ solves:'A discrete level presented as steps rather than a continuum.',
+ use:'Common. Shines when the value naturally has rungs.',
+ limits:'Step count fixed by the ladder.',
+ origin:'Level ladders on meters and mixers.',difficulty:'Intuitive.',
+ prefer:'The scale is genuinely stepped (volume notches, severity).'},
+'14':{input:'Display; drag the gallery bar to drive the demo. Real use: none.',
+ solves:'How full, with zone warnings, in one horizontal read.',
+ use:'Common. Shines for capacity: fuel, disk, battery.',
+ limits:'One value; zones must be pre-agreed.',
+ origin:'Fuel gauges.',difficulty:'Intuitive.',
+ prefer:'The question is how much is left.'},
+'15':{input:'Display; click/drag drives the demo. Real use: none.',
+ solves:'Progress or proportion wrapped around a compact center.',
+ use:'Common in modern UI. Shines when the center holds the number.',
+ limits:'Angular reading is coarser than linear.',
+ origin:'Modern dashboard rings.',difficulty:'Intuitive.',
+ prefer:'A percentage needs to share space with its own label.',
+ period:'Modern (2010s UI) — pair with restraint on a period console.'},
+'16':{input:'Display only; live rolling data.',
+ solves:'The shape of recent history in a text-sized strip.',
+ use:'Common in dashboards. Shines inline next to a current value.',
+ limits:'No axes or absolute scale — trend only.',
+ origin:'Tufte sparklines.',difficulty:'Intuitive.',
+ prefer:'Direction-of-travel matters and space is text-sized.',
+ period:'Modern (2000s information design).'},
+'17':{input:'Display only; live signal.',
+ solves:'Raw signal texture over the last moments.',
+ use:'Specialty. Shines for audio/telemetry liveliness.',
+ limits:'Qualitative, not measured.',
+ origin:'Waveform strips on recorders.',difficulty:'Intuitive.',
+ prefer:'Presence and texture of a signal beat any single number.'},
+'N11':{input:'Display only; live trace.',
+ solves:'Seeing the waveform itself, not a summary of it.',
+ use:'Specialty. Shines for diagnosing shape, noise, clipping.',
+ limits:'Needs a real drawing surface everywhere it ports.',
+ origin:'Cathode-ray oscilloscopes.',difficulty:'Easy to watch, expert to interpret.',
+ prefer:'The shape of the signal is the information.'},
+'N12':{input:'Display only; live bands.',
+ solves:'Energy distribution across bands at a glance.',
+ use:'Common in audio. Shines for spectrum and per-core loads.',
+ limits:'Band resolution fixed; no phase/history.',
+ origin:'Spectrum analyzers and EQ displays.',difficulty:'Intuitive.',
+ prefer:'The question is where the energy sits, not how much total.'},
+'N13':{input:'Display only.',
+ solves:'Two related values on one face, their crossing point meaningful.',
+ use:'Specialty. Shines for forward/reflected power, in/out levels.',
+ limits:'Reading the iso-curves takes practice.',
+ origin:'Ham radio SWR meters.',difficulty:'Expert.',
+ prefer:'The ratio between two needles is the real value.'},
+'N14':{input:'Display only.',
+ solves:'A bounded scalar with universal instant recognition.',
+ use:'Common. Shines for temperature and anything temperature-like.',
+ limits:'Vertical footprint for one value.',
+ origin:'Mercury thermometers.',difficulty:'Intuitive.',
+ prefer:'The metaphor does the labeling for you.'},
+'N15':{input:'Display only.',
+ solves:'Industrial magnitude with legal-looking zone arcs.',
+ use:'Common industrial. Shines for pressure and load with red-line semantics.',
+ limits:'Face room; coarse read.',
+ origin:'Bourdon-tube pressure gauges (1849).',difficulty:'Intuitive.',
+ prefer:'The red zone must carry authority.'},
+'N16':{input:'Display only; live scrolling pen.',
+ solves:'A continuous value plotted against time, automatically.',
+ use:'Specialty. Shines for drift, cycles, events-over-hours.',
+ limits:'One channel per pen; horizontal footprint.',
+ origin:'Chart recorders.',difficulty:'Easy.',
+ prefer:'History matters as much as the current value.',
+ period:'Pre-digital logging (1900s-80s) styling.'},
+'N17':{input:'Display only.',
+ solves:'A signed relationship swinging around a marked zero.',
+ use:'Specialty. Shines for stereo correlation, balance, error.',
+ limits:'Meaning of + and - must be learned per domain.',
+ origin:'Broadcast correlation meters.',difficulty:'Moderate.',
+ prefer:'Zero is the healthy state and deviation is the signal.'},
+'N18':{input:'Display only.',
+ solves:'Charge state in discrete, honest cells.',
+ use:'Common. Shines for battery and quota displays.',
+ limits:'Cell resolution; green here is the LED green (sevgrn), not the phosphor green.',
+ origin:'Consumer battery gauges.',difficulty:'Intuitive.',
+ prefer:'Discrete cells match how users think about the resource.'},
+'R01':{input:'Display only; true VU ballistics on the live signal.',
+ solves:'Perceived loudness via standardized needle mass and dB law.',
+ use:'Common in audio. Shines as THE program-level meter.',
+ limits:'Slow by standard — misses fast peaks by design.',
+ origin:'The 1939 VU standard.',difficulty:'Easy.',
+ prefer:'Loudness as humans hear it beats instantaneous peaks.'},
+'R07':{input:'Display only.',
+ solves:'A dB read in a sealed porthole, panel-mount style.',
+ use:'Skin variant of the VU idea. Shines flush-mounted in rows.',
+ limits:'Small face, coarse scale.',
+ origin:'Panel meters on lab and broadcast gear.',difficulty:'Easy.',
+ prefer:'Several meters must sit in a disciplined row.',
+ period:'1950s-60s.'},
+'R08':{input:'Display only.',
+ solves:'A tuning/level window dressed in chrome for the living room.',
+ use:'Skin variant. Shines on consumer hi-fi faces.',
+ limits:'Ornamental furniture around a small read.',
+ origin:'Consumer hi-fi indicators.',difficulty:'Intuitive.',
+ prefer:'The console is consumer-facing, not lab-facing.',
+ period:'1960s-70s hi-fi.'},
+'R09':{input:'Display only.',
+ solves:'A flight-critical value with green/amber/red zones readable in the dark.',
+ use:'Specialty. Shines where out-of-zone must be instantly obvious.',
+ limits:'Zones must be authoritative or they train complacency.',
+ origin:'Aircraft engine and airspeed gauges.',difficulty:'Intuitive.',
+ prefer:'The zone, not the number, drives the response.'},
+'R13':{input:'Display only.',
+ solves:'A meter squeezed edgewise into a rack unit.',
+ use:'Common on rack gear. Shines where vertical space is the scarce resource.',
+ limits:'Compressed log scale takes a moment to read.',
+ origin:'Edgewise meters on processors.',difficulty:'Moderate.',
+ prefer:'Rack density forbids a round face.',
+ period:'1960s-80s rack.'},
+'R43':{input:'Drag: left/right banks, up/down pitches. The second 2D drag in the kit (after R26) — the hardest input model to port.',
+ solves:'Two-axis orientation read at a glance — no pair of scalar gauges can match it.',
+ use:'Specialty. Shines for anything with attitude, tilt, or balance across two axes.',
+ limits:'Needs interpretation training in the real world; here the drag teaches it.',
+ origin:'Cockpit artificial horizons.',difficulty:'Moderate.',
+ prefer:'The two values are physically one orientation, not two numbers.'},
+'R44':{input:'Drag to set the amber bug (commanded); the needle (actual) chases it with servo lag. Drag-only.',
+ solves:'Setpoint versus reality on one dial — the gap between bug and needle is the state.',
+ use:'Common in cockpits and process control. Shines for anything with a target and a lagging actual: thermostat, autopilot, motor speed.',
+ limits:'Wrap-around chase must take the short way or the story reads wrong.',
+ origin:'HSI heading bugs, synchro repeaters, motorized pointers.',difficulty:'Easy.',
+ prefer:'Showing only the actual hides the intent; showing both shows the system working.'},
+'R17':{input:'Display only; live trace. Screen chips switch the phosphor family.',
+ solves:'The round-tube scope look: waveform inside bezel and screws.',
+ use:'Skin variant of the oscilloscope. Shines in period lab compositions.',
+ limits:'Round face wastes corners; same porting need as any trace.',
+ origin:'Round-CRT lab scopes.',difficulty:'Easy to watch.',
+ prefer:'The era calls for a tube, not a rectangle.',
+ period:'1940s-60s.'},
+'18':{input:'Click cycles the demo state. Real use: display.',
+ solves:'One state, one lamp: ok, busy, fault at a glance.',
+ use:'Common everywhere. Shines beside the thing it describes.',
+ limits:'Color alone excludes colorblind users — pair with a label.',
+ origin:'Pilot lamps.',difficulty:'Intuitive.',
+ prefer:'A single state needs the smallest honest indicator.'},
+'19':{input:'Display only.',
+ solves:'A short status word with categorical color.',
+ use:'Common. Shines in lists and headers.',
+ limits:'Text-sized; not for continuous values.',
+ origin:'Tags and badges in modern UI.',difficulty:'Intuitive.',
+ prefer:'The state has a name and belongs inline.'},
+'20':{input:'Display only.',
+ solves:'Labeled values aligned for vertical scanning.',
+ use:'Common. Shines for 3-8 key/value facts.',
+ limits:'Static layout; long values break alignment.',
+ origin:'Instrument data plates.',difficulty:'Intuitive.',
+ prefer:'Several named numbers must be scannable in a column.'},
+'21':{input:'None — panel furniture.',
+ solves:'Naming a region so controls need shorter labels.',
+ use:'Common. Shines grouping related controls under one legend.',
+ limits:'Informational only.',
+ origin:'Engraved faceplate legends.',difficulty:'Intuitive.',
+ prefer:'A cluster needs a name more than each member does.'},
+'22':{input:'Display region; the overlay pair adds copy/clear.',
+ solves:'A place for streaming text output that is clearly not chrome.',
+ use:'Common in tools. Shines for logs and command output.',
+ limits:'Unstructured text; needs its own scroll discipline.',
+ origin:'Terminal wells in tool UIs.',difficulty:'Intuitive.',
+ prefer:'Output length is unbounded and text-shaped.'},
+'23':{input:'Display only; transient.',
+ solves:'A message that matters now but not forever.',
+ use:'Common. Shines for confirmations and background events.',
+ limits:'Easy to miss; sticky errors need a dismissal policy.',
+ origin:'Status lines and toasts.',difficulty:'Intuitive.',
+ prefer:'The message should not occupy permanent space.'},
+'26':{input:'Display only.',
+ solves:'A glowing digit with unmatched period warmth.',
+ use:'Specialty. Shines for counters and clocks meant to be loved.',
+ limits:'Neon is only ever orange — no color variants exist honestly; digits stack in depth.',
+ origin:'Nixie tubes.',difficulty:'Intuitive.',
+ prefer:'Warmth and era matter more than crispness.',
+ period:'1955-75 hard period: sits with keypads and telegraphs, clashes with VFD and LED seven-seg.'},
+'N20':{input:'Display only; flaps animate on change.',
+ solves:'A changed value that announces itself mechanically.',
+ use:'Specialty. Shines for arrivals-board style updates.',
+ limits:'Character set and flap speed are the charm and the constraint.',
+ origin:'Split-flap rail/airport boards.',difficulty:'Intuitive.',
+ prefer:'The moment of change deserves sound and motion.',
+ period:'1960s-90s boards.'},
+'N21':{input:'Display only.',
+ solves:'Digits readable at distance with segment honesty.',
+ use:'Common. Shines for counts, clocks, channel numbers.',
+ limits:'Digits and a few letters only.',
+ origin:'LED seven-segment displays.',difficulty:'Intuitive.',
+ prefer:'Numeric state should read like an appliance.',
+ period:'1975 onward.'},
+'N22':{input:'Display only; scrolls.',
+ solves:'More text than fits, one glowing window at a time.',
+ use:'Common on period appliances. Shines for track names, status crawl.',
+ limits:'Reading takes waiting; blue-green is the honest VFD color.',
+ origin:'Vacuum-fluorescent displays.',difficulty:'Intuitive.',
+ prefer:'A short crawl beats truncation.',
+ period:'1980s appliance/hi-fi.'},
+'R45':{input:'Display; the gallery makes each disc clickable. The real sign takes no input.',
+ solves:'Persistent visual state with zero holding power — the disc stays where it flipped.',
+ use:'Common on transit signs and industrial boards. Shines for slow-changing state that must survive power loss.',
+ limits:'Two colors, no glow (needs ambient light); flip rate limits animation.',
+ origin:'Flip-dot transit signs and magnetic indicators.',difficulty:'Intuitive.',
+ prefer:'The dot matrix glows and forgets; the flip-disc is dimmer but remembers.',
+ period:'1960s-90s transit era; still in service.'},
+'R46':{input:'Click = one pulse; the glow steps a cathode clockwise, wrapping with a carry blink. Click-only.',
+ solves:'A count where the counting itself is visible — position, motion, and carry.',
+ use:'Specialty. Shines for event counters and rate displays where each increment should be seen.',
+ limits:'One digit per tube; chains needed for big numbers, exactly like the hardware.',
+ origin:'Dekatron glow-transfer counter tubes.',difficulty:'Intuitive.',
+ prefer:'The nixie announces the digit; the Dekatron performs the increment.',
+ period:'1950s-60s counting gear; pre-IC logic made visible.'},
+'R47':{input:'Click the lever to cycle the gear; lamps pulse amber through transit. Click-only.',
+ solves:'Spatial status — each lamp is a physical thing in its physical place, plus an honest in-between state.',
+ use:'Specialty. Shines when components have positions (gear, doors, valves) and "moving" is a real state.',
+ limits:'The three-greens convention must be known; transit needs a timeout in real use.',
+ origin:'Aircraft landing-gear panels.',difficulty:'Intuitive.',
+ prefer:'The annunciator names conditions; this maps them onto the machine body.'},
+'N23':{input:'Click a cell to raise or cycle its alarm; ACK steadies the flashing master caution, RESET clears the board, TEST proves the bulbs. Click-only.',
+ solves:'Many named conditions, each with its own lit cell.',
+ use:'Specialty. Shines for fault boards where absence of light is health.',
+ limits:'Cell text must stay short; layout is fixed.',
+ origin:'Power-plant and aircraft annunciators.',difficulty:'Intuitive.',
+ prefer:'Every fault deserves a permanent, named home.'},
+'N24':{input:'Display only.',
+ solves:'A few states as colored jewels with period glow.',
+ use:'Common on period gear. Shines for power/signal/fault triples.',
+ limits:'Jewel green (jewel-g) is its own green — distinct from phosphor and VFD.',
+ origin:'Jewel pilot lamps.',difficulty:'Intuitive.',
+ prefer:'Indicator lights should be furniture, not pixels.',
+ period:'1930s-60s.'},
+'N25':{input:'Display only; counts.',
+ solves:'A resettable position count with mechanical drum digits.',
+ use:'Specialty. Shines for tape position, cycle counts.',
+ limits:'Relative number — meaningful only against a noted zero.',
+ origin:'Tape deck counters.',difficulty:'Easy.',
+ prefer:'A relative index beats an absolute timestamp.',
+ period:'1950s-80s decks.'},
+'N26':{input:'Display only; live time.',
+ solves:'Time of day, read culturally, instantly.',
+ use:'Common. Shines as console furniture that also works.',
+ limits:'Analog read is approximate by nature.',
+ origin:'Clocks.',difficulty:'Intuitive.',
+ prefer:'Approximate time at a glance beats digits.'},
+'N27':{input:'Display only.',
+ solves:'Where in the band the tuning currently sits.',
+ use:'Specialty. Shines paired with a tuning control.',
+ limits:'Furniture without its knob.',
+ origin:'Radio dial scales.',difficulty:'Intuitive.',
+ prefer:'Position-in-range should be visible independent of the control.',
+ period:'1930s-60s radio.'},
+'N28':{input:'Display only in the gallery; real bays patch.',
+ solves:'Which connections exist, shown as physical presence.',
+ use:'Specialty. Shines for routing state.',
+ limits:'Dense; jack labels carry all the meaning.',
+ origin:'Telephone and studio patch bays.',difficulty:'Moderate.',
+ prefer:'Connections are the state worth showing.'},
+'R10':{input:'Click steps the page. Screen chips switch the family. Otherwise display.',
+ solves:'Several small status pages in one fixed window.',
+ use:'Common on gear. Shines for SYS/NET/TIME-style rotations.',
+ limits:'One page visible; page order must be learned.',
+ origin:'Status LCDs on rack gear.',difficulty:'Intuitive.',
+ prefer:'A few short pages beat one crowded panel.',
+ period:'1980s-90s LCD era.'},
+'R11':{input:'Display only; the flag slides in on fault.',
+ solves:'A fault state that physically occludes the healthy read.',
+ use:'Specialty. Shines where ignoring a failure must be impossible.',
+ limits:'Binary; the barber pole means only one thing.',
+ origin:'Avionics warning flags.',difficulty:'Intuitive.',
+ prefer:'Failure must block the reading, not sit next to it.',
+ period:'1950s-70s avionics.'},
+'R25':{input:'Display only.',
+ solves:'Letters and digits in segments, still glowing, still period.',
+ use:'Specialty. Shines for short mode names (ZOOM, HALL).',
+ limits:'Fourteen segments approximate the alphabet, with charm.',
+ origin:'Alphanumeric LED displays.',difficulty:'Intuitive.',
+ prefer:'Words must glow like the rest of the rack.',
+ period:'1970s-80s.'},
+'R26':{input:'Drag the peak anywhere on the plot — the first 2D drag in the kit. Drag-only.',
+ solves:'A frequency response edited by grabbing the curve itself.',
+ use:'Common in DAWs. Shines for parametric EQ, any x/y parameter pair.',
+ limits:'Log-frequency mapping assumed knowledge; 2D drag is the hardest Emacs port in the kit.',
+ origin:'Mastering processor displays.',difficulty:'Expert.',
+ prefer:'Two coupled parameters live naturally on a plot.',
+ period:'Modern (digital-era display).'},
+'R35':{input:'Live readout initialized to today; click rolls midnight forward one day. Click-only.',
+ solves:'A compound value (weekday plus date) read at one index from coaxial discs.',
+ use:'Specialty. Shines wherever two coupled discrete values should share one pointer.',
+ limits:'The 31-slot date disc ignores short months — faithfully, since that is the real complication problem.',
+ origin:'Wristwatch day-date movements.',difficulty:'Intuitive.',
+ prefer:'Digits would work, but the mechanism itself is the display worth showing.',
+ period:'1950s-onward watchmaking; the exposed-movement look is the charm.'},
+'R36':{input:'Display; the gallery makes each dot clickable so the bitmap is paintable. The real part takes no input.',
+ solves:'A free-form bitmap in indicator form — glyphs, icons, patterns, tiny animations from one part.',
+ use:'Common in signs, elevators, synth modules. Shines when one small display must show anything.',
+ limits:'8x8 holds one glyph; multiplex flicker shows on camera; red-first technology.',
+ origin:'LED dot-matrix modules (HP displays, scrolling signs, the K4816 faceplate).',difficulty:'Intuitive.',
+ prefer:'Segments cover digits; reach for the matrix when the content is not characters.',
+ period:'1970s-90s LED-matrix era styling; the part is still made.'},
+'R30':{input:'Click steps the state, wrapping. Click-only.',
+ solves:'Current state shown by a pointer into labeled sectors.',
+ use:'Specialty. Shines for coarse machine states (RUN/STOP/TEST).',
+ limits:'Sector count fixed by the dial face.',
+ origin:'Ship engine telegraphs.',difficulty:'Intuitive.',
+ prefer:'State should read like a dial position, not a word.',
+ period:'1900s-50s marine styling.'},
+'R31':{input:'Click marks the current bearing. Screen chips switch the phosphor. Otherwise live display.',
+ solves:'Position around you, decaying like a real PPI.',
+ use:'Specialty. Shines for anything with bearing and range.',
+ limits:'The sweep metaphor implies periodic refresh.',
+ origin:'Radar plan-position indicators.',difficulty:'Easy to watch.',
+ prefer:'Angular position matters and the era wants a tube.',
+ period:'1940s-70s radar styling.'},
+};
+const INFO_FIELDS=[['input','input'],['solves','solves'],['use','use'],['limits','limits'],
+ ['origin','origin'],['difficulty','difficulty'],['prefer','prefer when'],['period','period']];
+const VLAMP_TITLES={off:'validation: not done',amber:'validation: in progress',green:'validation: done'};
+/* card(host, no, name, htmlOrBuild, note) — the declarative card record.
+ htmlOrBuild: a legacy stage-HTML string, or a builder function (stage, rd) => handle
+ that instantiates a GW.* widget into the stage; rd(txt) writes the card readout. */
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="wrd" id="rd-${no}">—</div><div class="wnote">${note}</div>`;
- host.appendChild(c); return c;
+ const isBuild=typeof html==='function';
+ const c=document.createElement('div'); c.className='card'; c.id='card-'+no;
+ c.innerHTML=`<div class="wname"><span class="no">${no}</span>${name}<span class="vlamp" data-v="off"></span></div>`+
+ `<div class="stagew">${isBuild?'':html}</div><div class="wrd" id="rd-${no}">—</div>`+
+ `<div class="opts"></div><div class="wnote">${note}</div>`;
+ const lamp=c.querySelector('.vlamp'), VKEY='gv-'+no, VSTATES=['off','amber','green'];
+ const setV=v=>{lamp.dataset.v=v;lamp.title=VLAMP_TITLES[v];};
+ setV(localStorage.getItem(VKEY)||'off');
+ lamp.addEventListener('click',e=>{e.stopPropagation();
+ const v=VSTATES[(VSTATES.indexOf(lamp.dataset.v)+1)%3];
+ setV(v);localStorage.setItem(VKEY,v);});
+ const inf=(typeof INFO!=='undefined')&&INFO[no];
+ if(inf){const d=document.createElement('details'); d.className='winfo';
+ let rows='';
+ for(const [k,lbl] of INFO_FIELDS) if(inf[k]) rows+=`<div class="ik">${lbl}</div><div class="iv">${inf[k]}</div>`;
+ if(inf.ref) rows+=`<div class="ik">reference</div><div class="iv"><a href="${inf.ref}" target="_blank">photo</a></div>`;
+ d.innerHTML=`<summary>spec sheet</summary><div class="igrid">${rows}</div>`;
+ c.appendChild(d);}
+ host.appendChild(c);
+ if(isBuild)c.gw=html(c.querySelector('.stagew'),txt=>setRd(no,txt));
+ return c;
}
function setRd(no,txt){const e=$('rd-'+no);if(e)e.textContent=txt;}
-function buildBars(el,n){el.innerHTML='';for(let k=0;k<n;k++)el.appendChild(document.createElement('i'));}
-
-/* ---- pointer-drag helpers ---- */
-function dragX(el,onPct){ el.style.touchAction='none';
- const at=e=>{const r=el.getBoundingClientRect();onPct(Math.max(0,Math.min(100,(e.clientX-r.left)/r.width*100)));};
- el.addEventListener('pointerdown',e=>{el.setPointerCapture(e.pointerId);at(e);
- const mv=ev=>at(ev),up=()=>{el.removeEventListener('pointermove',mv);el.removeEventListener('pointerup',up);el.removeEventListener('pointercancel',up);};
- el.addEventListener('pointermove',mv);el.addEventListener('pointerup',up);el.addEventListener('pointercancel',up);e.preventDefault();});
-}
-function dragY(el,onPct){ el.style.touchAction='none';
- const at=e=>{const r=el.getBoundingClientRect();onPct(Math.max(0,Math.min(100,(r.bottom-e.clientY)/r.height*100)));};
- el.addEventListener('pointerdown',e=>{el.setPointerCapture(e.pointerId);at(e);
- const mv=ev=>at(ev),up=()=>{el.removeEventListener('pointermove',mv);el.removeEventListener('pointerup',up);el.removeEventListener('pointercancel',up);};
- el.addEventListener('pointermove',mv);el.addEventListener('pointerup',up);el.addEventListener('pointercancel',up);e.preventDefault();});
-}
-function dragDelta(el,get,set,opts){ opts=opts||{}; el.style.touchAction='none';
- const min=opts.min==null?0:opts.min, max=opts.max==null?100:opts.max, sens=opts.sens||((max-min)/160);
- el.addEventListener('pointerdown',e=>{if(opts.stop)e.stopPropagation();el.setPointerCapture(e.pointerId);
- const y0=e.clientY, v0=get();
- const mv=ev=>{let v=v0+(y0-ev.clientY)*sens;set(Math.max(min,Math.min(max,v)));};
- const up=()=>{el.removeEventListener('pointermove',mv);el.removeEventListener('pointerup',up);el.removeEventListener('pointercancel',up);};
- el.addEventListener('pointermove',mv);el.addEventListener('pointerup',up);el.addEventListener('pointercancel',up);e.preventDefault();});
-}
-
-/* ---- seven-segment digit builder ---- */
-const SEG={'0':'abcdef','1':'bc','2':'abged','3':'abgcd','4':'fbgc','5':'afgcd','6':'afgedc','7':'abc','8':'abcdefg','9':'abcdfg','-':'g',' ':''};
-function hseg(cy){return `5,${cy} 7,${cy-2} 17,${cy-2} 19,${cy} 17,${cy+2} 7,${cy+2}`;}
-function vseg(cx,ty,by){return `${cx},${ty} ${cx-2},${ty+2} ${cx-2},${by-2} ${cx},${by} ${cx+2},${by-2} ${cx+2},${ty+2}`;}
-const SEGPTS={a:hseg(3),g:hseg(22),d:hseg(41),f:vseg(5,4,21),b:vseg(19,4,21),e:vseg(5,23,40),c:vseg(19,23,40)};
-function seg7(ch,cls){
- const lit=SEG[ch]||''; let s=`<svg class="seg7 ${cls||''}" viewBox="0 0 24 44">`;
- for(const k of 'abcdefg'){const on=lit.includes(k);
- const col=on?(cls==='red'?'var(--sevred)':'var(--sevgrn)'):'var(--sevoff)';
- s+=`<polygon points="${SEGPTS[k]}" fill="${col}"/>`;}
- return s+'</svg>';
-}
+/* drag helpers, seg7, buildBars live in widgets.js (GW) */
/* ============ CONTROLS ============ */
const C=$('controls');
-card(C,'01','Toggle switch',
- `<span id="toggle01"><span class="switch on"></span></span>`,
- '<b>on / off.</b> The faceplate master control — power, mute, enable. Click to flip; readout shows the state.');
+card(C,'01','Slide toggle',
+ (st,rd)=>GW.slideToggle(st,{on:true,onChange:(v,t)=>rd(t)}),
+ '<b>on / off, the touchscreen way.</b> The one born-digital control in the kit — the smartphone pill toggle the live net/bt/audio panels use. Click to flip; readout shows the state. Physical cousins: the bat-handle (R03) and the rocker (N01).');
card(C,'02','Console key',
- `<span id="keys02"><button class="key on">LIVE</button><button class="key">SCAN</button><button class="key red">MUTED</button></span>`,
+ (st,rd)=>GW.consoleKeys(st,{onChange:(i,t)=>rd(t)}),
'<b>physical push button.</b> Mutually exclusive — click one to engage. Gold = live, terracotta = muted.');
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>`,
+ (st,rd)=>GW.faderH(st,{value:68,onChange:(v,t)=>rd(t)}),
'<b>continuous 0-100.</b> Per-device volume, brightness. Drag the gold cap; readout tracks.');
card(C,'04','Vertical fader',
- `<div class="vfader" id="v1"><div class="slot"><div class="fill" style="height:60%"></div></div><div class="cap" style="bottom:60%"></div></div>
- <div class="vfader" id="v2"><div class="slot"><div class="fill" style="height:35%"></div></div><div class="cap" style="bottom:35%"></div></div>`,
+ (st,rd)=>{let ready=false;
+ const upd=()=>{if(ready)rd('A '+Math.round(a.get())+' · B '+Math.round(b.get()));};
+ const a=GW.faderV(st,{value:60,onChange:upd});
+ const b=GW.faderV(st,{value:35,onChange:upd});
+ ready=true;upd();return {a,b};},
'<b>channel-strip style.</b> A mixer column per device. Drag each fader up or down.');
card(C,'05','Rotary knob',
- `<span class="knob" id="knob"><span class="ind" id="kind"></span></span>`,
+ (st,rd)=>GW.knob(st,{value:53,onChange:v=>rd('gain '+Math.round(v))}),
'<b>dial in a value.</b> Volume/gain the analog way. Drag up/down to turn; readout shows the level.');
card(C,'06','Segmented selector',
- `<div class="seg" id="seg06"><button class="on">TIMER</button><button>ALARM</button><button>POMO</button></div>`,
+ (st,rd)=>GW.segmented(st,{onChange:(i,t)=>rd(t)}),
'<b>pick one of a few.</b> Timer type, layout mode. Click a segment; readout names the choice.');
card(C,'07','Chip toggle',
- `<span class="chip on" id="chip07">discoverable on</span>`,
+ (st,rd)=>GW.chipToggle(st,{on:true,onChange:(v,t)=>rd(t)}),
'<b>inline binary.</b> A soft toggle inside a line of text. Click to flip; gold when on.');
card(C,'08','Arm-to-fire',
- `<button class="arm" id="arm">forget</button>`,
+ (st,rd)=>GW.armButton(st,{onChange:(v,t)=>rd(t)}),
'<b>two-stage confirm.</b> Destructive actions. First click arms (red), second fires. Readout shows the stage.');
card(C,'09','Lamp row',
- `<div class="lrow" id="lrow9"><span class="lamp gold"></span><span class="who"><b>WH-1000XM4</b></span><span class="what">tap to connect</span></div>`,
+ (st,rd)=>GW.lampRow(st,{onChange:(i,t)=>rd(t)}),
'<b>actionable list item.</b> Lamp + name + status. Click to cycle idle → connecting → connected.');
card(C,'24','Rotary selector',
- `<span class="rotsel" id="rotsel24">
- <span class="pos" style="left:14%;top:53%">4</span><span class="pos" style="left:28%;top:18%">6</span><span class="pos on" style="left:50%;top:7%">8</span><span class="pos" style="left:72%;top:18%">10</span><span class="pos" style="left:86%;top:53%">12</span>
- <span class="knob"><span class="ind"></span></span>
- </span>`,
+ (st,rd)=>GW.rotarySelector(st,{onChange:(v,t)=>rd(t)}),
'<b>pick one of N by position.</b> Printed detents, the pointer names the value. Click to turn; readout shows it.');
card(C,'25','Slide-rule dial',
- `<span class="tuner" id="tuner" tabindex="0" role="slider" aria-label="slide-rule value">
- <span class="tick" style="left:12px"></span><span class="tick" style="left:51px"></span><span class="tick" style="left:90px"></span><span class="tick" style="left:129px"></span><span class="tick" style="left:168px"></span>
- <span class="mk" style="left:12px">4</span><span class="mk" style="left:51px">6</span><span class="mk on" style="left:90px">8</span><span class="mk" style="left:129px">10</span><span class="mk" style="left:168px">12</span>
- <span class="ndl" id="tundl" style="left:90px"></span>
- </span>`,
+ (st,rd)=>GW.slideRule(st,{onChange:(v,t)=>rd(t)}),
'<b>value on a printed scale.</b> A lit pointer glides a warm-backlit strip. Click a mark to jump, or focus it and press ←/→ (↑/↓); readout shows it.');
card(C,'N01','Rocker power switch',
- `<span class="rocker on" id="rocker"><span class="half top">ON</span><span class="half bot">OFF</span></span>`,
+ (st,rd)=>GW.rocker(st,{on:true,onChange:(v,t)=>rd(t)}),
'<b>hard on / off, lit legend.</b> A master power paddle — the pressed half glows. Click to rock.');
card(C,'N02','Transport cluster',
- `<div style="display:flex;flex-direction:column;gap:6px;align-items:center">
- <div class="reels"><span class="reel spin"><i></i><i></i><i></i></span><span class="reel spin"><i></i><i></i><i></i></span></div>
- <div class="transport" id="tport">
- <button class="tbtn" data-t="rew" onclick="transport(this)">⏮</button>
- <button class="tbtn on" data-t="play" onclick="transport(this)">▶</button>
- <button class="tbtn" data-t="stop" onclick="transport(this)">⏹</button>
- <button class="tbtn rec" data-t="rec" onclick="transport(this)">⏺</button>
- </div></div>`,
+ (st,rd)=>GW.transport(st,{mode:'play',animate:!reduced,onChange:(m,t)=>rd(t)}),
'<b>run / pause / capture.</b> One mode lit at a time, reels turn while it plays. Click a button.');
card(C,'N03','Radio preset bank',
- `<div class="radiobank" id="rbank">
- <button class="preset on" onclick="preset(this)">WIFI</button>
- <button class="preset" onclick="preset(this)">ETH</button>
- <button class="preset" onclick="preset(this)">CELL</button>
- <button class="preset" onclick="preset(this)">OFF</button>
- </div>`,
+ (st,rd)=>GW.presetBank(st,{onChange:(i,t)=>rd(t)}),
'<b>pick one, the rest release.</b> The car-radio preset row — mechanically exclusive. Click one.');
card(C,'N04','Concentric dual knob',
- `<span class="dualknob">
- <span class="outer" id="dko"><span class="tick" style="transform:rotate(-120deg)"></span><span class="tick" style="transform:rotate(120deg)"></span></span>
- <span class="inner" id="dki"><span class="ind"></span></span>
- </span>`,
+ (st,rd)=>GW.dualKnob(st,{outer:50,inner:50,onChange:(o,i)=>rd('bass '+Math.round(o)+' · treble '+Math.round(i))}),
'<b>two values on one spindle.</b> Bass / treble, coarse / fine. Drag the outer ring or inner cap — each is independent.');
card(C,'N05','Rotary encoder + LED ring',
- `<span class="encoder" id="enc"></span>`,
+ (st,rd)=>GW.encoder(st,{value:7,onChange:(v,t)=>rd(t)}),
'<b>endless dial, lit position.</b> A detentless encoder; the LED arc lights to the level and the value keeps accumulating. Drag to turn.');
card(C,'N06','Keyed mode switch',
- `<span class="keylock" id="keylock6">
- <span class="kpos" style="left:16%;top:64%">OFF</span><span class="kpos on" style="left:50%;top:12%">ON</span><span class="kpos" style="left:84%;top:64%">RUN</span>
- <span class="body" onclick="keyTurn()"><span class="bit" id="kbit"></span><span class="barrel"></span></span>
- </span>`,
+ (st,rd)=>GW.keySwitch(st,{index:1,onChange:(i,t)=>rd(t)}),
'<b>guarded three-position mode.</b> Off · on · run. The key-bit points at the live position. Click to turn.');
card(C,'N07','Center-detented crossfader',
- `<div class="xfader" id="xf">
- <div class="slot"></div><div class="detent"></div>
- <span class="end" style="left:0">A</span><span class="end" style="right:0">B</span>
- <div class="cap" id="xfcap" style="left:50%"></div>
- </div>`,
+ (st,rd)=>GW.crossfader(st,{onChange:(v,t)=>rd(t)}),
'<b>throw to either side of zero.</b> A fader notched at center — balance, L/R mix. Drag; readout shows the signed value.');
card(C,'N08','Thumbwheel',
- `<div class="thumbw"><span class="thumbwheel" id="thumbwheel"></span><span class="win" id="thval">42</span></div>`,
+ (st,rd)=>GW.thumbwheel(st,{value:42,onChange:(v,t)=>rd(t)}),
'<b>ribbed edge-wheel, one value.</b> A knurled wheel with a windowed number. Drag up/down to roll.');
card(C,'N09','DIP-switch bank',
- `<span class="dip" id="dips">
- <span class="dipsw on" onclick="dip(this)"><i></i></span><span class="dipsw" onclick="dip(this)"><i></i></span>
- <span class="dipsw on" onclick="dip(this)"><i></i></span><span class="dipsw on" onclick="dip(this)"><i></i></span>
- <span class="dipsw" onclick="dip(this)"><i></i></span><span class="dipsw" onclick="dip(this)"><i></i></span>
- </span>`,
+ (st,rd)=>GW.dipBank(st,{onChange:(w,t)=>rd(t)}),
'<b>a bank of hard flags.</b> Hardware slide switches — up is on. Click each to flip; readout is the binary word.');
card(C,'N10','Jog / shuttle wheel',
- `<span class="jog" id="jog10"><span class="shuttle" style="--sh:40deg"></span><span class="inner" id="jogi"><span class="dimple"></span></span></span>`,
+ (st,rd)=>GW.jogWheel(st,{onChange:(v,t)=>rd(t)}),
'<b>scrub fine, shuttle fast.</b> The edit-deck wheel. Drag to jog the inner wheel; readout shows the position.');
card(C,'R02','Calibrated vernier dial',
- `<svg id="vern" class="rsvg drag" viewBox="0 0 150 150" width="150" height="150"></svg>`,
+ (st,rd)=>GW.vernierDial(st,{value:42.0,onChange:(v,t)=>rd(t)}),
'<b>precision on a rotating disc.</b> The big lab dial — the calibrated disc turns under a fixed hairline. Drag up/down; readout reads to a tenth. After a 1950s lab-oscillator vernier.');
card(C,'R03','Bat-handle toggle',
- `<svg id="batsw" class="rsvg press" viewBox="0 0 70 90" width="70" height="90"></svg>`,
+ (st,rd)=>GW.batToggle(st,{on:true,onChange:(v,t)=>rd(t)}),
'<b>the period power switch.</b> Chrome bat lever on a hex bushing, snaps up ON / down OFF. Click to throw it. After a lab-instrument panel toggle.');
card(C,'R04','Bakelite fluted knob',
- `<svg id="bakek" class="rsvg drag" viewBox="0 0 110 110" width="110" height="110"></svg>`,
+ (st,rd)=>GW.flutedKnob(st,{value:63,onChange:(v,t)=>rd(t)}),
'<b>the skirted console knob.</b> Scalloped bakelite skirt, glossy dome, amber index over a printed 0-10 scale. Drag up/down to turn. After a vintage console mixer knob.');
card(C,'R05','Filter slider bank',
- `<svg id="fbank" class="rsvg" viewBox="0 0 190 96" width="190" height="96"></svg>`,
+ (st,rd)=>GW.filterBank(st,{onChange:(v,t)=>rd(t)}),
'<b>a wall of band faders.</b> One slider per band, arrow-head caps on black tracks, dB rail at the side. Drag any cap; readout names band and level. After a variable multi-band filter.');
card(C,'R06','Chicken-head selector',
- `<svg id="chick" class="rsvg press" viewBox="0 0 110 96" width="110" height="96"></svg>`,
+ (st,rd)=>GW.chickenHead(st,{index:2,onChange:(i,t)=>rd(t)}),
'<b>the pointer-lever switch.</b> The tapered bakelite lever IS the indicator — it aims at the engraved position. Click to step through. After a modulator mode switch.');
card(C,'R12','Chrome slot fader',
- `<svg id="slotf" class="rsvg drag" viewBox="0 0 90 150" width="90" height="150"></svg>`,
+ (st,rd)=>GW.slotFader(st,{value:-4,onChange:(v,t)=>rd(t)}),
'<b>the console gain slide.</b> A chrome T-handle rides a recessed slot, the dB scale engraved on the plate — gain above zero, cut below. Drag; readout shows signed dB. After a vintage channel-strip output fader.');
card(C,'R14','Spade-pointer tuning knob',
- `<svg id="spade" class="rsvg drag" viewBox="0 0 140 124" width="140" height="124"></svg>`,
+ (st,rd)=>GW.spadeKnob(st,{value:8.3,onChange:(v,t)=>rd(t)}),
'<b>the pointer rides the knob.</b> A knurled bakelite knob carries a bright spade pointer over a scale engraved into the panel itself. Drag up/down to tune. After a 1920s radio receiver dial.');
card(C,'R15','Multi-band dial',
- `<svg id="mband" class="rsvg" viewBox="0 0 190 110" width="190" height="110"></svg>`,
+ (st,rd)=>GW.multiBandDial(st,{onChange:(v,t)=>rd(t)}),
'<b>four scales, one needle.</b> Nested band arcs on a cream face; the active ring reads bold. Drag the left dial to tune; click the bandspread dial to switch bands. After a shortwave receiver dial pair.');
card(C,'R16','Entry keypad',
- `<svg id="keypad" class="rsvg" viewBox="0 0 150 190" width="150" height="190"></svg>`,
+ (st,rd)=>GW.entryKeypad(st,{onChange:(v,t)=>rd(t)}),
'<b>a composed entry instrument.</b> Worn keycaps feed the amber display; the lamp watches the buffer; the amber keys confirm and clear. Click the keys. After an industrial keypad prop.');
card(C,'R18','Thumb-slide attenuator pair',
- `<svg id="thumbsl" class="rsvg" viewBox="0 0 130 150" width="130" height="150"></svg>`,
+ (st,rd)=>GW.thumbSlide(st,{onChange:(v,t)=>rd(t)}),
'<b>a lit column of numbers, a thumb tab.</b> The scale glows inside the rail; the cream tab rides beside it and the nearest figure lights. Drag either channel. After a compressor blend/mix pair.');
card(C,'R19','Waveform region editor',
- `<svg id="wregion" class="rsvg" viewBox="0 0 190 100" width="190" height="100"></svg>`,
+ (st,rd)=>GW.waveRegion(st,{start:22,end:76,onChange:(v,t)=>rd(t)}),
'<b>pick a region on the wave.</b> The monochrome LCD shows the sample; drag near either flag to move START or END, and the selection draws bright. After a sampler edit screen.');
card(C,'R20','Drum roller selector',
- `<svg id="drumr" class="rsvg" viewBox="0 0 130 140" width="130" height="140"></svg>`,
+ (st,rd)=>GW.drumRoller(st,{onChange:(v,t)=>rd(t)}),
'<b>roll to the number.</b> The numbered drum shows through a tall window and the center value sits on an inverted chip. Drag either drum. After an equalizer tone selector.');
card(C,'R21','LED program row',
- `<svg id="ledrow" class="rsvg" viewBox="0 0 190 58" width="190" height="58"></svg>`,
+ (st,rd)=>GW.ledRow(st,{index:5,onChange:(v,t)=>rd(t)}),
'<b>identical keys, one lit dot.</b> The LED above the button carries the selection, not the key itself — the classic program-select row. Click a key; readout names the program. After a digital reverb’s program bank.');
card(C,'R22','Three-position slide',
- `<svg id="varslide" class="rsvg press" viewBox="0 0 110 64" width="110" height="64"></svg>`,
+ (st,rd)=>GW.pillSlide(st,{index:1,onChange:(v,t)=>rd(t)}),
'<b>a detented pill, three stops.</b> The chrome pill parks at A, AB, or B, and the LED pair below tells the truth — both light on AB. Click a position. After a drum machine variation switch.');
card(C,'R23','Spun-aluminum knob',
- `<svg id="spunk" class="rsvg drag" viewBox="0 0 110 110" width="110" height="110"></svg>`,
+ (st,rd)=>GW.spunKnob(st,{value:62,onChange:(v,t)=>rd(t)}),
'<b>bright metal, machined rings.</b> A spun face with concentric tooling marks in a knurled grip ring, red index over dot ticks. Drag up/down. After a boutique pedal knob, confirmed by an avionics cluster.');
card(C,'R24','Stomp switch + jewel',
- `<svg id="stomp" class="rsvg press" viewBox="0 0 110 110" width="110" height="110"></svg>`,
+ (st,rd)=>GW.stompSwitch(st,{on:false,onChange:(v,t)=>rd(t)}),
'<b>the engage pair.</b> A chrome dome stomp and the faceted jewel that reports it — press to engage, the jewel lights; press again to bypass. Click it. After a tremolo pedal footswitch.');
card(C,'R27','Winged gain selector',
- `<svg id="wingk" class="rsvg drag" viewBox="0 0 110 110" width="110" height="110"></svg>`,
+ (st,rd)=>GW.wingSelector(st,{index:7,onChange:(v,t)=>rd(t)}),
'<b>the red T-bar, stepped.</b> A bar-grip handle over a dot ring, snapping between labeled gain detents. Drag up/down to step it. After a classic mic-preamp gain switch.');
card(C,'R28','Rotary disc switch',
- `<svg id="discsw" class="rsvg press" viewBox="0 0 110 116" width="110" height="116"></svg>`,
+ (st,rd)=>GW.discSwitch(st,{index:1,onChange:(v,t)=>rd(t)}),
'<b>the master power disc.</b> The whole red disc turns, grip bar molded in — a heavy three-position selector for OFF, ON, and COMBINE. Click to step. After a marine battery switch.');
card(C,'R29','Guarded toggle',
- `<svg id="guardsw" class="rsvg press" viewBox="0 0 90 100" width="90" height="100"></svg>`,
+ (st,rd)=>GW.guardedToggle(st,{on:true,onChange:(v,t)=>rd(t)}),
'<b>friction sized to consequence.</b> Guard posts flank the lever and a red collar marks it critical — the avionics way to prevent an accidental throw. Click to throw it deliberately. After a flight-deck switch panel.');
+card(C,'R32','Mechanical timer dial',
+ (st,rd)=>GW.timerDial(st,{minutes:0,onChange:(v,t)=>rd(t)}),
+ '<b>turn to start, push to stop.</b> Drag the knurled dial to wind minutes onto the fixed red index; it winds back down to OFF on its own (demo runs a minute per second). Click the red knob to stop it dead. After a mechanical interval timer.');
+card(C,'R33','Four-way rocker',
+ (st,rd)=>GW.rockerPad(st,{onChange:(v,t)=>rd(t)}),
+ '<b>discrete direction, one press at a time.</b> Click an arrow (the whole quadrant is the target) to step that way; the readout tracks the cursor position it drives. After a remote-control navigation pad.');
+card(C,'R34','Four-way toggle selector',
+ (st,rd)=>GW.fourWayToggle(st,{position:'C',onChange:(v,t)=>rd(t)}),
+ '<b>a lever that lives in one of four states.</b> Click a quadrant and the ball lever throws to A, B, C or D; the matching corner lamp takes the light. The rocker steps — this one selects. After a test-panel quadrant switch.');
+card(C,'R37','Pin routing matrix',
+ (st,rd)=>GW.pinMatrix(st,{onChange:(v,t)=>rd(t)}),
+ '<b>many-to-many routing you can read.</b> Sources are rows, destinations are columns; click an intersection to seat a pin and the route exists. Every simultaneous route stays visible. After the EMS VCS3 pin matrix.');
+card(C,'R38','Dead-man button',
+ (st,rd)=>GW.deadMan(st,{onChange:(v,t)=>rd(t)}),
+ '<b>active only while held.</b> Press and hold — the ring arms, the lamp runs, the dwell clock counts. Let go anywhere and it drops to SAFE. The kit has toggles and momentaries; this is the only control whose state IS your grip.');
+card(C,'R39','Rotary telephone dial',
+ (st,rd)=>GW.telephoneDial(st,{onChange:(v,t)=>rd(t)}),
+ '<b>numeric entry embodied as time.</b> Click a hole: the finger wheel winds to the stop and spins back, and the digit lands in the number. Bigger digits take longer, exactly like the pulses did. After a Western Electric dial.');
+card(C,'R40','Circuit breaker panel',
+ (st,rd)=>GW.breakerPanel(st,{onChange:(v,t)=>rd(t)}),
+ '<b>the first control the system can throw.</b> Click a breaker to close or open it; hit TRIP and a fault pops one out to the tripped mid-position, amber collar showing. A tripped breaker must be pulled to reset before it closes again. After an aircraft breaker row.');
+card(C,'R41','DSKY verb-noun panel',
+ (st,rd)=>GW.dsky(st,{onChange:(v,t)=>rd(t)}),
+ '<b>a command grammar, not a button per command.</b> VERB is the action, NOUN is the operand: press VERB 3 5 ENTR for the lamp test. Wrong grammar lights OPR ERR. Two keys plus digits reach every function — the Apollo answer to a panel with too few buttons.');
+card(C,'R42','Cam-timer program drum',
+ (st,rd)=>GW.camTimer(st,{position:0,onChange:(v,t)=>rd(t)}),
+ '<b>a procedure as a rotating position.</b> The ring is the whole program — fill, wash, rinse, spin — and the pointer is where you are in it. Click to advance (or start from OFF); it then walks itself through the steps. After a washing-machine cam timer.');
+card(C,'R48','Knife switch',
+ (st,rd)=>GW.knifeSwitch(st,{closed:true,onChange:(v,t)=>rd(t)}),
+ '<b>the visible disconnect.</b> Copper blades either sit in their jaws or hang open in the air — you can SEE the circuit is broken, which is the entire safety argument. Click the handle to throw it. After early power-station switchboards.');
+card(C,'R49','Decade switch box',
+ (st,rd)=>GW.decadeBox(st,{digits:[3,5,0,0],onChange:(v,t)=>rd(t)}),
+ '<b>a number composed a digit at a time.</b> Four skirted knobs, one per decade — drag each to its digit and the total just IS their sum. The thumbwheel does one digit; this is how a lab built a precise value. After a General Radio decade resistance box.');
+card(C,'R50','Two-hand safety control',
+ (st,rd)=>GW.twoHandSafety(st,{onChange:(v,t)=>rd(t)}),
+ '<b>both hands, deliberately.</b> Click one palm button to arm the window, the other within half a second to cycle the press. Too slow — TIE-DOWN fault, start over. The hardware keeps hands out of the die; the grammar survives on screen. After OSHA press controls.');
+card(C,'R51','Voice-loop keyset',
+ (st,rd)=>GW.voiceLoop(st,{onChange:(v,t)=>rd(t)}),
+ '<b>attention management, not selection.</b> Each loop is independent: click to monitor (green bar, flickering with activity), again to talk (amber, exclusive — one voice out), again to drop. Flight controllers ran a dozen of these at once. After a mission-control comm keyset.');
/* ============ METERS & GAUGES ============ */
const M=$('meters');
card(M,'10','Needle gauge',
- `<div class="gauge" id="gauge10"><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>`,
+ (st,rd)=>GW.needleGauge(st,{value:50,onChange:(v,t)=>rd(t)}),
'<b>analog dial.</b> Throughput, battery, volume. Drag up/down to sweep the needle; readout tracks. <b>Smooth sweep wants a Cairo/GTK area.</b>');
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>`,
+ (st,rd)=>GW.vuPair(st,{onChange:(v,t)=>rd(t)}),
'<b>live signal level.</b> The sound panel meter row, peak-hold outline. Runs a live signal; readout shows L/R.');
card(M,'12','Mini signal (4-bar)',
- `<span class="sig" id="mini"></span>`,
+ (st,rd)=>GW.miniSig(st,{onChange:(v,t)=>rd(t)}),
'<b>compact activity.</b> Per-row "is this device playing". Live signal; readout shows activity %.');
card(M,'13','Signal ladder',
- `<span class="ladder" id="ladder"><i></i><i></i><i></i><i></i></span>`,
+ (st,rd)=>GW.signalLadder(st,{value:3,onChange:(v,t)=>rd(t)}),
'<b>discrete strength.</b> Wifi bars, bt RSSI — a stepped 0-4. Click to cycle strength; readout shows bars.');
card(M,'14','Linear fuel bar',
- `<div class="bar" id="bar14"><span style="width:72%"></span></div><div class="bar" id="bar14b"><span style="width:40%"></span></div>`,
+ (st,rd)=>{const a=GW.fuelBar(st,{value:72,onChange:(v,t)=>rd(t)});
+ const b=GW.fuelBar(st,{value:40,onChange:(v,t)=>rd(t)});
+ a.set(72);return {a,b};},
'<b>a single 0-100.</b> Battery, disk, download. Drag either bar; warn tint under threshold; readout tracks.');
card(M,'15','Radial ring',
- `<span class="ring" id="ring15" style="--p:68"><b>68</b></span>`,
+ (st,rd)=>GW.radialRing(st,{value:68,onChange:(v,t)=>rd(t)}),
'<b>percentage as a donut.</b> CPU, battery. Drag up/down to set; conic-gradient tracks the readout.');
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>`,
+ (st,rd)=>GW.sparkline(st,{onChange:(v,t)=>rd(t)}),
'<b>recent history.</b> Throughput/CPU over the last minute. Live trace; readout shows the current value.');
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>`,
+ (st,rd)=>GW.waveStrip(st,{onChange:(v,t)=>rd(t)}),
'<b>audio waveform / scope.</b> A richer signal view. Live trace; readout shows amplitude. <b>Needs a drawing surface.</b>');
card(M,'N11','Oscilloscope',
- `<span class="scope"><span class="grat"></span><svg viewBox="0 0 176 78" preserveAspectRatio="none"><polyline id="scopep"/></svg></span>`,
+ (st,rd)=>GW.scope(st,{onChange:(v,t)=>rd(t)}),
'<b>a live phosphor trace.</b> A waveform swept over a graticule. Live; readout shows Vpp. <b>Needs a Cairo/GTK area for a smooth trace.</b>');
card(M,'N12','Spectrum / EQ bars',
- `<span class="eq" id="eq"></span>`,
+ (st,rd)=>GW.eqBars(st,{onChange:(v,t)=>rd(t)}),
'<b>level across bands.</b> A live multi-band bar graph — spectrum, per-core CPU. Live; readout shows the peak band.');
card(M,'N13','Crossed-needle meter',
- `<div class="crossm"><div class="face" id="crossm13"><div class="arc"></div><div class="nA" id="cnA"></div><div class="nB" id="cnB"></div></div>
- <div class="lbl"><span>FWD</span><span>RFL</span></div></div>`,
+ (st,rd)=>GW.crossNeedle(st,{value:55,onChange:(v,t)=>rd(t)}),
'<b>two readings, one face.</b> Forward + reflected needles cross. Drag up/down to drive power; readout shows both. <b>Round build wants a drawing surface.</b>');
card(M,'N14','Thermometer column',
- `<div class="thermo" id="thermo14"><div class="scale"><span>90</span><span>60</span><span>30</span></div>
- <div class="wrapcol"><div class="tube"><div class="fill" id="thermf" style="height:58%"></div></div><div class="bulb"></div></div></div>`,
+ (st,rd)=>GW.thermometer(st,{value:58,onChange:(v,t)=>rd(t)}),
'<b>a rising column.</b> The classic mercury tube — temperature, a climbing load. Drag up/down; readout shows the value.');
card(M,'N15','Bourdon pressure gauge',
- `<div class="bourdon" id="bourdon15">
- <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" id="bourd" style="transform:rotate(30deg)"></div><div class="hub"></div><div class="cap">PSI</div></div>`,
+ (st,rd)=>GW.bourdon(st,{value:45,onChange:(v,t)=>rd(t)}),
'<b>a round dial with a red zone.</b> Needle over a printed arc, redline in terracotta. Drag to set PSI; readout tracks. <b>Round sweep is a drawing-area job.</b>');
card(M,'N16','Strip-chart recorder',
- `<span class="strip"><span class="rule"></span><svg viewBox="0 0 176 62" preserveAspectRatio="none"><polyline id="stripp"/></svg><span class="pen" id="strippen" style="top:31px"></span></span>`,
+ (st,rd)=>GW.stripChart(st,{onChange:(v,t)=>rd(t)}),
'<b>a scrolling pen trace.</b> History scrolls left, the pen writes at the right edge. Live; readout shows the current value. <b>Real trace wants a drawing surface.</b>');
card(M,'N17','Correlation meter (±)',
- `<div class="corr" id="corr17"><div class="face"><div class="arc"></div><div class="zero"></div><div class="ndl" id="corrn" style="transform:rotate(8deg)"></div></div>
- <div class="lbl"><span>−1</span><span>0</span><span>+1</span></div></div>`,
+ (st,rd)=>GW.corrMeter(st,{value:58,onChange:(v,t)=>rd(t)}),
'<b>deviation from center.</b> The needle rests at 0 and swings ±. Drag left/right; readout shows the signed value.');
card(M,'N18','Battery-cell gauge',
- `<div class="batt" id="batt18"><div class="cells" id="battc"></div><span class="nub"></span></div>`,
+ (st,rd)=>GW.battCells(st,{value:62,onChange:(v,t)=>rd(t)}),
'<b>charge as discrete cells.</b> Filled cells count up, the last warn in terracotta. Drag left/right to set; readout shows %.');
card(M,'R01','Moving-coil VU meter',
- `<svg id="mcvu" class="rsvg" viewBox="0 0 150 96" width="150" height="96"></svg>`,
+ (st,rd)=>GW.mcVu(st,{onChange:(v,t)=>rd(t)}),
'<b>the classic cream-faced VU.</b> Printed dB arc, red zone past 0, ballistic needle. Runs the live signal; readout shows VU. After a vintage console meter pair.');
card(M,'R07','Round panel meter',
- `<svg id="rndm" class="rsvg drag" viewBox="0 0 110 104" width="110" height="104"></svg>`,
+ (st,rd)=>GW.roundMeter(st,{value:50,onChange:(v,t)=>rd(t)}),
'<b>the porthole lab meter.</b> Cream dial in a round black bezel, printed dB arc, black needle. Drag up/down to drive it; readout shows dB. After a modulator front meter.');
card(M,'R08','Chrome MIN/MAX indicator',
- `<svg id="chrmm" class="rsvg drag" viewBox="0 0 110 104" width="110" height="104"></svg>`,
+ (st,rd)=>GW.chromeMinMax(st,{value:50,onChange:(v,t)=>rd(t)}),
'<b>the hi-fi trim window.</b> Polished chrome ring, brushed dome, a dark pointer sweeping MIN to MAX over the metal. Drag up/down; readout shows the position. After a hi-fi amplifier level window.');
card(M,'R09','Black-face aviation gauge',
- `<svg id="avgauge" class="rsvg drag" viewBox="0 0 110 104" width="110" height="104"></svg>`,
+ (st,rd)=>GW.aviationGauge(st,{value:52,onChange:(v,t)=>rd(t)}),
'<b>amber on black, zone-marked.</b> The cockpit round: amber ticks and numerals on a black face, painted caution and limit sectors, amber needle. Drag up/down; readout tracks. After a flight-instrument cluster.');
card(M,'R13','Edgewise strip meter',
- `<svg id="edgem" class="rsvg drag" viewBox="0 0 70 130" width="70" height="130"></svg>`,
+ (st,rd)=>GW.edgeMeter(st,{value:.62,onChange:(v,t)=>rd(t)}),
'<b>a needle on a vertical scale.</b> The parchment strip reads attenuation down a compressed log scale; the bar rides its edge. Drag up/down; readout shows dB. After a channel-strip gain-reduction meter.');
+card(M,'R43','Attitude indicator',
+ (st,rd)=>GW.attitudeIndicator(st,{onChange:(v,t)=>rd(t)}),
+ '<b>orientation, not a number.</b> The horizon rolls and shifts behind the fixed amber airplane — two axes in one read. Drag it: left/right banks, up/down pitches. The first two-axis instrument in the kit. After a cockpit artificial horizon.');
+card(M,'R44','Heading bug + servo needle',
+ (st,rd)=>GW.headingBug(st,{value:90,onChange:(v,t)=>rd(t)}),
+ '<b>commanded versus actual on one dial.</b> Drag to park the amber bug where you want to go; the needle chases it with honest servo lag. The gap between them IS the information. After an HSI heading bug and synchro repeaters.');
+card(M,'R54','Vertical tape instrument',
+ (st,rd)=>GW.verticalTape(st,{value:24,onChange:(v,t)=>rd(t)}),
+ '<b>the scale moves, the index does not.</b> A scrolling tape behind a fixed amber index — your value is always at eye level and the neighborhood above and below stays visible. Drag to spin the tape. After the Ki-57 remote tachometer; modern cockpits tape everything.');
+card(M,'R55','Twin-needle gauge',
+ (st,rd)=>GW.twinNeedle(st,{fuel:2.4,oil:3.1,onChange:(v,t)=>rd(t)}),
+ '<b>two values, one housing, mirrored scales.</b> Fuel presses up the left arc, oil up the right, each with its own needle from the shared hub. Drag either half to set that side. Not the crossed-needle (N13) — these are read separately, not at their intersection. After the Ki-57 fuel/oil pressure gauge.');
+card(M,'R56','Comfort-zone crossed needles',
+ (st,rd)=>GW.comfortMeter(st,{temp:72,humidity:45,onChange:(v,t)=>rd(t)}),
+ '<b>two values, one verdict.</b> Temperature and humidity needles cross over a face printed with judgments — the crossing point lands in TOO WARM, TOO DRY, or JUST RIGHT. N13 derives a number from the crossing; this one derives an opinion. Drag each half. After a brass weather-station comfort meter.');
+card(M,'R53','Circular chart recorder',
+ (st,rd)=>GW.chartRecorder(st,{onChange:(v,t)=>rd(t)}),
+ '<b>cyclic time on round paper.</b> The pen sweeps a full day per revolution, so "same time yesterday" is the same angle and daily rhythm draws itself as a flower. Click for fresh paper. The strip chart shows a window; this shows the cycle. After a Partlow round-chart recorder.');
card(M,'R17','Round CRT scope',
- `<svg id="rcrt" class="rsvg" viewBox="0 0 110 104" width="110" height="104"></svg>`,
+ (st,rd)=>GW.roundCrt(st,{onChange:(v,t)=>rd(t)}),
'<b>the round-tube scope.</b> Pale phosphor face in a screwed porthole bezel, dark graticule, a bright live trace. Runs live; readout shows Vpp. After a 1950s bench oscilloscope.');
/* ============ INDICATORS & READOUTS ============ */
const I=$('indicators');
card(I,'18','Status lamp',
- `<span id="lamp18" style="display:inline-flex;gap:9px;align-items:center"><span class="lamp"></span><span class="lamp gold"></span><span class="lamp red"></span><span class="lamp off"></span><span class="lamp busy"></span></span>`,
+ (st,rd)=>GW.statusLamps(st,{onChange:(v,t)=>rd(t)}),
'<b>one-glance health.</b> Green ok · gold engaged · red fail · dim off · pulsing busy. Click any lamp to cycle its state.');
card(I,'19','Badge / tag',
- `<span id="badge19"><span class="badge">TUNNEL</span> <span class="badge red">LOW BATT</span> <span class="badge ghost">2.4G</span></span>`,
+ (st,rd)=>GW.badges(st,{onChange:(v,t)=>rd(t)}),
'<b>a labelled flag.</b> MUTED, AIRPLANE, a band tag. Click a badge to cycle its variant.');
card(I,'20','Tabular readout',
- `<div style="text-align:center"><div class="readout" id="treadout">24:10</div><span class="u">timer</span></div>`,
+ (st,rd)=>GW.tabularReadout(st,{onChange:(v,t)=>rd(t)}),
'<b>a precise number.</b> Clock, countdown, volume %. Runs a live countdown; click to pause/resume.');
card(I,'21','Engraved label',
- `<span class="engrave" id="engrave">outputs<span class="cnt" id="engc">· 3</span></span>`,
+ (st,rd)=>GW.engravedLabel(st,{onChange:(v,t)=>rd(t)}),
'<b>section divider.</b> The hairline-flanked caps label with a count. Click to bump the count.');
card(I,'22','Output well',
- `<div class="owell" id="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>`,
+ (st,rd)=>GW.outputWell(st,{onChange:(v,t)=>rd(t)}),
'<b>streaming step log.</b> Lamp-per-step with evidence. Click to stream the next step.');
card(I,'23','Toast / status line',
- `<span class="toastw" id="toast">joined @Hyatt_WiFi — saved</span>`,
+ (st,rd)=>GW.toast(st,{onChange:(v,t)=>rd(t)}),
'<b>transient confirmation.</b> The one-line result after an action. Click to fire the next toast.');
card(I,'26','Nixie tube',
- `<span class="nixie" id="nixie"><span class="tube off"><b>0</b></span><span class="tube"><b>8</b></span></span>`,
+ (st,rd)=>GW.nixie(st,{onChange:(v,t)=>rd(t)}),
'<b>a single warm-glowing numeral.</b> One lit digit per tube, leading zeros dark. Click to increment the count.');
card(I,'N20','Split-flap display',
- `<span class="flap" id="flap"><span class="flapd"><b>D</b></span><span class="flapd"><b>N</b></span><span class="flapd"><b>S</b></span></span>`,
+ (st,rd)=>GW.splitFlap(st,{animate:!reduced,onChange:(v,t)=>rd(t)}),
'<b>flips to the new value.</b> Each card drops to the next glyph with the mechanical clack. Auto-flips; click to flip now.');
card(I,'N21','Seven-segment display',
- `<span class="seven" id="seven"></span>`,
+ (st,rd)=>GW.sevenSeg(st,{onChange:(v,t)=>rd(t)}),
'<b>a lit-segment number.</b> Green segments, distinct from the nixie glow. Live countdown; click to add a minute.');
card(I,'N22','VFD marquee',
- `<span class="vfdm" id="vfdm"><span class="txt" id="vfdt">ARCHSETUP · NET OK · BT 2 · SND 62%</span><span class="mesh"></span></span>`,
+ (st,rd)=>GW.vfdMarquee(st,{onChange:(v,t)=>rd(t)}),
'<b>scrolling status line.</b> Teal glow, dot-matrix mesh, text tracks across. Scrolls live; click to change the message.');
card(I,'N23','Annunciator panel',
- `<div class="annun" id="annun">
- <span class="acell">SYNC</span><span class="acell warn">LOW BATT</span><span class="acell">LINK</span>
- <span class="acell fault">NO DNS</span><span class="acell">VPN</span><span class="acell">DISK</span>
- </div>`,
- '<b>a grid of named alarms.</b> Cells light amber to warn, red to fault (faults pulse). Click a cell to cycle its state.');
+ (st,rd)=>GW.annunciator(st,{onChange:(v,t)=>rd(t)}),
+ '<b>a grid of named alarms with the alarm lifecycle.</b> Cells light amber to warn, red to fault; any active alarm flashes MSTR CAUTION until ACK steadies it, a new alarm re-flashes it, RESET clears the board, TEST proves the bulbs. Click cells to raise alarms.');
card(I,'N24','Jewel pilot lamps',
- `<span id="jewels" style="display:inline-flex;gap:10px;align-items:center"><span class="jewel" style="--jc:var(--jewel-r)"></span><span class="jewel" style="--jc:var(--jewel-a)"></span>
- <span class="jewel" style="--jc:var(--jewel-g)"></span><span class="jewel dim"></span></span>`,
+ (st,rd)=>GW.jewels(st,{onChange:(v,t)=>rd(t)}),
'<b>a faceted glass indicator.</b> The chunky bezel pilot lamp with a real bloom. Click a jewel to cycle red · amber · green · dark.');
card(I,'N25','Tape counter',
- `<span class="counter" id="counter"></span>`,
+ (st,rd)=>GW.tapeCounter(st,{onChange:(v,t)=>rd(t)}),
'<b>rolling odometer digits.</b> Numbered wheels roll to the next figure. Rolls live; readout shows the total.');
card(I,'N26','Analog clock',
- `<span class="clock" id="clock"><span class="hh" id="clkh"></span><span class="mh" id="clkm"></span><span class="sh" id="clks"></span><span class="pin"></span></span>`,
+ (st,rd)=>GW.analogClock(st,{onChange:(v,t)=>rd(t)}),
'<b>time on a real face.</b> Engraved ticks, three hands, live seconds sweep. Runs live; readout shows the time.');
card(I,'N27','Frequency-dial scale',
- `<span class="freqscale" id="fscale"><span class="band">MHz</span></span>`,
+ (st,rd)=>GW.freqScale(st,{onChange:(v,t)=>rd(t)}),
'<b>a printed log scale.</b> Marks crowd low, spread high — a real logarithmic axis. Drag the pointer; readout shows the frequency.');
card(I,'N28','Patch-bay jack field',
- `<div class="patch" id="patch"></div>`,
+ (st,rd)=>GW.patchBay(st,{onChange:(v,t)=>rd(t)}),
'<b>routing shown as patches.</b> A grid of jacks with cables between connected pairs. Click one jack then another to add/remove a cable.');
card(I,'R10','Data matrix readout',
`<svg id="dmx" class="rsvg press" viewBox="0 0 190 74" width="190" height="74"></svg>`,
@@ -897,515 +1867,54 @@ card(I,'R30','Telegraph indicator',
card(I,'R31','Radar sweep',
`<svg id="radar" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`,
'<b>the rotating scan.</b> A sweep beam circles the bearing ring, contacts bloom as it passes and fade after. Click to mark the current bearing. After a radar PPI scope.');
+card(I,'R35','Day-date disc calendar',
+ `<svg id="daydate" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ '<b>two coaxial discs, one index.</b> The outer ring carries the date, the inner disc the weekday; both read where the yellow hand points. Starts on today; click to roll midnight forward and watch the discs step. After a watch day-date movement.');
+card(I,'R36','LED dot matrix',
+ `<svg id="dotmx" class="rsvg press" viewBox="0 0 108 108" width="108" height="108"></svg>`,
+ '<b>a bitmap, one red dot at a time.</b> 64 discrete LEDs behind a tinted window — segments draw digits, but a matrix draws anything. Click dots to paint the pattern; the readout counts what is lit. After the K4816 pattern generator faceplate.');
+card(I,'R45','Flip-disc tile array',
+ `<svg id="flipdisc" class="rsvg press" viewBox="0 0 120 92" width="120" height="92"></svg>`,
+ '<b>the mechanical pixel.</b> Each disc flips between its yellow face and its black back and then STAYS — no power, no glow, state that persists. Click a disc and watch it flip. The dot matrix glows; this one clacks. After transit flip-dot signs.');
+card(I,'R46','Dekatron counting ring',
+ `<svg id="dekatron" class="rsvg press" viewBox="0 0 120 120" width="120" height="120"></svg>`,
+ '<b>a count as a position, and the counting is visible.</b> Each click is a pulse; the neon glow steps one cathode around the ring and carries on wrap. Nixies show the digit — the Dekatron shows the arithmetic. After glow-transfer counter tubes.');
+card(I,'R47','Landing gear indicator',
+ `<svg id="gearind" class="rsvg press" viewBox="0 0 140 110" width="140" height="110"></svg>`,
+ '<b>three greens or nothing.</b> A spatial lamp panel: nose and mains each get a light, green only when down and locked; the amber transit pulse means neither state. Click the wheel lever to cycle the gear. After a cockpit gear panel.');
+card(I,'R52','Blinkenlights front panel',
+ `<svg id="blinken" class="rsvg press" viewBox="0 0 170 100" width="170" height="100"></svg>`,
+ '<b>the machine thinking, out loud.</b> Address and data lamps ripple with the live computation; the switch register below is yours — flip bits and watch the pattern change. DIP switches hold config; this shows work happening. After a PDP-11/70 front panel.');
/* ============ CONTROL WIRING ============ */
/* 01 toggle */
-(function(){const sw=$('toggle01').querySelector('.switch');
- function upd(){setRd('01',sw.classList.contains('on')?'ON':'OFF');}
- sw.onclick=()=>{sw.classList.toggle('on');upd();};upd();})();
-/* 02 console key */
-$('keys02').querySelectorAll('.key').forEach(k=>k.onclick=()=>{
- $('keys02').querySelectorAll('.key').forEach(x=>x.classList.remove('on','red'));
- k.classList.add(k.textContent==='MUTED'?'red':'on');setRd('02',k.textContent);});
-setRd('02','LIVE');
-/* 03 horizontal fader */
-function setF1(p){const f=$('f1');f.querySelector('.fill').style.width=p+'%';f.querySelector('.cap').style.left=p+'%';setRd('03','level '+Math.round(p));}
-dragX($('f1'),setF1);setF1(68);
-/* 04 vertical faders */
-let vfA=60,vfB=35;
-function vfUpd(){setRd('04','A '+Math.round(vfA)+' · B '+Math.round(vfB));}
-function setVf(el,p){el.querySelector('.fill').style.height=p+'%';el.querySelector('.cap').style.bottom=p+'%';}
-dragY($('v1'),p=>{vfA=p;setVf($('v1'),p);vfUpd();});
-dragY($('v2'),p=>{vfB=p;setVf($('v2'),p);vfUpd();});
-setVf($('v1'),60);setVf($('v2'),35);vfUpd();
-/* 05 knob */
-let knobV=53;
-function setKnob(v){knobV=v;$('kind').style.transform=`rotate(${-150+v/100*300}deg)`;setRd('05','gain '+Math.round(v));}
-dragDelta($('knob'),()=>knobV,setKnob,{min:0,max:100});setKnob(53);
-/* 06 segmented */
-$('seg06').querySelectorAll('button').forEach(b=>b.onclick=()=>{
- $('seg06').querySelectorAll('button').forEach(x=>x.classList.remove('on'));b.classList.add('on');setRd('06',b.textContent);});
-setRd('06','TIMER');
-/* 07 chip */
-$('chip07').onclick=()=>{$('chip07').classList.toggle('on');setRd('07',$('chip07').classList.contains('on')?'ON':'OFF');};
-setRd('07','ON');
-/* 08 arm */
-let armed=false;
-$('arm').onclick=()=>{const a=$('arm');if(!armed){armed=true;a.classList.add('armed');a.textContent='forget? again';setRd('08','ARMED');}
- else{armed=false;a.classList.remove('armed');a.textContent='forget';setRd('08','FIRED · reset');}};
-setRd('08','SAFE');
-/* 09 lamp row */
-const lrStates=[['gold','tap to connect'],['busy','connecting…'],['','connected']];let lrI=0;
-$('lrow9').onclick=()=>{lrI=(lrI+1)%3;const s=lrStates[lrI];$('lrow9').querySelector('.lamp').className='lamp '+s[0];
- $('lrow9').querySelector('.what').textContent=s[1];setRd('09',s[1]);};
-setRd('09','tap to connect');
-/* 24 rotary selector */
-const rotVals=[4,6,8,10,12];let rotIdx=2;const rotAng=[-70,-35,0,35,70];
-$('rotsel24').querySelector('.knob').onclick=()=>{rotIdx=(rotIdx+1)%5;
- $('rotsel24').querySelector('.ind').style.transform=`rotate(${rotAng[rotIdx]}deg)`;
- $('rotsel24').querySelectorAll('.pos').forEach((p,i)=>p.classList.toggle('on',i===rotIdx));setRd('24','size '+rotVals[rotIdx]);};
-setRd('24','size 8');
-/* 25 slide-rule */
-const tunVals=[4,6,8,10,12];let tunIdx=2;const tunX=[12,51,90,129,168];
-function setTun(i){tunIdx=Math.max(0,Math.min(tunVals.length-1,i));$('tundl').style.left=tunX[tunIdx]+'px';
- $('tuner').querySelectorAll('.mk').forEach((m,j)=>m.classList.toggle('on',j===tunIdx));setRd('25','pos '+tunVals[tunIdx]);}
-$('tuner').onclick=(e)=>{const r=$('tuner').getBoundingClientRect(),x=e.clientX-r.left;
- let best=0,bd=1e9;tunX.forEach((tx,j)=>{const d=Math.abs(tx-x);if(d<bd){bd=d;best=j;}});setTun(best);$('tuner').focus();};
-$('tuner').onkeydown=(e)=>{if(e.key==='ArrowLeft'||e.key==='ArrowDown'){e.preventDefault();setTun(tunIdx-1);}
- else if(e.key==='ArrowRight'||e.key==='ArrowUp'){e.preventDefault();setTun(tunIdx+1);}};
-setRd('25','pos 8');
-/* N01 rocker */
-$('rocker').onclick=()=>{$('rocker').classList.toggle('on');setRd('N01',$('rocker').classList.contains('on')?'ON':'OFF');};
-setRd('N01','ON');
-/* N02 transport */
-function transport(b){b.parentElement.querySelectorAll('.tbtn').forEach(x=>x.classList.remove('on'));b.classList.add('on');
- const playing=b.dataset.t==='play';document.querySelectorAll('.reel.spin').forEach(r=>r.style.animationPlayState=playing&&!reduced?'running':'paused');
- setRd('N02',{rew:'REW',play:'PLAY',stop:'STOP',rec:'REC'}[b.dataset.t]);}
-setRd('N02','PLAY');
-/* N03 radio preset */
-function preset(b){b.parentElement.querySelectorAll('.preset').forEach(x=>x.classList.remove('on'));b.classList.add('on');setRd('N03',b.textContent);}
-setRd('N03','WIFI');
-/* N04 concentric dual knob */
-let dkoV=50,dkiV=50;
-function setDual(){$('dko').style.transform=`rotate(${-150+dkoV/100*300}deg)`;$('dki').style.transform=`rotate(${-150+dkiV/100*300}deg)`;
- setRd('N04','bass '+Math.round(dkoV)+' · treble '+Math.round(dkiV));}
-dragDelta($('dko'),()=>dkoV,v=>{dkoV=v;setDual();},{min:0,max:100});
-dragDelta($('dki'),()=>dkiV,v=>{dkiV=v;setDual();},{min:0,max:100,stop:true});
-setDual();
-/* N05 encoder */
-let encV=7;
-function paintEnc(){const leds=$('enc').querySelectorAll('.led');const lit=((Math.floor(encV)%12)+12)%12;
- leds.forEach((l,i)=>l.classList.toggle('on',i<=lit));}
-function setEnc(v){encV=v;paintEnc();const ind=$('enc').querySelector('.knob .ind');if(ind)ind.parentElement.style.transform=`rotate(${encV*30}deg)`;
- setRd('N05','pos '+Math.round(encV));}
-(function(){const enc=$('enc');const R=27,cx=33,cy=33;
- for(let i=0;i<12;i++){const a=(i*30-90)*Math.PI/180;const d=document.createElement('span');d.className='led';
- d.style.left=(cx+R*Math.cos(a))+'px';d.style.top=(cy+R*Math.sin(a))+'px';enc.appendChild(d);}
- const k=document.createElement('span');k.className='knob';k.innerHTML='<span class="ind"></span>';enc.appendChild(k);})();
-dragDelta($('enc'),()=>encV,setEnc,{min:0,max:100000,sens:0.25});setEnc(7);
-/* N06 keylock */
-let keyIdx=1;const keyAng=[-70,0,70];const keyNm=['OFF','ON','RUN'];
-function keyTurn(){keyIdx=(keyIdx+1)%3;$('kbit').style.transform=`rotate(${keyAng[keyIdx]}deg)`;
- $('keylock6').querySelectorAll('.kpos').forEach((p,i)=>p.classList.toggle('on',i===keyIdx));setRd('N06',keyNm[keyIdx]);}
-setRd('N06','ON');
-/* N07 crossfader */
-function setXf(p){$('xfcap').style.left=p+'%';const v=Math.round(p-50);setRd('N07',(v>0?'+':'')+v);}
-dragX($('xf'),setXf);setXf(50);
-/* N08 thumbwheel */
-let thumbV=42;
-function setThumb(v){thumbV=Math.round(v);const s=(((thumbV%100)+100)%100);$('thval').textContent=String(s).padStart(2,'0');setRd('N08','value '+s);}
-dragDelta($('thumbwheel'),()=>thumbV,setThumb,{min:0,max:99,sens:0.2});setThumb(42);
-/* N09 DIP */
-function dipUpd(){const bits=[...$('dips').querySelectorAll('.dipsw')].map(x=>x.classList.contains('on')?'1':'0').join('');setRd('N09',bits);}
-function dip(s){s.classList.toggle('on');dipUpd();}
-dipUpd();
-/* N10 jog */
-let jogV=0;
-function setJog(v){jogV=v;$('jogi').style.transform=`rotate(${jogV*4}deg)`;setRd('N10','pos '+Math.round(jogV));}
-dragDelta($('jog10'),()=>jogV,setJog,{min:-100000,max:100000,sens:0.5});setJog(0);
+/* 01-09, 24, 25, N01-N10 extracted to widgets.js (GW.*) — wired at their card records */
/* ============ METER WIRING (drag-driven) ============ */
-/* 10 needle gauge */
-let gV=50;
-function setGauge(v){gV=v;$('g1').style.transform=`rotate(${-60+v/100*120}deg)`;$('g1v').textContent=Math.round(v);setRd('10','value '+Math.round(v)+'%');}
-dragDelta($('gauge10'),()=>gV,setGauge,{min:0,max:100});setGauge(50);
-/* 13 signal ladder */
-let ladV=3;
-function setLadder(v){ladV=v;const bars=$('ladder').children;
- for(let i=0;i<bars.length;i++)bars[i].style.background=i<v?'var(--gold)':'var(--wash)';setRd('13',v+'/4');}
-$('ladder').onclick=()=>setLadder((ladV+1)%5);setLadder(3);
-/* 14 fuel bars */
-function setBar(id,no,p){const b=$(id);b.querySelector('span').style.width=p+'%';b.classList.toggle('warn',p<20);setRd(no,Math.round(p)+'%');}
-dragX($('bar14'),p=>setBar('bar14','14',p));dragX($('bar14b'),p=>setBar('bar14b','14',p));
-setBar('bar14','14',72);
-/* 15 radial ring */
-let ringV=68;
-function setRing(v){ringV=v;const r=$('ring15');r.style.setProperty('--p',v);r.querySelector('b').textContent=Math.round(v);setRd('15',Math.round(v)+'%');}
-dragDelta($('ring15'),()=>ringV,setRing,{min:0,max:100});setRing(68);
-/* N13 crossed-needle */
-let cnPow=55;
-function setCross(v){cnPow=v;const fwd=v,rfl=v*0.68;$('cnA').style.transform=`rotate(${-42+fwd/100*84}deg)`;
- $('cnB').style.transform=`rotate(${42-rfl/100*84}deg)`;setRd('N13','FWD '+Math.round(fwd)+' · RFL '+Math.round(rfl));}
-dragDelta($('crossm13'),()=>cnPow,setCross,{min:0,max:100});setCross(55);
-/* N14 thermometer */
-let thV=58;
-function setThermo(v){thV=v;$('thermf').style.height=v+'%';setRd('N14',Math.round(30+v/100*60)+'°');}
-dragDelta($('thermo14'),()=>thV,setThermo,{min:0,max:100});setThermo(58);
-/* N15 bourdon */
-let bV=45;
-function setBour(v){bV=v;$('bourd').style.transform=`rotate(${-60+v/100*120}deg)`;setRd('N15',Math.round(v/100*160)+' PSI');}
-dragDelta($('bourdon15'),()=>bV,setBour,{min:0,max:100});setBour(45);
-/* N17 correlation */
-function setCorr(p){const v=(p-50)/50;$('corrn').style.transform=`rotate(${v*38}deg)`;setRd('N17',(v>=0?'+':'')+v.toFixed(2));}
-dragX($('corr17'),setCorr);setCorr(58);
-/* N18 battery */
-(function(){const bc=$('battc');for(let i=0;i<8;i++){const c=document.createElement('span');c.className='cell';bc.appendChild(c);}})();
-function setBatt(p){const cells=$('battc').children,lit=Math.round(p/100*cells.length);
- for(let i=0;i<cells.length;i++)cells[i].className='cell'+(i<lit?' on':'')+((p<=25&&i<lit)?' warn':'');setRd('N18',Math.round(p)+'%');}
-dragX($('batt18'),setBatt);setBatt(62);
+/* 10-17, N11-N18 extracted to widgets.js (GW.*) — wired at their card records */
/* ============ INDICATOR WIRING ============ */
-/* 18 status lamp */
-(function(){const lst=['lamp','lamp gold','lamp red','lamp off','lamp busy'];const nm=['ok','engaged','fault','off','busy'];
- $('lamp18').querySelectorAll('.lamp').forEach(l=>{l.style.cursor='pointer';l.onclick=()=>{
- let i=lst.indexOf(l.className);if(i<0)i=0;i=(i+1)%lst.length;l.className=lst[i];setRd('18',nm[i]);};});})();
-setRd('18','five states');
-/* 19 badge */
-(function(){const bv=['badge','badge red','badge ghost'];
- $('badge19').querySelectorAll('.badge').forEach(b=>{b.style.cursor='pointer';b.onclick=()=>{
- let i=bv.indexOf(b.className);if(i<0)i=0;i=(i+1)%bv.length;b.className=bv[i];setRd('19',b.textContent);};});})();
-setRd('19','three variants');
-/* 21 engraved */
-let engN=3;$('engrave').onclick=()=>{engN=engN%9+1;$('engc').textContent='· '+engN;setRd('21','count '+engN);};
-setRd('21','count 3');
-/* 22 output well */
-const owSteps=[['gold','Probe','8.8.8.8 …'],['','Gateway','10.0.0.1 ok'],['','DNS','1.1.1.1 ok'],['red','Retry','timeout']];let owI=0;
-$('owell').onclick=()=>{owI=(owI+1)%owSteps.length;const s=owSteps[owI];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>`;
- $('owell').appendChild(d);while($('owell').children.length>5)$('owell').removeChild($('owell').firstChild);setRd('22','+ '+s[1]);};
-setRd('22','click to stream');
-/* 23 toast */
-const toastMsgs=['link up · 300 Mbps','bt paired — WH-1000XM4','profile switched','joined @Hyatt_WiFi — saved'];let toastN=0;
-$('toast').onclick=()=>{toastN++;$('toast').textContent=toastMsgs[toastN%toastMsgs.length];
- $('toast').style.opacity='0';requestAnimationFrame(()=>{$('toast').style.transition='opacity .3s';$('toast').style.opacity='1';});setRd('23','fired '+toastN);};
-setRd('23','click to fire');
-/* 26 nixie */
-let nixV=8;
-function setNix(v){nixV=v;const t=$('nixie').children,s=String(v).padStart(2,'0');
- t[0].classList.toggle('off',v<10);t[0].querySelector('b').textContent=s[0];t[1].querySelector('b').textContent=s[1];setRd('26',s);}
-$('nixie').onclick=()=>setNix((nixV+1)%100);setNix(8);
-/* N23 annunciator */
-(function(){const st=['acell','acell warn','acell fault'];const nm=['ok','warn','fault'];
- $('annun').querySelectorAll('.acell').forEach(cell=>{const label=cell.textContent;cell.onclick=()=>{
- let cur=cell.className.trim();let i=st.indexOf(cur);if(i<0)i=0;i=(i+1)%st.length;cell.className=st[i];setRd('N23',label+' → '+nm[i]);};});})();
-setRd('N23','click a cell');
-/* N24 jewel */
-(function(){const cols=['var(--jewel-r)','var(--jewel-a)','var(--jewel-g)'];const nm=['red','amber','green'];
- $('jewels').querySelectorAll('.jewel').forEach(j=>{j.onclick=()=>{
- if(j.classList.contains('dim')){j.classList.remove('dim');j.style.setProperty('--jc',cols[0]);setRd('N24','red');return;}
- let cur=j.style.getPropertyValue('--jc').trim();let i=cols.indexOf(cur);
- if(i>=cols.length-1||i<0){j.classList.add('dim');setRd('N24','dark');}else{j.style.setProperty('--jc',cols[i+1]);setRd('N24',nm[i+1]);}};});})();
-setRd('N24','click to cycle');
-/* N27 frequency-dial + pointer */
-(function(){const fs=$('fscale');const W=184;const marks=[0.5,1,2,3,5,7,10,14,20];const lo=Math.log10(0.5),hi=Math.log10(20);
- marks.forEach(m=>{const x=6+((Math.log10(m)-lo)/(hi-lo))*(W-12);
- const tk=document.createElement('span');tk.className='tick';tk.style.left=x+'px';tk.style.height='11px';fs.appendChild(tk);
- const mk=document.createElement('span');mk.className='mk';mk.style.left=x+'px';mk.textContent=m;fs.appendChild(mk);});
- for(let m=0.5;m<=20;m+=(m<2?0.25:m<10?1:2)){const x=6+((Math.log10(m)-lo)/(hi-lo))*(W-12);
- const tk=document.createElement('span');tk.className='tick';tk.style.left=x+'px';tk.style.height='6px';tk.style.opacity='.5';fs.appendChild(tk);}
- const ptr=document.createElement('span');ptr.className='fptr';fs.appendChild(ptr);
- function freqAt(p){return Math.pow(10,lo+(p/100)*(hi-lo));}
- window.setFreq=function(p){ptr.style.left=(6+p/100*(W-12))+'px';const f=freqAt(p);setRd('N27',(f<10?f.toFixed(1):Math.round(f))+' MHz');};
- dragX(fs,window.setFreq);window.setFreq(45);})();
-/* N28 patch-bay */
-(function(){const patch=$('patch');const rows=2,cols=4;const jacks=[];
- for(let r=0;r<rows;r++){const rowEl=document.createElement('div');rowEl.className='row';
- for(let c=0;c<cols;c++){const j=document.createElement('span');j.className='jack';j.dataset.id=(r*cols+c);rowEl.appendChild(j);jacks.push(j);}
- patch.appendChild(rowEl);}
- const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');patch.appendChild(svg);
- let conns=[];let pending=null;
- function key(a,b){return Math.min(a,b)+'-'+Math.max(a,b);}
- function draw(){while(svg.firstChild)svg.removeChild(svg.firstChild);
- const pr=patch.getBoundingClientRect();
- jacks.forEach(j=>j.classList.remove('hot'));
- conns.forEach(pair=>{const[a,b]=pair.split('-').map(Number);
- const ra=jacks[a].getBoundingClientRect(),rb=jacks[b].getBoundingClientRect();
- const x1=ra.left-pr.left+ra.width/2,y1=ra.top-pr.top+ra.height/2;
- const x2=rb.left-pr.left+rb.width/2,y2=rb.top-pr.top+rb.height/2;
- const p=document.createElementNS('http://www.w3.org/2000/svg','path');
- const my=Math.max(y1,y2)+16;p.setAttribute('d',`M${x1} ${y1} C${x1} ${my} ${x2} ${my} ${x2} ${y2}`);svg.appendChild(p);
- jacks[a].classList.add('hot');jacks[b].classList.add('hot');});
- setRd('N28',conns.length?conns.map(c=>{const[a,b]=c.split('-');return a+'↔'+b;}).join(' '):'no cables');}
- jacks.forEach(j=>{j.onclick=()=>{const id=+j.dataset.id;
- if(pending==null){pending=id;j.classList.add('sel');return;}
- if(pending===id){pending=null;j.classList.remove('sel');return;}
- const k=key(pending,id);const i=conns.indexOf(k);if(i>=0)conns.splice(i,1);else conns.push(k);
- jacks[pending].classList.remove('sel');pending=null;draw();};});
- conns=['0-5','2-7'];draw();window.addEventListener('resize',draw);})();
+/* 18-23, 26 extracted to widgets.js (GW.*) — wired at their card records */
+/* N20-N28 extracted to widgets.js (GW.*) — wired at their card records */
/* ============ REFERENCE-BATCH (R) WIDGETS — SVG builders ============ */
-const SVGNS='http://www.w3.org/2000/svg';
-function svgEl(parent,tag,attrs){const e=document.createElementNS(SVGNS,tag);
- for(const k in attrs)e.setAttribute(k,attrs[k]);parent.appendChild(e);return e;}
-function polar(cx,cy,r,deg){const a=deg*Math.PI/180;return [cx+r*Math.sin(a),cy-r*Math.cos(a)];}
-
-/* R01 moving-coil VU: pivot below the face, authentic nonlinear dB scale */
-const VUDB=[[-20,0],[-10,.20],[-7,.31],[-5,.40],[-3,.50],[-2,.58],[-1,.66],[0,.75],[1,.84],[2,.91],[3,1]];
-const mcvu={t:.35,needle:null};
-(function(){const s=$('mcvu'),cx=75,cy=112,sweep=t=>-43+t*86;
- const defs=svgEl(s,'defs',{});
- const g1=svgEl(defs,'linearGradient',{id:'vuGlass',x1:0,y1:0,x2:0,y2:1});
- svgEl(g1,'stop',{offset:'0','stop-color':'rgba(255,255,255,.13)'});
- svgEl(g1,'stop',{offset:'.35','stop-color':'rgba(255,255,255,0)'});
- const clip=svgEl(defs,'clipPath',{id:'vuFace'});
- svgEl(clip,'rect',{x:8,y:8,width:134,height:74,rx:3});
- svgEl(s,'rect',{x:.5,y:.5,width:149,height:95,rx:7,fill:'#16130f',stroke:'#060505'});
- svgEl(s,'rect',{x:8,y:8,width:134,height:74,rx:3,fill:'var(--cream)'});
- const face=svgEl(s,'g',{'clip-path':'url(#vuFace)'});
- const arcPt=t=>polar(cx,cy,58,sweep(t));
- const [ax1,ay1]=arcPt(0),[ax2,ay2]=arcPt(1);
- svgEl(face,'path',{d:`M ${ax1} ${ay1} A 58 58 0 0 1 ${ax2} ${ay2}`,fill:'none',stroke:'#3a3128','stroke-width':1.2});
- const [rx1,ry1]=arcPt(.75);
- svgEl(face,'path',{d:`M ${rx1} ${ry1} A 58 58 0 0 1 ${ax2} ${ay2}`,fill:'none',stroke:'var(--fail)','stroke-width':3.5});
- for(const [db,t] of VUDB){const a=sweep(t),[x1,y1]=polar(cx,cy,55,a),[x2,y2]=polar(cx,cy,63,a);
- svgEl(face,'line',{x1,y1,x2,y2,stroke:db>0?'var(--fail)':'#3a3128','stroke-width':1.1});}
- for(const [db,t] of VUDB){if(![-20,-10,-5,-3,0,1,2,3].includes(db))continue;
- const [x,y]=polar(cx,cy,69,sweep(t));
- svgEl(face,'text',{x,y:y+2,'text-anchor':'middle','font-size':5.8,'font-family':'var(--mono)',
- fill:db>0?'var(--fail)':'#3a3128'}).textContent=Math.abs(db);}
- svgEl(face,'text',{x:75,y:78,'text-anchor':'middle','font-size':11,'font-weight':700,
- 'font-family':'var(--mono)',fill:'#3a3128'}).textContent='VU';
- mcvu.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})`});
- 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(#vuGlass)'});
- mcvu.sweep=sweep;})();
-function setMcVu(t){t=Math.max(0,Math.min(1.02,t));mcvu.t=t;
- mcvu.needle.setAttribute('transform',`rotate(${mcvu.sweep(Math.min(1,t))},75,112)`);
- const db=vuDb(Math.min(1,t));
- setRd('R01',(db>0?'+':'')+db.toFixed(1)+' VU');}
+/* svgEl, polar, VUDB live in widgets.js (GW) */
+
+/* R01 ballistics: the page owns the signal chase; the meter just takes set(t) */
function tickMcVu(){const a=lvl();const target=Math.min(1.02,a*1.12);
- setMcVu(mcvu.t+(target-mcvu.t)*(target>mcvu.t?0.32:0.10));}
+ const t=MH.mcvu.get();MH.mcvu.set(t+(target-t)*(target>t?0.32:0.10));}
+
+/* R02-R06, R12, R14, R15 extracted to widgets.js (GW.*) */
+
+
+
+
+
-/* R02 calibrated vernier dial: the disc turns under a fixed hairline */
-let vernV=42.0,vernDisc=null;
-(function(){const s=$('vern'),cx=75,cy=75;
- const defs=svgEl(s,'defs',{});
- const fg=svgEl(defs,'radialGradient',{id:'vernFace',cx:'50%',cy:'45%',r:'60%'});
- svgEl(fg,'stop',{offset:'0','stop-color':'#f7f2da'});
- svgEl(fg,'stop',{offset:'1','stop-color':'#e4dfc2'});
- const bk=svgEl(defs,'radialGradient',{id:'bakeKnob',cx:'42%',cy:'36%',r:'75%'});
- svgEl(bk,'stop',{offset:'0','stop-color':'#37332c'});
- svgEl(bk,'stop',{offset:'1','stop-color':'#060505'});
- svgEl(s,'circle',{cx,cy,r:72,fill:'#0c0b0a',stroke:'#262320'});
- vernDisc=svgEl(s,'g',{});
- svgEl(vernDisc,'circle',{cx,cy,r:66,fill:'url(#vernFace)',stroke:'#b7b29a','stroke-width':1});
- for(let u=0;u<=100;u++){const a=u*3,major=u%10===0,half=u%5===0;
- const len=major?10:half?7:4.5,[x1,y1]=polar(cx,cy,63,a),[x2,y2]=polar(cx,cy,63-len,a);
- svgEl(vernDisc,'line',{x1,y1,x2,y2,stroke:'#3a3128','stroke-width':major?1.3:half?1:.7});
- if(major){const g=svgEl(vernDisc,'g',{transform:`rotate(${a},${cx},${cy})`});
- svgEl(g,'text',{x:cx,y:cy-44,'text-anchor':'middle','font-size':7.5,'font-family':'var(--mono)',
- fill:'#3a3128'}).textContent=u;}}
- for(let k=0;k<12;k++){const [x,y]=polar(cx,cy,24,k*30);
- svgEl(s,'circle',{cx:x,cy:y,r:4.8,fill:'url(#bakeKnob)'});}
- svgEl(s,'circle',{cx,cy,r:24,fill:'url(#bakeKnob)',stroke:'#000','stroke-width':.6});
- svgEl(s,'circle',{cx,cy,r:16,fill:'url(#bakeKnob)'});
- svgEl(s,'ellipse',{cx:69,cy:66,rx:7,ry:4,fill:'rgba(255,255,255,.10)',transform:'rotate(-32,69,66)'});
- svgEl(s,'line',{x1:cx,y1:6,x2:cx,y2:22,stroke:'var(--fail)','stroke-width':2.2,'stroke-linecap':'round'});})();
-function setVern(v){vernV=v;vernDisc.setAttribute('transform',`rotate(${-v*3},75,75)`);setRd('R02',v.toFixed(1));}
-dragDelta($('vern'),()=>vernV,setVern,{min:0,max:100,sens:0.15});setVern(42.0);
-
-/* R03 bat-handle toggle */
-let batOn=true,batLever=null,batLblOn=null,batLblOff=null;
-(function(){const s=$('batsw'),cx=35,cy=44;
- const defs=svgEl(s,'defs',{});
- const ng=svgEl(defs,'linearGradient',{id:'nutG',x1:0,y1:0,x2:0,y2:1});
- svgEl(ng,'stop',{offset:'0','stop-color':'#8a8578'});
- svgEl(ng,'stop',{offset:'1','stop-color':'#4e4a42'});
- const cg=svgEl(defs,'linearGradient',{id:'chromeG',x1:0,y1:0,x2:1,y2:0});
- svgEl(cg,'stop',{offset:'0','stop-color':'#7e7a70'});
- svgEl(cg,'stop',{offset:'.45','stop-color':'#e8e5db'});
- svgEl(cg,'stop',{offset:'1','stop-color':'#8a867c'});
- const tg=svgEl(defs,'radialGradient',{id:'tipG',cx:'40%',cy:'32%',r:'75%'});
- svgEl(tg,'stop',{offset:'0','stop-color':'#f2efe8'});
- svgEl(tg,'stop',{offset:'1','stop-color':'#7e7a70'});
- batLblOn=svgEl(s,'text',{x:cx,y:9,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em',
- 'font-family':'var(--mono)',fill:'var(--gold-hi)'});batLblOn.textContent='ON';
- batLblOff=svgEl(s,'text',{x:cx,y:87,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em',
- 'font-family':'var(--mono)',fill:'var(--dim)'});batLblOff.textContent='OFF';
- const pts=[];for(let k=0;k<6;k++){const a=(k*60+30)*Math.PI/180;pts.push((cx+17*Math.cos(a))+','+(cy+17*Math.sin(a)));}
- svgEl(s,'polygon',{points:pts.join(' '),fill:'url(#nutG)',stroke:'#2b2822'});
- svgEl(s,'circle',{cx,cy,r:9,fill:'#14110e',stroke:'#000'});
- batLever=svgEl(s,'g',{});
- svgEl(batLever,'path',{d:`M ${cx-3.2} ${cy} L ${cx-1.8} 21 L ${cx+1.8} 21 L ${cx+3.2} ${cy} Z`,fill:'url(#chromeG)',stroke:'#4e4a42','stroke-width':.5});
- svgEl(batLever,'ellipse',{cx,cy:17,rx:6.5,ry:8,fill:'url(#tipG)',stroke:'#5e5a52','stroke-width':.6});
- batLever.style.transformOrigin=`${cx}px ${cy}px`;batLever.style.transition='transform .12s';
- $('batsw').onclick=()=>setBat(!batOn);})();
-function setBat(on){batOn=on;batLever.style.transform=on?'rotate(0deg)':'rotate(180deg)';
- batLblOn.setAttribute('fill',on?'var(--gold-hi)':'var(--dim)');
- batLblOff.setAttribute('fill',on?'var(--dim)':'var(--gold-hi)');setRd('R03',on?'ON':'OFF');}
-setBat(true);
-
-/* R04 bakelite fluted knob over a printed 0-10 scale */
-let bakeV=63,bakeIdx=null;
-(function(){const s=$('bakek'),cx=55,cy=54;
- const defs=svgEl(s,'defs',{});
- const sk=svgEl(defs,'radialGradient',{id:'bakeSk',cx:'42%',cy:'36%',r:'80%'});
- svgEl(sk,'stop',{offset:'0','stop-color':'#312d27'});
- svgEl(sk,'stop',{offset:'1','stop-color':'#050404'});
- const dm=svgEl(defs,'radialGradient',{id:'bakeDome',cx:'40%',cy:'32%',r:'80%'});
- svgEl(dm,'stop',{offset:'0','stop-color':'#3a362f'});
- svgEl(dm,'stop',{offset:'1','stop-color':'#0a0908'});
- for(let i=0;i<=10;i++){const a=-135+i*27,major=i%5===0;
- const [x1,y1]=polar(cx,cy,38,a),[x2,y2]=polar(cx,cy,major?45:43.5,a);
- svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--steel)','stroke-width':major?1.5:1});
- if(major){const [x,y]=polar(cx,cy,50.5,a);
- svgEl(s,'text',{x,y:y+2.6,'text-anchor':'middle','font-size':8,'font-family':'var(--mono)',
- fill:'var(--steel)'}).textContent=i;}}
- for(let k=0;k<18;k++){const [x,y]=polar(cx,cy,31,k*20);
- svgEl(s,'circle',{cx:x,cy:y,r:3.4,fill:'url(#bakeSk)'});}
- svgEl(s,'circle',{cx,cy,r:31,fill:'url(#bakeSk)',stroke:'#000','stroke-width':.6});
- bakeIdx=svgEl(s,'line',{x1:cx,y1:cy-30,x2:cx,y2:cy-21,stroke:'var(--gold-hi)','stroke-width':2.4,
- 'stroke-linecap':'round'});
- svgEl(s,'circle',{cx,cy,r:19,fill:'url(#bakeDome)'});
- svgEl(s,'ellipse',{cx:49,cy:46,rx:6,ry:3.5,fill:'rgba(255,255,255,.14)',transform:'rotate(-28,49,46)'});})();
-function setBake(v){bakeV=v;bakeIdx.setAttribute('transform',`rotate(${v*2.7-135},55,54)`);
- setRd('R04',(v/10).toFixed(1)+' / 10');}
-dragDelta($('bakek'),()=>bakeV,setBake,{min:0,max:100,sens:0.25});setBake(63);
-
-/* shared: interpolate a dB reading from a 0..1 sweep position (VU law) */
-function vuDb(t){let i=0;while(i<VUDB.length-2&&VUDB[i+1][1]<t)i++;
- const [d1,t1]=VUDB[i],[d2,t2]=VUDB[i+1];
- return d1+(d2-d1)*Math.max(0,Math.min(1,(t-t1)/(t2-t1)));}
-
-/* R05 filter slider bank: one fader per band, arrow caps on black tracks */
-const FB_FREQ=[78,136,235,406,701,1210,2090,3620];
-const fbank={vals:[18,30,42,25,35,55,20,48],caps:[]};
-(function(){const s=$('fbank'),y0=14,y1=84,x0=30,dx=19.5;
- const yOf=v=>y0+v/60*(y1-y0);
- for(const [lbl,y] of [['0',y0],['20',y0+(y1-y0)/3],['40',y0+2*(y1-y0)/3],['60',y1]]){
- svgEl(s,'text',{x:14,y:y+2,'text-anchor':'end','font-size':6,'font-family':'var(--mono)',
- fill:'var(--steel)'}).textContent=lbl;}
- svgEl(s,'text',{x:14,y:94,'text-anchor':'end','font-size':5.5,'font-family':'var(--mono)',
- fill:'var(--dim)'}).textContent='dB';
- FB_FREQ.forEach((f,i)=>{const x=x0+i*dx;
- svgEl(s,'text',{x,y:8,'text-anchor':'middle','font-size':5.2,'font-family':'var(--mono)',
- fill:'var(--steel)'}).textContent=f<1000?f:(f/1000)+'k';
- svgEl(s,'line',{x1:x,y1:y0,x2:x,y2:y1,stroke:'#0a0908','stroke-width':3.4,'stroke-linecap':'round'});
- svgEl(s,'line',{x1:x-.7,y1:y0,x2:x-.7,y2:y1,stroke:'rgba(255,255,255,.05)','stroke-width':.7});
- const cap=svgEl(s,'g',{});
- svgEl(cap,'polygon',{points:`${x-8.5},${-4.5} ${x-2.5},0 ${x-8.5},${4.5}`,fill:'var(--vfd)',stroke:'#123028','stroke-width':.6});
- svgEl(cap,'polygon',{points:`${x+8.5},${-4.5} ${x+2.5},0 ${x+8.5},${4.5}`,fill:'var(--vfd)',stroke:'#123028','stroke-width':.6});
- svgEl(cap,'circle',{cx:x,cy:0,r:1.5,fill:'#123028'});
- fbank.caps.push(cap);
- const hit=svgEl(s,'rect',{x:x-9.5,y:y0-6,width:19,height:y1-y0+12,fill:'transparent'});
- hit.style.cursor='ns-resize';
- dragY(hit,pct=>setFbank(i,(100-pct)/100*60));});
- })();
-function setFbank(i,db){db=Math.max(0,Math.min(60,db));fbank.vals[i]=db;
- fbank.caps[i].setAttribute('transform',`translate(0,${14+db/60*70})`);
- setRd('R05',`${FB_FREQ[i]<1000?FB_FREQ[i]:(FB_FREQ[i]/1000)+'k'} Hz · −${Math.round(db)} dB`);}
-fbank.vals.forEach((v,i)=>setFbank(i,v));setRd('R05','drag a band');
-
-/* R06 chicken-head selector: the tapered lever aims at the position */
-const CHICK_POS=[['OFF',-60],['LO',-20],['MID',20],['HI',60]];
-let chickI=2,chickLever=null,chickLbls=[];
-(function(){const s=$('chick'),cx=55,cy=58;
- const defs=svgEl(s,'defs',{});
- const bg=svgEl(defs,'radialGradient',{id:'chickG',cx:'42%',cy:'34%',r:'80%'});
- svgEl(bg,'stop',{offset:'0','stop-color':'#35312b'});
- svgEl(bg,'stop',{offset:'1','stop-color':'#070606'});
- CHICK_POS.forEach(([lbl,a],i)=>{const [tx,ty]=polar(cx,cy,42,a);
- const t=svgEl(s,'text',{x:tx,y:ty+2.5,'text-anchor':'middle','font-size':6.5,'letter-spacing':'.06em',
- 'font-family':'var(--mono)',fill:'var(--dim)'});t.textContent=lbl;chickLbls.push(t);
- const [mx1,my1]=polar(cx,cy,33,a),[mx2,my2]=polar(cx,cy,29,a);
- svgEl(s,'line',{x1:mx1,y1:my1,x2:mx2,y2:my2,stroke:'var(--steel)','stroke-width':1.2});});
- svgEl(s,'circle',{cx,cy,r:12,fill:'#0c0b0a'});
- chickLever=svgEl(s,'g',{});
- svgEl(chickLever,'path',{d:`M ${cx} ${cy-29} L ${cx+8.5} ${cy-4} Q ${cx+9} ${cy+8} ${cx+5} ${cy+11} A 7 7 0 0 1 ${cx-5} ${cy+11} Q ${cx-9} ${cy+8} ${cx-8.5} ${cy-4} Z`,
- fill:'url(#chickG)',stroke:'#000','stroke-width':.7});
- svgEl(chickLever,'line',{x1:cx,y1:cy-26,x2:cx,y2:cy+6,stroke:'#4a463e','stroke-width':1.4,'stroke-linecap':'round'});
- chickLever.style.transformOrigin=`${cx}px ${cy}px`;chickLever.style.transition='transform .12s';
- $('chick').onclick=()=>setChick((chickI+1)%CHICK_POS.length);})();
-function setChick(i){chickI=i;chickLever.style.transform=`rotate(${CHICK_POS[i][1]}deg)`;
- chickLbls.forEach((t,k)=>t.setAttribute('fill',k===i?'var(--gold-hi)':'var(--dim)'));
- setRd('R06',CHICK_POS[i][0]);}
-setChick(2);
-
-/* R07 round panel meter: porthole bezel, same VU law as R01 */
-let rndT=.5,rndNeedle=null;
-(function(){const s=$('rndm'),cx=55,fy=54,py=74,sweep=t=>-40+t*80;
- const defs=svgEl(s,'defs',{});
- const clip=svgEl(defs,'clipPath',{id:'rndFace'});
- svgEl(clip,'circle',{cx,cy:fy,r:40});
- svgEl(s,'circle',{cx,cy:fy,r:48,fill:'#141110',stroke:'#060505','stroke-width':2});
- svgEl(s,'circle',{cx,cy:fy,r:40,fill:'var(--cream)'});
- const g=svgEl(s,'g',{'clip-path':'url(#rndFace)'});
- const pt=(r,t)=>polar(cx,py,r,sweep(t));
- const [ax1,ay1]=pt(44,0),[ax2,ay2]=pt(44,1);
- svgEl(g,'path',{d:`M ${ax1} ${ay1} A 44 44 0 0 1 ${ax2} ${ay2}`,fill:'none',stroke:'#3a3128','stroke-width':1.1});
- const [rzx,rzy]=pt(44,.75);
- svgEl(g,'path',{d:`M ${rzx} ${rzy} A 44 44 0 0 1 ${ax2} ${ay2}`,fill:'none',stroke:'var(--fail)','stroke-width':3});
- for(const [db,t] of VUDB){const [x1,y1]=pt(41.5,t),[x2,y2]=pt(47,t);
- svgEl(g,'line',{x1,y1,x2,y2,stroke:db>0?'var(--fail)':'#3a3128','stroke-width':1});}
- for(const [db,t] of VUDB){if(![-20,-10,-5,-3,0,3].includes(db))continue;
- const [x,y]=pt(52,t);
- svgEl(g,'text',{x,y:y+2,'text-anchor':'middle','font-size':5.5,'font-family':'var(--mono)',
- fill:db>0?'var(--fail)':'#3a3128'}).textContent=Math.abs(db);}
- svgEl(g,'text',{x:cx,y:fy+14,'text-anchor':'middle','font-size':8,'font-weight':700,
- 'font-family':'var(--mono)',fill:'#3a3128'}).textContent='dB';
- rndNeedle=svgEl(g,'line',{x1:cx,y1:py,x2:cx,y2:py-46,stroke:'#1a1613','stroke-width':1.5,
- transform:`rotate(0,${cx},${py})`});
- svgEl(g,'circle',{cx,cy:py,r:5,fill:'#141110'});
- svgEl(s,'ellipse',{cx:cx-8,cy:fy-22,rx:20,ry:9,fill:'rgba(255,255,255,.07)',transform:`rotate(-18,${cx-8},${fy-22})`});})();
-function setRnd(v){const t=Math.max(0,Math.min(1,v/100));rndT=t;
- rndNeedle.setAttribute('transform',`rotate(${-40+t*80},55,74)`);
- const db=vuDb(t);setRd('R07',(db>0?'+':'')+db.toFixed(1)+' dB');}
-dragDelta($('rndm'),()=>rndT*100,setRnd,{min:0,max:100});setRnd(50);
-
-/* R08 chrome MIN/MAX indicator */
-let chrV=50,chrPtr=null;
-(function(){const s=$('chrmm'),cx=55,cy=58;
- const defs=svgEl(s,'defs',{});
- const ring=svgEl(defs,'linearGradient',{id:'chrRing',x1:0,y1:0,x2:0,y2:1});
- svgEl(ring,'stop',{offset:'0','stop-color':'#eceef0'});
- svgEl(ring,'stop',{offset:'.5','stop-color':'#9aa0a8'});
- svgEl(ring,'stop',{offset:'1','stop-color':'#54585e'});
- const dome=svgEl(defs,'radialGradient',{id:'chrDome',cx:'48%',cy:'20%',r:'85%'});
- svgEl(dome,'stop',{offset:'0','stop-color':'#e6e8ec'});
- svgEl(dome,'stop',{offset:'1','stop-color':'#82878f'});
- svgEl(s,'circle',{cx,cy:54,r:47,fill:'url(#chrRing)'});
- svgEl(s,'circle',{cx,cy:54,r:40,fill:'#101214',stroke:'#2c2f33','stroke-width':1});
- svgEl(s,'path',{d:`M 21 60 A 34 34 0 0 1 89 60 Z`,fill:'url(#chrDome)',stroke:'#55595f','stroke-width':.5});
- for(const r of [14,22,30])
- svgEl(s,'path',{d:`M ${cx-r} 60 A ${r} ${r} 0 0 1 ${cx+r} 60`,fill:'none',stroke:'rgba(255,255,255,.13)','stroke-width':.7});
- chrPtr=svgEl(s,'line',{x1:cx,y1:cy-10,x2:cx,y2:cy-31,stroke:'#17191b','stroke-width':2.2,'stroke-linecap':'round'});
- svgEl(s,'circle',{cx,cy,r:6,fill:'#23262a'});
- for(let k=0;k<8;k++){const [x,y]=polar(cx,cy,6,k*45);
- svgEl(s,'circle',{cx:x,cy:y,r:1.3,fill:'#23262a'});}
- svgEl(s,'circle',{cx,cy,r:1.6,fill:'#0c0d0e'});
- svgEl(s,'text',{x:24,y:66,'text-anchor':'middle','font-size':6,'letter-spacing':'.05em',
- 'font-family':'var(--mono)',fill:'#d7dade'}).textContent='MIN';
- svgEl(s,'text',{x:86,y:66,'text-anchor':'middle','font-size':6,'letter-spacing':'.05em',
- 'font-family':'var(--mono)',fill:'#d7dade'}).textContent='MAX';
- svgEl(s,'text',{x:cx,y:84,'text-anchor':'middle','font-size':7,'font-style':'italic',
- 'font-family':'var(--mono)',fill:'#787c82'}).textContent='dupre.';
- svgEl(s,'ellipse',{cx:47,cy:34,rx:22,ry:9,fill:'rgba(255,255,255,.10)',transform:'rotate(-16,47,34)'});})();
-function setChr(v){chrV=Math.max(0,Math.min(100,v));
- chrPtr.setAttribute('transform',`rotate(${-55+chrV/100*110},55,58)`);
- setRd('R08',chrV<=2?'MIN':chrV>=98?'MAX':Math.round(chrV)+'%');}
-dragDelta($('chrmm'),()=>chrV,setChr,{min:0,max:100});setChr(50);
-
-/* R09 black-face aviation gauge: zone arcs per the airspeed-indicator scheme */
-let avV=52,avNeedle=null;
-(function(){const s=$('avgauge'),cx=55,cy=52,sweep=t=>-135+t*270;
- const defs=svgEl(s,'defs',{});
- const fl=svgEl(defs,'filter',{id:'avGlow',x:'-60%',y:'-60%',width:'220%',height:'220%'});
- svgEl(fl,'feGaussianBlur',{in:'SourceGraphic',stdDeviation:1.4});
- svgEl(s,'circle',{cx,cy,r:47,fill:'#1b1917',stroke:'#060505','stroke-width':2});
- svgEl(s,'circle',{cx,cy,r:41,fill:'#0a0908',stroke:'#3a352c','stroke-width':1});
- const zone=(t1,t2,color)=>{const [x1,y1]=polar(cx,cy,37,sweep(t1)),[x2,y2]=polar(cx,cy,37,sweep(t2));
- svgEl(s,'path',{d:`M ${x1} ${y1} A 37 37 0 ${(t2-t1)>.5?1:0} 1 ${x2} ${y2}`,fill:'none',stroke:color,'stroke-width':3,opacity:.85});};
- zone(.15,.6,'var(--pass)');zone(.6,.85,'var(--gold)');zone(.85,1,'var(--fail)');
- for(let i=0;i<=40;i++){const t=i/40,major=i%5===0,a=sweep(t);
- const [x1,y1]=polar(cx,cy,40,a),[x2,y2]=polar(cx,cy,major?32:36,a);
- svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--gold)','stroke-width':major?1.6:.8,opacity:major?1:.7});
- if(major){const [nx,ny]=polar(cx,cy,25,a);
- svgEl(s,'text',{x:nx,y:ny+2.4,'text-anchor':'middle','font-size':6.5,'font-family':'var(--mono)',
- fill:'var(--gold-hi)'}).textContent=i/5;}}
- svgEl(s,'text',{x:cx,y:cy+16,'text-anchor':'middle','font-size':5.5,'letter-spacing':'.14em',
- 'font-family':'var(--mono)',fill:'var(--gold)',opacity:.8}).textContent='×100 RPM';
- avNeedle=svgEl(s,'g',{});
- svgEl(avNeedle,'line',{x1:cx,y1:cy+8,x2:cx,y2:cy-30,stroke:'var(--gold-hi)','stroke-width':4,
- 'stroke-linecap':'round',opacity:.5,filter:'url(#avGlow)'});
- svgEl(avNeedle,'line',{x1:cx,y1:cy+8,x2:cx,y2:cy-30,stroke:'var(--gold-hi)','stroke-width':2.2,
- 'stroke-linecap':'round'});
- svgEl(s,'circle',{cx,cy,r:5,fill:'#1b1917',stroke:'#3a352c'});
- svgEl(s,'ellipse',{cx:cx-10,cy:cy-24,rx:18,ry:8,fill:'rgba(255,255,255,.05)',transform:`rotate(-18,${cx-10},${cy-24})`});})();
-function setAv(v){avV=Math.max(0,Math.min(100,v));
- avNeedle.setAttribute('transform',`rotate(${-135+avV/100*270},55,52)`);
- setRd('R09',(avV/100*8).toFixed(1)+' ×100 rpm');}
-dragDelta($('avgauge'),()=>avV,setAv,{min:0,max:100});setAv(52);
+
+
+
+/* R07-R09 extracted to widgets.js (GW.*) */
/* R10 data matrix readout */
const DMX_PAGES=[
@@ -1421,14 +1930,14 @@ let dmxI=0,dmxLines=[];
const fl=svgEl(defs,'filter',{id:'dmxGlow',x:'-30%',y:'-30%',width:'160%',height:'160%'});
svgEl(fl,'feGaussianBlur',{in:'SourceGraphic',stdDeviation:1.1});
const bg=svgEl(defs,'linearGradient',{id:'dmxBg',x1:0,y1:0,x2:0,y2:1});
- svgEl(bg,'stop',{offset:'0','stop-color':'#140d06'});
- svgEl(bg,'stop',{offset:'1','stop-color':'#0a0705'});
+ svgEl(bg,'stop',{offset:'0','stop-color':'var(--scr-bg1,#140d06)'});
+ svgEl(bg,'stop',{offset:'1','stop-color':'var(--scr-bg2,#0a0705)'});
svgEl(s,'rect',{x:1,y:1,width:188,height:72,rx:5,fill:'url(#dmxBg)',stroke:'#2c261d'});
for(let r=0;r<3;r++){
const glow=svgEl(s,'text',{x:12,y:24+r*18,'font-size':9,'letter-spacing':'.15em',
- 'font-family':'var(--mono)',fill:'var(--gold-hi)',opacity:.6,filter:'url(#dmxGlow)'});
+ 'font-family':'var(--mono)',fill:'var(--scr-hi,var(--gold-hi))',opacity:.6,filter:'url(#dmxGlow)'});
const crisp=svgEl(s,'text',{x:12,y:24+r*18,'font-size':9,'letter-spacing':'.15em',
- 'font-family':'var(--mono)',fill:'var(--gold-hi)'});
+ 'font-family':'var(--mono)',fill:'var(--scr-hi,var(--gold-hi))'});
dmxLines.push([glow,crisp]);}
$('dmx').onclick=()=>setDmx((dmxI+1)%DMX_PAGES.length);})();
function setDmx(i){dmxI=i;const [name,rowsFn]=DMX_PAGES[i];
@@ -1459,532 +1968,9 @@ function setFlag(on){flagOn=on;flagStripes.style.transform=on?'translateX(0)':'t
setRd('R11',on?'FLAG':'clear');}
setFlag(false);
-/* R12 chrome slot fader: engraved dB scale, chrome T-handle */
-let slotDb=-4,slotHandle=null;
-(function(){const s=$('slotf'),cx=45,yTop=30,yBot=120;
- const yOf=db=>yTop+(12-db)/36*(yBot-yTop);
- const defs=svgEl(s,'defs',{});
- const pl=svgEl(defs,'linearGradient',{id:'sfPlate',x1:0,y1:0,x2:1,y2:1});
- svgEl(pl,'stop',{offset:'0','stop-color':'#242019'});
- svgEl(pl,'stop',{offset:'1','stop-color':'#131110'});
- const ch=svgEl(defs,'linearGradient',{id:'sfChrome',x1:0,y1:0,x2:0,y2:1});
- svgEl(ch,'stop',{offset:'0','stop-color':'#f0efec'});
- svgEl(ch,'stop',{offset:'.45','stop-color':'#c4c1b9'});
- svgEl(ch,'stop',{offset:'1','stop-color':'#75726a'});
- const sc=svgEl(defs,'radialGradient',{id:'sfScrew',cx:'40%',cy:'35%',r:'75%'});
- svgEl(sc,'stop',{offset:'0','stop-color':'#9b968a'});
- svgEl(sc,'stop',{offset:'1','stop-color':'#4a463e'});
- svgEl(s,'rect',{x:14,y:8,width:62,height:134,rx:6,fill:'url(#sfPlate)',stroke:'#0a0908','stroke-width':1.5});
- svgEl(s,'rect',{x:15.5,y:9.5,width:59,height:131,rx:5,fill:'none',stroke:'rgba(255,255,255,.05)','stroke-width':1});
- for(const sy of [16,134]){
- svgEl(s,'circle',{cx,cy:sy,r:3.2,fill:'url(#sfScrew)',stroke:'#14110e','stroke-width':.6});
- const [x1,y1]=polar(cx,sy,3,112),[x2,y2]=polar(cx,sy,3,292);
- svgEl(s,'line',{x1,y1,x2,y2,stroke:'#221f1a','stroke-width':1});}
- svgEl(s,'rect',{x:40.5,y:yTop-4,width:9,height:yBot-yTop+8,rx:4,fill:'#0b0a09',stroke:'#050404'});
- for(const db of [12,8,4,0,-4,-8,-12,-16,-20,-24]){const y=yOf(db);
- svgEl(s,'line',{x1:26,y1:y,x2:39,y2:y,stroke:'var(--steel)','stroke-width':db===0?1.3:.8,opacity:db===0?1:.75});
- svgEl(s,'line',{x1:51,y1:y,x2:64,y2:y,stroke:'var(--steel)','stroke-width':db===0?1.3:.8,opacity:db===0?1:.75});
- svgEl(s,'text',{x:23,y:y+2.2,'text-anchor':'end','font-size':6,'font-family':'var(--mono)',
- fill:db===0?'var(--cream)':'var(--steel)'}).textContent=Math.abs(db);}
- slotHandle=svgEl(s,'g',{});
- svgEl(slotHandle,'polygon',{points:'45,0 58,-5 58,5',fill:'#b9b6ae',stroke:'#5e5a52','stroke-width':.5});
- svgEl(slotHandle,'rect',{x:58,y:-7,width:22,height:14,rx:3.5,fill:'url(#sfChrome)',stroke:'#4e4a42','stroke-width':.6});
- svgEl(slotHandle,'rect',{x:60,y:-5.2,width:18,height:2.2,rx:1,fill:'rgba(255,255,255,.4)'});
- dragY(s,pct=>setSlot(-24+pct/100*36));})();
-function setSlot(db){slotDb=Math.max(-24,Math.min(12,db));
- slotHandle.setAttribute('transform',`translate(0,${30+(12-slotDb)/36*90})`);
- setRd('R12',(slotDb>0?'+':slotDb<0?'−':'')+Math.abs(slotDb).toFixed(1)+' dB');}
-setSlot(-4);
-
-/* R13 edgewise strip meter: compressed log scale, bar rides the edge */
-const EDGE_SCALE=[[0,.08],[3,.26],[6,.44],[12,.62],[20,.78],[40,.94]];
-let edgeFrac=.62,edgeBar=null;
-(function(){const s=$('edgem'),wy0=12,wy1=118;
- const defs=svgEl(s,'defs',{});
- const pg=svgEl(defs,'linearGradient',{id:'edgeFace',x1:0,y1:0,x2:1,y2:0});
- svgEl(pg,'stop',{offset:'0','stop-color':'#efe6ca'});
- svgEl(pg,'stop',{offset:'1','stop-color':'#d9cfae'});
- svgEl(s,'rect',{x:8,y:6,width:54,height:118,rx:8,fill:'#171412',stroke:'#060505','stroke-width':1.5});
- svgEl(s,'rect',{x:14,y:wy0,width:42,height:wy1-wy0,fill:'url(#edgeFace)',stroke:'#0a0908'});
- const yOf=f=>wy0+f*(wy1-wy0);
- for(const [db,f] of EDGE_SCALE){const y=yOf(f);
- svgEl(s,'line',{x1:19,y1:y,x2:40,y2:y,stroke:'#3a3128','stroke-width':1});
- svgEl(s,'text',{x:52,y:y+2.4,'text-anchor':'end','font-size':7,'font-family':'var(--mono)',
- fill:'#3a3128'}).textContent=db;}
- for(let i=0;i<EDGE_SCALE.length-1;i++){const y=yOf((EDGE_SCALE[i][1]+EDGE_SCALE[i+1][1])/2);
- svgEl(s,'line',{x1:19,y1:y,x2:32,y2:y,stroke:'#3a3128','stroke-width':.6,opacity:.7});}
- edgeBar=svgEl(s,'polygon',{points:'56,0 24,-1.4 24,1.4',fill:'#1a1613'});
- svgEl(s,'rect',{x:14,y:wy0,width:42,height:24,fill:'rgba(255,255,255,.08)'});
- dragY(s,pct=>setEdge(.08+(100-pct)/100*.86));})();
-function setEdge(f){edgeFrac=Math.max(.08,Math.min(.94,f));
- edgeBar.setAttribute('transform',`translate(0,${12+edgeFrac*106})`);
- let i=0;while(i<EDGE_SCALE.length-2&&EDGE_SCALE[i+1][1]<edgeFrac)i++;
- const [d1,f1]=EDGE_SCALE[i],[d2,f2]=EDGE_SCALE[i+1];
- const db=d1+(d2-d1)*Math.max(0,Math.min(1,(edgeFrac-f1)/(f2-f1)));
- setRd('R13','−'+db.toFixed(1)+' dB');}
-setEdge(.62);
-
-/* R14 spade-pointer tuning knob: engraved panel arc, pointer rides the knob */
-let spadeV=8.3,spadeGrp=null;
-(function(){const s=$('spade'),cx=70,cy=82,sweep=v=>-80+v/10*160;
- const defs=svgEl(s,'defs',{});
- const bk=svgEl(defs,'radialGradient',{id:'spadeKnob',cx:'42%',cy:'34%',r:'80%'});
- svgEl(bk,'stop',{offset:'0','stop-color':'#33302a'});
- svgEl(bk,'stop',{offset:'1','stop-color':'#080706'});
- const mt=svgEl(defs,'linearGradient',{id:'spadeMetal',x1:0,y1:0,x2:1,y2:1});
- svgEl(mt,'stop',{offset:'0','stop-color':'#e9e7e0'});
- svgEl(mt,'stop',{offset:'1','stop-color':'#8d897f'});
- // engraved arc: every stroke carries a faint light twin offset below (incised relief)
- const engrave=(x1,y1,x2,y2,w)=>{
- svgEl(s,'line',{x1,y1:y1+.8,x2,y2:y2+.8,stroke:'rgba(255,255,255,.13)','stroke-width':w});
- svgEl(s,'line',{x1,y1,x2,y2,stroke:'#050404','stroke-width':w});};
- for(let i=0;i<=40;i++){const v=i/4,a=sweep(v),major=i%4===0;
- const [x1,y1]=polar(cx,cy,major?50:52,a),[x2,y2]=polar(cx,cy,58,a);
- engrave(x1,y1,x2,y2,major?1.6:.9);
- if(major){const [nx,ny]=polar(cx,cy,65,a);
- svgEl(s,'text',{x:nx,y:ny+3.2,'text-anchor':'middle','font-size':7.5,'font-family':'var(--mono)',
- fill:'rgba(255,255,255,.13)'}).textContent=v;
- svgEl(s,'text',{x:nx,y:ny+2.4,'text-anchor':'middle','font-size':7.5,'font-family':'var(--mono)',
- fill:'#0a0908'}).textContent=v;}}
- spadeGrp=svgEl(s,'g',{});
- svgEl(spadeGrp,'path',{d:`M ${cx-2} ${cy-28} L ${cx-4.5} ${cy-42} L ${cx} ${cy-54} L ${cx+4.5} ${cy-42} L ${cx+2} ${cy-28} Z`,
- fill:'url(#spadeMetal)',stroke:'#55524a','stroke-width':.6});
- svgEl(spadeGrp,'circle',{cx,cy,r:30,fill:'url(#spadeKnob)',stroke:'#000','stroke-width':.8});
- svgEl(spadeGrp,'circle',{cx,cy,r:28.5,fill:'none',stroke:'#0a0908','stroke-width':3,'stroke-dasharray':'2.2 2.2'});
- svgEl(spadeGrp,'circle',{cx,cy,r:22,fill:'url(#spadeKnob)'});
- svgEl(spadeGrp,'ellipse',{cx:cx-8,cy:cy-9,rx:8,ry:5,fill:'rgba(255,255,255,.08)',transform:`rotate(-30,${cx-8},${cy-9})`});})();
-function setSpade(v){spadeV=Math.max(0,Math.min(10,v));
- spadeGrp.setAttribute('transform',`rotate(${-80+spadeV/10*160},70,82)`);
- setRd('R14',spadeV.toFixed(1));}
-dragDelta($('spade'),()=>spadeV,setSpade,{min:0,max:10,sens:.05});setSpade(8.3);
-
-/* R15 multi-band dial: nested arcs, one needle; bandspread selects the ring */
-const MBAND_RANGES=[[0.54,1.6],[1.6,5.1],[5.1,15.5],[15.5,30.5]];
-let mbandV=45,mbandB=2,mbNeedle=null,mbRings=[],mbSpread=null;
-(function(){const s=$('mband'),cx=62,cy=62,sweep=t=>-70+t*140;
- const defs=svgEl(s,'defs',{});
- const pf=svgEl(defs,'linearGradient',{id:'mbFace',x1:0,y1:0,x2:1,y2:1});
- svgEl(pf,'stop',{offset:'0','stop-color':'#eee5c8'});
- svgEl(pf,'stop',{offset:'1','stop-color':'#dcd2ae'});
- svgEl(s,'rect',{x:4,y:4,width:182,height:102,rx:12,fill:'#171412',stroke:'#060505'});
- svgEl(s,'rect',{x:9,y:9,width:172,height:92,rx:9,fill:'url(#mbFace)',stroke:'#0a0908'});
- const INK='#2c2318';
- [18,26,34,42].forEach((r,bi)=>{const ring=svgEl(s,'g',{});
- const [x1,y1]=polar(cx,cy,r,-70),[x2,y2]=polar(cx,cy,r,70);
- svgEl(ring,'path',{d:`M ${x1} ${y1} A ${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',stroke:INK,'stroke-width':.9});
- for(let i=0;i<=8;i++){const a=sweep(i/8),[tx1,ty1]=polar(cx,cy,r,a),[tx2,ty2]=polar(cx,cy,r+3,a);
- svgEl(ring,'line',{x1:tx1,y1:ty1,x2:tx2,y2:ty2,stroke:INK,'stroke-width':.8});}
- mbRings.push(ring);});
- mbNeedle=svgEl(s,'line',{x1:cx,y1:cy,x2:cx,y2:cy-46,stroke:'#1a1613','stroke-width':1.6,'stroke-linecap':'round'});
- svgEl(s,'circle',{cx,cy,r:7,fill:'url(#spadeKnob)',stroke:'#000','stroke-width':.6});
- svgEl(s,'text',{x:14,y:100,'font-size':5,'letter-spacing':'.12em','font-family':'var(--mono)',
- fill:INK,opacity:.8}).textContent='MEGACYCLES';
- // bandspread dial (click = band select)
- const bx=142,by=56;
- for(let i=0;i<12;i++){const a=i*30,[x1,y1]=polar(bx,by,24,a),[x2,y2]=polar(bx,by,21,a);
- svgEl(s,'line',{x1,y1,x2,y2,stroke:INK,'stroke-width':.8});}
- svgEl(s,'circle',{cx:bx,cy:by,r:17,fill:'none',stroke:INK,'stroke-width':.7,opacity:.5});
- mbSpread=svgEl(s,'line',{x1:bx,y1:by,x2:bx,y2:by-22,stroke:'#1a1613','stroke-width':1.4,'stroke-linecap':'round'});
- svgEl(s,'circle',{cx:bx,cy:by,r:6,fill:'url(#spadeKnob)',stroke:'#000','stroke-width':.6});
- svgEl(s,'text',{x:bx,y:96,'text-anchor':'middle','font-size':5,'letter-spacing':'.12em',
- 'font-family':'var(--mono)',fill:INK,opacity:.8}).textContent='BANDSPREAD';
- const dragHit=svgEl(s,'rect',{x:9,y:9,width:100,height:92,fill:'transparent'});
- dragHit.style.cursor='ns-resize';
- dragDelta(dragHit,()=>mbandV,v=>setMband(v,mbandB),{min:0,max:100});
- const clickHit=svgEl(s,'rect',{x:112,y:9,width:69,height:92,fill:'transparent'});
- clickHit.style.cursor='pointer';
- clickHit.addEventListener('click',()=>setMband(mbandV,(mbandB+1)%4));})();
-function setMband(v,b){mbandV=Math.max(0,Math.min(100,v));mbandB=b;
- mbNeedle.setAttribute('transform',`rotate(${-70+mbandV/100*140},62,62)`);
- mbSpread.setAttribute('transform',`rotate(${-45+b*30},142,56)`);
- mbRings.forEach((g,i)=>g.setAttribute('opacity',i===b?'1':'.4'));
- const [lo,hi]=MBAND_RANGES[b];const mc=lo+mbandV/100*(hi-lo);
- setRd('R15',`B${b+1} · ${mc.toFixed(2)} Mc`);}
-setMband(45,2);
-
-/* R16 entry keypad: worn keys feed the amber display; lamps watch state */
-let kpBuf='',kpDisp=null,kpLampTop=null,kpLampBot=null;
-(function(){const s=$('keypad');
- const defs=svgEl(s,'defs',{});
- const kg=svgEl(defs,'linearGradient',{id:'kpKey',x1:0,y1:0,x2:0,y2:1});
- svgEl(kg,'stop',{offset:'0','stop-color':'#dbd8cf'});
- svgEl(kg,'stop',{offset:'1','stop-color':'#a29d92'});
- const ag=svgEl(defs,'linearGradient',{id:'kpAmber',x1:0,y1:0,x2:0,y2:1});
- svgEl(ag,'stop',{offset:'0','stop-color':'var(--amber-grad-top)'});
- svgEl(ag,'stop',{offset:'1','stop-color':'var(--amber-grad-mid)'});
- // lamp pod
- const lamp=cy=>{const l=svgEl(s,'circle',{cx:13,cy,r:6.5,fill:'#3a0f0a',stroke:'#14100e','stroke-width':1.5});return l;};
- kpLampTop=lamp(20);kpLampBot=lamp(38);
- // display window
- svgEl(s,'rect',{x:26,y:8,width:116,height:32,rx:5,fill:'#0a0806',stroke:'#2c261d','stroke-width':2});
- kpDisp=svgEl(s,'text',{x:84,y:31,'text-anchor':'middle','font-size':16,'letter-spacing':'.22em',
- 'font-family':'var(--mono)',fill:'var(--gold-hi)'});
- // key plate
- svgEl(s,'rect',{x:24,y:48,width:118,height:138,rx:8,fill:'#1a1714',stroke:'#0a0908','stroke-width':1.5});
- const KEYS=[['1','2','3'],['4','5','6'],['7','8','9'],['✓','0','✗']];
- const jitter=[-2,1,-1,2,0,-2,1,-1,2,0,-1,1];
- KEYS.forEach((row,r)=>row.forEach((k,c)=>{
- const x=47+c*36,y=68+r*32,amber=(k==='✓'||k==='✗');
- const g=svgEl(s,'g',{});g.style.cursor='pointer';g.style.transition='transform .07s';
- svgEl(g,'rect',{x:x-14,y:y-11,width:28,height:24,rx:4,fill:amber?'url(#kpAmber)':'url(#kpKey)',
- stroke:'#4e4a42','stroke-width':1,'stroke-opacity':.8});
- svgEl(g,'text',{x,y:y+6,'text-anchor':'middle','font-size':13,'font-weight':700,
- 'font-family':'var(--mono)',fill:k==='✓'?'#3f5a1f':k==='✗'?'#7a2a1a':'#2b2721',
- transform:`rotate(${jitter[r*3+c]},${x},${y})`}).textContent=k;
- g.addEventListener('click',()=>{g.style.transform='translateY(1.5px)';
- setTimeout(()=>g.style.transform='',80);kpPress(k);});}));
- })();
-function kpRender(){kpDisp.textContent=kpBuf.padEnd(6,'–');
- kpLampTop.setAttribute('fill',kpBuf?'var(--jewel-r)':'#3a0f0a');}
-function kpPress(k){
- if(k==='✓'){setRd('R16',kpBuf?'OK · '+kpBuf:'empty');kpBuf='';kpRender();return;}
- if(k==='✗'){kpBuf='';kpRender();setRd('R16','cleared');
- kpLampBot.setAttribute('fill','var(--jewel-r)');
- setTimeout(()=>kpLampBot.setAttribute('fill','#3a0f0a'),350);return;}
- if(kpBuf.length<6){kpBuf+=k;kpRender();setRd('R16',kpBuf);}}
-kpRender();setRd('R16','enter a code');
-
-/* R17 round CRT scope: pale phosphor face, live trace */
-let rcrtPh=0,rcrtTrace=null;
-(function(){const s=$('rcrt'),cx=55,cy=52;
- const defs=svgEl(s,'defs',{});
- const ph=svgEl(defs,'radialGradient',{id:'rcrtFace',cx:'50%',cy:'44%',r:'70%'});
- svgEl(ph,'stop',{offset:'0','stop-color':'#b9d8c0'});
- svgEl(ph,'stop',{offset:'1','stop-color':'#8bb296'});
- const clip=svgEl(defs,'clipPath',{id:'rcrtClip'});
- svgEl(clip,'circle',{cx,cy,r:38});
- svgEl(s,'circle',{cx,cy,r:48,fill:'#141110',stroke:'#060505','stroke-width':2});
- for(let k=0;k<4;k++){const [x,y]=polar(cx,cy,43,45+k*90);
- svgEl(s,'circle',{cx:x,cy:y,r:2.6,fill:'url(#sfScrew)',stroke:'#0a0908','stroke-width':.5});}
- svgEl(s,'circle',{cx,cy,r:38,fill:'url(#rcrtFace)'});
- const g=svgEl(s,'g',{'clip-path':'url(#rcrtClip)'});
- for(let i=-4;i<=4;i++){
- svgEl(g,'line',{x1:cx+i*9,y1:cy-38,x2:cx+i*9,y2:cy+38,stroke:'#3d5c46','stroke-width':i===0?.9:.5,opacity:.8});
- svgEl(g,'line',{x1:cx-38,y1:cy+i*9,x2:cx+38,y2:cy+i*9,stroke:'#3d5c46','stroke-width':i===0?.9:.5,opacity:.8});}
- svgEl(g,'polyline',{id:'rcrtGlow',points:'',fill:'none',stroke:'#eef7ee','stroke-width':3,opacity:.35,filter:'url(#avGlow)'});
- rcrtTrace=svgEl(g,'polyline',{points:'',fill:'none',stroke:'#f4fcf4','stroke-width':1.3});
- svgEl(s,'ellipse',{cx:cx-10,cy:cy-16,rx:20,ry:10,fill:'rgba(255,255,255,.10)',transform:`rotate(-20,${cx-10},${cy-16})`});})();
-function tickRcrt(){rcrtPh+=0.12;const amp=8+3*Math.sin(rcrtPh*0.35);let pts='';
- for(let x=-38;x<=38;x+=2){const y=Math.sin(x*0.55+rcrtPh*2.5)*amp;
- pts+=`${55+x},${(52+y).toFixed(1)} `;}
- rcrtTrace.setAttribute('points',pts.trim());
- document.getElementById('rcrtGlow').setAttribute('points',pts.trim());
- setRd('R17','Vpp '+Math.round(amp*2*4.5));}
-tickRcrt();
-if(!matchMedia('(prefers-reduced-motion: reduce)').matches)setInterval(tickRcrt,80);
-
-/* R18 thumb-slide attenuator pair: lit numeral strip, side thumb tab */
-const THSL=[{name:'BLEND',v:74},{name:'MIX',v:92}];
-const thslParts=[];
-(function(){const s=$('thumbsl'),y0=22,y1=122;
- const defs=svgEl(s,'defs',{});
- const rg=svgEl(defs,'linearGradient',{id:'thRail',x1:0,y1:0,x2:1,y2:0});
- svgEl(rg,'stop',{offset:'0','stop-color':'#3a3733'});
- svgEl(rg,'stop',{offset:'.5','stop-color':'#211f1c'});
- svgEl(rg,'stop',{offset:'1','stop-color':'#161412'});
- const tg=svgEl(defs,'linearGradient',{id:'thTab',x1:0,y1:0,x2:0,y2:1});
- svgEl(tg,'stop',{offset:'0','stop-color':'#f2efe6'});
- svgEl(tg,'stop',{offset:'1','stop-color':'#c6c1b3'});
- THSL.forEach((ch,i)=>{const x=40+i*50;
- svgEl(s,'rect',{x:x-9,y:y0-10,width:18,height:y1-y0+20,rx:3,fill:'url(#thRail)',stroke:'#0a0908','stroke-width':1.2});
- for(const sy of [y0-6,y1+6]){svgEl(s,'circle',{cx:x,cy:sy,r:2.4,fill:'url(#sfScrew)',stroke:'#0a0908','stroke-width':.5});
- svgEl(s,'line',{x1:x-1.8,y1:sy-1,x2:x+1.8,y2:sy+1,stroke:'#14110e','stroke-width':.8});}
- svgEl(s,'rect',{x:x-5,y:y0,width:10,height:y1-y0,fill:'#0d0a08',stroke:'#050404'});
- for(let n=0;n<=10;n++){const val=100-n*10,y=y0+6+n*(y1-y0-12)/10;
- svgEl(s,'text',{x,y:y+2,'text-anchor':'middle','font-size':5.5,'font-family':'var(--mono)',
- fill:'var(--gold)',opacity:.55}).textContent=val;}
- const tab=svgEl(s,'g',{});
- svgEl(tab,'rect',{x:x+7,y:-6,width:8,height:12,rx:2,fill:'url(#thTab)',stroke:'#7a766a','stroke-width':.6});
- svgEl(tab,'rect',{x:x+8.2,y:-1,width:5.6,height:1.6,fill:'rgba(0,0,0,.25)'});
- svgEl(s,'text',{x,y:145,'text-anchor':'middle','font-size':6,'letter-spacing':'.14em',
- 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=ch.name;
- const hit=svgEl(s,'rect',{x:x-12,y:y0-10,width:30,height:y1-y0+20,fill:'transparent'});
- hit.style.cursor='ns-resize';
- thslParts.push({tab,nums:[...s.querySelectorAll('text')].filter(t=>t.getAttribute('x')==String(x)&&t.textContent!==ch.name)});
- dragY(hit,pct=>setThsl(i,pct));});
- })();
-function setThsl(i,v){v=Math.max(0,Math.min(100,Math.round(v)));THSL[i].v=v;
- const p=thslParts[i];
- p.tab.setAttribute('transform',`translate(0,${28+(100-v)*0.88})`);
- p.nums.forEach(t=>{const on=Math.abs(parseInt(t.textContent)-v)<=5;
- t.setAttribute('fill',on?'var(--gold-hi)':'var(--gold)');t.setAttribute('opacity',on?'1':'.55');});
- setRd('R18',`${THSL[0].name} ${THSL[0].v} · ${THSL[1].name} ${THSL[1].v}`);}
-setThsl(0,74);setThsl(1,92);
-
-/* R19 waveform region editor: monochrome LCD, draggable S/E flags */
-let wrS=22,wrE=76,wrBars=[],wrFlagS=null,wrFlagE=null;
-(function(){const s=$('wregion'),x0=10,x1=180,yMid=48,N=85;
- svgEl(s,'rect',{x:1,y:1,width:188,height:98,rx:6,fill:'#0b0c0b',stroke:'#2a2c2a','stroke-width':2});
- svgEl(s,'text',{x:12,y:13,'font-size':7,'font-family':'var(--mono)',fill:'#e8eae8'}).textContent='C1:START';
- svgEl(s,'text',{x:78,y:13,'font-size':7,'font-family':'var(--mono)',fill:'#e8eae8'}).textContent='C2:OFF';
- svgEl(s,'text',{x:136,y:13,'font-size':7,'font-family':'var(--mono)',fill:'#e8eae8'}).textContent='C3:END';
- svgEl(s,'line',{x1:x0,y1:yMid,x2:x1,y2:yMid,stroke:'#3a3c3a','stroke-width':.6,'stroke-dasharray':'1.5 2'});
- for(let i=0;i<N;i++){const x=x0+(i/(N-1))*(x1-x0);
- const env=Math.abs(Math.sin(i*0.19))*Math.abs(Math.sin(i*0.045))*(i%17<11?1:.25);
- const h=Math.max(1.2,env*26);
- wrBars.push(svgEl(s,'rect',{x:x-0.8,y:yMid-h,width:1.6,height:h*2,fill:'#5a5c5a'}));}
- const flag=(lbl)=>{const g=svgEl(s,'g',{});
- svgEl(g,'line',{x1:0,y1:18,x2:0,y2:78,stroke:'#f2f4f2','stroke-width':1});
- svgEl(g,'rect',{x:lbl==='S'?0:-8,y:70,width:8,height:8,fill:'#f2f4f2'});
- svgEl(g,'text',{x:lbl==='S'?4:-4,y:76.5,'text-anchor':'middle','font-size':6.5,'font-weight':700,
- 'font-family':'var(--mono)',fill:'#0b0c0b'}).textContent=lbl;return g;};
- wrFlagS=flag('S');wrFlagE=flag('E');
- svgEl(s,'rect',{x:10,y:84,width:78,height:11,fill:'#e8eae8'});
- svgEl(s,'text',{x:14,y:92.5,'font-size':6.5,'font-family':'var(--mono)',fill:'#0b0c0b'}).textContent='ENC:ZOOM(1x)';
- svgEl(s,'text',{x:96,y:92.5,'font-size':6.5,'font-family':'var(--mono)',fill:'#e8eae8'}).textContent='A-3 M:[S]';
- svgEl(s,'rect',{x:148,y:84,width:32,height:11,fill:'#e8eae8'});
- svgEl(s,'text',{x:164,y:92.5,'text-anchor':'middle','font-size':6.5,'font-weight':700,
- 'font-family':'var(--mono)',fill:'#0b0c0b'}).textContent='MENU';
- s.style.cursor='ew-resize';
- dragX(s,pct=>{
- // move whichever flag is nearer the pointer
- if(Math.abs(pct-wrS)<=Math.abs(pct-wrE))setWregion(Math.min(pct,wrE-4),wrE);
- else setWregion(wrS,Math.max(pct,wrS+4));});})();
-function setWregion(sv,ev){wrS=Math.max(0,Math.min(96,sv));wrE=Math.max(4,Math.min(100,ev));
- const x0=10,x1=180,xOf=p=>x0+p/100*(x1-x0);
- wrFlagS.setAttribute('transform',`translate(${xOf(wrS)},0)`);
- wrFlagE.setAttribute('transform',`translate(${xOf(wrE)},0)`);
- wrBars.forEach((b,i)=>{const p=i/(wrBars.length-1)*100;
- b.setAttribute('fill',(p>=wrS&&p<=wrE)?'#f2f4f2':'#5a5c5a');});
- setRd('R19',`S ${Math.round(wrS)}% · E ${Math.round(wrE)}%`);}
-setWregion(22,76);
-
-/* R20 drum roller selector: numbered drum in a window, center chip reads it */
-const DRUMR=[{name:'HIGH',v:6},{name:'LOW',v:8}];
-const drumrGrps=[];
-(function(){const s=$('drumr'),cy=76,step=17;
- const defs=svgEl(s,'defs',{});
- const paper=svgEl(defs,'linearGradient',{id:'drPaper',x1:0,y1:0,x2:1,y2:0});
- svgEl(paper,'stop',{offset:'0','stop-color':'#cfc6a8'});
- svgEl(paper,'stop',{offset:'.5','stop-color':'#ece4c8'});
- svgEl(paper,'stop',{offset:'1','stop-color':'#c6bd9e'});
- const curve=svgEl(defs,'linearGradient',{id:'drCurve',x1:0,y1:0,x2:0,y2:1});
- svgEl(curve,'stop',{offset:'0','stop-color':'rgba(0,0,0,.4)'});
- svgEl(curve,'stop',{offset:'.28','stop-color':'rgba(0,0,0,0)'});
- svgEl(curve,'stop',{offset:'.72','stop-color':'rgba(0,0,0,0)'});
- svgEl(curve,'stop',{offset:'1','stop-color':'rgba(0,0,0,.4)'});
- svgEl(s,'text',{x:65,y:12,'text-anchor':'middle','font-size':7,'letter-spacing':'.18em',
- 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='EQUALIZER';
- DRUMR.forEach((ch,i)=>{const x=45+i*50;
- svgEl(s,'text',{x:x+18,y:34,'text-anchor':'middle','font-size':8,fill:'var(--steel)'}).textContent='+';
- svgEl(s,'text',{x:x+18,y:124,'text-anchor':'middle','font-size':8,fill:'var(--steel)'}).textContent='−';
- svgEl(s,'rect',{x:x-12,y:24,width:24,height:104,rx:4,fill:'url(#thRail)',stroke:'#0a0908','stroke-width':1.2});
- const clip=svgEl(defs,'clipPath',{id:'drClip'+i});
- svgEl(clip,'rect',{x:x-9,y:28,width:18,height:96});
- svgEl(s,'rect',{x:x-9,y:28,width:18,height:96,fill:'url(#drPaper)'});
- const g=svgEl(s,'g',{'clip-path':`url(#drClip${i})`});
- const nums=svgEl(g,'g',{});nums.style.transition='transform .12s';
- for(let n=1;n<=10;n++)
- svgEl(nums,'text',{x,y:cy+3+(ch.v-n)*step,'text-anchor':'middle','font-size':9,'font-weight':700,
- 'font-family':'var(--mono)',fill:'#2b2418'}).textContent=n;
- svgEl(g,'rect',{x:x-9,y:28,width:18,height:96,fill:'url(#drCurve)','pointer-events':'none'});
- svgEl(s,'rect',{x:x-8,y:cy-6.5,width:16,height:13,rx:1.5,fill:'#1a1613',opacity:.92});
- const chip=svgEl(s,'text',{x,y:cy+3,'text-anchor':'middle','font-size':9,'font-weight':700,
- 'font-family':'var(--mono)',fill:'var(--cream)'});
- svgEl(s,'text',{x,y:136,'text-anchor':'middle','font-size':6,'letter-spacing':'.12em',
- 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=ch.name;
- const hit=svgEl(s,'rect',{x:x-14,y:24,width:28,height:104,fill:'transparent'});
- hit.style.cursor='ns-resize';
- drumrGrps.push({nums,chip,base:ch.v});
- dragDelta(hit,()=>DRUMR[i].v,v=>setDrumr(i,v),{min:1,max:10,sens:.08});});
- })();
-function setDrumr(i,v){v=Math.max(1,Math.min(10,v));DRUMR[i].v=v;
- const p=drumrGrps[i];
- p.nums.style.transform=`translateY(${(v-p.base)*17}px)`;
- p.chip.textContent=Math.round(v);
- setRd('R20',`${DRUMR[0].name} ${Math.round(DRUMR[0].v)} · ${DRUMR[1].name} ${Math.round(DRUMR[1].v)}`);}
-setDrumr(0,6);setDrumr(1,8);
-
-/* R21 LED program row: exclusive select, the LED carries the state */
-const LEDROW_PROGS=['Sm Hall B','VocPlate','Lg Hall B','Chamber','ParcPlate','Sm Hall A','Room A','Const Plate'];
-let ledrowI=5,ledrowLeds=[];
-(function(){const s=$('ledrow');
- const defs=svgEl(s,'defs',{});
- const kb=svgEl(defs,'linearGradient',{id:'lrKey',x1:0,y1:0,x2:0,y2:1});
- svgEl(kb,'stop',{offset:'0','stop-color':'#2b2823'});
- svgEl(kb,'stop',{offset:'1','stop-color':'#171512'});
- LEDROW_PROGS.forEach((name,i)=>{const x=16+i*22.6;
- svgEl(s,'text',{x,y:9,'text-anchor':'middle','font-size':6,'font-family':'var(--mono)',
- fill:'var(--steel)'}).textContent=i+1;
- ledrowLeds.push(svgEl(s,'circle',{cx:x,cy:17,r:2.4,fill:'#3a0f0a'}));
- const g=svgEl(s,'g',{});g.style.cursor='pointer';g.style.transition='transform .07s';
- svgEl(g,'rect',{x:x-8.5,y:24,width:17,height:22,rx:2.5,fill:'url(#lrKey)',stroke:'#0a0908','stroke-width':1});
- svgEl(g,'rect',{x:x-6.5,y:26.5,width:13,height:3,rx:1.5,fill:'rgba(255,255,255,.06)'});
- g.addEventListener('click',()=>{g.style.transform='translateY(1.5px)';
- setTimeout(()=>g.style.transform='',80);setLedrow(i);});});
- svgEl(s,'text',{x:95,y:56,'text-anchor':'middle','font-size':6,'letter-spacing':'.16em',
- 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='PROGRAM';})();
-function setLedrow(i){ledrowI=i;
- ledrowLeds.forEach((l,k)=>{const on=k===i;
- l.setAttribute('fill',on?'var(--jewel-r)':'#3a0f0a');
- l.setAttribute('style',on?'filter:drop-shadow(0 0 3px rgba(255,91,69,.8))':'');});
- setRd('R21',`${ledrowI+1} · ${LEDROW_PROGS[ledrowI]}`);}
-setLedrow(5);
-
-/* R22 three-position slide: chrome pill, detents, honest LED pair */
-const VARSL_POS=['A','AB','B'];
-let varslI=1,varslPill=null,varslLbls=[],varslLeds=[];
-(function(){const s=$('varslide');const detX=[34,55,76];
- svgEl(s,'text',{x:55,y:10,'text-anchor':'middle','font-size':5.5,'letter-spacing':'.16em',
- 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='BASIC · VARIATION';
- svgEl(s,'rect',{x:20,y:16,width:70,height:14,rx:7,fill:'#0b0a09',stroke:'#000','stroke-width':1});
- svgEl(s,'rect',{x:21,y:17,width:68,height:5,rx:2.5,fill:'rgba(255,255,255,.04)'});
- varslPill=svgEl(s,'rect',{x:-13,y:18.5,width:26,height:9,rx:4.5,fill:'url(#sfChrome)',stroke:'#4e4a42','stroke-width':.6});
- varslPill.style.transition='transform .12s';
- VARSL_POS.forEach((lbl,i)=>{
- varslLbls.push(svgEl(s,'text',{x:detX[i],y:40,'text-anchor':'middle','font-size':6.5,
- 'font-family':'var(--mono)',fill:'var(--dim)'}));
- varslLbls[i].textContent=lbl;});
- svgEl(s,'rect',{x:32,y:45,width:46,height:11,rx:2,fill:'#141210',stroke:'#060505'});
- [44,66].forEach(x=>varslLeds.push(svgEl(s,'circle',{cx:x,cy:50.5,r:2.4,fill:'#3a0f0a'})));
- s.addEventListener('click',e=>{const r=s.getBoundingClientRect();
- const x=(e.clientX-r.left)/r.width*110;
- setVarsl(x<45?0:x<66?1:2);});})();
-function setVarsl(i){varslI=i;const detX=[34,55,76];
- varslPill.setAttribute('transform',`translate(${detX[i]},0)`);
- varslLbls.forEach((t,k)=>t.setAttribute('fill',k===i?'var(--gold-hi)':'var(--dim)'));
- const lit=[i===0||i===1,i===2||i===1];
- varslLeds.forEach((l,k)=>l.setAttribute('fill',lit[k]?'var(--jewel-r)':'#3a0f0a'));
- setRd('R22',VARSL_POS[i]);}
-setVarsl(1);
-
-/* R23 spun-aluminum knob: machined rings in a knurled grip */
-let spunV=62,spunIdx=null;
-(function(){const s=$('spunk'),cx=55,cy=52;
- const defs=svgEl(s,'defs',{});
- const al=svgEl(defs,'radialGradient',{id:'spunFace',cx:'42%',cy:'34%',r:'85%'});
- svgEl(al,'stop',{offset:'0','stop-color':'#e8e6e0'});
- svgEl(al,'stop',{offset:'.7','stop-color':'#b3b0a8'});
- svgEl(al,'stop',{offset:'1','stop-color':'#8a877e'});
- for(let k=0;k<8;k++){const [x,y]=polar(cx,cy,45,-135+k*38.6);
- svgEl(s,'circle',{cx:x,cy:y,r:1.3,fill:'var(--steel)'});}
- for(let k=0;k<22;k++){const [x,y]=polar(cx,cy,36,k*16.36);
- svgEl(s,'circle',{cx:x,cy:y,r:2.6,fill:'#14110e'});}
- svgEl(s,'circle',{cx,cy,r:36,fill:'#1a1714',stroke:'#000','stroke-width':.8});
- svgEl(s,'circle',{cx,cy,r:30,fill:'url(#spunFace)',stroke:'#6e6b63','stroke-width':.8});
- for(const r of [24,18,12,6])
- svgEl(s,'circle',{cx,cy,r,fill:'none',stroke:r%12===0?'rgba(0,0,0,.12)':'rgba(255,255,255,.3)','stroke-width':.6});
- spunIdx=svgEl(s,'line',{x1:cx,y1:cy-28,x2:cx,y2:cy-10,stroke:'var(--fail)','stroke-width':2.6,'stroke-linecap':'round'});
- svgEl(s,'ellipse',{cx:cx-9,cy:cy-11,rx:9,ry:5,fill:'rgba(255,255,255,.28)',transform:`rotate(-32,${cx-9},${cy-11})`});
- svgEl(s,'text',{x:cx,y:103,'text-anchor':'middle','font-size':6,'letter-spacing':'.16em',
- 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='SPEED';})();
-function setSpun(v){spunV=Math.max(0,Math.min(100,v));
- spunIdx.setAttribute('transform',`rotate(${-135+spunV/100*270},55,52)`);
- setRd('R23',Math.round(spunV)+'%');}
-dragDelta($('spunk'),()=>spunV,setSpun,{min:0,max:100});setSpun(62);
-
-/* R24 stomp switch + jewel: press to engage, the jewel reports */
-let stompOn=false,stompDome=null,stompJewel=null,stompGlow=null;
-(function(){const s=$('stomp'),cx=55;
- const defs=svgEl(s,'defs',{});
- const dg=svgEl(defs,'radialGradient',{id:'stompDome',cx:'40%',cy:'30%',r:'80%'});
- svgEl(dg,'stop',{offset:'0','stop-color':'#f4f2ec'});
- svgEl(dg,'stop',{offset:'.55','stop-color':'#b9b6ae'});
- svgEl(dg,'stop',{offset:'1','stop-color':'#6e6b63'});
- const jg=svgEl(defs,'radialGradient',{id:'stompJewel',cx:'38%',cy:'30%',r:'80%'});
- svgEl(jg,'stop',{offset:'0','stop-color':'#ffe9b0'});
- svgEl(jg,'stop',{offset:'.45','stop-color':'var(--jewel-a)'});
- svgEl(jg,'stop',{offset:'1','stop-color':'#5c3d0c'});
- stompGlow=svgEl(s,'circle',{cx,cy:22,r:14,fill:'rgba(255,180,58,.35)',filter:'url(#avGlow)',opacity:0});
- stompJewel=svgEl(s,'circle',{cx,cy:22,r:9,fill:'#241f1b',stroke:'#0a0908','stroke-width':1.5});
- for(let k=0;k<6;k++){const a1=k*60,a2=k*60+30;
- const [x1,y1]=polar(cx,22,9,a1),[x2,y2]=polar(cx,22,9,a2);
- svgEl(s,'path',{d:`M ${cx} 22 L ${x1} ${y1} A 9 9 0 0 1 ${x2} ${y2} Z`,
- fill:k%2?'rgba(255,255,255,.09)':'rgba(0,0,0,.10)','pointer-events':'none'});}
- for(let k=0;k<20;k++){const [x,y]=polar(cx,72,25,k*18);
- svgEl(s,'circle',{cx:x,cy:y,r:2.4,fill:'#14110e'});}
- svgEl(s,'circle',{cx,cy:72,r:25,fill:'#1a1714',stroke:'#000','stroke-width':.8});
- stompDome=svgEl(s,'g',{});stompDome.style.transition='transform .08s';
- svgEl(stompDome,'circle',{cx,cy:72,r:19,fill:'url(#stompDome)',stroke:'#55524a','stroke-width':.8});
- svgEl(stompDome,'ellipse',{cx:cx-6,cy:64,rx:8,ry:4.5,fill:'rgba(255,255,255,.5)',transform:`rotate(-24,${cx-6},64)`});
- $('stomp').onclick=()=>{stompDome.style.transform='translateY(1.5px)';
- setTimeout(()=>stompDome.style.transform='',90);setStomp(!stompOn);};})();
-function setStomp(on){stompOn=on;
- stompJewel.setAttribute('fill',on?'url(#stompJewel)':'#241f1b');
- stompGlow.setAttribute('opacity',on?'1':'0');
- setRd('R24',on?'ENGAGED':'bypass');}
-setStomp(false);
-
-/* R27 winged gain selector: red T-bar over a dot ring, stepped detents */
-const WINGK_STEPS=[-80,-70,-60,-50,-40,-30,-20,-10,-5,0,5,10];
-let wingI=7,wingGrp=null;
-(function(){const s=$('wingk'),cx=55,cy=52;
- const defs=svgEl(s,'defs',{});
- const rg2=svgEl(defs,'radialGradient',{id:'wingRed',cx:'40%',cy:'32%',r:'80%'});
- svgEl(rg2,'stop',{offset:'0','stop-color':'#d98a6f'});
- svgEl(rg2,'stop',{offset:'.6','stop-color':'var(--fail)'});
- svgEl(rg2,'stop',{offset:'1','stop-color':'#6e2415'});
- WINGK_STEPS.forEach((db,i)=>{const a=-135+i/(WINGK_STEPS.length-1)*270;
- const [dx,dy]=polar(cx,cy,38,a);
- svgEl(s,'circle',{cx:dx,cy:dy,r:1.5,fill:'var(--steel)'});
- if(i%2===1||db===0||db===10||db===-80){const [tx,ty]=polar(cx,cy,46.5,a);
- svgEl(s,'text',{x:tx,y:ty+2.2,'text-anchor':'middle','font-size':5.5,'font-family':'var(--mono)',
- fill:'var(--steel)'}).textContent=db>0?'+'+db:db;}});
- wingGrp=svgEl(s,'g',{});wingGrp.style.transition='transform .09s';
- svgEl(wingGrp,'rect',{x:cx-6.5,y:cy-30,width:13,height:60,rx:6,fill:'url(#wingRed)',stroke:'#4e150a','stroke-width':.8});
- svgEl(wingGrp,'circle',{cx,cy,r:15,fill:'url(#wingRed)',stroke:'#4e150a','stroke-width':.8});
- svgEl(wingGrp,'line',{x1:cx,y1:cy-28,x2:cx,y2:cy-18,stroke:'var(--cream)','stroke-width':2,'stroke-linecap':'round'});
- svgEl(wingGrp,'ellipse',{cx:cx-4,cy:cy-8,rx:5,ry:3,fill:'rgba(255,255,255,.18)',transform:`rotate(-30,${cx-4},${cy-8})`});})();
-function setWing(v){const i=Math.max(0,Math.min(WINGK_STEPS.length-1,Math.round(v)));wingI=i;
- wingGrp.style.transform=`rotate(${-135+i/(WINGK_STEPS.length-1)*270}deg)`;
- wingGrp.style.transformOrigin='55px 52px';
- const db=WINGK_STEPS[i];
- setRd('R27',(db>0?'+':'')+db+' dB');}
-dragDelta($('wingk'),()=>wingI,setWing,{min:0,max:WINGK_STEPS.length-1,sens:.05});setWing(7);
-
-/* R28 rotary disc switch: the whole disc turns between heavy positions */
-const DISCSW_POS=[['OFF',-45],['ON',45],['COMBINE',135]];
-let discI=1,discGrp=null,discLbls=[];
-(function(){const s=$('discsw'),cx=55,cy=58;
- const defs=svgEl(s,'defs',{});
- const rr=svgEl(defs,'radialGradient',{id:'discRed',cx:'42%',cy:'34%',r:'85%'});
- svgEl(rr,'stop',{offset:'0','stop-color':'#d98a6f'});
- svgEl(rr,'stop',{offset:'.65','stop-color':'var(--fail)'});
- svgEl(rr,'stop',{offset:'1','stop-color':'#7a2a1a'});
- svgEl(s,'circle',{cx,cy,r:48,fill:'#1a1714',stroke:'#060505','stroke-width':1.5});
- for(let k=0;k<4;k++){const [x,y]=polar(cx,cy,44,45+k*90);
- svgEl(s,'circle',{cx:x,cy:y,r:2.6,fill:'url(#sfScrew)',stroke:'#0a0908','stroke-width':.5});}
- DISCSW_POS.forEach(([lbl,a],i)=>{const [tx,ty]=polar(cx,cy,42,a);
- const t=svgEl(s,'text',{x:tx,y:ty+2.4,'text-anchor':'middle','font-size':5.8,'letter-spacing':'.06em',
- 'font-family':'var(--mono)',fill:'var(--dim)'});t.textContent=lbl;discLbls.push(t);});
- svgEl(s,'circle',{cx,cy,r:36,fill:'url(#discRed)',stroke:'#4e150a','stroke-width':1});
- discGrp=svgEl(s,'g',{});discGrp.style.transition='transform .15s';discGrp.style.transformOrigin=`${cx}px ${cy}px`;
- svgEl(discGrp,'rect',{x:cx-27,y:cy-8,width:54,height:16,rx:8,fill:'url(#discRed)',stroke:'#8f3520','stroke-width':1});
- svgEl(discGrp,'rect',{x:cx-25,y:cy-6.5,width:50,height:4,rx:2,fill:'rgba(255,255,255,.16)'});
- svgEl(discGrp,'line',{x1:cx+20,y1:cy,x2:cx+26,y2:cy,stroke:'var(--cream)','stroke-width':2.5,'stroke-linecap':'round'});
- svgEl(s,'ellipse',{cx:cx-12,cy:cy-14,rx:12,ry:6,fill:'rgba(255,255,255,.10)',transform:`rotate(-28,${cx-12},${cy-14})`});
- $('discsw').onclick=()=>setDisc((discI+1)%DISCSW_POS.length);})();
-function setDisc(i){discI=i;
- discGrp.style.transform=`rotate(${DISCSW_POS[i][1]}deg)`;
- discLbls.forEach((t,k)=>t.setAttribute('fill',k===i?'var(--gold-hi)':'var(--dim)'));
- setRd('R28',DISCSW_POS[i][0]);}
-setDisc(1);
-
-/* R29 guarded toggle: guard posts + red collar mark the critical throw */
-let guardOn=true,guardLever=null,guardLblOn=null,guardLblOff=null;
-(function(){const s=$('guardsw'),cx=45,cy=52;
- guardLblOn=svgEl(s,'text',{x:cx,y:12,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em',
- 'font-family':'var(--mono)',fill:'var(--gold-hi)'});guardLblOn.textContent='ON';
- guardLblOff=svgEl(s,'text',{x:cx,y:96,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em',
- 'font-family':'var(--mono)',fill:'var(--dim)'});guardLblOff.textContent='OFF';
- svgEl(s,'circle',{cx,cy,r:13,fill:'url(#discRed)',stroke:'#4e150a','stroke-width':1});
- const pts=[];for(let k=0;k<6;k++){const a=(k*60+30)*Math.PI/180;pts.push((cx+11*Math.cos(a))+','+(cy+11*Math.sin(a)));}
- svgEl(s,'polygon',{points:pts.join(' '),fill:'url(#nutG)',stroke:'#2b2822'});
- svgEl(s,'circle',{cx,cy,r:6,fill:'#14110e',stroke:'#000'});
- for(const gx of [16,74]){
- svgEl(s,'rect',{x:gx-6,y:cy-24,width:12,height:48,rx:6,fill:'url(#nutG)',stroke:'#2b2822','stroke-width':1});
- svgEl(s,'rect',{x:gx-4,y:cy-21,width:3.5,height:42,rx:2,fill:'rgba(255,255,255,.14)'});}
- guardLever=svgEl(s,'g',{});
- svgEl(guardLever,'path',{d:`M ${cx-3} ${cy} L ${cx-2} 26 L ${cx+2} 26 L ${cx+3} ${cy} Z`,fill:'url(#chromeG)',stroke:'#4e4a42','stroke-width':.5});
- svgEl(guardLever,'rect',{x:cx-4.5,y:16,width:9,height:12,rx:2,fill:'url(#tipG)',stroke:'#5e5a52','stroke-width':.6});
- for(let r=0;r<4;r++)svgEl(guardLever,'line',{x1:cx-3.5,y1:18.5+r*2.4,x2:cx+3.5,y2:18.5+r*2.4,stroke:'rgba(0,0,0,.25)','stroke-width':.8});
- guardLever.style.transformOrigin=`${cx}px ${cy}px`;guardLever.style.transition='transform .12s';
- $('guardsw').onclick=()=>setGuard(!guardOn);})();
-function setGuard(on){guardOn=on;guardLever.style.transform=on?'rotate(0deg)':'rotate(180deg)';
- guardLblOn.setAttribute('fill',on?'var(--gold-hi)':'var(--dim)');
- guardLblOff.setAttribute('fill',on?'var(--dim)':'var(--gold-hi)');
- setRd('R29',on?'ON':'OFF');}
-setGuard(true);
+
+
+/* R13, R17 extracted to widgets.js (GW.*) */
/* R30 telegraph indicator: the pointer names the state on labeled sectors */
const TELIND_STATES=['OK','TEST','BUSY','STOP','ATTN','FAULT'];
@@ -2017,30 +2003,30 @@ const RADAR_CONTACTS=[[60,.55],[205,.7],[318,.4]];
const clip=svgEl(defs,'clipPath',{id:'radarClip'});
svgEl(clip,'circle',{cx,cy,r:46});
svgEl(s,'circle',{cx,cy,r:62,fill:'#1a1714',stroke:'#060505','stroke-width':2});
- svgEl(s,'circle',{cx,cy,r:52,fill:'#120b05',stroke:'#3a2a12','stroke-width':1});
+ svgEl(s,'circle',{cx,cy,r:52,fill:'var(--scr-bg1,#120b05)',stroke:'var(--scr-brd,#3a2a12)','stroke-width':1});
for(let d=0;d<360;d+=30){const a=d;
const [x1,y1]=polar(cx,cy,51,a),[x2,y2]=polar(cx,cy,47,a);
- svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--gold)','stroke-width':1,opacity:.8});
+ svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--scr-ink,var(--gold))','stroke-width':1,opacity:.8});
const g=svgEl(s,'g',{transform:`rotate(${a},${cx},${cy})`});
svgEl(g,'text',{x:cx,y:cy-47.5,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)',
- fill:'var(--gold)',opacity:.9}).textContent=String(d).padStart(3,'0');}
+ fill:'var(--scr-ink,var(--gold))',opacity:.9}).textContent=String(d).padStart(3,'0');}
for(let d=0;d<360;d+=10){if(d%30===0)continue;
const [x1,y1]=polar(cx,cy,51,d),[x2,y2]=polar(cx,cy,49,d);
- svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--gold)','stroke-width':.6,opacity:.5});}
+ svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--scr-ink,var(--gold))','stroke-width':.6,opacity:.5});}
const g=svgEl(s,'g',{'clip-path':'url(#radarClip)'});
- svgEl(g,'circle',{cx,cy,r:46,fill:'#0d0803'});
+ svgEl(g,'circle',{cx,cy,r:46,fill:'var(--scr-bg2,#0d0803)'});
for(const r of [15,30,45])
- svgEl(g,'circle',{cx,cy,r,fill:'none',stroke:'rgba(226,160,56,.18)','stroke-width':.7});
+ svgEl(g,'circle',{cx,cy,r,fill:'none',stroke:'var(--scr-ink,var(--gold))','stroke-opacity':.18,'stroke-width':.7});
radarSweep=svgEl(g,'g',{});
for(let t=0;t<5;t++){const a0=-14+t*2.8,a1=a0+2.8;
const [x1,y1]=polar(cx,cy,46,a0),[x2,y2]=polar(cx,cy,46,a1);
svgEl(radarSweep,'path',{d:`M ${cx} ${cy} L ${x1} ${y1} A 46 46 0 0 1 ${x2} ${y2} Z`,
- fill:`rgba(226,160,56,${(0.04+t*0.045).toFixed(3)})`});}
- svgEl(radarSweep,'line',{x1:cx,y1:cy,x2:cx,y2:cy-46,stroke:'var(--gold-hi)','stroke-width':1.4,
+ fill:'var(--scr-ink,var(--gold))','fill-opacity':(0.04+t*0.045).toFixed(3)});}
+ svgEl(radarSweep,'line',{x1:cx,y1:cy,x2:cx,y2:cy-46,stroke:'var(--scr-hi,var(--gold-hi))','stroke-width':1.4,
transform:`rotate(0,${cx},${cy})`,opacity:.95});
RADAR_CONTACTS.forEach(([bd,rf])=>{const [x,y]=polar(cx,cy,46*rf,bd);
- radarBlips.push(svgEl(g,'circle',{cx:x,cy:y,r:2.2,fill:'var(--gold-hi)',opacity:0}));});
- svgEl(s,'circle',{cx,cy,r:3,fill:'var(--gold)',opacity:.8});
+ radarBlips.push(svgEl(g,'circle',{cx:x,cy:y,r:2.2,fill:'var(--scr-hi,var(--gold-hi))',opacity:0}));});
+ svgEl(s,'circle',{cx,cy,r:3,fill:'var(--scr-ink,var(--gold))',opacity:.8});
radarMark=svgEl(s,'text',{x:cx,y:124,'text-anchor':'middle','font-size':6,'letter-spacing':'.12em',
'font-family':'var(--mono)',fill:'var(--steel)'});
radarMark.textContent='BEARING —';
@@ -2141,86 +2127,38 @@ function setRgraph(fc,gain){rgFc=Math.max(32,Math.min(16000,fc));rgGain=Math.max
setRgraph(1200,6.5);
/* ============ LIVE SIGNAL LOOPS ============ */
-buildBars($('vuL'),16);buildBars($('vuR'),16);$('mini').innerHTML='<i></i><i></i><i></i><i></i>';
-(function(){const eq=$('eq');for(let b=0;b<11;b++){const band=document.createElement('span');band.className='band';
- for(let s=0;s<9;s++)band.appendChild(document.createElement('i'));eq.appendChild(band);}})();
-(function(){const cl=$('clock');for(let i=0;i<12;i++){const t=document.createElement('span');t.className='tk';
- t.style.transform=`rotate(${i*30}deg)`;cl.insertBefore(t,cl.firstChild);}})();
-const counterDigits=[4,7,1,3,0,0];
-(function(){const ct=$('counter');counterDigits.forEach((d,idx)=>{const w=document.createElement('span');
- w.className='cwheel'+(idx>=4?' redw':'');const col=document.createElement('span');col.className='col';
- for(let n=-1;n<=10;n++){const s=document.createElement('span');s.textContent=((n+10)%10);col.appendChild(s);}
- w.appendChild(col);ct.appendChild(w);setWheel(w,d);});})();
-function setWheel(w,d){w.querySelector('.col').style.top=(-(d+1)*34)+'px';}
+/* extracted meters: the page keeps the clock + demo signal, drives card handles */
+const MH={vu:$('card-11').gw,mini:$('card-12').gw,spark:$('card-16').gw,wave:$('card-17').gw,
+ scope:$('card-N11').gw,eq:$('card-N12').gw,strip:$('card-N16').gw,mcvu:$('card-R01').gw};
+const IH={flap:$('card-N20').gw,seven:$('card-N21').gw,vfd:$('card-N22').gw,
+ counter:$('card-N25').gw,clock:$('card-N26').gw};
let ph=0;
-const hist=Array.from({length:40},()=>0.5);
const eqBands=Array.from({length:11},(_,i)=>({v:0.4,ph:i*0.6}));
-const stripHist=Array.from({length:60},()=>31);
-const pkL={v:0},pkR={v:0};
-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;}}
-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 paintEq(){const bands=$('eq').children;let peak=0;
- for(let b=0;b<bands.length;b++){const cells=bands[b].children,n=cells.length,val=eqBands[b].v;const lit=Math.round(val*n);peak=Math.max(peak,val);
- for(let k=0;k<n;k++){let c='';if(k<lit)c=(k>=n-1?'clip':k>=n-3?'hot':'on');cells[k].className=c?c:'';}}return peak;}
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 fastTick(){
ph+=0.09;
const a=lvl(),b=lvl();
- paintVU($('vuL'),a,pkL);paintVU($('vuR'),b,pkR);paintMini($('mini'),a);
- setRd('11','L '+Math.round(a*100)+' · R '+Math.round(b*100));
- setRd('12',Math.round(a*100)+'%');
- 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(' '));
- setRd('16',Math.round(Math.max(0,Math.min(1,hist[hist.length-1]))*100));
- let d='M0 19';const env=Math.min(1,0.6+0.3*Math.sin(ph*0.4));
- 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);setRd('17','amp '+Math.round(env*100)+'%');
- let sp='';for(let x=0;x<=176;x+=3){const y=39+Math.sin(x*0.16+ph*3.2)*Math.min(1,0.6+0.3*Math.sin(ph*0.4))*22;sp+=`${x},${y.toFixed(1)} `;}
- $('scopep').setAttribute('points',sp.trim());setRd('N11','Vpp '+Math.round((0.6+0.3*Math.sin(ph*0.4))*100));
+ MH.vu.set(a,b);MH.mini.set(a);
+ MH.spark.push(0.5+0.42*Math.sin(ph*0.9)+(Math.random()-0.5)*0.25);
+ const env=Math.min(1,0.6+0.3*Math.sin(ph*0.4));
+ const smp=[];for(let x=0;x<=170;x+=3)smp.push(Math.sin(x*0.18+ph*3)*Math.sin(x*0.05));
+ MH.wave.set(smp,env);
+ const senv=Math.min(1,0.6+0.3*Math.sin(ph*0.4));
+ const ssmp=[];for(let x=0;x<=176;x+=3)ssmp.push(Math.sin(x*0.16+ph*3.2)*senv);
+ MH.scope.set(ssmp,0.6+0.3*Math.sin(ph*0.4));
eqBands.forEach(bd=>{bd.ph+=0.16;bd.v=Math.max(0.05,Math.min(1,0.5+0.42*Math.sin(bd.ph)+(Math.random()<0.2?Math.random()*0.3:0)-Math.random()*0.06));});
- const peak=paintEq();setRd('N12','peak '+Math.round(peak*100)+'%');
- stripHist.push(31-24*Math.sin(ph*0.7)*(0.6+0.4*Math.sin(ph*0.13))+(Math.random()-0.5)*4);stripHist.shift();
- const last=Math.max(3,Math.min(59,stripHist[stripHist.length-1]));
- $('stripp').setAttribute('points',stripHist.map((v,i)=>`${i/(stripHist.length-1)*176},${Math.max(3,Math.min(59,v)).toFixed(1)}`).join(' '));
- $('strippen').style.top=last+'px';setRd('N16',Math.round((59-last)/56*100));
+ MH.eq.set(eqBands.map(bd=>bd.v));
+ MH.strip.push((28+24*Math.sin(ph*0.7)*(0.6+0.4*Math.sin(ph*0.13))-(Math.random()-0.5)*4)/56);
}
-/* clock */
-function tickClock(){const d=new Date();const s=d.getSeconds(),m=d.getMinutes(),h=d.getHours()%12;
- $('clks').style.transform=`rotate(${s*6}deg)`;$('clkm').style.transform=`rotate(${m*6+s*0.1}deg)`;$('clkh').style.transform=`rotate(${h*30+m*0.5}deg)`;
- setRd('N26',String(d.getHours()).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0'));}
-/* split-flap */
-const flapWords=['DNS ','LINK','SYNC','WIFI','SCAN'];let flapI=0;
-function tickFlap(){flapI=(flapI+1)%flapWords.length;const w=flapWords[flapI].padEnd(4,' ');
- $('flap').querySelectorAll('.flapd').forEach((dd,i)=>{const ch=w[i]||' ';const bel=dd.querySelector('b');
- if(bel.textContent!==ch){bel.textContent=ch;if(!reduced){dd.classList.remove('flip');void dd.offsetWidth;dd.classList.add('flip');}}});
- setRd('N20',flapWords[flapI].trim());}
-$('flap').onclick=tickFlap;
-/* seven-segment countdown */
-let sevenSecs=24*60+10;
-function renderSeven(){const mm=String(Math.floor(sevenSecs/60)).padStart(2,'0'),ss=String(sevenSecs%60).padStart(2,'0');
- $('seven').innerHTML=seg7(mm[0])+seg7(mm[1])+'<span class="colon"><i></i><i></i></span>'+seg7(ss[0])+seg7(ss[1]);setRd('N21',mm+':'+ss);}
-function tickSeven(){sevenSecs=(sevenSecs-1+3600)%3600;renderSeven();}
-$('seven').onclick=()=>{sevenSecs=(sevenSecs+60)%3600;renderSeven();};
-renderSeven();
-/* tabular timer */
-let tSecs=24*60+10,tRun=true;
-function renderT(){$('treadout').textContent=String(Math.floor(tSecs/60)).padStart(2,'0')+':'+String(tSecs%60).padStart(2,'0');
- setRd('20',tRun?'running':'paused');}
-function tickT(){if(tRun){tSecs=(tSecs-1+3600)%3600;renderT();}}
-$('treadout').onclick=()=>{tRun=!tRun;renderT();};renderT();
-/* VFD marquee */
-const vfdMsgs=['ARCHSETUP · NET OK · BT 2 · SND 62%','WIFI @Hyatt · 300 Mbps · VPN UP','BATTERY 84% · DISK 61% · TEMP 47C'];let vfdMi=0,vfdX=176;
-function tickVfd(){const t=$('vfdt');vfdX-=1.1;if(vfdX< -t.offsetWidth)vfdX=176;t.style.left=vfdX+'px';}
-$('vfdm').onclick=()=>{vfdMi=(vfdMi+1)%vfdMsgs.length;$('vfdt').textContent=vfdMsgs[vfdMi];vfdX=176;setRd('N22','msg '+(vfdMi+1)+'/'+vfdMsgs.length);};
-setRd('N22','scrolling');
-/* tape counter */
-let ctrVal=471300;
-function tickCounter(){ctrVal+=137;const s=String(ctrVal).padStart(6,'0');
- $('counter').querySelectorAll('.cwheel').forEach((w,i)=>setWheel(w,+s[i]));setRd('N25',s);}
-setRd('N25',String(ctrVal).padStart(6,'0'));
+/* indicator loops: builders own state + clicks; the page keeps the clocks and
+ the time source (tick contract) */
+function tickClock(){const d=new Date();IH.clock.set(d.getHours(),d.getMinutes(),d.getSeconds());}
+function tickFlap(){IH.flap.next();}
+function tickSeven(){IH.seven.tick();}
+function tickT(){$('card-20').gw.tick();}
+function tickVfd(){IH.vfd.tick();}
+function tickCounter(){IH.counter.set(IH.counter.get()+137);}
/* reels spin keyframe */
(function(){const st=document.createElement('style');st.textContent='@keyframes reelspin{to{transform:rotate(360deg)}}';document.head.appendChild(st);})();
@@ -2228,15 +2166,15 @@ document.querySelectorAll('.reel.spin').forEach(r=>{r.style.animation='reelspin
/* static paint for reduced motion */
function paintStatic(){
- paintVU($('vuL'),0.55,pkL);paintVU($('vuR'),0.5,pkR);paintMini($('mini'),0.55);
- setRd('11','L 55 · R 50');setRd('12','55%');
- $('sparkp').setAttribute('points',hist.map((v,i)=>`${i/(hist.length-1)*170},${44-0.5*40-2}`).join(' '));setRd('16','50');
- $('wavep').setAttribute('d','M0 19 L170 19');setRd('17','amp 60%');
- eqBands.forEach((bd,i)=>bd.v=0.3+0.5*Math.abs(Math.sin(i*0.9)));setRd('N12','peak '+Math.round(paintEq()*100)+'%');
- $('scopep').setAttribute('points',Array.from({length:60},(_,i)=>`${i*3},${39+Math.sin(i*0.5)*18}`).join(' '));setRd('N11','Vpp 74');
- $('stripp').setAttribute('points',stripHist.map((v,i)=>`${i/(stripHist.length-1)*176},${31-Math.sin(i*0.3)*16}`).join(' '));setRd('N16','50');
- tickClock();renderSeven();setRd('N20','DNS');setRd('N22','scrolling');setRd('N25',String(ctrVal).padStart(6,'0'));
- setMcVu(.5);
+ MH.vu.set(0.55,0.5);MH.mini.set(0.55);
+ MH.spark.fill(0.5);
+ MH.wave.set([],0.6);
+ eqBands.forEach((bd,i)=>bd.v=0.3+0.5*Math.abs(Math.sin(i*0.9)));
+ MH.eq.set(eqBands.map(bd=>bd.v));
+ MH.scope.set(Array.from({length:60},(_,i)=>Math.sin(i*0.5)*18/22),0.74);
+ MH.strip.set(Array.from({length:60},(_,i)=>0.5+Math.sin(i*0.3)*16/56),0.5);
+ tickClock();IH.flap.set(0);
+ MH.mcvu.set(.5);
}
if(!reduced){
@@ -2253,6 +2191,234 @@ if(!reduced){
paintStatic();
document.querySelectorAll('.reel.spin').forEach(r=>r.style.animationPlayState='paused');
}
+
+/* R35 day-date disc calendar: coaxial printed discs rotate so today reads under the fixed hand */
+const DD_DAYS=['SUN','MON','TUE','WED','THU','FRI','SAT'];
+let ddDate=1,ddDay=0,ddDateG=null,ddDayG=null;
+(function(){const s=$('daydate'),cx=65,cy=65,HAND=135;
+ const defs=svgEl(s,'defs',{});
+ const fc=svgEl(defs,'radialGradient',{id:'ddFace',cx:'46%',cy:'40%',r:'75%'});
+ svgEl(fc,'stop',{offset:'0','stop-color':'#efe9da'});
+ svgEl(fc,'stop',{offset:'1','stop-color':'#cfc8b6'});
+ // movement plate + white calendar disc
+ svgEl(s,'circle',{cx,cy,r:62,fill:'#17140f',stroke:'#060505','stroke-width':2});
+ for(let k=0;k<5;k++){const [x,y]=polar(cx,cy,58,20+k*72);
+ svgEl(s,'circle',{cx:x,cy:y,r:1.8,fill:'#060505'});}
+ svgEl(s,'circle',{cx,cy,r:54,fill:'url(#ddFace)',stroke:'#8f897b','stroke-width':.8});
+ // date ring: 31 radial numerals on its own group
+ ddDateG=svgEl(s,'g',{});
+ for(let d=1;d<=31;d++){const a=(d-1)*(360/31);
+ const g=svgEl(ddDateG,'g',{transform:`rotate(${a},${cx},${cy})`});
+ svgEl(g,'text',{x:cx,y:cy-46,'text-anchor':'middle','font-size':7.5,'font-weight':700,
+ 'font-family':'var(--mono)',fill:'#14110e'}).textContent=String(d);}
+ // inner weekday disc: 7 names twice around, separated by a hairline ring
+ svgEl(s,'circle',{cx,cy,r:38,fill:'none',stroke:'#8f897b','stroke-width':.7});
+ ddDayG=svgEl(s,'g',{});
+ for(let i=0;i<14;i++){const a=i*(360/14);
+ const g=svgEl(ddDayG,'g',{transform:`rotate(${a},${cx},${cy})`});
+ svgEl(g,'text',{x:cx,y:cy-28,'text-anchor':'middle','font-size':6.8,'font-weight':700,
+ 'font-family':'var(--mono)',fill:'#14110e'}).textContent=DD_DAYS[i%7];}
+ // fixed yellow hand from hub toward the read index, and the hub
+ const [hx,hy]=polar(cx,cy,44,HAND);
+ svgEl(s,'line',{x1:cx,y1:cy,x2:hx,y2:hy,stroke:'#e8cf4a','stroke-width':3,'stroke-linecap':'round'});
+ svgEl(s,'line',{x1:cx,y1:cy,x2:hx,y2:hy,stroke:'rgba(0,0,0,.25)','stroke-width':.8});
+ svgEl(s,'circle',{cx,cy,r:6,fill:'#b8b2a4',stroke:'#5c574c','stroke-width':1.2});
+ svgEl(s,'circle',{cx,cy,r:2.4,fill:'#7a7466'});
+ s.style.cursor='pointer';
+ s.addEventListener('click',()=>{setDdcal(ddDate%31+1,(ddDay+1)%7);});})();
+function setDdcal(d,w){ddDate=d;ddDay=w;const HAND=135;
+ ddDateG.setAttribute('transform',`rotate(${(HAND-(d-1)*(360/31)).toFixed(2)},65,65)`);
+ ddDayG.setAttribute('transform',`rotate(${(HAND-w*(360/14)).toFixed(2)},65,65)`);
+ setRd('R35',DD_DAYS[w]+' '+d);}
+(function(){const now=new Date();setDdcal(now.getDate(),now.getDay());})();
+
+/* R36 LED dot matrix: 8x8 paintable bitmap, starts on the reference K */
+(function(){const s=$('dotmx'),N=8,STEP=11,X0=10.5,Y0=10.5;
+ const K8=['10000100','10001000','10010000','11100000','10100000','10010000','10001000','10000100'];
+ svgEl(s,'rect',{x:2,y:2,width:104,height:104,rx:8,fill:'#17140f',stroke:'#060505','stroke-width':1.5});
+ svgEl(s,'rect',{x:6,y:6,width:96,height:96,rx:5,fill:'#0d0505',stroke:'#241010','stroke-width':1});
+ const dots=[];let lit=0;
+ const paint=(d,on)=>{d.on=on;
+ d.el.setAttribute('fill',on?'#ff4a30':'#2a1210');
+ d.el.setAttribute('filter',on?'drop-shadow(0 0 3.5px rgba(255,74,48,.85))':'none');};
+ for(let r=0;r<N;r++)for(let c=0;c<N;c++){
+ const el=svgEl(s,'circle',{cx:X0+c*STEP+STEP/2,cy:Y0+r*STEP+STEP/2,r:4.1,fill:'#2a1210'});
+ el.style.cursor='pointer';
+ const d={el,on:false};dots.push(d);
+ paint(d,K8[r][c]==='1');if(d.on)lit++;
+ el.addEventListener('click',()=>{paint(d,!d.on);lit+=d.on?1:-1;
+ setRd('R36',lit+' / 64 lit');});}
+ setRd('R36',lit+' / 64 lit');})();
+
+/* R43, R44 extracted to widgets.js (GW.*) */
+
+/* R45 flip-disc: bistable mechanical pixels; scaleX flip with color swap at the midpoint */
+(function(){const s=$('flipdisc'),COLS=7,ROWS=5,STEP=14,X0=13,Y0=13;
+ const GLYPH=['0000100','0000010','1111111','0000010','0000100']; // arrow right
+ svgEl(s,'rect',{x:2,y:2,width:116,height:88,rx:7,fill:'#17140f',stroke:'#060505','stroke-width':1.5});
+ let lit=0;
+ const discs=[];
+ for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++){
+ const el=svgEl(s,'circle',{cx:X0+c*STEP+STEP/2-2,cy:Y0+r*STEP+STEP/2-2,r:5.6,stroke:'#060505','stroke-width':.8});
+ el.style.transformBox='fill-box';el.style.transformOrigin='center';el.style.transition='transform .11s ease-in';
+ el.style.cursor='pointer';
+ const d={el,on:GLYPH[r][c]==='1'};
+ el.setAttribute('fill',d.on?'#e3d44f':'#141210');if(d.on)lit++;
+ el.addEventListener('click',()=>{d.on=!d.on;lit+=d.on?1:-1;
+ if(matchMedia('(prefers-reduced-motion: reduce)').matches){el.setAttribute('fill',d.on?'#e3d44f':'#141210');}
+ else{el.style.transform='scaleX(0)';
+ setTimeout(()=>{el.setAttribute('fill',d.on?'#e3d44f':'#141210');el.style.transform='scaleX(1)';},115);}
+ setRd('R45',lit+' discs showing');});
+ discs.push(d);}
+ setRd('R45',lit+' discs showing');})();
+
+/* R46 dekatron: click pulses the neon glow one cathode around; wrap = carry */
+let dkPos=0,dkCarry=0;
+(function(){const s=$('dekatron'),cx=60,cy=60;
+ svgEl(s,'circle',{cx,cy,r:52,fill:'#171310',stroke:'#060505','stroke-width':2});
+ svgEl(s,'circle',{cx,cy,r:46,fill:'#0d0a08',stroke:'#2a221c','stroke-width':1});
+ svgEl(s,'ellipse',{cx:cx-14,cy:cy-18,rx:20,ry:11,fill:'rgba(255,255,255,.05)',transform:`rotate(-24,${cx-14},${cy-18})`});
+ const dots=[];
+ for(let i=0;i<10;i++){const [x,y]=polar(cx,cy,34,i*36);
+ svgEl(s,'text',{x:polar(cx,cy,43,i*36)[0],y:polar(cx,cy,43,i*36)[1]+2,'text-anchor':'middle','font-size':5.2,
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=String(i);
+ dots.push(svgEl(s,'circle',{cx:x,cy:y,r:3.4,fill:'#3a2a20'}));}
+ const carry=svgEl(s,'circle',{cx,cy:cy-6,r:3,fill:'#2a1c14'});
+ svgEl(s,'text',{x:cx,y:cy+8,'text-anchor':'middle','font-size':4.6,'letter-spacing':'.12em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='CARRY';
+ const draw=()=>{dots.forEach((d,i)=>{const on=i===dkPos;
+ d.setAttribute('fill',on?'#ff9a4c':'#3a2a20');
+ d.setAttribute('filter',on?'drop-shadow(0 0 5px rgba(255,154,76,.9))':'none');});
+ setRd('R46','COUNT '+dkPos+(dkCarry?' · CARRY x'+dkCarry:''));};
+ s.style.cursor='pointer';
+ s.addEventListener('click',()=>{dkPos=(dkPos+1)%10;
+ if(dkPos===0){dkCarry++;carry.setAttribute('fill','#ff9a4c');
+ carry.setAttribute('filter','drop-shadow(0 0 4px rgba(255,154,76,.9))');
+ setTimeout(()=>{carry.setAttribute('fill','#2a1c14');carry.removeAttribute('filter');},220);}
+ draw();});
+ draw();})();
+
+/* R47 landing gear: three greens or nothing; lever cycles down -> transit -> up */
+(function(){const s=$('gearind');
+ svgEl(s,'rect',{x:2,y:2,width:136,height:106,rx:9,fill:'#1c1916',stroke:'#060505','stroke-width':1.5});
+ const POS=[['NOSE',48,28],['L',24,74],['R',72,74]];
+ const lamps={};
+ POS.forEach(([n,x,y])=>{
+ svgEl(s,'circle',{cx:x,cy:y,r:9,fill:'#0d0b09',stroke:'#2c2820','stroke-width':1.5});
+ lamps[n]=svgEl(s,'circle',{cx:x,cy:y,r:6.2,fill:'#16240f'});
+ svgEl(s,'text',{x,y:y+18,'text-anchor':'middle','font-size':5.2,'letter-spacing':'.08em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=n;});
+ // gear lever: slot + wheel-shaped knob
+ svgEl(s,'rect',{x:106,y:22,width:8,height:66,rx:4,fill:'#0d0b09',stroke:'#2c2820','stroke-width':1});
+ const lever=svgEl(s,'g',{});
+ svgEl(lever,'rect',{x:107.5,y:36,width:5,height:26,fill:'#8f8a7e'});
+ svgEl(lever,'circle',{cx:110,cy:32,r:8,fill:'#26221c',stroke:'#4a463e','stroke-width':2.4});
+ svgEl(lever,'circle',{cx:110,cy:32,r:3,fill:'#4a463e'});
+ svgEl(s,'text',{x:110,y:100,'text-anchor':'middle','font-size':5.2,'letter-spacing':'.08em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='GEAR';
+ let state='down',busy=false;
+ const paint=()=>{const c=state==='down'?'var(--pass)':state==='transit'?'var(--amber-warn)':'#16240f';
+ for(const n of Object.keys(lamps)){lamps[n].setAttribute('fill',c);
+ lamps[n].setAttribute('filter',state==='down'?'drop-shadow(0 0 4px rgba(116,147,47,.8))':
+ state==='transit'?'drop-shadow(0 0 4px rgba(var(--glow-lo),.7))':'none');
+ lamps[n].style.animation=state==='transit'?'pulse var(--pulse-rate) ease-in-out infinite':'none';}
+ lever.setAttribute('transform',state==='up'?'translate(0,-8)':state==='transit'?'translate(0,-4)':'');
+ setRd('R47',state==='down'?'GEAR DOWN · 3 GREEN':state==='up'?'GEAR UP':'IN TRANSIT');};
+ s.style.cursor='pointer';
+ s.addEventListener('click',()=>{if(busy)return;
+ const target=state==='down'?'up':'down';
+ if(matchMedia('(prefers-reduced-motion: reduce)').matches){state=target;paint();return;}
+ busy=true;state='transit';paint();
+ setTimeout(()=>{state=target;busy=false;paint();},1500);});
+ paint();})();
+
+/* R52 blinkenlights: live register lamps driven by a pseudo-PC that folds in the switch register */
+(function(){const s=$('blinken');
+ svgEl(s,'rect',{x:2,y:2,width:166,height:96,rx:8,fill:'#3a1420',stroke:'#060505','stroke-width':1.5});
+ svgEl(s,'rect',{x:8,y:8,width:154,height:62,rx:5,fill:'#14090e'});
+ const mkRow=(y,lbl)=>{const row=[];
+ svgEl(s,'text',{x:12,y:y+2.6,'font-size':4.6,'letter-spacing':'.08em','font-family':'var(--mono)',
+ fill:'#b08a96'}).textContent=lbl;
+ for(let i=0;i<12;i++)row.push(svgEl(s,'circle',{cx:46+i*10,cy:y,r:3,fill:'#2a1210'}));
+ return row;};
+ const addr=mkRow(24,'ADDR'),data=mkRow(46,'DATA');
+ const paint=(row,val)=>row.forEach((d,i)=>{const on=(val>>(11-i))&1;
+ d.setAttribute('fill',on?'#ffb43a':'#2a1210');
+ d.setAttribute('filter',on?'drop-shadow(0 0 3px rgba(255,180,58,.8))':'none');});
+ // switch register: 8 mini toggles
+ let sr=0b10100101;
+ const toggles=[];
+ for(let i=0;i<8;i++){const x=46+i*14;
+ svgEl(s,'rect',{x:x-3,y:76,width:6,height:16,rx:3,fill:'#0d0b09',stroke:'#4a2432','stroke-width':1});
+ const t=svgEl(s,'circle',{cx:x,cy:80,r:4,fill:'#8f8a7e',stroke:'#3c382f','stroke-width':1});
+ t.style.cursor='pointer';
+ const hit=svgEl(s,'rect',{x:x-7,y:72,width:14,height:24,fill:'transparent'});
+ hit.style.cursor='pointer';
+ const flip=()=>{sr^=(1<<(7-i));draw();};
+ hit.addEventListener('click',flip);
+ toggles.push(t);}
+ svgEl(s,'text',{x:12,y:83,'font-size':4.6,'letter-spacing':'.08em','font-family':'var(--mono)',
+ fill:'#b08a96'}).textContent='SR';
+ let pc=0o1234;
+ const draw=()=>{toggles.forEach((t,i)=>t.setAttribute('cy',((sr>>(7-i))&1)?76:84));
+ setRd('R52','SR '+sr.toString(8).padStart(3,'0')+' (octal)');};
+ const tick=()=>{pc=((pc*5+sr+1)&0xFFF);paint(addr,pc);paint(data,(pc^(pc<<3)^sr)&0xFFF);};
+ draw();tick();
+ if(!matchMedia('(prefers-reduced-motion: reduce)').matches)setInterval(tick,250);})();
+
+/* R53-R56 extracted to widgets.js (GW.*) */
+
+/* ---- screen-family chips: first round, the clearly-screen widgets ----
+ Defaults match each widget's shipped color (var fallbacks), so no family is applied until a chip is clicked.
+ The nixie stays chipless on purpose: neon is only ever orange, so alternates would be historically false. */
+screenChips('R10',$('dmx'),['amber','green','red','blue','vfd'],'amber');
+screenChips('R17',$('card-R17').gw.el,['green','amber','red','blue','vfd'],'green');
+screenChips('R19',$('card-R19').gw.el,['white','green','amber','blue','vfd'],'white');
+screenChips('R31',$('radar'),['amber','green','red','blue','vfd'],'amber');
+screenChips('N11',$('card-N11').gw.el,['green','amber','red','blue','vfd'],'green');
+BOOST.forEach(no=>{const rd=document.getElementById('rd-'+no);
+ if(rd)rd.closest('.card').querySelector('.stagew').classList.add('boost');});
+
+/* cross-reference links: card IDs mentioned in notes and spec sheets jump to that card */
+(function(){const ids=new Set([...document.querySelectorAll('.card .no')].map(n=>n.textContent));
+ document.querySelectorAll('.wnote,.igrid .iv').forEach(el=>{
+ if(el.querySelector('a[href^="reference/"]'))return; // leave the photo-link rows alone
+ el.innerHTML=el.innerHTML.replace(/\b(R\d{2}|N\d{2})\b/g,
+ m=>ids.has(m)?`<a class="xref" href="#card-${m}">${m}</a>`:m);});})();
+
+/* slide-toggle color choices: track color per state, thumb never changes */
+(function(){const sw=document.querySelector('.switch');if(!sw)return;
+ const cardEl=sw.closest('.card');
+ const ON_OPTS={amber:{dot:'var(--gold-hi)',vars:{'--sw-on-bg':'linear-gradient(180deg,var(--amber-grad-top),var(--gold))','--sw-on-brd':'var(--gold-hi)','--sw-on-ink':'var(--panel)'}},
+ green:{dot:'#8fb944',vars:{'--sw-on-bg':'linear-gradient(180deg,#a9c95f,var(--pass))','--sw-on-brd':'#a9c95f','--sw-on-ink':'var(--panel)'}},
+ dark:{dot:'#242019',vars:{'--sw-on-bg':'linear-gradient(180deg,#15130f,#242019)','--sw-on-brd':'var(--slate)','--sw-on-ink':'var(--cream)'}}};
+ const OFF_OPTS={dark:{dot:'#242019',vars:{'--sw-off-bg':'linear-gradient(180deg,#15130f,#242019)','--sw-off-brd':'var(--slate)'}},
+ red:{dot:'var(--fail)',vars:{'--sw-off-bg':'linear-gradient(180deg,#d98a6f,var(--fail))','--sw-off-brd':'var(--fail)'}}};
+ const OFFTXT_OPTS={white:{dot:'var(--cream)',vars:{'--sw-off-ink':'var(--cream)'}},
+ red:{dot:'#e0523a',vars:{'--sw-off-ink':'#e0523a'}},
+ black:{dot:'#14110e',vars:{'--sw-off-ink':'#14110e'}}};
+ const THUMB_OPTS={light:{dot:'#d8d2c4',vars:{'--sw-thumb':'radial-gradient(circle at 35% 28%,#f6f2e8,#b0aa9a)'}},
+ dark:{dot:'#2c2824',vars:{'--sw-thumb':'radial-gradient(circle at 35% 28%,#4a453e,#16130f)'}},
+ chrome:{dot:'#9aa2b4',vars:{'--sw-thumb':'radial-gradient(circle at 35% 28%,#dfe6f2,#6d7484)'}},
+ brass:{dot:'#b8944a',vars:{'--sw-thumb':'radial-gradient(circle at 35% 28%,#e2c47a,#8a6a24)'}}};
+ const row=document.createElement('div'); row.className='famchips';
+ const group=(label,opts,def)=>{
+ const g=document.createElement('span'); g.className='fgroup';
+ const lab=document.createElement('span'); lab.className='lab'; lab.textContent=label; g.appendChild(lab);
+ const chips=[];
+ for(const [name,o] of Object.entries(opts)){
+ const b=document.createElement('span'); b.className='fc'+(name===def?' on':'');
+ b.style.background=o.dot; b.title=name; chips.push(b);
+ b.addEventListener('click',()=>{
+ for(const [k,v] of Object.entries(o.vars)) sw.style.setProperty(k,v);
+ chips.forEach(x=>x.classList.toggle('on',x===b));});
+ g.appendChild(b);}
+ row.appendChild(g);};
+ group('on',ON_OPTS,'amber');
+ group('off',OFF_OPTS,'dark');
+ group('off text',OFFTXT_OPTS,'white');
+ group('thumb',THUMB_OPTS,'light');
+ cardEl.querySelector('.opts').appendChild(row);})();
</script>
</body>
</html>