Back to Cyberspace Contacts
SOURCE / PINNED RELEASE

Made of little things.

Cyberspace Contacts

Release
c80eb8f29ce0…
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" />
    <title>Cyberspace Contacts</title>
    <script type="module" crossorigin>function be(){const e=new Map;function t(s,i){if(e.has(s))throw new Error(`NAP domain "${s}" is already registered`);e.set(s,i)}function n(s){const i=s.type.indexOf(".");if(i<=0)return!1;const o=s.type.slice(0,i),l=e.get(o);return l?(l(s),!0):!1}function r(){return Array.from(e.keys())}return{registerNap:t,dispatch:n,getRegisteredDomains:r}}var we=be(),T=we.registerNap,ee="relay";T(ee,e=>{});var B="identity";T(B,e=>{});var te="theme";function ne(){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 ve(){return ne().get()}function ke(e){return ne().onChanged(e)}T(te,e=>{});var oe="outbox";T(oe,e=>{});function xe(){const e=window;if(!e.napplet)throw new Error("window.napplet is unavailable -- runtime did not inject the napplet namespace");return e.napplet}function u(e){const n=xe()[e];if(!n)throw new Error(`window.napplet.${String(e)} is unavailable -- runtime did not inject this domain`);return n}var Ee={subscribe(e,t,n,r){return u("relay").subscribe(e,t,n,r)},publish(e,t){return u("relay").publish(e,t)},publishEncrypted(e,t,n="nip44"){return u("relay").publishEncrypted(e,t,n)},query(e){return u("relay").query(e)}},O={getPublicKey(){return u("identity").getPublicKey()},onChanged(e){return u("identity").onChanged(e)},getRelays(){return u("identity").getRelays()},getProfile(){return u("identity").getProfile()},getFollows(){return u("identity").getFollows()},getList(e){return u("identity").getList(e)},getZaps(){return u("identity").getZaps()},getMutes(){return u("identity").getMutes()},getBlocked(){return u("identity").getBlocked()},getBadges(){return u("identity").getBadges()}},re={getEvent(e,t){return u("outbox").getEvent(e,t)},query(e,t){return u("outbox").query(e,t)},subscribe(e,t){return u("outbox").subscribe(e,t)},publish(e,t){return u("outbox").publish(e,t)},resolveRelays(e){return u("outbox").resolveRelays(e)}};function Ce(e,t){return!!(e!=null&&e[t])}function C(e){return Ce(window.napplet,e)}const $e=85;function se(e){const t=Number(e&1n);let n=0n,r=0n,s=0n;for(let i=0;i<$e;i++){const o=BigInt(i);s|=(e>>1n+o*3n&1n)<<o,r|=(e>>2n+o*3n&1n)<<o,n|=(e>>3n+o*3n&1n)<<o}return{x:n,y:r,z:s,plane:t}}function ie(e){const t=e.startsWith("0x")?e.slice(2):e;return BigInt("0x"+t.padStart(64,"0"))}function Ae(e){return se(ie(e))}function Pe(e){const t=30n;return{sx:e.x>>t,sy:e.y>>t,sz:e.z>>t}}function Me(e){return`${e.sx}-${e.sy}-${e.sz}`}function q(e){return Number(e>>12n)/2**73}function R(e){return e.toString(16).padStart(22,"0")}const ae=/^[0-9a-f]{64}$/;function Ne(e){var r;const t=new Set,n=[];for(const s of e.tags){if(s[0]!=="p"||!ae.test(s[1]??""))continue;const i=s[1].toLowerCase();t.has(i)||(t.add(i),n.push({pubkey:i,petname:(r=s[3])!=null&&r.trim()?s[3].trim():null}))}return n}async function Se(e){const t=await O.getFollows().catch(()=>[]),n=new Set,r=[];for(const o of t)ae.test(o)&&!n.has(o)&&(n.add(o),r.push({pubkey:o.toLowerCase(),petname:null}));if(r.length>0)return{contacts:r,source:"identity"};const i=(await re.query([{kinds:[3],authors:[e],limit:5}],{authors:[e],timeoutMs:8e3})).events.map(o=>o.event).filter(o=>o.kind===3).sort((o,l)=>l.created_at-o.created_at);for(const o of i)for(const l of Ne(o))n.has(l.pubkey)||(n.add(l.pubkey),r.push(l));return{contacts:r,source:"outbox"}}const le=3333,j=["spawn","hop","sidestep","enter-hyperspace","hyperjump"],Ie=/^[0-9a-f]{64}$/;function L(e,t){var n;return(n=e.tags.find(r=>r[0]===t))==null?void 0:n[1]}function F(e){if(e.kind!==le)return null;const t=L(e,"A");if(!j.includes(t??""))return null;const n=L(e,"C");return!n||!Ie.test(n)||!L(e,"S")||t==="spawn"&&n!==e.pubkey?null:{id:e.id,pubkey:e.pubkey,createdAt:e.created_at,type:t,coordHex:n,position:se(ie(n))}}function U(e,t){const n=e.get(t.pubkey);(!n||t.createdAt>n.createdAt||t.createdAt===n.createdAt&&t.id>n.id)&&e.set(t.pubkey,t)}function ce(e,t){return!!(t&&t.coordHex!==e)}const Te="wss://cyberspace.nostr1.com";function De(e,t,n){const r={sub:null,sawRelayEvent:!1};return r.sub=Ee.subscribe([{kinds:[3333],authors:e,"#A":[...j],limit:500}],s=>{r.sawRelayEvent=!0,t(s.event)},n,{relay:Te}),r}const Re="qpzry9x8gf2tvdw0s3jn54khce6mua7l",Le=[996825010,642813549,513874426,1027748829,705979059];function _e(e){let t=1;for(const n of e){const r=t>>25;t=(t&33554431)<<5^n;for(let s=0;s<5;s++)r>>s&1&&(t^=Le[s])}return t}function He(e){const t=[],n=[];for(const r of e){const s=r.codePointAt(0)??0;t.push(s>>5),n.push(s&31)}return[...t,0,...n]}function qe(e,t){const n=[...He(e),...t,0,0,0,0,0,0],r=_e(n)^1,s=[];for(let i=0;i<6;i++)s.push(r>>5*(5-i)&31);return s}function Be(e,t,n,r){let s=0,i=0;const o=(1<<n)-1,l=[];for(const p of e)for(s=s<<t|p,i+=t;i>=n;)i-=n,l.push(s>>i&o);return i&&l.push(s<<n-i&o),l}function Oe(e){const t=e.toLowerCase();if(!/^[0-9a-f]{64}$/.test(t))throw new Error("npub expects 64 hex chars");const n=[];for(let o=0;o<64;o+=2)n.push(Number.parseInt(t.slice(o,o+2),16));const r=Be(n,8,5),s="npub",i=[...r,...qe(s,r)];return`${s}1${i.map(o=>Re[o]).join("")}`}function g(e){const t=document.querySelector(e);if(!t)throw new Error(`Missing required element: ${e}`);return t}const a={status:g("#status"),planeData:g("#planeData"),planeIdea:g("#planeIdea"),rescan:g("#rescan"),map:g("#map"),tooltip:g("#tooltip"),state:g("#state"),sheet:g("#sheet"),list:g("#list")},_=1500,Y=100,je=12;let z=0,$=null,K=null,f=[],ue="identity";const v=new Map,A=new Map;let w=null,P=0,N=null,S=null,m=0,I=!0,x=!0,H=!1;function G(e,t,n){let r=0;const s=new Promise((i,o)=>{r=window.setTimeout(()=>o(new Error(`${t} timed out after ${n}ms`)),n)});return Promise.race([e,s]).finally(()=>window.clearTimeout(r))}function ze(e){return e instanceof Error?e.message:String(e)}function Ke(e){return`${e.slice(0,8)}…${e.slice(-6)}`}function We(e){try{return Oe(e)}catch{return e}}function de(e){const t=Math.max(0,Date.now()/1e3-e);return t<60?"just now":t<3600?`${Math.floor(t/60)}m ago`:t<86400?`${Math.floor(t/3600)}h ago`:`${Math.floor(t/86400)}d ago`}function d(e,t){a.status.className=`status status-${e}`,a.status.textContent=t}function pe(e){let t=A.get(e.pubkey);t||(t=Ae(e.pubkey),A.set(e.pubkey,t));const n=v.get(e.pubkey)??null;return{pubkey:e.pubkey,name:e.petname??Ke(e.pubkey),pos:n?n.position:t,moved:ce(e.pubkey,n??void 0),head:n}}function W(){const e=[];for(const t of f){const n=pe(t);n.pos.plane===z&&e.push(n)}return e.sort((t,n)=>t.name.localeCompare(n.name)),e}function fe(e){return`hsl(${Math.floor(q(e.y)*360)} 65% 60%)`}function h(){I=!0,x=!0,!H&&(H=!0,requestAnimationFrame(()=>{H=!1,I&&(I=!1,Ue()),x&&(x=!1,Xe())}))}function V(e,t){return getComputedStyle(document.documentElement).getPropertyValue(e).trim()||t}function me(e,t,n){const s=[];for(const i of e)s.push({...i,px:14+q(i.pos.x)*(t-28),py:14+q(i.pos.z)*(n-28)});return s}function Xe(){const e=a.map,t=e.clientWidth,n=e.clientHeight;if(t===0||n===0)return;const r=Math.min(window.devicePixelRatio||1,2),s=Math.round(t*r),i=Math.round(n*r);(e.width!==s||e.height!==i)&&(e.width=s,e.height=i);const o=e.getContext("2d");if(!o)return;o.setTransform(r,0,0,r,0,0),o.clearRect(0,0,t,n);const l=V("--fg","#d7e0ee"),p=V("--primary","#7ef0c8");o.strokeStyle=l,o.lineWidth=1,o.globalAlpha=.07;for(let c=1;c<8;c++){const k=c/8;o.beginPath(),o.moveTo(k*t,0),o.lineTo(k*t,n),o.stroke(),o.beginPath(),o.moveTo(0,k*n),o.lineTo(t,k*n),o.stroke()}o.globalAlpha=.16,o.strokeRect(.5,.5,t-1,n-1),o.globalAlpha=1,o.globalAlpha=.14,o.fillStyle=l,o.font="600 11px ui-monospace, Menlo, Consolas, monospace",o.fillText(z===0?"DATASPACE":"IDEASPACE",8,16),o.globalAlpha=1;const y=me(W(),t,n);for(const c of y){const k=fe(c.pos);c.moved?(o.fillStyle=k,o.beginPath(),o.arc(c.px,c.py,3.5,0,Math.PI*2),o.fill()):(o.strokeStyle=k,o.lineWidth=1.5,o.beginPath(),o.arc(c.px,c.py,3,0,Math.PI*2),o.stroke()),c.pubkey===K&&(o.strokeStyle=l,o.globalAlpha=.8,o.lineWidth=1,o.beginPath(),o.arc(c.px,c.py,6.5,0,Math.PI*2),o.stroke(),o.globalAlpha=1),c.pubkey===$&&(o.strokeStyle=p,o.lineWidth=1.5,o.beginPath(),o.arc(c.px,c.py,8,0,Math.PI*2),o.stroke())}}function he(e,t){const n=a.map.getBoundingClientRect(),r=a.map.clientWidth,s=a.map.clientHeight;if(r===0||s===0)return null;const i=e-n.left,o=t-n.top;let l=null,p=je;for(const y of me(W(),r,s)){const c=Math.hypot(y.px-i,y.py-o);c<=p&&(l=y,p=c)}return l}function Fe(e){var r;const t=a.map.getBoundingClientRect(),n=((r=a.map.parentElement)==null?void 0:r.getBoundingClientRect())??t;a.tooltip.textContent=`${e.name} · ${e.moved?"moved":"spawned"} · ${e.pos.plane===0?"◇ data":"◆ idea"}`,a.tooltip.style.left=`${e.px+(t.left-n.left)}px`,a.tooltip.style.top=`${e.py+(t.top-n.top)}px`,a.tooltip.hidden=!1}function Ue(){const e=[];for(const t of W()){const n=document.createElement("li"),r=document.createElement("button");r.type="button",r.className=t.pubkey===$?"row is-selected":"row";const s=document.createElement("span");s.className="dot",s.style.background=fe(t.pos);const i=document.createElement("span");i.className="nm",i.textContent=t.name;const o=document.createElement("span");o.className="st",o.textContent=t.moved?"⇢":"◦",o.title=t.moved?`moved · ${t.head?de(t.head.createdAt):""}`:"at spawn coordinate",r.append(s,i,o),r.addEventListener("click",()=>M(t.pubkey)),n.append(r),e.push(n)}a.list.replaceChildren(...e)}function b(e,t,n=!1){const r=document.createElement("dt");r.textContent=e;const s=document.createElement("dd");return n&&(s.className="mono",s.dataset.nappletSelect="text"),s.textContent=t,[r,s]}function Ye(){const e=$?f.find(y=>y.pubkey===$):void 0;if(!e){a.sheet.hidden=!0;return}const t=pe(e),n=f.find(y=>y.pubkey===t.pubkey);a.sheet.replaceChildren();const r=document.createElement("header"),s=document.createElement("strong");s.className="sheet-name",s.textContent=t.name;const i=document.createElement("span");i.className="sheet-spacer";const o=document.createElement("button");o.type="button",o.className="close",o.textContent="×",o.ariaLabel="Close contact details",o.addEventListener("click",()=>M(null)),r.append(s,i,o);const l=document.createElement("dl");l.append(...b("npub",We(t.pubkey),!0),...b("plane",t.pos.plane===0?"◇ dataspace":"◆ ideaspace"),...b("sector",Me(Pe(t.pos))),...b("state",t.moved&&t.head?`${t.head.type} · ${de(t.head.createdAt)}`:"spawned — never moved"),...b("x",R(t.pos.x),!0),...b("y",R(t.pos.y),!0),...b("z",R(t.pos.z),!0),...b("coord",t.head?t.head.coordHex:t.pubkey,!0));const p=document.createElement("p");p.className="sheet-note",p.textContent=(n!=null&&n.petname?"petname from your follow list · ":"")+`follows via ${ue==="identity"?"shell identity":"outbox kind 3"}`,a.sheet.append(r,l,p),a.sheet.hidden=!1}function M(e){$=e,Ye(),I=!0,x=!0,h()}function E(e,t){const n=document.createElement("strong");n.textContent=e==="empty"?"No follows yet":e==="signedout"?"No identity":"Cannot run";const r=document.createElement("p");r.append(n,document.createTextNode(` — ${t}`)),a.state.replaceChildren(r),a.state.hidden=!1}function Z(){a.state.hidden=!0}function D(){var e;(e=w==null?void 0:w.sub)==null||e.close(),w=null}function Q(){let e=0;for(const n of f)ce(n.pubkey,v.get(n.pubkey))&&(e+=1);const t=[`${f.length} plotted`,`${e} moved`];w!=null&&w.sub&&t.push(P>0?`${P} via relay`:"relay open"),d("ok",t.join(" · "))}async function ye(e){const t=f.slice(0,_).map(r=>r.pubkey);if(t.length===0)return;f.length>_&&d("warn",`movement scan capped at ${_} follows`);let n=t;if(C(oe)){const r=[];for(let i=0;i<t.length;i+=Y)r.push(t.slice(i,i+Y));let s=0;if(d("idle",`scanning movement 0/${r.length}`),await Promise.all(r.map(async i=>{try{const o=await re.query([{kinds:[le],authors:i,"#A":[...j],limit:500}],{authors:i,timeoutMs:8e3,limit:500});if(e!==m)return;for(const l of o.events){const p=F(l.event);p&&U(v,p)}}catch{}s+=1,e===m&&(d("idle",`scanning movement ${s}/${r.length}`),h())})),e!==m)return;n=t.filter(i=>!v.has(i))}n.length>0&&C(ee)&&(d("idle",`fallback scan · ${n.length} unresolved`),w=De(n,r=>{if(e!==m)return;const s=F(r);s&&(U(v,s),P+=1,h())},()=>{e===m&&(Q(),h())})),e===m&&(Q(),h())}async function X(){const e=++m;if(D(),P=0,M(null),Z(),!C(B)){E("error","this runtime does not inject the identity domain."),d("error","identity domain missing");return}d("idle","reading identity…");let t="";try{t=await G(O.getPublicKey(),"identity.getPublicKey",5e3)}catch(r){E("error",ze(r)),d("error","identity read failed");return}if(e!==m)return;if(!t){f=[],v.clear(),A.clear(),h(),E("signedout","connect or sign in with a nostr identity to plot your follows."),d("warn","no identity");return}d("idle","loading follows…");let n=null;try{n=await G(Se(t),"follows",12e3)}catch{n=null}if(e===m){if(f=(n==null?void 0:n.contacts)??[],ue=(n==null?void 0:n.source)??"identity",v.clear(),A.clear(),h(),f.length===0){E("empty","follow people on nostr and rescan — every pubkey is also an address somewhere in the cube."),d("warn","no follows");return}d("ok",`${f.length} plotted (spawn) · scanning…`),Z(),await ye(e)}}function J(e){const t=document.documentElement.style;t.setProperty("--bg",e.colors.background),t.setProperty("--fg",e.colors.text),t.setProperty("--primary",e.colors.primary),x=!0,h()}function Ge(){if(C(te)){ve().then(J).catch(()=>{});try{S=ke(J)}catch{}}}function ge(e){z=e,a.planeData.classList.toggle("is-on",e===0),a.planeData.ariaPressed=String(e===0),a.planeIdea.classList.toggle("is-on",e===1),a.planeIdea.ariaPressed=String(e===1),h()}a.planeData.addEventListener("click",()=>ge(0));a.planeIdea.addEventListener("click",()=>ge(1));a.rescan.addEventListener("click",()=>{if(f.length===0){X();return}const e=++m;D(),P=0,d("idle","rescanning…"),ye(e)});a.map.addEventListener("pointermove",e=>{const t=he(e.clientX,e.clientY);K=(t==null?void 0:t.pubkey)??null,a.map.style.cursor=t?"pointer":"default",t?Fe(t):a.tooltip.hidden=!0});a.map.addEventListener("pointerleave",()=>{K=null,a.tooltip.hidden=!0});a.map.addEventListener("pointerdown",e=>{const t=he(e.clientX,e.clientY);M(t?t.pubkey:null)});new ResizeObserver(()=>{x=!0,h()}).observe(a.map);Ge();if(C(B))try{N=O.onChanged(e=>{if(!e){++m,D(),f=[],v.clear(),A.clear(),M(null),h(),E("signedout","identity disconnected."),d("warn","no identity");return}X()})}catch{}window.addEventListener("beforeunload",()=>{N==null||N.close(),S==null||S.close(),D()});X();
</script>
    <style>:root{--bg: #0b0e14;--fg: #d7e0ee;--primary: #7ef0c8;--surface: color-mix(in srgb, var(--bg) 88%, var(--fg));--surface-2: color-mix(in srgb, var(--bg) 78%, var(--fg));--border: color-mix(in srgb, var(--fg) 16%, transparent);--muted: color-mix(in srgb, var(--fg) 55%, var(--bg));--ok: #57c994;--warn: #d9a13f;--danger: #e2695e;--space-1: 4px;--space-2: 8px;--control-h: 28px;--radius: 8px;--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;--napplet-text-selection: none;color-scheme:dark light;font:13px/1.4 Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}*{box-sizing:border-box}:where(html,body,body *){-webkit-user-select:var(--napplet-text-selection, none);user-select:var(--napplet-text-selection, none)}:where(input,textarea,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}html,body,#app{height:100%;margin:0;background:var(--bg);color:var(--fg)}button{font:inherit;color:inherit}:focus-visible{outline:2px solid var(--primary);outline-offset:1px}.app{container-type:inline-size;display:grid;grid-template-rows:auto minmax(0,1fr);gap:var(--space-2);padding:var(--space-2);height:100%;overflow:hidden}.toolbar{display:flex;align-items:center;gap:var(--space-2);min-width:0}.seg{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;flex:none}.seg-btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;height:var(--control-h);padding:0 8px;border:0;background:transparent;color:var(--muted);cursor:pointer;font-size:12px}.seg-btn+.seg-btn{border-left:1px solid var(--border)}.seg-btn.is-on{background:color-mix(in srgb,var(--primary) 18%,var(--bg));color:var(--fg)}.icon-btn{flex:none;width:var(--control-h);height:var(--control-h);display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:var(--radius);background:transparent;color:var(--fg);cursor:pointer;font-size:14px;line-height:1}.icon-btn:hover,.seg-btn:hover{background:var(--surface)}.status{margin-left:auto;min-width:0;max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;font-family:var(--mono);color:var(--muted);text-align:right}.status-ok{color:var(--ok)}.status-warn{color:var(--warn)}.status-error{color:var(--danger)}.stage{position:relative;display:grid;grid-template-columns:minmax(0,1fr);gap:var(--space-2);min-height:0;min-width:0}#map{grid-column:1;grid-row:1;width:100%;height:100%;display:block;touch-action:manipulation}.tooltip{position:absolute;z-index:6;pointer-events:none;transform:translate(-50%,calc(-100% - 10px));padding:3px var(--space-2);border:1px solid var(--border);border-radius:6px;background:color-mix(in srgb,var(--bg) 90%,var(--fg));box-shadow:0 4px 14px #00000059;font-size:11px;font-family:var(--mono);white-space:nowrap}.state{position:absolute;top:0;right:0;bottom:0;left:0;z-index:5;display:grid;place-items:center;padding:var(--space-3, 12px);background:color-mix(in srgb,var(--bg) 84%,transparent);text-align:center}.state p{max-width:42ch;margin:0;color:var(--muted);font-size:12px}.state strong{color:var(--fg);font-size:13px}.list{display:none;grid-column:2;grid-row:1;min-height:0;overflow-y:auto;border:1px solid var(--border);border-radius:var(--radius);background:color-mix(in srgb,var(--bg) 94%,var(--fg));list-style:none;margin:0;padding:0;scrollbar-width:thin}.row{display:flex;align-items:center;gap:6px;width:100%;padding:5px var(--space-2);border:0;border-bottom:1px solid var(--border);background:none;color:var(--fg);font-size:12px;text-align:left;cursor:pointer}.row:hover{background:var(--surface)}.row.is-selected{background:color-mix(in srgb,var(--primary) 14%,var(--bg))}.row .dot{flex:none;width:8px;height:8px;border-radius:50%}.row .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.row .st{flex:none;color:var(--muted);font-size:11px}.sheet{position:absolute;inset:auto var(--space-2) var(--space-2) var(--space-2);z-index:6;max-height:55%;overflow-y:auto;padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius);background:color-mix(in srgb,var(--bg) 92%,var(--fg));box-shadow:0 6px 20px #0006;scrollbar-width:thin}.sheet header{display:flex;align-items:center;gap:var(--space-2);margin-bottom:6px}.sheet-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}.sheet-spacer{flex:1}.sheet .close{flex:none;width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:6px;background:none;color:var(--muted);font-size:14px;line-height:1;cursor:pointer}.sheet dl{display:grid;grid-template-columns:auto minmax(0,1fr);gap:2px 10px;margin:0;font-size:12px}.sheet dt{color:var(--muted)}.sheet dd{margin:0;min-width:0;overflow-wrap:anywhere}.sheet .mono{font-family:var(--mono);font-size:11px}.sheet-note{margin:8px 0 0;color:var(--muted);font-size:10px}.seg-btn .label{display:none}.status{font-size:10px}@container (min-width: 240px){.seg-btn{padding:0 10px}.seg-btn .label{display:inline}.status{font-size:11px}}@container (min-width: 480px){.stage{grid-template-columns:minmax(0,1fr) 190px}.list{display:block}}@container (min-width: 900px){.stage{grid-template-columns:minmax(0,1fr) 230px 300px}.list{display:block}.sheet{position:static;grid-column:3;grid-row:1;max-height:none;box-shadow:none}}@media(max-height:240px){.app{gap:var(--space-1);padding:var(--space-1)}.sheet{inset:auto var(--space-1) var(--space-1) var(--space-1);max-height:calc(100% - 10px)}.state p{font-size:11px}}
</style>
    <meta name="napplet-type" content="cyberspace">
    <meta name="napplet-requires" content="identity">
  </head>
  <body>
    <!--
      A napplet is an applet framed by a runtime: the runtime already shows
      this napplet's name, so the frame is working surface from the first
      pixel. Layout adapts to any frame size via container queries.
    -->
    <main id="app" class="app">
      <div class="toolbar" role="toolbar" aria-label="Map controls">
        <div class="seg" role="group" aria-label="Plane">
          <button id="planeData" class="seg-btn is-on" type="button" aria-pressed="true">
            <span aria-hidden="true">◇</span><span class="label">Data</span>
          </button>
          <button id="planeIdea" class="seg-btn" type="button" aria-pressed="false">
            <span aria-hidden="true">◆</span><span class="label">Idea</span>
          </button>
        </div>
        <button id="rescan" class="icon-btn" type="button" title="Rescan" aria-label="Rescan">⟳</button>
        <output id="status" class="status status-idle" aria-live="polite">Starting…</output>
      </div>

      <div class="stage">
        <canvas id="map" aria-label="Contacts plotted in cyberspace"></canvas>
        <div id="tooltip" class="tooltip" hidden></div>
        <div id="state" class="state" hidden></div>
        <aside id="sheet" class="sheet" hidden aria-label="Contact details"></aside>
        <ul id="list" class="list" aria-label="Contacts"></ul>
      </div>
    </main>
  </body>
</html>