Back to ROM Foundry
SOURCE / PINNED RELEASE

Made of little things.

ROM Foundry

Release
4dc326ee7687…
Author-recorded commit
Not provided
License
Check the original source

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

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="Upload ROM bytes and publish a NIP-ROM event." />
    <title>ROM Foundry</title>
    <script type="module" crossorigin>function X(){const e=new Map;function t(o,i){if(e.has(o))throw new Error(`NAP domain "${o}" is already registered`);e.set(o,i)}function r(o){const i=o.type.indexOf(".");if(i<=0)return!1;const u=o.type.slice(0,i),s=e.get(u);return s?(s(o),!0):!1}function a(){return Array.from(e.keys())}return{registerNap:t,dispatch:r,getRegisteredDomains:a}}var Y=X(),O=Y.registerNap,ee="theme";function W(){var t;const e=window;if(!((t=e.napplet)!=null&&t.theme))throw new Error("window.napplet.theme is unavailable -- runtime did not inject this domain");return e.napplet.theme}function te(){return W().get()}function ne(e){return W().onChanged(e)}O(ee,e=>{});var re="outbox";function ae(){var t;const e=window;if(!((t=e.napplet)!=null&&t.outbox))throw new Error("window.napplet.outbox is unavailable -- runtime did not inject this domain");return e.napplet.outbox}function oe(e,t){return ae().publish(e,t)}O(re,e=>{});var ie="upload";function $(){var t;const e=window;if(!((t=e.napplet)!=null&&t.upload))throw new Error("window.napplet.upload is unavailable -- runtime did not inject this domain");return e.napplet.upload}function le(){return $().info()}function se(e){return $().upload(e)}function de(e){return $().status(e)}O(ie,e=>{});var ue="common";function ce(){var t;const e=window;if(!((t=e.napplet)!=null&&t.common))throw new Error("window.napplet.common is unavailable -- runtime did not inject this domain");return e.napplet.common}function fe(e){return ce().encodeNip19(e)}O(ue,e=>{});const z=32560,R="application/octet-stream",me=new Set(["data","https","blossom","htree","nostr"]);function pe(e,t){return`rom-${e.normalize("NFKD").replace(/[\u0300-\u036f]/g,"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"untitled"}-${t.toLowerCase().slice(0,8)}`}function A(e){var a,o;const t=e.trim(),r=(o=(a=/^([a-z][a-z0-9+.-]*):/i.exec(t))==null?void 0:a[1])==null?void 0:o.toLowerCase();if(!r||!me.has(r)||/[\u0000-\u0020\u007f]/.test(t))return!1;try{return new URL(t),!0}catch{return!1}}function he(e){const t=ge(e),r=we(e.uploads);if(r.length===0)throw new Error("A ROM event requires at least one upload source.");const a=ve(t,r);return{kind:z,content:"",tags:a,created_at:e.createdAt??Math.floor(Date.now()/1e3)}}function ge(e){const t=e.name.trim(),r=e.identifier.trim(),a=e.mime.trim()||R,o=e.sha256.trim().toLowerCase();if(!t)throw new Error("ROM name is required.");if(!r)throw new Error("ROM identifier is required.");if(!Number.isSafeInteger(e.bytes)||e.bytes<=0)throw new Error("ROM byte size must be a positive integer.");if(!/^[a-f0-9]{64}$/.test(o))throw new Error("ROM hash must be a lowercase SHA-256 digest.");return{name:t,identifier:r,mime:a,bytes:e.bytes,sha256:o,platforms:be(e.platforms)}}function be(e){const t=e.map(r=>r.trim().toLowerCase()).filter(Boolean);return[...new Set(t)]}function we(e){var a;const t=[],r=new Set;for(const o of e){const i=((a=o.nip94)==null?void 0:a.filter(([u,s])=>u==="url"&&!!s).map(([,u])=>u))??[];for(const u of[o.url,...o.fallbackUrls??[],...i]){const s=u==null?void 0:u.trim();if(!(!s||r.has(s))){if(!A(s))throw new Error(`Invalid NAP-RESOURCE URL: ${s}`);r.add(s),t.push({url:s,hint:ye(o.rail,s)})}}}return t}function ye(e,t){return t.startsWith("data:")?"inline":e==="blossom"||t.startsWith("blossom:")?"blossom":"resource"}function ve(e,t){return[["d",e.identifier],["name",e.name],...e.platforms.map(r=>["platform",r]),["mime",e.mime],["bytes",String(e.bytes)],["sha256",e.sha256],...t.map(({url:r,hint:a})=>["resource",r,a,String(e.bytes),e.sha256])]}const n={status:l("#status"),statusText:l("#statusText"),dropZone:l("#dropZone"),fileInput:l("#romFile"),fileCard:l("#fileCard"),fileName:l("#fileName"),fileSize:l("#fileSize"),fileMime:l("#fileMime"),fileHash:l("#fileHash"),clearFileButton:l("#clearFileButton"),fileNextButton:l("#fileNextButton"),metadataBackButton:l("#metadataBackButton"),metadataNextButton:l("#metadataNextButton"),metadataFileName:l("#metadataFileName"),metadataFileMeta:l("#metadataFileMeta"),metadataForm:l("#metadataForm"),romTitle:l("#romTitle"),romIdentifier:l("#romIdentifier"),romPlatform:l("#romPlatform"),romMime:l("#romMime"),targetList:l("#targetList"),routeCount:l("#routeCount"),routeHelp:l("#routeHelp"),uploadPolicy:l("#uploadPolicy"),publishBackButton:l("#publishBackButton"),publishRomName:l("#publishRomName"),publishRomMeta:l("#publishRomMeta"),deliverButton:l("#deliverButton"),deliverButtonLabel:l("#deliverButtonLabel"),eventPanel:l("#eventPanel"),eventSize:l("#eventSize"),eventPreview:l("#eventPreview"),publishResult:l("#publishResult"),startOverButton:l("#startOverButton")};let c=null,p="",x=null,y=[],b=[],k="",m=null,D=0,h="",f=!1,L=0,M=0,N=null;function l(e){const t=document.querySelector(e);if(!t)throw new Error(`Missing required element: ${e}`);return t}async function xe(){Ee(),C("file"),d("working","Preparing routes"),await Promise.allSettled([Te(),qe()]),v(),y.length>0&&d("idle","Select a ROM")}function Ee(){n.fileInput.addEventListener("click",()=>{n.fileInput.value=""}),n.fileInput.addEventListener("change",()=>{var t;const e=(t=n.fileInput.files)==null?void 0:t[0];e&&J(e)}),n.clearFileButton.addEventListener("click",F),n.metadataForm.addEventListener("input",Pe),n.metadataForm.addEventListener("invalid",Le,!0),n.fileNextButton.addEventListener("click",Ne),n.metadataBackButton.addEventListener("click",()=>C("file",!0)),n.metadataNextButton.addEventListener("click",Oe),n.publishBackButton.addEventListener("click",()=>C("metadata",!0)),n.deliverButton.addEventListener("click",()=>void Ie()),n.startOverButton.addEventListener("click",F),n.dropZone.addEventListener("dragenter",Ce),n.dropZone.addEventListener("dragover",Re),n.dropZone.addEventListener("dragleave",Me),n.dropZone.addEventListener("drop",ke),window.addEventListener("beforeunload",()=>N==null?void 0:N.close())}function Ce(e){e.preventDefault(),!f&&(M+=1,n.dropZone.classList.add("is-dragging"))}function Re(e){e.preventDefault(),!f&&e.dataTransfer&&(e.dataTransfer.dropEffect="copy")}function Me(e){e.preventDefault(),M=Math.max(0,M-1),M===0&&n.dropZone.classList.remove("is-dragging")}function ke(e){var r;if(e.preventDefault(),M=0,n.dropZone.classList.remove("is-dragging"),f)return;const t=(r=e.dataTransfer)==null?void 0:r.files[0];t&&J(t)}async function J(e){if(f)return;const t=++L;if(Se(e)){c=e,B(),_(e,"Calculating fingerprint…"),g(!0),d("working","Hashing locally");try{const r=await crypto.subtle.digest("SHA-256",await e.arrayBuffer());if(t!==L)return;p=nt(r),Be(e,p),_(e,p),d("ok","Fingerprint ready")}catch(r){F(),d("error",P(r,"Could not read the ROM bytes."))}finally{t===L&&g(!1)}}}function Se(e){return e.size<=0?(d("error","Choose a non-empty ROM file"),!1):x!=null&&x.maxBytes&&e.size>x.maxBytes?(d("error",`ROM exceeds the shell limit of ${U(x.maxBytes)}`),!1):!0}function Be(e,t){const r=Q(e.name);n.romTitle.value=r,n.romIdentifier.value=pe(r,t),n.romPlatform.value=et(e.name),n.romMime.value=e.type||R}function _(e,t){n.dropZone.hidden=!0,n.fileCard.hidden=!1,n.fileName.textContent=e.name,n.fileSize.textContent=U(e.size),n.fileMime.textContent=e.type||R,n.fileHash.textContent=t,S()}function F(){var e;L+=1,c=null,p="",n.fileInput.value="",n.dropZone.hidden=!1,n.fileCard.hidden=!0,n.metadataForm.reset(),(e=n.metadataForm.querySelector(".advanced-fields"))==null||e.removeAttribute("open"),n.romMime.value=R,B(),g(!1),C("file"),d("idle","Select a ROM")}function Pe(){b.length>0||m||h?B():v(),S()}function Le(e){if(!(e.target instanceof HTMLElement))return;const t=e.target.closest("details");t&&(t.open=!0)}function Ne(){!c||!p||f||(S(),C("metadata",!0),d("idle","Review ROM metadata"))}function Oe(){f||!n.metadataForm.reportValidity()||(S(),C("publish",!0),d("idle","Choose upload targets"))}function S(){if(!c)return;const e=U(c.size),t=n.romPlatform.value.trim()||"platform not set",r=n.romTitle.value.trim()||Q(c.name),a=T().length;n.metadataFileName.textContent=c.name,n.metadataFileMeta.textContent=`${e} · SHA-256 ${p.slice(0,12)}…`,n.publishRomName.textContent=r,n.publishRomMeta.textContent=`${t} · ${e} · ${a} route${a===1?"":"s"} selected`}async function Te(){var e;try{x=await le();const t=x.rails.filter(({enabled:a})=>a),r=t.some(({rail:a})=>a==="blossom")?"blossom":String(((e=t[0])==null?void 0:e.rail)??"");y=t.map(a=>{var o;return{key:String(a.rail),rail:a.rail,label:Qe(a.rail),detail:(o=a.returns)!=null&&o.length?a.returns.join(" · "):"Shell-managed storage",selected:String(a.rail)===r,state:"ready"}}),y.length===0&&Z("No named rails were disclosed.")}catch{Z("Rail details are private; the shell will choose.")}E()}function Z(e){y=[{key:"shell-managed",label:"Shell managed",detail:"Default upload policy",selected:!0,state:"ready"}],n.routeHelp.textContent=e}function E(e){var a;const t=y.map(o=>Ue(o));n.targetList.replaceChildren(...t),e&&((a=[...n.targetList.querySelectorAll('input[name="uploadTarget"]')].find(({value:i})=>i===e))==null||a.focus());const r=T().length;n.routeCount.textContent=`${r} selected`,S(),v()}function Ue(e){const t=document.createElement("label");t.className="target-card",t.dataset.state=e.state;const r=document.createElement("input");r.type="checkbox",r.name="uploadTarget",r.value=e.key,r.checked=e.selected,r.disabled=f||e.state==="uploading",r.addEventListener("change",()=>{e.selected=r.checked,B(e.key)});const a=document.createElement("span");a.className="target-mark",a.textContent=Xe(e.rail),a.setAttribute("aria-hidden","true");const o=document.createElement("span");o.className="target-copy";const i=document.createElement("strong");i.textContent=e.label;const u=document.createElement("small");u.textContent=e.detail,o.append(i,u);const s=document.createElement("span");return s.className="target-state",s.textContent=Ye(e.state),t.append(r,a,o,s),t}async function Ie(){if(!(f||h)){if(m&&b.length>0){await j();return}await Fe()&&await j()}}async function Fe(){if(!c||!p||!n.metadataForm.reportValidity())return!1;const e=T();if(e.length===0)return d("warn","Choose at least one route"),!1;B(),g(!0),d("working",`Uploading to ${e.length} route${e.length===1?"":"s"}`);const t=[];for(const a of e){const o=await $e(a,c,p);o&&t.push(o)}if(b=t,t.length!==e.length)return b=[],g(!1),E(),n.eventPreview.textContent="Every selected route must complete before this event can be published.",d("error","Upload incomplete"),n.uploadPolicy.textContent="Resolve or deselect failed routes, then upload again.",!1;if(!t.some(a=>H(a).length>0)){d("working","Preparing portable inline bytes");try{k=await je(c,n.romMime.value.trim()||R)}catch(a){return b=[],g(!1),E(),d("error","Could not prepare portable bytes"),n.uploadPolicy.textContent=P(a,"Inline resource encoding failed."),!1}}g(!1),E(),D=Math.floor(Date.now()/1e3),De();const r=t.some(a=>q(a).some(o=>!A(o)));return k?(d("warn","Inline bytes prepared"),n.uploadPolicy.textContent="The shell returned only local pointers, so the event embeds the bytes; large ROMs may exceed relay limits."):r?(d("warn","Private locators omitted"),n.uploadPolicy.textContent="Portable sources are ready; shell-local pointers were omitted from the event."):(d("ok",`${t.length} route${t.length===1?"":"s"} ready`),n.uploadPolicy.textContent="All selected routes expose portable URLs for the canonical ROM bytes."),!!m}async function $e(e,t,r){e.state="uploading",e.detail="Awaiting shell consent",E();try{const a=await se({...e.rail?{rail:e.rail}:{},data:t,mimeType:n.romMime.value.trim()||R,filename:t.name,caption:`ROM: ${n.romTitle.value.trim()}`,noTransform:!0,metadata:{kind:z,sha256:r}}),o=await ze(a,e);Ae(o,t,r),e.result=o,e.state="complete";const i=Ge(o);return e.detail=o.sha256?i:`${i} · stored hash not attested`,o}catch(a){return e.state="failed",e.detail=P(a,"Upload failed"),null}}async function ze(e,t){let r=e;for(let a=0;!Je(r.status);a+=1){if(a>=60)throw new Error("Upload status timed out");t.detail=We(r),E(),await rt(500),r=await de(r.uploadId)}return r}function Ae(e,t,r){if(!e.ok||e.status!=="complete")throw new Error(e.error||`Upload ended as ${e.status}`);const a=q(e);if(a.length===0)throw new Error("Upload completed without a retrieval URL");if(a.some(i=>!Ze(i)))throw new Error("Upload returned an invalid retrieval URL");if([e.sha256,e.originalSha256].filter(i=>!!i).map(i=>i.toLowerCase()).some(i=>i!==r))throw new Error("Uploaded bytes do not match the local ROM hash");if(e.size!==void 0&&e.size!==t.size)throw new Error("Uploaded byte size does not match the local ROM")}function De(){if(!(!c||b.length===0)){if(!K()){m=null,n.eventPreview.dataset.ready="false",n.eventPreview.textContent="Complete the required event metadata.",v();return}try{const e=b.flatMap(t=>{const r=H(t);return r[0]?[{rail:String(t.rail),url:r[0],fallbackUrls:r.slice(1)}]:[]});k&&e.push({rail:"inline",url:k,fallbackUrls:[]}),m=he({name:n.romTitle.value,identifier:n.romIdentifier.value,platforms:Ke(n.romPlatform.value),mime:n.romMime.value,bytes:c.size,sha256:p,uploads:e,createdAt:D}),n.eventPanel.hidden=!1,n.eventSize.textContent=`${U(tt(m))} unsigned`,n.eventPreview.textContent=He(m),n.eventPreview.dataset.ready="true"}catch(e){m=null,n.eventPanel.hidden=!0,n.eventSize.textContent="—",n.eventPreview.dataset.ready="false",n.eventPreview.textContent=P(e,"Complete the event metadata.")}v()}}async function j(){var e;if(!(!m||h)){g(!0),d("working","Requesting signature");try{const t=await oe(m);if(!t.ok)throw new Error(t.error||"The shell could not publish this event");if(h=((e=t.event)==null?void 0:e.id)||t.eventId||"",!h)throw new Error("Published event returned without an id");const r=await _e(t);n.publishResult.hidden=!1,n.publishResult.dataset.kind=r?"ok":"warn",n.publishResult.textContent=r?`Published · ${r}`:"Published · NIP-19 pointer unavailable",n.startOverButton.hidden=!1,n.uploadPolicy.textContent="Published through the shell-owned outbox.",d(r?"ok":"warn",r?"ROM event published":"Published without a pointer")}catch(t){n.publishResult.hidden=!1,n.publishResult.dataset.kind="error",n.publishResult.textContent=P(t,"Publish failed"),d("error","Publish failed")}finally{g(!1)}}}function B(e){b=[],k="",m=null,D=0,h="",n.publishResult.hidden=!0,n.startOverButton.hidden=!0,n.eventPanel.hidden=!0,n.eventSize.textContent="—",n.eventPreview.dataset.ready="false",n.eventPreview.textContent="Upload the ROM to forge a publishable event.",n.uploadPolicy.textContent="One action uploads every selected route, then signs and publishes the event.";for(const t of y)t.state="ready",t.result=void 0,t.detail=t.rail?"Shell-managed storage":"Default upload policy";y.length>0&&E(e),v()}function g(e){f=e,document.documentElement.dataset.busy=String(e),n.fileInput.disabled=e,n.clearFileButton.disabled=e;for(const t of n.metadataForm.elements)t instanceof HTMLInputElement&&(t.disabled=e);for(const t of n.targetList.querySelectorAll('input[name="uploadTarget"]'))t.disabled=e;v()}function v(){const e=K();n.fileNextButton.disabled=f||!c||!p,n.metadataBackButton.disabled=f,n.metadataNextButton.disabled=f||!c||!p,n.publishBackButton.disabled=f||!!h,n.deliverButton.disabled=f||!c||!p||!e||T().length===0||!!h,n.deliverButtonLabel.textContent=h?"Published":m&&b.length>0?"Retry publish":"Upload & publish ROM"}function K(){return n.metadataForm.querySelector(":invalid")===null}function T(){return y.filter(({selected:e})=>e)}function d(e,t){n.status.dataset.kind=e,n.statusText.textContent=t}function C(e,t=!1){var o;const r=["file","metadata","publish"],a=r.indexOf(e);for(const i of document.querySelectorAll("[data-stage]")){const u=i.dataset.stage,s=r.indexOf(u);i.dataset.state=s<a?"done":s===a?"active":"idle",s===a?i.setAttribute("aria-current","step"):i.removeAttribute("aria-current")}for(const i of document.querySelectorAll("[data-step-panel]"))i.hidden=i.dataset.stepPanel!==e;t&&((o=document.querySelector(`[data-step-panel="${e}"] [data-step-heading]`))==null||o.focus()),v()}async function qe(){var e;if((e=window.napplet)!=null&&e.theme)try{V(await te()),N=ne(V)}catch{}}function V(e){const t=document.documentElement;t.style.setProperty("--shell-background",e.colors.background),t.style.setProperty("--shell-text",e.colors.text),t.style.setProperty("--shell-primary",e.colors.primary),t.style.colorScheme=Ve(e.colors.background),t.dataset.shellTheme="true"}function He(e){return JSON.stringify(e,null,2)}async function _e(e){var u,s;const t=e.event,r=(t==null?void 0:t.id)||e.eventId||h,a=Object.entries(e.relays??{}).filter(([,w])=>w).map(([w])=>w),o=(s=(u=t==null?void 0:t.tags.find(([w,I])=>w==="d"&&(I==null?void 0:I.trim())))==null?void 0:u[1])==null?void 0:s.trim();if(t&&o){const w=await G({type:"naddr",identifier:o,pubkey:t.pubkey,kind:t.kind,...a.length>0?{relays:a}:{}});if(w)return w}return await G({type:"nevent",eventId:r,...a.length>0?{relays:a}:{},...t!=null&&t.pubkey?{author:t.pubkey}:{},kind:(t==null?void 0:t.kind)??z})}async function G(e){try{const t=await fe(e);return t.ok?t.value:void 0}catch{return}}function q(e){var r;const t=((r=e.nip94)==null?void 0:r.filter(([a,o])=>a==="url"&&!!o).map(([,a])=>a))??[];return[e.url,...e.fallbackUrls??[],...t].filter(a=>!!a).map(a=>a.trim())}function H(e){return[...new Set(q(e).filter(A))]}function Ze(e){const t=e.trim();if(!/^[a-z][a-z0-9+.-]*:/i.test(t)||/[\u0000-\u0020\u007f]/.test(t))return!1;try{return new URL(t),!0}catch{return!1}}function je(e,t){const r=e.type===t?e:e.slice(0,e.size,t);return new Promise((a,o)=>{const i=new FileReader;i.addEventListener("load",()=>{typeof i.result=="string"?a(i.result):o(new Error("Could not encode the ROM bytes"))},{once:!0}),i.addEventListener("error",()=>o(i.error??new Error("Could not encode the ROM bytes")),{once:!0}),i.readAsDataURL(r)})}function Ve(e){const t=e.trim().replace(/^#/,""),r=t.length===3?[...t].map(u=>`${u}${u}`).join(""):t;if(!/^[a-f0-9]{6}$/i.test(r))return"dark";const a=Number.parseInt(r.slice(0,2),16),o=Number.parseInt(r.slice(2,4),16),i=Number.parseInt(r.slice(4,6),16);return(a*.2126+o*.7152+i*.0722)/255>.55?"light":"dark"}function Ge(e){const t=H(e).length;return t===0?"Local-only result · inline fallback":`${t} portable source${t===1?"":"s"} ready`}function We(e){return"bytesSent"in e&&e.bytesTotal?`Uploading · ${Math.min(100,Math.round((e.bytesSent??0)/e.bytesTotal*100))}%`:e.status==="pending"?"Queued by shell":"Uploading bytes"}function Je(e){return e==="complete"||e==="failed"||e==="cancelled"}function Ke(e){return e.split(/[,;]/).map(t=>t.trim()).filter(Boolean)}function Qe(e){return e==="nip96"?"NIP-96":e==="blossom"?"Blossom":String(e).replace(/[-_]+/g," ").replace(/\b\w/g,t=>t.toUpperCase())}function Xe(e){return e==="nip96"?"N96":e==="blossom"?"BLO":e?String(e).slice(0,3).toUpperCase():"AUTO"}function Ye(e){return{ready:"Ready",uploading:"Working",complete:"Stored",failed:"Failed"}[e]}function Q(e){return e.replace(/\.[^.]+$/,"").replace(/[-_]+/g," ").replace(/\s+/g," ").trim()||"Untitled ROM"}function et(e){var a;const t=((a=e.split(".").pop())==null?void 0:a.toLowerCase())??"";return{gb:"gb",gbc:"gbc",gba:"gba",nes:"nes",sfc:"snes",smc:"snes",md:"genesis",gen:"genesis",iso:"psx",chd:"arcade",rom:"",bin:""}[t]??""}function U(e){if(e<1024)return`${e} B`;const t=["KB","MB","GB"];let r=e/1024,a=t[0];for(let o=1;r>=1024&&o<t.length;o+=1)r/=1024,a=t[o];return`${r>=10?r.toFixed(1):r.toFixed(2)} ${a}`}function tt(e){return new TextEncoder().encode(JSON.stringify(e)).byteLength}function nt(e){return[...new Uint8Array(e)].map(t=>t.toString(16).padStart(2,"0")).join("")}function rt(e){return new Promise(t=>window.setTimeout(t,e))}function P(e,t){return e instanceof Error&&e.message?e.message:t}xe();
</script>
    <style>:root{color-scheme:dark light;font-family:Avenir Next,Avenir,Segoe UI Variable,sans-serif;--shell-background: #0b0e0c;--shell-text: #edf1df;--shell-primary: #d7ff65;--background: var(--shell-background);--text: var(--shell-text);--accent: var(--shell-primary);--accent-hot: color-mix(in srgb, var(--shell-primary) 68%, var(--shell-text));--surface: color-mix(in srgb, var(--background) 88%, var(--text) 12%);--surface-raised: color-mix(in srgb, var(--background) 82%, var(--text) 18%);--surface-soft: color-mix(in srgb, var(--background) 93%, var(--text) 7%);--border: color-mix(in srgb, var(--text) 18%, transparent);--border-strong: color-mix(in srgb, var(--text) 31%, transparent);--muted: color-mix(in srgb, var(--text) 58%, var(--background));--success: color-mix(in srgb, var(--shell-primary) 74%, var(--shell-text));--warning: color-mix(in srgb, var(--shell-primary) 56%, var(--shell-text));--danger: color-mix(in srgb, var(--shell-primary) 38%, var(--shell-text));--shadow: 0 24px 70px rgb(0 0 0 / 32%);--radius: 18px;--mono: "Iosevka", "Berkeley Mono", "Cascadia Mono", "SFMono-Regular", monospace;--napplet-text-selection: none;background:var(--background);color:var(--text);font-synthesis:none;text-rendering:optimizeLegibility}*{box-sizing:border-box}html{min-width:0;background:var(--background)}body{min-width:0;margin:0;overflow-x:clip}:where(body,body *){-webkit-user-select:var(--napplet-text-selection, none);user-select:var(--napplet-text-selection, none)}:where(input,select,[contenteditable],[data-napplet-select=text]){-webkit-user-select:text;user-select:text}:where([data-napplet-select=all]){-webkit-user-select:all;user-select:all}button,input{font:inherit}button,label{-webkit-tap-highlight-color:transparent}button{border:0}button:focus-visible,input:focus-visible,.drop-zone:focus-within{outline:3px solid color-mix(in srgb,var(--accent) 52%,transparent);outline-offset:3px}[hidden]{display:none!important}.visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.app-shell{position:relative;isolation:isolate;padding:clamp(.7rem,1.8vw,1.35rem);background:radial-gradient(circle at 12% 3%,color-mix(in srgb,var(--accent) 9%,transparent),transparent 30rem),radial-gradient(circle at 96% 76%,color-mix(in srgb,var(--accent-hot) 6%,transparent),transparent 26rem),var(--background)}.ambient-grid{position:absolute;z-index:-1;top:0;right:0;bottom:0;left:0;pointer-events:none;opacity:.2;background-image:linear-gradient(var(--border) 1px,transparent 1px),linear-gradient(90deg,var(--border) 1px,transparent 1px);background-size:36px 36px;-webkit-mask-image:linear-gradient(to bottom,black,transparent 80%);mask-image:linear-gradient(to bottom,black,transparent 80%)}.workspace{container-type:inline-size;width:min(1180px,100%);margin:0 auto}.section-index,.micro-label,.count-badge,.drop-meta,.boundary-strip,.target-state{font-family:var(--mono);text-transform:uppercase;letter-spacing:.1em}h2,p{margin-top:0}.status{display:flex;align-items:center;gap:.65rem;width:fit-content;max-width:100%;min-height:1.7rem;margin:.55rem 0 0 auto;padding:.2rem .15rem;color:var(--muted);font-family:var(--mono);font-size:.6rem;font-weight:700;letter-spacing:.035em}.status-light{flex:0 0 auto;width:.45rem;height:.45rem;border-radius:50%;background:currentColor}.status[data-kind=working]{color:var(--warning)}.status[data-kind=working] .status-light{animation:blink .85s ease-in-out infinite alternate}.status[data-kind=ok]{color:var(--success)}.status[data-kind=warn]{color:var(--warning)}.status[data-kind=error]{color:var(--danger)}.stage-line{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));width:min(46rem,100%);margin:0 auto 1rem;padding:0;list-style:none;border:1px solid var(--border);border-radius:999px;background:var(--surface-soft);overflow:hidden;animation:reveal .52s 80ms cubic-bezier(.2,.75,.2,1) both}.stage-line li{position:relative;display:flex;justify-content:center;align-items:center;gap:.55rem;min-width:0;padding:.55rem .8rem;color:var(--muted);font-family:var(--mono);font-size:.7rem;font-weight:750;text-transform:uppercase;letter-spacing:.08em}.stage-line li+li:before{position:absolute;left:0;width:1px;height:45%;background:var(--border);content:""}.stage-line li span{color:color-mix(in srgb,var(--muted) 70%,transparent)}.stage-line li[data-state=active]{background:color-mix(in srgb,var(--accent) 8%,transparent);color:var(--accent)}.stage-line li[data-state=active] span,.stage-line li[data-state=done]{color:var(--accent)}.wizard-shell{width:min(46rem,100%);margin:0 auto}.wizard-panel{min-width:0;border:1px solid var(--border);border-radius:var(--radius);background:color-mix(in srgb,var(--surface) 96%,transparent);padding:clamp(.9rem,2.5cqi,1.4rem);box-shadow:var(--shadow);animation:reveal .24s cubic-bezier(.2,.75,.2,1) both}.section-heading{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;margin-bottom:.8rem}.section-index{margin-bottom:.4rem;color:var(--accent-hot);font-size:.62rem;font-weight:800}h2{margin-bottom:0;font-size:clamp(1.12rem,2cqi,1.45rem);line-height:1.15;letter-spacing:-.025em}.micro-label,.count-badge{flex:0 0 auto;border:1px solid var(--border);border-radius:999px;background:var(--surface-soft);padding:.4rem .55rem;color:var(--muted);font-size:.55rem;font-weight:800}.count-badge{color:var(--accent)}.drop-zone{position:relative;display:grid;place-items:center;min-height:clamp(8.5rem,20cqi,10.5rem);border:1px dashed var(--border-strong);border-radius:calc(var(--radius) - 4px);background:linear-gradient(135deg,transparent 0 49%,color-mix(in srgb,var(--text) 4%,transparent) 50% 51%,transparent 52%) 0 0 / 18px 18px,var(--surface-soft);padding:1rem;text-align:center;cursor:pointer;overflow:hidden;transition:border-color .18s ease,background-color .18s ease,transform .18s ease}.drop-zone:after{position:absolute;top:0;right:0;bottom:0;left:0;border:1px solid transparent;border-radius:inherit;pointer-events:none;content:"";transition:border-color .18s ease,box-shadow .18s ease}.drop-zone:hover,.drop-zone.is-dragging{border-color:var(--accent);background-color:color-mix(in srgb,var(--accent) 5%,var(--surface-soft))}.drop-zone.is-dragging{transform:scale(.992)}.drop-zone.is-dragging:after{border-color:var(--accent);box-shadow:inset 0 0 50px color-mix(in srgb,var(--accent) 12%,transparent)}.cartridge-mark{position:relative;display:grid;place-items:center;width:clamp(4.1rem,9cqi,5rem);aspect-ratio:1 / .82;margin-bottom:.65rem;border:1px solid color-mix(in srgb,var(--accent) 55%,var(--border));border-radius:.45rem .45rem .85rem .85rem;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 13%,transparent),transparent 58%),var(--surface-raised);box-shadow:inset 0 0 0 6px var(--surface-soft),0 14px 35px #00000038;color:var(--accent);font-family:var(--mono)}.cartridge-notch{position:absolute;top:-1px;left:50%;width:42%;height:.55rem;border-right:1px solid var(--border-strong);border-bottom:1px solid var(--border-strong);border-left:1px solid var(--border-strong);border-radius:0 0 .35rem .35rem;transform:translate(-50%)}.cartridge-label{border:1px solid color-mix(in srgb,var(--accent) 45%,transparent);padding:.45rem .7rem;font-size:.74rem;font-weight:850;letter-spacing:.16em}.cartridge-pins{position:absolute;right:16%;bottom:.52rem;left:16%;height:.22rem;background:repeating-linear-gradient(90deg,var(--accent) 0 3px,transparent 3px 7px);opacity:.55}.drop-copy{display:grid;gap:.35rem;color:var(--muted);font-size:.88rem}.drop-copy strong{color:var(--text);font-size:clamp(1.1rem,2.5cqi,1.4rem);letter-spacing:-.025em}.text-link{color:var(--accent);text-decoration:underline;text-decoration-color:color-mix(in srgb,var(--accent) 45%,transparent);text-underline-offset:.18em}.drop-meta{margin-top:.6rem;color:color-mix(in srgb,var(--muted) 75%,transparent);font-size:.52rem}.file-card{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;gap:.85rem;margin-top:.8rem;border:1px solid color-mix(in srgb,var(--accent) 26%,var(--border));border-radius:.8rem;background:color-mix(in srgb,var(--accent) 4%,var(--surface-soft));padding:.8rem}.drop-zone[hidden]+.file-card{margin-top:0}.file-icon{display:grid;place-items:center;width:3rem;aspect-ratio:1;border:1px solid var(--border-strong);border-radius:.55rem;background:var(--surface-raised);color:var(--accent);font-family:var(--mono);font-size:.55rem;font-weight:850}.file-summary{display:grid;min-width:0;gap:.25rem}.file-summary strong,.file-summary span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-summary span{color:var(--muted);font-family:var(--mono);font-size:.62rem}.quiet-action,.icon-button{color:var(--muted);cursor:pointer}.quiet-action{padding:.5rem;font-family:var(--mono);font-size:.64rem;text-decoration:underline;text-underline-offset:.2em}.quiet-action:hover{color:var(--accent)}.icon-button{display:grid;place-items:center;width:2rem;height:2rem;border-radius:50%;background:transparent;font-size:1.25rem}.icon-button:hover{background:var(--surface-raised);color:var(--danger)}.hash-row{grid-column:1 / -1;display:grid;grid-template-columns:auto minmax(0,1fr);gap:.7rem;border-top:1px solid var(--border);padding-top:.7rem;color:var(--muted);font-family:var(--mono);font-size:.58rem}.hash-row code{min-width:0;color:color-mix(in srgb,var(--text) 82%,var(--muted));overflow-wrap:anywhere;text-align:right}.metadata-form{margin:0}.form-note{margin-bottom:.75rem;color:var(--muted);font-family:var(--mono);font-size:.56rem;text-align:right}.step-context{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:.8rem;margin-bottom:1rem;border:1px solid var(--border);border-radius:.8rem;background:var(--surface-soft);padding:.75rem}.advanced-fields{margin-top:.85rem;border:1px solid var(--border);border-radius:.75rem;background:var(--surface-soft)}.advanced-fields summary{padding:.75rem;color:var(--muted);font-family:var(--mono);font-size:.62rem;font-weight:750;cursor:pointer}.advanced-fields[open] summary{border-bottom:1px solid var(--border)}.advanced-fields .field-grid{padding:.8rem}.field-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem}.field{display:grid;min-width:0;gap:.42rem;color:var(--muted);font-family:var(--mono);font-size:.62rem;font-weight:700;letter-spacing:.02em}.field-wide{grid-column:1 / -1}.field input{width:100%;min-width:0;border:1px solid var(--border);border-radius:.65rem;background:var(--surface-soft);padding:.72rem .78rem;color:var(--text);font-family:Avenir Next,Avenir,Segoe UI Variable,sans-serif;font-size:.88rem;line-height:1.35;transition:border-color .16s ease,background-color .16s ease}.field input:hover,.field input:focus{border-color:color-mix(in srgb,var(--accent) 55%,var(--border));background:color-mix(in srgb,var(--accent) 3%,var(--surface-soft))}.target-fieldset{min-width:0;margin:0;padding:0;border:0}.target-list{display:grid;min-width:0;gap:.55rem}.target-card{display:grid;grid-template-columns:auto auto minmax(0,1fr) auto;align-items:center;gap:.7rem;min-width:0;border:1px solid var(--border);border-radius:.78rem;background:var(--surface-soft);padding:.7rem;cursor:pointer;transition:border-color .16s ease,background-color .16s ease,transform .16s ease}.target-card:hover{border-color:var(--border-strong);transform:translateY(-1px)}.target-card:has(input:checked){border-color:color-mix(in srgb,var(--accent) 48%,var(--border));background:color-mix(in srgb,var(--accent) 5%,var(--surface-soft))}.target-card[data-state=complete]{border-color:color-mix(in srgb,var(--success) 45%,var(--border))}.target-card[data-state=failed]{border-color:color-mix(in srgb,var(--danger) 52%,var(--border))}.target-card input{width:1.05rem;height:1.05rem;margin:0;accent-color:var(--accent)}.target-mark{display:grid;place-items:center;width:2.55rem;height:2.55rem;border:1px solid var(--border);border-radius:.6rem;background:var(--surface-raised);color:var(--accent);font-family:var(--mono);font-size:.56rem;font-weight:850;letter-spacing:.035em}.target-copy{display:grid;min-width:0;gap:.18rem}.target-copy strong,.target-copy small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.target-copy strong{font-size:.84rem}.target-copy small{color:var(--muted);font-family:var(--mono);font-size:.55rem}.target-state{color:var(--muted);font-size:.5rem;font-weight:800}.target-card[data-state=uploading] .target-state{color:var(--warning)}.target-card[data-state=complete] .target-state{color:var(--success)}.target-card[data-state=failed] .target-state{color:var(--danger)}.target-skeleton{display:grid;gap:.5rem;border:1px solid var(--border);border-radius:.78rem;padding:1rem}.target-skeleton span{width:68%;height:.55rem;border-radius:999px;background:var(--surface-raised);animation:shimmer 1.2s ease-in-out infinite alternate}.target-skeleton span+span{width:42%}.route-help,.policy-note{color:var(--muted);font-size:.72rem;line-height:1.5}.route-help{margin:.85rem 0 1rem}.policy-note{margin:.65rem 0 0;text-align:center}.step-actions{display:grid;grid-template-columns:minmax(6.5rem,auto) minmax(0,1fr);gap:.7rem;margin-top:1rem}.step-actions-forward{grid-template-columns:minmax(0,1fr)}.primary-button,.secondary-button,.restart-button{width:100%;cursor:pointer;transition:transform .16s ease,filter .16s ease,opacity .16s ease}.primary-button{display:flex;justify-content:space-between;align-items:center;min-height:3.15rem;border-radius:.8rem;background:var(--accent);padding:.8rem 1rem;color:var(--background);font-weight:800;letter-spacing:-.015em}.secondary-button,.restart-button{min-height:3.15rem;border:1px solid var(--border-strong);border-radius:.8rem;background:var(--surface-soft);padding:.8rem 1rem;color:var(--text);font-weight:750}.restart-button{margin-top:.7rem}.primary-button:hover:not(:disabled),.secondary-button:hover:not(:disabled),.restart-button:hover:not(:disabled){filter:brightness(1.06);transform:translateY(-2px)}.primary-button:disabled,.secondary-button:disabled,.restart-button:disabled,.icon-button:disabled{cursor:not-allowed;opacity:.38}.event-details{margin-top:.8rem;border:1px solid var(--border);border-radius:.8rem;background:var(--surface-soft);overflow:hidden}.event-details summary{padding:.75rem;color:var(--muted);font-family:var(--mono);font-size:.62rem;font-weight:750;cursor:pointer}.event-size{color:color-mix(in srgb,var(--accent) 72%,var(--text))}.event-preview{min-height:7.5rem;max-height:19rem;margin:0;border:0;border-top:1px solid var(--border);background:linear-gradient(to bottom,color-mix(in srgb,var(--accent) 3%,transparent),transparent 4rem),color-mix(in srgb,var(--background) 94%,var(--text) 6%);padding:.9rem;overflow:auto;color:color-mix(in srgb,var(--text) 76%,var(--muted));font-family:var(--mono);font-size:.62rem;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere}.event-preview[data-ready=true]{color:color-mix(in srgb,var(--accent) 68%,var(--text))}.publish-result{display:block;margin-top:.65rem;border:1px solid color-mix(in srgb,var(--success) 40%,var(--border));border-radius:.7rem;background:color-mix(in srgb,var(--success) 7%,var(--surface-soft));padding:.75rem;color:var(--success);font-family:var(--mono);font-size:.6rem;overflow-wrap:anywhere}.publish-result[data-kind=error]{border-color:color-mix(in srgb,var(--danger) 45%,var(--border));background:color-mix(in srgb,var(--danger) 7%,var(--surface-soft));color:var(--danger)}.publish-result[data-kind=warn]{border-color:color-mix(in srgb,var(--warning) 45%,var(--border));background:color-mix(in srgb,var(--warning) 7%,var(--surface-soft));color:var(--warning)}.boundary-strip{display:flex;flex-wrap:wrap;justify-content:center;gap:.7rem 1.6rem;padding:.9rem .5rem .1rem;color:color-mix(in srgb,var(--muted) 72%,transparent);font-size:.52rem}.boundary-strip span{position:relative}.boundary-strip span+span:before{position:absolute;top:50%;left:-.9rem;width:.22rem;height:.22rem;border-radius:50%;background:var(--accent-hot);content:"";transform:translateY(-50%)}:root[data-busy=true] .ambient-grid{opacity:.28}@container (max-width: 580px){.field-grid{grid-template-columns:minmax(0,1fr)}.field-wide{grid-column:auto}}@container (max-width: 410px){.app-shell{padding:.65rem}.wizard-panel{padding:.75rem}.stage-line li{gap:.35rem;padding-inline:.45rem;font-size:.6rem}.stage-line li span{display:none}.section-heading{align-items:flex-start}.micro-label{display:none}.drop-zone{min-height:8rem;padding:1rem}.step-actions{grid-template-columns:minmax(0,1fr)}.primary-button,.secondary-button,.restart-button{min-height:2.75rem}.file-card{grid-template-columns:auto minmax(0,1fr) auto}.quiet-action{display:none}.target-card{grid-template-columns:auto auto minmax(0,1fr)}.target-state{grid-column:2 / -1}.boundary-strip{display:none}}@keyframes reveal{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes blink{to{opacity:.3;box-shadow:0 0 12px currentColor}}@keyframes shimmer{0%{opacity:.35}to{opacity:.8}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}
</style>
    <meta name="napplet-type" content="rom-foundry">
    <meta name="napplet-requires" content="upload,outbox,common">
  </head>
  <body>
    <main class="app-shell">
      <div class="ambient-grid" aria-hidden="true"></div>
      <section class="workspace" aria-label="ROM publishing workflow">
        <ol class="stage-line" aria-label="Publishing workflow">
          <li data-stage="file" data-state="active" aria-current="step"><span>01</span> File</li>
          <li data-stage="metadata"><span>02</span> Metadata</li>
          <li data-stage="publish"><span>03</span> Publish</li>
        </ol>

        <div class="wizard-shell">
          <section class="wizard-panel" data-step-panel="file" aria-labelledby="file-step-title">
            <div class="section-heading">
              <div>
                <p class="section-index">01 / File</p>
                <h2 id="file-step-title" data-step-heading tabindex="-1">Choose the canonical bytes</h2>
              </div>
              <span class="micro-label">Local hash only</span>
            </div>

            <label id="dropZone" class="drop-zone" for="romFile" data-drop-zone>
              <input id="romFile" class="visually-hidden" type="file" />
              <span class="cartridge-mark" aria-hidden="true">
                <span class="cartridge-notch"></span>
                <span class="cartridge-label">ROM</span>
                <span class="cartridge-pins"></span>
              </span>
              <span class="drop-copy">
                <strong>Drop a ROM here</strong>
                <span>or <span class="text-link">select a file</span> from this device</span>
              </span>
              <span class="drop-meta">Any platform · original bytes preserved</span>
            </label>

            <article id="fileCard" class="file-card" hidden aria-label="Selected ROM">
              <div class="file-icon" aria-hidden="true">.ROM</div>
              <div class="file-summary">
                <strong id="fileName">No file</strong>
                <span><span id="fileSize">0 B</span> · <span id="fileMime">unknown</span></span>
              </div>
              <label class="quiet-action" for="romFile">Replace</label>
              <button id="clearFileButton" class="icon-button" type="button" aria-label="Remove selected ROM">
                ×
              </button>
              <div class="hash-row">
                <span>SHA-256</span>
                <code id="fileHash" data-napplet-select="all">pending</code>
              </div>
            </article>

            <div class="step-actions step-actions-forward">
              <button id="fileNextButton" class="primary-button" type="button" disabled>
                <span>Continue to metadata</span>
                <span aria-hidden="true">→</span>
              </button>
            </div>
          </section>

          <section
            class="wizard-panel"
            data-step-panel="metadata"
            aria-labelledby="metadata-step-title"
            hidden
          >
            <div class="section-heading">
              <div>
                <p class="section-index">02 / Metadata</p>
                <h2 id="metadata-step-title" data-step-heading tabindex="-1">Describe the ROM</h2>
              </div>
              <span class="micro-label">Kind 32560</span>
            </div>

            <div class="step-context" aria-label="Selected ROM summary">
              <div class="file-icon" aria-hidden="true">.ROM</div>
              <div class="file-summary">
                <strong id="metadataFileName">Selected ROM</strong>
                <span id="metadataFileMeta">Fingerprint ready</span>
              </div>
            </div>

            <form id="metadataForm" class="metadata-form" novalidate>
              <p class="form-note">Required fields marked *</p>
              <div class="field-grid">
                <label class="field field-wide">
                  <span>ROM title *</span>
                  <input id="romTitle" name="title" type="text" autocomplete="off" required />
                </label>
                <label class="field field-wide">
                  <span>Platform(s)</span>
                  <input
                    id="romPlatform"
                    name="platform"
                    type="text"
                    list="platformOptions"
                    autocomplete="off"
                    placeholder="gb, gbc"
                  />
                  <datalist id="platformOptions">
                    <option value="gb"></option>
                    <option value="gbc"></option>
                    <option value="gba"></option>
                    <option value="nes"></option>
                    <option value="snes"></option>
                    <option value="genesis"></option>
                    <option value="psx"></option>
                    <option value="arcade"></option>
                    <option value="dos"></option>
                  </datalist>
                </label>
              </div>

              <details class="advanced-fields">
                <summary>Advanced metadata</summary>
                <div class="field-grid">
                  <label class="field field-wide">
                    <span>Stable identifier *</span>
                    <input id="romIdentifier" name="identifier" type="text" autocomplete="off" required />
                  </label>
                  <label class="field field-wide">
                    <span>Byte MIME *</span>
                    <input id="romMime" name="mime" type="text" value="application/octet-stream" required />
                  </label>
                </div>
              </details>
            </form>

            <div class="step-actions">
              <button id="metadataBackButton" class="secondary-button" type="button">Back</button>
              <button id="metadataNextButton" class="primary-button" type="button" disabled>
                <span>Continue to publish</span>
                <span aria-hidden="true">→</span>
              </button>
            </div>
          </section>

          <section
            class="wizard-panel"
            data-step-panel="publish"
            aria-labelledby="publish-step-title"
            hidden
          >
            <div class="section-heading">
              <div>
                <p class="section-index">03 / Upload + publish</p>
                <h2 id="publish-step-title" data-step-heading tabindex="-1">Choose routes and send</h2>
              </div>
              <span id="routeCount" class="count-badge">0 ready</span>
            </div>

            <div class="step-context" aria-label="ROM publication summary">
              <div class="file-icon" aria-hidden="true">.ROM</div>
              <div class="file-summary">
                <strong id="publishRomName">ROM ready</strong>
                <span id="publishRomMeta">Metadata complete</span>
              </div>
            </div>

            <fieldset class="target-fieldset">
              <legend class="visually-hidden">Upload targets</legend>
              <div id="targetList" class="target-list" aria-live="polite">
                <div class="target-skeleton"><span></span><span></span></div>
              </div>
            </fieldset>
            <p id="routeHelp" class="route-help">
              The shell chooses servers, credentials, and consent for each route.
            </p>

            <div class="step-actions">
              <button id="publishBackButton" class="secondary-button" type="button">Back</button>
              <button id="deliverButton" class="primary-button" type="button" disabled>
                <span id="deliverButtonLabel">Upload &amp; publish ROM</span>
                <span aria-hidden="true">↗</span>
              </button>
            </div>
            <p id="uploadPolicy" class="policy-note">
              One action uploads every selected route, then signs and publishes the event.
            </p>

            <output
              id="publishResult"
              class="publish-result"
              data-napplet-select="all"
              hidden
              aria-live="polite"
            ></output>

            <details id="eventPanel" class="event-details" hidden>
              <summary>
                NIP-ROM event · kind 32560 ·
                <span id="eventSize" class="event-size">—</span>
              </summary>
              <pre
                id="eventPreview"
                class="event-preview"
                data-event-preview
                data-napplet-select="text"
                aria-live="polite"
              >Upload the ROM to forge a publishable event.</pre>
            </details>

            <button id="startOverButton" class="restart-button" type="button" hidden>
              Start another ROM
            </button>
          </section>

          <output id="status" class="status" data-kind="idle" aria-live="polite">
            <span class="status-light"></span>
            <span id="statusText">Preparing routes</span>
          </output>
        </div>

        <footer class="boundary-strip">
          <span>Bytes → NAP-UPLOAD</span>
          <span>Signature → NAP-OUTBOX</span>
          <span>Network + keys stay with the shell</span>
        </footer>
      </section>
    </main>
  </body>
</html>