SOURCE / PINNED RELEASE
Made of little things.
Limb Race
- Release
- 6a8ae4fc626e…
- Author-recorded commit
- 008c01ee6454…
- License
- Check the original source
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-c2869f12ad8
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,viewport-fit=cover" /><meta name="theme-color" content="#eff1e9" /><title>Limb Race</title> <script type="module" crossorigin>var Ut=Object.defineProperty;var Bt=(e,t,i)=>t in e?Ut(e,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[t]=i;var I=(e,t,i)=>Bt(e,typeof t!="symbol"?t+"":t,i);function Ft(){const e=new Map;function t(s,n){if(e.has(s))throw new Error(`NAP domain "${s}" is already registered`);e.set(s,n)}function i(s){const n=s.type.indexOf(".");if(n<=0)return!1;const a=s.type.slice(0,n),h=e.get(a);return h?(h(s),!0):!1}function r(){return Array.from(e.keys())}return{registerNap:t,dispatch:i,getRegisteredDomains:r}}var zt=Ft(),Kt=zt.registerNap,Ht="theme";function Ct(){var t;const e=window;if(!((t=e.napplet)!=null&&t.theme))throw new Error("window.napplet.theme is unavailable -- runtime did not inject this domain");return e.napplet.theme}function Jt(){return Ct().get()}function Wt(e){return Ct().onChanged(e)}Kt(Ht,e=>{});function Xt(){const e=window;if(!e.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return e.napplet}function x(e){const i=Xt()[e];if(!i)throw new Error(`window.napplet.${String(e)} is unavailable -- runtime did not inject this domain`);return i}var De={getItem(e){return x("storage").getItem(e)},setItem(e,t){return x("storage").setItem(e,t)},removeItem(e){return x("storage").removeItem(e)},keys(){return x("storage").keys()},instance:{getItem(e){return x("storage").instance.getItem(e)},setItem(e,t){return x("storage").instance.setItem(e,t)},removeItem(e){return x("storage").instance.removeItem(e)},keys(){return x("storage").instance.keys()}}},Re={getPublicKey(){return x("identity").getPublicKey()},onChanged(e){return x("identity").onChanged(e)},getRelays(){return x("identity").getRelays()},getProfile(){return x("identity").getProfile()},getFollows(){return x("identity").getFollows()},getList(e){return x("identity").getList(e)},getZaps(){return x("identity").getZaps()},getMutes(){return x("identity").getMutes()},getBlocked(){return x("identity").getBlocked()},getBadges(){return x("identity").getBadges()}},et={get(){return x("config").get()},subscribe(e){return x("config").subscribe(e)},openSettings(e){x("config").openSettings(e)},registerSchema(e,t){return x("config").registerSchema(e,t)},onSchemaError(e){return x("config").onSchemaError(e)},get schema(){return x("config").schema}},tt={discover(e){return x("cvm").discover(e)},request(e,t,i){return x("cvm").request(e,t,i)},listTools(e,t){return x("cvm").listTools(e,t)},callTool(e,t,i,r){return x("cvm").callTool(e,t,i,r)},listResources(e,t){return x("cvm").listResources(e,t)},readResource(e,t,i){return x("cvm").readResource(e,t,i)},close(e){return x("cvm").close(e)},onEvent(e){return x("cvm").onEvent(e)},registry:{list(e){return x("cvm").registry.list(e)},has(e,t){return x("cvm").registry.has(e,t)},describe(e,t){return x("cvm").registry.describe(e,t)},call(e,t,i,r){return x("cvm").registry.call(e,t,i,r)}}},Vt={getEvent(e,t){return x("outbox").getEvent(e,t)},query(e,t){return x("outbox").query(e,t)},subscribe(e,t){return x("outbox").subscribe(e,t)},publish(e,t){return x("outbox").publish(e,t)},resolveRelays(e){return x("outbox").resolveRelays(e)}};function Zt(e,t){return!!(e!=null&&e[t])}function H(e){return Zt(window.napplet,e)}const Gt="http://json-schema.org/draft-07/schema#",Qt=1,en="object",tn=!1,nn={textSize:{type:"integer",title:"Text size",description:"Base text size in pixels. Changes apply while the napplet is running.",minimum:10,maximum:20,default:13,"x-napplet-section":"Appearance","x-napplet-order":1},controlSize:{type:"integer",title:"Control height",description:"Choose compact controls or larger touch targets, in pixels.",minimum:24,maximum:44,default:28,"x-napplet-section":"Appearance","x-napplet-order":2},selectText:{type:"boolean",title:"Allow text selection",description:"Allow selecting interface text. Text inputs always remain selectable.",default:!1,"x-napplet-section":"Interaction"}},rn={$schema:Gt,$version:Qt,type:en,additionalProperties:tn,properties:nn};H("config")&&(async()=>{et.schema||await et.registerSchema(rn);const e=et.subscribe(t=>{const i=document.documentElement.style,r=typeof t.textSize=="number"?t.textSize:13,s=typeof t.controlSize=="number"?t.controlSize:28;i.fontSize=`${r}px`,i.setProperty("--control-h",`${s}px`),i.setProperty("--napplet-text-selection",t.selectText===!0?"text":"none")});window.addEventListener("pagehide",()=>e.close(),{once:!0})})().catch(()=>{});const ee=2,fe=[{id:"meadow",name:"Meadow Run",difficulty:"Easy",description:"Rolling hills. A little hop. Find your feet.",color:"#a9d7a1",length:2600,ground:[[-600,300],[430,300],[620,250],[790,300],[1060,300],[1230,215],[1330,215],[1450,300],[1740,300],[1830,270],[1920,300],[2170,265],[2370,300],[3100,300]].map(([e,t])=>({x:e,y:t}))},{id:"steps",name:"Step It Up",difficulty:"Hard",description:"Square risers. Reach over the lip and pull yourself up.",color:"#b4b6e7",length:1800,ground:[[-600,300],[420,300],[420,205],[760,205],[760,110],[1080,110],[1330,300],[2300,300]].map(([e,t])=>({x:e,y:t}))},{id:"dunes",name:"Dune Dash",difficulty:"Hard",description:"Big ramps. Soft landings. Keep the momentum.",color:"#e9c477",length:2900,ground:[[-600,300],[400,300],[660,175],[710,175],[810,330],[1060,330],[1310,205],[1400,205],[1530,315],[1800,315],[2050,155],[2110,155],[2230,330],[2530,300],[3400,300]].map(([e,t])=>({x:e,y:t}))},{id:"cliffs",name:"High Society",difficulty:"Expert",description:"Tall ledges and sheer drops. Long legs earn their keep.",color:"#dbb193",length:2200,ground:[[-600,300],[500,300],[500,195],[840,195],[840,300],[1250,300],[1250,195],[1600,195],[1600,300],[2700,300]].map(([e,t])=>({x:e,y:t}))},{id:"sawtooth",name:"Sawtooth",difficulty:"Expert",description:"Uneven teeth. The next step never matches the last.",color:"#d5bf8b",length:3e3,ground:[[-600,300],[450,300],[450,190],[590,190],[690,290],[750,290],[750,175],[890,175],[1e3,305],[1080,305],[1080,185],[1190,185],[1310,295],[1400,295],[1400,180],[1510,180],[1640,310],[1770,310],[1770,190],[1900,190],[2030,300],[2160,300],[2160,175],[2300,175],[2460,300],[3400,300]].map(([e,t])=>({x:e,y:t}))},{id:"pinpoint",name:"Pinpoint",difficulty:"Expert",description:"Thin pillars. Short landings. Make every foot count.",color:"#b6cdb5",length:2700,ground:[[-600,300],[460,300],[460,195],[540,195],[540,300],[840,300],[840,190],[910,190],[910,300],[1180,300],[1180,185],[1240,185],[1240,300],[1520,300],[1520,180],[1620,180],[1620,300],[1970,300],[1970,180],[2040,180],[2040,300],[3100,300]].map(([e,t])=>({x:e,y:t}))},{id:"trenches",name:"Trench Trouble",difficulty:"Expert",description:"Deep cuts with tall exits. Bridge them or climb back out.",color:"#9acbcd",length:3200,ground:[[-600,300],[400,300],[800,185],[820,185],[820,310],[1010,310],[1010,185],[1240,185],[1240,310],[1440,310],[1440,185],[1700,185],[1700,310],[1910,310],[1910,185],[2200,185],[2200,310],[2440,310],[2440,185],[2600,185],[2850,300],[3600,300]].map(([e,t])=>({x:e,y:t}))},{id:"skyline",name:"Skyline",difficulty:"Extreme",description:"Towering 130-unit walls. Hooks and long reach earn the summit.",color:"#d4a8bc",length:3800,ground:[[-600,300],[500,300],[500,175],[820,175],[820,300],[1260,300],[1260,170],[1560,170],[1560,300],[2040,300],[2040,170],[2340,170],[2340,300],[2780,300],[2780,170],[3100,170],[3100,300],[4200,300]].map(([e,t])=>({x:e,y:t}))},{id:"switchback",name:"Ridge Runner",difficulty:"Extreme",description:"Steep ramps into ledges. Reach without losing your footing.",color:"#b0bbdd",length:3900,ground:[[-600,300],[360,300],[640,235],[800,235],[800,110],[920,110],[1080,300],[1170,300],[1450,230],[1640,230],[1640,105],[1790,105],[1990,300],[2080,300],[2350,230],[2550,230],[2550,100],[2720,100],[2950,300],[2980,300],[3130,260],[3310,260],[3310,130],[3410,130],[3520,300],[4300,300]].map(([e,t])=>({x:e,y:t}))},{id:"gauntlet",name:"The Gauntlet",difficulty:"Extreme",description:"Pillars, trenches, ramps and walls. One design. A long way home.",color:"#c8a78b",length:6e3,ground:[[-600,300],[470,300],[470,180],[570,180],[570,300],[930,300],[930,180],[1050,180],[1050,300],[1370,300],[1720,235],[1720,185],[1760,185],[1760,310],[1970,310],[1970,185],[2170,185],[2170,310],[2390,310],[2390,185],[2590,185],[2800,300],[2880,300],[3160,235],[3360,235],[3360,110],[3460,110],[3680,300],[4020,300],[4020,170],[4340,170],[4340,300],[4760,300],[4760,170],[5080,170],[5080,300],[6400,300]].map(([e,t])=>({x:e,y:t}))}],W=1/120,ne=58,ct=240,dt=8;function Z(e){return e.map(t=>t.map(i=>({...i})))}function Le(e){return Array.isArray(e)&&e.length<=dt&&e.every(t=>Array.isArray(t)&&t.length>=2&&t.every(i=>i&&Number.isFinite(i.x)&&Number.isFinite(i.y)&&Math.abs(i.x)<=1&&Math.abs(i.y)<=1))&&e.reduce((t,i)=>t+i.length,0)<=ct}function sn(e){if(e==="climber")return[[1,1],[-1,1],[-1,-1],[1,-1]].map(([i,r])=>[{x:0,y:0},{x:i,y:r}]);const t=e==="star"?5:3;return Array.from({length:t},(i,r)=>{const s=r/t*Math.PI*2-Math.PI/2;return[{x:0,y:0},{x:Math.cos(s)*.65,y:Math.sin(s)*.65},{x:Math.cos(s+.32)*.94,y:Math.sin(s+.32)*.94}]})}function wt(e,t=!0){const i=[{x:0,y:0,limb:-1,weight:0}];for(const[s,n]of e.entries()){const a=[{x:0,y:0},...n];for(let h=1;h<a.length;h++){const c=a[h-1],g=a[h],f=Math.hypot(g.x-c.x,g.y-c.y)*ne,y=Math.max(1,Math.ceil(f/5));for(let d=1;d<=y;d++){const b=(c.x+(g.x-c.x)*d/y)*ne,w=(c.y+(g.y-c.y)*d/y)*ne;i.push({x:b,y:w,limb:s,weight:Math.min(1.4,(b*b+w*w)/(ne*ne))})}}}if(!t)return i;const r=new Set;return i.filter(s=>{const n=`${s.limb}:${Math.round(s.x/4)},${Math.round(s.y/4)}`;return r.has(n)?!1:(r.add(n),!0)})}function be(e,t){var i;for(let r=1;r<e.ground.length;r++){const s=e.ground[r-1],n=e.ground[r],a=n.x-s.x;if(a>0&&t>=s.x&&t<n.x){const h=n.y-s.y,c=Math.hypot(a,h);return{y:s.y+h*(t-s.x)/a,nx:h/c,ny:-a/c}}}return{y:((i=e.ground.at(-1))==null?void 0:i.y)??300,nx:0,ny:-1}}function an(e,t,i,r){const s=i>=be(e,t).y;let n=1/0,a=0,h=0,c=0,g=-1;for(let y=1;y<e.ground.length;y++){const d=e.ground[y-1],b=e.ground[y],w=b.x-d.x,A=b.y-d.y,L=w*w+A*A;if(!L||!s&&(t<Math.min(d.x,b.x)-r||t>Math.max(d.x,b.x)+r||i<Math.min(d.y,b.y)-r||i>Math.max(d.y,b.y)+r))continue;const u=Math.max(0,Math.min(1,((t-d.x)*w+(i-d.y)*A)/L)),v=d.x+u*w,T=d.y+u*A,q=(t-v)**2+(i-T)**2;if(q<n){n=q,a=v,h=T;const Y=Math.sqrt(L);c=A/Y,g=-w/Y}}const f=Math.sqrt(n);if(!s&&f>=r)return null;if(f>1e-7){const y=s?-1:1;c=(t-a)/f*y,g=(i-h)/f*y}return{depth:s?r+f:r-f,nx:c,ny:g}}class ht{constructor(t,i,r="rigid"){I(this,"x",130);I(this,"y",210);I(this,"vx",0);I(this,"vy",0);I(this,"angle",0);I(this,"omega",0);I(this,"elapsed",0);I(this,"finished",!1);I(this,"failed",!1);I(this,"samples",[]);I(this,"visualSamples",[]);I(this,"inertia",1500);I(this,"furthestX",130);I(this,"lastProgressAt",0);I(this,"bends",[]);I(this,"motorDirection",1);this.track=t,this.design=i,this.material=r,this.setDesign(i)}get stalled(){return this.elapsed-this.lastProgressAt>6}setDesign(t){this.bends=t.map((i,r)=>this.bends[r]??{angle:0,velocity:0}),this.design=Z(t),this.samples=wt(t),this.visualSamples=wt(t,!1),this.inertia=Math.max(400,this.samples.reduce((i,r)=>i+r.x*r.x+r.y*r.y,0)/this.samples.length*.85)}visualDesign(){var i;if(this.material==="rigid")return this.design;const t=this.design.map(()=>[]);for(const r of this.visualSamples){if(r.limb<0)continue;const s=(((i=this.bends[r.limb])==null?void 0:i.angle)??0)*r.weight,n=Math.cos(s),a=Math.sin(s);t[r.limb].push({x:(r.x*n-r.y*a)/ne,y:(r.x*a+r.y*n)/ne})}return t}step(){var i;if(this.finished||this.failed)return;if(this.elapsed+=W,this.material==="bendy")for(const r of this.bends){const s=-120*r.angle-16*r.velocity;r.velocity+=s*W,r.velocity=Math.max(-12,Math.min(12,r.velocity)),r.angle+=r.velocity*W,Math.abs(r.angle)>.65&&(r.angle=Math.sign(r.angle)*.65,r.angle*r.velocity>0&&(r.velocity=0))}const t=120*W;this.omega+=Math.max(-t,Math.min(t,6.2*this.motorDirection-this.omega)),this.vy+=900*W,this.vx*=.9995,this.x+=this.vx*W,this.y+=this.vy*W,this.angle+=this.omega*W;for(let r=0;r<8;r++){let s;for(const v of this.samples){const T=this.angle+(this.material==="bendy"?(((i=this.bends[v.limb])==null?void 0:i.angle)??0)*v.weight:0),q=Math.cos(T),Y=Math.sin(T),U=v.x*q-v.y*Y,K=v.x*Y+v.y*q,J=an(this.track,this.x+U,this.y+K,v.x===0&&v.y===0?9:4);J&&(!s||J.depth>s.depth)&&(s={...J,rx:U,ry:K,limb:v.limb,weight:v.weight})}if(!s)break;const{depth:n,rx:a,ry:h,nx:c,ny:g,limb:f,weight:y}=s,d=this.material==="bendy"?this.bends[f]:void 0,b=this.inertia*2,w=d?y:0;this.x+=c*n*.85,this.y+=g*n*.85;const A=this.omega+((d==null?void 0:d.velocity)??0)*w,L=(this.vx-A*h)*c+(this.vy+A*a)*g,u=a*g-h*c;if(L<0){const v=u*w,T=-L/(1+u*u/this.inertia+v*v/b);this.vx+=c*T,this.vy+=g*T,this.omega+=u*T/this.inertia,d&&(d.velocity+=v*T/b);const q=-g,Y=c,U=a*Y-h*q,K=this.omega+((d==null?void 0:d.velocity)??0)*w,J=(this.vx-K*h)*q+(this.vy+K*a)*Y,ae=Math.max(-T*.95,Math.min(T*.95,-J/(1+U*U/this.inertia+(U*w)**2/b)));this.vx+=q*ae,this.vy+=Y*ae,this.omega+=U*ae/this.inertia,d&&(d.velocity+=U*w*ae/b)}}this.vx=Math.max(-500,Math.min(650,this.vx)),this.vy=Math.max(-650,Math.min(850,this.vy)),this.x>this.furthestX+10&&(this.furthestX=this.x,this.lastProgressAt=this.elapsed),this.x>=this.track.length&&(this.finished=!0),(this.elapsed>=180||this.x<-450||this.y>900)&&(this.failed=!0)}}function Ce(e){return`${(e/1e3).toFixed(2)}s`}function ut(e){const{width:t,height:i}=e.getBoundingClientRect(),r=Math.min(2,window.devicePixelRatio||1);(e.width!==Math.round(t*r)||e.height!==Math.round(i*r))&&(e.width=Math.round(t*r),e.height=Math.round(i*r));const s=e.getContext("2d");return s.setTransform(r,0,0,r,0,0),{ctx:s,w:t,h:i}}function ft(e,t,i,r,s,n=0,a="#263c39"){e.save(),e.translate(i,r),e.rotate(n),e.strokeStyle=a,e.lineWidth=s*.13,e.lineCap="round",e.lineJoin="round";for(const h of t){e.beginPath(),e.moveTo(0,0);for(const c of h)e.lineTo(c.x*s,c.y*s);e.stroke()}e.beginPath(),e.arc(0,0,s*.165,0,Math.PI*2),e.fillStyle="#f35748",e.fill(),e.lineWidth=2,e.strokeStyle="#fff5e9",e.stroke(),e.restore()}function Ie(e,t,i){const{ctx:r,w:s,h:n}=ut(e),a=Math.min(s,n)*.43;r.clearRect(0,0,s,n),r.fillStyle="#fffefa",r.fillRect(0,0,s,n),r.fillStyle="#d9ded6";for(let h=s/2%20;h<s;h+=20)for(let c=n/2%20;c<n;c+=20)r.beginPath(),r.arc(h,c,.75,0,7),r.fill();r.strokeStyle="#d7ded5",r.lineWidth=1,r.setLineDash([4,6]),r.strokeRect(s/2-a,n/2-a,a*2,a*2),r.setLineDash([]),ft(r,t,s/2,n/2,a,0,i?"#86938a":"#263c39"),t.length||(r.fillStyle="#6d7a72",r.textAlign="center",r.font='12px "Avenir Next", sans-serif',s>200&&r.fillText("Start at the red dot.",s/2,n-5))}function on(e,t,i,r,s){const{ctx:n,w:a,h}=ut(e);if(!a||!h)return;const c=Math.max(.45,Math.min(1.35,h/355)),g=a/c,f=Math.max(0,Math.min(t.length-g*.65,i.x-g*.27)),y=h*.78-300*c,d=i.y*c+y,b=y+Math.max(0,h*.3-d)-Math.max(0,d-h*.65);n.fillStyle=s?"#253d41":"#e9f1e8",n.fillRect(0,0,a,h),n.save(),n.translate(-f*c*.18,0),n.fillStyle=s?"#304e4d":"#dce8d1";for(let u=-1;u<12;u++)n.beginPath(),n.ellipse(u*290+150,h*.68,230,h*.36,0,Math.PI,0),n.fill();n.fillStyle=s?"#3f5855":"#f9fbef";for(let u=0;u<9;u++){const v=u*350+160,T=h*.18+u%3*19;n.beginPath(),n.roundRect(v,T,77,13,7),n.fill(),n.beginPath(),n.ellipse(v+37,T,22,10,0,Math.PI,0),n.fill()}n.restore(),n.save(),n.translate(-f*c,b),n.scale(c,c),n.beginPath(),n.moveTo(t.ground[0].x,700);for(const u of t.ground)n.lineTo(u.x,u.y);n.lineTo(t.ground.at(-1).x,700),n.closePath(),n.fillStyle=s?"#637e60":t.color,n.fill(),n.save(),n.clip(),n.strokeStyle=s?"#73906b":"#91b78a",n.globalAlpha=.35,n.lineWidth=1;for(let u=Math.floor(f/28)*28;u<f+g+100;u+=28)n.beginPath(),n.moveTo(u,150),n.lineTo(u-180,700),n.stroke();n.restore(),n.beginPath();for(const[u,v]of t.ground.entries())u===0?n.moveTo(v.x,v.y):n.lineTo(v.x,v.y);n.strokeStyle=s?"#b9d0a2":"#3e6852",n.lineWidth=4,n.stroke();for(let u=350;u<t.length;u+=400){const v=be(t,u).y;n.fillStyle=s?"#b4c7b5":"#628268",n.font="10px monospace",n.textAlign="center",n.fillText(`${Math.round((t.length-u)/10)} m`,u,v+27)}for(let u=270;u<t.length;u+=277){const v=be(t,u).y;n.strokeStyle=s?"#a3bd8c":"#557b50",n.lineWidth=2,n.beginPath(),n.moveTo(u,v),n.lineTo(u-4,v-9),n.moveTo(u,v),n.lineTo(u+5,v-13),n.stroke()}const w=be(t,130).y;n.fillStyle=s?"#b9d0ba":"#50775d",n.font="bold 10px monospace",n.fillText("START",116,w+35),n.strokeStyle="#50775d",n.setLineDash([4,4]),n.lineWidth=1,n.beginPath(),n.moveTo(130,w),n.lineTo(130,w+18),n.stroke(),n.setLineDash([]);const A=be(t,t.length).y;n.fillStyle="#35493e",n.fillRect(t.length,A-110,4,110);for(let u=0;u<4;u++)for(let v=0;v<5;v++)n.fillStyle=(v+u)%2?"#fffdf0":"#35493e",n.fillRect(t.length+4+v*9,A-110+u*9,9,9);if(n.fillStyle=s?"#d7dfcb":"#3b5a48",n.font="bold 11px monospace",n.fillText("FINISH",t.length+24,A+26),r&&Math.abs(i.vx)>60){n.strokeStyle=s?"#89a69b":"#b0c2a5",n.lineWidth=2;for(let u=0;u<3;u++)n.beginPath(),n.moveTo(i.x-65-u*9,i.y-10+u*13),n.lineTo(i.x-85-u*9,i.y-10+u*13),n.stroke()}const L=be(t,i.x).y;n.fillStyle="#23362918",n.beginPath(),n.ellipse(i.x,L+3,33,5,0,0,7),n.fill(),ft(n,i.visualDesign(),i.x,i.y,ne,i.angle,s?"#f2f3d9":"#263c39"),r||(n.fillStyle=s?"#d9e6d0":"#506b59",n.font='11px "Avenir Next", sans-serif',n.textAlign="center",n.fillText("YOUR INVENTION",i.x,i.y-77)),n.restore()}function ln(e){const t=document.createElement("canvas");return t.width=84,t.height=72,t.className="limb-thumb",t.setAttribute("aria-label","Recorded limb drawing"),ft(t.getContext("2d"),e,42,36,27),t}function cn(e){if(!e.length||!Le(e))throw new Error("This drawing cannot be shared.");return{version:1,physics:ee,limbs:e.map(t=>t.map(i=>[Math.round(i.x*1e3),Math.round(i.y*1e3)]))}}function dn(e){const t=e;if(!t||t.version!==1||t.physics!==ee||!Array.isArray(t.limbs)||!t.limbs.length||t.limbs.length>8)throw new Error("This run uses an unsupported drawing format.");let i=0;const r=t.limbs.map(s=>{if(!Array.isArray(s)||s.length<2||(i+=s.length)>240)throw new Error("The shared drawing is too large.");return s.map(n=>{if(!Array.isArray(n)||n.length!==2||!n.every(a=>Number.isSafeInteger(a)&&Math.abs(a)<=1e3))throw new Error("The shared drawing contains invalid points.");return{x:n[0]/1e3,y:n[1]/1e3}})});if(!Le(r))throw new Error("The shared drawing is invalid.");return r}const At=["flat","uphill","downhill","step","drop","trench"],xt={flat:"Flat",uphill:"Uphill",downhill:"Downhill",step:"Step up",drop:"Drop",trench:"Trench"};function hn(){return{physics:ee,pieces:[{kind:"uphill",width:300,height:60},{kind:"downhill",width:300,height:60},{kind:"flat",width:200,height:0}]}}function V(e){const t=e;if(!t||t.physics!==ee||!Array.isArray(t.pieces)||!t.pieces.length||t.pieces.length>24)throw new Error("A level needs 1–24 terrain pieces and the current physics rules.");let i=300,r=0;const s=t.pieces.map(n=>{if(!n||!At.includes(n.kind)||!Number.isSafeInteger(n.width)||n.width<40||n.width>600||!Number.isSafeInteger(n.height)||n.height<0||n.height>130||n.kind!=="flat"&&n.height<5)throw new Error("Use widths of 40–600 and heights of 5–130.");r+=n.width;const a=n.kind==="flat"?0:n.height;if((n.kind==="uphill"||n.kind==="step")&&(i-=a),(n.kind==="downhill"||n.kind==="drop")&&(i+=a),i<60||i>460||n.kind==="trench"&&i+a>460)throw new Error("Terrain is too high or too low. Balance climbs with descents.");return{kind:n.kind,width:n.width,height:a}});if(r>8e3)throw new Error("Keep the terrain below 8,000 units long.");return{physics:ee,pieces:s}}function He(e){const t=V(e.data);let i=400,r=300;const s=[{x:-600,y:r},{x:i,y:r}];for(const a of t.pieces)(a.kind==="step"||a.kind==="drop")&&(r+=a.kind==="step"?-a.height:a.height,s.push({x:i,y:r})),a.kind==="trench"&&s.push({x:i,y:r+a.height}),i+=a.width,(a.kind==="uphill"||a.kind==="downhill")&&(r+=a.kind==="uphill"?-a.height:a.height),a.kind==="trench"&&s.push({x:i,y:r+a.height}),s.push({x:i,y:r});const n=i+250;return s.push({x:n+500,y:r}),{id:e.key,name:e.title,difficulty:"Custom",description:"A player-made course. Find your own way through.",color:"#a6ced0",length:n,ground:s}}function un(e){const t=e;if(!t||typeof t.key!="string"||!/^(?:draft|custom):[a-zA-Z0-9:_-]{1,150}$/.test(t.key)||typeof t.title!="string"||!t.title.trim()||t.title.length>160||/[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/.test(t.title))throw new Error("Invalid saved level.");return{key:t.key,title:t.title,data:V(t.data)}}const fn="naddr1qvzqqqyf8ypzqw49s9e88sajl985jxzquprj7pyapugqp83rme3sqctxhj5mxm4rqqxkuttrxgurvwtxxyexzepc2vmt5l",pn={pubkey:"60517f64548279854ee9a122531e4d1a413e3dc956e2d0a450217420623ca688",relays:["wss://relay.napplet.soy/"]},gn=["meadow-classic-rigid-v2-draw1","meadow-classic-bendy-v2-draw1","meadow-live-rigid-v2-draw1","meadow-live-bendy-v2-draw1","steps-classic-rigid-v2-draw1","steps-classic-bendy-v2-draw1","steps-live-rigid-v2-draw1","steps-live-bendy-v2-draw1","dunes-classic-rigid-v2-draw1","dunes-classic-bendy-v2-draw1","dunes-live-rigid-v2-draw1","dunes-live-bendy-v2-draw1","cliffs-classic-rigid-v2-draw1","cliffs-classic-bendy-v2-draw1","cliffs-live-rigid-v2-draw1","cliffs-live-bendy-v2-draw1"],Ue={napplet:fn,provider:pn,boards:gn},Mt="limb-race-v2";function Ze(e,t,i){return`${e}-${t}-${i}-v${ee}-draw1`}function Ae(e,t,i){return Ue.boards.includes(Ze(e,t,i))}async function Oe(e,t={}){var s;if(!H("cvm"))throw new Error("Shared scores are unavailable in this host. Personal races still work.");if(!await tt.registry.has("soy.boards.v2"))throw new Error("Shared drawings need an updated score service (soy.boards.v2). Your race is kept locally.");const i=Ue.provider,r=i?await tt.callTool(i,e,t):await tt.registry.call("soy.boards.v2",e,t);if(r.isError)throw new Error(((s=r.content)==null?void 0:s.filter(n=>n.type==="text").map(n=>n.text).join(" ").slice(0,180))||"Score service unavailable. Try again.");return r.structuredContent}async function mn(e,t,i,r=0){if(!Ae(e,t,i))throw new Error("This course keeps personal records only.");const s=await Oe("soy_board_read",{napplet:Ue.napplet,board:Ze(e,t,i),limit:20,offset:r});if(!s||!Array.isArray(s.rows))throw new Error("The score service returned an unreadable board.");return{...s,rows:s.rows.filter(n=>Number.isFinite(n.score)&&n.score>0&&n.score<=18e4&&typeof n.actor=="string"&&typeof n.name=="string"&&typeof n.revision=="string"&&typeof n.hasData=="boolean").sort((n,a)=>n.score-a.score)}}async function yn(e){if(!Ae(e.track,e.mode,e.material))throw new Error("This course keeps personal records only.");await Oe("soy_board_submit",{napplet:Ue.napplet,board:Ze(e.track,e.mode,e.material),score:e.ms,name:e.name.slice(0,40),data:cn(e.design)})}async function vn(e,t,i,r){const s=await Oe("soy_board_entry",{napplet:Ue.napplet,board:Ze(e,t,i),actor:r.actor,revision:r.revision});if(s!=null&&s.stale)throw new Error("This racer has a newer best. Refresh the board and select it again.");const n=s==null?void 0:s.entry;if(!n||n.revision!==r.revision||n.actor!==r.actor||n.score!==r.score||typeof n.name!="string"||!Number.isFinite(n.updated))throw new Error("This run is no longer available. Refresh the board.");return{id:n.revision,track:e,mode:t,material:i,physics:ee,ms:n.score,name:n.name,actor:n.actor,created:n.updated,design:dn(n.data),edits:[],shared:!0}}function bn(e){if(fe.some(t=>t.id===e.track))return!0;try{return!!e.level&&un(e.level).key===e.track}catch{return!1}}async function wn(){if(!H("storage"))return null;const e=await De.getItem(Mt),t=e?null:await De.getItem("limb-race-v1");if(!e&&!t)return null;const i=JSON.parse(e??t);return e||(i.runs=[]),{runs:Array.isArray(i.runs)?i.runs.filter(r=>r&&typeof r.id=="string"&&typeof r.name=="string"&&Number.isFinite(r.ms)&&r.ms>0&&r.ms<=18e4&&bn(r)&&r.physics===ee&&["rigid","bendy"].includes(r.material)&&["classic","live"].includes(r.mode)&&Le(r.design)&&Array.isArray(r.edits)&&r.edits.length<=100&&r.edits.every(s=>Number.isFinite(s.at)&&Le(s.design))).slice(0,40):[],design:Le(i.design)?i.design:[],name:typeof i.name=="string"?i.name.slice(0,24):"Guest racer"}}async function xn(e,t,i){if(!H("storage"))return!1;let r=e.slice(0,40),s=JSON.stringify({runs:r,design:t,name:i});for(;s.length>23e4&&r.length>1;)r=r.slice(0,-1),s=JSON.stringify({runs:r,design:t,name:i});return await De.setItem(Mt,s),!0}const de="soy.app-data/1",ce=30078,pt=16*1024,Rt=/^[a-f0-9]{64}$/;function Je(e,t,i){if(typeof e!="string"||typeof t!="string"||typeof i!="string"||!Rt.test(e)||!/^[a-z][a-z0-9_-]{0,47}$/.test(t)||!/^[a-zA-Z0-9_-]{1,64}$/.test(i))throw new Error("app-data-invalid-id: use a collection name and a stable record ID.");return`${de}:${e}:${t}:${i}`}function Lt(e){let t=0;const i=new Set,r=(s,n)=>{if(++t>4096||n>12)throw new Error("app-data-too-complex: simplify the data or use Blossom.");if(!(s===null||typeof s=="boolean")&&!(typeof s=="number"&&Number.isFinite(s))&&!(typeof s=="string"&&s.length<=pt)){if(typeof s!="object"||s===null||i.has(s))throw new Error("app-data-invalid-json: data must contain only finite JSON values.");if(i.add(s),Array.isArray(s)){if(s.length>1024)throw new Error("app-data-too-complex: arrays are limited to 1024 items.");for(const a of s)r(a,n+1)}else{if(Object.getPrototypeOf(s)!==Object.prototype&&Object.getPrototypeOf(s)!==null)throw new Error("app-data-invalid-json: use plain JSON objects.");const a=Object.keys(s);if(a.length>128||a.some(h=>h.length>128||["__proto__","constructor","prototype"].includes(h)))throw new Error("app-data-invalid-json: invalid object keys or too many fields.");for(const h of a)r(s[h],n+1)}i.delete(s)}};r(e,0)}function rt(e,t,i,r,s){return[["d",Je(e,t,i)],["s",e],["c",t],["L",de],["l",r,de],["v",String(s)]]}function nt(e){var g;if(e.kind!==ce||typeof e.content!="string"||new TextEncoder().encode(e.content).length>pt)throw new Error("app-data-invalid-record: expected a kind-30078 record of at most 16 KiB.");let t;try{t=JSON.parse(e.content)}catch{throw new Error("app-data-invalid-json: record content must be JSON.")}if(Lt(t),!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).sort().join(",")!=="data,deleted,previous,schema,title,version"||typeof t.schema!="string"||!/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,95}$/.test(t.schema)||!Number.isSafeInteger(t.version)||t.version<1||t.version>1e6||typeof t.title!="string"||!t.title.trim()||t.title.length>160||/[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/.test(t.title)||t.previous!==null&&(typeof t.previous!="string"||!Rt.test(t.previous))||typeof t.deleted!="boolean"||t.deleted&&(t.data!==null||t.previous===null))throw new Error("app-data-invalid-record: check schema, version, title, previous revision and deletion fields.");if(!Array.isArray(e.tags)||e.tags.length!==6)throw new Error("app-data-invalid-tags: use the documented application-data tags.");const i=((g=e.tags.find(f=>f[0]==="d"))==null?void 0:g[1])??"",r=i.split(":"),[s,n,a,h]=r;if(r.length!==4||s!==de)throw new Error("app-data-invalid-id: unsupported record identifier.");if(!rt(n,a,h,t.schema,t.version).every(f=>e.tags.filter(y=>JSON.stringify(y)===JSON.stringify(f)).length===1))throw new Error("app-data-invalid-tags: record tags must match its schema and namespace.");return{scope:n,collection:a,id:h,identifier:i,body:t}}async function En(e){var h;const t=e.host??globalThis.napplet;if(!((h=t==null?void 0:t.shell)!=null&&h.onReady)||!t.outbox||!t.identity)throw new Error("app-data-unavailable: this host needs shell, outbox and identity support.");const i=await new Promise((c,g)=>{let f,y=!1;const d=setTimeout(()=>{f==null||f.close(),g(new Error("app-data-unavailable: host initialization did not finish. Keep drafts locally and retry."))},5e3);f=t.shell.onReady(b=>{var w;y=!0,clearTimeout(d),f==null||f.close(),c((w=b.capabilities)==null?void 0:w.appData)}),y&&f.close()});if((i==null?void 0:i.profile)!==de)throw new Error("app-data-unavailable: update soyLI or use a host supporting soy.app-data/1. Keep drafts locally.");const r=e.relays??i.defaultRelays;if(!Array.isArray(r)||!r.length||r.length>8)throw new Error("app-data-no-relays: choose a storage relay in host Network settings.");Je(i.scope,e.collection,"check"),nt({kind:ce,tags:rt(i.scope,e.collection,"check",e.schema,e.version),content:JSON.stringify({schema:e.schema,version:e.version,title:"Check",previous:null,deleted:!1,data:null})});const s=c=>{const{scope:g,collection:f,id:y,body:d}=nt(c);if(d.schema!==e.schema||d.version!==e.version||f!==e.collection)throw new Error("app-data-incompatible: this record uses another collection or schema version.");return{id:y,author:c.pubkey,scope:g,collection:f,title:d.title,revision:c.id,createdAt:c.created_at,deleted:d.deleted,data:d.deleted?null:e.validate(d.data),event:c}},n=async c=>{const g=await t.outbox.query(c,{relays:r,timeoutMs:8e3});if(g.error)throw new Error(g.error);return g},a=c=>c.sort((g,f)=>f.created_at-g.created_at||g.id.localeCompare(f.id));return{policy:i,relays:r,async list(c={}){var L;const g=c.scope??i.scope;Je(g,e.collection,"check");const f=c.limit??40;if(!Number.isSafeInteger(f)||f<1||f>100||c.until!==void 0&&(!Number.isSafeInteger(c.until)||c.until<0))throw new Error("app-data-invalid-query: limit must be 1–100 and until a Unix timestamp.");const y=await n([{kinds:[ce],"#s":[g],"#c":[e.collection],"#L":[de],limit:f,...c.until===void 0?{}:{until:c.until}}]),d=new Map;for(const{event:u}of y.events){const v=(L=u.tags.find(Y=>Y[0]==="d"))==null?void 0:L[1];if(u.kind!==ce||!(v!=null&&v.startsWith(`${de}:${g}:${e.collection}:`)))continue;const T=`${u.pubkey}:${v}`,q=d.get(T);(!q||a([q,u])[0]===u)&&d.set(T,u)}const b=a([...d.values()]).slice(0,f),w=[];let A=0;for(const u of b)try{const v=s(u);v.deleted||w.push(v)}catch{A++}return{records:w,incomplete:!!y.incomplete,invalid:A,nextUntil:b.length>=f?b.at(-1).created_at:null}},async get(c,g,f=i.scope){if(!/^[a-f0-9]{64}$/.test(c))throw new Error("app-data-invalid-author: expected a public key.");const y=Je(f,e.collection,g),d=await n([{kinds:[ce],authors:[c],"#d":[y],limit:1}]),b=a(d.events.map(w=>w.event).filter(w=>w.kind===ce&&w.pubkey===c&&w.tags.some(A=>A[0]==="d"&&A[1]===y)))[0];return{record:b?s(b):null,incomplete:!!d.incomplete}},async prepare(c){const g=await t.identity.getPublicKey();if(!/^[a-f0-9]{64}$/.test(g))throw new Error("not-signed-in: connect a viewer identity to publish.");const{base:f}=c;if(f&&(f.author!==g||f.scope!==i.scope||f.collection!==e.collection||f.id!==c.id))throw new Error("app-data-owner-mismatch: copy this creation to a new ID to make it your own.");const y={schema:e.schema,version:e.version,title:c.title,previous:(f==null?void 0:f.revision)??null,deleted:c.deleted??!1,data:null};if(!y.deleted){const b=e.validate(c.data);Lt(b),y.data=b}const d={kind:ce,created_at:Math.max(Math.floor(Date.now()/1e3),((f==null?void 0:f.createdAt)??0)+1),tags:rt(i.scope,e.collection,c.id,e.schema,e.version),content:JSON.stringify(y)};if(nt(d),new TextEncoder().encode(d.content).length>Math.min(i.maxContentBytes,pt))throw new Error("app-data-too-large: store the large payload on Blossom and publish its URL/hash.");return{template:structuredClone(d),async publish(){if(await t.identity.getPublicKey()!==g)throw new Error("app-data-identity-changed: prepare the change again with the selected identity.");const b=await t.outbox.publish(structuredClone(d),{relays:r,toOutbox:!1});if(!b.ok||!b.event)throw new Error(b.error??"app-data-publish-failed: no signed record returned.");if(b.event.pubkey!==g||b.event.kind!==d.kind||b.event.created_at!==d.created_at||b.event.content!==d.content||JSON.stringify(b.event.tags)!==JSON.stringify(d.tags))throw new Error("app-data-invalid-result: host returned a different record.");return s(b.event)}}}}}const kn=["wss://relay.napplet.soy"],st={relays:kn},Et="limb-race-levels-v1",Sn=()=>{var e,t;return((t=(e=globalThis.crypto)==null?void 0:e.randomUUID)==null?void 0:t.call(e))??`level-${Date.now()}-${Math.random().toString(36).slice(2)}`};function it(e=hn(),t="My new track"){return{id:Sn(),title:t,version:1,data:V(e),base:null}}function Fe(e){return{key:`draft:${e.id}:${e.version}`,title:e.title.trim()||"Untitled track",data:V(e.data)}}function kt(e){if(e.deleted||!e.data)throw new Error("This level was unpublished.");return{key:`custom:${e.revision}`,title:e.title,data:V(e.data)}}async function Tn(){try{return H("identity")?await Re.getPublicKey():""}catch{return""}}class Cn{constructor(){I(this,"drafts",[]);I(this,"saves",Promise.resolve())}async load(){if(!H("storage"))return;const t=await De.getItem(Et);if(!t)return;const i=JSON.parse(t);if(!Array.isArray(i))throw new Error("Saved levels could not be read.");this.drafts=i.slice(0,20).flatMap(r=>{try{if(!r||typeof r.id!="string"||!/^[a-zA-Z0-9_-]{1,64}$/.test(r.id)||typeof r.title!="string"||r.title.length>60||!Number.isSafeInteger(r.version)||r.version<1)return[];const s=V(r.data),n=r.base&&typeof r.base.author=="string"&&/^[a-f0-9]{64}$/.test(r.base.revision)?r.base:null;return[{id:r.id,title:r.title,version:r.version,data:s,base:n}]}catch{return[]}})}async save(t){const i=this.drafts.findIndex(s=>s.id===t.id);if(i<0&&this.drafts.length>=20)throw new Error("You have 20 drafts. Remove one before creating another.");const r=structuredClone(t);return i<0?this.drafts.unshift(r):this.drafts[i]=r,this.persist()}async remove(t){return this.drafts=this.drafts.filter(i=>i.id!==t),this.persist()}async persist(){if(!H("storage"))return!1;const t=JSON.stringify(this.drafts),i=this.saves.catch(()=>{}).then(()=>De.setItem(Et,t));return this.saves=i,await i,!0}}let ze;function gt(){if(!ze){const e=globalThis.napplet;ze=En({collection:"levels",schema:"limb-race.level",version:1,validate:V,relays:st.relays.length?st.relays.slice(0,1):void 0,host:{...e,identity:Re,outbox:Vt}}).catch(t=>{throw ze=void 0,t})}return ze}async function Nt(e){const t=await gt(),i=await t.get(e.author,e.id,e.scope);if(i.incomplete)throw new Error("Some relays did not respond. Retry before editing this level.");if(!i.record)throw new Error("This level is unavailable. Your draft is kept.");return i.record}class An{constructor(){I(this,"pending",null)}clear(){this.pending=null}async publish(t,i=!1){var n,a;const r=JSON.stringify([t.id,t.title,t.data,(n=t.base)==null?void 0:n.revision,i]);if(((a=this.pending)==null?void 0:a.fingerprint)!==r){const h=await gt();if(t.base&&(await Nt(t.base)).revision!==t.base.revision)throw new Error("This level changed elsewhere. Open the latest version in Community, or make a copy of your draft.");const c=await h.prepare({id:t.id,title:t.title.trim(),data:t.data,base:t.base,deleted:i});this.pending={fingerprint:r,change:c}}const s=await this.pending.change.publish();return this.pending=null,s}}function le(e){const t=e instanceof Error?e.message:"Could not reach the level library. Retry.";return/not-signed-in|identity-required/.test(t)?"Connect your Nostr identity in the host to publish. Your draft is kept.":/app-data-unavailable/.test(t)?"This host needs the updated public-level sharing support. Your draft is kept locally; try an updated host.":/relay-not-configured/.test(t)?`Add ${st.relays[0]||"a level storage relay"} in the host Network settings, then retry. Your draft is kept.`:/identity-changed|owner-mismatch/.test(t)?"The selected identity changed. Use the original identity, or make a copy to publish as yourself.":/stale|app-data-conflict/.test(t)?"This level changed elsewhere. Open the latest version in Community, or make a copy of your draft.":/denied|reject|cancel/.test(t)?"Publishing was not approved. Your draft is kept; you can retry.":t.slice(0,240)}const m=e=>document.getElementById(e);function te(e,t){const i=document.createElement("button");return i.textContent=e,i.addEventListener("click",t),i}function Mn(e){const t=new Cn,i=new An,r=m("level-editor"),s=m("tracks-dialog");let n=null,a=0,h="builtin",c=!1,g=!1,f=0,y=[],d=null,b="",w="";const A=t.load().catch(p=>{w=le(p)}),L=p=>{m("level-status").textContent=p},u=p=>{m("level-list-status").textContent=p};function v(p){c=p;for(const l of r.querySelectorAll("button,input,select"))l.disabled=p}async function T(p=!1){if(n)try{const l=await t.save(n);p&&L(l?"Draft saved. Test it before publishing.":"Draft kept for this session. Persistent storage is unavailable.")}catch(l){L(`Draft kept for this session. ${le(l)}`)}}function q(){n&&(n.version++,i.clear(),T(),Y())}function Y(){if(!n)return;const p=m("level-canvas"),{ctx:l,w:E,h:S}=ut(p),C=He(Fe(n)),M=E/(C.length+100),ye=(S-36)/430,j=F=>16+(F-50)*ye;l.clearRect(0,0,E,S),l.fillStyle="#edf2e8",l.fillRect(0,0,E,S);const Be=400+n.data.pieces.slice(0,a).reduce((F,ve)=>F+ve.width,0);l.fillStyle="#d3eb8899",l.fillRect(Be*M,0,n.data.pieces[a].width*M,S),l.beginPath(),l.moveTo(0,S);for(const F of C.ground)l.lineTo(F.x*M,j(F.y));l.lineTo(E,S),l.closePath(),l.fillStyle="#a6ced0",l.fill(),l.beginPath();for(const[F,ve]of C.ground.entries())F===0?l.moveTo(ve.x*M,j(ve.y)):l.lineTo(ve.x*M,j(ve.y));l.strokeStyle="#3e6852",l.lineWidth=2,l.stroke(),l.fillStyle="#f35748",l.beginPath(),l.arc(130*M,j(300)-9,5,0,7),l.fill(),l.fillStyle="#263c39",l.font="10px monospace",l.fillText("START",6,S-7),l.textAlign="right",l.fillText("FINISH",E-6,S-7),l.textAlign="left",m("level-length").textContent=`${n.data.pieces.length} / 24 pieces · ${Math.round(C.length/10)} m`}function U(){if(!n)return;const p=n.data.pieces[a];m("piece-kind").value=p.kind,m("piece-width").value=String(p.width),m("piece-height").value=String(p.height),m("piece-height").disabled=p.kind==="flat",m("piece-left").disabled=a===0,m("piece-right").disabled=a===n.data.pieces.length-1,m("piece-remove").disabled=n.data.pieces.length===1,m("unpublish-level").hidden=!n.base||n.base.deleted,m("publish-level").textContent=n.base&&!n.base.deleted?"Publish update":"Publish level";const l=m("piece-list");l.replaceChildren(),n.data.pieces.forEach((E,S)=>{const C=te(`${S+1} ${xt[E.kind]}`,()=>{a=S,U(),Y()});C.className="piece-chip",C.setAttribute("aria-pressed",String(S===a)),l.append(C)}),Y()}async function K(p){if(await A,!t.drafts.some(l=>l.id===p.id)&&t.drafts.length>=20){const l="You have 20 drafts. Remove one in Your levels before creating another.";r.open?L(l):(await vt("yours"),u(l));return}n=structuredClone(p),n.title=n.title.slice(0,60),a=0,i.clear(),m("level-name").value=n.title,s.close(),r.showModal(),U(),L("Edit the terrain, test your track, then publish it to Community."),await T()}function J(p,l=a){if(!(!n||c))try{n.data=V({physics:n.data.physics,pieces:p}),a=Math.max(0,Math.min(l,p.length-1)),q(),U(),L("Terrain updated. Test it to find out how it races.")}catch(E){L(le(E))}}for(const p of At)m("piece-palette").append(te(`+ ${xt[p]}`,()=>{if(!n||c)return;const l=p==="uphill"||p==="downhill"?300:180,E=p==="flat"?0:p==="uphill"||p==="downhill"?60:95;J([...n.data.pieces,{kind:p,width:l,height:E}],n.data.pieces.length)}));m("piece-form").addEventListener("submit",p=>{if(p.preventDefault(),!n||c)return;const l=structuredClone(n.data.pieces),E=m("piece-kind").value;l[a]={kind:E,width:Number(m("piece-width").value),height:E==="flat"?0:Number(m("piece-height").value)},J(l)}),m("piece-kind").addEventListener("change",()=>{m("piece-height").disabled=m("piece-kind").value==="flat"});for(const[p,l]of[["piece-left",-1],["piece-right",1]])m(p).addEventListener("click",()=>{if(!n||c)return;const E=structuredClone(n.data.pieces),S=a+l;S<0||S>=E.length||([E[a],E[S]]=[E[S],E[a]],J(E,S))});m("piece-remove").addEventListener("click",()=>{n&&n.data.pieces.length>1&&J(n.data.pieces.filter((p,l)=>l!==a),a-1)}),m("level-name").addEventListener("input",()=>{n&&(n.title=m("level-name").value,q())}),m("save-level").addEventListener("click",()=>void T(!0)),m("copy-level").addEventListener("click",()=>{n&&!c&&K(it(n.data,`${n.title.slice(0,50)} copy`))}),m("test-level").addEventListener("click",async()=>{!n||c||(await T(),r.close(),s.close(),e.onPlay(Fe(n)))}),m("create-level").addEventListener("click",()=>void K(it())),m("level-canvas").addEventListener("pointerdown",p=>{if(!n||c)return;const l=m("level-canvas").getBoundingClientRect(),E=He(Fe(n)),S=(p.clientX-l.left)/l.width*(E.length+100);let C=400;for(let M=0;M<n.data.pieces.length;M++)if(C+=n.data.pieces[M].width,S<C){a=M,U();return}});async function ae(p=!1){if(!(!n||c)){if(!n.title.trim()){L("Name your level before publishing.");return}v(!0),L(p?"Waiting for permission to unpublish…":"Saving draft, then asking the host to publish…");try{await T();const l=await i.publish(n,p);n.base=l,await T(),L(p?"Unpublished from Community. Old public copies may still exist.":"Published to Community. Other players can now discover this level."),y=y.filter(E=>!(E.id===l.id&&E.author===l.author)),p||y.unshift(l)}catch(l){L(`${le(l)} Retry uses the same prepared change until you edit it.`)}finally{v(!1),U()}}}m("publish-level").addEventListener("click",()=>void ae()),m("unpublish-level").addEventListener("click",()=>void ae(!0));async function jt(p){try{const l=await Nt(p);if(l.deleted)throw new Error("This level was unpublished.");await K({id:l.id,title:l.title,version:Date.now(),data:V(l.data),base:l})}catch(l){u(le(l))}}function oe(){const p=m("level-list");if(p.replaceChildren(),h==="yours"){for(const l of t.drafts){const E=document.createElement("div");E.className="level-card";const S=document.createElement("strong");S.textContent=l.title||"Untitled track";const C=document.createElement("small");C.textContent=l.base&&!l.base.deleted?"Your draft · published version exists":"Your draft · only on this device";const M=document.createElement("div");M.className="level-card-actions",M.append(te("Edit",()=>void K(l)),te("Play",()=>{s.close(),e.onPlay(Fe(l))}),te("Remove draft",()=>{t.remove(l.id).then(()=>oe()).catch(ye=>u(le(ye)))})),E.append(S,C,M),p.append(E)}u(w||(t.drafts.length?"Drafts stay on this device. Published levels also appear in Community.":"Create a level to get started."))}else{for(const l of y){const E=document.createElement("div");E.className="level-card";const S=document.createElement("strong");S.textContent=l.title;const C=document.createElement("small");C.textContent=`${l.author.slice(0,8)}…${l.author.slice(-4)}${l.author===b?" · you":""} · ${Math.round(He(kt(l)).length/10)} m`;const M=document.createElement("div");M.className="level-card-actions",M.append(te("Play",()=>{s.close(),e.onPlay(kt(l))}),te("Remix",()=>void K(it(V(l.data),`${l.title.slice(0,50)} remix`)))),l.author===b&&M.append(te("Edit published",()=>void jt(l))),E.append(S,C,M),p.append(E)}u(w||(y.length?"Recent public levels · player-made, with personal race times.":"No levels in this recent view yet. Publish the first."))}m("more-levels").hidden=h!=="community"||d===null,m("refresh-levels").disabled=g}async function Qe(p=!1){if(g)return;const l=++f;g=!0,w="Loading community levels…",oe();try{const E=await gt();b=await Tn();const S=p?d:null,C=await E.list({limit:40,...S===null?{}:{until:S}});if(l!==f)return;const M=new Map((p?y:[]).map(j=>[`${j.author}:${j.id}`,j]));for(const j of C.records){const Be=`${j.author}:${j.id}`,F=M.get(Be);(!F||j.createdAt>F.createdAt||j.createdAt===F.createdAt&&j.revision<F.revision)&&M.set(Be,j)}y=[...M.values()];const ye=p&&C.nextUntil!==null&&S!==null&&C.nextUntil>=S;d=ye?null:C.nextUntil,w=[C.incomplete?"Some relays did not respond. This list may be incomplete.":"",C.invalid?`${C.invalid} invalid or incompatible records skipped.`:"",ye?"The time cursor stopped advancing. Refresh later; this is not an exhaustive list.":""].filter(Boolean).join(" ")}catch(E){w=le(E)}finally{l===f&&(g=!1,oe())}}async function vt(p){h=p,await A;for(const l of["builtin","yours","community"])m(`levels-${l}`).setAttribute("aria-pressed",String(h===l));m("track-options").hidden=h!=="builtin",m("level-library").hidden=h==="builtin",m("track-picker-note").hidden=h!=="builtin",h==="community"?(w="",oe(),Qe()):h==="yours"&&oe()}for(const p of["builtin","yours","community"])m(`levels-${p}`).addEventListener("click",()=>void vt(p));m("refresh-levels").addEventListener("click",()=>{h==="community"?Qe():A.then(oe)}),m("more-levels").addEventListener("click",()=>void Qe(!0)),m("track-picker").addEventListener("click",()=>{h!=="builtin"&&A.then(oe)});const bt=new ResizeObserver(()=>{r.open&&Y()});bt.observe(m("level-canvas")),r.addEventListener("cancel",p=>{c&&p.preventDefault()}),r.addEventListener("close",()=>void T()),window.addEventListener("pagehide",()=>bt.disconnect(),{once:!0}),A.then(()=>{w&&e.onNotice(w)})}const o=e=>document.getElementById(e),B=o("draw-canvas"),Rn=o("track-canvas"),_e=o("racer-name");let P=fe[0],O="classic",D="rigid",R=[],$=new ht(P,R,D),k="ready",We=!1,ie=[],_="personal",Me=[],Ne="",$e="",we=!1,xe=[],Ee=null,ke=null,he=[],Q=null,ue=null,Ke=0,Ye=!1,at=0,mt=0,qe=0,Se=0,Te=[],Pe=!1,Xe=null,Ve=0,ot;const lt=[];function N(e){o("hint").textContent=e}function X(){return O==="classic"&&(k==="running"||k==="paused")}function je(e,t){o(e).setAttribute("aria-pressed",String(t))}function $t(){return ie.filter(e=>e.track===P.id&&e.mode===O&&e.material===D).sort((e,t)=>e.ms-t.ms)}function Ln(){const e=$t()[0];o("best").textContent=`PERSONAL BEST ${e?Ce(e.ms):"—"}`}function re(){const e=k==="running"||k==="paused";o("material").disabled=e,o("material").textContent=D==="bendy"?"Bendy":"Rigid",je("material",D==="bendy"),o("material").title=`Limb physics: ${D}. Switch to ${D==="rigid"?"bendy":"rigid"}.`,o("board-mode").textContent=`${O==="live"?"LIVE":"CLASSIC"} · ${D.toUpperCase()}`,o("race").textContent=k==="running"?"Rolling…":k==="paused"?"Resume →":k==="finished"||k==="failed"?"Race again →":"Let it roll →",o("race").disabled=k==="running"||R.length===0,o("pause").disabled=k!=="running"&&k!=="paused",o("pause").textContent=k==="paused"?"▷":"Ⅱ",o("pause").setAttribute("aria-label",k==="paused"?"Resume race":"Pause race");for(const t of document.querySelectorAll("[data-preset],#clear,#undo"))t.disabled=X();o("undo").disabled=X()||!Te.length,o("pad-badge").textContent=X()?"DESIGN LOCKED FOR THIS RACE":O==="live"?"LIVE · EVERY STROKE CHANGES YOUR RACER":"YOUR SKETCH → YOUR RACER",o("ink-count").textContent=`${R.length} / ${dt} strokes`,o("race-state").textContent={ready:"READY WHEN YOU ARE",running:"FIND YOUR RHYTHM",paused:"TAKE A BREATHER",finished:"ACROSS THE LINE",failed:"TIME FOR A NEW IDEA"}[k],B.setAttribute("aria-disabled",String(X())),Ie(B,R,X()),Ln()}function pe(){Q=null,ue=null,Pe=!1,$=new ht(P,R,D),k="ready",xe=[],Ee=null,Se=0,o("race-message").hidden=!0,o("share-result").hidden=!0,o("time").innerHTML="00.00<span>s</span>",o("progress-fill").style.width="0%",re()}function Nn(){if(O==="live"&&(k==="running"||k==="paused")){const e={at:Math.round($.elapsed*1e3),design:Z(R)};xe.length>=24&&xe.splice(1,1),xe.push(e)}}function ge(e=!0){Ye=!0,(k==="finished"||k==="failed")&&pe(),(k==="ready"||O==="live")&&$.setDesign(R),e&&(Nn(),Dt()),re()}function Ge(){Te.push(Z(R)),Te.length>30&&Te.shift()}async function Pt(){try{await xn(ie,R,_e.value||"Guest racer")||N("Progress is kept for this session. Saving is unavailable in this host.")}catch{N("Race kept for this session, but could not be saved. Try another race to retry.")}}function Dt(){clearTimeout(at),at=window.setTimeout(()=>void Pt(),350)}function $n(){if(k==="paused"){yt();return}if(!R.length){N("Draw some limbs, or pick a starter shape below.");return}Pe=!1,$=new ht(P,R,D),k="running",Ee=null,xe=[{at:0,design:Z(R)}],Se=0,qe=performance.now(),o("race-message").hidden=!0,o("share-result").hidden=!0,N(O==="live"?"Keep drawing! New limbs change your racer instantly.":"Your design is locked. Watch those little legs go."),re()}function me(){k==="running"&&(k="paused",o("message-title").textContent="A little breather",o("message-detail").textContent="Your time is paused. Ready when you are.",o("message-action").textContent="Keep going →",o("share-result").hidden=!0,o("race-message").hidden=!1,re())}function yt(){k==="paused"&&(k="running",qe=performance.now(),Se=0,o("race-message").hidden=!0,re())}function Pn(){var e,t;if(k=$.finished?"finished":"failed",o("message-title").textContent=$.finished?"Look at you go.":"A fresh pair of legs?",o("message-detail").textContent=$.finished?`${Ce($.elapsed*1e3)} · ${P.name}`:"Try longer legs or a different bend.",o("message-action").textContent="Draw & race again →",o("race-message").hidden=!1,$.finished){const i={id:((t=(e=globalThis.crypto)==null?void 0:e.randomUUID)==null?void 0:t.call(e))??`${Date.now()}-${Math.random().toString(36).slice(2)}`,track:P.id,mode:O,material:D,physics:ee,ms:Math.round($.elapsed*1e3),name:_e.value.trim()||"Guest racer",actor:Ne||void 0,created:Date.now(),design:Z($.design),edits:xe.map(r=>({at:r.at,design:Z(r.design)}))};ot&&(i.level=structuredClone(ot)),ie.unshift(i),ie=ie.slice(0,40),Ee=i,Ye=!0,Pt(),o("share-result").hidden=!H("cvm")||!Ae(P.id,O,D),o("share-result").textContent="Share time ↗",o("share-result").disabled=!1,N(Ae(P.id,O,D)?"Saved with your limbs. Share your time to join this track’s board.":"Personal time saved with your limbs. Can you beat it?"),G()}else N("No time recorded. Redraw, then give it another spin.");re()}function It(e){const t=B.getBoundingClientRect(),i=Math.min(t.width,t.height)*.43;let r=(e.clientX-t.left-t.width/2)/i,s=(e.clientY-t.top-t.height/2)/i;return r=Math.max(-1,Math.min(1,r)),s=Math.max(-1,Math.min(1,s)),{x:Math.round(r*1e3)/1e3,y:Math.round(s*1e3)/1e3}}B.addEventListener("pointerdown",e=>{if(X()||Q!==null)return;if(R.length>=dt){N("Eight strokes is the limit. Undo or clear to make room.");return}if(R.reduce((i,r)=>i+r.length,0)>=ct-2){N("This drawing is full. Undo or clear to make room.");return}e.preventDefault(),B.focus({preventScroll:!0}),Ge(),Q=e.pointerId,B.setPointerCapture(Q);const t=It(e);ue=[{x:0,y:0},t],R.push(ue),ge(!1)});B.addEventListener("pointermove",e=>{if(e.pointerId!==Q||!ue)return;e.preventDefault();const t=It(e),i=ue.at(-1);if(!(Math.hypot(t.x-i.x,t.y-i.y)<.045)){if(R.reduce((r,s)=>r+s.length,0)>=ct){N("Ink limit reached. Lift your finger to finish this stroke.");return}ue.push(t),ge(!1)}});function z(e){if(Q===null||e&&Q!==e.pointerId)return;const t=Q;Q=null,ue=null,B.hasPointerCapture(t)&&B.releasePointerCapture(t),ge()}B.addEventListener("pointerup",z);B.addEventListener("pointercancel",z);B.addEventListener("lostpointercapture",z);for(const e of document.querySelectorAll("[data-preset]"))e.addEventListener("click",()=>{X()||(Ge(),R=sn(e.dataset.preset),ge(),N(O==="live"?"Starter shape loaded. Tweak it as you roll.":"A starting point. Add your own twist, or let it roll."))});o("undo").addEventListener("click",()=>{X()||!Te.length||(R=Te.pop(),ge())});o("clear").addEventListener("click",()=>{X()||(Ge(),R=[],ge(),N(O==="live"&&k==="running"?"Your hub is still moving. Draw its new legs.":"Fresh paper. Try something unexpected."))});o("race").addEventListener("click",$n);o("pause").addEventListener("click",()=>k==="paused"?yt():me());o("restart").addEventListener("click",()=>{pe(),N("Back to the start. Your drawing is ready.")});o("message-action").addEventListener("click",()=>k==="paused"?yt():pe());for(const e of["classic","live"])o(e).addEventListener("click",()=>{O!==e&&(z(),O=e,je("classic",e==="classic"),je("live",e==="live"),o("board-mode").textContent=e==="live"?"LIVE DRAW":"CLASSIC",pe(),G(),_==="shared"&&se(),N(e==="live"?"Redraw while racing. Your changes take effect instantly.":"Draw first, race second. One design all the way to the finish."))});o("material").addEventListener("click",()=>{k==="running"||k==="paused"||(z(),D=D==="rigid"?"bendy":"rigid",pe(),G(),_==="shared"&&se(),N(D==="bendy"?"Bendy limbs flex under load and spring back. Long legs can buckle.":"Rigid limbs keep their drawn shape."))});o("track-picker").addEventListener("click",()=>{z(),me(),o("tracks-dialog").showModal()});fe.forEach((e,t)=>{const i=document.createElement("button");i.className=`track-option${t===0?" selected":""}`,i.dataset.track=e.id,i.setAttribute("aria-pressed",String(t===0));const r=document.createElementNS("http://www.w3.org/2000/svg","svg");r.classList.add("track-mini"),r.setAttribute("viewBox","0 0 100 50");const s=document.createElementNS("http://www.w3.org/2000/svg","path"),n=[{x:0,y:e.ground[0].y},...e.ground.filter(d=>d.x>=0&&d.x<=e.length),{x:e.length,y:e.ground.at(-1).y}],a=Math.min(...n.map(d=>d.y)),h=Math.max(120,Math.max(...n.map(d=>d.y))-a);s.setAttribute("d",`M0 50 ${n.map(d=>`L${d.x/e.length*100} ${8+(d.y-a)/h*32}`).join(" ")} L100 50Z`),s.setAttribute("fill",e.color),s.setAttribute("stroke","#51745d"),r.append(s);const c=document.createElement("span"),g=document.createElement("strong"),f=document.createElement("small");g.textContent=`${String(t+1).padStart(2,"0")} · ${e.name}`,f.textContent=e.description,c.append(g,f);const y=document.createElement("span");y.className="difficulty",y.textContent=e.difficulty.toUpperCase(),i.append(r,c,y),i.addEventListener("click",()=>{Ot(e),o("tracks-dialog").close()}),o("track-options").append(i)});function Ot(e,t){z(),Ve++,P=e,ot=t?structuredClone(t):void 0,pe(),o("track-name").textContent=P.name;const i=fe.findIndex(r=>r.id===P.id);o("track-number").textContent=i<0?"CUSTOM":`${String(i+1).padStart(2,"0")} / ${String(fe.length).padStart(2,"0")}`,o("difficulty").textContent=i<0?"PLAYER MADE":P.difficulty.toUpperCase(),document.querySelectorAll(".track-option").forEach(r=>{const s=r.dataset.track===P.id;r.classList.toggle("selected",s),r.setAttribute("aria-pressed",String(s))}),N(P.description),G(),_==="shared"&&se()}Mn({onPlay:e=>{Ot(He(e),e),o("app").classList.remove("show-scores"),N("Your custom track is ready. Draw limbs, then race.")},onNotice:N});o("track-number").textContent=`01 / ${String(fe.length).padStart(2,"0")}`;for(const e of document.querySelectorAll("[data-close]"))e.addEventListener("click",()=>o(e.dataset.close).close());o("scores-toggle").addEventListener("click",()=>{const e=o("app").classList.toggle("show-scores");o("scores-toggle").setAttribute("aria-label",e?"Show drawing":"Show scores"),e&&(z(),me())});function St(e,t){const i=document.createElement("div");i.className="empty-scores";const r=document.createElement("span");r.className="empty-sketch",r.textContent="⌁";const s=document.createElement("strong");s.textContent=e;const n=document.createElement("span");n.textContent=t,i.append(r,s,n),o("score-list").append(i)}function G(){o("score-list").replaceChildren(),je("personal",_==="personal"),je("shared",_==="shared");const e=$t();if(_==="shared"&&!Ae(P.id,O,D)){St("A personal challenge.","Your times and limb drawings are kept in Your runs on this course."),o("score-status").textContent="Personal records only on this course.";return}const t=_==="personal"?e:Me;if(t.length||St(_==="personal"?"A blank slate. A fast future.":we?"Finding the fastest inventions…":$e?"The board is taking a breather.":"The first time could be yours.",_==="personal"?"Finish a race to save your time and the limbs that got you there.":$e||"Finish a race and share your time."),t.forEach((i,r)=>{var y,d;const s="ms"in i?i:void 0,n=document.createElement(s||"hasData"in i&&i.hasData?"button":"div");n.className="score-row";const a=document.createElement("span");a.className="rank",a.textContent=String(r+1).padStart(2,"0");const h=document.createElement("span");h.className="row-name",h.textContent=i.name;const c=document.createElement("small");c.textContent=s?"View limbs ↗":`${(y=i.actor)==null?void 0:y.slice(0,6)}…${(d=i.actor)==null?void 0:d.slice(-4)}${i.actor===Ne?" · you":""}`,h.append(c);const g=s?ln(s.design):document.createElement("span");s||(g.className="no-design",g.textContent=i.hasData?"✎":"—",g.title=i.hasData?"Inspect the finish drawing":"No drawing on this older entry");const f=document.createElement("span");f.className="score-time",f.textContent=Ce(s?s.ms:i.score),n.append(a,h,g,f),s?n.addEventListener("click",()=>_t(s)):i.hasData&&n.addEventListener("click",()=>void Dn(i)),o("score-list").append(n)}),_==="shared"&&Xe!==null&&!we){const i=document.createElement("button");i.id="more-scores",i.textContent="Load more times",i.addEventListener("click",()=>void se(!0)),o("score-list").append(i)}o("score-status").textContent=_==="personal"?"Drawings stay on this device · tap a run to inspect.":$e||"Tap a racer to view their finish drawing · client-reported times."}async function se(e=!1){if(e&&we)return;const t=e?Xe??0:0,i=++Ke,r=P.id,s=O,n=D;if(we=!0,$e="",e||(Me=[],Xe=null),_==="shared"&&G(),!Ae(r,s,n)){we=!1;return}try{const a=await mn(r,s,n,t);if(i!==Ke||r!==P.id||s!==O||n!==D)return;Me=e?[...Me,...a.rows.filter(h=>!Me.some(c=>c.actor===h.actor))]:a.rows,Xe=Number.isSafeInteger(a.nextOffset)&&a.nextOffset!==null&&a.nextOffset>t?a.nextOffset:null}catch(a){if(i!==Ke)return;$e=a instanceof Error?a.message:"Scores unavailable. Try again."}finally{i===Ke&&(we=!1,_==="shared"&&G())}}o("personal").addEventListener("click",()=>{_="personal",G()});o("shared").addEventListener("click",()=>{_="shared",se()});o("refresh").addEventListener("click",()=>_==="shared"?void se():G());o("share-result").addEventListener("click",async()=>{const e=Ee;if(!e)return;const t=o("share-result");t.disabled=!0,t.textContent="Sharing…";try{if(await yn(e),Ee!==e)return;t.textContent="Time shared ✓",N("Your best time and its finish drawing are on the shared board."),_==="shared"&&se()}catch(i){if(Ee!==e)return;t.disabled=!1,t.textContent="Retry sharing",N(i instanceof Error?i.message:"Could not share your time. Try again.")}});async function Dn(e){z(),me();const t=++Ve,i=P.id,r=O,s=D;o("score-status").textContent="Loading the drawing from this run…";try{const n=await vn(i,r,s,e);if(t!==Ve||i!==P.id||r!==O||s!==D||_!=="shared")return;_t(n),o("score-status").textContent="Time and finish drawing from the same personal best."}catch(n){t===Ve&&(o("score-status").textContent=n instanceof Error?n.message:"Could not load this drawing. Retry.")}}function _t(e){var i,r;z(),me(),ke=e,he=Z(e.design),o("design-title").textContent=`${Ce(e.ms)} · ${e.name}`,o("design-detail").textContent=`${((i=e.level)==null?void 0:i.title)??((r=fe.find(s=>s.id===e.track))==null?void 0:r.name)} · ${e.material==="bendy"?"Bendy":"Rigid"} · ${e.mode==="classic"?"Classic · one locked design":e.shared?"Live Draw · finish drawing only":"Live Draw · initial + up to 23 recent stroke snapshots"} · ${e.shared?"Shared personal best":"Saved locally"}`,o("edit-controls").hidden=e.mode!=="live"||e.edits.length<2;const t=o("edit-slider");t.max=String(Math.max(0,e.edits.length-1)),t.value=t.max,e.mode==="live"&&e.edits.length>0&&(he=Z(e.edits.at(-1).design),o("edit-time").textContent=Ce(e.edits.at(-1).at)),o("design-dialog").showModal(),Ie(o("inspect-canvas"),he,!1)}o("edit-slider").addEventListener("input",()=>{const e=ke==null?void 0:ke.edits[Number(o("edit-slider").value)];e&&(he=Z(e.design),o("edit-time").textContent=Ce(e.at),Ie(o("inspect-canvas"),he,!1))});o("use-design").addEventListener("click",()=>{Ge(),R=Z(he),ke&&(D=ke.material),pe(),ge(),o("design-dialog").close(),o("app").classList.remove("show-scores"),N("Drawing loaded. Make it faster.")});_e.addEventListener("input",()=>{Ye=!0,Dt()});function Tt(e){const t=document.documentElement.style;t.setProperty("--bg",e.colors.background),t.setProperty("--fg",e.colors.text),t.setProperty("--primary",e.colors.primary);const i=e.colors.background,r=i.match(/[\da-f]{2}/gi);if(i.startsWith("#")&&(r==null?void 0:r.length)===3)We=r.reduce((s,n)=>s+parseInt(n,16),0)<390;else{const s=i.match(/[\d.]+/g);s&&s.length>=3&&(We=Number(s[0])+Number(s[1])+Number(s[2])<390)}document.documentElement.classList.toggle("dark",We)}async function In(){if(H("theme"))try{Tt(await Jt());const t=Wt(Tt);lt.push(()=>t.close())}catch{}try{const t=await wn();t&&(ie=[...ie,...t.runs.filter(i=>!ie.some(r=>r.id===i.id))].sort((i,r)=>r.created-i.created).slice(0,40),!Ye&&k==="ready"&&(R=t.design,_e.value=t.name,$.setDesign(R)),G(),re())}catch{N("Saved runs could not be read. New races still work.")}if(H("cvm"))try{const t=await Oe("soy_session");Ne=(t==null?void 0:t.actor)||""}catch{}async function e(){try{const t=await Re.getPublicKey();if(o("identity-status").textContent=t?`${t.slice(0,4)}…`:"guest",t){const i=await Re.getProfile();if(!Ye&&i){const r=i.displayName||i.name;typeof r=="string"&&(_e.value=r.slice(0,24))}}}catch{o("identity-status").textContent="guest"}}if(H("identity")){e();try{const t=Re.onChanged(()=>{Ne="",e(),Oe("soy_session").then(i=>{Ne=i.actor||""}).catch(()=>{}),_==="shared"&&se()});lt.push(()=>t.close())}catch{}}}function Yt(e){const t=Math.min(.08,(e-qe)/1e3);if(qe=e,k==="running"){for(Se+=t;Se>=W&&k==="running";)$.step(),Se-=W,($.finished||$.failed)&&(z(),Pn());$.stalled&&!Pe&&(N(O==="live"?"Stuck at the edge? Draw longer legs to reach over it.":"Stuck at the edge? Restart and draw longer legs."),o("race-state").textContent="TRY A LITTLE MORE REACH"),!$.stalled&&Pe&&(o("race-state").textContent="FIND YOUR RHYTHM"),Pe=$.stalled,o("time").innerHTML=`${$.elapsed.toFixed(2).padStart(5,"0")}<span>s</span>`,o("progress-fill").style.width=`${Math.max(0,Math.min(100,($.x-130)/(P.length-130)*100))}%`}on(Rn,P,$,k!=="ready",We),mt=requestAnimationFrame(Yt)}const qt=new ResizeObserver(()=>{Ie(B,R,X()),o("design-dialog").open&&Ie(o("inspect-canvas"),he,!1)});qt.observe(B);window.addEventListener("blur",()=>{z(),me()});document.addEventListener("visibilitychange",()=>{document.hidden&&(z(),me())});window.addEventListener("pagehide",()=>{cancelAnimationFrame(mt),clearTimeout(at),qt.disconnect(),lt.forEach(e=>e())},{once:!0});G();re();In();qe=performance.now();mt=requestAnimationFrame(Yt);
</script>
<style>:root{--bg:#f1f2ec;--fg:#273b34;--primary:#697d35;--surface:color-mix(in srgb,var(--bg) 94%,var(--fg));--border:color-mix(in srgb,var(--fg) 15%,var(--bg));--muted:color-mix(in srgb,var(--fg) 65%,var(--bg));--accent:#d3eb88;--control-h:28px;--napplet-text-selection:none;font:13px/1.4 Avenir Next,Trebuchet MS,sans-serif;color-scheme:light dark}*{box-sizing:border-box}html,body,#app{height:100%;margin:0;background:var(--bg);color:var(--fg)}body{overflow:hidden;-webkit-user-select:var(--napplet-text-selection);user-select:var(--napplet-text-selection)}button,input{font:inherit}button{cursor:pointer;min-height:var(--control-h);border:0;border-radius:5px;background:transparent;color:inherit;padding:4px 10px;white-space:nowrap}button:hover{background:var(--surface)}button:disabled{opacity:.4;cursor:default}button:focus-visible,input:focus-visible,canvas:focus-visible{outline:2px solid var(--primary);outline-offset:-2px}button[aria-pressed=true]{background:var(--bg);box-shadow:0 1px 3px #0001;color:var(--fg)}input{min-width:0;-webkit-user-select:text;user-select:text}#app{container-type:inline-size}.game{height:100%;display:grid;grid-template-rows:52px minmax(130px,1.15fr) minmax(230px,1fr)}.too-small{display:none}.toolbar{display:flex;align-items:center;gap:12px;padding:8px 16px;border-bottom:1px solid var(--border)}.track-picker{display:flex;align-items:center;gap:10px;font-weight:700;padding-left:0;min-width:0}.track-picker span:nth-child(2){overflow:hidden;text-overflow:ellipsis}.track-icon{font-size:21px;color:var(--muted)}.chevron{color:var(--muted)}.segmented{display:flex;gap:2px;background:var(--surface);border-radius:6px;padding:3px}.segmented button{font-size:12px;color:var(--muted)}.modes{margin-left:auto}.live-dot{display:inline-block;width:6px;height:6px;background:#77a569;border-radius:50%;margin-right:5px}.icon-button{font:20px/1 monospace;min-width:28px;padding:4px 6px;display:inline-grid;place-items:center}.mobile-scores{display:none}.race-view{position:relative;min-height:0;overflow:hidden}#track-canvas{width:100%;height:100%;display:block}.track-info{position:absolute;left:18px;top:16px;display:flex;gap:12px;font:10px SFMono-Regular,Consolas,monospace;letter-spacing:1px;color:#536e5f}.track-info span:first-child{opacity:.65}.timer{position:absolute;top:18px;right:22px;text-align:right;display:grid;gap:2px;color:#2c473c;pointer-events:none}.timer>span{font:9px SFMono-Regular,Consolas,monospace;letter-spacing:1px}.timer strong{font:500 clamp(26px,5.5vh,48px)/1.15 "SFMono-Regular",Consolas,monospace;letter-spacing:-2px}.timer strong span{font-size:.5em;letter-spacing:0;margin-left:4px}.timer>span:last-child{font-size:8px;opacity:.7}.progress{position:absolute;bottom:0;height:4px;width:100%;background:#45664c20}.progress i{display:block;height:100%;width:0;background:#708841;transition:width .1s linear}.workbench{min-height:0;display:grid;grid-template-columns:minmax(0,1.5fr) minmax(255px,1fr);border-top:1px solid var(--border)}.drawing-panel{min-width:0;min-height:0;display:grid;grid-template-rows:42px minmax(70px,1fr) 51px 25px}.panel-heading{display:flex;justify-content:space-between;align-items:center;padding:7px 16px;font-size:12px;font-weight:650;gap:8px;min-width:0}.panel-heading>span{display:flex;align-items:center;gap:8px}.step-number{border:1px solid var(--border);border-radius:50%;width:20px;height:20px;display:inline-grid;place-items:center;font:11px monospace;color:var(--muted)}.drawing-actions{display:flex;align-items:center;gap:4px}.drawing-actions button{font-size:11px}.drawing-actions .icon-button{font-size:20px}.pad-wrap{position:relative;min-height:0;background:#fffefa;border-block:1px solid var(--border);margin:0 16px;border-radius:5px;overflow:hidden}#draw-canvas{display:block;width:100%;height:100%;touch-action:none;cursor:crosshair}.pad-badge,.ink-count{position:absolute;top:9px;left:10px;font:8px monospace;letter-spacing:.5px;color:#89938a;pointer-events:none}.ink-count{left:auto;right:10px}.drawing-bottom{display:flex;gap:8px;align-items:center;justify-content:space-between;padding:7px 16px}.presets{display:flex;align-items:center;gap:4px}.presets>span{font-size:10px;color:var(--muted);margin-right:4px}.presets button{display:flex;gap:4px;align-items:center;font-size:20px;padding:2px 6px}.presets button span{font-size:10px}.race-button{background:var(--accent);color:#283b28;border:1px solid #34492b15;font-weight:750;padding:7px 15px;display:flex;align-items:center;justify-content:center;gap:18px;border-radius:5px;min-height:34px}.race-button:hover{background:#c3df70}.race-button span{font-size:20px;font-weight:400}.hint{font-size:10px;padding:0 16px;color:var(--muted);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.score-panel{border-left:1px solid var(--border);min-height:0;display:grid;grid-template-rows:42px 39px 1fr auto 35px}.small-flag{color:var(--muted);font-size:17px}.board-controls{display:flex;align-items:center;justify-content:space-between;gap:5px;padding:0 16px 5px}.board-controls .segmented button{font-size:10px;min-height:24px;padding:3px 8px}#board-mode{font:8px monospace;color:var(--muted);letter-spacing:1px}.score-list{overflow:auto;padding:0 16px;min-height:0}.empty-scores{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:6px;min-height:65px}.empty-scores strong{font-size:12px;font-weight:500}.empty-scores span{font-size:10px;color:var(--muted);max-width:220px}.empty-sketch{font-size:30px!important;line-height:1;color:var(--muted);margin:4px}.score-row{display:grid;grid-template-columns:20px 1fr 44px 61px;align-items:center;width:100%;padding:3px 0;border-bottom:1px solid var(--border);text-align:left;gap:6px;border-radius:0;min-height:41px}.rank{font:11px monospace;color:var(--muted)}.row-name{overflow:hidden;text-overflow:ellipsis;font-size:11px}.row-name small{display:block;color:var(--muted);font:8px monospace}.score-time{font:12px monospace;text-align:right}.limb-thumb{background:#fffefa;width:42px;height:36px;border-radius:4px}.no-design{font-size:9px;color:var(--muted);text-align:center}.score-status{font-size:9px;color:var(--muted);padding:5px 16px;line-height:1.4}.racer-label{display:flex;align-items:center;gap:8px;border-top:1px solid var(--border);padding:4px 16px;font-size:10px;color:var(--muted)}.racer-label input{border:0;background:transparent;color:var(--fg);width:100%;font-size:11px;outline-offset:0}#identity-status{font:8px monospace}.race-message{position:absolute;left:50%;top:48%;transform:translate(-50%,-50%);display:flex;align-items:center;flex-direction:column;gap:8px;padding:15px 24px;border:1px solid var(--border);border-radius:8px;background:var(--bg);box-shadow:0 8px 28px #1a2c3020;max-width:90%;width:max-content;text-align:center}.race-message[hidden]{display:none}.race-message strong{font-size:21px;letter-spacing:-.7px}.race-message span{font-size:11px;color:var(--muted)}.race-message button{background:var(--accent);color:#283b28;font-size:11px;padding:5px 14px}dialog{color:var(--fg);background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:16px;max-width:min(500px,calc(100% - 24px));width:500px;max-height:calc(100% - 24px);box-shadow:0 16px 80px #0003}dialog::backdrop{background:#14251f70;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}.dialog-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;font-size:16px}.dialog-heading strong{font-weight:650;letter-spacing:-.5px}dialog p{color:var(--muted);font-size:11px;line-height:1.5}#track-options{display:grid;gap:8px}.track-option{display:grid;grid-template-columns:65px 1fr auto;gap:10px;align-items:center;text-align:left;border:1px solid var(--border);padding:12px}.track-option.selected{border-color:var(--primary)}.track-option strong{display:block;font-size:13px}.track-option small{display:block;white-space:normal;font-size:10px;color:var(--muted);margin-top:3px}.track-option .difficulty{font:8px monospace;color:var(--muted)}.track-mini{width:65px;height:38px}#inspect-canvas{display:block;width:100%;height:220px;border-radius:6px}.dialog-heading .icon-button{font-size:24px}#edit-controls label{display:flex;align-items:center;gap:8px;font-size:11px;margin-top:8px}#edit-slider{flex:1;accent-color:var(--primary)}[hidden]{display:none!important}.dark .track-info{color:#bed0c1}.dark .timer{color:#e2ead5}.dark .progress{background:#14251f50}@media(min-width:1100px){.game{grid-template-rows:56px minmax(180px,1.3fr) minmax(260px,1fr)}.workbench{grid-template-columns:minmax(0,1.6fr) minmax(300px,1fr)}.pad-wrap{margin:0 20px}.drawing-bottom,.panel-heading{padding-inline:20px}.race-button{min-width:170px}.hint,.toolbar{padding-inline:20px}.score-row{min-height:48px}.score-panel{grid-template-rows:42px 39px 1fr auto 38px}}@media(max-width:640px){.toolbar{padding:7px 10px;gap:6px}.track-picker{gap:5px;font-size:11px}.track-icon{font-size:18px}.modes{gap:0}.modes button{padding:4px 6px;font-size:10px}.live-dot{display:none}.mobile-scores{display:inline-grid}.game{grid-template-rows:48px minmax(140px,1fr) minmax(225px,1.06fr)}.workbench{grid-template-columns:1fr;position:relative}.score-panel{display:none;border-left:0}.show-scores .score-panel{display:grid}.show-scores .drawing-panel{display:none}.track-info{top:12px;left:12px;gap:7px;font-size:8px}.timer{right:13px;top:32px}.timer>span{font-size:7px}.timer strong{font-size:32px}.timer>span:last-child{font-size:7px}.drawing-panel{grid-template-rows:39px minmax(70px,1fr) 49px 24px}.panel-heading{padding:5px 12px}.pad-wrap{margin:0 12px}.drawing-bottom{padding:5px 12px}.presets>span{display:none}.hint{padding-inline:12px;font-size:9px}.race-button{font-size:11px;gap:12px;padding-inline:12px}.presets button{padding:2px 4px}.presets button span{font-size:9px}.track-option{grid-template-columns:48px 1fr;padding:9px}.track-mini{width:48px}.track-option .difficulty{display:none}.race-message{padding:12px 16px}.race-message strong{font-size:18px}}@media(max-width:350px){.track-icon,.chevron{display:none}.track-picker{max-width:92px}.toolbar{gap:3px;padding-inline:8px}.modes button{padding-inline:5px}.toolbar>.icon-button{min-width:25px}.presets button span{display:none}.presets button{min-width:32px}.pad-badge,.ink-count{font-size:7px}}@media(max-height:460px)and (min-width:641px){.game{grid-template-rows:40px minmax(115px,1fr) minmax(158px,1fr)}.drawing-panel{grid-template-rows:31px minmax(60px,1fr) 42px}.hint{display:none}.score-panel{grid-template-rows:31px 34px 1fr auto}.racer-label{display:none}.timer{top:8px}.timer strong{font-size:27px}.pad-badge,.ink-count{top:5px}.panel-heading{padding-block:2px}.score-status{font-size:8px}}@media(pointer:coarse){button{min-height:44px;min-width:44px}.toolbar{padding-block:2px}.modes{padding:0}.modes button{min-width:0}.game{grid-template-rows:50px minmax(140px,1fr) minmax(253px,1.1fr)}.drawing-panel{grid-template-rows:46px minmax(90px,1fr) 56px 25px}.panel-heading{padding-block:0}.race-button{min-height:44px}.presets button{padding:2px 4px;min-width:44px}.score-panel{grid-template-rows:46px 50px 1fr auto 38px}.board-controls .segmented button{min-height:44px}.track-picker{min-width:0}.score-row{min-height:44px}.toolbar>.icon-button{min-width:44px}.presets{gap:0}.modes button{padding-inline:5px}.racer-label input{height:32px}}@media(pointer:coarse)and (max-height:460px)and (min-width:641px){.game{grid-template-rows:46px minmax(110px,1fr) minmax(182px,1fr)}.drawing-panel{grid-template-rows:44px minmax(70px,1fr) 48px}.score-panel{grid-template-rows:44px 44px 1fr auto}.drawing-bottom{padding-block:2px}}@media(max-width:239px),(max-height:319px){.game{display:none}.too-small{display:grid;place-content:center;text-align:center;height:100%;padding:12px;font-size:12px;line-height:1.7}}@media(prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto!important}}@media(pointer:coarse)and (max-width:400px){.toolbar{gap:2px;padding-inline:4px}.toolbar .track-icon,.toolbar .chevron{display:none}.toolbar .modes button{padding-inline:3px}.toolbar .track-picker{max-width:110px}.toolbar .modes{margin-left:auto}}.pad-wrap{container-type:size;display:grid;place-items:center;background:var(--surface);border:0}.pad-wrap #draw-canvas{width:min(100cqw,100cqh);height:min(100cqw,100cqh);border:1px solid var(--border);border-radius:3px}.pad-badge,.ink-count{font-size:7px}#material{border:1px solid var(--border);font-size:10px;min-width:48px}#material[aria-pressed=true]{background:var(--accent);color:#283b28}.board-controls{flex-wrap:wrap}#board-mode{white-space:nowrap}.drawing-actions{gap:2px}@media(max-width:350px){.panel-heading{padding-inline:8px}.drawing-panel .step-number{display:none}.drawing-panel .panel-heading>span{font-size:10px}.drawing-actions button{padding-inline:5px}.pad-badge{max-width:55%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}}#tracks-dialog[open]{display:flex;flex-direction:column;overflow:hidden;max-height:calc(100% - 24px)}#tracks-dialog .dialog-heading,#tracks-dialog>p{flex-shrink:0}#tracks-dialog>p{margin-bottom:0}#track-options{min-height:0;overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable;grid-auto-rows:max-content;align-content:start}.track-option{grid-template-columns:65px minmax(0,1fr) auto;flex-shrink:0}.track-option strong{white-space:normal}.track-option .difficulty{display:block;padding:3px 5px;background:var(--surface);border-radius:3px;letter-spacing:.3px}@media(max-width:640px){.track-option{grid-template-columns:42px minmax(0,1fr);column-gap:8px}.track-mini{width:42px;grid-row:1/3}.track-option .difficulty{grid-column:2;justify-self:start;font-size:8px}.track-option{row-gap:4px}}.level-tabs{display:flex;gap:8px;align-items:center;justify-content:space-between;flex-shrink:0;margin-bottom:10px}.level-tabs .segmented{min-width:0}.level-tabs button{font-size:11px;padding-inline:8px}.level-tabs #create-level{background:var(--accent);color:#283b28}#level-library{min-height:0;overflow:auto}.library-tools{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--muted)}#level-list{display:grid;gap:8px}.level-card{border-bottom:1px solid var(--border);padding:8px 0}.level-card>strong{font-size:13px;overflow-wrap:anywhere}.level-card>small{display:block;color:var(--muted);font-size:10px}.level-card-actions{display:flex;flex-wrap:wrap;gap:4px;margin-top:5px}.level-card-actions button{font-size:11px;background:var(--surface)}#level-list-status{font-size:11px}.no-design{font-size:16px}#more-scores{display:block;margin:8px auto;font-size:11px}#level-editor[open]{display:grid;grid-template-rows:auto minmax(0,1fr) auto;width:760px;max-width:calc(100% - 20px);max-height:calc(100% - 20px);overflow:hidden}.level-editor-body{min-height:0;overflow:auto}.level-name-label{display:flex;gap:10px;align-items:center;font-size:12px}.level-name-label input{flex:1;min-width:0}#level-editor input,#level-editor select{color:var(--fg);background:var(--surface);border:1px solid var(--border);border-radius:4px;min-height:32px;padding:4px 6px;font:inherit}#level-canvas{display:block;width:100%;height:150px;border-radius:5px;margin-top:10px}.level-editor-meta{display:flex;justify-content:space-between;align-items:center;font:10px monospace;color:var(--muted)}.level-editor-meta button{font-size:11px}#piece-palette{display:flex;flex-wrap:wrap;gap:4px;margin:8px 0}#piece-palette button{background:var(--surface);font-size:11px}#piece-list{display:flex;overflow:auto;gap:4px;padding:4px 0 8px}.piece-chip{font-size:10px;border:1px solid var(--border);flex-shrink:0}.piece-chip[aria-pressed=true]{background:var(--accent);color:#283b28}#piece-form{display:grid;grid-template-columns:minmax(80px,1fr) minmax(60px,1fr) minmax(60px,1fr) auto;gap:8px;align-items:end}#piece-form label{display:grid;gap:3px;font-size:10px;color:var(--muted)}#piece-form input,#piece-form select{width:100%;min-width:0}#piece-form button{background:var(--surface)}.piece-actions{display:flex;gap:4px;margin-top:8px}.piece-actions button{font-size:11px}.level-editor-footer{border-top:1px solid var(--border);padding-top:8px;margin-top:8px}.level-editor-footer>div{display:flex;justify-content:flex-end;gap:6px}.level-editor-footer button{font-size:11px}.level-editor-footer .race-button{min-width:0}.level-editor-footer p{margin:0 0 8px;max-height:58px;overflow:auto;overflow-wrap:anywhere}.dark #level-canvas{filter:brightness(.8)}@media(max-width:480px){.level-tabs{flex-wrap:wrap;gap:5px}.level-tabs .segmented{flex:1}.level-tabs .segmented button{flex:1;padding-inline:5px;font-size:10px}#level-editor{padding:12px}#level-canvas{height:120px}#piece-form{grid-template-columns:1.2fr 1fr 1fr;gap:6px}#piece-form button{grid-column:1/-1}.level-editor-footer>div{justify-content:stretch}.level-editor-footer>div button{flex:1;padding-inline:7px;white-space:normal}.level-editor-footer p{font-size:10px}#piece-palette{display:grid;grid-template-columns:repeat(3,1fr)}#piece-palette button{padding-inline:3px}.level-card-actions button{flex:1}}@media(pointer:coarse){#level-editor input,#level-editor select,.piece-chip,.level-tabs{min-height:44px}}@media(max-height:460px){#level-canvas{height:100px}#level-editor .dialog-heading{margin-bottom:5px}.level-editor-footer p{max-height:32px;margin-bottom:4px}}
</style>
<meta name="napplet-type" content="limb-race">
<meta name="napplet-config-schema" content="{"$schema":"http://json-schema.org/draft-07/schema#","$version":1,"type":"object","additionalProperties":false,"properties":{"textSize":{"type":"integer","title":"Text size","description":"Base text size in pixels. Changes apply while the napplet is running.","minimum":10,"maximum":20,"default":13,"x-napplet-section":"Appearance","x-napplet-order":1},"controlSize":{"type":"integer","title":"Control height","description":"Choose compact controls or larger touch targets, in pixels.","minimum":24,"maximum":44,"default":28,"x-napplet-section":"Appearance","x-napplet-order":2},"selectText":{"type":"boolean","title":"Allow text selection","description":"Allow selecting interface text. Text inputs always remain selectable.","default":false,"x-napplet-section":"Interaction"}}}">
</head>
<body>
<div id="app">
<div class="too-small">A little more room to race.<br>Enlarge this pane to at least 240 × 320.</div>
<main class="game">
<nav class="toolbar" aria-label="Race controls">
<button id="track-picker" class="track-picker" aria-haspopup="dialog"><span class="track-icon">⚑</span><span id="track-name">Meadow Run</span><span class="chevron">⌄</span></button>
<div class="segmented modes" aria-label="Race mode"><button id="classic" aria-pressed="true">Classic</button><button id="live" aria-pressed="false"><span class="live-dot"></span>Live Draw</button></div>
<button id="scores-toggle" class="icon-button mobile-scores" aria-label="Show scores" title="Scores">☷</button>
<button id="pause" class="icon-button" aria-label="Pause race" title="Pause race" disabled>Ⅱ</button>
<button id="restart" class="icon-button" aria-label="Restart race" title="Restart race">↻</button>
</nav>
<section class="race-view" aria-label="Racetrack">
<canvas id="track-canvas" aria-label="Physics racetrack with your red-hub racer"></canvas>
<div class="track-info"><span id="track-number">01 / 10</span><span id="difficulty">EASY RIDER</span></div>
<div class="timer"><span id="race-state">READY WHEN YOU ARE</span><strong id="time">00.00<span>s</span></strong><span id="best">PERSONAL BEST —</span></div>
<div id="race-message" class="race-message" hidden><strong id="message-title"></strong><span id="message-detail"></span><button id="message-action">Keep going</button><button id="share-result" hidden>Share time</button></div>
<div class="progress"><i id="progress-fill"></i></div>
</section>
<section class="workbench">
<div class="drawing-panel">
<div class="panel-heading"><span><b class="step-number">1</b> Draw limbs</span><div class="drawing-actions"><button id="material" aria-pressed="false" title="Limb physics: rigid. Switch to bendy.">Rigid</button><button id="undo" class="icon-button" title="Undo stroke" aria-label="Undo stroke">↶</button><button id="clear" title="Clear drawing">Clear</button></div></div>
<div class="pad-wrap"><canvas id="draw-canvas" tabindex="0" aria-label="Draw limbs around the red dot. Use a starter shape below as a keyboard alternative."></canvas><span id="pad-badge" class="pad-badge">YOUR SKETCH → YOUR RACER</span><span id="ink-count" class="ink-count">0 / 8 strokes</span></div>
<div class="drawing-bottom"><div class="presets"><span>Try a shape</span><button data-preset="runner" title="Three curved legs">⌁<span>Runner</span></button><button data-preset="star" title="Five-point spinner">✳<span>Star</span></button></div><button id="race" class="race-button">Let it roll <span>→</span></button></div>
<div id="hint" class="hint" role="status">Draw a shape, then send it spinning. The ground does the rest.</div>
</div>
<aside class="score-panel" aria-label="Leaderboard">
<div class="panel-heading"><span>Fastest inventions <span class="small-flag">⚑</span></span><button id="refresh" class="icon-button" aria-label="Refresh scores" title="Refresh scores">↻</button></div>
<div class="board-controls"><div class="segmented"><button id="personal" aria-pressed="true">Your runs</button><button id="shared" aria-pressed="false">Shared</button></div><span id="board-mode">CLASSIC</span></div>
<div id="score-list" class="score-list"></div>
<div id="score-status" class="score-status" role="status">Race the clock. Keep the drawing.</div>
<label class="racer-label"><span>Racing as</span><input id="racer-name" aria-label="Racer display name" maxlength="24" value="Guest racer" /><span id="identity-status" title="Shared scores use a scoped session identity">guest</span></label>
</aside>
</section>
</main>
<dialog id="tracks-dialog">
<div class="dialog-heading"><strong>Pick your playground</strong><button class="icon-button" data-close="tracks-dialog" aria-label="Close tracks">×</button></div>
<div class="level-tabs"><div class="segmented"><button id="levels-builtin" aria-pressed="true">Courses</button><button id="levels-yours" aria-pressed="false">Your levels</button><button id="levels-community" aria-pressed="false">Community</button></div><button id="create-level">+ Create</button></div>
<div id="track-options"></div>
<div id="level-library" hidden><div class="library-tools"><span>Player-made levels</span><button id="refresh-levels">Refresh</button></div><div id="level-list"></div><p id="level-list-status" role="status"></p><button id="more-levels" hidden>Load older levels</button></div>
<p id="track-picker-note">Same physics. Different problems to draw your way out of.</p>
</dialog>
<dialog id="level-editor">
<div class="dialog-heading"><strong>Build a track</strong><button class="icon-button" data-close="level-editor" aria-label="Close level editor">×</button></div>
<div class="level-editor-body">
<label class="level-name-label">Name<input id="level-name" maxlength="60" value="My new track" /></label>
<canvas id="level-canvas" aria-label="Level terrain preview. Tap a piece to select it, or use its numbered button below."></canvas>
<div class="level-editor-meta"><span id="level-length"></span><button id="copy-level">Make a copy</button></div>
<div id="piece-palette" aria-label="Add terrain"></div><div id="piece-list" aria-label="Select terrain piece"></div>
<form id="piece-form"><label>Piece<select id="piece-kind"><option value="flat">Flat</option><option value="uphill">Uphill</option><option value="downhill">Downhill</option><option value="step">Step up</option><option value="drop">Drop</option><option value="trench">Trench</option></select></label><label>Width<input id="piece-width" type="number" min="40" max="600" step="1" required /></label><label>Height<input id="piece-height" type="number" min="0" max="130" step="1" required /></label><button type="submit">Apply</button></form>
<div class="piece-actions"><button id="piece-left">← Earlier</button><button id="piece-right">Later →</button><button id="piece-remove">Remove piece</button></div>
<p>Start and finish are added automatically. Test your layout before sharing it. Published levels and their authors are public; custom race times stay personal.</p>
<button id="unpublish-level" hidden>Unpublish level</button>
</div>
<div class="level-editor-footer"><p id="level-status" role="status"></p><div><button id="save-level">Save draft</button><button id="test-level">Test track →</button><button id="publish-level" class="race-button">Publish level</button></div></div>
</dialog>
<dialog id="design-dialog"><div class="dialog-heading"><strong id="design-title">A closer look</strong><button class="icon-button" data-close="design-dialog" aria-label="Close design">×</button></div><canvas id="inspect-canvas" aria-label="Saved limb design"></canvas><div id="edit-controls" hidden><label>Drawing at <output id="edit-time">0.00s</output><input id="edit-slider" type="range" min="0" max="0" value="0" /></label></div><p id="design-detail"></p><button id="use-design" class="race-button">Try these limbs <span>→</span></button></dialog>
</div>
</body></html>
