aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/panel-widget-gallery.html
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-12 19:06:39 -0500
committerCraig Jennings <c@cjennings.net>2026-07-12 19:06:39 -0500
commitbc93388ddd5309476eac0a4547d6b66ab9fbb172 (patch)
tree786398c936d2cfad6d0108bc7f597d1736ef373c /docs/prototypes/panel-widget-gallery.html
parent4fd6e5a2da8b119189cac3b45c71f5da8507c6c5 (diff)
downloadarchsetup-bc93388ddd5309476eac0a4547d6b66ab9fbb172.tar.gz
archsetup-bc93388ddd5309476eac0a4547d6b66ab9fbb172.zip
feat(gallery): gallery upgrades — size/palette/screen-chips/spec-sheets, 25 new cards, grammar note
Squash of the gallery-upgrades branch (25 commits). Usability: widget stages scale S/M/L (2.4x default), reading text at 12-13pt, a live palette section parsed from the generated :root block, screen-color families with per-card chips on the five screen widgets, and a collapsible spec sheet on every card (input model, solves, use, limits, origin, difficulty, prefer-when, period on the non-timeless). Collection grew 84 → 109 cards, all CDP-verified: reference cards R32-R36 (mechanical timer, four-way rocker, four-way toggle, day-date discs, LED dot matrix), the takuzu-survey build R37-R47 plus the N23 alarm lifecycle (pin matrix, dead-man, rotary dial, breaker panel, DSKY, cam timer, attitude indicator, heading bug, flip-disc, dekatron, landing gear), the promoted banked six R48-R53 (knife switch, decade box, two-hand control, voice-loop keyset, blinkenlights, circular chart), and the Ki-57/comfort-meter trio R54-R56 (vertical tape, twin-needle, comfort-zone crossed needles). Also: docs/design control-grammars reference note (taxonomy sources and proposed axes for the classification work), nine reference photos filed, and a component cleanup pass (dial digits above the finger wheel, legible demo defaults, unified captions).
Diffstat (limited to 'docs/prototypes/panel-widget-gallery.html')
-rw-r--r--docs/prototypes/panel-widget-gallery.html1979
1 files changed, 1928 insertions, 51 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index f8330bb..d67034b 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,24 @@ 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}
+/* 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}
+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:.74rem;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
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 +78,28 @@ 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}
.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;
+.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}
-.wrd::before{content:"▸";color:var(--steel);font-size:.6rem}
-.wnote{color:var(--dim);font-size:.72rem;line-height:1.4}
+.wrd::before{content:"▸";color:var(--steel);font-size:.7rem}
+.wnote{color:var(--dim);font-size:.85rem;line-height:1.45}
.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}
+
/* ---- 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)}
@@ -347,16 +377,37 @@ 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,1fr);gap:4px}
+.vk{position:relative;font-size:7px;letter-spacing:.05em;text-align:center;padding:7px 4px 9px;cursor:pointer;
+ color:var(--dim);background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-radius:4px;
+ font-family:var(--mono)}
+.vk .bar{position:absolute;left:4px;right:4px;bottom:3px;height:3px;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)}
+
+/* screen-family chips (alt phosphor / tube colors per screen widget) */
+.famchips{display:flex;gap:7px;align-items:center;margin-top:1px}
+.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 +491,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 +517,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,7 +594,7 @@ 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="3">
<div class="wrap">
<header class="masthead">
<div class="eyebrow">archsetup · dupre panel family · live kit</div>
@@ -530,8 +606,19 @@ 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" data-sz="2">M</button>
+ <button class="key on" data-sz="3">L</button>
+ </div>
</header>
+<h2>Palette — design tokens</h2>
+<p class="secnote">Read live from the generated :root block (tokens.json via gen_tokens.py), so this section always shows
+ exactly what the widgets render with. Hex tokens get a swatch; the glow entries are rgb triples used inside
+ rgba() glows; mono and pulse-rate are the non-color tokens.</p>
+<div class="palgrid" id="palette"></div>
+
<h2>Controls — take input</h2>
<div class="grid" id="controls"></div>
@@ -545,10 +632,812 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
<script>
const $ = id => document.getElementById(id);
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
+document.querySelectorAll('.szbar .key').forEach(b=>b.addEventListener('click',()=>{
+ document.body.dataset.size=b.dataset.sz;
+ document.querySelectorAll('.szbar .key').forEach(k=>k.classList.toggle('on',k===b));
+}));
+
+/* ---- palette section: read the generated :root block live ---- */
+(function buildPalette(){
+ const host=$('palette'); if(!host)return;
+ let rootRule=null;
+ for(const sh of document.styleSheets){
+ for(const r of sh.cssRules){ if(r.selectorText===':root'){rootRule=r;break;} }
+ if(rootRule)break;
+ }
+ if(!rootRule)return;
+ const st=rootRule.style;
+ for(let i=0;i<st.length;i++){
+ const name=st[i]; if(!name.startsWith('--'))continue;
+ const val=st.getPropertyValue(name).trim();
+ const isHex=/^#[0-9a-f]{3,8}$/i.test(val);
+ const isTriple=/^\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}$/.test(val);
+ const esc=val.replace(/&/g,'&amp;').replace(/</g,'&lt;');
+ const sw=isHex?`<div class="sw" style="background:${val}"></div>`
+ :isTriple?`<div class="sw" style="background:rgb(${val})"></div>`
+ :`<div class="sw txt">${esc}</div>`;
+ const tile=document.createElement('div'); tile.className='swatch';
+ tile.innerHTML=`${sw}<div class="swn">${name}</div><div class="swv">${isTriple?esc+' (glow rgb)':esc}</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. */
+const SCREEN_FAMS={
+ green:{'--scr-hi':'#7fe0a0','--scr-ink':'#58b87e','--scr-dim':'#3d5c46',
+ '--scr-bg1':'#0a120c','--scr-bg2':'#050c07','--scr-bgc':'#04140a','--scr-bge':'#020a05','--scr-brd':'#123018',
+ '--scr-grat':'rgba(111,206,51,.18)','--scr-gratc':'rgba(111,206,51,.35)','--scr-glow':'rgba(127,224,160,.8)',
+ '--crt-face1':'#b9d8c0','--crt-face2':'#8bb296','--crt-glow':'#eef7ee'},
+ amber:{'--scr-hi':'#ffbe54','--scr-ink':'#e2a038','--scr-dim':'#7d5c16',
+ '--scr-bg1':'#140d06','--scr-bg2':'#0a0705','--scr-bgc':'#140d06','--scr-bge':'#0a0603','--scr-brd':'#33230e',
+ '--scr-grat':'rgba(226,160,56,.18)','--scr-gratc':'rgba(226,160,56,.35)','--scr-glow':'rgba(255,190,84,.8)',
+ '--crt-face1':'#d8cba6','--crt-face2':'#b3a276','--crt-glow':'#ffe7c0'},
+ red:{'--scr-hi':'#ff9a4c','--scr-ink':'#e0742e','--scr-dim':'#5c3416',
+ '--scr-bg1':'#140a05','--scr-bg2':'#0c0603','--scr-bgc':'#140a05','--scr-bge':'#0a0502','--scr-brd':'#331c0c',
+ '--scr-grat':'rgba(224,116,46,.18)','--scr-gratc':'rgba(224,116,46,.35)','--scr-glow':'rgba(255,154,76,.8)',
+ '--crt-face1':'#d8b9a6','--crt-face2':'#b39276','--crt-glow':'#ffdcc0'},
+ blue:{'--scr-hi':'#cfe4ff','--scr-ink':'#94b8d8','--scr-dim':'#3a4c60',
+ '--scr-bg1':'#0a0e14','--scr-bg2':'#05080c','--scr-bgc':'#0a1018','--scr-bge':'#04070c','--scr-brd':'#16283a',
+ '--scr-grat':'rgba(148,184,216,.18)','--scr-gratc':'rgba(148,184,216,.35)','--scr-glow':'rgba(207,228,255,.8)',
+ '--crt-face1':'#c2d2dd','--crt-face2':'#93a8b6','--crt-glow':'#eaf2ff'},
+ vfd:{'--scr-hi':'#63e6c8','--scr-ink':'#46b89e','--scr-dim':'#1e4a40',
+ '--scr-bg1':'#06100d','--scr-bg2':'#040b09','--scr-bgc':'#041410','--scr-bge':'#020a08','--scr-brd':'#0e3028',
+ '--scr-grat':'rgba(70,184,158,.18)','--scr-gratc':'rgba(70,184,158,.35)','--scr-glow':'rgba(99,230,200,.8)',
+ '--crt-face1':'#b0d8cd','--crt-face2':'#84ada2','--crt-glow':'#dcfff5'},
+ white:{'--scr-hi':'#f2f4f2','--scr-ink':'#c8cac8','--scr-dim':'#5a5c5a',
+ '--scr-bg1':'#0b0c0b','--scr-bg2':'#070807','--scr-bgc':'#0d0e0d','--scr-bge':'#060706','--scr-brd':'#2a2c2a',
+ '--scr-grat':'rgba(200,202,200,.18)','--scr-gratc':'rgba(200,202,200,.35)','--scr-glow':'rgba(242,244,242,.7)',
+ '--crt-face1':'#d0d2d0','--crt-face2':'#a8aaa8','--crt-glow':'#f7f7f7'},
+};
+function screenChips(no,el,fams,def){
+ const cardEl=el.closest('.card'); if(!cardEl)return;
+ const row=document.createElement('div'); row.className='famchips';
+ const lab=document.createElement('span'); lab.className='lab'; lab.textContent='screen'; row.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);
+ row.querySelectorAll('.fc').forEach(x=>x.classList.toggle('on',x===b));
+ setRd(no,'screen '+f);});
+ row.appendChild(b);});
+ cardEl.insertBefore(row,cardEl.querySelector('.wrd'));
+}
+/* ---- 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 everywhere. Shines as a master control: power, mute, enable.',
+ limits:'Two states only. Ambiguous if the on-direction is not marked.',
+ origin:'Electrical panels generally.',difficulty:'Intuitive.',
+ prefer:'The state persists and gets glanced at more than changed.'},
+'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']];
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>`;
+ 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>`;
+ d.innerHTML=`<summary>spec sheet</summary><div class="igrid">${rows}</div>`;
+ c.appendChild(d);}
host.appendChild(c); return c;
}
function setRd(no,txt){const e=$('rd-'+no);if(e)e.textContent=txt;}
@@ -745,6 +1634,45 @@ card(C,'R28','Rotary disc switch',
card(C,'R29','Guarded toggle',
`<svg id="guardsw" class="rsvg press" viewBox="0 0 90 100" width="90" height="100"></svg>`,
'<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',
+ `<svg id="mtimer" class="rsvg" viewBox="0 0 158 132" width="158" height="132"></svg>`,
+ '<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',
+ `<svg id="rocker4" class="rsvg press" viewBox="0 0 110 110" width="110" height="110"></svg>`,
+ '<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',
+ `<svg id="fourway" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ '<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',
+ `<svg id="pinmx" class="rsvg press" viewBox="0 0 160 110" width="160" height="110"></svg>`,
+ '<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',
+ `<svg id="deadman" class="rsvg" viewBox="0 0 120 110" width="120" height="110"></svg>`,
+ '<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',
+ `<svg id="teldial" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ '<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',
+ `<svg id="brkpanel" class="rsvg press" viewBox="0 0 160 110" width="160" height="110"></svg>`,
+ '<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',
+ `<div class="dsky" id="dsky"></div>`,
+ '<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',
+ `<svg id="camtimer" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ '<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',
+ `<svg id="knifesw" class="rsvg press" viewBox="0 0 130 110" width="130" height="110"></svg>`,
+ '<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',
+ `<svg id="decbox" class="rsvg" viewBox="0 0 160 100" width="160" height="100"></svg>`,
+ '<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',
+ `<svg id="twohand" class="rsvg press" viewBox="0 0 160 100" width="160" height="100"></svg>`,
+ '<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',
+ `<div class="vloop" id="vloop"></div>`,
+ '<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');
@@ -820,6 +1748,24 @@ card(M,'R09','Black-face aviation gauge',
card(M,'R13','Edgewise strip meter',
`<svg id="edgem" class="rsvg drag" viewBox="0 0 70 130" width="70" height="130"></svg>`,
'<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',
+ `<svg id="attind" class="rsvg" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ '<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',
+ `<svg id="hdgbug" class="rsvg" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ '<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',
+ `<svg id="vtape" class="rsvg" viewBox="0 0 70 130" width="70" height="130"></svg>`,
+ '<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',
+ `<svg id="twinng" class="rsvg" viewBox="0 0 120 110" width="120" height="110"></svg>`,
+ '<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',
+ `<svg id="comfort" class="rsvg" viewBox="0 0 130 122" width="130" height="122"></svg>`,
+ '<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',
+ `<svg id="circhart" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ '<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>`,
'<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.');
@@ -858,11 +1804,13 @@ 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>`,
'<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">
+ `<div class="annwrap"><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.');
+ </div><div class="annbar" id="annbar"><span class="mc" id="anmc">MSTR CAUTION</span>
+ <button class="key" data-k="ACK">ACK</button><button class="key" data-k="TEST">TEST</button>
+ <button class="key" data-k="RESET">RESET</button></div></div>`,
+ '<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>`,
@@ -897,6 +1845,24 @@ 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 */
@@ -1074,11 +2040,27 @@ 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 */
+/* N23 annunciator with alarm lifecycle: raise -> MSTR CAUTION flashes -> ACK steadies -> RESET clears */
(function(){const st=['acell','acell warn','acell fault'];const nm=['ok','warn','fault'];
+ const mc=$('anmc');let acked=false;
+ const active=()=>$('annun').querySelectorAll('.warn,.fault').length;
+ const refresh=()=>{const n=active();
+ mc.classList.toggle('on',n>0);
+ mc.classList.toggle('fl',n>0&&!acked);
+ setRd('N23',n===0?'clear':n+' active · '+(acked?'ACK':'UNACK'));};
$('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');
+ let cur=cell.className.trim();let i=st.indexOf(cur);if(i<0)i=0;i=(i+1)%st.length;cell.className=st[i];
+ if(i>0)acked=false; // a new alarm re-arms the flasher
+ refresh();setRd('N23',label+' → '+nm[i]);};});
+ $('annbar').querySelectorAll('.key').forEach(k=>k.addEventListener('click',()=>{
+ const a=k.dataset.k;
+ if(a==='ACK'){if(active()>0)acked=true;refresh();}
+ else if(a==='RESET'){$('annun').querySelectorAll('.acell').forEach(c=>c.className='acell');acked=false;refresh();}
+ else if(a==='TEST'){const prev=[...$('annun').querySelectorAll('.acell')].map(c=>c.className);
+ $('annun').querySelectorAll('.acell').forEach(c=>c.className='acell fault');
+ mc.classList.add('on','fl');setRd('N23','lamp test');
+ setTimeout(()=>{[...$('annun').querySelectorAll('.acell')].forEach((c,i)=>c.className=prev[i]);refresh();},1100);}}));
+ refresh();})();
/* 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=()=>{
@@ -1421,14 +2403,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];
@@ -1649,8 +2631,8 @@ 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'});
+ svgEl(ph,'stop',{offset:'0','stop-color':'var(--crt-face1,#b9d8c0)'});
+ svgEl(ph,'stop',{offset:'1','stop-color':'var(--crt-face2,#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});
@@ -1659,9 +2641,9 @@ let rcrtPh=0,rcrtTrace=null;
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)'});
+ svgEl(g,'line',{x1:cx+i*9,y1:cy-38,x2:cx+i*9,y2:cy+38,stroke:'var(--scr-dim,#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:'var(--scr-dim,#3d5c46)','stroke-width':i===0?.9:.5,opacity:.8});}
+ svgEl(g,'polyline',{id:'rcrtGlow',points:'',fill:'none',stroke:'var(--crt-glow,#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='';
@@ -1714,27 +2696,27 @@ 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,'rect',{x:1,y:1,width:188,height:98,rx:6,fill:'var(--scr-bg1,#0b0c0b)',stroke:'var(--scr-brd,#2a2c2a)','stroke-width':2});
+ svgEl(s,'text',{x:12,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--scr-hi,#e8eae8)'}).textContent='C1:START';
+ svgEl(s,'text',{x:78,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--scr-hi,#e8eae8)'}).textContent='C2:OFF';
+ svgEl(s,'text',{x:136,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--scr-hi,#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'}));}
+ wrBars.push(svgEl(s,'rect',{x:x-0.8,y:yMid-h,width:1.6,height:h*2,fill:'var(--scr-dim,#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,'line',{x1:0,y1:18,x2:0,y2:78,stroke:'var(--scr-hi,#f2f4f2)','stroke-width':1});
+ svgEl(g,'rect',{x:lbl==='S'?0:-8,y:70,width:8,height:8,fill:'var(--scr-hi,#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;};
+ 'font-family':'var(--mono)',fill:'var(--scr-bg1,#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,'rect',{x:10,y:84,width:78,height:11,fill:'var(--scr-hi,#e8eae8)'});
+ svgEl(s,'text',{x:14,y:92.5,'font-size':6.5,'font-family':'var(--mono)',fill:'var(--scr-bg1,#0b0c0b)'}).textContent='ENC:ZOOM(1x)';
+ svgEl(s,'text',{x:96,y:92.5,'font-size':6.5,'font-family':'var(--mono)',fill:'var(--scr-hi,#e8eae8)'}).textContent='A-3 M:[S]';
+ svgEl(s,'rect',{x:148,y:84,width:32,height:11,fill:'var(--scr-hi,#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';
+ 'font-family':'var(--mono)',fill:'var(--scr-bg1,#0b0c0b)'}).textContent='MENU';
s.style.cursor='ew-resize';
dragX(s,pct=>{
// move whichever flag is nearer the pointer
@@ -1745,7 +2727,7 @@ function setWregion(sv,ev){wrS=Math.max(0,Math.min(96,sv));wrE=Math.max(4,Math.m
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');});
+ b.setAttribute('fill',(p>=wrS&&p<=wrE)?'var(--scr-hi,#f2f4f2)':'var(--scr-dim,#5a5c5a)');});
setRd('R19',`S ${Math.round(wrS)}% · E ${Math.round(wrE)}%`);}
setWregion(22,76);
@@ -2017,30 +2999,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 —';
@@ -2253,6 +3235,901 @@ if(!reduced){
paintStatic();
document.querySelectorAll('.reel.spin').forEach(r=>r.style.animationPlayState='paused');
}
+
+/* R32 mechanical timer dial: dial rotates under a fixed index; wind by drag, stop by the red knob */
+let mtMin=0,mtRot=null;
+(function(){const s=$('mtimer'),cx=62,cy=58;
+ const defs=svgEl(s,'defs',{});
+ const fc=svgEl(defs,'radialGradient',{id:'mtFace',cx:'50%',cy:'42%',r:'75%'});
+ svgEl(fc,'stop',{offset:'0','stop-color':'#282320'});
+ svgEl(fc,'stop',{offset:'1','stop-color':'#14110e'});
+ const rd=svgEl(defs,'radialGradient',{id:'mtRed',cx:'38%',cy:'32%',r:'80%'});
+ svgEl(rd,'stop',{offset:'0','stop-color':'#e0523a'});
+ svgEl(rd,'stop',{offset:'1','stop-color':'#8f2416'});
+ const hb=svgEl(defs,'linearGradient',{id:'mtHub',x1:0,y1:0,x2:0,y2:1});
+ svgEl(hb,'stop',{offset:'0','stop-color':'#c9c4b8'});
+ svgEl(hb,'stop',{offset:'1','stop-color':'#6e685c'});
+ // bezel + knurled coin edge
+ svgEl(s,'circle',{cx,cy,r:50,fill:'#17140f',stroke:'#060505','stroke-width':2});
+ svgEl(s,'circle',{cx,cy,r:47.5,fill:'none',stroke:'#3a352c','stroke-width':4,'stroke-dasharray':'2 1.6'});
+ svgEl(s,'circle',{cx,cy,r:44,fill:'url(#mtFace)',stroke:'#0a0908','stroke-width':1});
+ // rotating scale: OFF at 0, minutes climb clockwise at 4.5 deg/min (60 at 270)
+ mtRot=svgEl(s,'g',{});
+ for(let m=0;m<=60;m+=5){const major=m%20===0;
+ const [x1,y1]=polar(cx,cy,40,m*4.5),[x2,y2]=polar(cx,cy,major?34:37,m*4.5);
+ svgEl(mtRot,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':major?1.2:.55,opacity:major?.9:.55});}
+ for(const m of [20,40,60]){const [x,y]=polar(cx,cy,27,m*4.5);
+ svgEl(mtRot,'text',{x,y:y+3,'text-anchor':'middle','font-size':9,'font-weight':700,
+ 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent=String(m);}
+ const [ox,oy]=polar(cx,cy,27,0);
+ svgEl(mtRot,'text',{x:ox,y:oy+3,'text-anchor':'middle','font-size':7.5,'font-weight':700,
+ 'font-family':'var(--mono)',fill:'#e0523a'}).textContent='OFF';
+ const [mx,my]=polar(cx,cy,16,180);
+ svgEl(mtRot,'text',{x:mx,y:my+2.5,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.14em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='MINUTES';
+ // hub with screw slot
+ svgEl(s,'circle',{cx,cy,r:9,fill:'url(#mtHub)',stroke:'#3c382f','stroke-width':1});
+ svgEl(s,'line',{x1:cx-5.5,y1:cy,x2:cx+5.5,y2:cy,stroke:'#4a4438','stroke-width':2});
+ svgEl(s,'circle',{cx,cy,r:2.2,fill:'#8f897b'});
+ // fixed red index at top + winding-direction arrow
+ svgEl(s,'line',{x1:cx,y1:cy-44,x2:cx,y2:cy-32,stroke:'#e0523a','stroke-width':1.6});
+ svgEl(s,'path',{d:'M 122 14 A 26 26 0 0 1 136 26',fill:'none',stroke:'var(--steel)','stroke-width':1.6});
+ svgEl(s,'polygon',{points:'136,26 130.5,24.5 134.5,19.5',fill:'var(--steel)'});
+ // engraved plate captions
+ const cap=(t,y)=>svgEl(s,'text',{x:4,y,'font-size':6.2,'letter-spacing':'.1em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=t;
+ cap('PUSH TO STOP',118); cap('TURN TO START',128);
+ // dial drag surface (under the knob in DOM so the knob wins the overlap)
+ const hit=svgEl(s,'circle',{cx,cy,r:50,fill:'transparent'});
+ hit.style.cursor='grab';
+ dragDelta(hit,()=>mtMin,v=>setMtimer(v),{min:0,max:60});
+ // red stop knob: fluted edge, domed center
+ const knob=svgEl(s,'g',{});
+ svgEl(knob,'circle',{cx:126,cy:104,r:20,fill:'url(#mtRed)',stroke:'#5c150c','stroke-width':1.5});
+ svgEl(knob,'circle',{cx:126,cy:104,r:18,fill:'none',stroke:'rgba(0,0,0,.4)','stroke-width':3.4,'stroke-dasharray':'3 2.4'});
+ svgEl(knob,'circle',{cx:126,cy:104,r:8.5,fill:'url(#mtRed)',stroke:'rgba(0,0,0,.3)','stroke-width':.8});
+ svgEl(knob,'ellipse',{cx:121,cy:98,rx:6,ry:3.4,fill:'rgba(255,255,255,.18)',transform:'rotate(-28,121,98)'});
+ knob.style.cursor='pointer';
+ knob.addEventListener('click',()=>{setMtimer(0);setRd('R32','STOP · OFF');});})();
+function setMtimer(m){mtMin=Math.max(0,Math.min(60,m));
+ mtRot.setAttribute('transform',`rotate(${(-mtMin*4.5).toFixed(2)},62,58)`);
+ setRd('R32',mtMin>0.5?'T-'+Math.round(mtMin)+' MIN':'OFF');}
+setMtimer(0);
+if(!matchMedia('(prefers-reduced-motion: reduce)').matches)
+ setInterval(()=>{if(mtMin>0){setMtimer(mtMin-1);if(mtMin===0)setRd('R32','DING · OFF');}},1000);
+
+/* R33 four-way rocker: quadrant clicks step a tracked cursor; arrows flash on press */
+(function(){const s=$('rocker4'),cx=55,cy=55;
+ const defs=svgEl(s,'defs',{});
+ const pd=svgEl(defs,'radialGradient',{id:'rk4Pad',cx:'42%',cy:'34%',r:'85%'});
+ svgEl(pd,'stop',{offset:'0','stop-color':'#33302b'});
+ svgEl(pd,'stop',{offset:'1','stop-color':'#141210'});
+ const ar=svgEl(defs,'linearGradient',{id:'rk4Arr',x1:0,y1:0,x2:0,y2:1});
+ svgEl(ar,'stop',{offset:'0','stop-color':'#e9e4d6'});
+ svgEl(ar,'stop',{offset:'1','stop-color':'#9d988b'});
+ const nb=svgEl(defs,'radialGradient',{id:'rk4Nub',cx:'38%',cy:'30%',r:'85%'});
+ svgEl(nb,'stop',{offset:'0','stop-color':'#4a453e'});
+ svgEl(nb,'stop',{offset:'1','stop-color':'#0d0c0a'});
+ // recessed plate + rubber pad
+ svgEl(s,'rect',{x:3,y:3,width:104,height:104,rx:18,fill:'#211e1a',stroke:'#0c0b09','stroke-width':1.5});
+ svgEl(s,'circle',{cx,cy,r:45,fill:'#0f0d0b'});
+ svgEl(s,'circle',{cx,cy,r:41,fill:'url(#rk4Pad)',stroke:'#060505','stroke-width':1});
+ // four arrows, pointing outward
+ const ARR={up:'55,22 45,36 65,36',down:'55,88 45,74 65,74',left:'22,55 36,45 36,65',right:'88,55 74,45 74,65'};
+ const arrows={};
+ for(const [dir,pts] of Object.entries(ARR))
+ arrows[dir]=svgEl(s,'polygon',{points:pts,fill:'url(#rk4Arr)',stroke:'#3c382f','stroke-width':.7});
+ // center pivot nub
+ svgEl(s,'circle',{cx,cy,r:7,fill:'url(#rk4Nub)',stroke:'#060505','stroke-width':.8});
+ svgEl(s,'ellipse',{cx:cx-2,cy:cy-2.5,rx:2.6,ry:1.7,fill:'rgba(255,255,255,.25)'});
+ // quadrant hit zones (drawn last so they win), press flash + tracked position
+ let rx=0,ry=0;
+ const press=(dir,dx,dy)=>{rx+=dx;ry+=dy;
+ setRd('R33',dir.toUpperCase()+' · x '+rx+' y '+ry);
+ arrows[dir].setAttribute('fill','var(--gold-hi)');
+ setTimeout(()=>arrows[dir].setAttribute('fill','url(#rk4Arr)'),160);};
+ const zone=(dir,pts,dx,dy)=>{const z=svgEl(s,'polygon',{points:pts,fill:'transparent'});
+ z.style.cursor='pointer';z.addEventListener('click',()=>press(dir,dx,dy));};
+ zone('up',`${cx},${cy} 20,20 90,20`,0,1);
+ zone('down',`${cx},${cy} 20,90 90,90`,0,-1);
+ zone('left',`${cx},${cy} 20,20 20,90`,-1,0);
+ zone('right',`${cx},${cy} 90,20 90,90`,1,0);
+ setRd('R33','x 0 y 0');})();
+
+/* R34 four-way toggle selector: ball lever throws to a diagonal; corner lamps show the state */
+(function(){const s=$('fourway'),cx=65,cy=65;
+ const defs=svgEl(s,'defs',{});
+ const ch=svgEl(defs,'linearGradient',{id:'fw4Chrome',x1:0,y1:0,x2:1,y2:1});
+ svgEl(ch,'stop',{offset:'0','stop-color':'#e8e6e0'});
+ svgEl(ch,'stop',{offset:'.5','stop-color':'#9a968c'});
+ svgEl(ch,'stop',{offset:'1','stop-color':'#55524a'});
+ const bl=svgEl(defs,'radialGradient',{id:'fw4Ball',cx:'36%',cy:'30%',r:'80%'});
+ svgEl(bl,'stop',{offset:'0','stop-color':'#f2f0ea'});
+ svgEl(bl,'stop',{offset:'.55','stop-color':'#8e8a80'});
+ svgEl(bl,'stop',{offset:'1','stop-color':'#3c3a34'});
+ // printed panel graphics: axes, circle, diagonal square, labels
+ svgEl(s,'line',{x1:8,y1:cy,x2:122,y2:cy,stroke:'var(--steel)','stroke-width':.7,opacity:.7});
+ svgEl(s,'line',{x1:cx,y1:8,x2:cx,y2:122,stroke:'var(--steel)','stroke-width':.7,opacity:.7});
+ svgEl(s,'text',{x:cx+2,y:13,'font-size':7,'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='Y';
+ svgEl(s,'text',{x:116,y:cy-4,'font-size':7,'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='X';
+ svgEl(s,'circle',{cx,cy,r:42,fill:'none',stroke:'var(--steel)','stroke-width':.9,opacity:.8});
+ const QUAD={A:315,B:45,C:135,D:225};
+ const diag=Object.values(QUAD).map(a=>polar(cx,cy,42,a));
+ svgEl(s,'polygon',{points:diag.map(p=>p.join(',')).join(' '),fill:'none',stroke:'var(--steel)','stroke-width':.8,opacity:.8});
+ const LAMP_AT={A:[14,14],B:[116,14],C:[116,116],D:[14,116]};
+ const lamps={};
+ for(const [q,a] of Object.entries(QUAD)){
+ const [lx,ly]=polar(cx,cy,42,a);
+ const [px,py]=LAMP_AT[q];
+ svgEl(s,'line',{x1:lx,y1:ly,x2:px,y2:py,stroke:'var(--steel)','stroke-width':.7,opacity:.5});
+ svgEl(s,'circle',{cx:lx,cy:ly,r:6.5,fill:'#14110e',stroke:'var(--steel)','stroke-width':.9});
+ svgEl(s,'text',{x:lx,y:ly+2.8,'text-anchor':'middle','font-size':7.5,'font-weight':700,
+ 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent=q;
+ svgEl(s,'circle',{cx:px,cy:py,r:5.5,fill:'#0d0b09',stroke:'#2c2820','stroke-width':1});
+ lamps[q]=svgEl(s,'circle',{cx:px,cy:py,r:3.6,fill:'#3a3426'});}
+ // bezel + boot
+ svgEl(s,'circle',{cx,cy,r:20,fill:'#0d0c0a',stroke:'#2a2722','stroke-width':1.5});
+ svgEl(s,'circle',{cx,cy,r:15,fill:'#17140f',stroke:'#060505','stroke-width':1});
+ // lever: shaft + grip rotate to the selected diagonal; ball pivot stays centered
+ const lever=svgEl(s,'g',{});
+ svgEl(lever,'polygon',{points:`${cx-3},${cy} ${cx+3},${cy} ${cx+2},${cy-26} ${cx-2},${cy-26}`,fill:'url(#fw4Chrome)',stroke:'#3c3a34','stroke-width':.6});
+ svgEl(lever,'rect',{x:cx-3.4,y:cy-40,width:6.8,height:16,rx:3,fill:'url(#fw4Chrome)',stroke:'#3c3a34','stroke-width':.6});
+ svgEl(s,'circle',{cx,cy,r:8.5,fill:'url(#fw4Ball)',stroke:'#26241f','stroke-width':.8});
+ svgEl(s,'ellipse',{cx:cx-2.6,cy:cy-3,rx:2.8,ry:1.9,fill:'rgba(255,255,255,.5)'});
+ let fwPos=null;
+ const setFw=q=>{fwPos=q;
+ lever.setAttribute('transform',`rotate(${QUAD[q]},${cx},${cy})`);
+ for(const k of Object.keys(QUAD)){
+ lamps[k].setAttribute('fill',k===q?'var(--jewel-a)':'#3a3426');
+ lamps[k].setAttribute('filter',k===q?'drop-shadow(0 0 4px rgba(255,180,58,.8))':'none');}
+ setRd('R34','POS '+q);};
+ // quadrant click zones (diagonal quadrants, drawn last)
+ const zone=(q,pts)=>{const z=svgEl(s,'polygon',{points:pts,fill:'transparent'});
+ z.style.cursor='pointer';z.addEventListener('click',()=>setFw(q));};
+ zone('A',`${cx},${cy} ${cx},5 5,5 5,${cy}`);
+ zone('B',`${cx},${cy} ${cx},5 125,5 125,${cy}`);
+ zone('C',`${cx},${cy} 125,${cy} 125,125 ${cx},125`);
+ zone('D',`${cx},${cy} ${cx},125 5,125 5,${cy}`);
+ setFw('C');})();
+
+/* 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');})();
+
+/* R37 pin routing matrix: click an intersection to seat/pull a pin; many-to-many */
+(function(){const s=$('pinmx');
+ const ROWS=['OSC1','OSC2','LFO','NOIS','ENV'],COLS=['VCF','VCA','PAN','DLY','OUT','MOD'];
+ const X0=42,Y0=30,DX=19,DY=15.5;
+ svgEl(s,'rect',{x:2,y:2,width:156,height:106,rx:8,fill:'#1c1916',stroke:'#060505','stroke-width':1.5});
+ COLS.forEach((c,j)=>svgEl(s,'text',{x:X0+j*DX,y:16,'text-anchor':'middle','font-size':5.4,
+ 'letter-spacing':'.05em','font-family':'var(--mono)',fill:'var(--steel)'}).textContent=c);
+ ROWS.forEach((r,i)=>svgEl(s,'text',{x:34,y:Y0+i*DY+2,'text-anchor':'end','font-size':5.4,
+ 'letter-spacing':'.05em','font-family':'var(--mono)',fill:'var(--steel)'}).textContent=r);
+ const pins=new Set(['OSC1>VCF','LFO>PAN','ENV>VCA']); // a legible default patch
+ ROWS.forEach((r,i)=>COLS.forEach((c,j)=>{
+ const cx=X0+j*DX,cy=Y0+i*DY,key=r+'>'+c;
+ svgEl(s,'circle',{cx,cy,r:3.4,fill:'#0a0908',stroke:'#2c2820','stroke-width':1});
+ const cap=svgEl(s,'circle',{cx,cy,r:4.4,fill:'var(--gold)',stroke:'#7d5c16','stroke-width':1,
+ opacity:pins.has(key)?1:0,filter:'drop-shadow(0 1px 2px rgba(0,0,0,.6))'});
+ const hit=svgEl(s,'circle',{cx,cy,r:8,fill:'transparent'});
+ hit.style.cursor='pointer';
+ hit.addEventListener('click',()=>{const on=!pins.has(key);
+ if(on)pins.add(key);else pins.delete(key);
+ cap.setAttribute('opacity',on?1:0);
+ setRd('R37',(on?r+' → '+c:'pulled '+r+' → '+c)+' · '+pins.size+' routes');});}));
+ setRd('R37',pins.size+' routes');})();
+
+/* R38 dead-man button: state exists only while the pointer holds it down */
+(function(){const s=$('deadman'),cx=60,cy=58;
+ const defs=svgEl(s,'defs',{});
+ const rb=svgEl(defs,'radialGradient',{id:'dmBtn',cx:'40%',cy:'32%',r:'85%'});
+ svgEl(rb,'stop',{offset:'0','stop-color':'#3a3631'});
+ svgEl(rb,'stop',{offset:'1','stop-color':'#16130f'});
+ svgEl(s,'rect',{x:4,y:4,width:112,height:102,rx:12,fill:'#211e1a',stroke:'#0c0b09','stroke-width':1.5});
+ svgEl(s,'text',{x:cx,y:18,'text-anchor':'middle','font-size':6.2,'letter-spacing':'.16em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='HOLD TO RUN';
+ const lamp=svgEl(s,'circle',{cx:104,cy:16,r:4.5,fill:'#1e2a12'});
+ const ring=svgEl(s,'circle',{cx,cy,r:33,fill:'none',stroke:'#2c2820','stroke-width':3});
+ const btn=svgEl(s,'g',{});
+ svgEl(btn,'circle',{cx,cy,r:28,fill:'url(#dmBtn)',stroke:'#060505','stroke-width':1.5});
+ svgEl(btn,'circle',{cx,cy,r:22,fill:'none',stroke:'rgba(255,255,255,.07)','stroke-width':1});
+ svgEl(btn,'text',{x:cx,y:cy+2.5,'text-anchor':'middle','font-size':7,'letter-spacing':'.1em',
+ 'font-family':'var(--mono)',fill:'var(--silver)'}).textContent='RUN';
+ btn.style.cursor='pointer';
+ let dmT0=null,dmIv=null;
+ const stop=()=>{if(dmT0===null)return;
+ const held=((Date.now()-dmT0)/1000).toFixed(1);
+ dmT0=null;clearInterval(dmIv);
+ btn.removeAttribute('transform');
+ ring.setAttribute('stroke','#2c2820');ring.removeAttribute('filter');
+ lamp.setAttribute('fill','#1e2a12');lamp.removeAttribute('filter');
+ setRd('R38','SAFE (held '+held+'s)');};
+ btn.addEventListener('pointerdown',e=>{btn.setPointerCapture(e.pointerId);
+ dmT0=Date.now();
+ btn.setAttribute('transform','translate(0,1.5)');
+ ring.setAttribute('stroke','var(--gold)');ring.setAttribute('filter','drop-shadow(0 0 5px rgba(var(--glow-lo),.7))');
+ lamp.setAttribute('fill','var(--pass)');lamp.setAttribute('filter','drop-shadow(0 0 4px rgba(116,147,47,.8))');
+ setRd('R38','RUNNING 0.0s');
+ dmIv=setInterval(()=>{if(dmT0!==null)setRd('R38','RUNNING '+((Date.now()-dmT0)/1000).toFixed(1)+'s');},100);
+ e.preventDefault();});
+ btn.addEventListener('pointerup',stop);
+ btn.addEventListener('pointercancel',stop);
+ setRd('R38','SAFE');})();
+
+/* R39 rotary telephone dial: click a hole; the wheel winds to the stop and returns */
+(function(){const s=$('teldial'),cx=65,cy=63;
+ const DIGITS=['1','2','3','4','5','6','7','8','9','0'];
+ const angOf=i=>(60-i*30+360)%360; // 1 at 60cw, counterclockwise to 0 at 150
+ const STOP=105;
+ const defs=svgEl(s,'defs',{});
+ const mt=svgEl(defs,'radialGradient',{id:'tdWheel',cx:'42%',cy:'36%',r:'85%'});
+ svgEl(mt,'stop',{offset:'0','stop-color':'#8f8a7e'});
+ svgEl(mt,'stop',{offset:'1','stop-color':'#4a463e'});
+ svgEl(s,'circle',{cx,cy,r:56,fill:'#14110e',stroke:'#060505','stroke-width':2});
+ svgEl(s,'circle',{cx,cy,r:50,fill:'#1e1a16',stroke:'#0a0908','stroke-width':1});
+ // rotating finger wheel: ring + hole openings (dark wells punched in the metal)
+ const wheel=svgEl(s,'g',{});
+ svgEl(wheel,'circle',{cx,cy,r:46,fill:'none',stroke:'url(#tdWheel)','stroke-width':17,opacity:.95});
+ DIGITS.forEach((d,i)=>{const [x,y]=polar(cx,cy,38,angOf(i));
+ svgEl(wheel,'circle',{cx:x,cy:y,r:7.2,fill:'#1e1a16',stroke:'#26221c','stroke-width':1.4});});
+ // fixed digits ON TOP so they read through the holes (they stay put while the wheel spins)
+ DIGITS.forEach((d,i)=>{const [x,y]=polar(cx,cy,38,angOf(i));
+ svgEl(s,'text',{x,y:y+3,'text-anchor':'middle','font-size':8.5,'font-weight':700,
+ 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent=d;});
+ svgEl(s,'circle',{cx,cy,r:16,fill:'url(#tdWheel)',stroke:'#26221c','stroke-width':1});
+ // finger stop
+ const [fx,fy]=polar(cx,cy,52,STOP);
+ svgEl(s,'rect',{x:fx-2.5,y:fy-8,width:5,height:16,rx:2,fill:'#b8b2a4',stroke:'#3c382f','stroke-width':1,
+ transform:`rotate(${STOP},${fx},${fy})`});
+ let dialed='',spinning=false;
+ const setW=a=>wheel.setAttribute('transform',`rotate(${a},${cx},${cy})`);
+ const finish=d=>{dialed=(dialed+d).slice(-10);setRd('R39',dialed);spinning=false;};
+ DIGITS.forEach((d,i)=>{const [x,y]=polar(cx,cy,38,angOf(i));
+ const hit=svgEl(s,'circle',{cx:x,cy:y,r:8.5,fill:'transparent'});
+ hit.style.cursor='pointer';
+ hit.addEventListener('click',()=>{if(spinning)return;spinning=true;
+ const R=(STOP-angOf(i)+360)%360;
+ if(matchMedia('(prefers-reduced-motion: reduce)').matches){setW(R);setTimeout(()=>{setW(0);finish(d);},160);return;}
+ const t0=performance.now(),Tf=R/300*1000,Tb=R/450*1000;
+ const step=t=>{const el=t-t0;
+ if(el<Tf){setW(R*el/Tf);requestAnimationFrame(step);}
+ else if(el<Tf+80){setW(R);requestAnimationFrame(step);}
+ else if(el<Tf+80+Tb){setW(R*(1-(el-Tf-80)/Tb));requestAnimationFrame(step);}
+ else{setW(0);finish(d);}};
+ requestAnimationFrame(step);});});
+ setRd('R39','dial a number');})();
+
+/* R40 circuit breaker panel: on/off by click, TRIP pops one to the amber mid-state, reset is two-step */
+(function(){const s=$('brkpanel');
+ const NAMES=['MAIN','PUMP','LAMP','AUX'];
+ svgEl(s,'rect',{x:2,y:2,width:156,height:106,rx:8,fill:'#1c1916',stroke:'#060505','stroke-width':1.5});
+ const brk=[];
+ const draw=()=>{let closed=0,trip=null;
+ brk.forEach(b=>{if(b.state==='on')closed++;if(b.state==='tripped')trip=b.name;
+ b.lamp.setAttribute('fill',b.state==='on'?'var(--pass)':'#1e2a12');
+ b.lamp.setAttribute('filter',b.state==='on'?'drop-shadow(0 0 3px rgba(116,147,47,.8))':'none');
+ b.handle.setAttribute('y',b.state==='on'?40:b.state==='tripped'?57:74);
+ b.collar.setAttribute('opacity',b.state==='tripped'?1:0);});
+ setRd('R40',closed+'/4 closed'+(trip?' · '+trip+' TRIPPED':''));};
+ NAMES.forEach((name,i)=>{const x=14+i*30;
+ svgEl(s,'rect',{x,y:36,width:20,height:52,rx:4,fill:'#14110e',stroke:'#2c2820','stroke-width':1});
+ const lamp=svgEl(s,'circle',{cx:x+10,cy:26,r:4,fill:'#1e2a12'});
+ const collar=svgEl(s,'rect',{x:x+4,y:69,width:12,height:5,fill:'var(--amber-warn)',opacity:0});
+ const handle=svgEl(s,'rect',{x:x+4,y:40,width:12,height:14,rx:2,fill:'#3a3631',stroke:'#060505','stroke-width':1});
+ svgEl(s,'text',{x:x+10,y:98,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.05em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=name;
+ const hit=svgEl(s,'rect',{x,y:20,width:20,height:80,fill:'transparent'});
+ hit.style.cursor='pointer';
+ const b={name,state:i<3?'on':'off',lamp,collar,handle};
+ hit.addEventListener('click',()=>{b.state=b.state==='on'?'off':b.state==='off'?'on':'off';draw();});
+ brk.push(b);});
+ const tb=svgEl(s,'g',{});
+ svgEl(tb,'rect',{x:132,y:40,width:22,height:16,rx:4,fill:'var(--fail)',stroke:'#5c150c','stroke-width':1});
+ svgEl(tb,'text',{x:143,y:50.5,'text-anchor':'middle','font-size':6,'font-weight':700,
+ 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent='TRIP';
+ tb.style.cursor='pointer';
+ tb.addEventListener('click',()=>{for(let i=brk.length-1;i>=0;i--)if(brk[i].state==='on'){brk[i].state='tripped';break;}draw();});
+ draw();})();
+
+/* R41 DSKY: verb/noun command grammar with status lamps and a lamp-test verb */
+(function(){const host=$('dsky');
+ const LAMPS=['UPLINK','TEMP','GIMBAL','PROG','RESTART','OPR ERR'];
+ host.innerHTML=
+ `<div class="lampcol">${LAMPS.map(l=>`<div class="sl" data-l="${l}">${l}</div>`).join('')}</div>`+
+ `<div class="right"><div class="wins">`+
+ `<div class="win" data-w="prog"><span class="wl">PROG</span><span class="wd"></span></div>`+
+ `<div class="win" data-w="verb"><span class="wl">VERB</span><span class="wd"></span></div>`+
+ `<div class="win" data-w="noun"><span class="wl">NOUN</span><span class="wd"></span></div>`+
+ `</div><div class="pad"></div></div>`;
+ const win=w=>host.querySelector(`[data-w="${w}"]`);
+ const setWin=(w,txt)=>{win(w).querySelector('.wd').innerHTML=seg7(txt[0]||' ')+seg7(txt[1]||' ');};
+ const lampEl=l=>host.querySelector(`[data-l="${l}"]`);
+ let mode=null,vals={prog:' ',verb:' ',noun:' '},entry='';
+ const KEYS=['VERB','NOUN','CLR','ENTR','RSET','7','8','9','4','5','6','1','2','3','0'];
+ const pad=host.querySelector('.pad');
+ const hot=()=>{host.querySelectorAll('.win').forEach(w=>w.classList.toggle('hot',w.dataset.w===mode));};
+ const oprErr=()=>{lampEl('OPR ERR').classList.add('on');setTimeout(()=>lampEl('OPR ERR').classList.remove('on'),1200);setRd('R41','OPR ERR');};
+ const commit=()=>{
+ if(vals.verb.trim().length<2){oprErr();return;}
+ const v=vals.verb,n=vals.noun.trim();
+ if(v==='35'){LAMPS.forEach(l=>lampEl(l).classList.add('on'));
+ setTimeout(()=>LAMPS.forEach(l=>lampEl(l).classList.remove('on')),1400);
+ setRd('R41','V35 · lamp test');}
+ else if(v==='16'&&n==='36'){vals.prog='16';setWin('prog',vals.prog);setRd('R41','V16 N36 · monitor clock');}
+ else setRd('R41','V'+v+(n?' N'+n:'')+' ENTR');
+ vals.verb=' ';vals.noun=' ';entry='';mode=null;hot();};
+ KEYS.forEach(k=>{const b=document.createElement('button');b.className='key';b.textContent=k;
+ b.addEventListener('click',()=>{
+ if(k==='VERB'||k==='NOUN'){mode=k.toLowerCase();entry='';vals[mode]=' ';setWin(mode,' ');hot();}
+ else if(k==='CLR'){if(mode){vals[mode]=' ';entry='';setWin(mode,' ');}}
+ else if(k==='RSET'){LAMPS.forEach(l=>lampEl(l).classList.remove('on'));setRd('R41','RSET');}
+ else if(k==='ENTR')commit();
+ else if(/\d/.test(k)){if(!mode){oprErr();return;}
+ entry=(entry+k).slice(0,2);vals[mode]=entry.padEnd(2,' ');setWin(mode,vals[mode]);}});
+ pad.appendChild(b);});
+ vals.prog='00';setWin('prog',vals.prog);setWin('verb',' ');setWin('noun',' ');
+ setRd('R41','VERB ## ENTR');})();
+
+/* R42 cam-timer program drum: the program is a ring; the pointer self-advances through it */
+const CAM_STEPS=['OFF','FILL','WASH','WASH','WASH','RINSE','RINSE','DRAIN','SPIN','SPIN','FLUFF','COOL'];
+const CAM_COLS={OFF:'#3a3426',FILL:'#54677d',WASH:'#d29638',RINSE:'#46b89e',DRAIN:'#969385',SPIN:'#cb6b4d',FLUFF:'#c9b98a',COOL:'#7c99b0'};
+let camPos=0,camPtr=null;
+(function(){const s=$('camtimer'),cx=65,cy=65;
+ svgEl(s,'circle',{cx,cy,r:60,fill:'#17140f',stroke:'#060505','stroke-width':2});
+ CAM_STEPS.forEach((st,i)=>{const a0=i*30-90,a1=a0+30;
+ const p0o=polar(cx,cy,52,a0+90),p1o=polar(cx,cy,52,a1+90),p0i=polar(cx,cy,38,a0+90),p1i=polar(cx,cy,38,a1+90);
+ svgEl(s,'path',{d:`M ${p0i[0]} ${p0i[1]} L ${p0o[0]} ${p0o[1]} A 52 52 0 0 1 ${p1o[0]} ${p1o[1]} L ${p1i[0]} ${p1i[1]} A 38 38 0 0 0 ${p0i[0]} ${p0i[1]} Z`,
+ fill:CAM_COLS[st],opacity:.75,stroke:'#0a0908','stroke-width':.8});
+ const [tx,ty]=polar(cx,cy,45,i*30+15);
+ svgEl(s,'text',{x:tx,y:ty+2,'text-anchor':'middle','font-size':4.4,'font-weight':700,
+ 'font-family':'var(--mono)',fill:st==='OFF'?'var(--cream)':'#14110e'}).textContent=st==='OFF'?'OFF':st[0];});
+ svgEl(s,'circle',{cx,cy,r:34,fill:'#211e1a',stroke:'#0a0908','stroke-width':1.5});
+ CAM_STEPS.forEach((st,i)=>{if(i===0)return;const [lx,ly]=polar(cx,cy,27,i*30+15);
+ svgEl(s,'text',{x:lx,y:ly+1.8,'text-anchor':'middle','font-size':4.2,
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=st;});
+ svgEl(s,'circle',{cx,cy,r:17,fill:'#14110e',stroke:'#2c2820','stroke-width':1.5});
+ camPtr=svgEl(s,'polygon',{points:`${cx-2.5},${cy} ${cx+2.5},${cy} ${cx},${cy-31}`,fill:'var(--gold-hi)',
+ filter:'drop-shadow(0 0 3px rgba(var(--glow-lo),.7))'});
+ svgEl(s,'circle',{cx,cy,r:5,fill:'#3a3631',stroke:'#060505','stroke-width':1});
+ s.style.cursor='pointer';
+ s.addEventListener('click',()=>setCam((camPos+1)%12));})();
+function setCam(i){camPos=i;
+ camPtr.setAttribute('transform',`rotate(${i*30+15},65,65)`);
+ setRd('R42',camPos===0?'OFF':'STEP '+camPos+' · '+CAM_STEPS[camPos]);}
+setCam(0);
+if(!matchMedia('(prefers-reduced-motion: reduce)').matches)
+ setInterval(()=>{if(camPos>0){const n=(camPos+1)%12;setCam(n);if(n===0)setRd('R42','CYCLE DONE · OFF');}},2000);
+
+/* R43 attitude indicator: sky/ground roll+shift behind a fixed miniature aircraft; 2D drag */
+(function(){const s=$('attind'),cx=65,cy=65;
+ const defs=svgEl(s,'defs',{});
+ const clip=svgEl(defs,'clipPath',{id:'aiClip'});
+ svgEl(clip,'circle',{cx,cy,r:48});
+ const sky=svgEl(defs,'linearGradient',{id:'aiSky',x1:0,y1:0,x2:0,y2:1});
+ svgEl(sky,'stop',{offset:'0','stop-color':'#54677d'});
+ svgEl(sky,'stop',{offset:'1','stop-color':'#3a4c60'});
+ const gnd=svgEl(defs,'linearGradient',{id:'aiGnd',x1:0,y1:0,x2:0,y2:1});
+ svgEl(gnd,'stop',{offset:'0','stop-color':'#5c4630'});
+ svgEl(gnd,'stop',{offset:'1','stop-color':'#3c2e1e'});
+ svgEl(s,'circle',{cx,cy,r:56,fill:'#14110e',stroke:'#060505','stroke-width':2});
+ const g=svgEl(s,'g',{'clip-path':'url(#aiClip)'});
+ const ball=svgEl(g,'g',{});
+ svgEl(ball,'rect',{x:cx-90,y:cy-180,width:180,height:180,fill:'url(#aiSky)'});
+ svgEl(ball,'rect',{x:cx-90,y:cy,width:180,height:180,fill:'url(#aiGnd)'});
+ svgEl(ball,'line',{x1:cx-90,y1:cy,x2:cx+90,y2:cy,stroke:'#f3e7c5','stroke-width':1.4});
+ for(const p of [-20,-10,10,20]){const y=cy-p*1.6,w=p%20===0?16:10;
+ svgEl(ball,'line',{x1:cx-w,y1:y,x2:cx+w,y2:y,stroke:'#f3e7c5','stroke-width':.8,opacity:.85});
+ svgEl(ball,'text',{x:cx+w+3,y:y+2,'font-size':4.6,'font-family':'var(--mono)',
+ fill:'#f3e7c5',opacity:.85}).textContent=String(Math.abs(p));}
+ // fixed bank scale + pointer + miniature aircraft
+ for(const b of [-60,-45,-30,-20,-10,0,10,20,30,45,60]){
+ const [x1,y1]=polar(cx,cy,47,b),[x2,y2]=polar(cx,cy,b%30===0?42:44,b);
+ svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':b===0?1.6:.9,opacity:.9});}
+ svgEl(s,'polygon',{points:`${cx-3.5},${cy-38} ${cx+3.5},${cy-38} ${cx},${cy-44}`,fill:'var(--gold-hi)'});
+ svgEl(s,'path',{d:`M ${cx-22} ${cy} L ${cx-7} ${cy} L ${cx-4} ${cy+4} M ${cx+22} ${cy} L ${cx+7} ${cy} L ${cx+4} ${cy+4}`,
+ fill:'none',stroke:'var(--gold-hi)','stroke-width':2.4,'stroke-linecap':'round'});
+ svgEl(s,'circle',{cx,cy,r:2,fill:'var(--gold-hi)'});
+ let aiBank=0,aiPitch=0;
+ const setAi=(b,p)=>{aiBank=Math.max(-60,Math.min(60,b));aiPitch=Math.max(-20,Math.min(20,p));
+ ball.setAttribute('transform',`rotate(${(-aiBank).toFixed(1)},${cx},${cy}) translate(0,${(aiPitch*1.6).toFixed(1)})`);
+ setRd('R43','BANK '+(aiBank<0?'L':aiBank>0?'R':'')+Math.abs(Math.round(aiBank))+' · PITCH '+(aiPitch>=0?'+':'-')+String(Math.abs(Math.round(aiPitch))).padStart(2,'0'));};
+ s.style.touchAction='none';s.style.cursor='move';
+ s.addEventListener('pointerdown',e=>{s.setPointerCapture(e.pointerId);
+ const x0=e.clientX,y0=e.clientY,b0=aiBank,p0=aiPitch;
+ const mv=ev=>setAi(b0+(ev.clientX-x0)*0.4,p0+(ev.clientY-y0)*0.25);
+ const up=()=>{s.removeEventListener('pointermove',mv);s.removeEventListener('pointerup',up);s.removeEventListener('pointercancel',up);};
+ s.addEventListener('pointermove',mv);s.addEventListener('pointerup',up);s.addEventListener('pointercancel',up);e.preventDefault();});
+ setAi(0,0);})();
+
+/* R44 heading bug + servo needle: drag sets the command; the needle chases with lag */
+let hbCmd=90,hbAct=90,hbBug=null,hbNeedle=null;
+(function(){const s=$('hdgbug'),cx=65,cy=65;
+ svgEl(s,'circle',{cx,cy,r:56,fill:'#14110e',stroke:'#060505','stroke-width':2});
+ svgEl(s,'circle',{cx,cy,r:50,fill:'#1a1714',stroke:'#2c2820','stroke-width':1});
+ for(let d=0;d<360;d+=10){const maj=d%30===0;
+ const [x1,y1]=polar(cx,cy,48,d),[x2,y2]=polar(cx,cy,maj?42:45,d);
+ svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':maj?1:.5,opacity:maj?.9:.5});}
+ for(let d=0;d<360;d+=30){const lbl=d===0?'N':d===90?'E':d===180?'S':d===270?'W':String(d/10);
+ const [x,y]=polar(cx,cy,35,d);
+ svgEl(s,'text',{x,y:y+2.6,'text-anchor':'middle','font-size':d%90===0?8:6.4,'font-weight':700,
+ 'font-family':'var(--mono)',fill:d%90===0?'var(--cream)':'var(--steel)'}).textContent=lbl;}
+ svgEl(s,'line',{x1:cx,y1:cy-56,x2:cx,y2:cy-48,stroke:'var(--fail)','stroke-width':2});
+ hbBug=svgEl(s,'path',{d:`M ${cx-6} ${cy-49} L ${cx-6} ${cy-44} L ${cx-2.5} ${cy-44} L ${cx} ${cy-47} L ${cx+2.5} ${cy-44} L ${cx+6} ${cy-44} L ${cx+6} ${cy-49} Z`,
+ fill:'var(--gold-hi)',stroke:'#7d5c16','stroke-width':.7});
+ hbNeedle=svgEl(s,'g',{});
+ svgEl(hbNeedle,'polygon',{points:`${cx-2},${cy+10} ${cx+2},${cy+10} ${cx+1.2},${cy-40} ${cx},${cy-44} ${cx-1.2},${cy-40}`,
+ fill:'#bfc4d0',stroke:'#4a4e58','stroke-width':.6});
+ svgEl(s,'circle',{cx,cy,r:4.5,fill:'#3a3631',stroke:'#060505','stroke-width':1});
+ const draw=()=>{hbBug.setAttribute('transform',`rotate(${hbCmd.toFixed(1)},${cx},${cy})`);
+ hbNeedle.setAttribute('transform',`rotate(${hbAct.toFixed(1)},${cx},${cy})`);
+ setRd('R44','CMD '+String(Math.round((hbCmd%360+360)%360)).padStart(3,'0')+' · ACT '+String(Math.round((hbAct%360+360)%360)).padStart(3,'0'));};
+ dragDelta(s,()=>hbCmd,v=>{hbCmd=(v%360+360)%360;draw();},{min:-100000,max:100000,sens:1.2});
+ draw();
+ if(!matchMedia('(prefers-reduced-motion: reduce)').matches)
+ setInterval(()=>{const diff=((hbCmd-hbAct+540)%360)-180;
+ if(Math.abs(diff)<0.4){if(hbAct!==hbCmd){hbAct=hbCmd;draw();}return;}
+ hbAct=(hbAct+diff*0.07+360)%360;draw();},80);
+ else{hbAct=hbCmd;}})();
+
+/* 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();})();
+
+/* R48 knife switch (side view): the blade hinges at the left post and lands in the right jaw */
+(function(){const s=$('knifesw');
+ const defs=svgEl(s,'defs',{});
+ const cu=svgEl(defs,'linearGradient',{id:'ksCu',x1:0,y1:0,x2:0,y2:1});
+ svgEl(cu,'stop',{offset:'0','stop-color':'#d09a5c'});
+ svgEl(cu,'stop',{offset:'1','stop-color':'#8a5a2a'});
+ const HX=32,HY=74; // hinge pivot
+ svgEl(s,'rect',{x:6,y:56,width:118,height:44,rx:6,fill:'#22262a',stroke:'#0c0d0e','stroke-width':1.5});
+ const lamp=svgEl(s,'circle',{cx:116,cy:22,r:4.5,fill:'#1e2a12'});
+ svgEl(s,'text',{x:116,y:36,'text-anchor':'middle','font-size':4.8,'letter-spacing':'.08em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='LIVE';
+ // hinge post (left) and jaw clip (right), both on the base
+ svgEl(s,'rect',{x:HX-6,y:HY-6,width:12,height:16,rx:2,fill:'url(#ksCu)',stroke:'#5c3a18','stroke-width':.8});
+ svgEl(s,'rect',{x:96,y:HY-9,width:5,height:18,rx:1.5,fill:'url(#ksCu)',stroke:'#5c3a18','stroke-width':.7});
+ svgEl(s,'rect',{x:104,y:HY-9,width:5,height:18,rx:1.5,fill:'url(#ksCu)',stroke:'#5c3a18','stroke-width':.7});
+ // blade + upright handle, hinged at (HX,HY)
+ const blade=svgEl(s,'g',{});
+ svgEl(blade,'rect',{x:HX,y:HY-3,width:71,height:6,rx:2.5,fill:'url(#ksCu)',stroke:'#5c3a18','stroke-width':.8});
+ svgEl(blade,'rect',{x:88,y:HY-26,width:7,height:26,rx:3,fill:'#14110e',stroke:'#000','stroke-width':1});
+ svgEl(blade,'circle',{cx:91.5,cy:HY-26,r:5,fill:'#14110e',stroke:'#000','stroke-width':1});
+ svgEl(s,'circle',{cx:HX,cy:HY,r:3,fill:'#5c3a18'});
+ blade.style.transformBox='view-box';blade.style.transformOrigin=`${HX}px ${HY}px`;
+ blade.style.transition='transform .28s ease';
+ let closed=true;
+ const draw=()=>{blade.style.transform=closed?'rotate(0deg)':'rotate(-46deg)';
+ lamp.setAttribute('fill',closed?'var(--pass)':'#1e2a12');
+ lamp.setAttribute('filter',closed?'drop-shadow(0 0 4px rgba(116,147,47,.8))':'none');
+ setRd('R48',closed?'CLOSED · live':'OPEN · visibly dead');};
+ s.style.cursor='pointer';
+ s.addEventListener('click',()=>{closed=!closed;draw();});
+ draw();})();
+
+/* R49 decade box: four skirted knobs, one digit each; the value is their sum */
+(function(){const s=$('decbox');
+ const MUL=[1000,100,10,1],LBL=['x1000','x100','x10','x1'];
+ const digs=[3,5,0,0];
+ svgEl(s,'rect',{x:2,y:2,width:156,height:96,rx:8,fill:'#17140f',stroke:'#060505','stroke-width':1.5});
+ const wins=[],knobs=[];
+ const total=()=>digs.reduce((a,d,i)=>a+d*MUL[i],0);
+ const draw=()=>{wins.forEach((w,i)=>w.textContent=String(digs[i]));
+ knobs.forEach((k,i)=>k.setAttribute('transform',`rotate(${digs[i]*36},${28+i*35},58)`));
+ setRd('R49',total().toLocaleString('en-US')+' Ω');};
+ MUL.forEach((m,i)=>{const cx=28+i*35,cy=58;
+ svgEl(s,'rect',{x:cx-8,y:16,width:16,height:12,rx:2,fill:'#0a0806',stroke:'#2c2820','stroke-width':1});
+ wins.push(svgEl(s,'text',{x:cx,y:25.5,'text-anchor':'middle','font-size':8,'font-weight':700,
+ 'font-family':'var(--mono)',fill:'var(--cream)'}));
+ svgEl(s,'circle',{cx,cy,r:15,fill:'#211e1a',stroke:'#0a0908','stroke-width':1.5}); // skirt
+ for(let t=0;t<10;t++){const [x1,y1]=polar(cx,cy,14,t*36),[x2,y2]=polar(cx,cy,12,t*36);
+ svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--steel)','stroke-width':.6,opacity:.7});}
+ const k=svgEl(s,'g',{});
+ svgEl(k,'circle',{cx,cy,r:10,fill:'#2c2824',stroke:'#060505','stroke-width':1});
+ svgEl(k,'line',{x1:cx,y1:cy-9,x2:cx,y2:cy-4,stroke:'var(--gold-hi)','stroke-width':1.8});
+ knobs.push(k);
+ svgEl(s,'text',{x:cx,y:88,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.06em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=LBL[i];
+ const hit=svgEl(s,'circle',{cx,cy,r:16,fill:'transparent'});
+ hit.style.cursor='ns-resize';
+ dragDelta(hit,()=>digs[i],v=>{digs[i]=Math.round(Math.max(0,Math.min(9,v)));draw();},{min:0,max:9,sens:9/70});});
+ draw();})();
+
+/* R50 two-hand safety: one palm button arms a 500ms window; the other completes the cycle */
+(function(){const s=$('twohand');
+ svgEl(s,'rect',{x:2,y:2,width:156,height:96,rx:8,fill:'#1c1916',stroke:'#060505','stroke-width':1.5});
+ const runLamp=svgEl(s,'circle',{cx:80,cy:24,r:5.5,fill:'#1e2a12'});
+ svgEl(s,'text',{x:80,y:40,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.12em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='CYCLE';
+ const mkBtn=(cx,lbl)=>{const g=svgEl(s,'g',{});
+ svgEl(g,'ellipse',{cx,cy:70,rx:19,ry:6,fill:'#0d0b09'});
+ const cap=svgEl(g,'circle',{cx,cy:62,r:16,fill:'#8f2416',stroke:'#5c150c','stroke-width':1.5});
+ svgEl(g,'ellipse',{cx:cx-5,cy:56,rx:6,ry:3.4,fill:'rgba(255,255,255,.22)'});
+ svgEl(s,'text',{x:cx,y:92,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.08em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent=lbl;
+ g.style.cursor='pointer';return {g,cap};};
+ const L=mkBtn(32,'LEFT PALM'),R=mkBtn(128,'RIGHT PALM');
+ let armed=null,armT=null,busy=false;
+ const flash=(cap)=>{cap.setAttribute('fill','#e0523a');setTimeout(()=>cap.setAttribute('fill','#8f2416'),200);};
+ const fault=()=>{armed=null;clearTimeout(armT);
+ setRd('R50','TIE-DOWN FAULT · release and retry');};
+ const press=side=>{if(busy)return;
+ if(armed===null){armed=side;flash(side==='L'?L.cap:R.cap);
+ setRd('R50',side+' armed · other hand within 0.5s');
+ armT=setTimeout(fault,500);return;}
+ if(armed===side){fault();return;} // same hand twice = tie-down
+ clearTimeout(armT);armed=null;busy=true;
+ flash(side==='L'?L.cap:R.cap);
+ runLamp.setAttribute('fill','var(--pass)');
+ runLamp.setAttribute('filter','drop-shadow(0 0 5px rgba(116,147,47,.8))');
+ setRd('R50','CYCLE RUNNING');
+ setTimeout(()=>{runLamp.setAttribute('fill','#1e2a12');runLamp.removeAttribute('filter');
+ busy=false;setRd('R50','ready');},1500);};
+ L.g.addEventListener('click',()=>press('L'));
+ R.g.addEventListener('click',()=>press('R'));
+ setRd('R50','ready');})();
+
+/* R51 voice-loop keyset: independent monitor states, exclusive talk, activity flicker */
+(function(){const host=$('vloop');
+ const LOOPS=['FD','GNC','ECOM','SURG','A/G','NET1','NET2','PAO'];
+ const keys=LOOPS.map((l,i)=>{const k=document.createElement('div');k.className='vk';
+ k.innerHTML=l+'<span class="bar"></span>';k.dataset.state='0';host.appendChild(k);return k;});
+ // a legible default: FD + A/G monitored, GNC talking
+ keys[0].dataset.state='1';keys[0].classList.add('mon');
+ keys[4].dataset.state='1';keys[4].classList.add('mon');
+ keys[1].dataset.state='2';keys[1].classList.add('mon','tlk');
+ const refresh=()=>{const mon=keys.filter(k=>k.dataset.state!=='0').length;
+ const tlk=keys.findIndex(k=>k.dataset.state==='2');
+ setRd('R51',mon===0?'all loops dropped':mon+' monitored'+(tlk>=0?' · talk '+LOOPS[tlk]:''));};
+ keys.forEach((k,i)=>k.addEventListener('click',()=>{
+ const st=k.dataset.state;
+ if(st==='0'){k.dataset.state='1';k.classList.add('mon');}
+ else if(st==='1'){keys.forEach(o=>{if(o.dataset.state==='2'){o.dataset.state='1';o.classList.remove('tlk');}});
+ k.dataset.state='2';k.classList.add('tlk');}
+ else{k.dataset.state='0';k.classList.remove('mon','tlk','act');}
+ refresh();}));
+ if(!matchMedia('(prefers-reduced-motion: reduce)').matches)
+ setInterval(()=>{const t=performance.now()/1000;
+ keys.forEach((k,i)=>{if(k.dataset.state==='0'){k.classList.remove('act');return;}
+ k.classList.toggle('act',Math.sin(t*(1.1+i*0.37)+i*2.1)>0.55);});},300);
+ refresh();})();
+
+/* 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 circular chart recorder: a day per revolution; the pen draws the cycle */
+(function(){const s=$('circhart'),cx=65,cy=65;
+ svgEl(s,'circle',{cx,cy,r:60,fill:'#17140f',stroke:'#060505','stroke-width':2});
+ svgEl(s,'circle',{cx,cy,r:54,fill:'#efe9da',stroke:'#8f897b','stroke-width':.8});
+ for(const r of [20,31,42])
+ svgEl(s,'circle',{cx,cy,r,fill:'none',stroke:'#b8ae98','stroke-width':.5});
+ for(let h=0;h<24;h++){const a=h*15;
+ const [x1,y1]=polar(cx,cy,54,a),[x2,y2]=polar(cx,cy,h%6===0?12:48,a);
+ svgEl(s,'line',{x1,y1,x2,y2,stroke:'#b8ae98','stroke-width':h%6===0?.6:.35,opacity:.8});
+ if(h%6===0){const [tx,ty]=polar(cx,cy,50,a+5);
+ svgEl(s,'text',{x:tx,y:ty+2,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)',
+ fill:'#6e6552'}).textContent=String(h).padStart(2,'0');}}
+ const trace=svgEl(s,'path',{d:'',fill:'none',stroke:'#8f2416','stroke-width':1.1,'stroke-linejoin':'round'});
+ const pen=svgEl(s,'line',{x1:cx,y1:cy,x2:cx,y2:cy-54,stroke:'#4a463e','stroke-width':1});
+ const penDot=svgEl(s,'circle',{cx,cy:cy-30,r:2,fill:'#8f2416'});
+ svgEl(s,'circle',{cx,cy,r:4,fill:'#4a463e'});
+ const val=h=>52+30*Math.sin(h/24*2*Math.PI-2.1)+12*Math.sin(h/24*6*Math.PI)+4*Math.sin(h*2.7);
+ const rOf=v=>12+Math.max(0,Math.min(100,v))*0.36;
+ let pts=[],hour=0;
+ const draw=()=>{const a=hour*15,r=rOf(val(hour));
+ pts.push(polar(cx,cy,r,a));
+ if(pts.length>1)trace.setAttribute('d','M '+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L '));
+ pen.setAttribute('transform',`rotate(${a},${cx},${cy})`);
+ const [px,py]=polar(cx,cy,r,a);
+ penDot.setAttribute('cx',px);penDot.setAttribute('cy',py);
+ setRd('R53',String(Math.floor(hour)).padStart(2,'0')+':'+(hour%1>=0.5?'30':'00')+' · '+Math.round(val(hour))+'%');};
+ const reset=()=>{pts=[];hour=0;trace.setAttribute('d','');draw();};
+ s.style.cursor='pointer';
+ s.addEventListener('click',reset);
+ if(matchMedia('(prefers-reduced-motion: reduce)').matches){
+ for(hour=0;hour<24;hour+=0.25)draw();hour=23.75;}
+ else{reset();setInterval(()=>{hour+=0.25;if(hour>=24){pts=[];hour=0;}draw();},375);}})();
+
+/* R54 vertical tape: the scale scrolls behind a fixed index; drag to drive the value */
+(function(){const s=$('vtape'),W=70,H=130,CX=38,CY=65;
+ const MIN=5,MAX=35,PPU=5.5; // rpm x100, px per unit
+ const defs=svgEl(s,'defs',{});
+ const clip=svgEl(defs,'clipPath',{id:'vtClip'});
+ svgEl(clip,'rect',{x:14,y:8,width:48,height:114,rx:4});
+ svgEl(s,'rect',{x:2,y:2,width:66,height:126,rx:7,fill:'#1c1916',stroke:'#060505','stroke-width':1.5});
+ svgEl(s,'rect',{x:14,y:8,width:48,height:114,rx:4,fill:'#0d0c0a',stroke:'#2c2820','stroke-width':1});
+ const tapeG=svgEl(s,'g',{'clip-path':'url(#vtClip)'});
+ const tape=svgEl(tapeG,'g',{});
+ for(let u=MIN;u<=MAX;u++){const y=-u*PPU; // higher value further up the tape
+ const maj=u%5===0;
+ svgEl(tape,'line',{x1:maj?20:26,y1:y,x2:34,y2:y,stroke:'var(--cream)','stroke-width':maj?1.1:.55,opacity:maj?.95:.6});
+ if(maj)svgEl(tape,'text',{x:48,y:y+3,'text-anchor':'middle','font-size':9,'font-weight':700,
+ 'font-family':'var(--mono)',fill:'var(--cream)'}).textContent=String(u);}
+ // fixed index: side pointer + line across the window at center height
+ svgEl(s,'polygon',{points:`8,${CY} 15,${CY-4} 15,${CY+4}`,fill:'var(--gold-hi)'});
+ svgEl(s,'line',{x1:15,y1:CY,x2:62,y2:CY,stroke:'var(--gold-hi)','stroke-width':1,opacity:.85});
+ svgEl(s,'text',{x:CX,y:126,'text-anchor':'middle','font-size':5,'letter-spacing':'.1em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='RPM x100';
+ let vt=24;
+ const draw=()=>{tape.setAttribute('transform',`translate(0,${(CY+vt*PPU).toFixed(1)})`);
+ setRd('R54','RPM '+Math.round(vt*100));};
+ s.style.cursor='ns-resize';
+ dragDelta(s,()=>vt,v=>{vt=Math.max(MIN,Math.min(MAX,v));draw();},{min:MIN,max:MAX,sens:(MAX-MIN)/140});
+ draw();})();
+
+/* R55 twin-needle gauge: mirrored half-scales, one hub, two independent needles */
+(function(){const s=$('twinng'),cx=60,cy=62,R=44;
+ svgEl(s,'circle',{cx,cy,r:52,fill:'#14110e',stroke:'#060505','stroke-width':2});
+ svgEl(s,'circle',{cx,cy,r:46,fill:'#1a1714',stroke:'#2c2820','stroke-width':1});
+ // mirrored arcs: left = FUEL (0 at bottom-left up to 4 at top), right = OIL
+ const angL=v=>-170+v/4*160; // 0..4 -> -170..-10 (left side, cw from top)
+ const angR=v=>170-v/4*160; // 0..4 -> 170..10 (right side)
+ for(let v=0;v<=4;v++){
+ for(const [ang,anchor] of [[angL(v),'end'],[angR(v),'start']]){
+ const [x1,y1]=polar(cx,cy,R,ang),[x2,y2]=polar(cx,cy,R-5,ang);
+ svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':1,opacity:.9});
+ const [tx,ty]=polar(cx,cy,R-11,ang);
+ svgEl(s,'text',{x:tx,y:ty+2.4,'text-anchor':'middle','font-size':6,'font-family':'var(--mono)',
+ fill:'var(--cream)'}).textContent=String(v);}}
+ for(let v=0.5;v<4;v+=0.5){if(v%1===0)continue;
+ for(const ang of [angL(v),angR(v)]){
+ const [x1,y1]=polar(cx,cy,R,ang),[x2,y2]=polar(cx,cy,R-3,ang);
+ svgEl(s,'line',{x1,y1,x2,y2,stroke:'var(--cream)','stroke-width':.5,opacity:.55});}}
+ svgEl(s,'text',{x:cx-20,y:cy+34,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.08em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='FUEL';
+ svgEl(s,'text',{x:cx+20,y:cy+34,'text-anchor':'middle','font-size':5.4,'letter-spacing':'.08em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='OIL';
+ svgEl(s,'text',{x:cx,y:cy-26,'text-anchor':'middle','font-size':4.6,'letter-spacing':'.06em',
+ 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='kg/cm2';
+ const needle=color=>{const g=svgEl(s,'g',{});
+ svgEl(g,'polygon',{points:`${cx-1.6},${cy+8} ${cx+1.6},${cy+8} ${cx+1},${cy-R+8} ${cx},${cy-R+4} ${cx-1},${cy-R+8}`,
+ fill:color,stroke:'rgba(0,0,0,.4)','stroke-width':.5});return g;};
+ const nF=needle('#e0523a'),nO=needle('#bfc4d0');
+ svgEl(s,'circle',{cx,cy,r:5,fill:'#3a3631',stroke:'#060505','stroke-width':1});
+ let vF=2.4,vO=3.1;
+ const draw=()=>{nF.setAttribute('transform',`rotate(${angL(vF).toFixed(1)},${cx},${cy})`);
+ nO.setAttribute('transform',`rotate(${angR(vO).toFixed(1)},${cx},${cy})`);
+ setRd('R55','FUEL '+vF.toFixed(1)+' · OIL '+vO.toFixed(1));};
+ // each half is its own drag surface
+ const half=(x,get,set)=>{const hit=svgEl(s,'rect',{x,y:10,width:60,height:104,fill:'transparent'});
+ hit.style.cursor='ns-resize';
+ dragDelta(hit,get,v=>{set(Math.max(0,Math.min(4,v)));draw();},{min:0,max:4,sens:4/110});};
+ half(0,()=>vF,v=>vF=v);
+ half(60,()=>vO,v=>vO=v);
+ draw();})();
+
+/* R56 comfort meter: temp and humidity needles cross over printed zone verdicts */
+(function(){const s=$('comfort'),cx=65,cy=60;
+ const defs=svgEl(s,'defs',{});
+ const br=svgEl(defs,'linearGradient',{id:'cmBrass',x1:0,y1:0,x2:0,y2:1});
+ svgEl(br,'stop',{offset:'0','stop-color':'#d8b25c'});
+ svgEl(br,'stop',{offset:'.5','stop-color':'#a07c30'});
+ svgEl(br,'stop',{offset:'1','stop-color':'#6e5218'});
+ svgEl(s,'circle',{cx,cy,r:56,fill:'url(#cmBrass)',stroke:'#4a3610','stroke-width':2});
+ svgEl(s,'circle',{cx,cy,r:53,fill:'none',stroke:'#7a5c1e','stroke-width':2.5,'stroke-dasharray':'2 1.4'});
+ svgEl(s,'circle',{cx,cy,r:49,fill:'#efe9da',stroke:'#8f897b','stroke-width':1});
+ const PT=[38,96],PH=[92,96]; // temp pivot (BL), humidity pivot (BR)
+ const angT=t=>105-(t-40)/60*70; // 40..100F -> bearing 105..35 from PT
+ const angH=h=>-105+h/100*70; // 0..100% -> bearing -105..-35 from PH
+ // scale ticks along each sweep, near the rim
+ for(let t=40;t<=100;t+=10){const a=angT(t);
+ const [x1,y1]=polar(PT[0],PT[1],86,a),[x2,y2]=polar(PT[0],PT[1],82,a);
+ if(Math.hypot(x1-cx,y1-cy)<48){svgEl(s,'line',{x1,y1,x2,y2,stroke:'#3c382f','stroke-width':t%20===0?1:.5});
+ if(t%20===0){const [tx,ty]=polar(PT[0],PT[1],78,a);
+ svgEl(s,'text',{x:tx,y:ty+2,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)',
+ fill:'#3c382f'}).textContent=String(t);}}}
+ for(let h=0;h<=100;h+=10){const a=angH(h);
+ const [x1,y1]=polar(PH[0],PH[1],86,a),[x2,y2]=polar(PH[0],PH[1],82,a);
+ if(Math.hypot(x1-cx,y1-cy)<48){svgEl(s,'line',{x1,y1,x2,y2,stroke:'#3c382f','stroke-width':h%20===0?1:.5});
+ if(h%20===0){const [tx,ty]=polar(PH[0],PH[1],78,a);
+ svgEl(s,'text',{x:tx,y:ty+2,'text-anchor':'middle','font-size':4.6,'font-family':'var(--mono)',
+ fill:'#3c382f'}).textContent=String(h);}}}
+ // printed zone verdicts; the active one goes red
+ const zones={};
+ const zone=(key,txt,x,y,rot,size)=>{zones[key]=svgEl(s,'text',{x,y,'text-anchor':'middle',
+ 'font-size':size||5.2,'font-weight':700,'letter-spacing':'.06em','font-family':'var(--mono)',
+ fill:'#8f897b',transform:`rotate(${rot},${x},${y})`});zones[key].textContent=txt;};
+ zone('humid','TOO HUMID',42,34,-32);
+ zone('warm','TOO WARM',89,34,32);
+ zone('right','JUST RIGHT',65,58,0,6.2);
+ zone('dry','TOO DRY',38,74,28);
+ zone('cold','TOO COLD',93,74,-28);
+ svgEl(s,'text',{x:cx,y:92,'text-anchor':'middle','font-size':5.6,'letter-spacing':'.18em',
+ 'font-family':'var(--mono)',fill:'#3c382f'}).textContent='WEATHER STATION';
+ // needles: temp from bottom-left, humidity from bottom-right — clipped to the face
+ const nclip=svgEl(defs,'clipPath',{id:'cmClip'});
+ svgEl(nclip,'circle',{cx,cy,r:49});
+ const ng=svgEl(s,'g',{'clip-path':'url(#cmClip)'});
+ const needle=()=>{const g=svgEl(ng,'g',{});
+ svgEl(g,'line',{x1:0,y1:0,x2:0,y2:-84,stroke:'#c23a28','stroke-width':1.6,'stroke-linecap':'round'});
+ svgEl(g,'circle',{cx:0,cy:0,r:2.6,fill:'#8f2416'});return g;};
+ const nT=needle(),nH=needle();
+ nT.setAttribute('transform',`translate(${PT[0]},${PT[1]})`);
+ nH.setAttribute('transform',`translate(${PH[0]},${PH[1]})`);
+ let vT=72,vH=45;
+ const verdict=()=>vT>78?'warm':vT<62?'cold':vH>60?'humid':vH<30?'dry':'right';
+ const draw=()=>{
+ nT.setAttribute('transform',`translate(${PT[0]},${PT[1]}) rotate(${angT(vT).toFixed(1)})`);
+ nH.setAttribute('transform',`translate(${PH[0]},${PH[1]}) rotate(${angH(vH).toFixed(1)})`);
+ const v=verdict();
+ for(const k of Object.keys(zones))zones[k].setAttribute('fill',k===v?'#c23a28':'#8f897b');
+ setRd('R56',Math.round(vT)+'F · '+Math.round(vH)+'% RH · '+zones[v].textContent);};
+ const half=(x,get,set,min,max)=>{const hit=svgEl(s,'rect',{x,y:8,width:65,height:106,fill:'transparent'});
+ hit.style.cursor='ns-resize';
+ dragDelta(hit,get,v=>{set(Math.max(min,Math.min(max,v)));draw();},{min,max,sens:(max-min)/110});};
+ half(0,()=>vT,v=>vT=v,40,100);
+ half(65,()=>vH,v=>vH=v,0,100);
+ draw();})();
+
+/* ---- 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',$('rcrt'),['green','amber','red','blue','vfd'],'green');
+screenChips('R19',$('wregion'),['white','green','amber','blue','vfd'],'white');
+screenChips('R31',$('radar'),['amber','green','red','blue','vfd'],'amber');
+screenChips('N11',$('scopep').closest('.scope'),['green','amber','red','blue','vfd'],'green');
</script>
</body>
</html>