aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-12 22:40:22 -0500
committerCraig Jennings <c@cjennings.net>2026-07-12 22:40:22 -0500
commit066bdac1c195fa27d4b71f2ba53ee9265ee7c833 (patch)
tree451f613874801c291b9fb692c8ed6da3d731dce9
parent4064de53a9abfa7620d9d7e3f95e867ba0bf54e3 (diff)
downloadarchsetup-066bdac1c195fa27d4b71f2ba53ee9265ee7c833.tar.gz
archsetup-066bdac1c195fa27d4b71f2ba53ee9265ee7c833.zip
refactor(gallery): extract indicators R10, R11, R25, R26, R30, R31 into GW builders
-rw-r--r--docs/prototypes/panel-widget-gallery.html226
-rw-r--r--docs/prototypes/widgets.js314
-rw-r--r--tests/gallery-probes/probe-fams.mjs14
3 files changed, 335 insertions, 219 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index 2d7bcb9..f81b60b 100644
--- a/docs/prototypes/panel-widget-gallery.html
+++ b/docs/prototypes/panel-widget-gallery.html
@@ -1850,22 +1850,22 @@ card(I,'N28','Patch-bay jack field',
(st,rd)=>GW.patchBay(st,{onChange:(v,t)=>rd(t)}),
'<b>routing shown as patches.</b> A grid of jacks with cables between connected pairs. Click one jack then another to add/remove a cable.');
card(I,'R10','Data matrix readout',
- `<svg id="dmx" class="rsvg press" viewBox="0 0 190 74" width="190" height="74"></svg>`,
+ (st,rd)=>GW.dataMatrix(st,{onChange:(v,t)=>rd(t)}),
'<b>a page of amber fields.</b> The console data block — several labeled values on one lit matrix. Click to cycle pages; readout names the page. After a cockpit data readout.');
card(I,'R11','Warning flag window',
- `<svg id="wflag" class="rsvg press" viewBox="0 0 120 64" width="120" height="64"></svg>`,
+ (st,rd)=>GW.warningFlag(st,{onChange:(v,t)=>rd(t)}),
'<b>the mechanical alarm flag.</b> A striped barber-pole slides into a window when the condition trips — not a lamp, a flag. Click to trip and clear it. After the VIB flag on an altimeter.');
card(I,'R25','Fourteen-segment display',
- `<svg id="seg14" class="rsvg press" viewBox="0 0 130 54" width="130" height="54"></svg>`,
+ (st,rd)=>GW.seg14(st,{onChange:(v,t)=>rd(t)}),
'<b>segments that spell.</b> The starburst alphanumeric — diagonals and split bars let it write words, not just digits. Click to cycle the word. After a pedal preset display.');
card(I,'R26','Response graph',
- `<svg id="rgraph" class="rsvg" viewBox="0 0 190 110" width="190" height="110"></svg>`,
+ (st,rd)=>GW.responseGraph(st,{onChange:(v,t)=>rd(t)}),
'<b>a curve on labeled axes.</b> Log frequency across, dB up the side, and the amber peak is yours to place — drag it anywhere on the plot. After the response display of a mastering processor.');
card(I,'R30','Telegraph indicator',
- `<svg id="telind" class="rsvg press" viewBox="0 0 110 110" width="110" height="110"></svg>`,
+ (st,rd)=>GW.telegraphIndicator(st,{onChange:(v,t)=>rd(t)}),
'<b>state on a pie of sectors.</b> The pointer and its little flag name the active state, engine-telegraph style. Click to step the state. After an electric indicator dial.');
card(I,'R31','Radar sweep',
- `<svg id="radar" class="rsvg press" viewBox="0 0 130 130" width="130" height="130"></svg>`,
+ (st,rd)=>GW.radarSweep(st,{onChange:(v,t)=>rd(t)}),
'<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>`,
@@ -1916,215 +1916,17 @@ function tickMcVu(){const a=lvl();const target=Math.min(1.02,a*1.12);
/* R07-R09 extracted to widgets.js (GW.*) */
-/* R10 data matrix readout */
-const DMX_PAGES=[
- ['SYS',()=>['CPU 42 MEM 61','DSK 58 TMP 47C','ARCH 6.18 LTS']],
- ['NET',()=>['WIFI @HYATT 300M','VPN UP DNS OK','BT 2 PAIRED']],
- ['TIME',()=>{const d=new Date(),p=n=>String(n).padStart(2,'0');
- return [`${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`,
- `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`,
- 'UP 14D 06:12'];}]];
-let dmxI=0,dmxLines=[];
-(function(){const s=$('dmx');
- const defs=svgEl(s,'defs',{});
- 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':'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(--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(--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];
- rowsFn().forEach((txt,r)=>{dmxLines[r][0].textContent=txt;dmxLines[r][1].textContent=txt;});
- setRd('R10','page '+name);}
-setDmx(0);
-if(!matchMedia('(prefers-reduced-motion: reduce)').matches)
- setInterval(()=>{if(DMX_PAGES[dmxI][0]==='TIME')setDmx(dmxI);},1000);
-
-/* R11 warning flag window */
-let flagOn=false,flagStripes=null;
-(function(){const s=$('wflag');
- const defs=svgEl(s,'defs',{});
- const pat=svgEl(defs,'pattern',{id:'barber',width:10,height:10,patternUnits:'userSpaceOnUse',
- patternTransform:'rotate(45)'});
- svgEl(pat,'rect',{x:0,y:0,width:10,height:10,fill:'#17130c'});
- svgEl(pat,'rect',{x:0,y:0,width:5,height:10,fill:'var(--gold)'});
- const clip=svgEl(defs,'clipPath',{id:'flagWin'});
- svgEl(clip,'rect',{x:14,y:20,width:56,height:24});
- svgEl(s,'rect',{x:11,y:17,width:62,height:30,rx:3,fill:'#0a0908',stroke:'#2c261d','stroke-width':2});
- const g=svgEl(s,'g',{'clip-path':'url(#flagWin)'});
- flagStripes=svgEl(g,'rect',{x:14,y:20,width:56,height:24,fill:'url(#barber)'});
- flagStripes.style.transition='transform .18s';flagStripes.style.transform='translateX(-58px)';
- svgEl(s,'text',{x:92,y:35,'text-anchor':'middle','font-size':7,'letter-spacing':'.14em',
- 'font-family':'var(--mono)',fill:'var(--steel)'}).textContent='VIB';
- $('wflag').onclick=()=>setFlag(!flagOn);})();
-function setFlag(on){flagOn=on;flagStripes.style.transform=on?'translateX(0)':'translateX(-58px)';
- setRd('R11',on?'FLAG':'clear');}
-setFlag(false);
+/* R10, R11 extracted to widgets.js (GW.*) */
+
/* R13, R17 extracted to widgets.js (GW.*) */
-/* R30 telegraph indicator: the pointer names the state on labeled sectors */
-const TELIND_STATES=['OK','TEST','BUSY','STOP','ATTN','FAULT'];
-let telI=0,telPtr=null;
-(function(){const s=$('telind'),cx=55,cy=55,N=TELIND_STATES.length;
- svgEl(s,'circle',{cx,cy,r:50,fill:'#171412',stroke:'#060505','stroke-width':2});
- svgEl(s,'circle',{cx,cy,r:43,fill:'var(--cream)',stroke:'#b7b29a','stroke-width':1});
- TELIND_STATES.forEach((st,i)=>{
- const a=i*360/N-90+180/N;
- const [lx1,ly1]=polar(cx,cy,9,i*360/N),[lx2,ly2]=polar(cx,cy,43,i*360/N);
- svgEl(s,'line',{x1:lx1,y1:ly1,x2:lx2,y2:ly2,stroke:'#2b2418','stroke-width':1});
- const g=svgEl(s,'g',{transform:`rotate(${a+90},${cx},${cy})`});
- svgEl(g,'text',{x:cx,y:cy-28,'text-anchor':'middle','font-size':7,'font-weight':700,
- 'font-family':'var(--mono)',fill:'#2b2418'}).textContent=st;});
- telPtr=svgEl(s,'g',{});telPtr.style.transition='transform .18s';telPtr.style.transformOrigin=`${cx}px ${cy}px`;
- svgEl(telPtr,'polygon',{points:`${cx-2.5},${cy} ${cx},${cy-38} ${cx+2.5},${cy}`,fill:'var(--fail)',stroke:'#7a2a1a','stroke-width':.5});
- svgEl(telPtr,'rect',{x:cx+1,y:cy-16,width:10,height:7,rx:1,fill:'var(--gold)',stroke:'#7d5c16','stroke-width':.6});
- svgEl(s,'circle',{cx,cy,r:8,fill:'#171412',stroke:'#000'});
- $('telind').onclick=()=>setTelind((telI+1)%TELIND_STATES.length);})();
-function setTelind(i){telI=i;const N=TELIND_STATES.length;
- telPtr.style.transform=`rotate(${i*360/N+180/N}deg)`;
- setRd('R30',TELIND_STATES[i]);}
-setTelind(0);
-
-/* R31 radar sweep: rotating beam, blooming contacts, click to mark */
-let radarA=210,radarSweep=null,radarBlips=[],radarMark=null;
-const RADAR_CONTACTS=[[60,.55],[205,.7],[318,.4]];
-(function(){const s=$('radar'),cx=65,cy=65;
- const defs=svgEl(s,'defs',{});
- 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:'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(--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(--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(--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:'var(--scr-bg2,#0d0803)'});
- for(const r of [15,30,45])
- 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:'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(--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 —';
- $('radar').onclick=()=>{radarMark.textContent='MARK '+String(Math.round(radarA)%360).padStart(3,'0');
- setRd('R31','mark '+String(Math.round(radarA)%360).padStart(3,'0'));};})();
-function tickRadar(){radarA=(radarA+2.6)%360;
- radarSweep.setAttribute('transform',`rotate(${radarA},65,65)`);
- RADAR_CONTACTS.forEach(([bd],i)=>{
- let d=(radarA-bd+360)%360; // degrees since beam passed
- radarBlips[i].setAttribute('opacity',d<200?Math.max(0,.95-d/200):0);});
- radarMark.textContent=radarMark.textContent.startsWith('MARK')?radarMark.textContent:
- 'BEARING '+String(Math.round(radarA)%360).padStart(3,'0');}
-tickRadar();setRd('R31','sweeping');
-if(!matchMedia('(prefers-reduced-motion: reduce)').matches)setInterval(tickRadar,50);
-
-/* R25 fourteen-segment display: the starburst that spells */
-const SEG14_WORDS=['ZOOM','ECHO','TAPE','MOOD','HALL','COMP'];
-const SEG14_MAP={A:['a','b','c','e','f','g1','g2'],C:['a','d','e','f'],
- D:['a','b','c','d','i','l'],E:['a','d','e','f','g1','g2'],H:['b','c','e','f','g1','g2'],
- L:['d','e','f'],M:['b','c','e','f','h','j'],O:['a','b','c','d','e','f'],
- P:['a','b','e','f','g1','g2'],T:['a','i','l'],Z:['a','d','j','k']};
-let seg14I=0,seg14Cells=[];
-(function(){const s=$('seg14');
- svgEl(s,'rect',{x:1,y:1,width:128,height:52,rx:5,fill:'#0a0f08',stroke:'#1c2a16','stroke-width':2});
- // segment shapes in a 22x36 local box (x0,y0 top-left)
- const TH=2.6,W=22,H=36,mid=H/2;
- const segs={
- a:[[2,0],[W-2,0],[W-4,TH],[4,TH]],
- d:[[2,H],[W-2,H],[W-4,H-TH],[4,H-TH]],
- f:[[0,2],[TH,4],[TH,mid-2],[0,mid-1]],
- e:[[0,H-2],[TH,H-4],[TH,mid+2],[0,mid+1]],
- b:[[W,2],[W-TH,4],[W-TH,mid-2],[W,mid-1]],
- c:[[W,H-2],[W-TH,H-4],[W-TH,mid+2],[W,mid+1]],
- g1:[[2,mid],[4,mid-TH/2],[W/2-1,mid-TH/2],[W/2-1,mid+TH/2],[4,mid+TH/2]],
- g2:[[W-2,mid],[W-4,mid-TH/2],[W/2+1,mid-TH/2],[W/2+1,mid+TH/2],[W-4,mid+TH/2]],
- i:[[W/2-TH/2,3],[W/2+TH/2,3],[W/2+TH/2,mid-2],[W/2-TH/2,mid-2]],
- l:[[W/2-TH/2,H-3],[W/2+TH/2,H-3],[W/2+TH/2,mid+2],[W/2-TH/2,mid+2]],
- h:[[3,3],[5.5,3],[W/2-2,mid-3],[W/2-4.5,mid-3]],
- j:[[W-3,3],[W-5.5,3],[W/2+2,mid-3],[W/2+4.5,mid-3]],
- k:[[3,H-3],[5.5,H-3],[W/2-2,mid+3],[W/2-4.5,mid+3]],
- m:[[W-3,H-3],[W-5.5,H-3],[W/2+2,mid+3],[W/2+4.5,mid+3]]};
- for(let d=0;d<4;d++){const x0=13+d*28,y0=9,cell={};
- for(const k in segs){
- cell[k]=svgEl(s,'polygon',{points:segs[k].map(p=>`${x0+p[0]},${y0+p[1]}`).join(' '),
- fill:'var(--sevoff)'});}
- seg14Cells.push(cell);}
- $('seg14').onclick=()=>setSeg14((seg14I+1)%SEG14_WORDS.length);})();
-function setSeg14(i){seg14I=i;const word=SEG14_WORDS[i].padEnd(4,' ');
- seg14Cells.forEach((cell,d)=>{const lit=SEG14_MAP[word[d]]||[];
- for(const k in cell){const on=lit.includes(k);
- cell[k].setAttribute('fill',on?'var(--sevgrn)':'var(--sevoff)');
- cell[k].setAttribute('style',on?'filter:drop-shadow(0 0 2.5px rgba(87,211,87,.6))':'');}});
- setRd('R25',SEG14_WORDS[i]);}
-setSeg14(0);
-
-/* R26 response graph: log-frequency axes, a draggable amber peak */
-const RG={x0:30,x1:182,y0:8,y1:86,fLo:Math.log10(32),fHi:Math.log10(16000),db:12};
-let rgFc=1200,rgGain=6.5,rgCurve=null,rgGlowLine=null,rgHandle=null;
-(function(){const s=$('rgraph');
- svgEl(s,'rect',{x:1,y:1,width:188,height:108,rx:5,fill:'#0d1410',stroke:'#22301f','stroke-width':2});
- const FREQS=[32,64,128,250,500,1000,2000,4000,8000,16000];
- FREQS.forEach(f=>{const x=RG.x0+(Math.log10(f)-RG.fLo)/(RG.fHi-RG.fLo)*(RG.x1-RG.x0);
- svgEl(s,'line',{x1:x,y1:RG.y0,x2:x,y2:RG.y1,stroke:'#22301f','stroke-width':.6});
- svgEl(s,'text',{x,y:RG.y1+9,'text-anchor':'middle','font-size':4.8,'font-family':'var(--mono)',
- fill:'var(--steel)'}).textContent=f<1000?f:(f/1000)+'k';});
- for(const db of [12,6,0,-6,-12]){const y=RG.y0+(RG.db-db)/(2*RG.db)*(RG.y1-RG.y0);
- svgEl(s,'line',{x1:RG.x0,y1:y,x2:RG.x1,y2:y,stroke:'#22301f','stroke-width':db===0?1:.6});
- svgEl(s,'text',{x:RG.x0-4,y:y+2,'text-anchor':'end','font-size':4.8,'font-family':'var(--mono)',
- fill:'var(--steel)'}).textContent=(db>0?'+':'')+db;}
- rgGlowLine=svgEl(s,'polyline',{points:'',fill:'none',stroke:'var(--gold-hi)','stroke-width':3,
- opacity:.35,filter:'url(#avGlow)'});
- rgCurve=svgEl(s,'polyline',{points:'',fill:'none',stroke:'var(--gold)','stroke-width':1.6});
- rgHandle=svgEl(s,'circle',{r:4,fill:'var(--gold-hi)',stroke:'#7d5c16','stroke-width':1});
- rgHandle.style.cursor='move';
- // 2D drag: the handle (and the plot) place the peak in both axes at once
- s.style.touchAction='none';
- s.addEventListener('pointerdown',e=>{s.setPointerCapture(e.pointerId);
- const move=ev=>{const r=s.getBoundingClientRect(),sx=190/r.width,sy=110/r.height;
- const px=(ev.clientX-r.left)*sx,py=(ev.clientY-r.top)*sy;
- const lf=RG.fLo+Math.max(0,Math.min(1,(px-RG.x0)/(RG.x1-RG.x0)))*(RG.fHi-RG.fLo);
- const g=RG.db-Math.max(0,Math.min(1,(py-RG.y0)/(RG.y1-RG.y0)))*2*RG.db;
- setRgraph(Math.pow(10,lf),g);};
- move(e);
- const up=()=>{s.removeEventListener('pointermove',move);s.removeEventListener('pointerup',up);s.removeEventListener('pointercancel',up);};
- s.addEventListener('pointermove',move);s.addEventListener('pointerup',up);s.addEventListener('pointercancel',up);
- e.preventDefault();});})();
-function setRgraph(fc,gain){rgFc=Math.max(32,Math.min(16000,fc));rgGain=Math.max(-12,Math.min(12,gain));
- const xOf=f=>RG.x0+(Math.log10(f)-RG.fLo)/(RG.fHi-RG.fLo)*(RG.x1-RG.x0);
- const yOf=db=>RG.y0+(RG.db-db)/(2*RG.db)*(RG.y1-RG.y0);
- const sigma=0.28;let pts='';
- for(let i=0;i<=76;i++){const lf=RG.fLo+i/76*(RG.fHi-RG.fLo);
- const db=rgGain*Math.exp(-Math.pow(lf-Math.log10(rgFc),2)/(2*sigma*sigma));
- pts+=`${xOf(Math.pow(10,lf)).toFixed(1)},${yOf(db).toFixed(1)} `;}
- rgCurve.setAttribute('points',pts.trim());rgGlowLine.setAttribute('points',pts.trim());
- rgHandle.setAttribute('cx',xOf(rgFc));rgHandle.setAttribute('cy',yOf(rgGain));
- const fLbl=rgFc<1000?Math.round(rgFc)+' Hz':(rgFc/1000).toFixed(1)+' kHz';
- setRd('R26',`${fLbl} · ${rgGain>=0?'+':''}${rgGain.toFixed(1)} dB`);}
-setRgraph(1200,6.5);
+/* R25, R26, R30, R31 extracted to widgets.js (GW.*) */
+
+
+
/* ============ LIVE SIGNAL LOOPS ============ */
/* extracted meters: the page keeps the clock + demo signal, drives card handles */
@@ -2371,10 +2173,10 @@ let dkPos=0,dkCarry=0;
/* ---- 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('R10',$('card-R10').gw.el,['amber','green','red','blue','vfd'],'amber');
screenChips('R17',$('card-R17').gw.el,['green','amber','red','blue','vfd'],'green');
screenChips('R19',$('card-R19').gw.el,['white','green','amber','blue','vfd'],'white');
-screenChips('R31',$('radar'),['amber','green','red','blue','vfd'],'amber');
+screenChips('R31',$('card-R31').gw.el,['amber','green','red','blue','vfd'],'amber');
screenChips('N11',$('card-N11').gw.el,['green','amber','red','blue','vfd'],'green');
BOOST.forEach(no=>{const rd=document.getElementById('rd-'+no);
if(rd)rd.closest('.card').querySelector('.stagew').classList.add('boost');});
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js
index f3e1127..4cd25eb 100644
--- a/docs/prototypes/widgets.js
+++ b/docs/prototypes/widgets.js
@@ -3207,6 +3207,320 @@ GW.patchBay = function (host, opts = {}) {
return { el: patch, get: () => conns.slice(), set: c => { conns = c.slice(); draw(); } };
};
+/* R10 data matrix readout — a page of labeled amber fields; click cycles pages;
+ a page marked live re-renders on the builder's own 1s clock (reduced-motion gated) */
+GW.dataMatrix = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const pages = opts.pages || [
+ ['SYS', () => ['CPU 42 MEM 61', 'DSK 58 TMP 47C', 'ARCH 6.18 LTS']],
+ ['NET', () => ['WIFI @HYATT 300M', 'VPN UP DNS OK', 'BT 2 PAIRED']],
+ ['TIME', () => {
+ const d = new Date(), p = n => String(n).padStart(2, '0');
+ return [`${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`,
+ `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`,
+ 'UP 14D 06:12'];
+ }, true]];
+ const s = stageSvg(host, 'rsvg press', 190, 74);
+ def('dmxGlow', d => {
+ const fl = svgEl(d, 'filter', { id: 'dmxGlow', x: '-30%', y: '-30%', width: '160%', height: '160%' });
+ svgEl(fl, 'feGaussianBlur', { in: 'SourceGraphic', stdDeviation: 1.1 });
+ });
+ /* the background gradient reads screen-family vars from this widget's subtree,
+ so it lives in the local defs, not the shared def sink */
+ const bgId = uid('dmxBg');
+ const defs = svgEl(s, 'defs', {});
+ const bg = svgEl(defs, 'linearGradient', { id: bgId, x1: 0, y1: 0, x2: 0, y2: 1 });
+ 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(#${bgId})`, stroke: '#2c261d' });
+ const lines = [];
+ 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(--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(--scr-hi,var(--gold-hi))'
+ });
+ lines.push([glow, crisp]);
+ }
+ let pi;
+ const set = i => {
+ pi = ((i % pages.length) + pages.length) % pages.length;
+ const [name, rowsFn] = pages[pi];
+ rowsFn().forEach((txt, r) => { lines[r][0].textContent = txt; lines[r][1].textContent = txt; });
+ onChange(pi, 'page ' + name);
+ };
+ s.addEventListener('click', () => set(pi + 1));
+ set(opts.page || 0);
+ if (!matchMedia('(prefers-reduced-motion: reduce)').matches)
+ setInterval(() => { if (pages[pi][2]) set(pi); }, 1000);
+ return { el: s, get: () => pi, set };
+};
+
+/* R11 warning flag window — the striped mechanical flag slides into the window
+ when the condition trips; click trips and clears it */
+GW.warningFlag = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const s = stageSvg(host, 'rsvg press', 120, 64);
+ def('barber', d => {
+ const pat = svgEl(d, 'pattern', { id: 'barber', width: 10, height: 10, patternUnits: 'userSpaceOnUse', patternTransform: 'rotate(45)' });
+ svgEl(pat, 'rect', { x: 0, y: 0, width: 10, height: 10, fill: '#17130c' });
+ svgEl(pat, 'rect', { x: 0, y: 0, width: 5, height: 10, fill: 'var(--gold)' });
+ });
+ const clipId = uid('flagWin');
+ const defs = svgEl(s, 'defs', {});
+ const clip = svgEl(defs, 'clipPath', { id: clipId });
+ svgEl(clip, 'rect', { x: 14, y: 20, width: 56, height: 24 });
+ svgEl(s, 'rect', { x: 11, y: 17, width: 62, height: 30, rx: 3, fill: '#0a0908', stroke: '#2c261d', 'stroke-width': 2 });
+ const g = svgEl(s, 'g', { 'clip-path': `url(#${clipId})` });
+ const stripes = svgEl(g, 'rect', { x: 14, y: 20, width: 56, height: 24, fill: 'url(#barber)' });
+ stripes.style.transition = 'transform .18s'; stripes.style.transform = 'translateX(-58px)';
+ svgEl(s, 'text', {
+ x: 92, y: 35, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.14em',
+ 'font-family': 'var(--mono)', fill: 'var(--steel)'
+ }).textContent = opts.label || 'VIB';
+ let on;
+ const set = v => { on = !!v; stripes.style.transform = on ? 'translateX(0)' : 'translateX(-58px)'; onChange(on, on ? 'FLAG' : 'clear'); };
+ s.addEventListener('click', () => set(!on));
+ set(opts.on || false);
+ return { el: s, get: () => on, set };
+};
+
+/* R25 fourteen-segment display — the starburst alphanumeric that spells words;
+ click cycles the word */
+GW.seg14 = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const words = opts.words || ['ZOOM', 'ECHO', 'TAPE', 'MOOD', 'HALL', 'COMP'];
+ const MAP = {
+ A: ['a', 'b', 'c', 'e', 'f', 'g1', 'g2'], C: ['a', 'd', 'e', 'f'],
+ D: ['a', 'b', 'c', 'd', 'i', 'l'], E: ['a', 'd', 'e', 'f', 'g1', 'g2'], H: ['b', 'c', 'e', 'f', 'g1', 'g2'],
+ L: ['d', 'e', 'f'], M: ['b', 'c', 'e', 'f', 'h', 'j'], O: ['a', 'b', 'c', 'd', 'e', 'f'],
+ P: ['a', 'b', 'e', 'f', 'g1', 'g2'], T: ['a', 'i', 'l'], Z: ['a', 'd', 'j', 'k']
+ };
+ const s = stageSvg(host, 'rsvg press', 130, 54);
+ svgEl(s, 'rect', { x: 1, y: 1, width: 128, height: 52, rx: 5, fill: '#0a0f08', stroke: '#1c2a16', 'stroke-width': 2 });
+ /* segment shapes in a 22x36 local box */
+ const TH = 2.6, W = 22, H = 36, mid = H / 2;
+ const segs = {
+ a: [[2, 0], [W - 2, 0], [W - 4, TH], [4, TH]],
+ d: [[2, H], [W - 2, H], [W - 4, H - TH], [4, H - TH]],
+ f: [[0, 2], [TH, 4], [TH, mid - 2], [0, mid - 1]],
+ e: [[0, H - 2], [TH, H - 4], [TH, mid + 2], [0, mid + 1]],
+ b: [[W, 2], [W - TH, 4], [W - TH, mid - 2], [W, mid - 1]],
+ c: [[W, H - 2], [W - TH, H - 4], [W - TH, mid + 2], [W, mid + 1]],
+ g1: [[2, mid], [4, mid - TH / 2], [W / 2 - 1, mid - TH / 2], [W / 2 - 1, mid + TH / 2], [4, mid + TH / 2]],
+ g2: [[W - 2, mid], [W - 4, mid - TH / 2], [W / 2 + 1, mid - TH / 2], [W / 2 + 1, mid + TH / 2], [W - 4, mid + TH / 2]],
+ i: [[W / 2 - TH / 2, 3], [W / 2 + TH / 2, 3], [W / 2 + TH / 2, mid - 2], [W / 2 - TH / 2, mid - 2]],
+ l: [[W / 2 - TH / 2, H - 3], [W / 2 + TH / 2, H - 3], [W / 2 + TH / 2, mid + 2], [W / 2 - TH / 2, mid + 2]],
+ h: [[3, 3], [5.5, 3], [W / 2 - 2, mid - 3], [W / 2 - 4.5, mid - 3]],
+ j: [[W - 3, 3], [W - 5.5, 3], [W / 2 + 2, mid - 3], [W / 2 + 4.5, mid - 3]],
+ k: [[3, H - 3], [5.5, H - 3], [W / 2 - 2, mid + 3], [W / 2 - 4.5, mid + 3]],
+ m: [[W - 3, H - 3], [W - 5.5, H - 3], [W / 2 + 2, mid + 3], [W / 2 + 4.5, mid + 3]]
+ };
+ const cells = [];
+ for (let d = 0; d < 4; d++) {
+ const x0 = 13 + d * 28, y0 = 9, cell = {};
+ for (const k in segs) {
+ cell[k] = svgEl(s, 'polygon', { points: segs[k].map(p => `${x0 + p[0]},${y0 + p[1]}`).join(' '), fill: 'var(--sevoff)' });
+ }
+ cells.push(cell);
+ }
+ let wi;
+ const set = i => {
+ wi = ((i % words.length) + words.length) % words.length;
+ const word = words[wi].padEnd(4, ' ');
+ cells.forEach((cell, d) => {
+ const lit = MAP[word[d]] || [];
+ for (const k in cell) {
+ const on = lit.includes(k);
+ cell[k].setAttribute('fill', on ? 'var(--sevgrn)' : 'var(--sevoff)');
+ cell[k].setAttribute('style', on ? 'filter:drop-shadow(0 0 2.5px rgba(87,211,87,.6))' : '');
+ }
+ });
+ onChange(wi, words[wi]);
+ };
+ s.addEventListener('click', () => set(wi + 1));
+ set(opts.index || 0);
+ return { el: s, get: () => wi, set };
+};
+
+/* R26 response graph — log-frequency axes with a draggable amber peak; 2D drag
+ places the peak in both axes at once */
+GW.responseGraph = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const s = stageSvg(host, 'rsvg', 190, 110);
+ const RG = { x0: 30, x1: 182, y0: 8, y1: 86, fLo: Math.log10(32), fHi: Math.log10(16000), db: 12 };
+ def('avGlow', d => {
+ const fl = svgEl(d, 'filter', { id: 'avGlow', x: '-60%', y: '-60%', width: '220%', height: '220%' });
+ svgEl(fl, 'feGaussianBlur', { in: 'SourceGraphic', stdDeviation: 1.4 });
+ });
+ svgEl(s, 'rect', { x: 1, y: 1, width: 188, height: 108, rx: 5, fill: '#0d1410', stroke: '#22301f', 'stroke-width': 2 });
+ const FREQS = [32, 64, 128, 250, 500, 1000, 2000, 4000, 8000, 16000];
+ const xOf = f => RG.x0 + (Math.log10(f) - RG.fLo) / (RG.fHi - RG.fLo) * (RG.x1 - RG.x0);
+ const yOf = db => RG.y0 + (RG.db - db) / (2 * RG.db) * (RG.y1 - RG.y0);
+ FREQS.forEach(f => {
+ const x = xOf(f);
+ svgEl(s, 'line', { x1: x, y1: RG.y0, x2: x, y2: RG.y1, stroke: '#22301f', 'stroke-width': .6 });
+ svgEl(s, 'text', {
+ x, y: RG.y1 + 9, 'text-anchor': 'middle', 'font-size': 4.8, 'font-family': 'var(--mono)',
+ fill: 'var(--steel)'
+ }).textContent = f < 1000 ? f : (f / 1000) + 'k';
+ });
+ for (const db of [12, 6, 0, -6, -12]) {
+ const y = yOf(db);
+ svgEl(s, 'line', { x1: RG.x0, y1: y, x2: RG.x1, y2: y, stroke: '#22301f', 'stroke-width': db === 0 ? 1 : .6 });
+ svgEl(s, 'text', {
+ x: RG.x0 - 4, y: y + 2, 'text-anchor': 'end', 'font-size': 4.8, 'font-family': 'var(--mono)',
+ fill: 'var(--steel)'
+ }).textContent = (db > 0 ? '+' : '') + db;
+ }
+ const glowLine = svgEl(s, 'polyline', { points: '', fill: 'none', stroke: 'var(--gold-hi)', 'stroke-width': 3, opacity: .35, filter: 'url(#avGlow)' });
+ const curve = svgEl(s, 'polyline', { points: '', fill: 'none', stroke: 'var(--gold)', 'stroke-width': 1.6 });
+ const handle = svgEl(s, 'circle', { r: 4, fill: 'var(--gold-hi)', stroke: '#7d5c16', 'stroke-width': 1 });
+ handle.style.cursor = 'move';
+ let fc, gain;
+ const set = (f, g) => {
+ fc = Math.max(32, Math.min(16000, f)); gain = Math.max(-12, Math.min(12, g));
+ const sigma = 0.28; let pts = '';
+ for (let i = 0; i <= 76; i++) {
+ const lf = RG.fLo + i / 76 * (RG.fHi - RG.fLo);
+ const db = gain * Math.exp(-Math.pow(lf - Math.log10(fc), 2) / (2 * sigma * sigma));
+ pts += `${xOf(Math.pow(10, lf)).toFixed(1)},${yOf(db).toFixed(1)} `;
+ }
+ curve.setAttribute('points', pts.trim()); glowLine.setAttribute('points', pts.trim());
+ handle.setAttribute('cx', xOf(fc)); handle.setAttribute('cy', yOf(gain));
+ const fLbl = fc < 1000 ? Math.round(fc) + ' Hz' : (fc / 1000).toFixed(1) + ' kHz';
+ onChange([fc, gain], `${fLbl} · ${gain >= 0 ? '+' : ''}${gain.toFixed(1)} dB`);
+ };
+ /* 2D drag: the plot places the peak in both axes at once */
+ s.style.touchAction = 'none';
+ s.addEventListener('pointerdown', e => {
+ s.setPointerCapture(e.pointerId);
+ const move = ev => {
+ const r = s.getBoundingClientRect(), sx = 190 / r.width, sy = 110 / r.height;
+ const px = (ev.clientX - r.left) * sx, py = (ev.clientY - r.top) * sy;
+ const lf = RG.fLo + Math.max(0, Math.min(1, (px - RG.x0) / (RG.x1 - RG.x0))) * (RG.fHi - RG.fLo);
+ const g = RG.db - Math.max(0, Math.min(1, (py - RG.y0) / (RG.y1 - RG.y0))) * 2 * RG.db;
+ set(Math.pow(10, lf), g);
+ };
+ move(e);
+ const up = () => { s.removeEventListener('pointermove', move); s.removeEventListener('pointerup', up); s.removeEventListener('pointercancel', up); };
+ s.addEventListener('pointermove', move); s.addEventListener('pointerup', up); s.addEventListener('pointercancel', up);
+ e.preventDefault();
+ });
+ set(opts.fc !== undefined ? opts.fc : 1200, opts.gain !== undefined ? opts.gain : 6.5);
+ return { el: s, get: () => [fc, gain], set };
+};
+
+/* R30 telegraph indicator — the pointer names the active state on labeled
+ sectors, engine-telegraph style; click steps the state */
+GW.telegraphIndicator = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const states = opts.states || ['OK', 'TEST', 'BUSY', 'STOP', 'ATTN', 'FAULT'];
+ const s = stageSvg(host, 'rsvg press', 110, 110), cx = 55, cy = 55, N = states.length;
+ svgEl(s, 'circle', { cx, cy, r: 50, fill: '#171412', stroke: '#060505', 'stroke-width': 2 });
+ svgEl(s, 'circle', { cx, cy, r: 43, fill: 'var(--cream)', stroke: '#b7b29a', 'stroke-width': 1 });
+ states.forEach((st, i) => {
+ const a = i * 360 / N - 90 + 180 / N;
+ const [lx1, ly1] = polar(cx, cy, 9, i * 360 / N), [lx2, ly2] = polar(cx, cy, 43, i * 360 / N);
+ svgEl(s, 'line', { x1: lx1, y1: ly1, x2: lx2, y2: ly2, stroke: '#2b2418', 'stroke-width': 1 });
+ const g = svgEl(s, 'g', { transform: `rotate(${a + 90},${cx},${cy})` });
+ svgEl(g, 'text', {
+ x: cx, y: cy - 28, 'text-anchor': 'middle', 'font-size': 7, 'font-weight': 700,
+ 'font-family': 'var(--mono)', fill: '#2b2418'
+ }).textContent = st;
+ });
+ const ptr = svgEl(s, 'g', {}); ptr.style.transition = 'transform .18s'; ptr.style.transformOrigin = `${cx}px ${cy}px`;
+ svgEl(ptr, 'polygon', { points: `${cx - 2.5},${cy} ${cx},${cy - 38} ${cx + 2.5},${cy}`, fill: 'var(--fail)', stroke: '#7a2a1a', 'stroke-width': .5 });
+ svgEl(ptr, 'rect', { x: cx + 1, y: cy - 16, width: 10, height: 7, rx: 1, fill: 'var(--gold)', stroke: '#7d5c16', 'stroke-width': .6 });
+ svgEl(s, 'circle', { cx, cy, r: 8, fill: '#171412', stroke: '#000' });
+ let si;
+ const set = i => {
+ si = ((i % N) + N) % N;
+ ptr.style.transform = `rotate(${si * 360 / N + 180 / N}deg)`;
+ onChange(si, states[si]);
+ };
+ s.addEventListener('click', () => set(si + 1));
+ set(opts.index || 0);
+ return { el: s, get: () => si, set };
+};
+
+/* R31 radar sweep — the beam circles the bearing ring on the builder's own
+ clock (reduced-motion gated), contacts bloom as it passes; click marks the bearing */
+GW.radarSweep = function (host, opts = {}) {
+ const onChange = opts.onChange || noop;
+ const contacts = opts.contacts || [[60, .55], [205, .7], [318, .4]];
+ const s = stageSvg(host, 'rsvg press', 130, 130), cx = 65, cy = 65;
+ const clipId = uid('radarClip');
+ const defs = svgEl(s, 'defs', {});
+ const clip = svgEl(defs, 'clipPath', { id: clipId });
+ 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: 'var(--scr-bg1,#120b05)', stroke: 'var(--scr-brd,#3a2a12)', 'stroke-width': 1 });
+ for (let d = 0; d < 360; d += 30) {
+ const [x1, y1] = polar(cx, cy, 51, d), [x2, y2] = polar(cx, cy, 47, d);
+ 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(${d},${cx},${cy})` });
+ svgEl(g, 'text', {
+ x: cx, y: cy - 47.5, 'text-anchor': 'middle', 'font-size': 4.6, 'font-family': 'var(--mono)',
+ 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(--scr-ink,var(--gold))', 'stroke-width': .6, opacity: .5 });
+ }
+ const g = svgEl(s, 'g', { 'clip-path': `url(#${clipId})` });
+ 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: 'var(--scr-ink,var(--gold))', 'stroke-opacity': .18, 'stroke-width': .7 });
+ const sweep = 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(sweep, 'path', {
+ d: `M ${cx} ${cy} L ${x1} ${y1} A 46 46 0 0 1 ${x2} ${y2} Z`,
+ fill: 'var(--scr-ink,var(--gold))', 'fill-opacity': (0.04 + t * 0.045).toFixed(3)
+ });
+ }
+ svgEl(sweep, '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
+ });
+ const blips = contacts.map(([bd, rf]) => {
+ const [x, y] = polar(cx, cy, 46 * rf, bd);
+ return 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 });
+ const mark = svgEl(s, 'text', {
+ x: cx, y: 124, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.12em',
+ 'font-family': 'var(--mono)', fill: 'var(--steel)'
+ });
+ mark.textContent = 'BEARING —';
+ let a = opts.bearing !== undefined ? opts.bearing : 210;
+ const tick = () => {
+ a = (a + 2.6) % 360;
+ sweep.setAttribute('transform', `rotate(${a},${cx},${cy})`);
+ contacts.forEach(([bd], i) => {
+ const d = (a - bd + 360) % 360; /* degrees since beam passed */
+ blips[i].setAttribute('opacity', d < 200 ? Math.max(0, .95 - d / 200) : 0);
+ });
+ mark.textContent = mark.textContent.startsWith('MARK') ? mark.textContent :
+ 'BEARING ' + String(Math.round(a) % 360).padStart(3, '0');
+ };
+ s.addEventListener('click', () => {
+ const b = String(Math.round(a) % 360).padStart(3, '0');
+ mark.textContent = 'MARK ' + b;
+ onChange(a, 'mark ' + b);
+ });
+ tick(); onChange(a, 'sweeping');
+ if (!matchMedia('(prefers-reduced-motion: reduce)').matches) setInterval(tick, 50);
+ return { el: s, get: () => a, tick };
+};
+
/* ---- widget CSS: injected once, grows as builders move in ---- */
const GW_CSS = ``;
function ensureCss() {
diff --git a/tests/gallery-probes/probe-fams.mjs b/tests/gallery-probes/probe-fams.mjs
index 35671db..99b4e92 100644
--- a/tests/gallery-probes/probe-fams.mjs
+++ b/tests/gallery-probes/probe-fams.mjs
@@ -20,19 +20,19 @@ ok('no exceptions on load', events.filter(e => e.method === 'Runtime.exceptionTh
ok('5 screen chip rows', await evl(`[...document.querySelectorAll('.famchips .lab')].filter(l=>l.textContent==='screen').length`) === 5);
// default fallback: dmx ink computes to gold-hi rgb(255,190,84)
-const dmxFill0 = await evl(`getComputedStyle(document.querySelector('#dmx text')).fill`);
+const dmxFill0 = await evl(`getComputedStyle(document.querySelector('#card-R10 svg text')).fill`);
ok('R10 default ink = gold-hi', dmxFill0 === 'rgb(255, 190, 84)', dmxFill0);
// click green chip on R10 -> ink becomes phos rgb(127,224,160)
-await evl(`document.querySelector('#dmx').closest('.card').querySelector('.fc[title="green"]').click()`);
-const dmxFill1 = await evl(`getComputedStyle(document.querySelector('#dmx text')).fill`);
+await evl(`document.querySelector('#card-R10').querySelector('.fc[title="green"]').click()`);
+const dmxFill1 = await evl(`getComputedStyle(document.querySelector('#card-R10 svg text')).fill`);
ok('R10 green chip recolors ink', dmxFill1 === 'rgb(127, 224, 160)', dmxFill1);
// radar: default sweep line = gold-hi; click green -> phos
-const swSel = `document.querySelector('#radar').closest('.card')`;
-const line0 = await evl(`getComputedStyle(document.querySelectorAll('#radar line')[0]).stroke`);
+const swSel = `document.querySelector('#card-R31')`;
+const line0 = await evl(`getComputedStyle(document.querySelectorAll('#card-R31 svg line')[0]).stroke`);
await evl(`${swSel}.querySelector('.fc[title="green"]').click()`);
-const line1 = await evl(`getComputedStyle(document.querySelectorAll('#radar line')[0]).stroke`);
+const line1 = await evl(`getComputedStyle(document.querySelectorAll('#card-R31 svg line')[0]).stroke`);
ok('R31 green chip recolors furniture', line0 !== line1 && line1 === 'rgb(88, 184, 126)', `${line0} -> ${line1}`);
// scope (CSS widget): trace stroke changes on amber chip
@@ -55,7 +55,7 @@ ok('R17 amber chip retints face', face === 'rgb(216, 203, 166)', face);
ok('no exceptions after chip clicks', events.filter(e => e.method === 'Runtime.exceptionThrown').length === 0);
// screenshot the recolored screens region (scroll R31 into view)
-await evl(`document.querySelector('#radar').scrollIntoView({block:'center'})`);
+await evl(`document.querySelector('#card-R31 svg').scrollIntoView({block:'center'})`);
await sleep(400);
const shot = await send('Page.captureScreenshot', { format: 'png' });
writeFileSync('fams.png', Buffer.from(shot.result.data, 'base64'));