SOURCE / PINNED RELEASE
Made of little things.
Tidewire Couriers
- Release
- e6c6732947eb…
- Author-recorded commit
- Not provided
- License
- Check the original source
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" /><title>Tidewire Couriers</title><meta name="description" content="Route three tiny sail-couriers through a braided tidal rail. Every departure flips a junction gate, so each delivery reshapes the paths available to the next craft."><meta name="napplet-id" content="tidewire-couriers"><meta name="napplet-optional" content="keys storage theme">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script type="module" crossorigin>const fe="tidewire-couriers",ye="Tidewire Couriers",be="Tap a sail, then its lit exit · ←/→ select · Space sends · R restarts",ve=["left","right"],N={slug:fe,title:ye,help:be,actions:ve};function ke(e,t,i=0){const s=Math.max(1,e.width-i*2),n=Math.max(1,e.height-i*2),r=Math.min(s,n*t),l=r/t;return{x:(e.width-r)/2,y:(e.height-l)/2,width:r,height:l}}const re=18,we=1/60,h={water:"#102a43",rail:"#7cc7c9",gate:"#f2c14e",coral:"#ef6f6c",mint:"#8bd3a8",signal:"#f7f3df",warning:"#a84a5b"},J=[{x:.5,y:.12,label:"North knot"},{x:.16,y:.76,label:"West knot"},{x:.84,y:.76,label:"East knot"},{x:.25,y:.37,label:"Circle harbor"},{x:.5,y:.91,label:"Stripe harbor"},{x:.75,y:.37,label:"Outer rail"},{x:.36,y:.48,label:"Triangle harbor"},{x:.5,y:.66,label:"Inner rail"},{x:.64,y:.48,label:"Inner rail"}],B=[[3,6],[4,7],[5,8]],H={3:1,6:1,4:2,7:2,5:0,8:0},Z={3:0,6:1,4:2},Me=[0,1,8],Ae=[0,0,0];function ae(e,t){return{couriers:[...Me],gates:[...Ae],locked:[!1,!1,!1],selected:null,movesRemaining:re,faults:0,result:"active"}}function oe(e,t,i){return e.couriers.some((s,n)=>n!==i&&s===t)}function Se(e,t){if(t>=0&&t<3){const i=e.gates[t]??0;return B[t][i]??null}return H[t]??null}function T(e,t){const i=e.couriers[t];if(i===void 0||e.result!=="active"||e.locked[t])return null;const s=Se(e,i);return s!==null&&!oe(e,s,t)?s:null}function Q(e,t,i){const s=T(e,t);if(s===null||s!==i)return e;const n=[...e.couriers],r=[...e.gates],l=new Set;let a=n[t],m=i;a<3&&(r[a]=1-(r[a]??0),l.add(a));for(let p=0;m!==null&&p<36&&!(oe({...e,couriers:n},m,t)||(a=m,n[t]=a,Z[a]!==void 0));p+=1)if(a<3){const v=r[a]??0;m=B[a][v],l.has(a)||(r[a]=1-v,l.add(a))}else m=H[a]??null;const g=[...e.locked];let M=e.faults;const f=Z[a];f!==void 0&&(f===t?g[t]=!0:M+=1);const o=e.movesRemaining-1,c=g.every(Boolean)?"won":o===0?"lost":"active";return{couriers:n,gates:r,locked:g,selected:null,movesRemaining:o,faults:M,result:c}}function V(e){return ke(e,1.18,Math.min(e.width,e.height)*.035)}function le(e,t){const i=V(e),s=J[t]??J[0];return{x:i.x+s.x*i.width,y:i.y+s.y*i.height}}function ee(e,t,i){const s=le(e,i),n=Math.max(18,Math.min(28,V(e).width*.075));return Math.hypot(t.x-s.x,t.y-s.y)<=n}function Ee(e){return e.couriers.map((t,i)=>i).filter(t=>!e.locked[t]&&T(e,t)!==null)}function j(e,t){const i=Ee(e);if(i.length===0)return e;const s=e.selected===null?-1:i.indexOf(e.selected),n=s<0?t>0?0:i.length-1:(s+t+i.length)%i.length;return{...e,selected:i[n]??i[0]}}function Re(e,t,i,s){if(t.pressed.has("restart"))return ae();if(e.result!=="active")return e;let n=e;if(t.pressed.has("left")&&(n=j(n,-1)),t.pressed.has("right")&&(n=j(n,1)),t.pressed.has("primary"))if(n.selected===null)n=j(n,1);else{const r=T(n,n.selected);r!==null&&(n=Q(n,n.selected,r))}for(const r of t.taps){let l=!1;for(let a=0;a<3;a+=1){const m=n.couriers[a];if(m!==void 0&&!n.locked[a]&&ee(s,r,m)){T(n,a)!==null&&(n={...n,selected:a}),l=!0;break}}if(!l&&n.selected!==null){const a=T(n,n.selected);a!==null&&ee(s,r,a)&&(n=Q(n,n.selected,a))}}return n}function te(e,t,i,s,n,r){if(e.strokeStyle=r,e.fillStyle=r,e.lineWidth=Math.max(2,n*.15),t===0)e.beginPath(),e.arc(i,s,n*.44,0,Math.PI*2),e.stroke();else if(t===1)e.beginPath(),e.moveTo(i,s-n*.5),e.lineTo(i+n*.48,s+n*.38),e.lineTo(i-n*.48,s+n*.38),e.closePath(),e.stroke();else for(let l=-1;l<=1;l+=1)e.beginPath(),e.moveTo(i-n*.45,s+l*n*.28),e.lineTo(i+n*.45,s+l*n*.28),e.stroke()}function Ie(e,t,i,s){e.fillStyle=h.water,e.fillRect(0,0,i.width,i.height);const n=V(i),r=Math.min(n.width,n.height),l=o=>le(i,o);e.fillStyle="rgba(247,243,223,.12)";const a=Math.max(18,r*.075);for(let o=n.y;o<n.y+n.height;o+=a)for(let c=n.x;c<n.x+n.width;c+=a)e.fillRect(c,o,1.4,1.4);e.lineCap="round",e.lineJoin="round",e.strokeStyle=h.rail,e.lineWidth=Math.max(5,r*.018),e.globalAlpha=.78;for(let o=0;o<3;o+=1)for(const c of B[o]){const p=l(o),v=l(c),S=l(H[c]);e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(v.x,v.y),e.lineTo(S.x,S.y),e.stroke()}e.globalAlpha=1;const m=[3,6,4],g=[h.coral,h.mint,h.signal];for(let o=0;o<3;o+=1){const c=l(m[o]);e.fillStyle=h.water,e.strokeStyle=g[o],e.lineWidth=Math.max(3,r*.012),e.beginPath(),e.arc(c.x,c.y,r*.052,0,Math.PI*2),e.fill(),e.stroke(),te(e,o,c.x,c.y,r*.045,g[o])}for(let o=0;o<3;o+=1){const c=t.gates[o]??0,p=l(o),v=l(B[o][c]),S=v.x-p.x,u=v.y-p.y,d=Math.hypot(S,u)||1;e.strokeStyle=h.gate,e.lineWidth=Math.max(4,r*.014),e.beginPath(),e.moveTo(p.x,p.y),e.lineTo(p.x+S/d*r*.065,p.y+u/d*r*.065),e.stroke(),e.fillStyle=h.gate,e.beginPath(),e.arc(p.x,p.y,r*.024,0,Math.PI*2),e.fill()}if(t.selected!==null){const o=T(t,t.selected);if(o!==null){const c=l(o);e.strokeStyle=h.gate,e.lineWidth=Math.max(3,r*.01),e.setLineDash([Math.max(4,r*.018),Math.max(3,r*.012)]),e.beginPath(),e.arc(c.x,c.y,r*.07,0,Math.PI*2),e.stroke(),e.setLineDash([])}}for(let o=0;o<3;o+=1){const c=l(t.couriers[o]),p=r*.064;t.selected===o&&(e.strokeStyle=h.gate,e.lineWidth=Math.max(3,r*.011),e.beginPath(),e.arc(c.x,c.y,p*1.2,0,Math.PI*2),e.stroke()),e.strokeStyle=h.signal,e.lineWidth=Math.max(2,r*.008),e.beginPath(),e.moveTo(c.x,c.y-p*.72),e.lineTo(c.x,c.y+p*.62),e.stroke(),e.fillStyle=g[o],e.beginPath(),e.moveTo(c.x,c.y-p*.62),e.lineTo(c.x+p*.72,c.y+p*.35),e.lineTo(c.x,c.y+p*.2),e.closePath(),e.fill(),te(e,o,c.x+p*.18,c.y,p*.48,h.water),t.locked[o]&&(e.strokeStyle=h.gate,e.lineWidth=Math.max(3,r*.01),e.beginPath(),e.arc(c.x,c.y,p,0,Math.PI*2),e.stroke())}const M=n.x+r*.02,f=n.y+n.height-r*.025;e.font=`600 ${Math.max(10,r*.035)}px system-ui, sans-serif`,e.textAlign="left",e.textBaseline="bottom",e.fillStyle=t.movesRemaining<=4?h.warning:h.signal,e.fillText(`${t.movesRemaining} moves`,M,f),e.textAlign="right",e.fillStyle=t.faults?h.coral:h.mint,e.fillText(`${t.faults} faults`,n.x+n.width-r*.02,f),t.result!=="active"&&(e.fillStyle=t.result==="won"?h.mint:h.warning,e.globalAlpha=s.reducedMotion?.16:.2,e.fillRect(n.x,n.y,n.width,n.height),e.globalAlpha=1)}const y={tickSeconds:we,init:ae,step:Re,isOver:e=>e.result!=="active",score:e=>e.result==="won"?Math.max(100,1200-40*(re-e.movesRemaining)-75*e.faults):0,draw:Ie,status:e=>`${e.movesRemaining} moves · ${e.faults} faults · ${e.locked.filter(Boolean).length}/3 delivered`},ne={ArrowLeft:"left",KeyA:"left",ArrowRight:"right",KeyD:"right",ArrowUp:"up",KeyW:"up",ArrowDown:"down",KeyS:"down",Space:"primary",Enter:"primary",KeyX:"secondary",KeyZ:"secondary",KeyP:"pause",Escape:"pause",KeyR:"restart"},Pe={left:"ArrowLeft",right:"ArrowRight",up:"ArrowUp",down:"ArrowDown",primary:"Space",secondary:"X",pause:"P",restart:"R"},Te=new Set(["left","right","up","down"]),ie=80,Le=24;function Ce(e,t){const i=new Set,s=new Set;let n=[];const r=new Map,l=new Map;let a=null;const m=u=>i.add(u),g=u=>{const d=ne[u.code];if(!d||(u.preventDefault(),s.add(d),u.repeat&&!Te.has(d)))return;const P=performance.now();P-(l.get(d)??-1/0)<ie||(r.set(d,P),m(d))},M=u=>{const d=ne[u.code];d&&s.delete(d)},f=()=>s.clear(),o=u=>{const d=t.getBoundingClientRect();return{x:u.clientX-d.left,y:u.clientY-d.top}},c=u=>{if(!u.isPrimary)return;t.focus({preventScroll:!0});const d=o(u);a={id:u.pointerId,...d}},p=u=>{if(!a||u.pointerId!==a.id)return;const d=o(u),P=d.x-a.x,W=d.y-a.y;a=null,Math.hypot(P,W)>=Le?m(Math.abs(P)>Math.abs(W)?P>0?"right":"left":W>0?"down":"up"):n.push(d)},v=()=>{a=null},S=u=>u.preventDefault();return e.addEventListener("keydown",g),e.addEventListener("keyup",M),window.addEventListener("blur",f),t.addEventListener("pointerdown",c),t.addEventListener("pointerup",p),t.addEventListener("pointercancel",v),t.addEventListener("contextmenu",S),{take(){const u={pressed:new Set(i),held:new Set(s),taps:n};return i.clear(),n=[],u},shellAction(u){const d=performance.now();d-(r.get(u)??-1/0)<ie||(l.set(u,d),m(u))},pending(){return{pressed:i,taps:n}},dispose(){e.removeEventListener("keydown",g),e.removeEventListener("keyup",M),window.removeEventListener("blur",f),t.removeEventListener("pointerdown",c),t.removeEventListener("pointerup",p),t.removeEventListener("pointercancel",v),t.removeEventListener("contextmenu",S)}}}function ce(e){let t=e>>>0;const i=()=>{t=t+1831565813>>>0;let s=t;return s=Math.imul(s^s>>>15,s|1),s^=s+Math.imul(s^s>>>7,s|61),((s^s>>>14)>>>0)/4294967296};return{next:i,int:s=>Math.floor(i()*Math.max(1,Math.floor(s))),pick:s=>{if(s.length===0)throw new Error("pick() from an empty list");return s[Math.floor(i()*s.length)]}}}function Ne(){const e=window;if(!e.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return e.napplet}function b(e){const i=Ne()[e];if(!i)throw new Error(`window.napplet.${String(e)} is unavailable -- runtime did not inject this domain`);return i}var pe={shell:{proposal:{url:"https://github.com/napplet/naps/pull/101",status:"merged",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Optional readiness and environment binding has focused shim and reference-runtime coverage.",evidence:["packages/nap/src/shell/shim.test.ts"]},relay:{proposal:{url:"https://github.com/napplet/naps/pull/2",status:"in-review",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Relay reads, publishing, and subscription lifecycle have maintained shim and integration tests; upstream naming remains under review.",evidence:["packages/nap/src/relay/shim.test.ts"],implementationReference:{revision:"cb8f8eabb2d09439e9d174cb9eb6ef2caad0eca1",evidence:".planning/quick/260703-ghg-align-napplet-packages-with-nap-relayeve/260703-ghg-SUMMARY.md",scope:"RelayEventResult sidecar migration; subsequent changes are not covered."}},identity:{proposal:{url:"https://github.com/napplet/naps/pull/102",status:"merged",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Read-only identity queries and updates have focused binding and reference-runtime coverage.",evidence:["packages/nap/src/identity/shim.test.ts"]},storage:{proposal:{url:"https://github.com/napplet/naps/pull/3",status:"in-review",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Key-value binding has focused request/result tests and tutorial usage; host storage isolation remains the host responsibility.",evidence:["packages/nap/src/storage/shim.test.ts"]},inc:{proposal:{url:"https://github.com/napplet/naps/pull/104",status:"merged",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Topic and channel bindings have compatibility tests and authenticated-endpoint reference-runtime coverage.",evidence:["packages/nap/src/inc/shim.ts","packages/conformance-cli/src/browser.test.ts"]},theme:{proposal:{url:"https://github.com/napplet/naps/pull/103",status:"merged",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Theme queries and pushed updates have focused binding and opaque-iframe integration coverage.",evidence:["packages/nap/src/theme/shim.test.ts","packages/conformance-cli/src/browser.test.ts"]},keys:{proposal:{url:"https://github.com/napplet/naps/pull/9",status:"in-review",checkedAt:"2026-10-31"},maturity:"experimental",rationale:"Keyboard binding ships, but dedicated domain regression coverage is limited; retain explicit evaluation opt-in.",evidence:["packages/nap/src/keys/shim.ts"]},media:{proposal:{url:"https://github.com/napplet/naps/pull/10",status:"in-review",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Session creation and playback binding have focused regression coverage; proposed torrent playback amendments are not implied.",evidence:["packages/nap/src/media/shim.test.ts"]},notify:{proposal:{url:"https://github.com/napplet/naps/pull/11",status:"in-review",checkedAt:"2026-10-31"},maturity:"experimental",rationale:"Notification binding ships with limited dedicated domain regression evidence; broader host integration confidence is unverified.",evidence:["packages/nap/src/notify/shim.ts"]},config:{proposal:{url:"https://github.com/napplet/naps/pull/14",status:"draft",checkedAt:"2026-10-31"},maturity:"exploratory",rationale:"Configuration binding is tested, but the upstream draft is marked MAY DEFER; use for design exploration.",evidence:["packages/nap/src/config/shim.test.ts"]},resource:{proposal:{url:"https://github.com/napplet/naps/pull/80",status:"in-review",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Resource fetching has focused shim and type tests; the reverted merge and reopened proposal do not change that implementation assessment.",evidence:["packages/nap/src/resource/shim.test.ts"],implementationReference:{revision:"8e75eadf317d7fe809415e4755cd48f63cfb63f3",evidence:".planning/quick/260703-ghg-align-napplet-packages-with-nap-relayeve/260703-ghg-SUMMARY.md",scope:"Shared ResourceSidecarEntry types during the sidecar migration; subsequent resource changes are not covered."}},cvm:{proposal:{url:"https://github.com/napplet/naps/pull/31",status:"in-review",checkedAt:"2026-10-31"},maturity:"experimental",rationale:"Session and registry bindings are tested; upstream explicitly marks the contract NEEDS WORK.",evidence:["packages/nap/src/cvm/shim.test.ts"]},outbox:{proposal:{url:"https://github.com/napplet/naps/pull/32",status:"in-review",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Routing and subscription bindings have focused tests and an integration regression for structured-clone boundaries.",evidence:["packages/nap/src/outbox/shim.test.ts","packages/nap/src/boundary-integration.test.ts"],implementationReference:{revision:"57db924bac022cc14c67d9fca1a4d92cf1bd4725",evidence:".planning/quick/260703-ghg-align-napplet-packages-with-nap-relayeve/260703-ghg-SUMMARY.md",scope:"RelayEventResult and stream lifecycle migration; subsequent changes are not covered."}},upload:{proposal:{url:"https://github.com/napplet/naps/pull/33",status:"in-review",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Upload binding has focused tests for request/result behavior; host rail support remains runtime-specific.",evidence:["packages/nap/src/upload/shim.test.ts"]},intent:{proposal:{url:"https://github.com/napplet/naps/pull/91",status:"merged",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Convention normalization and dispatch have focused binding and reference-runtime integration tests.",evidence:["packages/nap/src/intent/shim.test.ts"]},ble:{proposal:{url:"https://github.com/napplet/naps/pull/62",status:"in-review",checkedAt:"2026-10-31"},maturity:"experimental",rationale:"GATT binding has unit coverage; real-device behavior across host backends is not established by these tests.",evidence:["packages/nap/src/ble/shim.test.ts"]},webrtc:{proposal:{url:"https://github.com/napplet/naps/pull/59",status:"in-review",checkedAt:"2026-10-31"},maturity:"experimental",rationale:"Signaling and session binding have unit coverage; interoperable live peer sessions remain unverified here.",evidence:["packages/nap/src/webrtc/shim.test.ts"]},link:{proposal:{url:"https://github.com/napplet/naps/pull/53",status:"in-review",checkedAt:"2026-10-31"},maturity:"supported",rationale:"The small link-opening binding has focused regression coverage; navigation authorization stays with the host.",evidence:["packages/nap/src/link/shim.test.ts"]},count:{proposal:{url:"https://github.com/napplet/naps/pull/69",status:"in-review",checkedAt:"2026-10-31"},maturity:"experimental",rationale:"Count binding is tested, but accuracy and aggregation across real relay backends remain outside that evidence.",evidence:["packages/nap/src/count/shim.test.ts"]},lists:{proposal:{url:"https://github.com/napplet/naps/pull/68",status:"in-review",checkedAt:"2026-10-31"},maturity:"experimental",rationale:"List mutation binding is tested; concurrent real-host mutation behavior is not established by binding tests.",evidence:["packages/nap/src/lists/shim.test.ts"]},serial:{proposal:{url:"https://github.com/napplet/naps/pull/61",status:"in-review",checkedAt:"2026-10-31"},maturity:"experimental",rationale:"Serial binding has unit coverage; device and native-host integration evidence is limited.",evidence:["packages/nap/src/serial/shim.test.ts"]},fs:{proposal:{url:"https://github.com/napplet/naps/pull/88",status:"in-review",checkedAt:"2026-10-31"},maturity:"experimental",rationale:"Filesystem binding has MVP coverage; portability and authorization across storage backends remain under evaluation.",evidence:["packages/nap/src/fs/shim.test.ts"]},common:{proposal:{url:"https://github.com/napplet/naps/pull/67",status:"in-review",checkedAt:"2026-10-31"},maturity:"experimental",rationale:"Social action binding is tested; broader host integration and evolving action semantics warrant explicit opt-in.",evidence:["packages/nap/src/common/shim.test.ts"]},dm:{proposal:{url:"https://github.com/napplet/naps/pull/74",status:"draft",checkedAt:"2026-10-31"},maturity:"experimental",rationale:"Direct-message binding has shim and type coverage; transport interoperability is not established by those tests.",evidence:["packages/nap/src/dm/shim.test.ts"]},gamepad:{proposal:{url:"https://github.com/napplet/naps/pull/130",status:"in-review",checkedAt:"2026-10-31"},maturity:"supported",rationale:"Cross-repository delivery includes binding, security, runtime, and conformance verification; the reopened proposal is still subject to change.",evidence:["packages/nap/src/gamepad/shim.test.ts",".planning/phases/163-nap-gamepad-across-repositories/163-VERIFICATION.md"],implementationReference:{revision:"0b28f77e3dfa0f07dd7c0f9d61a7090ee99f3a5e",evidence:".planning/phases/163-nap-gamepad-across-repositories/163-SECURITY.md",scope:"Phase 163 binding, runtime isolation, and conformance verification."}},llm:{proposal:{url:"https://github.com/napplet/naps/pull/134",status:"draft",checkedAt:"2026-10-31"},maturity:"exploratory",rationale:"Early published draft has binding and mock-runtime tests; provider behavior and API design remain exploratory.",evidence:["packages/nap/src/llm/shim.test.ts","packages/conformance-cli/src/browser.test.ts"]}};for(const e of Object.values(pe))Object.freeze(e.proposal),Object.freeze(e.evidence),e.implementationReference&&Object.freeze(e.implementationReference),Object.freeze(e);Object.freeze(pe);function Oe(){const e=new Map;function t(n,r){if(e.has(n))throw new Error(`NAP domain "${n}" is already registered`);e.set(n,r)}function i(n){const r=n.type.indexOf(".");if(r<=0)return!1;const l=n.type.slice(0,r),a=e.get(l);return a?(a(n),!0):!1}function s(){return Array.from(e.keys())}return{registerNap:t,dispatch:i,getRegisteredDomains:s}}var xe=Oe(),De=xe.registerNap,Be="theme";function ue(){const e=window;if(!e.napplet?.theme)throw new Error("window.napplet.theme is unavailable -- runtime did not inject this domain");return e.napplet.theme}function $e(){return ue().get()}function Ke(e){return ue().onChanged(e)}De(Be,e=>{});var de={getItem(e){return b("storage").getItem(e)},setItem(e,t){return b("storage").setItem(e,t)},removeItem(e){return b("storage").removeItem(e)},keys(){return b("storage").keys()},instance:{getItem(e){return b("storage").instance.getItem(e)},setItem(e,t){return b("storage").instance.setItem(e,t)},removeItem(e){return b("storage").instance.removeItem(e)},keys(){return b("storage").instance.keys()}}},qe={registerAction(e){return b("keys").registerAction(e)},unregisterAction(e){b("keys").unregisterAction(e)},onAction(e,t){return b("keys").onAction(e,t)},async register(e,t){const i=b("keys"),s=await i.registerAction(e),n=i.onAction(e.id,t);return{...s,close(){n.close(),i.unregisterAction(e.id)}}}};function Y(e){const t=e.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);if(t){const s=t[1].length===3?t[1].split("").map(n=>n+n).join(""):t[1];return[parseInt(s.slice(0,2),16),parseInt(s.slice(2,4),16),parseInt(s.slice(4,6),16)]}const i=e.trim().match(/^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/i);return i?[Number(i[1]),Number(i[2]),Number(i[3])]:null}function _e(e){const t=Y(e);if(!t)return 0;const[i,s,n]=t.map(r=>{const l=r/255;return l<=.03928?l/12.92:((l+.055)/1.055)**2.4});return .2126*i+.7152*s+.0722*n}function We(e,t,i){const s=Y(e),n=Y(t);if(!s||!n)return e;const r=s.map((l,a)=>Math.round(l+(n[a]-l)*i));return`rgb(${r[0]}, ${r[1]}, ${r[2]})`}function je(){return typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches}function X(e,t,i,s=je()){const n=_e(e)<.4;return{background:e,foreground:t,accent:i,muted:We(t,e,.55),danger:n?"#ff6b6b":"#c62828",success:n?"#5ee38a":"#1b7f3b",dark:n,reducedMotion:s}}function Ue(e){return e?X("#11131a","#e8eaf2","#7aa2ff"):X("#f6f7fb","#1a1d26","#2f5fd0")}const Fe={left:"Move left",right:"Move right",up:"Move up",down:"Move down",primary:"Start / main action",secondary:"Secondary action",pause:"Pause",restart:"Restart"};function K(e,t){return new Promise((i,s)=>{const n=setTimeout(()=>s(new Error("timeout")),t);e.then(r=>{clearTimeout(n),i(r)},r=>{clearTimeout(n),s(r)})})}function q(e){const t=window.napplet;return!!(t&&t[e])}async function Ye(e,t){if(!q("keys"))return[];const i=[];for(const s of e)try{i.push(await K(qe.register({id:`game.${s}`,label:Fe[s],defaultKey:Pe[s]},()=>t(s)),2e3))}catch{}return i}async function Xe(e){if(!q("storage"))return 0;try{const t=await K(de.getItem(`best:${e}`),2e3),i=t===null?0:Number(t);return Number.isFinite(i)&&i>0?i:0}catch{return 0}}function ze(e,t){q("storage")&&K(de.setItem(`best:${e}`,String(t)),2e3).catch(()=>{})}function Ge(e){const t=window.matchMedia("(prefers-color-scheme: dark)"),i=window.matchMedia("(prefers-reduced-motion: reduce)");let s=!1,n=null;const r=g=>{n=g,e(g)},l=()=>{s||r(Ue(t.matches))},a=()=>{n&&r({...n,reducedMotion:i.matches})};l(),t.addEventListener("change",l),i.addEventListener("change",a);let m=null;if(q("theme")){const g=M=>{const f=M?.colors;!f?.background||!f.text||(s=!0,r(X(f.background,f.text,f.primary||f.text,i.matches)))};K($e(),3e3).then(g,()=>{});try{m=Ke(g)}catch{m=null}}return{close:()=>{t.removeEventListener("change",l),i.removeEventListener("change",a),m?.close()}}}const _=document.getElementById("app"),R=document.getElementById("stage"),O=R.getContext("2d"),He=document.getElementById("score"),Ve=document.getElementById("status"),Je=document.getElementById("best"),Ze=document.getElementById("overlay"),U=document.getElementById("overlay-title"),F=document.getElementById("overlay-hint"),Qe=new Set([...N.actions,"primary","pause","restart"]),k=Ce(document.body,R);let z,A={width:1,height:1},C=1,x=(Date.now()^Math.floor(Math.random()*2147483647))>>>0,he=ce(x),w=y.init(),I="ready",E=0,D=0,$=performance.now(),G=-1;function me(){const e=_.getBoundingClientRect();C=Math.max(1,Math.min(3,window.devicePixelRatio||1)),A={width:Math.max(1,Math.floor(e.width)),height:Math.max(1,Math.floor(e.height))},R.width=Math.round(A.width*C),R.height=Math.round(A.height*C),R.style.width=`${A.width}px`,R.style.height=`${A.height}px`,I==="ready"&&(w=y.init())}function L(e){I=e,_.dataset.phase=e,Ze.hidden=e==="playing",e==="ready"?(U.textContent=N.title,F.textContent=`Tap or press Space to start · ${N.help}`):e==="paused"?(U.textContent="Paused",F.textContent="Press P or tap to resume"):e==="over"&&(U.textContent=`Score ${y.score(w)}`,F.textContent=`Best ${E} · Tap or press Space to play again`)}function se(){x=x*1664525+1013904223>>>0,he=ce(x),w=y.init(),D=0,G=-1,L("playing")}function et(){const e=y.score(w);e!==G&&(G=e,He.textContent=String(e),_.dataset.score=String(e)),Ve.textContent=y.status?y.status(w):"",Je.textContent=E>0?`Best ${E}`:""}function tt(){const{pressed:e,taps:t}=k.pending();if(e.has("restart")){k.take(),se();return}if(I==="ready"||I==="over"){e.has("primary")||t.length>0?(k.take(),se()):e.size&&k.take();return}if(I==="paused"){e.has("pause")||e.has("primary")||t.length>0?(k.take(),L("playing"),$=performance.now()):e.size&&k.take();return}e.has("pause")&&(k.take(),L("paused"))}function ge(e){if(tt(),I==="playing"){D+=Math.min(.25,(e-$)/1e3);let t=0;for(;D>=y.tickSeconds&&t<8;)if(w=y.step(w,k.take(),he,A),D-=y.tickSeconds,t+=1,y.isOver(w)){const i=y.score(w);i>E&&(E=i,ze(N.slug,E)),L("over");break}}$=e,O.setTransform(C,0,0,C,0,0),O.fillStyle=z.background,O.fillRect(0,0,A.width,A.height),y.draw(O,w,A,z),et(),requestAnimationFrame(ge)}const nt=Ge(e=>{z=e,document.documentElement.style.setProperty("--bg",e.background),document.documentElement.style.setProperty("--fg",e.foreground),document.documentElement.style.setProperty("--accent",e.accent),document.documentElement.style.setProperty("--muted",e.muted),document.documentElement.dataset.scheme=e.dark?"dark":"light"});document.addEventListener("visibilitychange",()=>{document.hidden&&I==="playing"&&L("paused")});new ResizeObserver(me).observe(_);me();L("ready");R.focus({preventScroll:!0});Xe(N.slug).then(e=>{E=Math.max(E,e)});const it=Ye([...Qe],e=>k.shellAction(e));window.addEventListener("pagehide",()=>{k.dispose(),nt.close(),it.then(e=>e.forEach(t=>t.close()))});requestAnimationFrame(e=>{$=e,ge(e)});
</script>
<style>:root{--bg: #11131a;--fg: #e8eaf2;--accent: #7aa2ff;--muted: #8a8fa3;color-scheme:light dark}*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:var(--bg);color:var(--fg);font:500 14px/1.3 system-ui,-apple-system,Segoe UI,sans-serif}#app{position:relative;width:100%;height:100%;overflow:hidden;container-type:size;background:var(--bg);touch-action:none;user-select:none;-webkit-user-select:none}#stage{position:absolute;inset:0;display:block;outline:none}.hud{position:absolute;top:0;left:0;right:0;display:flex;gap:.6em;justify-content:space-between;align-items:center;padding:clamp(2px,1.2cqmin,10px) clamp(4px,2cqmin,14px);font-size:clamp(10px,3.6cqmin,20px);font-variant-numeric:tabular-nums;pointer-events:none;white-space:nowrap}.hud span{overflow:hidden;text-overflow:ellipsis}#score{font-weight:700}#status{color:var(--muted);flex:1;text-align:center}#best{color:var(--muted)}.overlay{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);max-width:calc(100% - 12px);padding:clamp(4px,2.5cqmin,18px) clamp(8px,4cqmin,28px);border-radius:clamp(6px,2cqmin,14px);background:color-mix(in srgb,var(--bg) 82%,transparent);border:1px solid color-mix(in srgb,var(--fg) 18%,transparent);text-align:center;pointer-events:none;backdrop-filter:blur(3px)}.overlay[hidden]{display:none}#overlay-title{margin:0;font-weight:800;font-size:clamp(12px,6cqmin,34px);color:var(--accent)}#overlay-hint{margin:.3em 0 0;font-size:clamp(9px,3cqmin,16px);color:var(--fg)}
</style>
</head>
<body>
<div id="app" data-phase="loading">
<canvas id="stage" tabindex="0" aria-label="Tidewire Couriers play area"></canvas>
<div class="hud" aria-live="polite">
<span id="score">0</span>
<span id="status"></span>
<span id="best"></span>
</div>
<div class="overlay" id="overlay" role="status">
<p id="overlay-title"></p>
<p id="overlay-hint"></p>
</div>
</div>
</body>
</html>
