SOURCE / PINNED RELEASE
Made of little things.
Freeze Frame Response
- Release
- 3e45de6048a3…
- Author-recorded commit
- d04885dad630…
- License
- Check the original source
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-ba52e30d5f2
The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Freeze Frame Response</title>
<script type="module" crossorigin>var He=Object.defineProperty;var qe=(e,t,n)=>t in e?He(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var C=(e,t,n)=>qe(e,typeof t!="symbol"?t+"":t,n);function Ue(){const e=window;if(!e.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return e.napplet}function w(e){const n=Ue()[e];if(!n)throw new Error(`window.napplet.${String(e)} is unavailable -- runtime did not inject this domain`);return n}var ie={getItem(e){return w("storage").getItem(e)},setItem(e,t){return w("storage").setItem(e,t)},removeItem(e){return w("storage").removeItem(e)},keys(){return w("storage").keys()},instance:{getItem(e){return w("storage").instance.getItem(e)},setItem(e,t){return w("storage").instance.setItem(e,t)},removeItem(e){return w("storage").instance.removeItem(e)},keys(){return w("storage").instance.keys()}}},ne={get(){return w("config").get()},subscribe(e){return w("config").subscribe(e)},openSettings(e){w("config").openSettings(e)},registerSchema(e,t){return w("config").registerSchema(e,t)},onSchemaError(e){return w("config").onSchemaError(e)},get schema(){return w("config").schema}};function Ve(e,t){return!!(e!=null&&e[t])}function le(e){return Ve(window.napplet,e)}const Ke="http://json-schema.org/draft-07/schema#",Ye=1,_e="object",Xe=!1,Ze={controlSize:{type:"integer",title:"Control height",description:"Height of the equipment dock controls in pixels. Compact for small frames, larger for touch.",minimum:34,maximum:56,default:46,"x-napplet-section":"Appearance","x-napplet-order":1},trajectoryHints:{type:"boolean",title:"Show trajectory hints",description:"Draw dotted projections while time is frozen. Turn off for a harder read.",default:!0,"x-napplet-section":"Assistance","x-napplet-order":1}},Je={$schema:Ke,$version:Ye,type:_e,additionalProperties:Xe,properties:Ze};le("config")&&(async()=>{ne.schema||await ne.registerSchema(Je);const e=ne.subscribe(t=>{const n=document.documentElement,i=typeof t.controlSize=="number"?Math.min(56,Math.max(34,t.controlSize)):46;n.style.setProperty("--control-h",`${i}px`),n.dataset.trajectoryHints=t.trajectoryHints===!1?"false":"true"});window.addEventListener("pagehide",()=>e.close(),{once:!0})})().catch(()=>{});const ae=700,f=520,g=960,L=600,P=(e,t,n,i)=>({kind:"pedestrian",x:e,y:t,vx:n,vy:i,radius:10}),c=(e,t,n,i,a)=>({kind:"debris",material:e,x:t,y:n,vx:i,vy:a,radius:e==="glass"?7:e==="furniture"?9:8}),B=(e,t,n,i)=>({kind:"volatile",x:e,y:t,vx:n,vy:i,radius:11}),S=[{id:"kettle",name:"The Kettle Incident",tagline:"Flat 2B, 07:12. A domestic appliance has overreached.",briefing:"A pressure cooker has removed itself from Flat 2B. One resident and four pieces of municipal debris are airborne. Mattress on the left, recycling on the right. You have four items of equipment and the authority to use them.",hint:"A trampoline under the resident will move them toward the mattress. A tilted funnel can nudge stubborn debris.",freezeSeconds:.8,source:{x:480,y:340,power:300},bodies:[P(450,320,-150,-300),c("masonry",500,320,150,-260),c("masonry",490,330,90,-280),c("furniture",505,330,230,-210),c("glass",520,320,330,-310)],mattresses:[{id:"m1",x1:40,x2:180,label:"MATTRESS 1"}],bins:[{id:"masonry",material:"masonry",x1:600,x2:710},{id:"furniture",material:"furniture",x1:730,x2:840},{id:"glass",material:"glass",x1:860,x2:950}],kit:{trampoline:2,funnel:1,cushion:1}},{id:"wedding",name:"The Wrong Wedding",tagline:"Rooftop reception, 16:40. The cake was not the problem.",briefing:"A caterer has ignited the celebratory gas ring. Two guests and five items of wedding furniture are in the air. The gas canister is still travelling and remains enthusiastic about secondary explosions.",hint:"Cushions kill momentum. Funnels preserve it but change its career path.",freezeSeconds:.9,source:{x:470,y:330,power:330},bodies:[P(450,310,-120,-330),P(460,310,-210,-240),c("masonry",500,310,60,-300),c("masonry",455,310,-80,-260),c("furniture",510,310,170,-250),c("furniture",530,320,240,-230),c("glass",490,310,320,-290),B(540,330,190,-330)],mattresses:[{id:"m1",x1:30,x2:150,label:"MATTRESS 1"},{id:"m2",x1:170,x2:290,label:"MATTRESS 2"}],bins:[{id:"masonry",material:"masonry",x1:545,x2:655},{id:"furniture",material:"furniture",x1:690,x2:800},{id:"glass",material:"glass",x1:850,x2:950}],kit:{trampoline:3,funnel:2,cushion:1}},{id:"piano",name:"Grand Piano, Minor Key",tagline:"Conservatoire, 11:05. The recital is now site-specific.",briefing:"The old boiler took issue with the allegro. Three audience members and six assorted objects are in the air, plus one gas cylinder from the catering tent. Preserve the soft landing zones; the bins are fussy about materials.",hint:"Place equipment early in a trajectory, not on top of the target. Let physics do the paperwork.",freezeSeconds:.75,source:{x:500,y:320,power:340},bodies:[P(470,300,-260,-340),P(480,300,-40,-300),P(500,300,-80,-240),c("masonry",520,310,120,-280),c("masonry",460,320,-170,-300),c("furniture",490,310,250,-300),c("furniture",530,320,170,-200),c("glass",510,310,340,-330),c("glass",480,330,400,-260),B(530,330,200,-300)],mattresses:[{id:"m1",x1:30,x2:130,label:"MATTRESS 1"},{id:"m2",x1:330,x2:430,label:"MATTRESS 2"}],bins:[{id:"masonry-left",material:"masonry",x1:210,x2:310},{id:"masonry",material:"masonry",x1:650,x2:750},{id:"furniture",material:"furniture",x1:760,x2:850},{id:"glass",material:"glass",x1:870,x2:950}],kit:{trampoline:3,funnel:2,cushion:2}},{id:"flatpack",name:"IKEA Aftermath",tagline:"Showroom annex, 14:25. Some assembly was required.",briefing:"A pallet of flat-pack bookcases has achieved self-assembly at speed. Three shoppers are airborne, along with eight material samples and two gas canisters. The blue bin does not accept masonry, however confident it looks.",hint:"Chain reactions are not covered by the standard insurance form. Use them or regret them.",freezeSeconds:.85,source:{x:480,y:310,power:360},bodies:[P(450,300,-200,-320),P(470,300,-140,-300),P(500,300,-10,-280),c("masonry",490,310,130,-290),c("masonry",500,310,210,-250),c("masonry",470,310,-60,-260),c("furniture",510,310,300,-300),c("furniture",520,310,360,-260),c("furniture",480,310,-220,-300),c("glass",500,310,260,-340),c("glass",520,300,440,-240),B(530,320,150,-320),B(460,320,-120,-310)],mattresses:[{id:"m1",x1:40,x2:140,label:"MATTRESS 1"},{id:"m2",x1:200,x2:300,label:"MATTRESS 2"},{id:"m3",x1:360,x2:460,label:"MATTRESS 3"}],bins:[{id:"masonry-left",material:"masonry",x1:470,x2:570},{id:"masonry",material:"masonry",x1:580,x2:680},{id:"furniture",material:"furniture",x1:700,x2:800},{id:"glass",material:"glass",x1:830,x2:930}],kit:{trampoline:4,funnel:2,cushion:2}},{id:"block",name:"The Entire Block",tagline:"Civic centre, 09:55. Everything is technically recyclable.",briefing:"The district heating plant has made a statement. Four residents, ten samples of municipal debris and three gas cylinders are in the air. This is the department's problem now. Make the mattress maths work.",hint:"You can tilt placement with Tilt or R before clicking. Space resumes time; Z undoes a mistake.",freezeSeconds:.95,source:{x:480,y:300,power:390},bodies:[P(480,290,-260,-320),P(480,290,-80,-300),P(480,290,70,-340),P(480,290,240,-280),c("masonry",490,290,140,-300),c("masonry",490,290,240,-260),c("masonry",490,290,-180,-320),c("masonry",490,290,280,-330),c("furniture",490,290,320,-300),c("furniture",490,290,380,-250),c("furniture",490,290,-60,-280),c("glass",490,290,200,-360),c("glass",490,290,440,-220),B(540,300,120,-330),B(450,300,-150,-300),B(520,300,300,-280)],mattresses:[{id:"m1",x1:20,x2:130,label:"MATTRESS 1"},{id:"m2",x1:150,x2:260,label:"MATTRESS 2"},{id:"m3",x1:280,x2:390,label:"MATTRESS 3"},{id:"m4",x1:410,x2:520,label:"MATTRESS 4"}],bins:[{id:"masonry",material:"masonry",x1:600,x2:700},{id:"furniture",material:"furniture",x1:720,x2:820},{id:"glass",material:"glass",x1:840,x2:940}],kit:{trampoline:4,funnel:3,cushion:3}}],Qe={trampoline:112,funnel:132,cushion:92},G={trampoline:"Trampoline",funnel:"Funnel",cushion:"Sticky cushion"},ke=1/120,Te=235;class xe{constructor(t){C(this,"level");C(this,"bodies",[]);C(this,"devices",[]);C(this,"blasts",[]);C(this,"marks",[]);C(this,"elapsed",0);C(this,"liveElapsed",0);C(this,"shake",0);C(this,"volatileBlasts",0);C(this,"nextBodyId",1);C(this,"nextDeviceId",1);C(this,"live",!1);this.level=t,this.bodies=t.bodies.map(n=>({...n,id:this.nextBodyId++,active:!0,outcome:"air",angle:0,spin:(n.vx>0?1:-1)*(n.radius%4+2),fuse:null,binId:null})),this.blasts.push({x:t.source.x,y:t.source.y,age:0,strength:t.source.power})}preSim(){const t=this.level.freezeSeconds;for(;this.elapsed<t-1e-4;)this.step(Math.min(ke,t-this.elapsed),!1)}beginLive(){this.live=!0,this.liveElapsed=0}isLive(){return this.live}advance(t){if(!this.live)return;let n=Math.min(t,.05);for(;n>0;){const i=Math.min(ke,n);this.step(i,!0),n-=i}this.shake=Math.max(0,this.shake-t*28)}placeDevice(t,n,i,a){const s={id:this.nextDeviceId++,type:t,x:U(n,20,g-20),y:U(i,30,f-6),angle:a,length:Qe[t]};return this.devices.push(s),s}removeLastDevice(){return this.devices.pop()??null}clearDevices(){return this.devices.splice(0,this.devices.length)}countDevices(t){return this.devices.filter(n=>n.type===t).length}getStats(){let t=0,n=0,i=0,a=0,s=0,r=0,d=0,u=0,v=!0;for(const h of this.bodies)h.kind==="pedestrian"?(t+=1,h.outcome==="saved"?n+=1:(h.outcome==="at-large"||h.outcome==="gone")&&(i+=1),h.active&&(v=!1)):h.kind==="debris"?(a+=1,h.outcome==="sorted"?s+=1:h.outcome==="contaminated"?r+=1:(h.outcome==="missed"||h.outcome==="gone")&&(d+=1),h.active&&(v=!1)):(u+=1,h.active?v=!1:h.outcome);return{pedsTotal:t,pedsSaved:n,pedsLost:i,debrisTotal:a,debrisSorted:s,debrisContaminated:r,debrisMissed:d,volatiles:u,volatileBlasts:this.volatileBlasts,devicesUsed:this.devices.length,allResolved:v,contamination:r}}step(t,n){this.elapsed+=t,n&&(this.liveElapsed+=t);for(const i of this.blasts)i.age+=t;for(const i of this.marks)i.age+=t;if(n){for(const i of this.bodies)if(i.active){if(i.fuse!==null&&(i.fuse-=t,i.fuse<=0)){this.detonate(i);continue}if(i.vy+=ae*t,i.x+=i.vx*t,i.y+=i.vy*t,i.angle+=i.spin*t,this.collideDevices(i),this.collideGround(i),i.active&&this.checkTargets(i),i.active&&this.settleBody(i),i.active&&this.checkBounds(i),i.kind==="volatile"&&i.active){const a=Math.abs(i.vx)<24&&Math.abs(i.vy)<34;i.fuse===null&&a&&i.y+i.radius>=f-2&&(i.active=!1,i.outcome="gone")}}if(this.liveElapsed>8.2)for(const i of this.bodies)i.active&&(i.active=!1,i.kind==="pedestrian"?i.outcome="at-large":i.kind==="debris"?i.outcome="missed":i.outcome="gone")}else for(const i of this.bodies)i.active&&(i.vy+=ae*t,i.x+=i.vx*t,i.y+=i.vy*t,i.angle+=i.spin*t,this.collideDevices(i),this.collideGround(i),(i.x<-80||i.x>g+80||i.y>L+120)&&(i.active=!1,i.outcome="gone"))}collideGround(t){if(t.y+t.radius<f)return;const n=t.vy;if(t.y=f-t.radius,this.live&&t.kind==="volatile"&&t.fuse===null&&n>Te&&(t.fuse=.12,this.addMark(t.x,t.y-24,"PRESSURE!","warn")),n>0){const i=t.kind==="pedestrian"?.22:.16;t.vy=n>55?-n*i:0}t.vx*=t.kind==="pedestrian"?.82:.8,Math.abs(t.vx)<5&&(t.vx=0),t.spin*=.86}settleBody(t){!this.live||!(t.y+t.radius>=f-2)||(t.kind==="pedestrian"?Math.abs(t.vx)<18&&Math.abs(t.vy)<22&&(t.active=!1,t.outcome="at-large",this.addMark(t.x,t.y-26,"AT LARGE","bad")):t.kind==="debris"&&Math.abs(t.vx)<14&&Math.abs(t.vy)<20&&(t.active=!1,t.outcome="missed",this.addMark(t.x,t.y-22,"MISSED BIN","bad")))}collideDevices(t){for(const n of this.devices){const i=n.length/2,a=Math.cos(n.angle),s=Math.sin(n.angle),r=n.x-a*i,d=n.y-s*i,u=n.x+a*i,v=n.y+s*i,h=u-r,A=v-d,O=h*h+A*A;if(O===0)continue;const pe=U(((t.x-r)*h+(t.y-d)*A)/O,0,1),Fe=r+h*pe,Ge=d+A*pe;let J=t.x-Fe,Q=t.y-Ge,$=Math.hypot(J,Q);const je=n.type==="cushion"?8:4,x=t.radius+je;if($>=x)continue;$<.001&&(J=-s,Q=a,$=1);const ee=J/$,te=Q/$;t.x+=ee*(x-$),t.y+=te*(x-$);const F=t.vx*ee+t.vy*te;if(F>=0)continue;const ge=a,ve=s,ye=t.vx*ge+t.vy*ve,Se=n.type==="trampoline"?.94:n.type==="funnel"?.14:.02,be=n.type==="trampoline"?.985:n.type==="funnel"?.993:.42;t.vx=ge*ye*be-ee*F*Se,t.vy=ve*ye*be-te*F*Se,t.spin+=n.type==="trampoline"?F*.0015:-F*.001,this.live&&t.kind==="volatile"&&t.fuse===null&&-F>Te&&(t.fuse=.12,this.addMark(t.x,t.y-24,"PRESSURE!","warn"))}}checkTargets(t){const n=t.y+t.radius>=f-8;if(t.kind==="pedestrian"){for(const i of this.level.mattresses)if(t.x>=i.x1&&t.x<=i.x2&&n){t.active=!1,t.outcome="saved",t.y=f-40,this.addMark(t.x,t.y-28,"SOFT LANDING","good");return}}else if(t.kind==="debris"){for(const i of this.level.bins)if(t.x>=i.x1&&t.x<=i.x2&&n){t.active=!1,i.material===t.material?(t.outcome="sorted",t.binId=i.id,this.addMark(t.x,t.y-24,"SORTED","good")):(t.outcome="contaminated",t.binId=i.id,this.addMark(t.x,t.y-24,"WRONG BIN","bad"));return}}}checkBounds(t){t.x>-60&&t.x<g+60&&t.y<L+140||(t.active=!1,t.kind==="pedestrian"||t.kind,t.outcome="gone",this.addMark(U(t.x,20,g-20),U(t.y,40,f-30),t.kind==="volatile"?"CANISTER LOST":"OFF SITE","warn"))}detonate(t){t.fuse=null,t.active=!1,t.outcome="exploded",this.volatileBlasts+=1,this.explodeAt(t.x,t.y,470,!0)}explodeAt(t,n,i,a){this.blasts.push({x:t,y:n,age:0,strength:i}),this.shake=Math.max(this.shake,a?12:6),this.addMark(t,n-30,a?"CHAIN BLAST":"BLAST","warn");for(const s of this.bodies){if(!s.active||s===null)continue;const r=s.x-t,d=s.y-n,u=Math.hypot(r,d);if(u>285||u<.01)continue;const v=1-u/285,h=r/u,A=d/u,O=(a?520:300)*v+50;s.vx+=h*O,s.vy+=A*O-70*v,s.spin+=(h>0?1:-1)*O*.01,s.kind==="volatile"&&s.fuse===null&&u<175&&(s.fuse=.1+u/2200)}}addMark(t,n,i,a){this.marks.push({x:t,y:n,text:i,tone:a,age:0}),this.marks.length>18&&this.marks.shift()}}function et(e){const t=e.length/2,n=Math.cos(e.angle),i=Math.sin(e.angle);return{x1:e.x-n*t,y1:e.y-i*t,x2:e.x+n*t,y2:e.y+i*t}}function tt(e,t=1.6){if(!e.active)return[];const n=[];let i=e.x,a=e.y,s=e.vy;const r=e.vx,d=.06;for(let u=d;u<=t;u+=d){if(s+=ae*d,i+=r*d,a+=s*d,a>f-e.radius){n.push({x:i,y:f-e.radius});break}n.push({x:i,y:a})}return n}function U(e,t,n){return Math.min(n,Math.max(t,e))}const l={skyA:"#16222c",skyB:"#35444d",skyC:"#5b625e",far:"#23323b",mid:"#1b2931",near:"#20303a",window:"#d6b46d",windowCold:"#7fb9bd",street:"#303b42",sidewalk:"#46535a",curb:"#71808a",cream:"#efe6d0",ink:"#14191c",hazard:"#f2b52f",freeze:"#8ce6e8",rust:"#cf6d44",masonry:"#c97146",furniture:"#5e90c8",glass:"#5fae8c",mattress:"#e9dfc7",mattressEdge:"#b7a883",ped:"#e7be72",coatA:"#c75a40",coatB:"#5b7fa8",coatC:"#7a9d5b",gas:"#d84e35",cushion:"#c96f9a",funnel:"#e2a13b"};function Re(e,t){const n=Math.min(e/g,t/L);return{scale:n,ox:(e-g*n)/2,oy:(t-L*n)/2}}function nt(e,t,n,i){const a=Re(n,i);return{x:(e-a.ox)/a.scale,y:(t-a.oy)/a.scale}}function it(e,t,n,i,a,s){const r=Re(n,i);e.setTransform(a,0,0,a,0,0),e.clearRect(0,0,n,i);const d=e.createLinearGradient(0,0,0,i);d.addColorStop(0,"#111a21"),d.addColorStop(1,"#2a353b"),e.fillStyle=d,e.fillRect(0,0,n,i);const u=t.shake,v=Math.sin(s.now*.09)*u,h=Math.cos(s.now*.11)*u;e.save(),e.translate(r.ox+v,r.oy+h),e.scale(r.scale,r.scale),e.beginPath(),e.rect(0,0,g,L),e.clip(),at(e,s),st(e),rt(e),ot(e,t),ft(e,t,s),dt(e,t,s),s.mode==="frozen"&&s.ghost&>(e,s.selected,s.ghost.x,s.ghost.y,s.angle),ut(e,t,s),pt(e,t.devices),vt(e,t.marks),yt(e,s),e.restore()}function at(e,t){const n=e.createLinearGradient(0,0,0,f);n.addColorStop(0,l.skyA),n.addColorStop(.62,l.skyB),n.addColorStop(1,l.skyC),e.fillStyle=n,e.fillRect(0,0,g,f),e.fillStyle="rgba(240, 231, 206, 0.72)",e.beginPath(),e.arc(812,78,26,0,Math.PI*2),e.fill(),e.fillStyle="rgba(240, 231, 206, 0.08)",e.beginPath(),e.arc(812,78,58,0,Math.PI*2),e.fill(),e.strokeStyle="rgba(140, 230, 232, 0.16)",e.lineWidth=1;for(let i=0;i<9;i+=1){const a=60+i*38;e.beginPath(),e.moveTo(0,a),e.lineTo(g,a-24),e.stroke()}(t.mode==="frozen"||t.mode==="report")&&(e.fillStyle="rgba(140, 230, 232, 0.05)",e.fillRect(0,0,g,L))}function z(e){const t=Math.sin(e*127.1+311.7)*43758.5453;return t-Math.floor(t)}function st(e){e.fillStyle=l.far;for(let t=0;t<16;t+=1){const n=42+z(t+3)*40,i=t*68-22,a=110+z(t+8)*170;e.fillRect(i,f-a,n,a),e.fillStyle="rgba(214, 180, 109, 0.16)";for(let s=0;s<3;s+=1)for(let r=0;r<2;r+=1)z(t*17+s*3+r)>.45&&e.fillRect(i+8+r*18,f-a+22+s*28,7,10);e.fillStyle=l.far}e.fillStyle=l.mid,e.fillRect(0,292,154,228),e.fillRect(806,250,154,270),e.fillStyle="rgba(214, 180, 109, 0.28)";for(let t=0;t<5;t+=1)for(let n=0;n<3;n+=1)z(t*7+n+40)>.3&&e.fillRect(20+n*42,326+t*38,18,20),z(t*9+n+70)>.35&&e.fillRect(830+n*42,286+t*38,18,20);e.strokeStyle="rgba(140, 230, 232, 0.18)",e.lineWidth=2,e.strokeRect(.5,292,154,228),e.strokeRect(806,250,154,270),e.fillStyle="rgba(20, 25, 28, 0.65)",e.fillRect(28,282,108,12),e.fillStyle=l.cream,e.font="700 13px ui-monospace, monospace",e.textAlign="center",e.fillText("MUNICIPAL WORKS",82,292),e.fillStyle=l.hazard,e.fillRect(0,514,154,6),e.fillRect(806,514,154,6)}function rt(e){e.fillStyle=l.street,e.fillRect(0,f,g,L-f),e.fillStyle=l.sidewalk,e.fillRect(0,f,g,10),e.fillStyle=l.hazard,e.fillRect(0,f+9,g,2),e.strokeStyle="rgba(240, 231, 206, 0.16)",e.lineWidth=2,e.setLineDash([22,18]),e.beginPath(),e.moveTo(0,f+58),e.lineTo(g,f+58),e.stroke(),e.setLineDash([]),e.fillStyle="rgba(14, 20, 23, 0.45)";for(let t=0;t<9;t+=1)e.fillRect(30+t*110,f+12,2,4);we(e,214),we(e,744),e.fillStyle="rgba(20, 25, 28, 0.6)",e.fillRect(424,f+26,112,42),e.fillStyle=l.cream,e.font="700 11px ui-monospace, monospace",e.textAlign="center",e.fillText("FROZEN ASSETS",480,f+44),e.fillStyle=l.hazard,e.fillText("DIVISION",480,f+58)}function we(e,t){e.strokeStyle="rgba(12, 18, 21, 0.8)",e.lineWidth=5,e.beginPath(),e.moveTo(t,f),e.lineTo(t,f-118),e.quadraticCurveTo(t,f-132,t+22,f-132),e.stroke(),e.fillStyle="rgba(242, 181, 47, 0.14)",e.beginPath(),e.arc(t+22,f-126,26,0,Math.PI*2),e.fill(),e.fillStyle=l.hazard,e.beginPath(),e.arc(t+22,f-126,5,0,Math.PI*2),e.fill()}function ot(e,t){for(const n of t.level.mattresses){const i=n.x2-n.x1;e.fillStyle="rgba(10, 14, 16, 0.35)",e.beginPath(),e.ellipse((n.x1+n.x2)/2,f+4,i/2+6,8,0,0,Math.PI*2),e.fill(),M(e,n.x1,f-26,i,26,8),e.fillStyle=l.mattress,e.fill(),e.strokeStyle=l.mattressEdge,e.lineWidth=2,e.stroke(),e.strokeStyle="rgba(183, 168, 131, 0.7)",e.lineWidth=1;for(let a=n.x1+12;a<n.x2-6;a+=18)e.beginPath(),e.moveTo(a,f-24),e.lineTo(a-5,f-2),e.stroke();e.fillStyle="rgba(20, 25, 28, 0.72)",M(e,n.x1+4,f-44,i-8,16,4),e.fill(),e.fillStyle=l.cream,e.font="700 10px ui-monospace, monospace",e.textAlign="center",e.fillText("SOFT LANDING",(n.x1+n.x2)/2,f-32);for(const a of t.bodies)a.outcome==="saved"&&a.x>=n.x1&&a.x<=n.x2&&se(e,a,!0)}for(const n of t.level.bins){const i=n.x2-n.x1,a=l[n.material];e.fillStyle="rgba(10, 14, 16, 0.4)",e.beginPath(),e.ellipse((n.x1+n.x2)/2,f+4,i/2+5,7,0,0,Math.PI*2),e.fill(),e.fillStyle=a,M(e,n.x1,f-62,i,62,6),e.fill(),e.fillStyle="rgba(14, 19, 22, 0.3)",M(e,n.x1+5,f-56,i-10,20,3),e.fill(),e.fillStyle="rgba(255, 255, 255, 0.5)",e.fillRect(n.x1+8,f-58,i-16,3),e.fillStyle="rgba(20, 25, 28, 0.82)",M(e,n.x1+8,f-31,i-16,22,3),e.fill(),e.fillStyle=l.cream,e.font="700 10px ui-monospace, monospace",e.textAlign="center",e.fillText(lt(n.material),(n.x1+n.x2)/2,f-16);const s=t.bodies.filter(r=>r.outcome==="sorted"&&r.binId===n.id).length;if(s>0){e.fillStyle=l.hazard;for(let r=0;r<s;r+=1)e.fillRect(n.x1+12+r*9,f-66,6,4)}}}function lt(e){return e==="masonry"?"MASONRY":e==="furniture"?"FURNITURE":"GLASS"}function ft(e,t,n){const i=t.level.source,a=n.mode==="frozen"?30:14;for(let s=0;s<a;s+=1){const r=z(s+1)*Math.PI*2,d=22+z(s+31)*(n.mode==="frozen"?235:120),u=n.mode==="frozen"?0:(n.now*.03+s*13)%90-45,v=i.x+Math.cos(r)*d+u,h=i.y+Math.sin(r)*d*.7,A=1.5+z(s+71)*3;e.fillStyle=n.mode==="frozen"?"rgba(140, 230, 232, 0.34)":"rgba(240, 181, 106, 0.18)",e.beginPath(),e.arc(v,h,A,0,Math.PI*2),e.fill()}}function dt(e,t,n){for(const[i,a]of t.blasts.entries()){if(n.mode!=="running"&&i===0){Me(e,a.x,a.y,84+Math.sin(n.now*.004)*5),e.fillStyle=l.hazard,e.font="700 11px ui-monospace, monospace",e.textAlign="center",e.fillText("BLAST ORIGIN",a.x,a.y+112);continue}if(a.age>1.15)continue;const s=a.age/1.15,r=18+s*235,d=Math.max(0,.55*(1-s));e.strokeStyle=`rgba(242, 181, 47, ${d})`,e.lineWidth=7*(1-s)+1,e.beginPath(),e.arc(a.x,a.y,r,0,Math.PI*2),e.stroke(),e.strokeStyle=`rgba(207, 109, 68, ${d*.8})`,e.lineWidth=2,e.beginPath(),e.arc(a.x,a.y,r*.68,0,Math.PI*2),e.stroke(),Me(e,a.x,a.y,58*(1-s)+18)}}function Me(e,t,n,i){const a=e.createRadialGradient(t,n,2,t,n,i);a.addColorStop(0,"rgba(255, 241, 197, 0.95)"),a.addColorStop(.35,"rgba(242, 181, 47, 0.72)"),a.addColorStop(.72,"rgba(216, 78, 53, 0.35)"),a.addColorStop(1,"rgba(216, 78, 53, 0)"),e.fillStyle=a,e.beginPath(),e.arc(t,n,i,0,Math.PI*2),e.fill(),e.strokeStyle="rgba(255, 241, 197, 0.5)",e.lineWidth=2;for(let s=0;s<10;s+=1){const r=s/10*Math.PI*2;e.beginPath(),e.moveTo(t+Math.cos(r)*i*.55,n+Math.sin(r)*i*.55),e.lineTo(t+Math.cos(r)*i*(1+z(s)*.25),n+Math.sin(r)*i),e.stroke()}}function ut(e,t,n){if(n.mode==="frozen"&&n.showHints)for(const i of t.bodies)i.active&&ct(e,i);for(const i of t.bodies){if(!i.active)continue;const a=Math.hypot(i.vx,i.vy);if(n.mode==="frozen"&&a>40){const s=Math.min(70,a*.1),r=i.vx/a,d=i.vy/a,u=e.createLinearGradient(i.x-r*s,i.y-d*s,i.x,i.y);u.addColorStop(0,"rgba(140, 230, 232, 0)"),u.addColorStop(1,"rgba(140, 230, 232, 0.45)"),e.strokeStyle=u,e.lineWidth=2,e.beginPath(),e.moveTo(i.x-r*s,i.y-d*s),e.lineTo(i.x,i.y),e.stroke()}i.kind==="pedestrian"?se(e,i,!1):i.kind==="debris"?ht(e,i):mt(e,i,n.now),n.mode==="frozen"&&i.active&&(e.strokeStyle="rgba(140, 230, 232, 0.55)",e.lineWidth=1.2,e.beginPath(),e.arc(i.x,i.y,i.radius+5,0,Math.PI*2),e.stroke())}for(const i of t.bodies)i.active||i.outcome==="exploded"||i.outcome==="gone"||i.outcome==="sorted"||i.outcome==="contaminated"||i.kind==="pedestrian"&&i.outcome==="at-large"&&se(e,i,!0)}function ct(e,t){const n=tt(t,1.35);if(n.length===0)return;e.fillStyle=t.kind==="pedestrian"?"rgba(242, 181, 47, 0.55)":t.kind==="volatile"?"rgba(216, 78, 53, 0.65)":"rgba(140, 230, 232, 0.45)";for(let a=2;a<n.length;a+=3){const s=n[a];e.beginPath(),e.arc(s.x,s.y,1.6,0,Math.PI*2),e.fill()}const i=n[n.length-1];e.strokeStyle="rgba(140, 230, 232, 0.5)",e.lineWidth=1,e.beginPath(),e.moveTo(i.x-4,i.y),e.lineTo(i.x+4,i.y),e.stroke()}function se(e,t,n){const i=[l.coatA,l.coatB,l.coatC][t.id%3];e.save(),e.translate(t.x,t.y),n||e.rotate(t.angle),e.lineCap="round",e.strokeStyle=l.ink,e.lineWidth=3,e.beginPath(),e.moveTo(-2,2),e.lineTo(-7,13),e.moveTo(2,2),e.lineTo(7,12),e.stroke(),e.fillStyle=i,M(e,-7,-5,14,18,5),e.fill(),e.fillStyle=l.hazard,e.fillRect(-6,2,12,3),e.strokeStyle=l.ped,e.lineWidth=2.5,e.beginPath(),e.moveTo(-6,0),e.lineTo(-12,n?8:-5),e.moveTo(6,0),e.lineTo(12,n?8:-7),e.stroke(),e.fillStyle=l.ped,e.beginPath(),e.arc(0,-10,6.5,0,Math.PI*2),e.fill(),e.fillStyle=l.ink,e.beginPath(),e.arc(-2,-11,1,0,Math.PI*2),e.arc(2,-11,1,0,Math.PI*2),e.fill(),e.restore()}function ht(e,t){e.save(),e.translate(t.x,t.y),e.rotate(t.angle),t.material==="masonry"?(e.fillStyle="rgba(10, 14, 16, 0.28)",M(e,-10,-6,20,14,2),e.fill(),e.fillStyle=l.masonry,M(e,-11,-8,20,13,2),e.fill(),e.strokeStyle="rgba(20, 25, 28, 0.5)",e.lineWidth=1,e.beginPath(),e.moveTo(-4,-8),e.lineTo(-4,5),e.moveTo(5,-8),e.lineTo(5,5),e.stroke(),e.fillStyle=l.cream,e.fillRect(-11,-7,20,2)):t.material==="furniture"?(e.fillStyle=l.furniture,M(e,-10,-7,20,7,2),e.fill(),M(e,-10,-17,6,20,2),e.fill(),e.fillStyle="rgba(20, 25, 28, 0.55)",e.fillRect(-8,0,3,8),e.fillRect(6,0,3,8),e.fillStyle="rgba(255, 255, 255, 0.35)",e.fillRect(-8,-14,3,6)):(e.fillStyle=l.glass,M(e,-5,-12,10,20,4),e.fill(),e.fillRect(-2,-16,4,5),e.fillStyle="rgba(255, 255, 255, 0.45)",M(e,-2.5,-9,2,12,1),e.fill()),e.restore()}function mt(e,t,n){e.save(),e.translate(t.x,t.y),e.rotate(t.angle);const i=t.fuse!==null?.65+Math.sin(n*.05)*.35:1;e.fillStyle=`rgba(216, 78, 53, ${i})`,M(e,-9,-12,18,24,7),e.fill(),e.strokeStyle=l.ink,e.lineWidth=2,e.stroke(),e.fillStyle=l.hazard,e.fillRect(-9,-3,18,6),e.fillStyle=l.cream,e.font="700 7px ui-monospace, monospace",e.textAlign="center",e.fillText("GAS",0,2),e.fillStyle=l.ink,e.fillRect(-3,-16,6,4),e.restore()}function pt(e,t,n){for(const i of t){const a=et(i);if(e.save(),e.shadowColor="rgba(140, 230, 232, 0.45)",e.shadowBlur=8,i.type==="trampoline"){e.strokeStyle=l.freeze,e.lineWidth=5,e.beginPath(),e.moveTo(a.x1,a.y1),e.lineTo(a.x2,a.y2),e.stroke(),e.strokeStyle="rgba(20, 25, 28, 0.85)",e.lineWidth=1;const s=8;for(let r=1;r<s;r+=1){const d=r/s,u=a.x1+(a.x2-a.x1)*d,v=a.y1+(a.y2-a.y1)*d;e.beginPath(),e.moveTo(u-1,v-4),e.lineTo(u+1,v+4),e.stroke()}e.fillStyle=l.hazard;for(const r of[a,{x1:a.x2,y1:a.y2,x2:a.x1,y2:a.y1}])e.beginPath(),e.arc(r.x1,r.y1,4,0,Math.PI*2),e.fill()}else if(i.type==="funnel"){e.strokeStyle=l.funnel,e.lineWidth=12,e.beginPath(),e.moveTo(a.x1,a.y1),e.lineTo(a.x2,a.y2),e.stroke(),e.strokeStyle="rgba(240, 231, 206, 0.7)",e.lineWidth=2,e.beginPath(),e.moveTo(a.x1,a.y1),e.lineTo(a.x2,a.y2),e.stroke(),e.fillStyle=l.ink;for(let s=1;s<=3;s+=1){const r=s/4,d=a.x1+(a.x2-a.x1)*r,u=a.y1+(a.y2-a.y1)*r;e.beginPath(),e.arc(d,u,2,0,Math.PI*2),e.fill()}}else{e.strokeStyle=l.cushion,e.lineWidth=13,e.lineCap="round",e.beginPath(),e.moveTo(a.x1,a.y1),e.lineTo(a.x2,a.y2),e.stroke(),e.strokeStyle="rgba(255, 255, 255, 0.4)",e.lineWidth=2,e.beginPath(),e.moveTo(a.x1+3,a.y1-3),e.lineTo(a.x2-3,a.y2-3),e.stroke(),e.fillStyle=l.cushion;for(let s=0;s<3;s+=1){const r=(s+1)/4,d=a.x1+(a.x2-a.x1)*r,u=a.y1+(a.y2-a.y1)*r;e.beginPath(),e.arc(d,u+9+s,2.2,0,Math.PI*2),e.fill()}}e.restore()}}function gt(e,t,n,i,a){const r=(t==="trampoline"?112:t==="funnel"?132:92)/2,d=Math.cos(a),u=Math.sin(a);e.save(),e.globalAlpha=.65,e.setLineDash([8,6]),e.strokeStyle=t==="trampoline"?l.freeze:t==="funnel"?l.funnel:l.cushion,e.lineWidth=t==="cushion"?14:7,e.lineCap="round",e.beginPath(),e.moveTo(n-d*r,i-u*r),e.lineTo(n+d*r,i+u*r),e.stroke(),e.setLineDash([]),e.globalAlpha=.9,e.fillStyle=l.cream,e.beginPath(),e.arc(n,i,3,0,Math.PI*2),e.fill(),e.restore()}function vt(e,t){for(const n of t){if(n.age>1.7)continue;const i=Math.min(1,Math.max(0,1-n.age/1.7)),a=n.y-n.age*20;e.font="700 12px ui-monospace, monospace",e.textAlign="center";const s=e.measureText(n.text).width+12;e.fillStyle=`rgba(20, 25, 28, ${i*.8})`,M(e,n.x-s/2,a-12,s,17,3),e.fill(),e.fillStyle=n.tone==="good"?`rgba(140, 230, 232, ${i})`:n.tone==="bad"?`rgba(240, 132, 109, ${i})`:`rgba(242, 181, 47, ${i})`,e.fillText(n.text,n.x,a)}}function yt(e,t){if(t.mode==="frozen"){e.strokeStyle="rgba(140, 230, 232, 0.09)",e.lineWidth=1;for(let n=-L;n<g;n+=26)e.beginPath(),e.moveTo(n,0),e.lineTo(n+L,L),e.stroke();e.strokeStyle="rgba(140, 230, 232, 0.32)",e.lineWidth=2,e.strokeRect(1,1,g-2,L-2),e.fillStyle="rgba(140, 230, 232, 0.8)",e.font="700 11px ui-monospace, monospace",e.textAlign="left",e.fillText("FRAME HELD — PHYSICS SUSPENDED",20,26),e.textAlign="right",e.fillText("PLACE KIT / RESUME TIME",g-20,26)}}function M(e,t,n,i,a,s){const r=Math.min(s,Math.abs(i)/2,Math.abs(a)/2);e.beginPath(),e.moveTo(t+r,n),e.arcTo(t+i,n,t+i,n+a,r),e.arcTo(t+i,n+a,t,n+a,r),e.arcTo(t,n+a,t,n,r),e.arcTo(t,n,t+i,n,r),e.closePath()}const re="ffr-service-record-v1",N=e=>String(e).padStart(2,"0"),o={caseNo:p("#caseNo"),caseName:p("#caseName"),pedStat:p("#pedStat"),debrisStat:p("#debrisStat"),kitStat:p("#kitStat"),canvas:p("#scene"),freezeBadge:p("#freezeBadge"),freezeLabel:p("#freezeLabel"),freezeClock:p("#freezeClock"),toast:p("#toast"),overlay:p("#overlay"),briefButton:p("#briefButton"),retryButton:p("#retryButton"),tiltButton:p("#tiltButton"),tiltValue:p("#tiltValue"),undoButton:p("#undoButton"),resumeButton:p("#resumeButton"),live:p("#live")};function St(){const e=o.canvas.getContext("2d");if(!e)throw new Error("Canvas is unavailable");return e}const bt=St();let k,b=0,m="frozen",E="trampoline",W=0,H=null,y=null,D=!1,T={x:480,y:380},q=!1,_=!1,Ie=1,Ee=performance.now(),Ce=0,Pe=0,j={},Le=!1;const kt=window.matchMedia("(prefers-reduced-motion: reduce)");function p(e){const t=document.querySelector(e);if(!t)throw new Error(`Missing required element: ${e}`);return t}function ze(e){const t=new xe(S[e]);return t.preSim(),t}function I(e){return Math.max(0,S[b].kit[e]-k.countDevices(e))}function fe(){return["trampoline","funnel","cushion"].find(t=>I(t)>0)??"trampoline"}function Ae(e){const t=Math.PI/12;return Math.round(e/t)*t}function V(){return`${(Math.round(W*180/Math.PI)%360+540)%360-180}°`}function de(e){return e.x>4&&e.x<g-4&&e.y>8&&e.y<f-4}function R(e){o.toast.textContent=e,o.toast.classList.add("show"),o.live.textContent=e,window.clearTimeout(Pe),Pe=window.setTimeout(()=>o.toast.classList.remove("show"),2400)}function K(){const e=["trampoline","funnel","cushion"];for(const n of e){const i=p(`.tool[data-tool="${n}"]`),a=p(`[data-count="${n}"]`),s=I(n);a.textContent=String(s),i.disabled=m!=="frozen"||s<=0,i.setAttribute("aria-pressed",String(n===E)),i.classList.toggle("selected",n===E)}const t=m==="frozen";o.tiltButton.disabled=!t,o.undoButton.disabled=!t||k.devices.length===0,o.resumeButton.disabled=m!=="frozen",o.tiltValue.textContent=V(),o.canvas.classList.toggle("placing",t&&I(E)>0)}function X(){const e=k.getStats(),t=S[b];o.caseNo.textContent=`CASE ${N(b+1)} / ${N(S.length)}`,o.caseName.textContent=t.name,o.pedStat.textContent=`${e.pedsSaved}/${e.pedsTotal}`,o.debrisStat.textContent=`${e.debrisSorted}/${e.debrisTotal}`;const n=I("trampoline")+I("funnel")+I("cushion");o.kitStat.textContent=String(n)}function Z(){m==="frozen"?(o.freezeBadge.className="freeze-badge frozen",o.freezeLabel.textContent="TIME SUSPENDED",o.freezeBadge.hidden=!1):m==="running"?(o.freezeBadge.className="freeze-badge running",o.freezeLabel.textContent="TIME RUNNING",o.freezeBadge.hidden=!1):(o.freezeBadge.className="freeze-badge settled",o.freezeLabel.textContent="INCIDENT SETTLED",o.freezeBadge.hidden=!1),K(),X()}function Y(e,t=!1){if(I(e)<=0){R(`${G[e]} stock is empty. Undo a placement or try another item.`);return}E=e,t&&R(`${G[e]} selected. ${I(e)} in the van.`),K()}function $e(e,t,n){if(m!=="frozen")return R("Time is already moving. Retry the case to re-plan."),!1;if(I(e)<=0)return R(`${G[e]} stock is empty.`),!1;if(!de({x:t,y:n}))return R("Outside the frozen frame. The paperwork does not cover that."),!1;k.placeDevice(e,t,n,W);const i=I(e);return R(`${G[e]} placed at ${V()}. ${i} left.`),i<=0&&(E=fe()),K(),X(),!0}function Be(){m==="frozen"&&(W=Ae(W+Math.PI/12),o.tiltValue.textContent=V(),o.live.textContent=`Placement tilt ${V()}.`)}function De(){if(m!=="frozen")return;const e=k.removeLastDevice();if(!e){R("Nothing to recover. The van is still full.");return}E=e.type,R(`${G[e.type]} recovered.`),K(),X()}function Ne(){m==="frozen"&&(y=null,H=null,m="running",Le=!0,k.beginLive(),Z(),R("Time resumed. Please do not cheer."))}function oe(e,t=!1){b=(e%S.length+S.length)%S.length,k=ze(b),m="frozen",_=!1,y=null,H=null,D=!1,T={x:480,y:380},W=0,E=fe(),ce(),Z(),R(`Case ${N(b+1)}: ${S[b].name}. Time is suspended.`),t&&ue()}function Tt(){const e=S[b],t=Object.values(j).filter(i=>i.perfect).length,n=t>0?`Service record: ${t} of ${S.length} cases closed.`:"No closed cases on file. The shredder is ready.";return`
<article class="card brief-card">
<p class="card-kicker">FROZEN ASSETS DIVISION · CASE ${N(b+1)} / ${N(S.length)}</p>
<h2>${e.name}</h2>
<p class="tagline">${e.tagline}</p>
<p class="brief-copy">${e.briefing}</p>
<div class="legend">
<span><i class="swatch mattress"></i>Mattress · pedestrians</span>
<span><i class="swatch masonry"></i>Masonry bin</span>
<span><i class="swatch furniture"></i>Furniture bin</span>
<span><i class="swatch glass"></i>Glass bin</span>
</div>
<div class="howto"><b>Frozen protocol</b> Pick a kit item. Tap the scene to place it, drag from the anchor to aim, or steer the cursor with the arrow keys and press <kbd>Enter</kbd>. <kbd>R</kbd> tilts, <kbd>Z</kbd> undoes, <kbd>Space</kbd> resumes. Dotted lines show where objects are already going.</div>
<p class="hint">${e.hint}</p>
<div class="card-actions">
<span class="service-record">${n}</span>
<button type="button" class="primary-btn" data-action="begin">${Le?"Back to the scene":"Begin inspection"}</button>
</div>
</article>`}function ue(){if(m==="running"){R("The incident is already in motion. Let the report finish.");return}if(m==="report")return;o.overlay.innerHTML=Tt(),o.overlay.hidden=!1,q=!0;const e=o.overlay.querySelector('[data-action="begin"]');e==null||e.addEventListener("click",()=>{ce(),R("Frame held. Place kit, then resume time.")}),e==null||e.focus()}function ce(){o.overlay.hidden=!0,o.overlay.innerHTML="",q=!1}function wt(e){const t=e.debrisMissed*1850+e.debrisContaminated*3200+e.pedsLost*14500+e.volatileBlasts*6800+e.devicesUsed*120;return Math.max(250,Math.round(t/50)*50)}function Mt(e,t){const n=[];return e.pedsLost===0?n.push("All pedestrians delivered to soft furnishings. Nobody signed the waiver."):n.push(`${e.pedsLost} pedestrian(s) remain at large and are listed as "taking a walk".`),e.debrisMissed===0&&e.debrisContaminated===0?n.push("Recycling targets met. The bins have been wiped down with pride."):(e.debrisMissed>0&&n.push(`${e.debrisMissed} item(s) missed the bins entirely.`),e.debrisContaminated>0&&n.push(`${e.debrisContaminated} item(s) contaminated a material stream.`)),e.volatileBlasts>0?n.push(`${e.volatileBlasts} gas canister(s) joined the meeting. Chain-reaction forms are in triplicate.`):e.volatiles>0&&n.push(`All ${e.volatiles} gas canister(s) remained unopinionated.`),e.devicesUsed===0&&n.push("No equipment was deployed. The department respects the confidence."),t?n.push("Commendation filed directly into the shredder."):e.pedsSaved>0&&n.push("At least one mattress justified its budget."),n}function Et(e,t,n){const i=S[b],a=t?"CASE CLOSED":e.pedsSaved===e.pedsTotal?"ACCEPTABLE LOSS":"REVIEW REQUIRED",s=t?"closed":e.pedsSaved===e.pedsTotal?"partial":"review",r=Mt(e,t).map(v=>`<li>${v}</li>`).join(""),d=new Intl.NumberFormat("en-US",{style:"currency",currency:"USD",maximumFractionDigits:0}).format(n),u=b+1>=S.length;return`
<article class="card report-card">
<p class="card-kicker">INCIDENT REPORT · CASE ${N(b+1)} / ${N(S.length)}</p>
<div class="report-head">
<h2>${i.name}</h2>
<span class="stamp ${s}">${a}</span>
</div>
<div class="report-grid">
<span>Pedestrians delivered</span><b>${e.pedsSaved} / ${e.pedsTotal}</b>
<span>Debris processed</span><b>${e.debrisSorted} / ${e.debrisTotal}</b>
<span>Contaminated streams</span><b>${e.debrisContaminated}</b>
<span>Pedestrians unaccounted</span><b>${e.pedsLost}</b>
<span>Canister detonations</span><b>${e.volatileBlasts}</b>
<span>Equipment deployed</span><b>${e.devicesUsed}</b>
</div>
<div class="damage">
<span>Estimated damage</span>
<strong>${d}</strong>
</div>
<ul class="report-notes">${r}</ul>
<p id="reportSaveNote" class="save-note">Filing result with the local service record…</p>
<div class="card-actions">
<button type="button" class="small-btn" data-action="retry">Retry case</button>
<button type="button" class="primary-btn" data-action="next">${u?"Restart case load":"Next case file"}</button>
</div>
</article>`}function Ct(){if(_)return;_=!0,m="report",Z();const e=k.getStats(),t=e.pedsSaved===e.pedsTotal&&e.debrisSorted===e.debrisTotal&&e.debrisContaminated===0,n=wt(e);o.overlay.innerHTML=Et(e,t,n),o.overlay.hidden=!1,q=!0;const i=o.overlay.querySelector('[data-action="retry"]'),a=o.overlay.querySelector('[data-action="next"]');i==null||i.addEventListener("click",()=>oe(b)),a==null||a.addEventListener("click",()=>oe(b+1,!0)),a==null||a.focus(),Pt(b,n,t).then(s=>{if(m!=="report")return;const r=o.overlay.querySelector("#reportSaveNote");r&&(r.textContent=s)})}async function Pt(e,t,n){if(!le("storage"))return"Local service record unavailable in this runtime. The result exists only in this form.";try{const i=await ie.getItem(re);let a={};if(i){const u=JSON.parse(i);u&&typeof u=="object"&&(a=u)}const s=S[e].id,r=a[s];return!r||t<r.damage||n&&!r.perfect?(a[s]={damage:t,perfect:n},await ie.setItem(re,JSON.stringify(a)),j=a,"Result filed. The service record has been updated."):(j=a,"Previous attempt had lower damage. Kept the original form.")}catch{return"Record could not be filed. The form remains optimistic."}}async function Rt(){if(le("storage"))try{const e=await ie.getItem(re);if(!e)return;const t=JSON.parse(e);t&&typeof t=="object"&&(j=t)}catch{j={}}}function he(e){const t=o.canvas.getBoundingClientRect();return t.width===0||t.height===0?null:nt(e.clientX-t.left,e.clientY-t.top,t.width,t.height)}o.canvas.addEventListener("pointerdown",e=>{if(m!=="frozen"||q||e.pointerType==="mouse"&&e.button!==0)return;const t=he(e);if(!(!t||!de(t))){if(I(E)<=0){R(`No ${G[E]} left. Undo a placement or choose another item.`);return}e.preventDefault(),D=!1,y={x:t.x,y:t.y,currentX:t.x,currentY:t.y,moved:!1},H=t,o.canvas.setPointerCapture(e.pointerId)}});o.canvas.addEventListener("pointermove",e=>{const t=he(e);if(H=t&&de(t)?t:null,t&&(D=!1),!y||!t)return;y.currentX=t.x,y.currentY=t.y;const n=t.x-y.x,i=t.y-y.y;Math.hypot(n,i)>16&&(y.moved=!0,W=Ae(Math.atan2(i,n)),o.tiltValue.textContent=V()),e.preventDefault()});function It(e){if(!y)return;const t=he(e),n=y.moved?{x:y.x,y:y.y}:t;n&&$e(E,n.x,n.y),y=null,o.canvas.classList.remove("placing"),o.canvas.hasPointerCapture(e.pointerId)&&o.canvas.releasePointerCapture(e.pointerId),K()}o.canvas.addEventListener("pointerup",It);o.canvas.addEventListener("pointercancel",()=>{y=null,o.canvas.classList.remove("placing")});document.querySelectorAll(".tool").forEach(e=>{e.addEventListener("click",()=>{const t=e.dataset.tool;t&&Y(t,!0)})});o.tiltButton.addEventListener("click",Be);o.undoButton.addEventListener("click",De);o.resumeButton.addEventListener("click",Ne);o.retryButton.addEventListener("click",()=>oe(b));o.briefButton.addEventListener("click",ue);window.addEventListener("keydown",e=>{if(e.metaKey||e.ctrlKey||e.altKey)return;const t=e.key.toLowerCase();if(q){t==="escape"&&m!=="report"&&ce();return}if(m!=="report"){if(t.startsWith("arrow")){e.preventDefault(),D=!0,H=null;const n=e.shiftKey?6:20;t==="arrowleft"?T.x-=n:t==="arrowright"?T.x+=n:t==="arrowup"?T.y-=n:t==="arrowdown"&&(T.y+=n),T.x=Math.min(g-10,Math.max(10,T.x)),T.y=Math.min(f-10,Math.max(20,T.y)),o.live.textContent=`Placement cursor at ${Math.round(T.x)}, ${Math.round(T.y)}.`;return}t==="1"?Y("trampoline"):t==="2"?Y("funnel"):t==="3"?Y("cushion"):t==="r"?Be():t==="z"?De():t==="enter"?(e.preventDefault(),m==="frozen"&&(D=!0,$e(E,T.x,T.y))):t===" "?(e.preventDefault(),Ne()):t==="escape"&&(D=!1)}});function me(){const e=o.canvas.getBoundingClientRect(),t=Math.min(2,window.devicePixelRatio||1),n=Math.max(1,Math.round(e.width*t)),i=Math.max(1,Math.round(e.height*t));(o.canvas.width!==n||o.canvas.height!==i)&&(o.canvas.width=n,o.canvas.height=i),Ie=t}function Lt(e){const t=o.canvas.getBoundingClientRect();if(t.width===0||t.height===0)return;const n=m==="frozen"&&!q?y?{x:y.x,y:y.y}:D?T:H:null;it(bt,k,t.width,t.height,Ie,{mode:m,selected:E,angle:W,ghost:n&&I(E)>0?n:null,showHints:document.documentElement.dataset.trajectoryHints!=="false",now:kt.matches?0:e})}function We(e){const t=Math.min(.05,Math.max(0,(e-Ee)/1e3));if(Ee=e,m==="running"){k.advance(t);const n=k.getStats();!_&&n.allResolved&&k.liveElapsed>.95&&Ct()}e-Ce>120&&(X(),Ce=e),m==="frozen"?o.freezeClock.textContent=`T+${k.elapsed.toFixed(2)}`:m==="running"?o.freezeClock.textContent=`T+${k.liveElapsed.toFixed(2)}`:o.freezeClock.textContent="FILED",Lt(e),window.requestAnimationFrame(We)}o.canvas.tabIndex=0;k=ze(0);E=fe();const Oe=new ResizeObserver(()=>me());Oe.observe(o.canvas);me();Rt().then(()=>{const e=o.overlay.querySelector(".service-record");if(e){const t=Object.values(j).filter(n=>n.perfect).length;e.textContent=t>0?`Service record: ${t} of ${S.length} cases closed.`:"No closed cases on file. The shredder is ready."}});Z();ue();window.addEventListener("resize",me);window.addEventListener("beforeunload",()=>Oe.disconnect());window.requestAnimationFrame(We);
</script>
<style>:root{--ink: #12181b;--ink-2: #182126;--ink-3: #202c32;--paper: #efe6d0;--paper-2: #e4d8bd;--paper-ink: #20221d;--line: rgba(239, 230, 208, .16);--line-strong: rgba(239, 230, 208, .3);--text: #e9e2d0;--muted: #9aa5a4;--hazard: #f2b52f;--hazard-soft: #f7cd6b;--freeze: #8ce6e8;--danger: #f0846d;--masonry: #c97146;--furniture: #5e90c8;--glass: #5fae8c;--mattress: #e9dfc7;--shadow: 6px 6px 0 rgba(0, 0, 0, .38);--control-h: 46px;--napplet-text-selection: none;color-scheme:dark;font:13px/1.4 ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}*{box-sizing:border-box}:where(html,body,body *){-webkit-user-select:var(--napplet-text-selection, none);user-select:var(--napplet-text-selection, none)}:where([data-napplet-select=text],input,textarea){-webkit-user-select:text;user-select:text}html,body,#app{height:100%;margin:0;background:var(--ink);color:var(--text)}button{font:inherit;color:inherit}.app{container-type:inline-size;display:grid;grid-template-rows:auto minmax(0,1fr) auto;gap:0;height:100%;min-height:0;background:radial-gradient(circle at 50% -30%,rgba(94,144,200,.1),transparent 60%),var(--ink);overflow:hidden}.hud{display:flex;align-items:center;gap:10px;padding:7px 10px;background:linear-gradient(180deg,#222e34,#172025);border-bottom:1px solid var(--line);border-top:2px solid var(--hazard);min-width:0}.case{display:flex;align-items:baseline;gap:8px;min-width:0;flex:1}.case-no{font:700 9px/1 ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:.12em;color:var(--hazard);white-space:nowrap;padding:3px 5px;border:1px solid rgba(242,181,47,.35);background:#f2b52f14}.case-name{font:700 14px/1.15 ui-sans-serif,system-ui,sans-serif;letter-spacing:.01em;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.stats{display:flex;gap:5px;align-items:center;flex-shrink:0}.stat{display:inline-flex;align-items:baseline;gap:4px;padding:3px 6px;border:1px solid var(--line);background:#0a0e1066;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;white-space:nowrap}.stat-k{font-size:8px;letter-spacing:.09em;color:var(--muted)}.stat b{font-size:12px;color:var(--freeze)}.stat:nth-child(2) b{color:var(--hazard)}.hud-actions{display:flex;gap:4px;flex-shrink:0}.icon-btn{width:28px;height:28px;display:grid;place-items:center;border:1px solid var(--line-strong);background:var(--ink-3);color:var(--text);font:700 14px/1 ui-monospace,monospace;cursor:pointer;padding:0}.icon-btn:hover{border-color:var(--hazard);color:var(--hazard)}.icon-btn:focus-visible,.tool:focus-visible,.small-btn:focus-visible,.resume-btn:focus-visible,.primary-btn:focus-visible{outline:2px solid var(--freeze);outline-offset:2px}.stage{position:relative;min-height:0;min-width:0;margin:6px;border:1px solid rgba(239,230,208,.14);background:#0b1114;overflow:hidden;box-shadow:inset 0 0 40px #00000080}#scene{display:block;width:100%;height:100%;touch-action:none;cursor:default;outline:none}#scene.placing{cursor:crosshair}.freeze-badge{position:absolute;top:8px;left:8px;display:inline-flex;align-items:center;gap:6px;padding:4px 7px;border:1px solid var(--freeze);background:#0f1518db;color:var(--freeze);font:700 9px/1 ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:.09em;pointer-events:none;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:2}.freeze-badge #freezeClock{color:var(--text);font-weight:400;letter-spacing:0}.freeze-dot{width:6px;height:6px;border-radius:50%;background:var(--freeze);box-shadow:0 0 8px var(--freeze);animation:pulse 1.8s ease-in-out infinite}.freeze-badge.running{border-color:var(--hazard);color:var(--hazard)}.freeze-badge.running .freeze-dot{background:var(--hazard);box-shadow:0 0 8px var(--hazard);animation-duration:.7s}.freeze-badge.settled{border-color:var(--line-strong);color:var(--muted)}.freeze-badge.settled .freeze-dot{background:var(--muted);box-shadow:none;animation:none}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.7)}}.toast{position:absolute;left:50%;bottom:10px;transform:translate(-50%,8px);max-width:calc(100% - 16px);padding:6px 10px;background:#0f1518f0;border-left:3px solid var(--hazard);color:var(--text);font:600 11px/1.35 ui-sans-serif,system-ui,sans-serif;opacity:0;transition:opacity .14s ease,transform .14s ease;pointer-events:none;z-index:3}.toast.show{opacity:1;transform:translate(-50%)}.overlay{position:fixed;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;padding:10px;background:#070b0db8;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);overflow:auto;z-index:20}.overlay[hidden]{display:none}.card{width:min(560px,100%);max-height:calc(100% - 2px);overflow:auto;padding:18px 20px 16px;background:linear-gradient(rgba(32,34,29,.04) 1px,transparent 1px) 0 0 / 100% 26px,var(--paper);color:var(--paper-ink);border:1px solid #c7bca2;box-shadow:var(--shadow)}.card-kicker{margin:0 0 7px;padding-bottom:6px;border-bottom:1px dashed #a99f88;font:700 9px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:.13em;color:#766e5c}.card h2{margin:0 0 4px;font:800 24px/1.02 ui-sans-serif,system-ui,sans-serif;letter-spacing:-.015em;text-transform:uppercase}.tagline{margin:0 0 9px;color:#5f594b;font:italic 12px/1.4 ui-sans-serif,system-ui,sans-serif}.brief-copy{margin:0 0 9px;font-size:12.5px}.legend{display:grid;grid-template-columns:1fr 1fr;gap:3px 8px;margin:9px 0;padding:7px 0;border-top:1px dashed #a99f88;border-bottom:1px dashed #a99f88;font:600 10px/1.3 ui-monospace,monospace;color:#4d493e}.legend span{display:inline-flex;align-items:center;gap:5px}.swatch{width:10px;height:10px;display:inline-block;border:1px solid rgba(20,25,28,.4)}.swatch.mattress{background:var(--mattress)}.swatch.masonry{background:var(--masonry)}.swatch.furniture{background:var(--furniture)}.swatch.glass{background:var(--glass)}.howto{margin:9px 0 8px;padding:7px 9px;border-left:3px solid var(--hazard);background:#20221d12;font-size:11.5px}.howto b{display:block;margin-bottom:2px;font:700 9px/1.2 ui-monospace,monospace;letter-spacing:.1em;text-transform:uppercase}kbd{padding:0 3px;border:1px solid #a99f88;background:#faf5e8;font:700 9px/1.4 ui-monospace,monospace}.hint{margin:0;color:#6a6252;font-size:11px;font-style:italic}.card-actions{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-top:12px}.service-record{font:600 9px/1.3 ui-monospace,monospace;letter-spacing:.04em;color:#776f5e}.primary-btn{min-height:38px;padding:0 14px;border:1px solid var(--ink);background:var(--ink);color:var(--paper);font:800 11px/1 ui-monospace,monospace;letter-spacing:.07em;text-transform:uppercase;cursor:pointer;box-shadow:3px 3px 0 var(--hazard)}.primary-btn:hover{background:#2a353b}.small-btn{min-height:34px;padding:0 10px;border:1px solid #8e8674;background:transparent;color:var(--paper-ink);font:700 10px/1 ui-monospace,monospace;letter-spacing:.06em;text-transform:uppercase;cursor:pointer}.small-btn:hover{border-color:var(--ink);background:#20221d14}.small-btn:disabled{opacity:.4;cursor:not-allowed}.report-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:8px}.report-head h2{flex:1;font-size:22px}.stamp{flex-shrink:0;transform:rotate(-3deg);padding:4px 7px;border:2px double currentColor;font:800 11px/1.15 ui-monospace,monospace;letter-spacing:.08em;text-align:center}.stamp.closed{color:#2c6b4c}.stamp.partial{color:#8a6411}.stamp.review{color:#a63827}.report-grid{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px 10px;padding:7px 0;border-top:1px dashed #a99f88;border-bottom:1px dashed #a99f88;font:600 10.5px/1.45 ui-monospace,monospace;color:#4d493e}.report-grid b{text-align:right;color:var(--paper-ink)}.damage{display:flex;align-items:baseline;justify-content:space-between;gap:10px;padding:7px 0 6px;border-bottom:2px solid var(--ink);font:700 9px/1.2 ui-monospace,monospace;letter-spacing:.1em;text-transform:uppercase;color:#6a6252}.damage strong{font:800 25px/1 ui-monospace,monospace;letter-spacing:-.03em;color:var(--paper-ink)}.report-notes{margin:8px 0;padding-left:16px;font-size:11px;color:#4a463a}.report-notes li{margin:3px 0}.save-note{min-height:13px;margin:0;font:600 9px/1.3 ui-monospace,monospace;color:#7b7361}.dock{display:grid;grid-template-columns:minmax(0,1fr);gap:5px;padding:6px;background:linear-gradient(180deg,#1a2429,#121a1e);border-top:1px solid var(--line)}.tools{display:flex;gap:4px;min-width:0}.tool{flex:1 1 0;min-width:0;min-height:var(--control-h, 46px);display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:6px;padding:4px 7px;border:1px solid var(--line);border-bottom:2px solid transparent;background:var(--ink-3);color:var(--text);cursor:pointer;text-align:left}.tool:hover:not(:disabled){border-color:var(--line-strong);background:#26343b}.tool.selected{border-color:var(--hazard);border-bottom-color:var(--hazard);background:#2a3129;box-shadow:inset 0 0 0 1px #f2b52f2e}.tool:disabled{opacity:.38;cursor:not-allowed}.tool-icon{width:22px;text-align:center;color:var(--hazard);font:700 19px/1 ui-monospace,monospace}.tool-text{display:grid;gap:1px;min-width:0}.tool-name{font:700 11.5px/1.15 ui-sans-serif,system-ui,sans-serif;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tool-key{color:var(--muted);font:600 8px/1 ui-monospace,monospace;letter-spacing:.08em}.tool-count{min-width:19px;height:19px;display:grid;place-items:center;align-self:center;border:1px solid var(--line-strong);background:#080c0eb3;color:var(--hazard);font:700 10px/1 ui-monospace,monospace}.dock-actions{display:flex;gap:4px;justify-content:flex-end}.dock .small-btn{min-height:calc(var(--control-h, 46px) - 4px);border-color:var(--line-strong);background:var(--ink-3);color:var(--text);white-space:nowrap}.dock .small-btn:hover:not(:disabled){border-color:var(--hazard);background:#26343b}.resume-btn{min-height:calc(var(--control-h, 46px) - 4px);padding:0 16px;border:1px solid var(--hazard-soft);background:var(--hazard);color:var(--ink);font:800 11px/1 ui-monospace,monospace;letter-spacing:.05em;text-transform:uppercase;cursor:pointer;box-shadow:2px 2px #00000073;white-space:nowrap}.resume-btn:hover:not(:disabled){background:var(--hazard-soft)}.resume-btn:disabled{opacity:.4;cursor:not-allowed;box-shadow:none}@container (min-width: 700px){.dock{grid-template-columns:minmax(0,1fr) auto;align-items:stretch;padding:7px 8px}.tool,.dock .small-btn,.resume-btn{min-height:var(--control-h, 46px)}}@container (max-width: 520px){.hud{gap:6px;padding:5px 7px}.case{gap:5px}.case-name{font-size:12px}.stat-k{display:none}.stat{padding:3px 5px}.tool-name{font-size:10.5px}}@container (max-width: 380px){.case-no{font-size:8px;padding:2px 4px}.case-name{font-size:11px;max-width:120px}.hud-actions{gap:3px}.icon-btn{width:26px;height:26px}.tool{padding:4px 5px;gap:4px}.tool-icon{width:17px;font-size:17px}.tool-count{min-width:16px;height:16px;font-size:9px}.resume-btn{padding:0 9px;font-size:10px}.card{padding:12px 13px}.card h2{font-size:19px}.legend{grid-template-columns:1fr}}@media(max-width:280px){.case-name,#tiltButton,.tool-key{display:none}.tool{grid-template-columns:minmax(0,1fr);place-items:center;padding:2px}.tool-icon{display:none}.tool-count{min-width:20px;height:20px;font-size:10px}.hud{justify-content:space-between}}@media(max-height:300px){.hud{gap:5px;padding:3px 6px;border-top-width:1px}.case-no{display:none}.case-name{font-size:10.5px;max-width:132px}.stat{padding:2px 4px}.stat-k{display:none}.stat b{font-size:10px}.icon-btn{width:23px;height:23px;font-size:12px}.stage{margin:2px}.dock{grid-template-columns:minmax(0,1fr) auto;gap:3px;padding:3px}.tools{gap:3px}.tool{min-height:32px;grid-template-columns:auto auto;gap:3px;padding:2px 4px}.tool-text{display:none}.tool-icon{width:14px;font-size:14px}.tool-count{min-width:14px;height:14px;font-size:8px}.dock-actions{gap:3px}.dock .small-btn{min-height:32px;padding:0 5px;font-size:8px}.resume-btn{min-height:32px;padding:0 7px;font-size:8.5px;letter-spacing:0}.freeze-badge{top:3px;left:3px;gap:3px;padding:2px 4px;font-size:7px}.freeze-dot{width:5px;height:5px}.toast{bottom:4px;padding:4px 6px;font-size:9.5px}.overlay{padding:4px}.card{padding:9px 10px;max-height:100%}.card h2{font-size:15px;line-height:1.05}.card-kicker{margin-bottom:4px;padding-bottom:3px;font-size:7px}.tagline,.brief-copy,.legend,.hint{display:none}.howto{margin:5px 0;padding:5px 6px;font-size:9px}.howto b{font-size:7px}.card-actions{margin-top:6px}.primary-btn{min-height:28px;padding:0 8px;font-size:8.5px}.small-btn{min-height:28px;font-size:8px}.report-grid,.report-notes{font-size:8.5px}.report-head h2{font-size:15px}.damage strong{font-size:18px}.service-record,.save-note{display:none}}@media(max-height:300px)and (max-width:440px){#tiltButton{display:none}}@media(prefers-reduced-motion:reduce){.freeze-dot{animation:none}.toast{transition:none}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
</style>
<meta name="napplet-type" content="my-napplet">
<meta name="napplet-config-schema" content="{"$schema":"http://json-schema.org/draft-07/schema#","$version":1,"type":"object","additionalProperties":false,"properties":{"controlSize":{"type":"integer","title":"Control height","description":"Height of the equipment dock controls in pixels. Compact for small frames, larger for touch.","minimum":34,"maximum":56,"default":46,"x-napplet-section":"Appearance","x-napplet-order":1},"trajectoryHints":{"type":"boolean","title":"Show trajectory hints","description":"Draw dotted projections while time is frozen. Turn off for a harder read.","default":true,"x-napplet-section":"Assistance","x-napplet-order":1}}}">
</head>
<body>
<main id="app" class="app">
<header class="hud">
<div class="case">
<span id="caseNo" class="case-no">CASE 01 / 05</span>
<strong id="caseName" class="case-name">The Kettle Incident</strong>
</div>
<div class="stats" aria-label="Incident status">
<span class="stat"><span class="stat-k">PEDS</span><b id="pedStat">0/1</b></span>
<span class="stat"><span class="stat-k">SORTED</span><b id="debrisStat">0/3</b></span>
<span class="stat"><span class="stat-k">KIT</span><b id="kitStat">4</b></span>
</div>
<div class="hud-actions">
<button id="briefButton" class="icon-btn" type="button" title="Case brief" aria-label="Show case brief">?</button>
<button id="retryButton" class="icon-btn" type="button" title="Retry incident" aria-label="Retry incident">↺</button>
</div>
</header>
<section class="stage" aria-label="Frozen incident scene">
<canvas id="scene" aria-label="A frozen city cross-section with airborne pedestrians and debris. Select equipment below, then tap the scene to place it."></canvas>
<div id="freezeBadge" class="freeze-badge" aria-hidden="true">
<span class="freeze-dot"></span><span id="freezeLabel">TIME SUSPENDED</span><span id="freezeClock">T+0.80</span>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<div id="overlay" class="overlay" hidden></div>
</section>
<footer class="dock">
<div class="tools" role="group" aria-label="Equipment">
<button class="tool" data-tool="trampoline" type="button" aria-pressed="true" title="Trampoline: lively bounce (1)">
<span class="tool-icon" aria-hidden="true">═</span>
<span class="tool-text"><span class="tool-name">Trampoline</span><span class="tool-key">1</span></span>
<span class="tool-count" data-count="trampoline">2</span>
</button>
<button class="tool" data-tool="funnel" type="button" aria-pressed="false" title="Funnel chute: smooth redirect (2)">
<span class="tool-icon" aria-hidden="true">╱</span>
<span class="tool-text"><span class="tool-name">Funnel</span><span class="tool-key">2</span></span>
<span class="tool-count" data-count="funnel">1</span>
</button>
<button class="tool" data-tool="cushion" type="button" aria-pressed="false" title="Sticky cushion: dead stop (3)">
<span class="tool-icon" aria-hidden="true">▬</span>
<span class="tool-text"><span class="tool-name">Cushion</span><span class="tool-key">3</span></span>
<span class="tool-count" data-count="cushion">1</span>
</button>
</div>
<div class="dock-actions">
<button id="tiltButton" class="small-btn" type="button" title="Rotate placement (R)">Tilt <span id="tiltValue">0°</span></button>
<button id="undoButton" class="small-btn" type="button" title="Undo last placement (Z)">Undo</button>
<button id="resumeButton" class="resume-btn" type="button" title="Resume time (Space)">Resume time</button>
</div>
</footer>
<p id="live" class="sr-only" aria-live="polite"></p>
</main>
</body>
</html>
