Back to Black Site Map Editor
SOURCE / PINNED RELEASE

Made of little things.

Black Site Map Editor

Release
32a57733b7e9…
Author-recorded commit
3f54890f542f…
License
Check the original source
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/black-site-map-editor

The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.

index.html
<!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="application-name" content="Black Site Map Editor" />
        <meta
            name="description"
            content="Create, edit and remix maps for Black Site. Draw terrain, buildings, cover, guards and loot; validate routes from insertion to extraction; save drafts and publish maps on Nostr."
        />
        <meta name="theme-color" content="#101510" />
        <meta name="color-scheme" content="dark" />
        <link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAFK0lEQVR42tWWWWxUVRjHz3fOuffcO0un07EdSrfQ2gWQIqXFEQkEQlkSHyzKZmJiXBJQ1AeexCdffMAoMaJo9AE1oobAC5qCaBGiKYFCpJVF2hSGtgxdpp31zp27nOPDYDvTlpZoSPR7vPnO97vf+Zb/gfzCfPQgDaMHbP9/AM2hEQCEOEdCiFn+CwMAEgiJ2ZwnAAAoGTMskysOiSkEALgQgotsB8AACFmWSMYNy+QAwFTCFDrVedyI4lQyh4203bylZvHyuWPDqfCgpsUNJBCVMCGAMQAGy+Ba3NQSpuKg9YHidVtqK2q9sTE9x5liQCg7Jci0aQbw0fGWOWXuZNwID2odp/rPtd3q6QprCQMAAJC/1F3/eHGgubyuoYiptKMtNLfSVf6wZ+BG7MLpu87JuKE4qKLSccYEIK3b+1ufOnyg88r5O2s2VS9dVVpa5dES5sUzA6GbsWVryypqvKmE+cf5O+d+6rt4pt9f5h0dTHh8cmBdRdPqstIqTzJudLaHfvzuek/XCJVwhpED+PhEy5FPu44dvEwZRQjNKXMvWTG3YWUpU+SBm2PtJ4I9XSPxaFqSCaUwv7FkqC86NBA3LY4xFJe7FzT6n9lRLzOyc91RppAMgE4ti6TQfJ9iWyIaTn3/1ZXTx26UzCu42tGvuhhTSUGhigAM3cyUlDmoS8aCo9iYfuKby7pmvbinCWCiDNMABBe2LTgXEiMen5rnVV0eOf8hB5WIzblti+zzGedMhWWH4s5nM83BJNM1y7a5zCRDt1OaSSlHIGRGpnUWAnFb8CmdOj2AEKwnjc076xc0+Q3ddrjkSLjG41Pbjvb8fKTb5WH/cJIRQggQt0UqaWhxo/IRX+vX1wb7ExU1vku/9a3ZVF3XUNR66E+YLhDA/QG4LZx58sJlcxJRI8/LBnqjQ7eTqlMJBSOhYMzrV8cDTVoQNhcYw9StQXP/AiyT+/yOjdtrDcMe6I3FIrrTLVMJY0JkRjITl2EQigEAIYQBBBJMIpGURilMSoXmFkowhQS7I++82iY4QkLICvH4HIIjIZAQyLa4bdqWyTHB4cF4WjclCY9G0ysW+z/b88Rzb/0yNqrblsiG0KnNQCm48xm3BAJkGnb2BvUWqkVleapKIqPpcCiRif78k9XrHyu5djPy0tN1r+87O3wrTiU8kx6MNxy3xfgXiZH+3qinQNn93srlG+eldUtRaTia3tpcuXdX07YNVb9eGnzt/bNORgduxO53DnKvjga7I2+/cBJTDIBcbnkonNq6dt7+3QGPS/7iWPfBH3pURhAgiZFshZhUA4QxYAzTpiVRIIQKIQiBkUh6PPq3J3tfebcdY5Ap5kIgJO6ZAaEQj6YN3UCIEQICIc5zmgxjhATEEunNq4o//Dv6jr3tlAIlmE8nODT7HuJj6Y3P1o3cTga7I7ExnUpYZiSjNgBI10xdswjllbVVn+x7o0D//cvW3p0zRs9WNOBcdHeGG1eXtry8aMP22vlLiySJhO8kdc1SVXnodqx6UeG2XUsWBnwV3vqGlS2fHzr+5gdtgKlE7xl9Qg8ys24aXNfMwhJXfaA40Fy+oNHPVHr1wnB/TyywvtTlYYN98cMHLl07FzWsVDiScjtlDDCz6EP2yw4AACMzbeuaZVk8z6vUPlrYtLrM5WHXO4c7TvWFgjFCsaxgDJhSzPlsr49JgKmvB10zLZMTArYtVKckKwSJu1tIzB78XoIjkLBFhuR0ywgmtGV89P7Fup60XLn4rz8dHzjgLzi/mRlzyCTYAAAAAElFTkSuQmCC" />
        <link rel="icon" type="image/png" sizes="16x16" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACPklEQVR42pWSPUxTURTHzzn33teW19JHS/mwJoDB+IWG+JlIjCQOxNnZwcXN2d3o4qariwth0UUXNUxqDH4QlZJCJBZNIC3WAgXK63v33XscWkOMLp7lLOeX//n/80cv58H/jGwtJEQEYLCWAQARkYAtAyIisGXmP4HAj3RgkDDeIQnR97UOTMJVxlgdGCcuY3HZgiQzSyWu3zy9/sNfmquWiuthYA6P5g6e6M70utZw+fvW0tzPldKmVISI0kSczigSWJgpj5ztO3VxPzMEfvTlc7X4oYoE+aHOS1eGEWHq/idjuP1Sox4WZsofX6163R1uKrZaqkkl+wbSzd1oYbYy/Qhv3BlLdcVqld02IBQBwtCRzPFz/Ug4cChdeFuRipRD6UzCRNbf1S3f1ALYspdNjF0eDJtmeWF94FCX4xBbIEKtTX07lJL2UkJEE9mefHKlVF+crTJjfsjX2lpjtxthOumMj/btNDTCb4VWUF/na2+efSOBUlHCVecnBsvlnaQjpm6Njwxnnjws7tQDKamtoLWxzKm0Q4TKoZdPSyA42yle3Jt4X6zenSy4ccmEgECIGAYm1+8Oj2SDptnaDBrbur6xq1Ry+sHVd/OVa7dfpzqUEG0P2NXj6dAeO9N78kJeKKpV/Ci02X3O2pLICG/y8fNYLE64Vw30ch4i+I3IGtvd7x44mnViYnlxY22l7gehl04C7F23AQAgQkDQoQ2bETM4MaEcQYTG2H+3tVVSIdBNOa3cmNkY/rvevwDwfxJEIJc3MAAAAABJRU5ErkJggg==" />
        <link rel="apple-touch-icon" sizes="180x180" href="data:image/png;base64,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" />
        <title>Black Site Map Editor</title>
      <script type="module" crossorigin>function fs(s,e){return!!s?.[e]}function T(s){return fs(window.napplet,s)}function bs(){const s=window;if(!s.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return s.napplet}function v(s){const t=bs()[s];if(!t)throw new Error(`window.napplet.${String(s)} is unavailable -- runtime did not inject this domain`);return t}var Re={getItem(s){return v("storage").getItem(s)},setItem(s,e){return v("storage").setItem(s,e)},removeItem(s){return v("storage").removeItem(s)},keys(){return v("storage").keys()},instance:{getItem(s){return v("storage").instance.getItem(s)},setItem(s,e){return v("storage").instance.setItem(s,e)},removeItem(s){return v("storage").instance.removeItem(s)},keys(){return v("storage").instance.keys()}}},gs={registerAction(s){return v("keys").registerAction(s)},unregisterAction(s){v("keys").unregisterAction(s)},onAction(s,e){return v("keys").onAction(s,e)},async register(s,e){const t=v("keys"),i=await t.registerAction(s),r=t.onAction(s.id,e);return{...i,close(){r.close(),t.unregisterAction(s.id)}}}},zt={getPublicKey(){return v("identity").getPublicKey()},onChanged(s){return v("identity").onChanged(s)},getRelays(){return v("identity").getRelays()},getProfile(){return v("identity").getProfile()},getFollows(){return v("identity").getFollows()},getList(s){return v("identity").getList(s)},getZaps(){return v("identity").getZaps()},getMutes(){return v("identity").getMutes()},getBlocked(){return v("identity").getBlocked()},getBadges(){return v("identity").getBadges()}},Ve={info(){return v("resource").info()},bytes(s,e){return v("resource").bytes(s,e)},bytesMany(s,e){return v("resource").bytesMany(s,e)},bytesAsObjectURL(s){return v("resource").bytesAsObjectURL(s)}},U={getEvent(s,e){return v("outbox").getEvent(s,e)},query(s,e){return v("outbox").query(s,e)},subscribe(s,e){return v("outbox").subscribe(s,e)},publish(s,e){return v("outbox").publish(s,e)},resolveRelays(s){return v("outbox").resolveRelays(s)}},Ne={info(){return v("upload").info()},upload(s){return v("upload").upload(s)},status(s){return v("upload").status(s)},onStatus(s){return v("upload").onStatus(s)}},ys={invoke(s,e){return v("intent").invoke(s,e)},open(s,e){return v("intent").open(s,e)},available(s){return v("intent").available(s)},handlers(){return v("intent").handlers()},onChanged(s){return v("intent").onChanged(s)},onDelivery(s){return v("intent").onDelivery(s)}};const Le=[35858,35859],lt=50,Ht=/^[a-z0-9][a-z0-9-]{0,31}$/,ge=/^[0-9a-f]{64}$/;function ws(s,{relay:e}={}){if(!s||!Le.includes(s.kind))throw new Error(`only kinds ${Le.join(" and ")} can be remixed`);if(!ge.test(s.pubkey||""))throw new Error("parent pubkey must be 64 lowercase hex chars");if(!ge.test(s.id||""))throw new Error("parent must be a signed event with an id");const t=Kt(s);if(!t.ok)throw new Error(`parent lineage is invalid:
${t.errors.join(`
`)}`);const i=s.tags.find(p=>p[0]==="d")?.[1];if(!Ht.test(i||""))throw new Error("parent d tag must be a valid id");const r=Be(e),a=String(s.kind),n=`${a}:${s.pubkey}:${i}`,o=t.lineage,l=o?o.root:{address:n,eventId:s.id,pubkey:s.pubkey,relay:r},c=qt([s.pubkey,...o?.authors||[],l.pubkey]);return c.length>lt&&c.splice(lt-1,1/0,l.pubkey),[dt(["A",l.address],l.relay),...l.eventId?[["E",l.eventId,l.relay||"",l.pubkey]]:[],["K",a],["P",l.pubkey],dt(["a",n],r),["e",s.id,r||"",s.pubkey],["k",a],...c.map(p=>["p",p])]}function Kt(s){const e=String(s.kind),t=Array.isArray(s.tags)?s.tags.filter(u=>Array.isArray(u)&&typeof u[0]=="string"):[],i=u=>t.filter(y=>y[0]===u&&typeof y[1]=="string"&&y[1].startsWith(`${e}:`)),r=i("A"),a=i("a");if(!r.length&&!a.length)return{ok:!0,lineage:null};const n=[];if(r.length!==1||a.length!==1)return{ok:!1,errors:[`a remix needs exactly one A (root) and one a (parent) tag of kind ${e}`]};const o=ct(r[0][1]),l=ct(a[0][1]);if(o||n.push(`A tag must be ${e}:<pubkey hex>:<id>`),l||n.push(`a tag must be ${e}:<pubkey hex>:<id>`),n.length)return{ok:!1,errors:n};const c=`${e}:${s.pubkey}:${t.find(u=>u[0]==="d")?.[1]}`;(l.address===c||o.address===c)&&n.push("an event cannot be a remix of itself");for(const[u,y]of[["K",t.find(f=>f[0]==="K")?.[1]],["k",t.find(f=>f[0]==="k")?.[1]]])y!==void 0&&y!==e&&n.push(`${u} tag must be "${e}"`);t.find(u=>u[0]==="P")?.[1]!==o.pubkey&&n.push("P tag must be the root author");const h=qt(t.filter(u=>u[0]==="p"&&ge.test(u[1]||"")).map(u=>u[1]));if(h.includes(l.pubkey)||n.push("p tags must include the parent author"),n.length)return{ok:!1,errors:n};const d=(u,y)=>{const f=t.find($=>$[0]===u&&ge.test($[1]||""));return f&&(f[3]===void 0||f[3]===y)?f[1]:null};return{ok:!0,lineage:{root:{...o,eventId:d("E",o.pubkey),relay:Be(r[0][2])},parent:{...l,eventId:d("e",l.pubkey),relay:Be(a[0][2])},authors:h}}}function ct(s){const[e,t,i,...r]=s.split(":");return r.length||!Le.includes(Number(e))||!ge.test(t||"")||!Ht.test(i||"")?null:{address:s,pubkey:t,id:i}}function Be(s){try{return typeof s=="string"&&new URL(s).protocol==="wss:"?s:null}catch{return null}}const dt=(s,e)=>e?[...s,e]:s,qt=s=>[...new Set(s)],ht="black-site-map",ye=1,$s=[64,44],vs=new Set(["$schema","format","version","id","name","desc","size","theme","generate","spawns","start","exfil","water","paved","roads","walls","buildings","containers","tanks","pipes","crates","wrecks","sandbags","enemies","caches"]),ks=["water","paved","roads","walls","pipes","crates","wrecks","sandbags"],ut=["grass","concrete","sand","snow"],pt=["b","c","T","h"];function Ge(s){const e=[],t=(d,u)=>e.push(`${d}: ${u}`);if(!ke(s))return["map: must be an object"];for(const d of Object.keys(s))vs.has(d)||t(d,"unknown field");s.format!==ht&&t("format",`must be "${ht}"`),s.version!==ye&&t("version",`must be ${ye}`),(typeof s.id!="string"||!/^[a-z0-9][a-z0-9-]{0,31}$/.test(s.id))&&t("id","must be 1-32 chars of a-z, 0-9 and -, not starting with -"),(typeof s.name!="string"||!s.name.length||s.name.length>32)&&t("name","must be a string of 1-32 chars"),s.desc!==void 0&&(typeof s.desc!="string"||s.desc.length>200)&&t("desc","must be a string of at most 200 chars");let[i,r]=$s;s.size!==void 0&&(!Array.isArray(s.size)||s.size.length!==2||!s.size.every(d=>z(d,16,256))?t("size","must be [width, height], each 16-256"):[i,r]=s.size);const a=(d,u,y=!1)=>{if(!Array.isArray(u)||u.length<2||!z(u[0],0,255)||!z(u[1],0,255))return t(d,"must start with integer x, y"),!1;const f=y?1:0;return u[0]<f||u[1]<f||u[0]>i-1-f||u[1]>r-1-f?(t(d,`[${u[0]}, ${u[1]}] is outside the ${y?"playable area":"map"} (${i}x${r})`),!1):!0},n=(d,u,y)=>Array.isArray(u)&&u.length!==2?(t(d,"must be [x, y]"),!1):a(d,u,y),o=(d,u,y=0)=>!Array.isArray(u)||u.length!==4+y||!u.slice(0,4).every(f=>z(f,0,255))?(t(d,`must be [x0, y0, x1, y1${y?", ...":""}]`),!1):u[0]>u[2]||u[1]>u[3]?(t(d,"corners must be ordered (x0 <= x1, y0 <= y1)"),!1):a(d,u)&&a(d,[u[2],u[3]]),l=(d,u)=>Array.isArray(u)&&u.length===2?n(d,u):o(d,u),c=(d,u)=>{const y=s[d];if(y!==void 0){if(!Array.isArray(y))return t(d,"must be an array");y.forEach((f,$)=>u(`${d}[${$}]`,f))}},p=s.theme;if(!ke(p))t("theme","is required");else{for(const d of Object.keys(p))["ground","road","style","markings","clutter"].includes(d)||t(`theme.${d}`,"unknown field");for(const d of["ground","road"])(!Array.isArray(p[d])||p[d].length!==3||!p[d].every(u=>z(u,0,255)))&&t(`theme.${d}`,"must be [r, g, b], each 0-255");ut.includes(p.style)||t("theme.style",`must be one of ${ut.join(", ")}`),p.markings!==void 0&&typeof p.markings!="boolean"&&t("theme.markings","must be a boolean"),p.clutter!==void 0&&(!Array.isArray(p.clutter)||!p.clutter.length||!p.clutter.every(d=>pt.includes(d)))&&t("theme.clutter",`must be a non-empty list of ${pt.join(", ")}`)}const h=(d,u)=>{const y=s[d];if(y!==void 0){if(!ke(y))return t(d,"must be an object");for(const f of Object.keys(y))f in u||t(`${d}.${f}`,"unknown field");for(const[f,$]of Object.entries(u))y[f]!==void 0&&!$[0](y[f])&&t(`${d}.${f}`,$[1])}};h("generate",{expand:[d=>typeof d=="boolean","must be a boolean"],buildings:[d=>z(d,0,64),"must be an integer 0-64"],cover:[d=>z(d,0,400),"must be an integer 0-400"],seed:[d=>z(d,0,4294967295),"must be an unsigned 32-bit integer"]}),h("spawns",{extraEnemies:[d=>typeof d=="number"&&d>=0&&d<=2,"must be a number 0-2"],extraCaches:[d=>z(d,0,20),"must be an integer 0-20"]}),s.start===void 0?t("start","is required"):n("start",s.start,!0),s.exfil===void 0?t("exfil","is required"):n("exfil",s.exfil,!0);for(const d of ks)c(d,l);return c("buildings",(d,u)=>{if(!ke(u))return t(d,"must be an object");for(const A of Object.keys(u))["r","doors","walls"].includes(A)||t(`${d}.${A}`,"unknown field");if(!o(`${d}.r`,u.r))return;const[y,f,$,S]=u.r;($-y<2||S-f<2)&&t(`${d}.r`,"must be at least 3x3"),u.doors!==void 0&&(Array.isArray(u.doors)?u.doors.forEach((A,E)=>{const se=`${d}.doors[${E}]`;if(!n(se,A))return;A[0]>=y&&A[0]<=$&&A[1]>=f&&A[1]<=S&&(A[0]===y||A[0]===$||A[1]===f||A[1]===S)||t(se,"must be on the building's outer wall")}):t(`${d}.doors`,"must be an array")),u.walls!==void 0&&(Array.isArray(u.walls)?u.walls.forEach((A,E)=>l(`${d}.walls[${E}]`,A)):t(`${d}.walls`,"must be an array"))}),c("containers",(d,u)=>{o(d,u,1)&&!(typeof u[4]=="string"&&/^#[0-9a-fA-F]{6}$/.test(u[4]))&&t(d,'colour must be "#rrggbb"')}),c("tanks",(d,u)=>{n(d,u)&&(u[0]+2>i-1||u[1]+2>r-1)&&t(d,"the 3x3 tank must fit inside the map")}),c("enemies",(d,u)=>{if(!Array.isArray(u)||u.length<2||u.length>3)return t(d,'must be [x, y] or [x, y, "p"]');a(d,u,!0)&&u.length===3&&u[2]!=="p"&&t(d,'third value must be "p" (patrol)')}),c("caches",(d,u)=>{if(!Array.isArray(u)||u.length<2||u.length>3)return t(d,"must be [x, y] or [x, y, kind]");a(d,u,!0)&&u.length===3&&!["start","weapons"].includes(u[2])&&t(d,'kind must be "start" or "weapons"')}),e}function ke(s){return s!==null&&typeof s=="object"&&!Array.isArray(s)}function z(s,e,t){return Number.isInteger(s)&&s>=e&&s<=t}const ne=35858,ze="application/json",V=256*1024,He=/^[0-9a-f]{64}$/;function As(s,e,t={}){const i=Ge(s);if(i.length)throw new Error(`map is invalid:
${i.join(`
`)}`);if(!He.test(e.sha256||""))throw new Error("upload.sha256 must be 64 lowercase hex chars");if(!Number.isInteger(e.size)||e.size<=0||e.size>V)throw new Error(`upload.size must be 1-${V} bytes`);if(t.remixOf&&t.remixOf.kind!==ne)throw new Error(`a map can only remix a kind ${ne} map`);const r=[...new Set([e.url,...e.fallbackUrls||[]].filter(Boolean).map(Xe).filter(Boolean))],a=[["d",s.id],["title",s.name],...s.desc?[["summary",s.desc]]:[],["version",String(ye)],["x",e.sha256],["m",ze],["size",String(e.size)],...r.map(n=>["server",n]),...t.image?[["image",t.image]]:[],...(t.hashtags||[]).map(n=>["t",String(n).toLowerCase()]),...t.remixOf?ws(t.remixOf,{relay:t.remixRelay}):[],...t.client?[["client",t.client]]:[],["alt",`Black Site map: ${s.name}`]];return{kind:ne,content:t.content||"",tags:a,created_at:Math.floor(Date.now()/1e3)}}function Ze(s){const e=[],t=d=>e.push(d);if(!s||s.kind!==ne)return{ok:!1,errors:[`kind must be ${ne}`]};He.test(s.pubkey||"")||t("pubkey must be 64 lowercase hex chars");const i=Array.isArray(s.tags)?s.tags.filter(d=>Array.isArray(d)&&typeof d[0]=="string"):[],r=d=>i.find(u=>u[0]===d)?.[1],a=d=>i.filter(u=>u[0]===d&&typeof u[1]=="string").map(u=>u[1]),n=r("d");(typeof n!="string"||!/^[a-z0-9][a-z0-9-]{0,31}$/.test(n))&&t("d tag must be a valid map id");const o=Number(r("version"));!Number.isInteger(o)||o<1?t("version tag must be a positive integer"):o>ye&&t(`map version ${o} is newer than this game supports (${ye})`);const l=r("x");He.test(l||"")||t("x tag must be the 64-char lowercase hex SHA-256 of the map JSON"),r("m")!==void 0&&r("m")!==ze&&t(`m tag must be "${ze}"`);const c=r("size")===void 0?void 0:Number(r("size"));c!==void 0&&(!Number.isInteger(c)||c<=0||c>V)&&t(`size tag must be 1-${V} bytes`);const p=r("title");(typeof p!="string"||!p.length)&&t("title tag is required");const h=Kt(s);return h.ok||e.push(...h.errors),e.length?{ok:!1,errors:e}:{ok:!0,map:{address:`${ne}:${s.pubkey}:${n}`,pubkey:s.pubkey,id:n,version:o,sha256:l,size:c,title:p,summary:r("summary")||"",image:r("image")||null,hashtags:a("t"),servers:a("server").map(Xe).filter(Boolean),content:typeof s.content=="string"?s.content:"",lineage:h.lineage,createdAt:s.created_at}}}function Ss(s,e=[]){const t=[...new Set([...s.servers,...e.map(Xe).filter(Boolean)])];return[`blossom:${s.sha256}`,...t.map(i=>`${i}/${s.sha256}.json`)]}async function Es(s,e,t){const i=s instanceof Uint8Array?s:new Uint8Array(s);if(i.byteLength>V)return{ok:!1,errors:[`map JSON is larger than ${V} bytes`]};if(e.size!==void 0&&i.byteLength!==e.size)return{ok:!1,errors:[`map JSON is ${i.byteLength} bytes, the event says ${e.size}`]};if(t&&await t(i)!==e.sha256)return{ok:!1,errors:["map JSON does not match the x tag hash"]};let r;try{r=JSON.parse(new TextDecoder("utf-8",{fatal:!0}).decode(i))}catch{return{ok:!1,errors:["map is not valid UTF-8 JSON"]}}const a=Ge(r);return!a.length&&r.id!==e.id&&a.push(`map id "${r.id}" does not match the d tag "${e.id}"`),a.length?{ok:!1,errors:a}:{ok:!0,map:r}}function Xe(s){try{const e=new URL(s);return e.protocol==="https:"&&!e.username&&!e.password?e.origin:null}catch{return null}}const Ae=(s,e,t)=>s.tags.some(i=>i[0]===e&&i[1]===t);async function xs(s){const e=await U.query([{kinds:[3],authors:[s],limit:1}],{authors:[s],timeoutMs:8e3});if(e.error||e.incomplete)throw new Error("Could not load your follows. Please retry.");const t=e.events.map(i=>i.event).filter(i=>i.kind===3&&i.pubkey===s).sort((i,r)=>r.created_at-i.created_at||i.id.localeCompare(r.id))[0];return[...new Set((t?.tags??[]).filter(i=>i[0]==="p"&&/^[a-f0-9]{64}$/.test(i[1])).map(i=>i[1]))]}async function Ms(s){const e=s.metadata.address,t=await Promise.allSettled([U.query([{kinds:[1111],"#A":[e],limit:100},{kinds:[1111],"#E":[s.event.id],limit:100}],{limit:200,timeoutMs:8e3}),U.query([{kinds:[7],"#a":[e],limit:200},{kinds:[7],"#e":[s.event.id],limit:200}],{limit:400,timeoutMs:8e3}),U.query([{kinds:[35858],"#a":[e],limit:100}],{limit:100,timeoutMs:8e3})]);let i=!1;const r=t.map(c=>c.status==="rejected"?(i=!0,[]):((c.value.error||c.value.incomplete)&&(i=!0),[...new Map(c.value.events.map(p=>[p.event.id,p])).values()])),a=r[0].map(c=>c.event).filter(c=>c.kind===1111&&Ae(c,"K","35858")&&(Ae(c,"A",e)||Ae(c,"E",s.event.id))).sort((c,p)=>c.created_at-p.created_at||c.id.localeCompare(p.id)),n=new Map;for(const{event:c}of r[1]){if(c.kind!==7||!(Ae(c,"a",e)||c.tags.filter(u=>u[0]==="e").at(-1)?.[1]===s.event.id))continue;const p=c.content===""?"+":c.content,h=`${c.pubkey}:${p}`,d=n.get(h);(!d||c.created_at>d.created_at)&&n.set(h,c)}const o=new Map;for(const c of n.values()){const p=c.content===""||c.content==="+"?"Upvotes":c.content==="-"?"Downvotes":c.content;o.set(p,(o.get(p)??0)+1)}const l=new Map;for(const c of r[2]){const p=Ze(c.event);if(!p.ok||p.map.lineage?.parent.address!==e)continue;const h=l.get(p.map.address);(!h||c.event.created_at>h.event.created_at||c.event.created_at===h.event.created_at&&c.event.id<h.event.id)&&l.set(p.map.address,{event:c.event,metadata:p.map,relayHints:c.sidecar?.relayHints??[]})}return{comments:a,reactions:[...o].map(([c,p])=>({label:c,count:p})),remixes:[...l.values()].sort((c,p)=>p.event.created_at-c.event.created_at),notice:`${i?"Some activity could not be loaded. Retry to refresh. ":""}Activity reflects available relays and may be incomplete (up to 200 comments, 400 reactions and 100 remixes).`}}async function Cs(s,e){const t=e.trim();if(!t||t.length>4e3)throw new Error("Write a comment of 1–4000 characters.");const i=await U.publish({kind:1111,content:t,created_at:Math.floor(Date.now()/1e3),tags:[["A",s.metadata.address],["K","35858"],["P",s.event.pubkey],["a",s.metadata.address],["e",s.event.id],["k","35858"],["p",s.event.pubkey]]});if(!i.ok||!i.event)throw new Error(i.error??"Comment could not be published.");return i.event}/**
 * @license
 * Copyright 2019 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */const Ce=globalThis,Qe=Ce.ShadowRoot&&(Ce.ShadyCSS===void 0||Ce.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Wt=Symbol(),mt=new WeakMap;let _s=class{constructor(e,t,i){if(this._$cssResult$=!0,i!==Wt)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(Qe&&e===void 0){const i=t!==void 0&&t.length===1;i&&(e=mt.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),i&&mt.set(t,e))}return e}toString(){return this.cssText}};const Rs=s=>new _s(typeof s=="string"?s:s+"",void 0,Wt),Os=(s,e)=>{if(Qe)s.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const t of e){const i=document.createElement("style"),r=Ce.litNonce;r!==void 0&&i.setAttribute("nonce",r),i.textContent=t.cssText,s.appendChild(i)}},ft=Qe?s=>s:s=>s instanceof CSSStyleSheet?(e=>{let t="";for(const i of e.cssRules)t+=i.cssText;return Rs(t)})(s):s;/**
 * @license
 * Copyright 2017 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */const{is:Ts,defineProperty:Ps,getOwnPropertyDescriptor:Is,getOwnPropertyNames:js,getOwnPropertySymbols:Us,getPrototypeOf:Ds}=Object,Oe=globalThis,bt=Oe.trustedTypes,Ns=bt?bt.emptyScript:"",Ls=Oe.reactiveElementPolyfillSupport,fe=(s,e)=>s,Ke={toAttribute(s,e){switch(e){case Boolean:s=s?Ns:null;break;case Object:case Array:s=s==null?s:JSON.stringify(s)}return s},fromAttribute(s,e){let t=s;switch(e){case Boolean:t=s!==null;break;case Number:t=s===null?null:Number(s);break;case Object:case Array:try{t=JSON.parse(s)}catch{t=null}}return t}},Yt=(s,e)=>!Ts(s,e),gt={attribute:!0,type:String,converter:Ke,reflect:!1,useDefault:!1,hasChanged:Yt};Symbol.metadata??=Symbol("metadata"),Oe.litPropertyMetadata??=new WeakMap;let ae=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=gt){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){const i=Symbol(),r=this.getPropertyDescriptor(e,i,t);r!==void 0&&Ps(this.prototype,e,r)}}static getPropertyDescriptor(e,t,i){const{get:r,set:a}=Is(this.prototype,e)??{get(){return this[t]},set(n){this[t]=n}};return{get:r,set(n){const o=r?.call(this);a?.call(this,n),this.requestUpdate(e,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??gt}static _$Ei(){if(this.hasOwnProperty(fe("elementProperties")))return;const e=Ds(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(fe("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(fe("properties"))){const t=this.properties,i=[...js(t),...Us(t)];for(const r of i)this.createProperty(r,t[r])}const e=this[Symbol.metadata];if(e!==null){const t=litPropertyMetadata.get(e);if(t!==void 0)for(const[i,r]of t)this.elementProperties.set(i,r)}this._$Eh=new Map;for(const[t,i]of this.elementProperties){const r=this._$Eu(t,i);r!==void 0&&this._$Eh.set(r,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const i=new Set(e.flat(1/0).reverse());for(const r of i)t.unshift(ft(r))}else e!==void 0&&t.push(ft(e));return t}static _$Eu(e,t){const i=t.attribute;return i===!1?void 0:typeof i=="string"?i:typeof e=="string"?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),this.renderRoot!==void 0&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const i of t.keys())this.hasOwnProperty(i)&&(e.set(i,this[i]),delete this[i]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return Os(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,i){this._$AK(e,i)}_$ET(e,t){const i=this.constructor.elementProperties.get(e),r=this.constructor._$Eu(e,i);if(r!==void 0&&i.reflect===!0){const a=(i.converter?.toAttribute!==void 0?i.converter:Ke).toAttribute(t,i.type);this._$Em=e,a==null?this.removeAttribute(r):this.setAttribute(r,a),this._$Em=null}}_$AK(e,t){const i=this.constructor,r=i._$Eh.get(e);if(r!==void 0&&this._$Em!==r){const a=i.getPropertyOptions(r),n=typeof a.converter=="function"?{fromAttribute:a.converter}:a.converter?.fromAttribute!==void 0?a.converter:Ke;this._$Em=r;const o=n.fromAttribute(t,a.type);this[r]=o??this._$Ej?.get(r)??o,this._$Em=null}}requestUpdate(e,t,i,r=!1,a){if(e!==void 0){const n=this.constructor;if(r===!1&&(a=this[e]),i??=n.getPropertyOptions(e),!((i.hasChanged??Yt)(a,t)||i.useDefault&&i.reflect&&a===this._$Ej?.get(e)&&!this.hasAttribute(n._$Eu(e,i))))return;this.C(e,t,i)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(e,t,{useDefault:i,reflect:r,wrapped:a},n){i&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,n??t??this[e]),a!==!0||n!==void 0)||(this._$AL.has(e)||(this.hasUpdated||i||(t=void 0),this._$AL.set(e,t)),r===!0&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const e=this.scheduleUpdate();return e!=null&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[r,a]of this._$Ep)this[r]=a;this._$Ep=void 0}const i=this.constructor.elementProperties;if(i.size>0)for(const[r,a]of i){const{wrapped:n}=a,o=this[r];n!==!0||this._$AL.has(r)||o===void 0||this.C(r,void 0,a,o)}}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(i=>i.hostUpdate?.()),this.update(t)):this._$EM()}catch(i){throw e=!1,this._$EM(),i}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(e){}firstUpdated(e){}};ae.elementStyles=[],ae.shadowRootOptions={mode:"open"},ae[fe("elementProperties")]=new Map,ae[fe("finalized")]=new Map,Ls?.({ReactiveElement:ae}),(Oe.reactiveElementVersions??=[]).push("2.1.2");/**
 * @license
 * Copyright 2017 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */const et=globalThis,yt=s=>s,_e=et.trustedTypes,wt=_e?_e.createPolicy("lit-html",{createHTML:s=>s}):void 0,Jt="$lit$",F=`lit$${Math.random().toFixed(9).slice(2)}$`,Ft="?"+F,Bs=`<${Ft}>`,te=document,we=()=>te.createComment(""),$e=s=>s===null||typeof s!="object"&&typeof s!="function",tt=Array.isArray,zs=s=>tt(s)||typeof s?.[Symbol.iterator]=="function",Ie=`[ 	
\f\r]`,he=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,$t=/-->/g,vt=/>/g,G=RegExp(`>|${Ie}(?:([^\\s"'>=/]+)(${Ie}*=${Ie}*(?:[^ 	
\f\r"'\`<>=]|("|')|))|$)`,"g"),kt=/'/g,At=/"/g,Vt=/^(?:script|style|textarea|title)$/i,Hs=s=>(e,...t)=>({_$litType$:s,strings:e,values:t}),b=Hs(1),j=Symbol.for("lit-noChange"),k=Symbol.for("lit-nothing"),St=new WeakMap,X=te.createTreeWalker(te,129);function Gt(s,e){if(!tt(s)||!s.hasOwnProperty("raw"))throw Error("invalid template strings array");return wt!==void 0?wt.createHTML(e):e}const Ks=(s,e)=>{const t=s.length-1,i=[];let r,a=e===2?"<svg>":e===3?"<math>":"",n=he;for(let o=0;o<t;o++){const l=s[o];let c,p,h=-1,d=0;for(;d<l.length&&(n.lastIndex=d,p=n.exec(l),p!==null);)d=n.lastIndex,n===he?p[1]==="!--"?n=$t:p[1]!==void 0?n=vt:p[2]!==void 0?(Vt.test(p[2])&&(r=RegExp("</"+p[2],"g")),n=G):p[3]!==void 0&&(n=G):n===G?p[0]===">"?(n=r??he,h=-1):p[1]===void 0?h=-2:(h=n.lastIndex-p[2].length,c=p[1],n=p[3]===void 0?G:p[3]==='"'?At:kt):n===At||n===kt?n=G:n===$t||n===vt?n=he:(n=G,r=void 0);const u=n===G&&s[o+1].startsWith("/>")?" ":"";a+=n===he?l+Bs:h>=0?(i.push(c),l.slice(0,h)+Jt+l.slice(h)+F+u):l+F+(h===-2?o:u)}return[Gt(s,a+(s[t]||"<?>")+(e===2?"</svg>":e===3?"</math>":"")),i]};class ve{constructor({strings:e,_$litType$:t},i){let r;this.parts=[];let a=0,n=0;const o=e.length-1,l=this.parts,[c,p]=Ks(e,t);if(this.el=ve.createElement(c,i),X.currentNode=this.el.content,t===2||t===3){const h=this.el.content.firstChild;h.replaceWith(...h.childNodes)}for(;(r=X.nextNode())!==null&&l.length<o;){if(r.nodeType===1){if(r.hasAttributes())for(const h of r.getAttributeNames())if(h.endsWith(Jt)){const d=p[n++],u=r.getAttribute(h).split(F),y=/([.?@])?(.*)/.exec(d);l.push({type:1,index:a,name:y[2],strings:u,ctor:y[1]==="."?Ws:y[1]==="?"?Ys:y[1]==="@"?Js:Te}),r.removeAttribute(h)}else h.startsWith(F)&&(l.push({type:6,index:a}),r.removeAttribute(h));if(Vt.test(r.tagName)){const h=r.textContent.split(F),d=h.length-1;if(d>0){r.textContent=_e?_e.emptyScript:"";for(let u=0;u<d;u++)r.append(h[u],we()),X.nextNode(),l.push({type:2,index:++a});r.append(h[d],we())}}}else if(r.nodeType===8)if(r.data===Ft)l.push({type:2,index:a});else{let h=-1;for(;(h=r.data.indexOf(F,h+1))!==-1;)l.push({type:7,index:a}),h+=F.length-1}a++}}static createElement(e,t){const i=te.createElement("template");return i.innerHTML=e,i}}function oe(s,e,t=s,i){if(e===j)return e;let r=i!==void 0?t._$Co?.[i]:t._$Cl;const a=$e(e)?void 0:e._$litDirective$;return r?.constructor!==a&&(r?._$AO?.(!1),a===void 0?r=void 0:(r=new a(s),r._$AT(s,t,i)),i!==void 0?(t._$Co??=[])[i]=r:t._$Cl=r),r!==void 0&&(e=oe(s,r._$AS(s,e.values),r,i)),e}class qs{constructor(e,t){this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=t}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(e){const{el:{content:t},parts:i}=this._$AD,r=(e?.creationScope??te).importNode(t,!0);X.currentNode=r;let a=X.nextNode(),n=0,o=0,l=i[0];for(;l!==void 0;){if(n===l.index){let c;l.type===2?c=new ce(a,a.nextSibling,this,e):l.type===1?c=new l.ctor(a,l.name,l.strings,this,e):l.type===6&&(c=new Fs(a,this,e)),this._$AV.push(c),l=i[++o]}n!==l?.index&&(a=X.nextNode(),n++)}return X.currentNode=te,r}p(e){let t=0;for(const i of this._$AV)i!==void 0&&(i.strings!==void 0?(i._$AI(e,i,t),t+=i.strings.length-2):i._$AI(e[t])),t++}}class ce{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,i,r){this.type=2,this._$AH=k,this._$AN=void 0,this._$AA=e,this._$AB=t,this._$AM=i,this.options=r,this._$Cv=r?.isConnected??!0}get parentNode(){let e=this._$AA.parentNode;const t=this._$AM;return t!==void 0&&e?.nodeType===11&&(e=t.parentNode),e}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(e,t=this){e=oe(this,e,t),$e(e)?e===k||e==null||e===""?(this._$AH!==k&&this._$AR(),this._$AH=k):e!==this._$AH&&e!==j&&this._(e):e._$litType$!==void 0?this.$(e):e.nodeType!==void 0?this.T(e):zs(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==k&&$e(this._$AH)?this._$AA.nextSibling.data=e:this.T(te.createTextNode(e)),this._$AH=e}$(e){const{values:t,_$litType$:i}=e,r=typeof i=="number"?this._$AC(e):(i.el===void 0&&(i.el=ve.createElement(Gt(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===r)this._$AH.p(t);else{const a=new qs(r,this),n=a.u(this.options);a.p(t),this.T(n),this._$AH=a}}_$AC(e){let t=St.get(e.strings);return t===void 0&&St.set(e.strings,t=new ve(e)),t}k(e){tt(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let i,r=0;for(const a of e)r===t.length?t.push(i=new ce(this.O(we()),this.O(we()),this,this.options)):i=t[r],i._$AI(a),r++;r<t.length&&(this._$AR(i&&i._$AB.nextSibling,r),t.length=r)}_$AR(e=this._$AA.nextSibling,t){for(this._$AP?.(!1,!0,t);e!==this._$AB;){const i=yt(e).nextSibling;yt(e).remove(),e=i}}setConnected(e){this._$AM===void 0&&(this._$Cv=e,this._$AP?.(e))}}class Te{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(e,t,i,r,a){this.type=1,this._$AH=k,this._$AN=void 0,this.element=e,this.name=t,this._$AM=r,this.options=a,i.length>2||i[0]!==""||i[1]!==""?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=k}_$AI(e,t=this,i,r){const a=this.strings;let n=!1;if(a===void 0)e=oe(this,e,t,0),n=!$e(e)||e!==this._$AH&&e!==j,n&&(this._$AH=e);else{const o=e;let l,c;for(e=a[0],l=0;l<a.length-1;l++)c=oe(this,o[i+l],t,l),c===j&&(c=this._$AH[l]),n||=!$e(c)||c!==this._$AH[l],c===k?e=k:e!==k&&(e+=(c??"")+a[l+1]),this._$AH[l]=c}n&&!r&&this.j(e)}j(e){e===k?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,e??"")}}class Ws extends Te{constructor(){super(...arguments),this.type=3}j(e){this.element[this.name]=e===k?void 0:e}}class Ys extends Te{constructor(){super(...arguments),this.type=4}j(e){this.element.toggleAttribute(this.name,!!e&&e!==k)}}class Js extends Te{constructor(e,t,i,r,a){super(e,t,i,r,a),this.type=5}_$AI(e,t=this){if((e=oe(this,e,t,0)??k)===j)return;const i=this._$AH,r=e===k&&i!==k||e.capture!==i.capture||e.once!==i.once||e.passive!==i.passive,a=e!==k&&(i===k||r);r&&this.element.removeEventListener(this.name,this,i),a&&this.element.addEventListener(this.name,this,e),this._$AH=e}handleEvent(e){typeof this._$AH=="function"?this._$AH.call(this.options?.host??this.element,e):this._$AH.handleEvent(e)}}class Fs{constructor(e,t,i){this.element=e,this.type=6,this._$AN=void 0,this._$AM=t,this.options=i}get _$AU(){return this._$AM._$AU}_$AI(e){oe(this,e)}}const Vs={I:ce},Gs=et.litHtmlPolyfillSupport;Gs?.(ve,ce),(et.litHtmlVersions??=[]).push("3.3.3");const Zs=(s,e,t)=>{const i=t?.renderBefore??e;let r=i._$litPart$;if(r===void 0){const a=t?.renderBefore??null;i._$litPart$=r=new ce(e.insertBefore(we(),a),a,void 0,t??{})}return r._$AI(s),r};/**
 * @license
 * Copyright 2017 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */const st=globalThis;let be=class extends ae{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){const t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=Zs(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return j}};be._$litElement$=!0,be.finalized=!0,st.litElementHydrateSupport?.({LitElement:be});const Xs=st.litElementPolyfillSupport;Xs?.({LitElement:be});(st.litElementVersions??=[]).push("4.2.2");/**
 * @license
 * Copyright 2017 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */const Y={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,ELEMENT:6},it=s=>(...e)=>({_$litDirective$:s,values:e});let rt=class{constructor(e){}get _$AU(){return this._$AM._$AU}_$AT(e,t,i){this._$Ct=e,this._$AM=t,this._$Ci=i}_$AS(e,t){return this.update(e,t)}update(e,t){return this.render(...t)}};/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */const{I:Qs}=Vs,Et=s=>s,ei=s=>s.strings===void 0,xt=()=>document.createComment(""),ue=(s,e,t)=>{const i=s._$AA.parentNode,r=e===void 0?s._$AB:e._$AA;if(t===void 0){const a=i.insertBefore(xt(),r),n=i.insertBefore(xt(),r);t=new Qs(a,n,s,s.options)}else{const a=t._$AB.nextSibling,n=t._$AM,o=n!==s;if(o){let l;t._$AQ?.(s),t._$AM=s,t._$AP!==void 0&&(l=s._$AU)!==n._$AU&&t._$AP(l)}if(a!==r||o){let l=t._$AA;for(;l!==a;){const c=Et(l).nextSibling;Et(i).insertBefore(l,r),l=c}}}return t},Z=(s,e,t=s)=>(s._$AI(e,t),s),ti={},Zt=(s,e=ti)=>s._$AH=e,si=s=>s._$AH,je=s=>{s._$AR(),s._$AA.remove()};/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */const x=it(class extends rt{constructor(s){if(super(s),s.type!==Y.PROPERTY&&s.type!==Y.ATTRIBUTE&&s.type!==Y.BOOLEAN_ATTRIBUTE)throw Error("The `live` directive is not allowed on child or event bindings");if(!ei(s))throw Error("`live` bindings can only contain a single expression")}render(s){return s}update(s,[e]){if(e===j||e===k)return e;const t=s.element,i=s.name;if(s.type===Y.PROPERTY){if(e===t[i])return j}else if(s.type===Y.BOOLEAN_ATTRIBUTE){if(!!e===t.hasAttribute(i))return j}else if(s.type===Y.ATTRIBUTE&&t.getAttribute(i)===e+"")return j;return Zt(s),e}}),ii="../../../spec/schemas/black-site-map.schema.json",ri="black-site-map",ai=1,ni="town",oi="TOWN",li="Streets and houses. Fight through the town to the helipad.",ci=[64,44],di={ground:[78,73,60],road:[44,45,47],style:"grass",clutter:["T","T","b","c"]},hi={expand:!0,buildings:16,cover:80},ui={extraEnemies:.2,extraCaches:3},pi=[3,41],mi=[57,5],fi=[[1,27,62,28],[36,1,37,42]],bi=[{r:[10,30,19,37],doors:[[14,30],[15,30],[19,33],[19,34]],walls:[[16,31,16,33]]},{r:[24,33,35,41],doors:[[24,36],[24,37],[28,33],[29,33],[35,38],[35,39]],walls:[[30,34,30,35],[30,38,30,40]]},{r:[8,14,17,23],doors:[[12,23],[13,23],[17,17],[17,18]],walls:[[9,19,12,19]]},{r:[22,16,33,26],doors:[[22,20],[22,21],[29,16],[30,16],[25,26],[26,26],[33,22],[33,23]],walls:[[27,17,27,19],[27,22,27,25]]},{r:[40,30,50,37],doors:[[40,33],[40,34],[45,30],[46,30],[50,32],[50,33]],walls:[[41,35,44,35],[47,35,49,35]]},{r:[38,8,47,17],doors:[[38,11],[38,12],[44,17],[45,17],[47,9],[47,10]],walls:[[42,9,42,11],[42,14,42,16]]},{r:[4,3,13,9],doors:[[8,9],[9,9],[13,5],[13,6]],walls:[]},{r:[52,18,60,25],doors:[[52,21],[52,22],[56,18],[57,18]],walls:[[53,23,55,23]]}],gi=[[6,35],[7,35],[15,26],[19,13],[20,13],[27,30],[28,30],[33,8],[34,8],[44,22],[45,22],[46,22],[55,13],[56,13],[30,12],[12,12],[3,20],[4,20],[60,30],[61,30],[57,38],[48,40],[49,40],[23,8],[16,3],[29,3],[24,23],[31,19],[26,40],[32,35],[43,32],[11,6]],yi=[[8,27,9,27],[20,28,21,28],[36,10,36,11],[37,35,37,36],[50,27,51,27],[14,40,15,40]],wi=[[6,39,6,41],[29,9,31,9],[47,24,48,24],[20,36,20,37],[58,34,59,34],[51,2,51,4],[51,7,51,10],[52,10,54,10],[58,10,62,10]],$i=[[9,25,"p"],[21,30,"p"],[31,30,"p"],[38,39,"p"],[45,40,"p"],[20,11,"p"],[34,13,"p"],[49,22,"p"],[57,31,"p"],[60,14,"p"],[15,34],[27,39],[33,35],[11,17],[24,19],[31,24],[43,33],[48,32],[40,14],[45,11],[10,5],[57,21],[54,5],[59,8],[56,3]],vi=[[5,39,"start"],[12,36],[26,34],[34,40,"weapons"],[9,22],[23,25],[32,17],[49,36],[46,16,"weapons"],[5,4],[59,24,"weapons"],[3,21],[61,31],[28,31],[44,34]],ki={$schema:ii,format:ri,version:ai,id:ni,name:oi,desc:li,size:ci,theme:di,generate:hi,spawns:ui,start:pi,exfil:mi,roads:fi,buildings:bi,crates:gi,wrecks:yi,sandbags:wi,enemies:$i,caches:vi},Ai="../../../spec/schemas/black-site-map.schema.json",Si="black-site-map",Ei=1,xi="docks",Mi="DOCKYARD",Ci="Container stacks and warehouses on the waterfront. Extract from the north pier.",_i=[64,44],Ri={ground:[88,88,86],road:[52,53,55],style:"concrete",clutter:["b","b","c"]},Oi={expand:!0,buildings:16,cover:80},Ti={extraEnemies:.2,extraCaches:3},Pi=[2,41],Ii=[57,5],ji=[[52,1,62,42]],Ui=[[52,3,61,7],[52,19,58,22],[52,35,59,38]],Di=[[1,12,50,13],[24,1,25,42]],Ni=[{r:[3,2,20,10],doors:[[10,10],[11,10],[20,5],[20,6]],walls:[[12,3,12,5]]},{r:[28,2,46,9],doors:[[28,5],[28,6],[36,9],[37,9],[46,4],[46,5]],walls:[[37,3,37,4]]},{r:[4,30,18,40],doors:[[10,30],[11,30],[18,35],[18,36]],walls:[[11,31,11,33]]},{r:[36,33,47,41],doors:[[40,33],[41,33],[36,37],[36,38],[47,36],[47,37]],walls:[[41,34,41,35]]}],Li=[[4,15,8,16,"#8a3b2a"],[4,18,8,19,"#2f5a7a"],[12,15,16,16,"#6a7a3a"],[12,19,16,20,"#a87a2a"],[28,15,29,19,"#2f5a7a"],[32,15,33,19,"#8a3b2a"],[36,16,40,17,"#5a5a5a"],[36,20,40,21,"#2f6a5a"],[44,15,45,19,"#a87a2a"],[44,22,48,23,"#8a3b2a"],[28,24,32,25,"#6a7a3a"],[4,24,8,25,"#a87a2a"],[12,25,13,28,"#2f5a7a"],[20,30,21,34,"#8a3b2a"],[28,30,32,31,"#2f5a7a"],[40,26,44,27,"#6a7a3a"]],Bi=[[6,22],[7,22],[18,17],[18,18],[22,27],[34,27],[35,27],[47,12],[49,30],[49,31],[30,36],[31,36],[16,8],[40,5],[8,37],[44,39]],zi=[[25,20,25,21],[5,13,6,13],[40,12,41,12]],Hi=[[50,2,50,3],[50,7,50,8],[20,22,21,22],[44,30,45,30],[55,21,56,21]],Ki=[[10,14,"p"],[22,18,"p"],[30,13,"p"],[42,13,"p"],[48,28,"p"],[26,35,"p"],[34,22,"p"],[14,23,"p"],[50,17,"p"],[55,20,"p"],[8,5],[16,7],[33,5],[42,7],[14,33],[15,38],[40,38],[45,35],[38,18],[30,22],[17,27],[54,4],[59,6],[50,10]],qi=[[3,40,"start"],[6,4],[18,4,"weapons"],[30,4],[44,7,"weapons"],[6,38],[16,32],[38,40],[46,34,"weapons"],[10,21],[34,28],[50,36],[57,20],[22,40]],Wi={$schema:Ai,format:Si,version:Ei,id:xi,name:Mi,desc:Ci,size:_i,theme:Ri,generate:Oi,spawns:Ti,start:Pi,exfil:Ii,water:ji,paved:Ui,roads:Di,buildings:Ni,containers:Li,crates:Bi,wrecks:zi,sandbags:Hi,enemies:Ki,caches:qi},Yi="../../../spec/schemas/black-site-map.schema.json",Ji="black-site-map",Fi=1,Vi="refinery",Gi="REFINERY",Zi="Fuel tanks, pipelines and plant buildings. Exfil from the northwest pad.",Xi=[64,44],Qi={ground:[70,67,60],road:[46,46,48],style:"concrete",clutter:["b","b","b","c"]},er={expand:!0,buildings:16,cover:80},tr={extraEnemies:.2,extraCaches:3},sr=[61,41],ir=[5,5],rr=[[1,20,62,21],[20,1,21,42],[44,1,45,42]],ar=[{r:[6,33,16,40],doors:[[11,33],[12,33],[16,36],[16,37]],walls:[]},{r:[34,10,41,17],doors:[[34,13],[34,14],[38,17],[39,17]],walls:[]},{r:[49,32,59,39],doors:[[49,35],[49,36],[54,32],[55,32]],walls:[[52,36,52,38]]},{r:[24,12,31,17],doors:[[27,17],[28,17],[24,14]],walls:[]}],nr=[[4,12],[8,12],[4,25],[8,25],[26,4],[30,4],[34,4],[26,26],[30,26],[48,5],[52,5],[56,5],[50,26],[54,26],[26,34],[30,34]],or=[[2,17,8,17],[11,17,17,17],[23,23,23,26],[23,28,23,31],[36,24,42,24],[47,14,47,18],[47,23,47,30],[56,12,61,12],[56,16,61,16],[33,39,42,39],[10,23,17,23]],lr=[[14,3],[15,3],[38,3],[39,3],[13,28],[14,28],[37,28],[38,28],[58,22],[59,22],[3,22],[40,41],[41,41],[60,30],[61,30],[25,40],[26,40],[8,38],[40,12],[57,37]],cr=[[22,10,22,11],[44,30,44,31],[5,20,6,20],[55,20,56,20]],dr=[[2,9,4,9],[7,9,9,9],[10,2,10,4],[10,7,10,8],[17,40,17,41]],hr=[[8,7],[3,8],[8,3],[7,16,"p"],[7,29,"p"],[15,25],[29,9,"p"],[33,8],[28,30,"p"],[33,30],[24,38],[52,9,"p"],[57,9],[52,30,"p"],[57,24],[40,21,"p"],[11,37],[14,35],[37,13],[40,15],[26,14],[29,15],[51,34],[50,37],[44,10,"p"],[33,40,"p"]],ur=[[59,41,"start"],[12,36],[39,16,"weapons"],[27,13],[57,38,"weapons"],[2,2],[15,10],[32,9],[45,25],[3,30],[36,32],[60,14],[18,27],[24,2]],pr={$schema:Yi,format:Ji,version:Fi,id:Vi,name:Gi,desc:Zi,size:Xi,theme:Qi,generate:er,spawns:tr,start:sr,exfil:ir,roads:rr,buildings:ar,tanks:nr,pipes:or,crates:lr,wrecks:cr,sandbags:dr,enemies:hr,caches:ur},mr="../../../spec/schemas/black-site-map.schema.json",fr="black-site-map",br=1,gr="desert",yr="DESERT OUTPOST",wr="A walled compound in the sand. Breach the gates or go around.",$r=[64,44],vr={ground:[146,122,84],road:[112,95,68],style:"sand",markings:!1,clutter:["b","c","h"]},kr={expand:!0,buildings:16,cover:80},Ar={extraEnemies:.2,extraCaches:3},Sr=[2,41],Er=[58,5],xr=[[1,34,62,35],[47,1,48,42]],Mr=[[18,12,29,12],[33,12,45,12],[18,32,23,32],[27,32,37,32],[41,32,45,32],[18,13,18,19],[18,23,18,31],[45,13,45,23],[45,27,45,31]],Cr=[{r:[21,15,28,20],doors:[[24,20],[25,20],[28,17],[28,18]],walls:[]},{r:[34,15,42,21],doors:[[34,18],[34,19],[38,21],[39,21]],walls:[]},{r:[22,25,30,30],doors:[[26,25],[27,25],[30,27],[30,28]],walls:[]},{r:[35,25,42,30],doors:[[35,27],[35,28],[38,25],[39,25]],walls:[]},{r:[3,3,9,8],doors:[[6,8],[9,5]],walls:[]},{r:[52,37,58,41],doors:[[52,39],[55,37]],walls:[]},{r:[4,22,10,28],doors:[[10,25],[7,22]],walls:[]},{r:[50,14,57,20],doors:[[50,17],[53,20]],walls:[]},{r:[26,37,33,41],doors:[[29,37],[30,37]],walls:[]}],_r=[[23,22],[24,22],[32,22],[33,23],[40,23],[41,23],[20,14],[14,10],[15,10],[58,22],[2,15],[3,15],[38,38],[39,38],[16,40],[44,5],[45,5],[40,17],[5,4]],Rr=[[10,34,11,34],[48,8,48,9],[60,34,61,34],[36,6,37,6]],Or=[[29,10,30,10],[33,10,34,10],[12,16,13,16],[56,25,56,26],[8,32,9,32],[52,9,55,9],[59,9,62,9],[52,2,52,4],[52,7,52,8]],Tr=[[24,17],[38,17],[26,28],[39,28],[31,14],[31,23],[20,26],[43,15],[43,29],[31,10,"p"],[25,34,"p"],[39,34,"p"],[16,21,"p"],[12,13,"p"],[47,28,"p"],[60,20,"p"],[6,5],[7,25],[53,17],[55,39],[29,39],[56,4],[60,7],[54,7]],Pr=[[3,40,"start"],[22,16],[41,20,"weapons"],[23,29],[41,29,"weapons"],[32,27],[5,7],[8,27],[56,15],[57,40,"weapons"],[32,40],[2,2],[60,28]],Ir={$schema:mr,format:fr,version:br,id:gr,name:yr,desc:wr,size:$r,theme:vr,generate:kr,spawns:Ar,start:Sr,exfil:Er,roads:xr,walls:Mr,buildings:Cr,crates:_r,wrecks:Rr,sandbags:Or,enemies:Tr,caches:Pr},jr="../../../spec/schemas/black-site-map.schema.json",Ur="black-site-map",Dr=1,Nr="snow",Lr="FROZEN BASE",Br="A fenced military base in the snow. Cross it to the southeast pad.",zr=[64,44],Hr={ground:[186,192,198],road:[78,80,84],style:"snow",clutter:["T","T","c","b"]},Kr={expand:!0,buildings:16,cover:80},qr={extraEnemies:.2,extraCaches:3},Wr=[2,2],Yr=[58,40],Jr=[[1,6,54,7],[30,1,31,42],[1,24,62,25]],Fr=[[8,10,14,10],[17,10,29,10],[32,10,54,10],[8,38,29,38],[32,38,44,38],[47,38,54,38],[8,11,8,15],[8,18,8,23],[8,26,8,37],[54,11,54,23],[54,26,54,32],[54,35,54,37]],Vr=[{r:[11,12,27,16],doors:[[19,16],[20,16],[11,14]],walls:[]},{r:[34,12,51,16],doors:[[42,16],[43,16],[51,14]],walls:[]},{r:[11,18,27,22],doors:[[19,18],[20,18],[27,20]],walls:[]},{r:[34,18,51,22],doors:[[42,18],[43,18],[34,20]],walls:[]},{r:[11,28,27,35],doors:[[19,28],[20,28],[27,31],[27,32]],walls:[[20,30,20,34]]},{r:[34,28,51,35],doors:[[42,28],[43,28],[34,31],[34,32],[51,30]],walls:[[40,29,40,31]]},{r:[56,2,62,9],doors:[[56,6],[56,7],[59,9]],walls:[]},{r:[1,30,6,36],doors:[[6,32],[6,33],[3,30]],walls:[]},{r:[57,14,62,21],doors:[[57,17],[57,18]],walls:[]}],Gr=[[14,8],[15,8],[40,4],[41,4],[24,2],[3,15],[4,15],[12,26],[13,26],[24,26],[36,26],[37,26],[48,26],[49,26],[20,36],[21,36],[40,36],[41,36],[58,28],[59,28],[3,40],[4,40],[14,14],[46,14],[14,20],[46,20],[16,33],[46,33]],Zr=[[22,6,23,6],[30,30,30,31],[45,24,46,24],[60,24,61,24]],Xr=[[28,9,29,9],[32,9,33,9],[7,23,7,23],[7,26,7,26],[55,23,55,23],[55,26,55,26],[55,36,57,36],[60,36,62,36]],Qr=[[30,9,"p"],[9,24,"p"],[53,24,"p"],[31,39,"p"],[15,14],[24,14],[38,14],[48,14],[15,20],[24,20],[38,20],[48,20],[15,31],[24,33],[38,31],[48,33],[20,26,"p"],[42,26,"p"],[30,17,"p"],[45,37,"p"],[45,4,"p"],[15,4,"p"],[58,6],[60,18],[3,33],[56,38],[61,39],[58,42]],ea=[[4,2,"start"],[25,13],[36,13,"weapons"],[13,21],[50,21],[13,34,"weapons"],[26,30],[49,34,"weapons"],[2,35],[61,16],[60,4],[20,3],[44,26],[9,40]],ta={$schema:jr,format:Ur,version:Dr,id:Nr,name:Lr,desc:Br,size:zr,theme:Hr,generate:Kr,spawns:qr,start:Wr,exfil:Yr,roads:Jr,walls:Fr,buildings:Vr,crates:Gr,wrecks:Zr,sandbags:Xr,enemies:Qr,caches:ea},sa="../../schemas/black-site-map.schema.json",ia="black-site-map",ra=1,aa="outpost",na="OUTPOST",oa="A small hand-built example: one road, two buildings, no procedural content.",la=[40,30],ca={ground:[78,73,60],road:[44,45,47],style:"grass"},da=[3,26],ha=[35,4],ua=[[1,14,38,15]],pa=[{r:[6,4,15,11],doors:[[10,11],[11,11],[15,7],[15,8]],walls:[[11,5,11,7]]},{r:[24,18,33,25],doors:[[28,18],[29,18],[24,21],[24,22]]}],ma=[[20,4,21,8,"#2f5a7a"]],fa=[[30,8]],ba=[[18,20],[19,20],[5,18,6,18]],ga=[[33,2,33,5]],ya=[[9,8],[28,22],[20,15,"p"],[34,6,"p"]],wa=[[4,26,"start"],[13,6,"weapons"],[31,23]],$a={$schema:sa,format:ia,version:ra,id:aa,name:na,desc:oa,size:la,theme:ca,start:da,exfil:ha,roads:ua,buildings:pa,containers:ma,tanks:fa,crates:ba,sandbags:ga,enemies:ya,caches:wa},P=s=>JSON.parse(JSON.stringify(s)),L=s=>Ge(s),ee=s=>s.size??[64,44],va=[$a,ki,Wi,pr,Ir,ta].map(s=>{const e=P(s);return delete e.generate,delete e.$schema,e});function qe(s="new-map",e="NEW MAP"){return{format:"black-site-map",version:1,id:s,name:e,desc:"",size:[40,30],theme:{ground:[78,73,60],road:[44,45,47],style:"grass"},start:[3,26],exfil:[35,4],caches:[[4,26,"start"]]}}function Xt(s){return[Number(s[0]),Number(s[1]),Number(s.length>=4?s[2]:s[0]),Number(s.length>=4?s[3]:s[1])]}const ka=(s,e)=>[Math.min(s[0],e[0]),Math.min(s[1],e[1]),Math.max(s[0],e[0]),Math.max(s[1],e[1])];function de(s,e){if(e.layer==="start"||e.layer==="exfil")return s[e.layer];if(e.layer==="doors"||e.layer==="interiorWalls"){const t=s.buildings?.[e.buildingIndex??-1];return(e.layer==="doors"?t?.doors:t?.walls)?.[e.index]}return s[e.layer]?.[e.index]}function We(s,e){const t=de(s,e);if(t)return Array.isArray(t)?e.layer==="tanks"?[t[0],t[1],t[0]+2,t[1]+2]:Xt(t):t.r}function Aa(s,e){if(!(e.layer==="start"||e.layer==="exfil")){if(e.layer==="doors"||e.layer==="interiorWalls"){const t=s.buildings?.[e.buildingIndex??-1];return e.layer==="doors"?t?.doors:t?.walls}return s[e.layer]}}function Mt(s,e){const t=P(s);return Aa(t,e)?.splice(e.index,1),t}function Sa(s,e,t,i){const r=P(s),a=de(r,e);if(!a)return r;const n=l=>{l[0]+=t,l[1]+=i,l.length>=4&&(l[2]+=t,l[3]+=i)};Array.isArray(a)?n(a):(n(a.r),a.doors?.forEach(n),a.walls?.forEach(n));const o=L(r);if(o.length)throw new Error(o.join(`
`));return r}function Ea(s,e,t,i=t,r={}){const a=P(s),n=ka(t,i);let o=n;const l={layer:e,index:0};if(e==="start"||e==="exfil")a[e]=[...t];else if(e==="doors"||e==="interiorWalls"){const p=r.buildingIndex??a.buildings?.findIndex(u=>t[0]>=u.r[0]&&t[0]<=u.r[2]&&t[1]>=u.r[1]&&t[1]<=u.r[3]),h=a.buildings?.[p??-1];if(!h)throw new Error("Select a building first.");l.buildingIndex=p;const d=e==="doors"?h.doors??=[]:h.walls??=[];l.index=d.length,d.push(e==="doors"?[...t]:n)}else{if(e==="buildings"){if(n[2]-n[0]<2||n[3]-n[1]<2)throw new Error("Buildings must be at least 3 × 3 tiles.");o={r:n,doors:[[n[0]+1,n[3]]],walls:[]}}else e==="containers"?o=[...n,r.color??"#2f5a7a"]:e==="tanks"?o=[...t]:e==="enemies"?o=r.patrol?[...t,"p"]:[...t]:e==="caches"&&(o=r.cacheKind?[...t,r.cacheKind]:[...t]);const p=a[e]??=[];l.index=p.length,p.push(o)}const c=L(a);if(c.length)throw new Error(c.join(`
`));return{map:a,selection:l}}function Ct(s,e,t,i){const r=i?[i]:["start","exfil","enemies","caches","sandbags","wrecks","crates","pipes","tanks","containers","walls","doors","interiorWalls","buildings","roads","paved","water"];for(const a of r){if(a==="doors"||a==="interiorWalls"){for(let o=(s.buildings?.length??0)-1;o>=0;o--){const l=a==="doors"?s.buildings[o].doors:s.buildings[o].walls;for(let c=(l?.length??0)-1;c>=0;c--){const p={layer:a,index:c,buildingIndex:o},h=We(s,p);if(e>=h[0]&&e<=h[2]&&t>=h[1]&&t<=h[3])return p}}continue}const n=a==="start"||a==="exfil"?1:s[a]?.length??0;for(let o=n-1;o>=0;o--){const l={layer:a,index:o},c=We(s,l);if(e>=c[0]&&e<=c[2]&&t>=c[1]&&t<=c[3])return l}}}const Ye="#wKopcvhbT";function at(s){const[e,t]=ee(s),i=Array.from({length:t},()=>Array(e).fill(".")),r=(o,l)=>{const c=Xt(o);for(let p=c[1];p<=c[3];p++)for(let h=c[0];h<=c[2];h++)i[p]?.[h]!==void 0&&(i[p][h]=l)};for(const[o,l]of[["water","w"],["paved","="],["roads","="]])s[o]?.forEach(c=>r(c,l));for(const o of s.buildings??[])r(o.r,"#"),r([o.r[0]+1,o.r[1]+1,o.r[2]-1,o.r[3]-1],","),o.walls?.forEach(l=>r(l,"#")),o.doors?.forEach(l=>r(l,","));s.walls?.forEach(o=>r(o,"#")),r([0,0,e-1,0],"#"),r([0,t-1,e-1,t-1],"#"),r([0,0,0,t-1],"#"),r([e-1,0,e-1,t-1],"#");const a=i.map(o=>[...o]);s.containers?.forEach(o=>r(o,"K")),s.tanks?.forEach(([o,l])=>r([o,l,o+2,l+2],"o"));for(const[o,l]of[["pipes","p"],["crates","c"],["wrecks","v"],["sandbags","h"]])s[o]?.forEach(c=>r(c,l));const n=o=>{const l=(c,p)=>i[p]?.[c]!==void 0&&!Ye.includes(i[p][c]);if(l(o[0],o[1]))return[...o];for(let c=1;c<6;c++)for(let p=-c;p<=c;p++)for(let h=-c;h<=c;h++)if(l(o[0]+h,o[1]+p))return[o[0]+h,o[1]+p,...o.slice(2)];return[...o]};return{w:e,h:t,grid:i,floor:a,start:n(s.start),exfil:n(s.exfil),enemies:(s.enemies??[]).map(n),caches:(s.caches??[]).map(n)}}function _t(s){const e=L(s);if(e.length)return e;const t=at(s),i=n=>t.grid[n[1]]?.[n[0]]===void 0||Ye.includes(t.grid[n[1]][n[0]]);for(const[n,o]of[["start",[t.start]],["exfil",[t.exfil]],["enemies",t.enemies],["caches",t.caches]])o.forEach((l,c)=>{i([l[0],l[1]])&&e.push(`${n}${n==="start"||n==="exfil"?"":`[${c}]`}: remains on a blocked tile after game relocation`)});if(e.length)return e;const r=new Set,a=[t.start];for(let n=0;n<a.length;n++){const[o,l]=a[n];if(t.grid[l]?.[o]===void 0||Ye.includes(t.grid[l][o]))continue;const c=l*t.w+o;if(!r.has(c)){if(r.add(c),o===t.exfil[0]&&l===t.exfil[1])return[];for(const[p,h]of[[1,0],[-1,0],[0,1],[0,-1]])a.push([o+p,l+h])}}return["exfil: no walkable route from insertion to extraction"]}function xa(s,e,t){const i=P(s);i.size=[e,t];const r=L(i);if(r.length)throw new Error(`Resize would clip authored objects. Move or delete them first.
${r.join(`
`)}`);return i}class Rt{constructor(e){this.current=e,this.current=P(e)}past=[];future=[];get canUndo(){return this.past.length>0}get canRedo(){return this.future.length>0}commit(e){JSON.stringify(e)!==JSON.stringify(this.current)&&(this.past.push(P(this.current)),this.past.length>100&&this.past.shift(),this.current=P(e),this.future=[])}undo(){const e=this.past.pop();return e&&(this.future.push(this.current),this.current=e),this.current}redo(){const e=this.future.pop();return e&&(this.past.push(this.current),this.current=e),this.current}}function Q(s,e){const t=s.querySelector(`[data-field="${e}"]`);if(!t)throw new Error("Choose an object to edit first.");return t}function D(s,e){const t=Q(s,e);if(!t.checkValidity()||!t.value.trim()||!Number.isFinite(t.valueAsNumber))throw new Error(`Check ${t.labels?.[0]?.textContent?.trim()??"the selected value"}.`);return t.valueAsNumber}const K=({key:s,label:e,min:t=0,max:i=255,step:r="1"},a)=>b`<label
        >${e}<input
            data-field=${s}
            type="number"
            min=${t}
            max=${i}
            step=${r}
            .value=${x(a)}
    /></label>`,Qt=(s,e,t)=>b`<label
        >${e}<input data-field=${s} type="checkbox" .checked=${x(t)}
    /></label>`,Ma=["containers","water","paved","roads","walls","pipes","crates","wrecks","sandbags","interiorWalls"];function Ca(s,e){if(!e)return k;const t=de(s,e);if(!t)return k;const i=Array.isArray(t)?t:t.r,r=!Array.isArray(t)||Ma.includes(e.layer),[a,n]=ee(s),o=e.layer==="buildings"?3:1,l=(c,p)=>String(i.length>=4?Number(i[p])-Number(i[c])+1:1);return b`<div class="row">
            ${K({key:"x",label:"Across",max:a-1},String(i[0]))}
            ${K({key:"y",label:"Down",max:n-1},String(i[1]))}
        </div>
        ${r?b`<div class="row">
                      ${K({key:"width",label:"Width (tiles)",min:o,max:a},l(0,2))}
                      ${K({key:"height",label:"Height (tiles)",min:o,max:n},l(1,3))}
                  </div>`:k}
        ${e.layer==="containers"?b`<label
                      >Container color<input
                          data-field="color"
                          type="color"
                          .value=${x(String(i[4]))}
                  /></label>`:k}
        ${e.layer==="enemies"?Qt("patrol","Patrolling guard",i[2]==="p"):k}
        ${e.layer==="caches"?b`<label
                      >Supplies<select data-field="cache" .value=${x(String(i[2]??""))}>
                          <option value="">Standard supplies</option>
                          <option value="start">Starting equipment</option>
                          <option value="weapons">Weapons</option>
                      </select></label
                  >`:k}
        ${Array.isArray(t)?k:b`<p>
                      ${t.doors?.length??0} doors · ${t.walls?.length??0} interior
                      walls. Moving the building moves these with it.
                  </p>`}`}function _a(s,e,t){const i=P(e),r=de(i,t);if(!r)throw new Error("Choose an object to edit first.");const a=Array.isArray(r)?r:r.r,n=D(s,"x"),o=D(s,"y"),l=n-Number(a[0]),c=o-Number(a[1]),p=s.querySelector('[data-field="width"]');if(Array.isArray(r)){let h=p?[n,o,n+D(s,"width")-1,o+D(s,"height")-1]:[n,o];p&&a.length===2&&h[0]===h[2]&&h[1]===h[3]&&(h=[n,o]),t.layer==="containers"&&h.push(Q(s,"color").value),t.layer==="enemies"&&Q(s,"patrol").checked&&h.push("p"),t.layer==="caches"&&Q(s,"cache").value&&h.push(Q(s,"cache").value),a.splice(0,a.length,...h)}else{r.r=[n,o,n+D(s,"width")-1,o+D(s,"height")-1];const h=d=>{d[0]+=l,d[1]+=c,d.length===4&&(d[2]+=l,d[3]+=c)};r.doors?.forEach(h),r.walls?.forEach(h)}return i}const es=[["b","Barrels"],["c","Crates"],["T","Trees"],["h","Sandbags"]];function Ra(s){const e=s.theme.clutter??["b","c"];return b`<p>
            These additions appear when the map is played. The editor shows the layout you placed.
        </p>
        ${Qt("expand","Add more open ground",s.generate?.expand??!1)}
        ${K({key:"buildings",label:"Extra buildings (up to)",max:64},String(s.generate?.buildings??0))}
        ${K({key:"cover",label:"Extra cover groups (up to)",max:400},String(s.generate?.cover??0))}
        <p>Cover mix · higher numbers make an item more common</p>
        ${es.map(([t,i])=>K({key:`cover-${t}`,label:i,max:1e3},String(e.filter(r=>r===t).length)))}
        ${K({key:"enemies",label:"Extra guards (%)",max:200,step:"any"},String((s.spawns?.extraEnemies??0)*100))}
        ${K({key:"caches",label:"Extra indoor supplies",max:20},String(s.spawns?.extraCaches??0))}
        <label
            >Layout variation (optional)<input
                data-field="seed"
                type="number"
                min="0"
                max="4294967295"
                step="1"
                placeholder="Automatic"
                .value=${x(s.generate?.seed===void 0?"":String(s.generate.seed))}
        /></label>`}function Oa(s,e){const t=P(e),i={...t.generate,expand:Q(s,"expand").checked,buildings:D(s,"buildings"),cover:D(s,"cover")};Q(s,"seed").value.trim()?i.seed=D(s,"seed"):delete i.seed,t.generate=i,t.spawns={...t.spawns,extraEnemies:D(s,"enemies")/100,extraCaches:D(s,"caches")};const a=[];for(const[n]of es)for(let o=D(s,`cover-${n}`);o>0;o--)a.push(n);if(!a.length)throw new Error("Choose at least one kind of extra cover.");return t.theme.clutter=a,t}function Se(s){const e=s instanceof Error?s.message:String(s);return/no walkable route/i.test(e)?"Clear a path from insertion to extraction.":/blocked tile/i.test(e)?"Move insertion, extraction, guards or caches onto open ground.":/Resize would clip/i.test(e)?"Move objects inside the new map size before resizing.":/door.*(perimeter|edge)|perimeter.*door/i.test(e)?"Place doors on the outside edge of the building.":/hash|sha256|bytes differ|size mismatch|integrity/i.test(e)?"The map file could not be verified. Try opening it again.":/map ID|published map keeps its ID/i.test(e)?"This map cannot be shared as a new map. Choose New or Remix to make a separate version.":/^name:/.test(e)?"Give your map a name of up to 32 characters.":/^desc:/.test(e)?"Keep the briefing within 200 characters.":/^size:/.test(e)?"Map width and height must each be between 16 and 256 tiles.":/outside the|corners must be ordered/i.test(e)?"Keep the selected object inside the map and check its size.":/^(?:start|exfil|theme|id|water|paved|roads|walls|buildings|containers|tanks|pipes|crates|wrecks|sandbags|enemies|caches)[.:]/.test(e)?"Check the map settings and object positions before continuing.":/^(?:map|format|version)[.:]|\b(schema|JSON)\b|\[\d+\]|must be (an? |one of)|unknown (field|property)/i.test(e)?"Some map settings are not valid. Check positions and sizes, or choose another map file.":/intent|convention|routing|payload|relay hint/i.test(e)?"This map request could not be opened. Try opening it from Maps.":/OUTBOX|RESOURCE|NAP-|35858|Blossom|application\/json|upload rail|domain/i.test(e)?"This service could not complete the request. Your map is still open; save a draft and try again.":/\b(seed|clutter|extraEnemies|extraCaches|generate|spawns)\b/i.test(e)?"Check the extra scenery and enemy settings. Use whole numbers within the limits shown.":/public key|hex/i.test(e)?"Choose a player from Community maps.":e}const q=new Map,ts=()=>zt.getPublicKey();let H=0;const Ta=s=>zt.onChanged(e=>{H++,s(e)});async function ss(s){const e=await crypto.subtle.digest("SHA-256",new Uint8Array(s));return Array.from(new Uint8Array(e),t=>t.toString(16).padStart(2,"0")).join("")}function Pe(s,e=[]){const t=Ze(s);if(!t.ok)throw new Error(t.errors.join(`
`));return{event:s,metadata:t.map,relayHints:e}}async function Pa(s={}){const e=Math.min(100,Math.max(1,s.limit??50)),t=await U.query([{kinds:[35858],...s.author?{authors:[s.author]}:s.authors?{authors:s.authors}:{},...s.until!==void 0?{until:s.until}:{},limit:e}],{...s.author?{authors:[s.author]}:s.authors?{authors:s.authors}:{},...s.relays?{relays:s.relays}:{},limit:e,timeoutMs:8e3}),i=new Map;for(const o of t.events){const l=o.event.tags.find(h=>h[0]==="d")?.[1],c=`${o.event.pubkey}:${l}`,p=i.get(c);(!p||o.event.created_at>p.event.created_at||o.event.created_at===p.event.created_at&&o.event.id<p.event.id)&&i.set(c,o)}const r=[];let a=0;for(const o of i.values())try{r.push(Pe(o.event,o.sidecar?.relayHints??[]))}catch{a++}r.sort((o,l)=>l.event.created_at-o.event.created_at||o.event.id.localeCompare(l.event.id));const n=t.events.length>=e?Math.min(...t.events.map(o=>o.event.created_at)):void 0;return{maps:r,invalid:a,incomplete:!!t.incomplete,error:t.error,nextUntil:n,paginationBlocked:n!==void 0&&(n===s.until||t.events.every(o=>o.event.created_at===n))}}async function Je(s){const i=[...(await U.query([{kinds:[10063],authors:[s.event.pubkey],limit:1}],{authors:[s.event.pubkey],timeoutMs:5e3}).catch(()=>({events:[]}))).events].sort((a,n)=>n.event.created_at-a.event.created_at)[0]?.event?.tags.filter(a=>a[0]==="server").map(a=>a[1])??[];let r=new Error("Map bytes are unavailable.");for(const a of Ss(s.metadata,i))try{const n=await Ve.bytes(a.replace(/^blossom:([0-9a-f]{64})$/,"blossom:sha256:$1"));if(n.size>V)throw new Error("Map exceeds the 256 KiB limit.");const o=await Es(new Uint8Array(await n.arrayBuffer()),s.metadata,ss);if(!o.ok)throw new Error(o.errors.join(`
`));return o.map}catch(n){r=n;const o=n.code;if(!["not-found","network-error","timeout","unsupported-scheme"].includes(o??""))throw n}throw r}async function J(s){const e=await ts();if(!e)throw new Error("Sign in with your Nostr identity in the host to publish maps.");if(s&&e!==s)throw new Error("Your identity changed. Save this draft and retry with its owner.");return e}async function Fe(s,e){if(s.event.pubkey!==e)throw new Error("Only the author can update or delete a map. Use Remix to create your own copy.");const t=await U.query([{kinds:[35858],authors:[e],"#d":[s.metadata.id],limit:1}],{authors:[e],relays:s.relayHints,timeoutMs:8e3});if(t.incomplete||t.error)throw new Error("Current revision could not be confirmed. Retry before overwriting this map.");const i=[...t.events].sort((r,a)=>a.event.created_at-r.event.created_at||r.event.id.localeCompare(a.event.id))[0]?.event;if(!i||i.id!==s.event.id)throw new Error("This map changed since you opened it. Reload it or remix your draft before publishing.")}async function Ia(s,e,t){let i=s;const r=Date.now()+18e4;for(;i.status==="uploading"||i.status==="pending";){if(Date.now()>r)throw new Error("Upload is still pending. Keep this draft and retry.");e?.("Waiting for sharing approval…"),await new Promise(a=>setTimeout(a,500)),i=await Ne.status(i.uploadId),t?.(i)}if(i.ok===!1||i.status!=="complete")throw new Error(i.error??`Upload ${i.status}.`);return i}async function ja(s,e={}){e.onStatus?.("Checking your map and account…");const t=L(s);if(t.length)throw new Error(t.join(`
`));const i=await J(),r=H;if(e.base){if(s.id!==e.base.metadata.id)throw new Error("An update must keep the published map ID. Remix to create another map.");e.onStatus?.("Checking the published revision…"),await Fe(e.base,i)}else{e.onStatus?.("Preparing your map address…");let h;try{h=await U.query([{kinds:[35858],authors:[i],"#d":[s.id],limit:1}],{authors:[i],timeoutMs:8e3})}catch{}if(h?.events.some(({event:d})=>d.kind===35858&&d.pubkey===i&&d.tags.find(u=>u[0]==="d")?.[1]===s.id))throw new Error("You already published a map at this address under your account. Open it from My maps to update it, or create a remix.")}const a=new TextEncoder().encode(JSON.stringify(s));if(a.length>V)throw new Error("Map exceeds the 256 KiB limit.");const n=await ss(a),o=`${i}:${n}`;let l=q.get(o);if(!l){if(await J(i),r!==H)throw new Error("Identity changed while preparing the upload. Keep the draft and retry.");e.onStatus?.("Preparing your map…");const h=await Ne.info();if(!h.rails.some(d=>d.rail==="blossom"&&d.enabled))throw new Error("This host has no enabled Blossom upload rail. Keep your draft and use a compatible host.");if(h.maxBytes!==void 0&&a.length>h.maxBytes)throw new Error(`This host limits uploads to ${h.maxBytes} bytes. Your map draft is retained.`);if(h.mimeTypes?.length&&!h.mimeTypes.includes("application/json")&&!h.mimeTypes.includes("application/*")&&!h.mimeTypes.includes("*/*"))throw new Error("This host does not permit application/json uploads. Your map draft is retained.");if(await J(i),r!==H)throw new Error("Identity changed during upload preflight. Keep your draft and retry.");e.onStatus?.("Uploading your map…"),l=await Ne.upload({rail:"blossom",data:new Blob([a],{type:"application/json"}),mimeType:"application/json",filename:`${s.id}.json`,noTransform:!0}),q.set(o,l)}try{if(l=await Ia(l,e.onStatus,h=>q.set(o,h)),l.sha256!==n||l.size!==a.length)throw q.delete(o),new Error("Uploaded bytes differ from the exact map JSON. Publication stopped.");q.set(o,l)}catch(h){throw["failed","cancelled"].includes(q.get(o)?.status??"")&&q.delete(o),h}if(await J(i),r!==H)throw new Error("Identity changed while publishing. Keep this draft and retry.");e.base&&await Fe(e.base,i);const c=As(s,{...l,sha256:n,size:a.length},e);if(e.base&&!e.remixOf){const h=new Set(["A","E","K","P","a","e","k","p"]);c.tags.push(...e.base.event.tags.filter(d=>h.has(d[0])).map(d=>[...d]))}if(e.base&&(c.created_at=Math.max(c.created_at,e.base.event.created_at+1)),e.onStatus?.("Sharing your map…"),await J(i),r!==H)throw new Error("Identity changed while publishing. Keep this draft and retry.");let p;try{p=await U.publish(c)}catch(h){throw Object.assign(h instanceof Error?h:new Error(String(h)),{upload:l,template:c})}if(await J(i),r!==H)throw new Error("Identity changed during publication. Reload your maps to confirm the result.");if(!p.ok||!p.event)throw Object.assign(new Error(`${p.error??"The host could not publish kind 35858 maps."} Your draft and verified Blossom upload are retained for Retry.`),{upload:l,template:c});if(p.event.pubkey!==i)throw new Error("The host returned a publication from another identity.");return q.delete(o),Pe(p.event,Object.keys(p.relays??{}).filter(h=>p.relays?.[h]))}async function Ua(s){const e=H,t=await J(s.event.pubkey);if(await Fe(s,t),await J(t),e!==H)throw new Error("Identity changed while preparing deletion. Retry with the map owner.");const i=await U.publish({kind:5,content:"Removed by the map author.",tags:[["a",s.metadata.address],["e",s.event.id],["k","35858"]],created_at:Math.max(Math.floor(Date.now()/1e3),s.event.created_at+1)});if(!i.ok)throw new Error(i.error??"The host does not permit map deletion.")}const is=s=>`black-site-editor:draft:${s||"guest"}`;async function Ue(s,e){await Re.setItem(is(s),JSON.stringify(e))}async function Da(s){const e=await Re.getItem(is(s));if(!e)return null;let t;try{t=JSON.parse(e)}catch{throw new Error("Stored draft is malformed JSON.")}if(!t||typeof t!="object"||L(t.map).length)throw new Error("Stored draft has invalid map data.");if(t.base){const i=t.base;if(!i.event||!Array.isArray(i.event.tags)||i.event.pubkey!==s||!/^[0-9a-f]{64}$/.test(i.event.id)||!Array.isArray(i.relayHints)||!i.relayHints.every(a=>typeof a=="string"))throw new Error("Stored draft has an invalid published base or owner.");const r=Pe(i.event,i.relayHints);if(r.metadata.id!==t.map.id)throw new Error("Stored draft ID differs from its published base.");t.base=r}if(t.remixOf&&(!t.remixOf.tags||!Ze(t.remixOf).ok))throw new Error("Stored draft has invalid remix attribution.");if(t.content!==void 0&&typeof t.content!="string"||t.image!==void 0&&typeof t.image!="string"||t.remixRelay!==void 0&&typeof t.remixRelay!="string"||t.hashtags!==void 0&&(!Array.isArray(t.hashtags)||!t.hashtags.every(i=>typeof i=="string"))||t.savedAt!==void 0&&(!Number.isFinite(t.savedAt)||t.savedAt<0))throw new Error("Stored draft has invalid metadata.");return t}const rs="black-site-editor:tutorial-seen:v1";async function Na(){return!!await Re.getItem(rs)}async function La(){await Re.setItem(rs,"1")}async function as(s){if(!/^[0-9a-f]{64}$/.test(s.author)||!/^[a-z0-9][a-z0-9-]{0,31}$/.test(s.id))throw new Error("Invalid map address.");const e=await U.query([{kinds:[35858],authors:[s.author],"#d":[s.id],limit:10}],{authors:[s.author],relays:s.relays,timeoutMs:8e3});if(e.error||e.incomplete)throw new Error("Could not confirm the requested map from its author. Retry after relay discovery completes.");const t=e.events.filter(i=>i.event.kind===35858&&i.event.pubkey===s.author&&i.event.tags.some(r=>r[0]==="d"&&r[1]===s.id));if(t.sort((i,r)=>r.event.created_at-i.event.created_at||i.event.id.localeCompare(r.event.id)),!t.length)throw new Error("The requested map was not found. It may have been deleted or its relays may be unavailable.");return Pe(t[0].event,t[0].sidecar?.relayHints??s.relays??[])}class Ba{#e;#t=new Set;constructor(e){this.#e=e}get state(){return this.#e}set(e){this.#e={...this.#e,...e};for(const t of this.#t)t(this.#e)}subscribe(e){return this.#t.add(e),()=>this.#t.delete(e)}}function za(s,e){return Object.is(s,e)?!0:!Array.isArray(s)||!Array.isArray(e)||s.length!==e.length?!1:s.every((t,i)=>Object.is(t,e[i]))}class Ha{constructor(e,t,i){this.host=e,this.store=t,this.select=i,this.value=i(t.state),e.addController(this)}value;#e;hostConnected(){this.value=this.select(this.store.state),this.#e=this.store.subscribe(e=>{const t=this.select(e);za(t,this.value)||(this.value=t,this.host.requestUpdate())})}hostDisconnected(){this.#e?.()}}function Ka(){return{map:qe(`map-${Date.now().toString(36)}`),canUndo:!1,canRedo:!1,tool:"select",placeOptions:{color:"#2f5a7a",patrol:!1,cacheKind:""},viewer:"",identityLabel:"Checking account…",content:"",hashtags:[],dirty:!1,busy:!1,showGrid:!0,workspaceTab:"map",status:{message:"Ready. Create a map or open Maps to browse published maps.",kind:""},validation:"Check the walkable route before publishing.",publication:{phase:"idle",message:"",stage:0,warnings:[]},publishDetails:"",exportFallback:"",library:{view:"none",records:[],status:"",loadMore:!1},shortcutsRev:0}}const g=new Ba(Ka()),nt=[["select","Select / move"],["pan","Pan map"],["water","Water"],["paved","Pavement"],["roads","Road"],["walls","Wall"],["buildings","Building"],["doors","Building door"],["interiorWalls","Interior wall"],["containers","Container"],["tanks","Tank (3×3)"],["pipes","Pipe"],["crates","Crate"],["wrecks","Wreck"],["sandbags","Sandbag"],["start","Insertion"],["exfil","Extraction"],["enemies","Guard"],["caches","Loot cache"],["erase","Erase object"]],qa=["select","pan","erase"],Wa=[["Terrain",["water","paved","roads"]],["Structures",["walls","buildings","doors","interiorWalls"]],["Cover & supplies",["containers","tanks","pipes","crates","wrecks","sandbags"]],["Mission",["start","exfil","enemies","caches"]]],Ot=nt.filter(([s])=>!["select","pan","erase","doors","interiorWalls"].includes(s)),Ya={select:"V",pan:"H",erase:"E",water:"W",paved:"A",roads:"R",walls:"L",buildings:"B",doors:"D",interiorWalls:"I",containers:"C",tanks:"T",pipes:"P",crates:"K",wrecks:"Q",sandbags:"S",start:"1",exfil:"2",enemies:"3",caches:"4"},le=s=>nt.find(([e])=>e===s)?.[1]??"Object",ns=s=>`tool.${s.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}`,Tt=s=>[1,3,5].map(e=>parseInt(s.slice(e,e+2),16)),Ee=s=>`${s.slice(0,19)}-${Date.now().toString(36)}`;class Ja{ui={};shortcuts={states:()=>[],get:()=>{},close(){}};#e=new Rt(g.state.map);#t=0;#s=0;#i=0;#r;#l;#a=!1;#n;#o;#d=!1;#h=0;get state(){return g.state}get map(){return this.#e.current}get workspaceVersion(){return this.#i}get intentPending(){return!!this.#o||this.#d}get anyDialogOpen(){return[...document.querySelectorAll("dialog")].some(e=>e.open)}#u(e={}){const t=this.#e.current;let i="selection"in e?e.selection:g.state.selection;i&&!de(t,i)&&(i=void 0),g.set({...e,map:t,canUndo:this.#e.canUndo,canRedo:this.#e.canRedo,selection:i})}status(e,t=""){g.set({status:{message:e,kind:t}})}fail=e=>this.status(Se(e),"error");guard=e=>()=>{try{const t=e();t instanceof Promise&&t.catch(this.fail)}catch(t){this.fail(t)}};bumpShortcuts(){g.set({shortcutsRev:g.state.shortcutsRev+1})}confirmAction(e,t="Confirm"){return this.ui.confirm?this.ui.confirm.ask(e,t):Promise.resolve(!1)}async replaceAllowed(){return!g.state.dirty||await this.confirmAction("Replace your unsaved map? Save a draft or download a backup first to keep it.","Replace map")}setMap(e,t={},i=!1){if(g.state.busy)throw new Error("Wait for the current publication to finish.");this.#i++,this.#s++,this.#e=new Rt(e),this.#r=void 0,this.#u({selection:void 0,base:t.base,remixOf:t.remixOf,remixRelay:t.remixRelay,content:t.content??"",hashtags:t.hashtags??[],image:t.image,dirty:i,publishDetails:""}),this.ui.canvas?.fit()}commit(e,t={}){const i=L(e);if(i.length)throw new Error(i.join(`
`));this.#e.commit(e),this.#i++,this.#u({...t,dirty:!0})}undo(){g.state.busy||(this.#e.undo(),this.#i++,this.#u({dirty:!0,selection:void 0}))}redo(){g.state.busy||(this.#e.redo(),this.#i++,this.#u({dirty:!0,selection:void 0}))}updateMetadata(e){const t=P(this.map);t.id=e.id,t.name=e.name,t.desc=e.desc,t.theme.style=e.style,t.theme.ground=Tt(e.ground),t.theme.road=Tt(e.road),t.theme.markings=e.markings,this.commit(t)}resize(e,t){this.commit(xa(this.map,e,t)),this.ui.canvas?.fit()}setContent(e){this.#i++,g.set({content:e,dirty:!0})}setHashtags(e){const t=e.split(",").map(i=>i.trim().toLowerCase()).filter(Boolean);this.#i++,g.set({hashtags:t,dirty:!0})}applyDeployment(e){this.commit(Oa(e,this.map)),this.status("Extra scenery and enemies updated.")}applyJson(e){const t=JSON.parse(e),i=L(t);if(i.length)throw new Error(i.join(`
`));const r=g.state.base;if(r&&t.id!==r.metadata.id)throw new Error("A published map keeps its ID. Use New or Remix to publish a separate map.");this.commit(t,{selection:void 0}),this.ui.canvas?.fit(),this.status("Map JSON applied.")}validate(){const e=_t(this.map);g.set({validation:e.length?e.map(Se).join(`
`):"Ready to play · insertion can reach extraction."}),this.status(e.length?"Map needs attention. See Map Check.":"Your map is ready to play.",e.length?"error":"success")}setTool(e){g.set({tool:e}),this.status(`${le(e)} tool ready.`)}setPlaceOptions(e){g.set({placeOptions:{...g.state.placeOptions,...e}})}toggleGrid(){g.set({showGrid:!g.state.showGrid})}setWorkspaceTab(e){g.set({workspaceTab:e})}select(e){g.set({selection:e})}selectObject(e){this.setTool("select"),this.select(e)}selectAt(e){const t=Ct(this.map,...e);return this.select(t),t}moveSelectionBy(e,t,i=g.state.selection){i&&this.commit(Sa(this.map,i,e,t))}nudge(e,t){if(!g.state.selection){this.status("Select an object first, then use the arrow keys to move it.");return}this.moveSelectionBy(e,t)}eraseAt(e){const t=Ct(this.map,...e);t&&!["start","exfil"].includes(t.layer)&&this.commit(Mt(this.map,t),{selection:void 0})}placeRect(e,t,i){const{selection:r,placeOptions:a}=g.state,n=Ea(this.map,e,t,i,{color:a.color,patrol:a.patrol,cacheKind:a.cacheKind||void 0,buildingIndex:r?.layer==="buildings"?r.index:r?.buildingIndex});this.commit(n.map,{selection:n.selection}),this.status(`Placed ${le(e).toLowerCase()}.`)}applyObject(e){const t=g.state.selection;t&&(this.commit(_a(e,this.map,t)),this.status("Object updated."))}deleteSelected(){const e=g.state.selection;e&&!["start","exfil"].includes(e.layer)&&this.commit(Mt(this.map,e),{selection:void 0})}download(e,t){const i=new Blob([e],{type:"application/json"}),r=URL.createObjectURL(i),a=document.createElement("a");a.href=r,a.download=t,a.click(),setTimeout(()=>URL.revokeObjectURL(r),1e3),g.set({exportFallback:"If the download does not appear, use Save draft to keep a backup."})}exportJson(){this.download(JSON.stringify(this.map,null,2),`${this.map.id}.json`)}exportEvent(){const{base:e,remixOf:t,remixRelay:i,content:r,hashtags:a}=g.state;this.download(JSON.stringify(this.#r??{map:this.map,base:e?.event,remixOf:t,remixRelay:i,content:r,hashtags:a},null,2),`${this.map.id}-publication.json`)}async importFile(e){if(e.size>262144)throw new Error("This map file is too large. Choose a smaller map file.");const t=JSON.parse(await e.text()),i=L(t);if(i.length)throw new Error(i.join(`
`));await this.replaceAllowed()&&(this.setMap(t,{},!0),this.status("Imported a local copy. Publishing creates a map under your identity."))}async newMap(){g.state.busy||!await this.replaceAllowed()||(this.setMap(qe(`map-${Date.now().toString(36)}`)),this.status("New map. Place insertion, extraction, terrain and units."))}async remixCurrent(){const{busy:e,viewer:t,base:i}=g.state;if(e||!t||!i)return;const r=this.#t;if(!await this.confirmAction("Create a new map from this one and keep the original credits?","Remix current map")||g.state.busy||r!==this.#t)return;const a=P(this.map);a.id=Ee(a.id);const{content:n,hashtags:o,image:l}=g.state;this.setMap(a,{remixOf:i.event,remixRelay:i.relayHints[0],content:n,hashtags:o,image:l},!0),this.status("Remix ready under your current identity. Original and parent credits retained.")}#c(e){g.set({library:{...g.state.library,...e}})}async openLibrary(){g.state.busy||(this.ui.library?.show(),g.state.viewer?await this.browse(g.state.viewer):this.showTemplates())}showTemplates(){g.state.busy||(this.#s++,this.#c({view:"templates",loadMore:!1,status:"Built-in Black Site maps · exact map tiles, procedural generation removed. Use as a local starting point."}))}async useTemplate(e){if(g.state.busy||!await this.replaceAllowed())return;const t=P(e);t.id=Ee(t.id),this.setMap(t,{content:`Based on Black Site built-in ${e.name}.`},!0),this.ui.library?.close(),this.status("Template ready. Author attribution is included in publication notes.")}searchAuthor(e){const t=e.trim().toLowerCase();if(!/^[0-9a-f]{64}$/.test(t))throw new Error("Enter a 64-character hex Nostr public key.");return this.browse(t)}loadMore(){return this.browse(this.#l,!0,this.#a)}async browse(e,t=!1,i=!1){if(e===""){this.#c({status:"Connect your Nostr identity in the host to load your maps."});return}if(!T("outbox"))throw new Error("Map browsing is unavailable here. Try another host.");const r=++this.#s,a=this.#t;this.#l=e,this.#a=i,this.#c({status:"Finding maps…",...t?{}:{view:"records",records:[],loadMore:!1}});try{let n;if(i){if(!g.state.viewer)throw new Error("Sign in to browse maps from people you follow.");if(n=await xs(g.state.viewer),r!==this.#s||a!==this.#t)return;if(!n.length){this.#c({view:"records",records:[],loadMore:!1,status:"You are not following anyone yet. Follow creators in your Nostr client, or explore Global maps."});return}}const o=await Pa({author:e,authors:n,until:t?this.#n:void 0});if(r!==this.#s||a!==this.#t)return;const l=g.state.library.records,c=t?[...l,...o.maps.filter(p=>!l.some(h=>h.metadata.address===p.metadata.address))]:o.maps;this.#n=o.nextUntil,this.#c({view:"records",records:c,loadMore:this.#n!==void 0&&!o.paginationBlocked,status:`${c.length} maps${o.invalid?" · some maps could not be opened":""}${o.paginationBlocked?" · more maps may be available; try again later":""}${o.incomplete?" · some results are missing; try again":""}${o.error?" · maps could not be loaded; try again":""}${!c.length&&!o.incomplete&&!o.error?" · no published maps found":""}`})}catch(n){if(r!==this.#s||a!==this.#t)return;this.#c({view:"records",records:[],loadMore:!1,status:n instanceof Error?n.message:"Maps could not be loaded. Please retry."})}}async deleteRecord(e){const t=e.metadata;if(!await this.confirmAction(`Remove ${t.title} from your published maps? Copies shared by others may remain.`,"Delete published map"))return;const i=this.#t;if(await Ua(e),i===this.#t)return this.#c({records:g.state.library.records.filter(r=>r.metadata.address!==t.address)}),this.status("Deletion request published.","success"),!0}async openRecord(e,t){if(g.state.busy||!await this.replaceAllowed())return;if(!T("resource"))throw new Error("Map downloads are unavailable here. Try another host.");const i=this.#t,r=this.#i,a=++this.#s;this.#c({status:"Opening map…"});const n=await Je(e);if(!(i!==this.#t||a!==this.#s||r!==this.#i||g.state.busy)){if(t)n.id=Ee(n.id),this.setMap(n,{remixOf:e.event,remixRelay:e.relayHints[0],content:e.metadata.content,hashtags:e.metadata.hashtags},!0);else{if(e.event.pubkey!==g.state.viewer)throw new Error("Only the author can edit this map. Use Remix.");this.setMap(n,{base:e,content:e.metadata.content,hashtags:e.metadata.hashtags,image:e.metadata.image??void 0})}this.ui.library?.close(),this.status(t?"Remix ready. Parent, original and author credits will be preserved.":"Map opened. Publishing will save your changes to this map.","success")}}#p(){const{base:e,remixOf:t,remixRelay:i,content:r,hashtags:a,image:n}=g.state;return{map:P(this.map),base:e,remixOf:t,remixRelay:i,content:r,hashtags:a,image:n,savedAt:Date.now()}}async saveDraft(){const e=g.state.viewer||"guest",t=this.#t,i=this.#i;await Ue(e,this.#p()),t===this.#t&&(i===this.#i&&g.set({dirty:!1}),this.status("Draft saved.","success"))}async restoreDraft(){if(g.state.busy)return;if(!T("storage"))throw new Error("Draft storage is unavailable in this host.");const e=this.#t,t=await Da(g.state.viewer||"guest");if(e===this.#t){if(!t){this.#c({status:"No saved draft for this identity."});return}await this.replaceAllowed()&&(e!==this.#t||g.state.busy||(this.setMap(t.map,t),this.ui.library?.close(),this.status("Saved draft restored.","success")))}}async publish(){if(g.state.busy)return;g.set({publication:{phase:"idle",message:"",stage:0,warnings:[]}});const e=this.#t,t=r=>g.set({publication:{...g.state.publication,...r}}),i=r=>t({warnings:[...g.state.publication.warnings,r]});try{const r=_t(this.map);if(g.set({validation:r.map(Se).join(`
`)}),r.length)throw new Error("Check your map and clear a path to extraction before publishing.");const{viewer:a,base:n}=g.state;if(!a)throw new Error("Connect your Nostr identity in the host first.");if(!await this.confirmAction(`${n?"Update":"Publish"} ${this.map.name} using your account? Your map and creator credits will be public. You may be asked to approve sharing.`,n?"Update map":"Publish map")||g.state.busy)return;if(g.state.viewer!==a||this.#t!==e)throw new Error("Identity changed during confirmation. Review and confirm again.");const o=this.#p();if(g.set({busy:!0,publication:{phase:"working",message:"Saving a draft backup…",stage:0,warnings:[]}}),this.ui.publication?.show(),T("storage"))try{await Ue(a,o)}catch{i("A draft backup could not be saved. Keep this window open or download your map.")}if(e!==this.#t)throw new Error("Identity changed before publication. Retry under the intended account.");const l=await ja(o.map,{base:o.base,remixOf:o.remixOf,remixRelay:o.remixRelay,content:o.content,hashtags:o.hashtags,image:o.image,onStatus:c=>{if(e!==this.#t)return;const p=/Sharing your map/.test(c)?2:/Uploading|Waiting for sharing/.test(c)?1:0;t({message:c,stage:p}),this.status(c)}});if(e!==this.#t)throw new Error("Your account changed during publication. Open My maps to confirm the result before retrying.");if(this.#r=l.event,g.set({base:l,remixOf:void 0,remixRelay:void 0,dirty:!1,publishDetails:JSON.stringify(l.event,null,2)}),t({phase:"success",message:`${l.metadata.title} published successfully.`,stage:3}),this.status(`${l.metadata.title} published.`,"success"),T("storage"))try{await Ue(a,this.#p())}catch{i("Your map was published, but the local draft backup could not be saved.")}}catch(r){const a=r instanceof Error?r.message:String(r);if(e===this.#t){const n=r;this.#r={upload:n?.upload,template:n?.template,error:a},g.set({publishDetails:JSON.stringify(this.#r,null,2),dirty:!0}),this.fail(r)}t({phase:"error",message:e===this.#t?Se(r):"Your account changed. Open My maps to check whether publication completed before retrying.",error:a}),this.ui.publication?.show()}finally{g.set({busy:!1}),this.#o&&this.processPendingIntent()}}identityChanged=e=>{if(e===g.state.viewer)return;this.#t++,this.#s++,this.#n=void 0;const t=g.state.base;g.set({viewer:e,identityLabel:e?"Signed in":"Sign in to share maps",library:{...g.state.library,view:"none",records:[],loadMore:!1}}),t&&t.event.pubkey!==e?this.status("Identity changed. This published map belongs to another account. Export it or create a remix; updates are disabled.","error"):this.status(e?"Identity connected. Open Maps to load your published maps.":"Connect your Nostr identity in the host to manage published maps. Local authoring is available."),this.#o&&this.processPendingIntent()};get identityEpoch(){return this.#t}setIdentityLabel(e){g.set({identityLabel:e})}tutorialClosed(){T("storage")&&La().catch(()=>{})}confirmClosed(){setTimeout(()=>{this.#o&&this.processPendingIntent()},0)}receiveMapIntent=async e=>{if(this.#o=e,this.#h++,g.state.busy||this.#d){this.status("Another napplet requested a map. It will open after the current map.");return}await this.processPendingIntent()};#m(){this.ui.library?.close(),this.ui.help?.close()}async processPendingIntent(){if(!this.#o||g.state.busy||this.#d||this.ui.confirm?.isOpen||this.ui.publication?.isOpen)return;const e=this.#o;if(e.action!=="new"&&!g.state.viewer){this.status("A napplet requested a published map. Connect your Nostr identity in the host to open it.");return}this.#o=void 0,this.#d=!0;const t=this.#h,i=this.#t,r=this.#i,a=()=>t!==this.#h||i!==this.#t||r!==this.#i||g.state.busy;try{if(!await this.replaceAllowed()){this.status("Map request cancelled. Your workspace is unchanged.");return}if(a())return;if(e.action==="new"){this.setMap(e.map??qe(`map-${Date.now().toString(36)}`),{},!0),this.#m(),this.status("New map opened from another napplet. Review the map before publishing.");return}const n=await as(e.target);if(a())return;let o=e.action==="remix";if(n.event.pubkey!==g.state.viewer&&!o){if(e.action==="edit")throw new Error("This map belongs to another player. Open Maps and choose Remix to make your own version.");if(!await this.confirmAction("This map belongs to another player. Create your own remix with the original author credits?","Open as remix"))return;o=!0}if(a())return;const l=await Je(n);if(a())return;this.#m(),this.setWorkspaceTab("map"),o?(l.id=Ee(l.id),this.setMap(l,{remixOf:n.event,remixRelay:n.relayHints[0],content:n.metadata.content,hashtags:n.metadata.hashtags},!0)):this.setMap(l,{base:n,content:n.metadata.content,hashtags:n.metadata.hashtags,image:n.metadata.image??void 0}),this.status(o?"Your remix is ready. Original creator credits are preserved.":"Your map is open for editing.","success")}catch(n){this.fail(n)}finally{this.#d=!1,this.#o&&this.processPendingIntent()}}}const m=new Ja,M=m.guard,Fa=["napplet:black-site/map-edit","napplet:black-site/map-remix","napplet:black-site/map-new","napplet:black-site/map-open"],Pt=s=>!!s&&typeof s=="object"&&!Array.isArray(s);function It(s,e){let t;try{t=JSON.stringify(s)}catch{throw new Error("Map intent payload must be plain JSON.")}if(t===void 0||new TextEncoder().encode(t).length>e)throw new Error("Map intent payload exceeds its size limit.")}function jt(s,e){if(Object.keys(s).some(t=>!e.includes(t)))throw new Error("Map intent payload contains unsupported fields.")}function Va(s){if(typeof s!="string"||s.length>256)throw new Error("Map intent relay hints must be public WSS URLs.");let e;try{e=new URL(s)}catch{throw new Error("Map intent relay hint is invalid.")}if(e.protocol!=="wss:"||e.username||e.password||e.hash||e.port&&e.port!=="443"||e.hostname==="localhost"||e.hostname.endsWith(".localhost")||e.hostname.endsWith(".local")||e.hostname.startsWith("[")||/^\d+(?:\.\d+){3}$/.test(e.hostname))throw new Error("Map intent relay hints must be public WSS URLs on port 443.");return e.href}function Ga(s){if(!Pt(s))throw new Error("Map intent delivery is invalid.");if(typeof s.convention!="string"||!Fa.includes(s.convention))throw new Error("Unsupported map intent convention.");const e=s.convention.slice(23);if(s.archetype!=="black-site"||s.action!==`map-${e}`)throw new Error("Map intent routing fields do not match its convention.");if(typeof s.sender!="string"||!s.sender||s.sender.length>1024||/[\u0000-\u001f]/.test(s.sender))throw new Error("Map intent source label is invalid.");const t=s.payload===void 0?{}:s.payload;if(!Pt(t))throw new Error("Map intent payload must be an object.");if(It(t,e==="new"?264192:4096),e==="new"){if(jt(t,["map"]),t.map===void 0)return{action:e,sender:s.sender};const a=L(t.map);if(a.length)throw new Error(`Map handoff is invalid:
${a.join(`
`)}`);return It(t.map,262144),{action:e,sender:s.sender,map:JSON.parse(JSON.stringify(t.map))}}if(jt(t,["address","relays"]),typeof t.address!="string")throw new Error("Map intent requires a published map address.");const i=/^35858:([0-9a-f]{64}):([a-z0-9][a-z0-9-]{0,31})$/.exec(t.address);if(!i)throw new Error("Map intent address must be 35858:<author hex>:<map ID>.");if(t.relays!==void 0&&(!Array.isArray(t.relays)||t.relays.length>8))throw new Error("Map intent accepts at most eight relay hints.");const r=[...new Set((t.relays??[]).map(Va))];return{action:e,sender:s.sender,target:{address:t.address,author:i[1],id:i[2],relays:r}}}function Za(s,e){if(!T("intent"))return{close(){}};let t=!0,i;try{i=ys.onDelivery(r=>{if(t)try{Promise.resolve(s(Ga(r))).catch(a=>{t&&e(a)})}catch(a){e(a)}})}catch(r){return e(r),{close(){}}}return{close(){t=!1,i.close()}}}const Xa={esc:"escape",del:"delete",space:" ",spacebar:" ",plus:"+",up:"arrowup",down:"arrowdown",left:"arrowleft",right:"arrowright",return:"enter"},os=s=>s.length===1&&!/[a-z0-9 ]/i.test(s);function ot(s){const e=s.trim().split(/\+(?!$)/),t={alt:!1,ctrl:!1,meta:!1,shift:!1,key:""};for(const[i,r]of e.entries()){const a=r.trim().toLowerCase();if(i===e.length-1){t.key=Xa[a]??a;break}if(a==="alt"||a==="option")t.alt=!0;else if(a==="ctrl"||a==="control")t.ctrl=!0;else if(a==="meta"||a==="cmd"||a==="command")t.meta=!0;else if(a==="shift")t.shift=!0;else return}if(!(!t.key||["alt","control","ctrl","meta","shift"].includes(t.key)))return os(t.key)&&(t.shift=!1),t}function Qa(s){const e=s.key.toLowerCase();return{alt:s.altKey,ctrl:s.ctrlKey,meta:s.metaKey,shift:os(e)?!1:s.shiftKey,key:e}}function en(s,e){const t=ot(s);if(!t)return!1;const i=Qa(e);return(t.ctrl&&!t.meta?(i.ctrl||i.meta)&&!(i.ctrl&&i.meta):i.ctrl===t.ctrl&&i.meta===t.meta)&&i.alt===t.alt&&i.shift===t.shift&&i.key===t.key}const tn={arrowup:"↑",arrowdown:"↓",arrowleft:"←",arrowright:"→"," ":"Space",escape:"Esc",delete:"Del",backspace:"Backspace",enter:"Enter"};function sn(s){const e=ot(s);if(!e)return s;const t=[e.ctrl&&"Ctrl",e.alt&&"Alt",e.meta&&"Meta",e.shift&&"Shift"].filter(Boolean);return t.push(tn[e.key]??(e.key.length===1?e.key.toUpperCase():e.key[0].toUpperCase()+e.key.slice(1))),t.join("+")}function rn(s){const e=ot(s);if(!e)return"";const t=[e.ctrl&&"Control",e.alt&&"Alt",e.meta&&"Meta",e.shift&&"Shift"].filter(Boolean),i={arrowup:"ArrowUp",arrowdown:"ArrowDown",arrowleft:"ArrowLeft",arrowright:"ArrowRight"," ":"Space",delete:"Delete",escape:"Escape",enter:"Enter",backspace:"Backspace"}[e.key];return t.push(i??(e.key.length===1?e.key.toUpperCase():e.key)),t.join("+")}function an(s){if(!s||typeof s.tagName!="string")return!1;const e=s;return e.isContentEditable||e.tagName==="TEXTAREA"||e.tagName==="SELECT"?!0:e.tagName!=="INPUT"?!1:!["button","checkbox","color","file","image","radio","range","reset","submit"].includes(e.type)}function nn(s,e){const t=new Map(s.map(n=>[n.id,{action:n,binding:n.defaultKey,source:"default"}])),i=[];let r=!1;if(T("keys"))for(const n of t.values())n.source="pending",gs.register({id:n.action.id,label:n.action.label,defaultKey:n.action.defaultKey},()=>{r||n.action.run()}).then(o=>{if(r){o.close();return}i.push(o),o.binding?(n.binding=o.binding,n.source="host"):(n.binding=n.action.defaultKey,n.source="default")}).catch(()=>{n.binding=n.action.defaultKey,n.source="default"}).finally(()=>{r||e()});const a=n=>{if(!(n.defaultPrevented||n.isComposing||an(n.target))){for(const o of t.values())if(!(o.source!=="default"||!o.binding||!en(o.binding,n))){n.preventDefault(),o.action.run();return}}};return document.addEventListener("keydown",a),{states:()=>[...t.values()],get:n=>t.get(n),close(){r=!0,document.removeEventListener("keydown",a);for(const n of i.splice(0))n.close()}}}class R extends be{createRenderRoot(){return this}watch(e){return new Ha(this,g,e)}}const ie=new Map;function on(s){const e=ie.get(s);if(e)return e;const t=(async()=>{const i=await U.query([{kinds:[0],authors:[s],limit:1}],{authors:[s],timeoutMs:5e3});if(i.error)throw new Error(i.error);const r=i.events.map(o=>o.event).filter(o=>o.kind===0&&o.pubkey===s).sort((o,l)=>l.created_at-o.created_at||o.id.localeCompare(l.id))[0],a=r?JSON.parse(r.content):{},n=o=>typeof o=="string"?o.trim().slice(0,100):"";return{name:n(a?.display_name)||n(a?.name),picture:typeof a?.picture=="string"?a.picture:void 0}})();return ie.size>=16&&ie.delete(ie.keys().next().value),ie.set(s,t),t.catch(()=>ie.delete(s)),t}class ln extends R{static properties={name:{state:!0},avatar:{state:!0}};#e;#t=0;constructor(){super(),this.name="",this.watch(e=>[e.viewer,e.identityLabel])}updated(){const e=m.state.viewer;if(e===this.#e)return;this.#e=e;const t=++this.#t;this.name="",this.#s(),e&&this.#i(e,t)}disconnectedCallback(){super.disconnectedCallback(),this.#t++,this.#e=void 0,this.#s()}#s(){this.avatar&&URL.revokeObjectURL(this.avatar),this.avatar=void 0}async#i(e,t){try{if(!T("outbox"))return;const i=await on(e);if(t!==this.#t||!this.isConnected||(this.name=i.name,!i.picture||!T("resource")))return;const r=await Ve.bytes(i.picture);if(t!==this.#t||!this.isConnected||r.size>5*1024*1024||!["image/png","image/jpeg","image/webp","image/gif","image/avif"].includes(r.type))return;this.avatar=URL.createObjectURL(r)}catch{}}render(){const{viewer:e,identityLabel:t}=m.state;if(!e)return b`<span class="account-status">${t}</span>`;const i=e===this.#e,r=i&&this.name||`${e.slice(0,8)}…${e.slice(-4)}`,a=i&&this.name?this.name.split(/\s+/).slice(0,2).map(n=>Array.from(n)[0]).join("").toUpperCase():"?";return b`<span class="account-profile" title=${`Signed in as ${r}`}>
            <span class="account-avatar" aria-hidden="true"
                >${i&&this.avatar?b`<img src=${this.avatar} alt="" @error=${()=>this.#s()} />`:a}</span
            >
            <span class="account-name">${r}</span>
        </span>`}}customElements.define("bs-account",ln);function ls(s){const e=m.shortcuts.get(s);return e?.binding&&e.source!=="pending"?sn(e.binding):""}class cn extends rt{constructor(e){if(super(e),e.type!==Y.ELEMENT)throw new Error("keyHint belongs on an element.")}render(e,t){return j}update(e,[t,i]){const r=m.shortcuts.get(t),a=i??r?.action.label??"",n=ls(t);return e.element.setAttribute("title",n?`${a} (${n})`:a),n?e.element.setAttribute("aria-keyshortcuts",rn(r.binding)):e.element.removeAttribute("aria-keyshortcuts"),j}}const _=it(cn);class dn extends R{constructor(){super(),this.watch(e=>[e.map,e.dirty,e.busy,e.canUndo,e.canRedo,e.base,e.viewer,e.identityLabel,e.shortcutsRev])}render(){const{map:e,dirty:t,busy:i,canUndo:r,canRedo:a,base:n,viewer:o}=m.state,l=n?.event.pubkey,c=!!(l&&l!==o);return b`<div class="toolbar">
            <div class="toolbar-heading">
                <span class="mission" id="mission">${e.name+(t?" *":"")}</span
                ><bs-account id="identity" class="identity"></bs-account>
            </div>
            <button
                id="new-map"
                ?disabled=${i}
                ${_("map.new")}
                @click=${M(()=>m.newMap())}
            >
                New
            </button>
            <button
                id="library-open"
                ?disabled=${i}
                ${_("map.library")}
                @click=${M(()=>m.openLibrary())}
            >
                Maps
            </button>
            <button
                id="settings-open"
                ?disabled=${i}
                ${_("map.settings")}
                @click=${()=>m.ui.settings?.show()}
            >
                Settings
            </button>
            <button
                id="undo"
                ?disabled=${!r}
                ${_("edit.undo")}
                @click=${()=>m.undo()}
            >
                Undo
            </button>
            <button
                id="redo"
                ?disabled=${!a}
                ${_("edit.redo")}
                @click=${()=>m.redo()}
            >
                Redo
            </button>
            <button
                id="save-draft"
                ?disabled=${i||!T("storage")||c}
                ${_("map.save-draft")}
                @click=${M(()=>m.saveDraft())}
            >
                Save draft
            </button>
            <button
                id="publish"
                class="primary"
                ?disabled=${i||!o||c||!["outbox","upload"].every(T)}
                ${_("map.publish")}
                @click=${M(()=>m.publish())}
            >
                ${n?"Update map":"Publish"}
            </button>
            <button
                id="help-open"
                ${_("help.tutorial")}
                @click=${()=>m.ui.help?.show()}
            >
                Help
            </button>
        </div>`}}customElements.define("bs-toolbar",dn);function hn(s,e){const t=a=>a.pane.getBoundingClientRect().width,i=a=>a===e.left?"--left-w":"--right-w";function r(a,n){const o=Math.round(Math.max(a.min,Math.min(a.max,n)));s.style.setProperty(i(a),`${o}px`),a.handle.setAttribute("aria-valuenow",String(o))}for(const a of[e.left,e.right]){const n=a.handle,o=a===e.left;let l;const c=h=>{if(!l)return;const d=(h.clientX-l.x)*(o?1:-1);r(a,l.width+d)},p=h=>{n.hasPointerCapture(h.pointerId)&&(l=void 0,n.releasePointerCapture(h.pointerId),n.classList.remove("dragging"),document.body.classList.remove("pane-resizing"))};n.setAttribute("aria-valuemin",String(a.min)),n.setAttribute("aria-valuemax",String(a.max)),n.setAttribute("aria-valuenow",String(t(a)||a.defaultWidth)),n.addEventListener("pointerdown",h=>{h.button===0&&(h.preventDefault(),l={width:t(a),x:h.clientX},n.setPointerCapture(h.pointerId),n.classList.add("dragging"),document.body.classList.add("pane-resizing"))}),n.addEventListener("pointermove",h=>{n.hasPointerCapture(h.pointerId)&&c(h)}),n.addEventListener("pointerup",p),n.addEventListener("pointercancel",p),n.addEventListener("lostpointercapture",()=>{l=void 0,n.classList.remove("dragging"),document.body.classList.remove("pane-resizing")}),n.addEventListener("dblclick",()=>r(a,a.defaultWidth)),n.addEventListener("keydown",h=>{if(h.key==="Home"){h.preventDefault(),r(a,a.defaultWidth);return}const d=h.shiftKey?48:16;let u=0;if(h.key==="ArrowLeft")u=o?-d:d;else if(h.key==="ArrowRight")u=o?d:-d;else return;h.preventDefault(),r(a,t(a)+u)})}}class un extends R{constructor(){super(),this.watch(e=>[e.tool,e.placeOptions,e.shortcutsRev])}#e(e){const t=m.state.tool===e;return b`<button
            data-tool=${e}
            class=${t?"active":""}
            aria-pressed=${t}
            ${_(ns(e),le(e))}
            @click=${()=>m.setTool(e)}
        >
            ${le(e)}
        </button>`}render(){const{color:e,patrol:t,cacheKind:i}=m.state.placeOptions;return b`<h2>MAP TOOLS</h2>
            <div class="tool-list">${qa.map(r=>this.#e(r))}</div>
            ${Wa.map(([r,a])=>b`<details class="tool-category" open>
                        <summary>${r}</summary>
                        <div class="tool-list">${a.map(n=>this.#e(n))}</div>
                    </details>`)}
            <label
                >Container color<input
                    type="color"
                    id="container-color"
                    .value=${x(e)}
                    @input=${r=>m.setPlaceOptions({color:r.target.value})}
            /></label>
            <label
                ><input
                    type="checkbox"
                    id="patrol"
                    .checked=${x(t)}
                    @change=${r=>m.setPlaceOptions({patrol:r.target.checked})}
                />
                Patrolling guards</label
            >
            <label
                >Cache contents<select
                    id="cache-kind"
                    .value=${x(i)}
                    @change=${r=>m.setPlaceOptions({cacheKind:r.target.value})}
                >
                    <option value="">Random loot</option>
                    <option value="weapons">Weapon cache</option>
                    <option value="start">Starter supplies</option>
                </select></label
            >
            <p class="notice">
                Drag to draw rectangles. Select an object to move it. Doors need a selected
                building. Pan with the Pan tool; zoom with the buttons or mouse wheel. Open Help for
                a step-by-step guide.
            </p>`}}customElements.define("bs-tool-palette",un);const C={wall:"#575a50",wallEdge:"#252920",water:"#28464d",floor:"#716452",crate:"#786943",sandbag:"#847856",pipe:"#686f66",wreck:"#514943",grid:"rgba(212,211,168,.13)",selection:"#e4c466",insertion:"#8ea66f",enemy:"#c07860",cache:"#e0bd6a",exfil:"#88ad98"};function cs(s,e,t){const i=t.grid??at(e),r=t.tileSize;s.save();const a=s.getTransform(),n=t.viewport??[Math.floor(-a.e/a.a/r),Math.floor(-a.f/a.d/r),Math.floor((s.canvas.width-a.e)/a.a/r),Math.floor((s.canvas.height-a.f)/a.d/r)],o=Math.max(0,n[0]),l=Math.max(0,n[1]),c=Math.min(i.w-1,n[2]),p=Math.min(i.h-1,n[3]);if(c<o||p<l){s.restore();return}s.beginPath(),s.rect(o*r,l*r,(c-o+1)*r,(p-l+1)*r),s.clip(),s.clearRect(o*r,l*r,(c-o+1)*r,(p-l+1)*r);const h=f=>`rgb(${f.join(",")})`,d={".":h(e.theme.ground),"=":h(e.theme.road),",":C.floor,"#":C.wall,w:C.water,K:"#365771",o:"#65716a",p:C.pipe,c:C.crate,v:C.wreck,h:C.sandbag};for(const f of e.containers??[])s.fillStyle=f[4],s.fillRect(f[0]*r,f[1]*r,(f[2]-f[0]+1)*r,(f[3]-f[1]+1)*r);for(let f=l;f<=p;f++)for(let $=o;$<=c;$++){const S=i.grid[f][$],A=$*r,E=f*r;if(S!=="K"&&(s.fillStyle=d[S]??d["."],s.fillRect(A,E,r,r)),S==="."){const se=($*17+f*37)%11/120;s.fillStyle=`rgba(18,20,15,${se})`,s.fillRect(A,E,r,r),r>=10&&(s.fillStyle="rgba(189,181,139,.12)",s.fillRect(A+r*.2,E+r*.65,Math.max(1,r*.06),Math.max(1,r*.06)))}else S==="#"?(s.fillStyle="rgba(0,0,0,.25)",s.fillRect(A,E+r*.75,r,r*.25),s.strokeStyle=C.wallEdge,s.lineWidth=Math.max(1,r*.07),s.strokeRect(A+1,E+1,r-2,r-2)):S===","?(s.strokeStyle="rgba(30,26,19,.17)",s.lineWidth=1,s.beginPath(),s.moveTo(A,E+r*.5),s.lineTo(A+r,E+r*.5),s.stroke()):S==="w"?(s.strokeStyle="rgba(116,150,145,.18)",s.beginPath(),s.moveTo(A+r*.12,E+r*.55),s.lineTo(A+r*.65,E+r*.55),s.stroke()):"chpvK".includes(S)?(s.strokeStyle="rgba(15,18,14,.65)",s.lineWidth=Math.max(1,r*.08),s.strokeRect(A+r*.13,E+r*.15,r*.74,r*.7),s.strokeStyle="rgba(194,181,141,.26)",s.beginPath(),s.moveTo(A+r*.2,E+r*.2),s.lineTo(A+r*.8,E+r*.8),s.stroke()):S==="o"&&(s.strokeStyle="#26372f",s.lineWidth=Math.max(1,r*.08),s.beginPath(),s.arc(A+r/2,E+r/2,r*.35,0,Math.PI*2),s.stroke())}if(e.theme.markings!==!1){s.strokeStyle="rgba(204,190,125,.35)",s.lineWidth=Math.max(1,r*.08);for(const f of e.roads??[]){const $=f.length===4?f:[f[0],f[1],f[0],f[1]],S=$[2]-$[0]>=$[3]-$[1],A=(S?$[1]+$[3]+1:$[0]+$[2]+1)/2,E=Math.max(S?o:l,S?$[0]:$[1]),se=Math.min(S?c:p,S?$[2]:$[3]);for(let B=E;B<=se;B++){const ps=S?B:Math.floor(A),ms=S?Math.floor(A):B;i.grid[ms]?.[ps]==="="&&(s.beginPath(),S?(s.moveTo((B+.15)*r,A*r),s.lineTo((B+.75)*r,A*r)):(s.moveTo(A*r,(B+.15)*r),s.lineTo(A*r,(B+.75)*r)),s.stroke())}}}if(t.showGrid){s.strokeStyle=C.grid,s.lineWidth=1,s.beginPath();for(let f=o;f<=c+1;f++)s.moveTo(f*r,0),s.lineTo(f*r,i.h*r);for(let f=l;f<=p+1;f++)s.moveTo(0,f*r),s.lineTo(i.w*r,f*r);s.stroke()}const u=(f,$,S)=>{if(f[0]<o||f[0]>c||f[1]<l||f[1]>p)return;const A=(f[0]+.5)*r,E=(f[1]+.5)*r;s.fillStyle="#171e17",s.strokeStyle=$,s.lineWidth=Math.max(1.5,r*.09),s.beginPath(),s.arc(A,E,r*.38,0,Math.PI*2),s.fill(),s.stroke(),r>=13&&(s.fillStyle=$,s.font=`bold ${Math.max(9,r*.5)}px monospace`,s.textAlign="center",s.textBaseline="middle",s.fillText(S,A,E))};for(const f of e.caches??[])u([f[0],f[1]],C.cache,f[2]==="weapons"?"W":"C");for(const f of e.enemies??[])u([f[0],f[1]],C.enemy,f[2]==="p"?"P":"E");u(e.start,C.insertion,"I"),u(e.exfil,C.exfil,"H");const y=(f,$)=>{s.strokeStyle=$,s.lineWidth=2,s.strokeRect(f[0]*r+1,f[1]*r+1,(f[2]-f[0]+1)*r-2,(f[3]-f[1]+1)*r-2)};if(t.selection){const f=We(e,t.selection);f&&y(f,C.selection)}if(t.hover&&y([...t.hover,...t.hover],"rgba(234,230,196,.7)"),t.dragRect){s.fillStyle="rgba(228,196,102,.18)";const f=t.dragRect;s.fillRect(f[0]*r,f[1]*r,(f[2]-f[0]+1)*r,(f[3]-f[1]+1)*r),y(f,C.selection)}s.restore()}class pn extends R{static properties={hover:{state:!0}};#e;#t;#s=16;#i=0;#r=0;#l=!0;#a;#n;#o;#d=new ResizeObserver(()=>this.#l?this.fit():this.draw());#h=()=>{this.#a=void 0,this.draw()};constructor(){super(),this.watch(e=>[e.map,e.selection,e.showGrid,e.tool,e.busy,e.shortcutsRev])}connectedCallback(){super.connectedCallback(),m.ui.canvas=this,window.addEventListener("blur",this.#h),window.addEventListener("pagehide",this.#u)}disconnectedCallback(){super.disconnectedCallback(),this.#u()}#u=()=>{window.removeEventListener("blur",this.#h),this.#d.disconnect(),this.#h()};render(){const{map:e,showGrid:t,tool:i}=m.state,[r,a]=ee(e),n=this.hover?`${this.hover[0]}, ${this.hover[1]} · ${r} × ${a}`:`${r} × ${a} · map tiles`;return b`<div class="canvas-tools">
                <button
                    id="zoom-out"
                    aria-label="Zoom out"
                    ${_("view.zoom-out")}
                    @click=${()=>this.zoom(1/1.3)}
                >
                    −
                </button>
                <button
                    id="zoom-in"
                    aria-label="Zoom in"
                    ${_("view.zoom-in")}
                    @click=${()=>this.zoom(1.3)}
                >
                    +
                </button>
                <button id="fit" ${_("view.fit")} @click=${()=>this.fit()}>Fit</button>
                <button
                    id="grid-toggle"
                    aria-pressed=${t}
                    ${_("view.grid")}
                    @click=${()=>m.toggleGrid()}
                >
                    Grid
                </button>
                <button id="validate" ${_("map.check")} @click=${()=>m.validate()}>
                    Check route
                </button>
                <button
                    id="shortcuts-open"
                    class="keys-button"
                    aria-label="Keyboard shortcuts"
                    ${_("help.shortcuts")}
                    @click=${()=>m.ui.shortcuts?.show()}
                >
                    Keys
                </button>
                <small id="coordinates">${n}</small>
            </div>
            <canvas
                id="map-canvas"
                tabindex="0"
                style="cursor: ${i==="pan"?"grab":"crosshair"}"
                aria-label="Map canvas. Select a tool and drag to place objects. Use arrow keys to move selected objects. Press question mark for keyboard shortcuts."
                @pointerdown=${this.#f}
                @pointermove=${this.#b}
                @pointerup=${this.#g}
                @pointercancel=${this.#h}
                @lostpointercapture=${this.#h}
                @contextmenu=${o=>o.preventDefault()}
                @wheel=${{handleEvent:o=>this.#y(o),passive:!1}}
            ></canvas>`}firstUpdated(){this.#e=this.querySelector("#map-canvas"),this.#t=this.#e.getContext("2d"),this.#d.observe(this.#e),this.fit()}updated(){this.draw()}#c(){const e=m.map;return this.#n!==e&&(this.#n=e,this.#o=at(e)),this.#o}draw(){const e=this.#e,t=this.#t;if(!e||!t)return;const i=Math.min(devicePixelRatio||1,2),r=e.getBoundingClientRect();if(!r.width||!r.height)return;(e.width!==Math.round(r.width*i)||e.height!==Math.round(r.height*i))&&(e.width=Math.round(r.width*i),e.height=Math.round(r.height*i));const a=this.#s;t.setTransform(i,0,0,i,0,0),t.fillStyle="#0b0d0b",t.fillRect(0,0,r.width,r.height),t.save(),t.translate(this.#i,this.#r);const{showGrid:n,selection:o}=m.state;cs(t,m.map,{tileSize:a,grid:this.#c(),showGrid:n,selection:o,hover:this.hover});const l=this.#a;if(l&&!["select","pan","erase"].includes(l.mode)){const[c,p]=l.from,[h,d]=l.last;t.strokeStyle="#e8c547",t.fillStyle="#e8c54722";const u=Math.min(c,h)*a,y=Math.min(p,d)*a,f=(Math.abs(h-c)+1)*a,$=(Math.abs(d-p)+1)*a;t.fillRect(u,y,f,$),t.lineWidth=2,t.strokeRect(u,y,f,$)}t.restore()}fit(){if(!this.#e)return;this.#l=!0;const e=this.#e.getBoundingClientRect(),[t,i]=ee(m.map);this.#s=Math.max(1,Math.min((e.width-24)/t,(e.height-24)/i)),this.#i=(e.width-t*this.#s)/2,this.#r=(e.height-i*this.#s)/2,this.draw()}zoom(e,t,i){if(!this.#e)return;t??=this.#e.clientWidth/2,i??=this.#e.clientHeight/2,this.#l=!1;const r=Math.max(1,Math.min(64,this.#s*e));this.#i=t-(t-this.#i)*(r/this.#s),this.#r=i-(i-this.#r)*(r/this.#s),this.#s=r,this.draw()}#p(e){const t=this.#e.getBoundingClientRect();return[Math.floor((e.clientX-t.left-this.#i)/this.#s),Math.floor((e.clientY-t.top-this.#r)/this.#s)]}#m(e){const[t,i]=ee(m.map);return e[0]>=0&&e[1]>=0&&e[0]<t&&e[1]<i}#f=e=>{const t=this.#e,{busy:i,tool:r}=m.state;if(i||this.#a)return;t.focus();const a=this.#p(e);if(!this.#m(a)&&r!=="pan")return;e.preventDefault(),t.setPointerCapture(e.pointerId);let n=r;(e.button===1||e.button===2)&&(n="pan");const o=n==="select"?m.selectAt(a):m.state.selection;this.#a={id:e.pointerId,from:a,last:a,screenX:e.clientX,screenY:e.clientY,offsetX:this.#i,offsetY:this.#r,mode:n,selection:o}};#b=e=>{const t=this.#p(e),i=this.#a;i&&e.pointerId===i.id&&(i.last=t,i.mode==="pan"&&(this.#l=!1,this.#i=i.offsetX+e.clientX-i.screenX,this.#r=i.offsetY+e.clientY-i.screenY));const r=this.#m(t)?t:void 0;(r?.[0]!==this.hover?.[0]||r?.[1]!==this.hover?.[1])&&(this.hover=r),this.draw()};#g=e=>{const t=this.#a;if(!t||e.pointerId!==t.id)return;this.#a=void 0,this.#e.releasePointerCapture(e.pointerId);const i=this.#p(e);M(()=>{t.mode!=="pan"&&(t.mode==="select"?t.selection&&(i[0]!==t.from[0]||i[1]!==t.from[1])&&m.moveSelectionBy(i[0]-t.from[0],i[1]-t.from[1],t.selection):t.mode==="erase"?m.eraseAt(i):this.#m(i)&&m.placeRect(t.mode,t.from,i))})(),this.draw()};#y(e){e.preventDefault();const t=this.#e.getBoundingClientRect();this.zoom(e.deltaY<0?1.15:1/1.15,e.clientX-t.left,e.clientY-t.top)}}customElements.define("bs-map-canvas",pn);class mn extends R{static properties={layer:{state:!0}};constructor(){super(),this.layer=Ot[0][0],this.watch(e=>[e.map,e.selection,e.busy,e.dirty,e.base,e.validation,e.shortcutsRev])}#e(){const e=m.map,t=this.layer,i=t==="start"||t==="exfil"?[e[t]]:e[t];return Array.isArray(i)?i:[]}render(){const{selection:e,busy:t,dirty:i,base:r,validation:a}=m.state,n=e?de(m.map,e):void 0,o=!e||["start","exfil"].includes(e.layer),l=()=>this.querySelector("#object-controls"),c=p=>Number(this.querySelector(`#move-${p}`).value);return b`<div class="section">
                <h3>SELECTED OBJECT</h3>
                <div id="selection-label" class="selection-label">
                    ${e?`${le(e.layer)} #${e.index+1}${e.buildingIndex!==void 0?` · building #${e.buildingIndex+1}`:""}`:"Nothing selected"}
                </div>
                <div id="object-controls">${Ca(m.map,e)}</div>
                <textarea
                    id="object-json"
                    hidden
                    .value=${x(n?JSON.stringify(n,null,2):"")}
                ></textarea>
                <div class="buttons">
                    <button
                        id="apply-object"
                        ?disabled=${!e||t}
                        @click=${M(()=>m.applyObject(l()))}
                    >
                        Apply changes
                    </button>
                    <button
                        id="delete-object"
                        class="danger"
                        ?disabled=${o||t}
                        ${_("selection.delete")}
                        @click=${M(()=>m.deleteSelected())}
                    >
                        Delete
                    </button>
                </div>
                <div class="row">
                    <label>Move across<input id="move-x" type="number" value="0" /></label>
                    <label>Move down<input id="move-y" type="number" value="0" /></label>
                </div>
                <button
                    id="move-object"
                    ?disabled=${!e||t}
                    @click=${M(()=>m.moveSelectionBy(c("x"),c("y")))}
                >
                    Move selected
                </button>
            </div>
            <div class="section">
                <h3>MAP CHECK</h3>
                <div id="validation" class="validation">${a}</div>
                <div id="draft-info" class="draft-info">
                    ${i?"Workspace has unsaved changes.":r?"Loaded published revision.":"Local workspace."}
                </div>
            </div>
            <details>
                <summary>Objects & layers</summary>
                <select
                    id="object-layer"
                    aria-label="Object layer"
                    @change=${p=>this.layer=p.target.value}
                >
                    ${Ot.map(([p,h])=>b`<option value=${p} ?selected=${p===this.layer}>${h}</option>`)}
                </select>
                <div id="object-list" class="object-list">
                    ${this.#e().map((p,h)=>b`<button
                                @click=${()=>m.selectObject({layer:this.layer,index:h})}
                            >
                                ${le(this.layer)} ${h+1}
                            </button>`)}
                </div>
            </details>`}}customElements.define("bs-inspector",mn);const fn=[["map","Map"],["tools","Tools"],["details","Details"]];class bn extends R{constructor(){super(),this.watch(e=>[e.workspaceTab,e.busy])}render(){const{workspaceTab:e,busy:t}=m.state;return b`<nav class="tabs" aria-label="Workspace">
                ${fn.map(([i,r])=>b`<button
                            data-tab=${i}
                            class=${i===e?"active":""}
                            @click=${()=>m.setWorkspaceTab(i)}
                        >
                            ${r}
                        </button>`)}
            </nav>
            <div class="workspace" data-tab=${e} ?inert=${t}>
                <aside id="tool-panel" class="pane left"><bs-tool-palette></bs-tool-palette></aside>
                <section class="canvas-pane" aria-label="Map workspace">
                    <bs-map-canvas></bs-map-canvas>
                    <div
                        id="resizer-left"
                        class="pane-resizer left"
                        role="separator"
                        aria-orientation="vertical"
                        tabindex="0"
                        aria-label="Resize map tools panel"
                    ></div>
                    <div
                        id="resizer-right"
                        class="pane-resizer right"
                        role="separator"
                        aria-orientation="vertical"
                        tabindex="0"
                        aria-label="Resize details panel"
                    ></div>
                </section>
                <aside id="inspector" class="pane right"><bs-inspector></bs-inspector></aside>
            </div>`}firstUpdated(){const e=t=>this.querySelector(t);hn(e(".workspace"),{left:{handle:e("#resizer-left"),pane:e("#tool-panel"),defaultWidth:174,min:120,max:420},right:{handle:e("#resizer-right"),pane:e("#inspector"),defaultWidth:260,min:200,max:560}})}updated(){requestAnimationFrame(()=>m.ui.canvas?.draw())}}customElements.define("bs-workspace",bn);class gn extends R{constructor(){super(),this.watch(e=>e.status)}render(){const{message:e,kind:t}=m.state.status;return b`<div id="status" class="status ${t}" role="status" aria-live="polite">
            ${e}
        </div>`}}customElements.define("bs-status",gn);/**
 * @license
 * Copyright 2017 Google LLC
 * SPDX-License-Identifier: BSD-3-Clause
 */const Ut=(s,e,t)=>{const i=new Map;for(let r=e;r<=t;r++)i.set(s[r],r);return i},Dt=it(class extends rt{constructor(s){if(super(s),s.type!==Y.CHILD)throw Error("repeat() can only be used in text expressions")}dt(s,e,t){let i;t===void 0?t=e:e!==void 0&&(i=e);const r=[],a=[];let n=0;for(const o of s)r[n]=i?i(o,n):n,a[n]=t(o,n),n++;return{values:a,keys:r}}render(s,e,t){return this.dt(s,e,t).values}update(s,[e,t,i]){const r=si(s),{values:a,keys:n}=this.dt(e,t,i);if(!Array.isArray(r))return this.ut=n,a;const o=this.ut??=[],l=[];let c,p,h=0,d=r.length-1,u=0,y=a.length-1;for(;h<=d&&u<=y;)if(r[h]===null)h++;else if(r[d]===null)d--;else if(o[h]===n[u])l[u]=Z(r[h],a[u]),h++,u++;else if(o[d]===n[y])l[y]=Z(r[d],a[y]),d--,y--;else if(o[h]===n[y])l[y]=Z(r[h],a[y]),ue(s,l[y+1],r[h]),h++,y--;else if(o[d]===n[u])l[u]=Z(r[d],a[u]),ue(s,r[h],r[d]),d--,u++;else if(c===void 0&&(c=Ut(n,u,y),p=Ut(o,h,d)),c.has(o[h]))if(c.has(o[d])){const f=p.get(n[u]),$=f!==void 0?r[f]:null;if($===null){const S=ue(s,r[h]);Z(S,a[u]),l[u]=S}else l[u]=Z($,a[u]),ue(s,r[h],$),r[f]=null;u++}else je(r[d]),d--;else je(r[h]),h++;for(;u<=y;){const f=ue(s,l[y+1]);Z(f,a[u]),l[u++]=f}for(;h<=d;){const f=r[h++];f!==null&&je(f)}return this.ut=n,Zt(s,l),j}});let De=0;const Nt=[];async function yn(s){De>=4&&await new Promise(e=>Nt.push(e)),De++;try{return await s()}finally{De--,Nt.shift()?.()}}class wn extends R{static properties={record:{attribute:!1},url:{state:!0},message:{state:!0}};constructor(){super(),this.message="Loading preview…"}#e;#t=0;connectedCallback(){super.connectedCallback(),this.#e=new IntersectionObserver(e=>{e.some(t=>t.isIntersecting)&&(this.#e?.disconnect(),this.#s())}),this.#e.observe(this)}updated(e){e.has("record")&&e.get("record")&&(this.url&&URL.revokeObjectURL(this.url),this.url=void 0,this.message="Loading preview…",this.#s())}disconnectedCallback(){super.disconnectedCallback(),this.#t++,this.#e?.disconnect(),this.url&&URL.revokeObjectURL(this.url)}async#s(){const e=++this.#t;await yn(async()=>{if(!(!this.isConnected||e!==this.#t))try{if(!T("resource"))throw new Error;const t=this.record.metadata.image;if(t)try{const o=await Ve.bytes(t);if(!["image/png","image/jpeg","image/webp","image/gif","image/avif"].includes(o.type)||o.size>5*1024*1024)throw new Error;if(e!==this.#t)return;this.url=URL.createObjectURL(o),await this.updateComplete;return}catch{}const i=await Je(this.record);if(e!==this.#t)return;await this.updateComplete;const r=this.querySelector("canvas"),[a,n]=ee(i);r.width=640,r.height=Math.max(160,Math.round(640*n/a)),cs(r.getContext("2d"),i,{tileSize:640/a}),this.message=""}catch{e===this.#t&&(this.message="Preview unavailable")}})}render(){return b`${this.url?b`<img
                      src=${this.url}
                      alt=${`Preview of ${this.record.metadata.title}`}
                      @error=${()=>{this.url&&URL.revokeObjectURL(this.url),this.url=void 0,this.message="Preview unavailable"}}
                  />`:b`<canvas
                          aria-label=${`Preview of ${this.record?.metadata.title??"map"}`}
                          ?hidden=${!!this.message}
                      ></canvas
                      >${this.message?b`<span>${this.message}</span>`:""}`}`}}customElements.define("bs-map-preview",wn);const pe=s=>`${s.slice(0,12)}…${s.slice(-6)}`,Lt=s=>new Date(s*1e3).toLocaleString();class $n extends R{static properties={tab:{state:!0},selected:{state:!0},discussion:{state:!0},detailStatus:{state:!0},posting:{state:!0}};#e=0;#t="";constructor(){super(),this.tab="mine",this.detailStatus="",this.posting=!1,this.watch(e=>[e.library,e.viewer,e.busy])}connectedCallback(){super.connectedCallback(),m.ui.library=this}disconnectedCallback(){super.disconnectedCallback(),this.#e++}updated(e){if(e.has("selected")&&this.selected){const t=this.querySelector(".map-browser-scroll");t&&(t.scrollTop=0)}this.#t!==m.state.viewer&&(this.#t=m.state.viewer,this.#e++,this.selected=void 0,this.discussion=void 0,this.isOpen&&this.#i(this.#t?"mine":"global"))}get#s(){return this.querySelector("dialog")}get isOpen(){return!!this.#s?.open}show(){this.selected=void 0,this.discussion=void 0,this.tab=m.state.viewer?"mine":"templates",this.isOpen||this.#s.showModal()}close(){this.#e++,this.selected=void 0,this.discussion=void 0,this.isOpen&&this.#s.close()}async#i(e){this.#e++,this.tab=e,this.selected=void 0,this.discussion=void 0,e==="templates"?m.showTemplates():await m.browse(e==="mine"?m.state.viewer:void 0,!1,e==="following")}async#r(e){const t=++this.#e;this.selected=e,this.discussion=void 0,this.detailStatus="Loading comments, reactions and remixes…";try{const i=await Ms(e);if(t!==this.#e||!this.isOpen)return;this.discussion=i,this.detailStatus=i.notice}catch{t===this.#e&&(this.detailStatus="Activity could not be loaded. Please retry.")}}async#l(e,t){const i=++this.#e,[,r,a]=e.split(":");this.detailStatus="Opening related map…";try{const n=await as({author:r,id:a,relays:t?[t]:void 0});i===this.#e&&this.isOpen&&await this.#r(n)}catch(n){i===this.#e&&(this.detailStatus=n instanceof Error?n.message:"Related map unavailable.")}}#a(e){return async()=>{try{await e()}catch(t){this.detailStatus=t instanceof Error?t.message:"This action failed. Please retry.",m.fail(t)}}}async#n(e){if(e.preventDefault(),!this.selected||this.posting||!m.state.viewer)return;const t=this.selected,i=this.#e,r=m.state.viewer,a=this.querySelector("#map-comment"),n=a.value;this.posting=!0;try{const o=await Cs(t,n);if(i!==this.#e||r!==m.state.viewer)return;a.value="",this.discussion?this.discussion={...this.discussion,comments:[...this.discussion.comments.filter(l=>l.id!==o.id),o]}:await this.#r(t),this.detailStatus="Comment published."}catch(o){i===this.#e&&(this.detailStatus=o instanceof Error?o.message:"Comment failed. Please retry.")}finally{this.posting=!1}}#o(e){const t=e.metadata;return b`<article class="map-card">
            <button
                class="map-card-open"
                @click=${()=>this.#r(e)}
                aria-label=${`View ${t.title}`}
            >
                <bs-map-preview .record=${e}></bs-map-preview>
                <div class="map-card-copy">
                    <h3>${t.title}</h3>
                    <p>${t.summary||"No briefing yet."}</p>
                    <small
                        >${t.pubkey===m.state.viewer?"Your map":`By ${pe(t.pubkey)}`}</small
                    >
                    <small
                        >${t.lineage?"Remixed map":"Original map"} ·
                        ${new Date(t.createdAt*1e3).toLocaleDateString()}</small
                    >
                </div>
            </button>
        </article>`}#d(e){const t=e.metadata,i=t.pubkey===m.state.viewer,r=this.discussion;return b`<div class="map-detail">
            <div class="map-detail-main">
                <bs-map-preview .record=${e}></bs-map-preview>
                <h1>${t.title}</h1>
                <p class="map-briefing">${t.summary}</p>
                <dl>
                    <dt>Creator</dt>
                    <dd title=${t.pubkey}>${pe(t.pubkey)}</dd>
                    <dt>Updated</dt>
                    <dd>${Lt(t.createdAt)}</dd>
                </dl>
                ${t.content?b`<h3>Notes & credits</h3>
                              <p class="map-notes">${t.content}</p>`:k}
                <div class="map-tags">${t.hashtags.map(a=>b`<span>#${a}</span>`)}</div>
                <div class="buttons map-actions">
                    ${i?b`<button class="primary" ?disabled=${m.state.busy} @click=${this.#a(()=>m.openRecord(e,!1))}>Edit map</button>`:k}
                    <button
                        ?disabled=${!m.state.viewer||m.state.busy}
                        @click=${this.#a(()=>m.openRecord(e,!0))}
                    >
                        Remix map
                    </button>
                    ${i?b`<button
                                  class="danger"
                                  ?disabled=${m.state.busy}
                                  @click=${this.#a(async()=>{await m.deleteRecord(e)&&(this.#e++,this.selected=void 0)})}
                              >
                                  Delete map
                              </button>`:k}
                </div>
                ${m.state.viewer?k:b`<p class="notice">Sign in through the host to remix maps or leave comments.</p>`}
                <section>
                    <h2>Remix lineage</h2>
                    ${t.lineage?b`<p>This map is a remix.</p>
                                  <button
                                      @click=${()=>this.#l(t.lineage.parent.address,t.lineage.parent.relay)}
                                  >
                                      View parent map
                                  </button>
                                  ${t.lineage.root.address!==t.lineage.parent.address?b`<button @click=${()=>this.#l(t.lineage.root.address,t.lineage.root.relay)}>View original map</button>`:k}
                                  <p>Contributors: ${t.lineage.authors.map(pe).join(", ")}</p>`:b`<p>Original map</p>`}
                </section>
                <section>
                    <h2>Remixes of this map</h2>
                    ${r?r.remixes.length?b`<div class="library-grid">
                                      ${Dt(r.remixes,a=>a.metadata.address,a=>this.#o(a))}
                                  </div>`:b`<p>No remixes found on available relays.</p>`:b`<p>Loading remixes…</p>`}
                </section>
            </div>
            <aside class="map-detail-social">
                <section>
                    <h2>Reactions</h2>
                    ${r?b`<div class="reaction-counts">
                                  <span
                                      >↑
                                      ${r.reactions.find(a=>a.label==="Upvotes")?.count??0}
                                      upvotes</span
                                  ><span
                                      >↓
                                      ${r.reactions.find(a=>a.label==="Downvotes")?.count??0}
                                      downvotes</span
                                  >
                                  ${r.reactions.filter(a=>!["Upvotes","Downvotes"].includes(a.label)).map(a=>b`<span>${a.label} ${a.count}</span>`)}
                              </div>`:b`<p>Loading reactions…</p>`}
                    <p class="draft-info" role="status">${this.detailStatus}</p>
                    <button @click=${()=>this.#r(e)}>Refresh activity</button>
                </section>
                <section>
                    <h2>Comments ${r?`(${r.comments.length})`:""}</h2>
                    <div class="comment-composer">
                        <label for="map-comment">Leave a comment</label
                        ><textarea
                            id="map-comment"
                            rows="3"
                            maxlength="4000"
                            required
                            ?disabled=${!m.state.viewer||this.posting}
                        ></textarea>
                        <button
                            type="button"
                            @click=${a=>this.#n(a)}
                            ?disabled=${!m.state.viewer||this.posting}
                        >
                            ${this.posting?"Posting…":"Post comment"}
                        </button>
                    </div>
                    ${r?.comments.length===0?b`<p>No comments found. Start the conversation.</p>`:k}
                    ${r?.comments.map(a=>b`<article class="map-comment">
                                <small title=${a.pubkey}
                                    >${pe(a.pubkey)} · ${Lt(a.created_at)}</small
                                >
                                ${a.tags.find(n=>n[0]==="k")?.[1]==="1111"?b`<small>Reply to ${pe(a.tags.find(n=>n[0]==="e")?.[1]??"")}</small>`:k}
                                <p>${a.content}</p>
                            </article>`)}
                </section>
            </aside>
        </div>`}render(){const{viewer:e,busy:t,library:i}=m.state;return b`<dialog
            id="library"
            aria-labelledby="maps-title"
            @cancel=${()=>{this.#e++,this.selected=void 0}}
        >
            <div class="browser-minimum">
                <b>Enlarge your map browser</b>
                <p>
                    Maps needs at least 280 × 240 pixels. Expand this napplet or rotate your phone.
                    Your place is kept.
                </p>
                <button @click=${()=>this.close()}>Back to editor</button>
            </div>
            <header class="dialog-head">
                <div>
                    <h2 id="maps-title">${this.selected?"MAP DETAILS":"MAP LIBRARY"}</h2>
                    <p class="draft-info">Your maps, new ideas, and the people building them.</p>
                </div>
                <div class="library-account">
                    <bs-account></bs-account
                    ><button id="library-close" @click=${()=>this.close()}>Back to editor</button>
                </div>
            </header>
            <div class="map-browser-scroll">
                ${this.selected?b`<button
                                  class="back-to-maps"
                                  @click=${()=>{this.#e++,this.selected=void 0}}
                              >
                                  ← Back to maps</button
                              >${this.#d(this.selected)}`:b` <nav class="library-controls" aria-label="Map collections">
                                  ${[["mine","my-maps","My maps"],["following","following-maps","Following"],["global","community-maps","Global"],["templates","templates","Templates"]].map(([r,a,n])=>b`<button
                                              id=${a}
                                              aria-current=${this.tab===r?"page":k}
                                              class=${this.tab===r?"active":""}
                                              ?disabled=${t||!e&&(r==="mine"||r==="following")}
                                              @click=${M(()=>this.#i(r))}
                                          >
                                              ${n}
                                          </button>`)}
                              </nav>
                              ${e?k:b`<p id="login-notice" class="notice">Sign in through the host to browse your maps and people you follow. Global maps and templates are available to everyone.</p>`}
                              <div class="library-utilities">
                                  <button
                                      id="restore-draft"
                                      ?disabled=${t}
                                      @click=${M(()=>m.restoreDraft())}
                                  >
                                      Open saved draft</button
                                  ><button @click=${M(()=>this.#i(this.tab))}>
                                      Refresh maps
                                  </button>
                              </div>
                              <p id="library-status" role="status">${i.status}</p>
                              <div id="library-grid" class="library-grid">
                                  ${i.view==="templates"?va.map(r=>b`<article class="map-card template-card">
                                                        <h3>${r.name}</h3>
                                                        <p>${r.desc??""}</p>
                                                        <button
                                                            @click=${M(()=>m.useTemplate(r))}
                                                        >
                                                            Use template
                                                        </button>
                                                    </article>`):Dt(i.records,r=>r.metadata.address,r=>this.#o(r))}
                              </div>
                              <button
                                  id="load-more"
                                  ?hidden=${!i.loadMore}
                                  @click=${M(()=>m.loadMore())}
                              >
                                  Load more
                              </button>`}
            </div>
        </dialog>`}}customElements.define("bs-library-dialog",$n);const xe=[["map","Map"],["appearance","Appearance"],["gameplay","Gameplay"],["files","Files"],["sharing","Sharing"]],Bt=s=>"#"+s.map(e=>e.toString(16).padStart(2,"0")).join("");class vn extends R{static properties={tab:{state:!0}};#e;#t="";constructor(){super(),this.tab="map",this.watch(e=>[e.map,e.base,e.busy,e.viewer,e.remixOf,e.content,e.hashtags,e.publishDetails,e.exportFallback,e.shortcutsRev])}connectedCallback(){super.connectedCallback(),m.ui.settings=this}get#s(){return this.querySelector("dialog")}get isOpen(){return this.#s.open}show(){this.#i("map"),this.#s.showModal()}close(){this.isOpen&&this.#s.close()}async#i(e,t=!1){if(this.tab=e,await this.updateComplete,this.querySelector(".settings-grid").scrollTop=0,t){const i=this.querySelector(`#settings-tab-${e}`);i.focus(),i.scrollIntoView({block:"nearest",inline:"nearest"})}}#r(e){let t=xe.findIndex(([r])=>r===this.tab);const i=xe.length-1;if(e.key==="ArrowRight")t=t===i?0:t+1;else if(e.key==="ArrowLeft")t=t===0?i:t-1;else if(e.key==="Home")t=0;else if(e.key==="End")t=i;else return;e.preventDefault(),this.#i(xe[t][0],!0)}#l(){const e=t=>this.querySelector(`#${t}`).value;return{id:e("id"),name:e("name"),desc:e("desc"),style:e("style"),ground:e("ground"),road:e("road"),markings:this.querySelector("#markings").checked}}#a(){return this.#e!==m.map&&(this.#e=m.map,this.#t=JSON.stringify(m.map,null,2)),this.#t}#n(e,t){return b`<section
            class="settings-panel"
            role="tabpanel"
            id="settings-panel-${e}"
            aria-labelledby="settings-tab-${e}"
            ?hidden=${this.tab!==e}
        >
            ${t}
        </section>`}render(){const{base:e,busy:t,viewer:i,remixOf:r,content:a,hashtags:n,publishDetails:o,exportFallback:l}=m.state,c=m.map,[p,h]=ee(c),d=M(()=>m.updateMetadata(this.#l())),u=y=>Number(this.querySelector(`#${y}`).value);return b`<dialog id="settings" aria-labelledby="settings-title">
            <div class="dialog-head">
                <h2 id="settings-title">MAP SETTINGS</h2>
                <button id="settings-close" @click=${()=>this.close()}>Done</button>
            </div>
            <div class="settings-tabs" role="tablist" aria-label="Map settings">
                ${xe.map(([y,f])=>b`<button
                            type="button"
                            role="tab"
                            id="settings-tab-${y}"
                            aria-controls="settings-panel-${y}"
                            aria-selected=${this.tab===y}
                            tabindex=${this.tab===y?0:-1}
                            data-settings-tab=${y}
                            @click=${()=>this.#i(y)}
                            @keydown=${$=>this.#r($)}
                        >
                            ${f}
                        </button>`)}
            </div>
            <div class="settings-grid" ?inert=${t}>
                ${this.#n("map",b`<h3>Map details</h3>
                        <form id="metadata" @submit=${y=>y.preventDefault()}>
                            <label hidden
                                >Map ID<input
                                    id="id"
                                    maxlength="32"
                                    pattern="[a-z0-9][a-z0-9\\-]{0,31}"
                                    required
                                    ?disabled=${!!e}
                                    .value=${x(c.id)}
                                    @change=${d}
                            /></label>
                            <label
                                >Map name<input
                                    id="name"
                                    maxlength="32"
                                    required
                                    .value=${x(c.name)}
                                    @change=${d}
                            /></label>
                            <label
                                >Briefing<textarea
                                    id="desc"
                                    maxlength="200"
                                    rows="2"
                                    .value=${x(c.desc??"")}
                                    @change=${d}
                                ></textarea>
                            </label>
                            <div class="row">
                                <label
                                    >Width<input
                                        id="width"
                                        type="number"
                                        min="16"
                                        max="256"
                                        .value=${x(String(p))}
                                /></label>
                                <label
                                    >Height<input
                                        id="height"
                                        type="number"
                                        min="16"
                                        max="256"
                                        .value=${x(String(h))}
                                /></label>
                            </div>
                            <button
                                id="resize"
                                type="button"
                                @click=${M(()=>m.resize(u("width"),u("height")))}
                            >
                                Resize grid
                            </button>
                        </form>`)}
                ${this.#n("appearance",b`<h3>Look & terrain</h3>
                        <label
                            >Terrain<select
                                id="style"
                                .value=${x(c.theme.style)}
                                @change=${d}
                            >
                                <option value="grass">Grass</option>
                                <option value="concrete">Concrete</option>
                                <option value="sand">Sand</option>
                                <option value="snow">Snow</option>
                            </select></label
                        >
                        <div class="row">
                            <label
                                >Ground<input
                                    id="ground"
                                    type="color"
                                    .value=${x(Bt(c.theme.ground))}
                                    @change=${d}
                            /></label>
                            <label
                                >Road<input
                                    id="road"
                                    type="color"
                                    .value=${x(Bt(c.theme.road))}
                                    @change=${d}
                            /></label>
                        </div>
                        <label
                            ><input
                                id="markings"
                                type="checkbox"
                                .checked=${x(c.theme.markings!==!1)}
                                @change=${d}
                            />
                            Road markings</label
                        >`)}
                ${this.#n("gameplay",b`<h3>Extra scenery & enemies</h3>
                        <p>Add variety when the game starts. These extras appear during play.</p>
                        <div id="deployment-controls">${Ra(c)}</div>
                        <textarea
                            id="advanced-json"
                            hidden
                            .value=${x(JSON.stringify({generate:c.generate??{},spawns:c.spawns??{},clutter:c.theme.clutter??["b","c"]},null,2))}
                        ></textarea>
                        <button
                            id="apply-advanced"
                            @click=${M(()=>m.applyDeployment(this.querySelector("#deployment-controls")))}
                        >
                            Apply settings
                        </button>`)}
                ${this.#n("files",b`<h3>Map files</h3>
                        <p>Open a saved map file or download a backup of this map.</p>
                        <input
                            id="import-file"
                            class="file-import"
                            type="file"
                            accept="application/json,.json"
                            aria-label="Open map file"
                            @change=${M(async()=>{const y=this.querySelector("#import-file"),f=y.files?.[0];f&&(await m.importFile(f),y.value="")})}
                        />
                        <textarea
                            id="json-editor"
                            hidden
                            .value=${x(this.#a())}
                        ></textarea>
                        <button
                            id="apply-json"
                            hidden
                            @click=${M(()=>m.applyJson(this.querySelector("#json-editor").value))}
                        >
                            Apply
                        </button>
                        <button
                            id="export-json"
                            ${_("map.download")}
                            @click=${()=>m.exportJson()}
                        >
                            Download map
                        </button>
                        <p id="export-fallback" class="draft-info">${l}</p>`)}
                ${this.#n("sharing",b`<h3>Sharing & credits</h3>
                        <button
                            id="remix-current"
                            ?disabled=${t||!i||!e}
                            @click=${M(()=>m.remixCurrent())}
                        >
                            Remix this map
                        </button>
                        <label
                            >Notes / credits<textarea
                                id="content"
                                rows="3"
                                maxlength="4000"
                                .value=${x(a)}
                                @change=${y=>m.setContent(y.target.value)}
                            ></textarea>
                        </label>
                        <label
                            >Tags (comma separated)<input
                                id="hashtags"
                                maxlength="200"
                                .value=${x(n.join(", "))}
                                @change=${y=>m.setHashtags(y.target.value)}
                        /></label>
                        <div id="lineage" class="draft-info">
                            ${r||e?.metadata.lineage?"Remix of an existing map. Original creator credits are preserved.":e?"Your published map. Changes update this map.":"New original map"}
                        </div>
                        <pre id="publish-details" hidden>${o}</pre>
                        <button id="export-event" hidden @click=${()=>m.exportEvent()}>
                            Export record
                        </button>`)}
            </div>
        </dialog>`}}customElements.define("bs-settings-dialog",vn);class kn extends R{constructor(){super(),this.watch(e=>e.shortcutsRev)}connectedCallback(){super.connectedCallback(),m.ui.shortcuts=this}get#e(){return this.querySelector("dialog")}get isOpen(){return this.#e.open}show(){this.isOpen?this.close():(this.requestUpdate(),this.#e.showModal())}close(){this.isOpen&&this.#e.close()}render(){const e=m.shortcuts.states(),t=[...new Set(e.map(i=>i.action.group))];return b`<dialog id="shortcuts" class="shortcuts-dialog">
            <div class="dialog-head">
                <h2>KEYBOARD SHORTCUTS</h2>
                <button id="shortcuts-close" @click=${()=>this.close()}>Close</button>
            </div>
            <p id="shortcuts-source" class="notice">
                ${e.some(i=>i.source==="host")?"Your host manages these keys. Change them in your host’s keyboard settings.":"Built-in keys. Shortcuts pause while you type in a field or a window is open."}
            </p>
            <div id="shortcuts-list" class="shortcuts-list">
                ${t.map(i=>b`<section>
                            <h3>${i.toUpperCase()}</h3>
                            <dl>
                                ${e.filter(r=>r.action.group===i).map(r=>{const a=ls(r.action.id);return b`<dt>${r.action.label}</dt>
                                            <dd>
                                                ${a?b`<kbd>${a}</kbd>`:r.source==="pending"?"Connecting…":"Not assigned"}
                                            </dd>`})}
                            </dl>
                        </section>`)}
            </div>
        </dialog>`}}customElements.define("bs-shortcuts-dialog",kn);const w=s=>b`<span class="ui-chip">${s}</span>`,I=(...s)=>s.map((e,t)=>b`${t?"+":""}<kbd>${e}</kbd>`),N=s=>b`<p class="notice">${s}</p>`,Me=(s,e,t)=>b`<li>
        <span class="help-marker" style="--marker:${s}">${e}</span><span>${t}</span>
    </li>`,me=[{id:"welcome",title:"Welcome",body:b` <p>
                This editor builds maps for Black Site: the ground, buildings, cover, guards and
                loot that players fight through.
            </p>
            <ul>
                <li><b>Centre:</b> your map. Everything you place appears here.</li>
                <li>
                    <b>Left:</b> the map tools, grouped into Terrain, Structures, Cover &amp;
                    supplies and Mission.
                </li>
                <li><b>Right:</b> details for the object you selected, plus the Map Check.</li>
                <li>
                    <b>Top bar:</b> ${w("New")} ${w("Maps")} ${w("Settings")} ${w("Undo")}
                    ${w("Redo")} ${w("Save draft")} ${w("Publish")}.
                </li>
            </ul>
            ${N(b`On a phone, the ${w("Map")} ${w("Tools")} ${w("Details")} tabs switch between these areas.`)}
            <p>
                Use ${w("Next")} to walk through the basics, or jump to any step from the list.
            </p>`},{id:"navigate",title:"Moving around",body:b` <ul>
                <li>
                    ${w("−")} and ${w("+")} zoom out and in. The mouse wheel zooms towards the
                    pointer.
                </li>
                <li>${w("Fit")} shows the whole map again.</li>
                <li>
                    Choose ${w("Pan map")} and drag to slide the map. With a mouse, you can also
                    drag with the right or middle button using any tool.
                </li>
                <li>${w("Grid")} shows or hides the tile grid.</li>
            </ul>
            ${N("The bottom-right corner of the map toolbar shows the tile under your pointer and the map size.")}`},{id:"terrain",title:"Ground & terrain",body:b` <p>Terrain shapes where players can walk and how the map looks.</p>
            <ul>
                <li>${w("Water")} blocks movement. Use it for rivers, harbours and moats.</li>
                <li>${w("Pavement")} lays hard ground for yards and lots.</li>
                <li>${w("Road")} draws roads in the map's road colour.</li>
                <li>Anything you leave unpainted is open ground in the map's terrain style.</li>
            </ul>
            <p>
                <b>To paint:</b> pick a tool, press on the map and drag. A gold box previews the
                rectangle you are drawing. Let go to place it.
            </p>
            ${N("Change the base terrain, ground colour and road colour in Settings.")}`},{id:"structures",title:"Structures",body:b` <ul>
                <li>${w("Wall")} draws solid walls that block movement.</li>
                <li>
                    ${w("Building")} drags out a building with a floor, outer walls and one door.
                    Buildings are at least 3 × 3 tiles.
                </li>
            </ul>
            <p>
                <b>Doors and rooms:</b> first select a building with ${w("Select / move")}. Then:
            </p>
            <ul>
                <li>
                    ${w("Building door")} adds another door on that building's <b>outside edge</b>.
                </li>
                <li>${w("Interior wall")} splits the inside into rooms.</li>
            </ul>
            ${N("If a door will not place, check that it sits on the outer wall of the selected building.")}`},{id:"cover",title:"Cover & supplies",body:b` <p>Cover blocks movement and gives players something to hide behind.</p>
            <ul>
                <li>
                    ${w("Container")}: shipping containers. Pick their colour with
                    <b>Container color</b> in the tool panel before drawing.
                </li>
                <li>${w("Tank (3×3)")}: a round storage tank. Click once to place it.</li>
                <li>
                    ${w("Pipe")} ${w("Crate")} ${w("Wreck")} ${w("Sandbag")}: smaller cover.
                    Drag to fill an area.
                </li>
            </ul>`},{id:"mission",title:"Mission setup",body:b` <p>
                These markers set the map's mission: where players start, where they escape, and
                what stands in the way.
            </p>
            <ul class="help-legend">
                ${Me(C.insertion,"I",b`<b>Insertion</b>: where players start. ${w("Insertion")} moves it.`)}
                ${Me(C.exfil,"H",b`<b>Extraction</b>: where players escape. ${w("Extraction")} moves it.`)}
                ${Me(C.enemy,"E",b`<b>Guard</b>: an enemy standing watch. Tick <b>Patrolling guards</b> first to place patrols (shown as P).`)}
                ${Me(C.cache,"C",b`<b>Loot cache</b>: pick <b>Cache contents</b> first: random loot, a weapon cache (shown as W) or starter supplies.`)}
            </ul>
            ${N("Every map has exactly one insertion and one extraction. You can move them, but not delete them.")}`},{id:"editing",title:"Selecting & editing",body:b` <ul>
            <li>
                Choose ${w("Select / move")} and click an object to select it. It gets a gold
                outline.
            </li>
            <li>
                Drag a selected object to move it, or use the arrow keys (hold Shift to move 5
                tiles).
            </li>
            <li>
                The <b>Selected object</b> panel shows plain settings for it. Change them, then
                press ${w("Apply changes")}.
            </li>
            <li>${w("Move selected")} shifts it by an exact number of tiles across and down.</li>
            <li>
                ${w("Delete")} or ${I("Delete")} removes it. ${w("Erase object")} removes
                whatever you click.
            </li>
            <li>
                <b>Objects &amp; layers</b> lists every object of one type, so you can find small or
                hidden ones.
            </li>
        </ul>`},{id:"check",title:"Undo & route check",body:b` <ul>
                <li>
                    ${w("Undo")} (${I("Ctrl","Z")}) and ${w("Redo")}
                    (${I("Ctrl","Shift","Z")}) step back and forward through your changes.
                </li>
                <li>
                    ${w("Check route")} makes sure players can walk from insertion to extraction.
                </li>
            </ul>
            <p>
                The result appears under <b>Map check</b> on the right. If something is wrong, it
                tells you what to fix, for example:
            </p>
            <ul>
                <li>
                    <i>Clear a path from insertion to extraction.</i> Open a gap in walls or water.
                </li>
                <li>
                    <i>Move insertion, extraction, guards or caches onto open ground.</i> A marker
                    is sitting on a wall or in water.
                </li>
            </ul>
            ${N("A map must pass the route check before it can be published.")}`},{id:"settings",title:"Map settings",body:b` <p>${w("Settings")} holds everything about the map as a whole:</p>
            <ul>
                <li>
                    <b>Map name</b> and <b>Briefing</b>: what players see when choosing the map.
                </li>
                <li>
                    <b>Width</b> and <b>Height</b>, then ${w("Resize grid")}. Move objects inside
                    the new size first.
                </li>
                <li><b>Terrain</b>, <b>Ground</b> and <b>Road</b> colours, and road markings.</li>
                <li>
                    <b>Extra scenery &amp; enemies</b>: random buildings, clutter and extra guards
                    and caches added each time the game starts.
                </li>
                <li>
                    <b>Map files</b>: ${w("Download map")} saves a backup file, and you can open a
                    map file from your device.
                </li>
            </ul>`},{id:"saving",title:"Saving your work",body:b` <ul>
                <li>
                    ${w("Save draft")} keeps your work in progress inside the host. A
                    <b>*</b> after the map name means there are unsaved changes.
                </li>
                <li>
                    To come back later, open ${w("Maps")} and choose ${w("Open saved draft")}.
                </li>
                <li>
                    Drafts are kept separately for each account, so switching accounts shows that
                    account's draft.
                </li>
            </ul>
            ${N("If saving drafts is unavailable in your host, use Settings → Map files → Download map to keep a backup.")}`},{id:"publishing",title:"Publishing",body:b` <ol>
                <li>Sign in through the host. The editor never sees your private keys.</li>
                <li>Run ${w("Check route")} and fix any problems.</li>
                <li>
                    Press ${w("Publish")}. For a map you already published, the button reads
                    ${w("Update map")}.
                </li>
                <li>Confirm. Your host may ask you to approve sharing.</li>
            </ol>
            <p>
                Published maps are public. Other players can find them in the game and in this
                editor.
            </p>
            ${N("Settings → Sharing & credits lets you add notes, credits and tags before you publish.")}`},{id:"library",title:"Maps & remixing",body:b` <p>${w("Maps")} opens the maps library:</p>
            <ul>
                <li>
                    ${w("My maps")}: your published maps. ${w("Edit")} opens one for changes;
                    ${w("Delete")} removes it from your published maps.
                </li>
                <li>${w("Community")}: maps by other players.</li>
                <li>
                    ${w("Built-in templates")}: the game's own maps, ready to use as a starting
                    point.
                </li>
            </ul>
            <p>
                <b>Remix</b> makes your own copy of any map. Remixes always credit the original
                creator, and publishing one never changes their map.
            </p>
            ${N("You can only edit maps you published yourself. For anyone else's map, use Remix.")}`},{id:"reference",title:"Quick reference",body:b` <table class="help-table">
                <tr>
                    <th>Action</th>
                    <th>Mouse &amp; keys</th>
                    <th>Touch</th>
                </tr>
                <tr>
                    <td>Draw a rectangle</td>
                    <td>Drag with a tool</td>
                    <td>Drag with a tool</td>
                </tr>
                <tr>
                    <td>Pan</td>
                    <td>Right or middle drag, or Pan map</td>
                    <td>Pan map, then drag</td>
                </tr>
                <tr>
                    <td>Zoom</td>
                    <td>Mouse wheel, − / +</td>
                    <td>− / + buttons</td>
                </tr>
                <tr>
                    <td>Show whole map</td>
                    <td>Fit</td>
                    <td>Fit</td>
                </tr>
                <tr>
                    <td>Move selected</td>
                    <td>
                        Drag, or ${I("←")}${I("↑")}${I("↓")}${I("→")} (${I("Shift")} for 5
                        tiles)
                    </td>
                    <td>Drag, or Move selected</td>
                </tr>
                <tr>
                    <td>Delete selected</td>
                    <td>${I("Delete")}</td>
                    <td>Delete button</td>
                </tr>
                <tr>
                    <td>Undo / redo</td>
                    <td>${I("Ctrl","Z")} / ${I("Ctrl","Shift","Z")}</td>
                    <td>Undo / Redo</td>
                </tr>
                <tr>
                    <td>Open this help</td>
                    <td>${I("F1")}</td>
                    <td>Help</td>
                </tr>
                <tr>
                    <td>All keyboard shortcuts</td>
                    <td>${I("Shift","?")}</td>
                    <td></td>
                </tr>
            </table>
            ${N("Every tool also has a single-letter key. Hover a tool to see it, or press Shift + ? for the full list. Your host may change these keys.")}`}],re=me.length;class An extends R{static properties={current:{state:!0}};constructor(){super(),this.current=0}connectedCallback(){super.connectedCallback(),m.ui.help=this}get#e(){return this.querySelector("dialog")}get isOpen(){return this.#e.open}show(e){const t=me.findIndex(i=>i.id===e);this.isOpen||this.#e.showModal(),this.#t(t<0?0:t,!0)}close(){this.isOpen&&this.#e.close()}async#t(e,t=!1){this.current=Math.max(0,Math.min(re-1,e)),await this.updateComplete,this.querySelector(".help-panels").scrollTop=0;const i=this.querySelector(`#help-tab-${me[this.current].id}`);i.scrollIntoView({block:"nearest"}),t&&i.focus()}#s(e){const t={ArrowUp:this.current-1,ArrowLeft:this.current-1,ArrowDown:this.current+1,ArrowRight:this.current+1,Home:0,End:re-1};e.key in t&&(e.preventDefault(),this.#t((t[e.key]+re)%re,!0))}render(){const e=this.current,t=e===re-1;return b`<dialog
            id="help"
            class="help-dialog"
            aria-labelledby="help-title"
            @close=${()=>m.tutorialClosed()}
            @keydown=${i=>{/^(Arrow|Page|Home$|End$| $)/.test(i.key)&&i.stopPropagation()}}
        >
            <div class="dialog-head">
                <h2 id="help-title">HOW TO BUILD A MAP</h2>
                <button id="help-close" @click=${()=>this.close()}>Close</button>
            </div>
            <div class="help-layout">
                <div
                    class="help-tabs"
                    role="tablist"
                    aria-orientation="vertical"
                    aria-label="Tutorial steps"
                    @keydown=${i=>this.#s(i)}
                >
                    ${me.map((i,r)=>b`<button
                                id="help-tab-${i.id}"
                                role="tab"
                                aria-controls="help-panel-${i.id}"
                                aria-label="Step ${r+1}: ${i.title}"
                                aria-selected=${r===e}
                                tabindex=${r===e?0:-1}
                                class=${r===e?"active":""}
                                @click=${()=>this.#t(r)}
                            >
                                <span class="help-step-number">${r+1}</span
                                ><span class="help-step-title">${i.title}</span>
                            </button>`)}
                </div>
                <div class="help-panels">
                    ${me.map((i,r)=>b`<section
                                id="help-panel-${i.id}"
                                class="help-panel"
                                role="tabpanel"
                                aria-labelledby="help-tab-${i.id}"
                                tabindex="0"
                                ?hidden=${r!==e}
                            >
                                <small class="help-progress">STEP ${r+1} OF ${re}</small>
                                <h3>${i.title}</h3>
                                ${i.body}
                            </section>`)}
                </div>
            </div>
            <div class="help-footer">
                <button
                    id="help-back"
                    ?disabled=${e===0}
                    @click=${()=>this.#t(e-1)}
                >
                    Back
                </button>
                <button
                    id="help-next"
                    class="primary"
                    .textContent=${t?"Start editing":"Next"}
                    @click=${()=>t?this.close():this.#t(e+1)}
                ></button>
            </div>
        </dialog>`}}customElements.define("bs-help-dialog",An);class Sn extends R{static properties={heading:{state:!0},message:{state:!0}};#e;constructor(){super(),this.heading="Confirm",this.message=""}connectedCallback(){super.connectedCallback(),m.ui.confirm=this}get#t(){return this.querySelector("dialog")}get isOpen(){return this.#t.open}ask(e,t){return this.#s(!1),this.heading=t,this.message=e,this.#t.showModal(),new Promise(i=>this.#e=i)}#s(e){const t=this.#e;this.#e=void 0,this.isOpen&&this.#t.close(),t?.(e)}render(){return b`<dialog
            id="confirm"
            class="confirm-dialog"
            @cancel=${e=>{e.preventDefault(),this.#s(!1)}}
            @close=${()=>m.confirmClosed()}
        >
            <h2 id="confirm-title">${this.heading}</h2>
            <p id="confirm-message">${this.message}</p>
            <div class="buttons">
                <button id="confirm-cancel" @click=${()=>this.#s(!1)}>Cancel</button>
                <button id="confirm-ok" class="primary" @click=${()=>this.#s(!0)}>
                    Continue
                </button>
            </div>
        </dialog>`}}customElements.define("bs-confirm-dialog",Sn);class En extends R{constructor(){super(),this.watch(e=>[e.publication,e.busy,e.viewer])}connectedCallback(){super.connectedCallback(),m.ui.publication=this}get#e(){return this.querySelector("dialog")}get isOpen(){return!!this.#e?.open}show(){this.isOpen||this.#e.showModal()}close(){!m.state.busy&&this.isOpen&&this.#e.close()}render(){const{publication:e,busy:t,viewer:i}=m.state,r=e.phase==="working";return b`<dialog
            id="publication"
            aria-labelledby="publication-title"
            @close=${()=>m.confirmClosed()}
            @cancel=${a=>{t&&a.preventDefault()}}
        >
            <h2 id="publication-title">
                ${r?"Publishing map…":e.phase==="success"?"Map published":"Map could not be published"}
            </h2>
            <ol class="publish-stages" aria-label="Publishing stages">
                ${["Prepare map","Upload map","Publish map"].map((a,n)=>b`<li class=${n<e.stage?"complete":n===e.stage?"current":""} aria-current=${r&&n===e.stage?"step":k}>${n<e.stage?"✓ ":`${n+1}. `}${a}</li>`)}
            </ol>
            ${r?b`<progress aria-label="Publishing map"></progress>
                          <p>
                              Keep this window open. If the host asks for approval, approve sharing
                              there.
                          </p>`:k}
            <p
                id="publication-message"
                role=${e.phase==="error"?"alert":"status"}
                aria-live="polite"
            >
                ${e.message}
            </p>
            ${e.phase==="error"?b`<p>Your map is still open. You can retry or download a backup.</p>
                          <details class="publish-error">
                              <summary>Service error details</summary>
                              <pre data-napplet-select="text">${e.error}</pre>
                          </details>`:k}
            ${e.warnings.map(a=>b`<p class="notice">${a}</p>`)}
            <div class="buttons">
                ${e.phase==="error"?b`<button
                                  id="publication-retry"
                                  class="primary"
                                  ?disabled=${t||!i}
                                  @click=${M(async()=>{this.close(),await m.publish()})}
                              >
                                  Retry publishing</button
                              ><button
                                  id="publication-download"
                                  @click=${()=>m.exportJson()}
                              >
                                  Download backup
                              </button>`:k}
                <button id="publication-close" ?disabled=${t} @click=${()=>this.close()}>
                    ${e.phase==="success"?"Done":"Close"}
                </button>
            </div>
        </dialog>`}}customElements.define("bs-publish-dialog",En);class xn extends R{render(){return b`<div class="minimum">
                <b>Enlarge your map workspace</b>
                <p>
                    Map editing needs at least 280 × 240 pixels. Expand this napplet or rotate your
                    phone. Your map stays open.
                </p>
            </div>
            <div class="app-shell">
                <bs-toolbar></bs-toolbar>
                <bs-workspace></bs-workspace>
                <bs-status></bs-status>
            </div>
            <bs-library-dialog></bs-library-dialog>
            <bs-settings-dialog></bs-settings-dialog>
            <bs-shortcuts-dialog></bs-shortcuts-dialog>
            <bs-help-dialog></bs-help-dialog>
            <bs-confirm-dialog></bs-confirm-dialog>
            <bs-publish-dialog></bs-publish-dialog>`}}customElements.define("bs-app",xn);async function Mn(s){const e=t=>[...t.querySelectorAll("*")].filter(i=>i instanceof R);s instanceof R&&await s.updateComplete;for(let t=0;;){const i=e(s);if(await Promise.all(i.map(r=>r.updateComplete)),i.length===t)return;t=i.length}}const O=s=>()=>{const e=document.getElementById(s);e&&!e.disabled&&e.click()},W=(s,e)=>()=>m.nudge(s,e),ds=[{id:"map.new",label:"New map",group:"Map",defaultKey:"Shift+N",run:O("new-map")},{id:"map.library",label:"Open maps",group:"Map",defaultKey:"Ctrl+O",run:O("library-open")},{id:"map.settings",label:"Map settings",group:"Map",defaultKey:"Ctrl+,",run:O("settings-open")},{id:"map.save-draft",label:"Save draft",group:"Map",defaultKey:"Ctrl+S",run:O("save-draft")},{id:"map.download",label:"Download map file",group:"Map",defaultKey:"Ctrl+Shift+S",run:O("export-json")},{id:"map.check",label:"Check route",group:"Map",defaultKey:"Shift+C",run:O("validate")},{id:"map.publish",label:"Publish or update",group:"Map",defaultKey:"Ctrl+Enter",run:O("publish")},{id:"edit.undo",label:"Undo",group:"Edit",defaultKey:"Ctrl+Z",run:O("undo")},{id:"edit.redo",label:"Redo",group:"Edit",defaultKey:"Ctrl+Shift+Z",run:O("redo")},{id:"edit.redo-alt",label:"Redo (alternate)",group:"Edit",defaultKey:"Ctrl+Y",run:O("redo")},{id:"selection.delete",label:"Delete selected object",group:"Edit",defaultKey:"Delete",run:O("delete-object")},{id:"selection.nudge-up",label:"Move selection up",group:"Edit",defaultKey:"ArrowUp",run:W(0,-1)},{id:"selection.nudge-down",label:"Move selection down",group:"Edit",defaultKey:"ArrowDown",run:W(0,1)},{id:"selection.nudge-left",label:"Move selection left",group:"Edit",defaultKey:"ArrowLeft",run:W(-1,0)},{id:"selection.nudge-right",label:"Move selection right",group:"Edit",defaultKey:"ArrowRight",run:W(1,0)},{id:"selection.jump-up",label:"Move selection up 5 tiles",group:"Edit",defaultKey:"Shift+ArrowUp",run:W(0,-5)},{id:"selection.jump-down",label:"Move selection down 5 tiles",group:"Edit",defaultKey:"Shift+ArrowDown",run:W(0,5)},{id:"selection.jump-left",label:"Move selection left 5 tiles",group:"Edit",defaultKey:"Shift+ArrowLeft",run:W(-5,0)},{id:"selection.jump-right",label:"Move selection right 5 tiles",group:"Edit",defaultKey:"Shift+ArrowRight",run:W(5,0)},{id:"view.zoom-in",label:"Zoom in",group:"View",defaultKey:"+",run:O("zoom-in")},{id:"view.zoom-out",label:"Zoom out",group:"View",defaultKey:"-",run:O("zoom-out")},{id:"view.fit",label:"Fit map to view",group:"View",defaultKey:"0",run:O("fit")},{id:"view.grid",label:"Show or hide grid",group:"View",defaultKey:"G",run:O("grid-toggle")},...nt.map(([s,e])=>({id:ns(s),label:`${e} tool`,group:"Tools",defaultKey:Ya[s],run:()=>m.setTool(s)})),{id:"help.shortcuts",label:"Show keyboard shortcuts",group:"Help",defaultKey:"Shift+?",run:()=>m.ui.shortcuts?.show(),inDialog:!0},{id:"help.tutorial",label:"Help & tutorial",group:"Help",defaultKey:"F1",run:()=>m.ui.help?.show()}];for(const s of ds){const e=s.run;s.run=()=>{m.state.busy||m.anyDialogOpen&&!s.inDialog||m.guard(e)()}}let hs,us;async function Cn(){const s=document.querySelector("#app");if(s.replaceChildren(document.createElement("bs-app")),await Mn(s),m.ui.canvas?.fit(),m.shortcuts=nn(ds,()=>m.bumpShortcuts()),m.bumpShortcuts(),T("identity"))try{hs=Ta(m.identityChanged);const t=m.identityEpoch,i=await ts();t===m.identityEpoch&&(m.identityChanged(i),i||m.setIdentityLabel("Sign in to share maps"))}catch(t){m.setIdentityLabel("Sign-in unavailable"),m.fail(t)}else m.setIdentityLabel("Sign-in unavailable"),m.status("Sign-in is unavailable here. You can still create a map and save a file.");const e=m.workspaceVersion;us=Za(m.receiveMapIntent,m.fail),await _n(e)}async function _n(s){if(!T("storage"))return;let e=!0;try{e=await Na()}catch{return}e||m.intentPending||m.workspaceVersion!==s||m.anyDialogOpen||m.ui.help?.show()}window.addEventListener("pagehide",()=>{hs?.close(),us?.close(),m.shortcuts.close()});Cn();
</script>
      <style>:root{font-family:Bahnschrift,Arial Narrow,Segoe UI,sans-serif;color:#e6eadf;background:#0b0d0b;--acc: #a5d63f;--gold: #e8c547;--red: #e0402f;--dim: #8c978a;--line: #ffffff20;--panel: #101410;--napplet-text-selection: none;color-scheme:dark}*{box-sizing:border-box}html,body,#app{height:100%;margin:0}body{overflow:hidden}bs-app,bs-toolbar,bs-workspace,bs-tool-palette,bs-map-canvas,bs-inspector,bs-status,bs-library-dialog,bs-settings-dialog,bs-shortcuts-dialog,bs-help-dialog,bs-confirm-dialog{display:contents}button,input,select,textarea{font:inherit;color:inherit}button,input,select,textarea{border:1px solid var(--line);background:#141914;padding:9px;min-height:38px;border-radius:0}button{cursor:pointer;letter-spacing:1px}button:hover:not(:disabled),button.active{border-color:var(--acc);background:#28341a}button.primary{color:#10140c;background:var(--acc);font-weight:700}button.danger{color:#ff897d}button:disabled{opacity:.4;cursor:default}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--gold);outline-offset:2px}input,select,textarea{width:100%;min-width:0}input[type=checkbox]{width:auto;min-height:0}textarea{resize:vertical;user-select:text;line-height:1.5}label{display:block;color:var(--dim);font-size:12px;letter-spacing:1px;margin:9px 0}label input,label select,label textarea{margin-top:5px;color:#e6eadf;letter-spacing:0}h2,h3{color:var(--acc);font-size:13px;letter-spacing:3px;margin:16px 0 10px}p{font-size:13px;line-height:1.5}small{color:var(--dim);line-height:1.5}.app-shell{height:100%;display:flex;flex-direction:column;background:radial-gradient(ellipse at 65% 10%,#1f2c1b,transparent 65%)}.toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:8px;border-bottom:1px solid var(--line);flex-shrink:0}.toolbar .mission{font-weight:700;color:var(--acc);letter-spacing:2px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.identity{margin-left:auto;font-size:12px;color:var(--dim)}.workspace{display:grid;grid-template-columns:var(--left-w, 174px) minmax(0,1fr) var(--right-w, 260px);flex:1;min-height:0}.pane{padding:12px;overflow:auto;background:#0c0f0cf0}.pane.left{border-right:1px solid var(--line)}.pane.right{border-left:1px solid var(--line)}.pane-resizer{position:absolute;top:0;bottom:0;width:10px;z-index:1;display:flex;justify-content:center;cursor:col-resize;touch-action:none}.pane-resizer.left{left:0}.pane-resizer.right{right:0}.pane-resizer:after{content:"";align-self:center;width:3px;height:38px;border:1px solid transparent;border-radius:2px}.pane-resizer:hover:after,.pane-resizer:focus-visible:after,.pane-resizer.dragging:after{border-color:var(--acc);background:#a5d63f2b}.pane-resizer:focus-visible{outline:2px solid var(--gold);outline-offset:-2px}body.pane-resizing,body.pane-resizing *{cursor:col-resize!important;user-select:none!important}.tool-list{display:grid;gap:5px}.tool-list button{text-align:left;min-height:36px;font-size:12px}.tool-list button.active{border-left:3px solid var(--acc);color:var(--acc)}.canvas-pane{min-width:0;min-height:0;position:relative;display:flex;flex-direction:column;overflow:hidden;background-image:linear-gradient(#a5d63f09 1px,transparent 1px),linear-gradient(90deg,#a5d63f09 1px,transparent 1px);background-size:40px 40px}.canvas-tools{display:flex;gap:5px;align-items:center;padding:6px;background:#0c0f0cee;border-bottom:1px solid var(--line);flex-wrap:wrap;position:relative;z-index:2}.canvas-tools button{padding:6px 10px;min-height:32px}.canvas-tools small{margin-left:auto}#map-canvas{width:100%;flex:1;min-height:0;display:block;touch-action:none;cursor:crosshair}.status{padding:7px 12px;border-top:1px solid var(--line);background:#0c0f0c;min-height:32px;font-size:12px;color:var(--dim);max-height:90px;overflow:auto}.status.error{color:#ff897d}.status.success{color:var(--acc)}.row{display:flex;gap:7px}.row>*{flex:1;min-width:0}.buttons{display:flex;flex-wrap:wrap;gap:6px}.buttons button{flex:1;font-size:12px}.section{border-top:1px solid var(--line);margin-top:14px;padding-top:4px}.notice{border-left:3px solid var(--gold);padding:8px 10px;background:#e8c5470b;font-size:12px;line-height:1.5;color:#c6cbbf}.validation{font-size:12px;line-height:1.5;white-space:pre-wrap;color:var(--gold);max-height:200px;overflow:auto}.json{font-family:ui-monospace,monospace;font-size:11px;min-height:180px}.tabs{display:none;border-bottom:1px solid var(--line)}.tabs button{flex:1;padding:8px 3px;font-size:12px}.minimum{display:none;height:100%;padding:12px;background:#101410;color:#e6eadf;font-size:13px;line-height:1.4;overflow:auto}.minimum b{color:var(--acc)}dialog{background:#0e130e;color:#e6eadf;border:1px solid #63734e;width:min(850px,96vw);max-height:92vh;padding:18px}dialog::backdrop{background:#000b}.dialog-head{display:flex;justify-content:space-between;align-items:center;gap:10px}.dialog-head h2{margin:0}.library-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;margin-top:12px}.map-card{border:1px solid var(--line);background:#141a12;padding:12px;min-width:0}.map-card h3{margin:0 0 8px;letter-spacing:1px}.map-card p{margin:7px 0;overflow-wrap:anywhere}.map-card .buttons{margin-top:10px}.map-card small{display:block;overflow-wrap:anywhere}.library-controls{display:flex;flex-wrap:wrap;gap:8px;margin-top:15px}.library-controls input{flex:1;min-width:150px}.library-controls button{font-size:12px}.draft-info{font-size:11px;color:var(--dim);overflow-wrap:anywhere}.object-list{max-height:150px;overflow:auto;display:grid;gap:4px}.object-list button{text-align:left;font-size:11px}.publish-details{word-break:break-all;font:11px/1.5 ui-monospace,monospace;white-space:pre-wrap}.mobile-only{display:none}details>summary{cursor:pointer;color:var(--acc);font-size:12px;letter-spacing:1px;padding:12px 0}.file-import{width:100%;font-size:12px}.selection-label{color:var(--gold);font-size:12px}.confirm-dialog{width:min(440px,96vw)}@media(max-width:760px){.workspace{grid-template-columns:var(--left-w, 140px) minmax(0,1fr)}.pane.right,.pane-resizer.right{display:none}.workspace[data-tab=details]{display:block}.workspace[data-tab=details] .pane.right{display:block;height:100%}.workspace[data-tab=details] .pane.left,.workspace[data-tab=details] .canvas-pane{display:none}.tabs{display:flex}.toolbar .mission{display:none}.toolbar{gap:4px}.toolbar button,.identity{font-size:11px}.mobile-only{display:inline-block}}@media(max-width:480px){.workspace{display:block}.pane-resizer{display:none}.workspace .pane,.workspace .canvas-pane{height:100%}.workspace[data-tab=map] .pane,.workspace[data-tab=tools] .canvas-pane,.workspace[data-tab=tools] .pane.right{display:none}.workspace[data-tab=tools] .pane.left{display:block}.tool-list{grid-template-columns:1fr 1fr}.toolbar{padding:5px}.toolbar button{padding:8px 6px}.identity{width:100%;margin:0}.canvas-tools small{font-size:10px}.pane{padding:12px}.status{max-height:66px}}@media(pointer:coarse){button,input,select{min-height:44px}.pane-resizer{width:16px}.tool-list button,.canvas-tools button{min-height:44px}.canvas-tools{gap:4px}.toolbar button,.tabs button{min-height:44px}}@media(max-width:279px),(max-height:239px){.app-shell{display:none}.minimum{display:block}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto}}[hidden]{display:none!important}.tool-category{border-top:1px solid var(--line);margin-top:8px}.tool-category>summary{padding:12px 2px;color:var(--gold);letter-spacing:1px}.tool-category .tool-list{padding-bottom:6px}.settings-grid{display:grid;grid-template-columns:1fr 1fr;gap:24px}.settings-grid>section{min-width:0}#settings .dialog-head{position:sticky;top:-18px;background:#0e130e;padding:10px 0;z-index:1}@media(max-width:600px){.settings-grid{grid-template-columns:1fr;gap:8px}}.shortcuts-dialog{width:min(620px,96vw)}.shortcuts-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:4px 22px}.shortcuts-list section{min-width:0}.shortcuts-list dl{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:5px 10px;margin:0;font-size:12px;align-items:center}.shortcuts-list dt{color:#c6cbbf;overflow-wrap:anywhere}.shortcuts-list dd{margin:0;color:var(--dim);text-align:right}kbd{font:11px/1 ui-monospace,monospace;color:var(--acc);border:1px solid #63734e;background:#141914;padding:3px 6px;white-space:nowrap}@media(pointer:coarse)and (hover:none){.keys-button{display:none}}.help-dialog{width:min(1120px,96vw);height:min(820px,94vh);max-height:94vh;padding:0;display:none;flex-direction:column;overflow:hidden}.help-dialog[open]{display:flex}.help-dialog>.dialog-head{padding:12px 20px;border-bottom:1px solid var(--line);flex-shrink:0}.help-dialog h2{margin:6px 0;font-size:16px}.help-dialog .dialog-head button{font-size:15px}.help-layout{display:grid;grid-template-columns:250px minmax(0,1fr);flex:1;min-height:0}.help-tabs{display:flex;flex-direction:column;gap:3px;padding:10px;border-right:1px solid var(--line);overflow-y:auto;background:var(--panel)}.help-tabs button{display:flex;align-items:center;gap:10px;text-align:left;border-color:transparent;border-left:3px solid transparent;background:none;font-size:15px;letter-spacing:.5px;min-height:44px;padding:8px 10px;flex-shrink:0}.help-tabs button.active{border-left-color:var(--acc);color:var(--acc);background:#28341a}.help-step-number{color:var(--gold);font-weight:700;min-width:1.5em;text-align:right}.help-panels{overflow-y:auto;padding:6px 28px 20px;min-width:0;font-size:16px;line-height:1.6}.help-panel{outline:none;max-width:760px}.help-panel h3{margin:6px 0 14px;font-size:22px;letter-spacing:2px}.help-panel p{font-size:16px;line-height:1.6}.help-progress{display:block;margin-top:18px;font-size:13px;letter-spacing:2px;color:var(--gold)}.help-panel ul,.help-panel ol{padding-left:22px;font-size:16px;line-height:1.6}.help-panel li{margin:8px 0}.help-panel .notice{margin-top:18px;font-size:15px;line-height:1.6;padding:10px 14px}.ui-chip{display:inline-block;border:1px solid #63734e;padding:0 7px;font-size:14px;letter-spacing:.5px;color:#e6eadf;background:#141914;white-space:nowrap;line-height:1.6}.help-panel kbd{display:inline-block;border:1px solid var(--line);border-bottom-width:2px;padding:0 6px;font:14px/1.6 ui-monospace,monospace;color:var(--gold);background:#141914}.help-legend{list-style:none;padding-left:0!important}.help-legend li{display:flex;gap:12px;align-items:flex-start}.help-marker{flex-shrink:0;width:30px;height:30px;border-radius:50%;border:2px solid var(--marker);background:#171e17;color:var(--marker);font:700 15px/26px sans-serif;text-align:center}.help-table{width:100%;border-collapse:collapse;font-size:15px}.help-table th{color:var(--acc);text-align:left;letter-spacing:1px;font-weight:400}.help-table th,.help-table td{border-bottom:1px solid var(--line);padding:9px 8px;vertical-align:top}.help-footer{display:flex;justify-content:flex-end;gap:10px;padding:12px 20px;border-top:1px solid var(--line);flex-shrink:0;background:#0e130e}.help-footer button{min-width:140px;min-height:44px;font-size:15px}@media(max-width:600px){.help-dialog{width:100vw;max-width:100vw;height:100vh;max-height:100vh;border:0}.help-layout{grid-template-columns:56px minmax(0,1fr)}.help-tabs{padding:6px 4px}.help-tabs button{justify-content:center;padding:6px 0}.help-step-title{display:none}.help-step-number{min-width:0;text-align:center;font-size:16px}.help-panels{padding:4px 14px 14px}.help-panel h3{font-size:19px}.help-table th:nth-child(3),.help-table td:nth-child(3){display:none}.help-footer{padding:8px 12px}.help-footer button{flex:1;min-width:0}}@media(max-height:480px){.help-dialog{height:100vh;max-height:100vh}.help-dialog>.dialog-head{padding:4px 14px}.help-progress{margin-top:8px}.help-footer{padding:6px 14px}}#settings{width:min(680px,96vw);height:min(680px,92vh);padding:0;overflow:hidden}#settings[open]{display:flex;flex-direction:column}#settings>.dialog-head{position:static;padding:12px 16px;border-bottom:1px solid var(--line);flex-shrink:0}#settings .settings-tabs{display:flex;overflow-x:auto;gap:0;padding:0 12px;border-bottom:1px solid var(--line);flex-shrink:0;background:#101410}#settings .settings-tabs button{flex:1;min-height:46px;border:0;border-bottom:3px solid transparent;background:transparent;letter-spacing:0;padding:10px 12px;font-size:13px;white-space:nowrap}#settings .settings-tabs button[aria-selected=true]{border-bottom-color:var(--acc);color:var(--acc);background:#28341a}#settings .settings-grid{display:block;overflow-y:auto;padding:8px 20px 20px;min-height:0;flex:1}#settings .settings-panel{max-width:540px;margin:0 auto}#settings .settings-panel h3{margin:12px 0 16px}#settings .settings-panel>button{min-height:44px;margin:8px 0}#settings .settings-panel p{color:#c6cbbf}#settings .settings-panel input[type=color]{height:44px}@media(max-width:480px){#settings>.dialog-head{padding:8px 12px}#settings .settings-tabs{padding:0 4px}#settings .settings-tabs button{padding:10px 11px;flex:0 0 auto}#settings .settings-grid{padding:4px 14px 16px}}@media(max-height:420px){#settings{height:96vh;max-height:96vh}#settings>.dialog-head{padding:4px 12px}#settings .settings-tabs button{min-height:40px;padding:6px 12px}}@media(max-width:480px){#settings .settings-tabs button{flex:1;padding:10px 5px;font-size:11px}}button.primary:hover:not(:disabled){background:#b8e85a;color:#10140c}#library{width:100vw;height:100dvh;max-width:none;max-height:none;margin:0;padding:0;border:0;overflow:hidden}#library[open]{display:flex;flex-direction:column}#library>.dialog-head{padding:16px 24px;border-bottom:1px solid var(--line);flex:0 0 auto}#library .dialog-head p{margin:4px 0 0}#library button{min-height:44px}.map-browser-scroll{overflow:auto;padding:16px 24px 32px;flex:1;min-height:0}#library .library-grid{grid-template-columns:repeat(auto-fill,minmax(min(260px,100%),1fr));gap:18px}#library .library-controls{margin-top:0}#library .library-controls button{font-size:14px}.library-utilities{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap}#library .map-card{padding:0;overflow:hidden}.map-card-open{display:block;width:100%;height:100%;padding:0;text-align:left;letter-spacing:normal;border:0}.map-card-copy{padding:16px}.map-card-copy p{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}bs-map-preview{display:flex;width:100%;aspect-ratio:16 / 10;background:#080b08;align-items:center;justify-content:center;overflow:hidden;color:var(--dim)}bs-map-preview img,bs-map-preview canvas{display:block;width:100%;height:100%;object-fit:contain}bs-map-preview canvas[hidden]{display:none}.map-detail{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(280px,1fr);gap:32px;margin-top:16px}.map-detail-main,.map-detail-social{min-width:0}.map-detail h1{font-size:clamp(24px,3vw,40px);margin-bottom:12px;overflow-wrap:anywhere}.map-detail section{border-top:1px solid var(--line);margin-top:24px;padding-top:12px}.map-detail h2{font-size:18px}.map-detail p,.map-detail dd{overflow-wrap:anywhere}.map-notes,.map-comment p,.map-briefing{white-space:pre-wrap}.map-detail dl{display:grid;grid-template-columns:auto 1fr;gap:8px 16px}.map-detail dd{margin:0}.map-detail dt{color:var(--dim)}.map-tags,.reaction-counts{display:flex;flex-wrap:wrap;gap:10px}.map-tags span,.reaction-counts span{border:1px solid var(--line);padding:6px 10px;overflow-wrap:anywhere;max-width:100%}.map-actions{margin-top:20px;flex-wrap:wrap}.map-comment{border-bottom:1px solid var(--line);padding:16px 0}.map-comment small{display:block;color:var(--dim);overflow-wrap:anywhere}#map-comment{width:100%;display:block;margin:8px 0}#library .template-card{padding:20px}@media(max-width:700px){.map-detail{grid-template-columns:minmax(0,1fr);gap:8px}#library>.dialog-head{padding:12px}.map-browser-scroll{padding:12px}#library .dialog-head p{display:none}#library .dialog-head h2{font-size:18px}#library-close{max-width:130px}}.browser-minimum{display:none}@media(max-width:279px),(max-height:239px){#library>.dialog-head,#library>.map-browser-scroll{display:none}#library>.browser-minimum{display:block;padding:12px;overflow:auto}}.toolbar-heading{display:flex;align-items:center;justify-content:space-between;width:100%;min-width:0;gap:12px}.toolbar-heading .identity{width:auto;margin-left:auto}.toolbar-heading .mission{max-width:min(50vw,420px)}bs-account{display:block;min-width:0;max-width:220px}.account-profile{display:flex;align-items:center;gap:8px;min-width:0}.account-avatar{display:inline-flex;align-items:center;justify-content:center;flex:0 0 32px;width:32px;height:32px;border:1px solid var(--line);background:#28341a;color:var(--acc);font-size:12px;overflow:hidden;border-radius:50%}.account-avatar img{width:100%;height:100%;object-fit:cover}.account-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#e6eadf;font-size:13px}.account-status{color:var(--dim);font-size:12px}.library-account{display:flex;flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:45%}@media(max-width:600px){bs-account{max-width:160px}}bs-publish-dialog{display:contents}#publication{width:min(560px,96vw)}#publication h2{font-size:22px}#publication progress{width:100%;accent-color:var(--acc)}#publication .buttons{flex-wrap:wrap;margin-top:20px}#publication button{min-height:44px}#publication-message{white-space:pre-wrap;overflow-wrap:anywhere}.publish-stages{padding:0;display:flex;gap:12px;list-style:none;font-size:13px;color:var(--dim);flex-wrap:wrap}.publish-stages .current{color:var(--gold)}.publish-stages .complete{color:var(--acc)}.publish-error pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:180px;overflow:auto;font-size:12px}
</style>
      <meta name="napplet-type" content="black-site-editor">
      <meta name="napplet-requires" content="identity,outbox,resource,upload">
      <meta name="napplet-config-schema" content="{&quot;$version&quot;:1,&quot;type&quot;:&quot;object&quot;,&quot;additionalProperties&quot;:false,&quot;properties&quot;:{}}">
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>