assets/template.html
assets/template.htmlBrowse 7 files
12,747 tokens
38,247 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1<!doctype html>2<meta charset="utf-8">3<title>__TITLE__</title>4<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap">5<style>6:root{7 --paper:#E6DFBE; --paper-2:#DCD4AF; --ink:#17170F; --ink-2:#6E6B54; --rule:#B9B293; --ghost:#8E8A70;8 --hi-bg:#17170F; --hi-fg:#E6DFBE; --face:#EFE9CC; --grid:#CFC8A3;9 --mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;10}11@media (prefers-color-scheme: dark){12 :root:not([data-theme="light"]){13 --paper:#1E1D15; --paper-2:#26251B; --ink:#E6DFBE; --ink-2:#A8A388; --rule:#4A4838; --ghost:#6E6B54;14 --hi-bg:#E6DFBE; --hi-fg:#1E1D15; --face:#2A2920; --grid:#33321F;15 }16}17:root[data-theme="dark"]{18 --paper:#1E1D15; --paper-2:#26251B; --ink:#E6DFBE; --ink-2:#A8A388; --rule:#4A4838; --ghost:#6E6B54;19 --hi-bg:#E6DFBE; --hi-fg:#1E1D15; --face:#2A2920; --grid:#33321F;20}21*{box-sizing:border-box}22html,body{height:100%;margin:0}23body{background:var(--paper);color:var(--ink);font-family:var(--mono);font-size:13px;line-height:1.55;overflow:hidden}24#app{display:grid;grid-template-rows:52px 1fr 30px;height:100vh;min-height:600px}25#top{display:flex;align-items:stretch;border-bottom:1.5px solid var(--ink);overflow:hidden}26.stat{padding:6px 14px 4px;border-right:1.5px solid var(--ink);min-width:0;white-space:nowrap}27.stat .k{font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-2)}28.stat .v{font-size:15px;font-variant-numeric:tabular-nums}29.stat.grow{flex:1;border-right:none;overflow:hidden}30#controls{display:flex;gap:8px;align-items:center;padding:0 14px;border-left:1.5px solid var(--ink)}31button.ctl{font:inherit;font-size:11px;letter-spacing:.06em;text-transform:uppercase;background:var(--paper);color:var(--ink);border:1.5px solid var(--ink);padding:5px 10px;cursor:pointer;box-shadow:2px 2px 0 var(--ink);white-space:nowrap}32button.ctl:hover{background:var(--face)}33button.ctl:active{transform:translate(1px,1px);box-shadow:1px 1px 0 var(--ink)}34button.ctl:focus-visible{outline:2px solid var(--ink);outline-offset:2px}35button.ctl.primary{background:var(--hi-bg);color:var(--hi-fg)}36button.ctl.primary:hover{background:var(--hi-bg);opacity:.9}37button.ctl:disabled{opacity:.4;cursor:default;box-shadow:none}38#main{display:grid;grid-template-columns:232px 1fr 440px;min-height:0}39#index{border-right:1.5px solid var(--ink);overflow-y:auto;padding:10px 8px 20px}40#index h4{font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-2);margin:12px 4px 6px;font-weight:500}41#index h4:first-child{margin-top:2px}42.ix{display:flex;align-items:flex-start;gap:8px;width:100%;text-align:left;font:inherit;font-size:11.5px;line-height:1.3;background:var(--paper);color:var(--ink);border:1.5px solid var(--ink);padding:7px 8px;margin:0 0 6px;cursor:pointer;box-shadow:2px 2px 0 var(--ink);text-transform:uppercase;letter-spacing:.02em}43.ix .code{color:var(--ink-2);min-width:16px;font-size:10px;padding-top:1px}44.ix .nm{flex:1}45.ix .n{color:var(--ink-2);font-size:10.5px;font-variant-numeric:tabular-nums}46.ix.ghost{border-style:dashed;box-shadow:none;color:var(--ink-2)}47.ix.later{border-color:var(--rule);box-shadow:none;color:var(--ink-2);opacity:.75}48.ix.later .n{font-size:9px;letter-spacing:.06em}49.ix.new{outline:1.5px dashed var(--ink);outline-offset:2px}50.ix.on{background:var(--hi-bg);color:var(--hi-fg)}51.ix.on .code,.ix.on .n{color:var(--hi-fg);opacity:.75}52.ix:hover:not(.on){background:var(--face)}53.ix:focus-visible{outline:2px solid var(--ink);outline-offset:1px}54#canvasWrap{position:relative;min-width:0;background:var(--paper)}55#svg{width:100%;height:100%;display:block;cursor:grab;touch-action:none}56#svg.dragging{cursor:grabbing}57#zoom{position:absolute;right:12px;top:12px;display:flex;flex-direction:column;gap:6px}58#zoom button{width:30px;height:26px;font:inherit;font-size:14px;background:var(--paper);border:1.5px solid var(--ink);color:var(--ink);cursor:pointer;box-shadow:2px 2px 0 var(--ink)}59#rail{position:absolute;left:12px;top:12px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;max-width:calc(100% - 70px)}60#rail .ch{width:22px;height:22px;font:inherit;font-size:10px;background:var(--paper);border:1.5px solid var(--ink);color:var(--ink);cursor:pointer;padding:0;line-height:1}61#rail .ch.done{background:var(--face)}62#rail .ch.on{background:var(--hi-bg);color:var(--hi-fg)}63#rail .ch.locked{border-style:dashed;color:var(--ink-2)}64#rail .ch:focus-visible{outline:2px solid var(--ink);outline-offset:1px}65#rail .title{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-2);margin-left:6px}66#rail .title b{color:var(--ink);font-weight:500}67#flowSel{position:absolute;left:12px;bottom:12px;display:none;gap:6px}68#flowSel button{font:inherit;font-size:10px;letter-spacing:.08em;text-transform:uppercase;background:var(--paper);border:1.5px solid var(--ink);color:var(--ink);padding:3px 8px;cursor:pointer}69#flowSel button.on{background:var(--hi-bg);color:var(--hi-fg)}70#tip{position:absolute;pointer-events:none;background:var(--hi-bg);color:var(--hi-fg);font-size:11px;padding:4px 8px;max-width:280px;line-height:1.35;display:none;z-index:5}71#panel{border-left:1.5px solid var(--ink);display:flex;flex-direction:column;min-height:0}72#tabs{display:flex;border-bottom:1.5px solid var(--ink)}73#tabs button{flex:1;font:inherit;font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;padding:9px 6px;background:var(--paper);color:var(--ink);border:none;border-right:1.5px solid var(--ink);cursor:pointer}74#tabs button:last-child{border-right:none}75#tabs button.on{background:var(--hi-bg);color:var(--hi-fg)}76#tabs button:focus-visible{outline:2px solid var(--ink);outline-offset:-3px}77#body{overflow-y:auto;padding:14px 18px 40px;flex:1}78.eyebrow{font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-2)}79h1.t{font-size:22px;font-weight:500;line-height:1.2;margin:2px 0 4px;text-wrap:balance}80.lede{font-size:14.5px;line-height:1.5;margin:0 0 14px;max-width:56ch}81.sub{color:var(--ink-2);font-size:11px;margin-bottom:14px}82h3.sec{font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-2);font-weight:500;margin:20px 0 6px;padding-bottom:4px;border-bottom:1.5px solid var(--ink)}83#body p{margin:0 0 12px;max-width:62ch}84#body ul{margin:0 0 12px;padding-left:18px;max-width:62ch}85#body li{margin-bottom:6px}86mark{background:var(--hi-bg);color:var(--hi-fg);padding:0 3px}87.chip{display:inline-block;border:1.5px solid var(--ink);padding:1px 6px;font-size:10px;letter-spacing:.06em;text-transform:uppercase;margin:0 6px 6px 0;background:var(--paper);color:var(--ink);font-family:inherit;cursor:pointer}88.chip.ghost{border-style:dashed;color:var(--ink-2)}89.chip.static{cursor:default}90pre{background:var(--face);border:1.5px solid var(--rule);padding:10px 12px;font-size:11px;line-height:1.45;overflow-x:auto;margin:0 0 12px;white-space:pre}91.step{display:grid;grid-template-columns:24px 1fr;gap:8px;margin:0 0 8px;font-size:12px}92.step .no{color:var(--ink-2);font-variant-numeric:tabular-nums}93.step.cur .no{color:var(--ink);font-weight:600}94.step.cur .tx{background:var(--face);outline:1.5px solid var(--ink);padding:2px 6px}95.actions{display:flex;gap:8px;margin:14px 0 0;flex-wrap:wrap}96.pk{border:1.5px solid var(--ink);padding:10px 12px;margin:0 0 12px}97.pk .lbl{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-2)}98.pk .route{font-size:12px;margin:2px 0 8px}99details{border-top:1.5px solid var(--rule);margin:8px 0 0;padding:6px 0 0}100details summary{cursor:pointer;font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-2);list-style:none;display:flex;gap:8px;align-items:center}101details summary::before{content:'+';display:inline-block;width:12px;color:var(--ink)}102details[open] summary::before{content:'−'}103details summary::-webkit-details-marker{display:none}104details > *:not(summary){margin-top:8px}105#hint{border-top:1.5px solid var(--ink);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-2);display:flex;align-items:center;padding:0 14px;gap:14px;overflow:hidden;white-space:nowrap}106.q{padding-left:0;list-style:none}107.q li{position:relative;padding-left:18px}108.q li::before{content:'?';position:absolute;left:0;top:0;color:var(--ink-2)}109.q li.res::before{content:'✓'}110.q li.to::before{content:'→'}111.q li.res>span,.q li.to>span{color:var(--ink-2);text-decoration:line-through;text-decoration-color:var(--rule)}112.q li.to>span{text-decoration:none}113.q .ans{margin-top:2px;padding-left:10px;border-left:1.5px solid var(--ink)}114.pkt>.lbl{visibility:hidden}115.pkt:hover>.lbl,.pkt.lbl-on>.lbl{visibility:visible}116.node>.hl{visibility:hidden}117.node:hover>.hl,.node.sel>.hl,.node.hov>.hl{visibility:visible}118@keyframes halo{0%{opacity:.9}50%{opacity:.25}100%{opacity:.9}}119.halo{animation:halo 1.6s ease-in-out infinite}120@media (prefers-reduced-motion: reduce){.halo{animation:none}}121@media (max-width:1100px){#main{grid-template-columns:200px 1fr 360px}}122</style>123 124<div id="app">125 <div id="top">126 <div id="stats" style="display:flex;min-width:0;flex:1"></div>127 <div id="controls">128 <button class="ctl" id="btnBack">◂ Back</button>129 <button class="ctl primary" id="btnNext">Next ▸</button>130 <button class="ctl" id="btnFlow">‖ Pause</button>131 <button class="ctl" id="btnStep">Trace one step</button>132 <button class="ctl" id="btnReset">Refit</button>133 </div>134 </div>135 <div id="main">136 <nav id="index" aria-label="Structures"></nav>137 <div id="canvasWrap">138 <svg id="svg" xmlns="http://www.w3.org/2000/svg">139 <defs>140 <pattern id="hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="currentColor" stroke-width="1.1"/></pattern>141 <pattern id="hatchLight" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)"><line x1="0" y1="0" x2="0" y2="7" stroke="currentColor" stroke-width=".8" opacity=".55"/></pattern>142 </defs>143 <g id="world"></g>144 </svg>145 <div id="rail"></div>146 <div id="flowSel"></div>147 <div id="zoom"><button id="zin" aria-label="Zoom in">+</button><button id="zout" aria-label="Zoom out">−</button></div>148 <div id="tip"></div>149 </div>150 <aside id="panel">151 <div id="tabs">152 <button data-tab="what" class="on">What it does</button>153 <button data-tab="how">How it's built</button>154 <button data-tab="cond">Open questions</button>155 </div>156 <div id="body"></div>157 </aside>158 </div>159 <div id="hint">160 <span>enter / ] next chapter</span><span>[ back</span><span>hover to read</span><span>click to pin</span><span>→ go inside</span><span>← come out</span><span>click a dot to inspect its packet</span>161 </div>162</div>163 164<script>165/*__DATA__*/166 167/* ---------------- CHAPTERS (progressive disclosure) ---------------- */168/* ---------------- ISO ---------------- */169const TW=72, TH=36;170function P(gx,gy,z=0){ return [(gx-gy)*TW/2, (gx+gy)*TH/2 - z]; }171function pts(a){ return a.map(p=>p.join(',')).join(' '); }172const byId = Object.fromEntries(NODES.map(n=>[n.id,n]));173const center = n => [n.gx+n.w/2, n.gy+n.d/2];174const NS='http://www.w3.org/2000/svg';175const el=(t,a={},...kids)=>{const e=document.createElementNS(NS,t);for(const k in a)e.setAttribute(k,a[k]);kids.forEach(k=>e.appendChild(k));return e;};176const question=c=>typeof c==='string'?{q:c}:c;177const qOpen=n=>(n.cond||[]).map(question).filter(c=>!c.r&&!c.to).length; const qHtml=list=>`<ul class="q">${list.map(question).map(c=>c.r?`<li class="res"><span>${c.q}</span><div class="ans">✓ ${c.r}</div></li>`:c.to?`<li class="to"><span>${c.q}</span><div class="ans">→ ${c.to}</div></li>`:`<li>${c.q}</li>`).join('')}</ul>`;178const esc=s=>String(s).replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));179 180/* ---------------- STATE ---------------- */181const S = { ch:0, view:'world', inside:null, selected:null, hover:null, tab:'what', playing:true, tx:0, ty:0, k:1, packets:[], selPacket:null, flowSel:(typeof FLOWS!=='undefined'&&FLOWS[0]?FLOWS[0].id:'turn'), maxCh:0 };182const svg=document.getElementById('svg'), world=document.getElementById('world'), tip=document.getElementById('tip'), body=document.getElementById('body');183let sceneNodes=[], sceneHops=[];184const revealedIds=()=>{ if(S.ch>=CH.length-1) return NODES.map(n=>n.id); const s=new Set(); for(let i=0;i<=S.ch;i++) CH[i].reveal.forEach(id=>s.add(id)); return [...s]; };185const isNew=id=>S.ch<CH.length-1 && CH[S.ch].reveal.includes(id);186const revealChapterOf=id=>{ for(let i=0;i<CH.length-1;i++) if(CH[i].reveal.includes(id)) return i; return CH.length-1; };187 188/* ---------------- SCENES ---------------- */189function buildWorldScene(){190 const ids=new Set(revealedIds()); sceneNodes=NODES.filter(n=>ids.has(n.id)); sceneHops=[];191 const c=CH[S.ch];192 if(c.flow){ c.flow.forEach((h,i)=>{ let bend=bendFor(h[0],h[1]); const rev=c.flow.findIndex(x=>x[0]===h[1]&&x[1]===h[0]); if(rev>-1&&rev<i) bend=bend==='xy'?'yx':'xy'; sceneHops.push({flow:'ch',idx:i,from:h[0],to:h[1],label:h[2],payload:h[3],bend}); }); }193 else { FLOWS.forEach(f=>{ if(S.flowSel!=='both'&&f.id!==S.flowSel)return; f.hops.forEach((h,i)=>sceneHops.push({flow:f.id,idx:i,from:h[0],to:h[1],label:h[2],payload:h[3],bend:h[4]||'xy'})); }); }194}195function bendFor(a,b){ const t=FLOWS.flatMap(f=>f.hops||[]).find(h=>h[0]===a&&h[1]===b); return t?(t[4]||'xy'):'yx'; }196function buildInsideScene(n){197 const steps=n.steps||[]; sceneNodes=steps.map((s,i)=>({id:'s'+i,code:String(i+1),name:s[0],short:s[0],desc:s[1],gx:2+i*3.4,gy:2+i*0.6,w:2,d:2,h:34,inside:true}));198 sceneHops=[]; for(let i=0;i<steps.length-1;i++) sceneHops.push({flow:'inside',idx:i,from:'s'+i,to:'s'+(i+1),label:steps[i][0]+' → '+steps[i+1][0],payload:{step:i+1,handsOff:steps[i+1][0]},bend:'xy'});199}200function hopPath(h){201 const A=sceneNodes.find(n=>n.id===h.from), B=sceneNodes.find(n=>n.id===h.to); if(!A||!B) return null;202 const [ax,ay]=center(A), [bx,by]=center(B); const mid = h.bend==='xy' ? [bx,ay] : [ax,by];203 const g=[[ax,ay],mid,[bx,by]].filter((p,i,arr)=>i===0||p[0]!==arr[i-1][0]||p[1]!==arr[i-1][1]);204 const scr=g.map(p=>P(p[0],p[1],0)); let len=0; const seg=[]; for(let i=1;i<scr.length;i++){const l=Math.hypot(scr[i][0]-scr[i-1][0],scr[i][1]-scr[i-1][1]);seg.push(l);len+=l;}205 return {grid:g,scr,seg,len};206}207function pointAt(path,t){ let d=t*path.len; for(let i=0;i<path.seg.length;i++){ if(d<=path.seg[i]||i===path.seg.length-1){const u=path.seg[i]?Math.min(1,d/path.seg[i]):1;const a=path.scr[i],b=path.scr[i+1];return [a[0]+(b[0]-a[0])*u,a[1]+(b[1]-a[1])*u];} d-=path.seg[i]; } return path.scr[0]; }208 209/* ---------------- RENDER ---------------- */210function render(){211 world.innerHTML='';212 const gg=el('g',{opacity:.55}); const R=[-4,26];213 for(let i=R[0];i<=R[1];i+=2){ const a=P(i,R[0]),b=P(i,R[1]);gg.appendChild(el('line',{x1:a[0],y1:a[1],x2:b[0],y2:b[1],stroke:'var(--grid)','stroke-width':.6})); const c=P(R[0],i),d=P(R[1],i);gg.appendChild(el('line',{x1:c[0],y1:c[1],x2:d[0],y2:d[1],stroke:'var(--grid)','stroke-width':.6})); }214 world.appendChild(gg);215 if(S.view==='world'&&S.ch>=CH.length-1){216 const plate=(x0,y0,x1,y1,dash)=>{const p=el('polygon',{points:pts([P(x0,y0),P(x1,y0),P(x1,y1),P(x0,y1)]),fill:'none',stroke:'var(--ink)','stroke-width':.9,opacity:.5});if(dash)p.setAttribute('stroke-dasharray','5 4');return p;};217 world.appendChild(plate(0.5,1.5,17,10.5,false)); world.appendChild(plate(2,9,17.5,16.5,false)); world.appendChild(plate(-2,-2,24,17.5,true));218 }219 const hg=el('g'); world.appendChild(hg);220 sceneHops.forEach(h=>{ h.path=hopPath(h); if(!h.path)return; const ghost=byId[h.from]?.ghost||byId[h.to]?.ghost;221 const pl=el('polyline',{points:pts(h.path.scr),fill:'none',stroke:'var(--ink)','stroke-width':h.flow!=='turn'&&h.flow!=='ch'&&h.flow!=='inside'?1:1.4,opacity:(h.flow!=='turn'&&h.flow!=='ch'&&h.flow!=='inside')||ghost?.7:1});222 if((h.flow!=='turn'&&h.flow!=='ch'&&h.flow!=='inside')||ghost)pl.setAttribute('stroke-dasharray','3 3'); hg.appendChild(pl);223 h.path.scr.slice(1,-1).forEach(p=>hg.appendChild(el('polygon',{points:pts([[p[0],p[1]-3.5],[p[0]+3.5,p[1]],[p[0],p[1]+3.5],[p[0]-3.5,p[1]]]),fill:'var(--ink)'})));224 });225 const ng=el('g'); world.appendChild(ng);226 [...sceneNodes].sort((a,b)=>(a.gx+a.gy+a.w+a.d)-(b.gx+b.gy+b.w+b.d)).forEach(n=>ng.appendChild(drawNode(n)));227 const lg=el('g'); world.appendChild(lg); sceneNodes.forEach(n=>lg.appendChild(drawLabel(n)));228 world.appendChild(el('g',{id:'pkts'})); drawPackets(); renderRail();229}230function drawLabel(n){231 const g=el('g',{style:'pointer-events:none'});232 const b=P(n.gx+n.w, n.gy+n.d, 0); const txt=(n.short||n.name).toUpperCase(); const w=txt.length*6.6+10; const y=b[1]+9;233 const dim=n.ghost&&!n.inside;234 g.appendChild(el('rect',{x:b[0]-w/2,y:y,width:w,height:14,fill:S.selected===n.id?'var(--hi-bg)':'var(--paper)',stroke:dim?'var(--ghost)':'var(--ink)','stroke-width':.9,'stroke-dasharray':dim?'3 2':'none'}));235 const t=el('text',{x:b[0],y:y+10.5,'text-anchor':'middle','font-size':'9','font-family':'var(--mono)','letter-spacing':'.06em',fill:S.selected===n.id?'var(--hi-fg)':(dim?'var(--ink-2)':'var(--ink)'),'font-weight':'500'}); t.textContent=txt; g.appendChild(t);236 return g;237}238function drawNode(n){239 const {gx,gy,w,d,h}=n; const sel=S.selected===n.id, hov=S.hover===n.id, fresh=!n.inside&&isNew(n.id);240 const g=el('g',{class:'node'+(sel?' sel':'')+(hov?' hov':''),style:'cursor:pointer','data-id':n.id});241 const sw=sel?2:1.2, dash=n.ghost?'4 3':null, face=n.ghost?'none':'var(--face)';242 const face3=(p,fill,hatch,extra={})=>{const poly=el('polygon',{points:pts(p),fill:fill,stroke:'var(--ink)','stroke-width':sw,'stroke-linejoin':'round',...extra});if(dash)poly.setAttribute('stroke-dasharray',dash);g.appendChild(poly);if(hatch&&!n.ghost){g.appendChild(el('polygon',{points:pts(p),fill:'url(#'+hatch+')',style:'color:var(--ink)',stroke:'none'}));}};243 const box=(z0,z1,hatchTop)=>{ face3([P(gx,gy+d,z0),P(gx+w,gy+d,z0),P(gx+w,gy+d,z1),P(gx,gy+d,z1)],face,'hatch'); face3([P(gx+w,gy,z0),P(gx+w,gy+d,z0),P(gx+w,gy+d,z1),P(gx+w,gy,z1)],face,'hatch'); face3([P(gx,gy,z1),P(gx+w,gy,z1),P(gx+w,gy+d,z1),P(gx,gy+d,z1)],face,hatchTop||null); };244 const kind=n.kind||'box';245 if(kind==='store'){ const L=3, gap=3, lh=(h-gap*(L-1))/L; for(let i=0;i<L;i++){ const z0=i*(lh+gap); box(z0,z0+lh,null);} }246 else if(kind==='cards'){ const L=5, lh=h/L; for(let i=0;i<L;i++) box(i*lh,(i+1)*lh,null); }247 else if(kind==='slab'){ box(0,h,'hatchLight'); }248 else if(kind==='screen'){ box(0,h,null); const inset=.3; if(!n.ghost){ const scr=[P(gx+inset,gy+inset,h),P(gx+w-inset,gy+inset,h),P(gx+w-inset,gy+d-inset,h),P(gx+inset,gy+d-inset,h)]; g.appendChild(el('polygon',{points:pts(scr),fill:'var(--paper)',stroke:'var(--ink)','stroke-width':.9})); for(let i=1;i<=3;i++){const a=P(gx+inset+.2,gy+inset+i*.4,h),b=P(gx+w-inset-.4-(i===3?.5:0),gy+inset+i*.4,h);g.appendChild(el('line',{x1:a[0],y1:a[1],x2:b[0],y2:b[1],stroke:'var(--ink)','stroke-width':.8,opacity:.7}));} } }249 else if(kind==='gate'){ box(0,h,null); if(!n.ghost){ const z0=h*.55,z1=h*.75; const band=[[P(gx,gy+d,z0),P(gx+w,gy+d,z0),P(gx+w,gy+d,z1),P(gx,gy+d,z1)],[P(gx+w,gy,z0),P(gx+w,gy+d,z0),P(gx+w,gy+d,z1),P(gx+w,gy,z1)]]; band.forEach(p=>g.appendChild(el('polygon',{points:pts(p),fill:'var(--ink)',opacity:.85,stroke:'none'}))); } }250 else if(kind==='tall'){ box(0,h,null); if(!n.ghost){ // roof ridge line on top to read as "the brain"251 const c=P(gx+w/2,gy+d/2,h); const a=P(gx,gy+d,h),b=P(gx+w,gy,h); g.appendChild(el('line',{x1:a[0],y1:a[1],x2:b[0],y2:b[1],stroke:'var(--ink)','stroke-width':.8,opacity:.5})); } }252 else if(kind==='job'){ box(0,h,'hatchLight'); }253 else { box(0,h,null); }254 const top=[P(gx,gy,h),P(gx+w,gy,h),P(gx+w,gy+d,h),P(gx,gy+d,h)];255 g.appendChild(el('polygon',{class:'hl',points:pts(top),fill:sel?'var(--ink)':'none',opacity:sel?.12:1,stroke:'var(--ink)','stroke-width':sel?2.5:1.8}));256 if(fresh&&!sel){ g.appendChild(el('polygon',{class:'halo',points:pts([P(gx-.35,gy-.35,0),P(gx+w+.35,gy-.35,0),P(gx+w+.35,gy+d+.35,0),P(gx-.35,gy+d+.35,0)]),fill:'none',stroke:'var(--ink)','stroke-width':1.4,'stroke-dasharray':'4 3'})); }257 const c=P(gx+w/2,gy+d/2,h); const chipW=16;258 g.appendChild(el('rect',{x:c[0]-chipW/2,y:c[1]-8,width:chipW,height:14,fill:sel?'var(--hi-bg)':'var(--paper)',stroke:'var(--ink)','stroke-width':1}));259 const t=el('text',{x:c[0],y:c[1]+3,'text-anchor':'middle','font-size':'9','font-family':'var(--mono)',fill:sel?'var(--hi-fg)':'var(--ink)','font-weight':'600'}); t.textContent=n.code; g.appendChild(t);260 /* No render() on hover: it clears #world and detaches this very element, which261 re-fires hover and kills the click. CSS :hover draws the highlight instead. */262 g.addEventListener('mouseenter',e=>{S.hover=n.id;showTip(e,n);renderPanel();});263 g.addEventListener('mousemove',e=>moveTip(e));264 g.addEventListener('mouseleave',()=>{S.hover=null;hideTip();renderPanel();});265 /* No click/dblclick here: hovering rebuilds the scene, so this element is gone266 before the browser can pair press with release. Hit-testing lives on the svg. */267 return g;268}269 270/* ---------------- PACKETS ---------------- */271function initPackets(){ S.packets=[]; const flows=[...new Set(sceneHops.map(h=>h.flow))]; flows.forEach(f=>S.packets.push({flow:f,hop:0,t:0,speed:.5,pause:0})); S.selPacket=null; }272function hopsOf(flow){ return sceneHops.filter(h=>h.flow===flow); }273function drawPackets(){274 const pg=world.querySelector('#pkts'); if(!pg)return; pg.innerHTML='';275 S.packets.forEach((p,i)=>{ const hs=hopsOf(p.flow); const h=hs[p.hop]; if(!h||!h.path)return; const [x,y]=pointAt(h.path,p.t);276 const isSel=S.selPacket===i; const g=el('g',{class:'pkt'+(isSel||S.ch<CH.length-1?' lbl-on':''),style:'cursor:pointer','data-i':i});277 if(isSel) g.appendChild(el('circle',{cx:x,cy:y,r:11,fill:'none',stroke:'var(--ink)','stroke-width':1.2,'stroke-dasharray':'2 2'}));278 g.appendChild(el('circle',{cx:x,cy:y,r:5.5,fill:'var(--ink)',stroke:'var(--paper)','stroke-width':1.6}));279 const lg=el('g',{class:'lbl'}); const lbl=el('text',{x:x+10,y:y-8,'font-size':'10','font-family':'var(--mono)',fill:'var(--ink)','font-weight':'500'}); lbl.textContent=h.label;280 lg.appendChild(el('rect',{x:x+7,y:y-19,width:h.label.length*6.4+6,height:14,fill:'var(--paper)',stroke:'var(--ink)','stroke-width':.8})); lg.appendChild(lbl); g.appendChild(lg);281 pg.appendChild(g);282 });283}284let last=0;285function tick(ts){ const dt=Math.min(.05,(ts-last)/1000||0); last=ts; tweenTick(); if(S.playing){ S.packets.forEach(p=>advance(p,dt)); drawPackets(); if(S.selPacket!=null) renderPanel(); } requestAnimationFrame(tick); }286function advance(p,dt){ const hs=hopsOf(p.flow); if(!hs.length)return; if(p.pause>0){p.pause-=dt;return;} const h=hs[p.hop]; if(!h||!h.path){p.hop=(p.hop+1)%hs.length;return;} p.t+=dt*p.speed*(320/Math.max(120,h.path.len)); if(p.t>=1){p.t=0;p.hop=(p.hop+1)%hs.length;p.pause=(p.hop===0?1.2:.35);} }287function stepOnce(){ S.playing=false; syncFlowBtn(); let i=S.selPacket??0; const p=S.packets[i]; if(!p)return; const hs=hopsOf(p.flow); p.hop=(p.hop+1)%hs.length; p.t=.5; p.pause=0; S.selPacket=i; drawPackets(); renderPanel(); }288function syncFlowBtn(){ document.getElementById('btnFlow').textContent = S.playing?'‖ Pause':'▸ Play'; }289requestAnimationFrame(tick);290 291/* ---------------- PANEL ---------------- */292function nodeChip(id){ const n=byId[id]; return `<button class="chip${n.ghost?' ghost':''}" onclick="select('${id}')">${esc(n.code)} · ${esc(n.name)}</button>`; }293function renderPanel(){294 document.querySelectorAll('#tabs button').forEach(b=>b.classList.toggle('on',b.dataset.tab===S.tab));295 if(S.selPacket!=null && S.packets[S.selPacket]){ const p=S.packets[S.selPacket]; const hs=hopsOf(p.flow); const h=hs[p.hop];296 const A=sceneNodes.find(n=>n.id===h.from), B=sceneNodes.find(n=>n.id===h.to); const flowName=p.flow==='ch'?CH[S.ch].title:p.flow==='inside'?'inside '+byId[S.inside].name:(FLOWS.find(f=>f.id===p.flow)||{}).name;297 body.innerHTML=`<div class="eyebrow">Data packet · ${esc(flowName)} · hop ${p.hop+1} of ${hs.length}</div><h1 class="t">${esc(h.label)}</h1>298 <div class="pk"><div class="lbl">Route</div><div class="route">${esc(A.code)} ${esc(A.name)} → ${esc(B.code)} ${esc(B.name)}</div><div class="lbl">Payload (representative)</div><pre>${esc(JSON.stringify(h.payload,null,2))}</pre></div>299 <p>Payload shapes are what the design implies, not measured traffic.</p>300 <div class="actions"><button class="ctl" onclick="stepOnce()">Trace one step</button><button class="ctl" onclick="S.selPacket=null;S.playing=true;syncFlowBtn();renderPanel();drawPackets()">Release & play</button><button class="ctl" onclick="select('${esc(B.id)}')">Open ${esc(B.code)}</button></div>`; return; }301 const id=S.selected??S.hover; const n=id?sceneNodes.find(x=>x.id===id):null;302 if(!n){ renderChapterPanel(); return; }303 if(n.inside){ const parent=byId[S.inside]; const i=+n.id.slice(1);304 body.innerHTML=`<div class="eyebrow">Inside ${esc(parent.code)} · ${esc(parent.name)} · step ${i+1} of ${parent.steps.length}</div><h1 class="t">${esc(n.name)}</h1><p class="lede">${esc(n.desc)}</p>305 <h3 class="sec">All steps</h3>${parent.steps.map((s,j)=>`<div class="step ${j===i?'cur':''}"><div class="no">${j+1}</div><div class="tx"><b>${esc(s[0])}</b> — ${esc(s[1])}</div></div>`).join('')}306 <div class="actions"><button class="ctl" onclick="comeOut()">← Come back out</button></div>`; return; }307 const status = n.ghost?'<span class="chip ghost static">Not switched on</span>':`<span class="chip static">${n.group==='loop'?'turn loop':n.group==='mem'?'memory':'supporting'}</span>`;308 const pinned=S.selected===n.id;309 let content='';310 if(S.tab==='what') content=`<p class="lede">${n.one}</p><details${pinned?' open':''}><summary>Read more</summary><p>${n.what}</p></details>${n.steps?`<details><summary>Steps in execution</summary>${n.steps.map((s,j)=>`<div class="step"><div class="no">${j+1}</div><div class="tx"><b>${esc(s[0])}</b> — ${esc(s[1])}</div></div>`).join('')}</details>`:''}`;311 else if(S.tab==='how') content=`<p class="lede">${n.one}</p><p>${n.how}</p>`;312 else content=`<p class="lede">${n.one}</p>`+(n.cond&&n.cond.length?qHtml(n.cond):`<p>Nothing open here right now.</p>`);313 body.innerHTML=`<div class="eyebrow">${esc(n.code)} · ${pinned?'pinned':'hovering'}${isNew(n.id)?' · new in this chapter':''}</div><h1 class="t">${esc(n.name)}</h1><div class="sub">${status}</div>${content}314 ${pinned?`<div class="actions">${n.steps?`<button class="ctl" onclick="goInside('${n.id}')">→ Go inside</button>`:''}<button class="ctl" onclick="select(null)">Back to chapter</button></div>`:''}`;315}316function renderChapterPanel(){317 const c=CH[S.ch]; const last=S.ch===CH.length-1;318 if(S.tab==='how'){ body.innerHTML=HOW_HTML; return; }319 if(S.tab==='cond'){ body.innerHTML=`<div class="eyebrow">${esc(TITLE)}</div><h1 class="t">Open questions</h1><div class="sub">${last?'everything unresolved, by structure':'for the structures revealed so far'}</div>`+NODES.filter(n=>revealedIds().includes(n.id)&&n.cond&&n.cond.length).map(n=>`<h3 class="sec">${n.code} · ${esc(n.name)}${n.ghost?' <span class="chip ghost static">later</span>':''}</h3>${qHtml(n.cond)}`).join(''); return; }320 const chips = last ? '' : `<h3 class="sec">New in this chapter</h3><div>${c.reveal.map(nodeChip).join('')}</div>`;321 const nav=`<div class="actions"><button class="ctl" onclick="prevCh()" ${S.ch===0?'disabled':''}>◂ Back</button><button class="ctl primary" onclick="nextCh()" ${last?'disabled':''}>${S.ch===CH.length-2?'Show the whole system ▸':'Next ▸'}</button></div>`;322 body.innerHTML=`<div class="eyebrow">Chapter ${S.ch+1} of ${CH.length}</div><h1 class="t">${esc(c.title)}</h1><p class="lede">${c.lede}</p>${c.story}${chips}${nav}323 ${last?`<h3 class="sec">Decisions locked</h3><ul>${DECISIONS_HTML}</ul>`:''}`;324}325document.querySelectorAll('#tabs button').forEach(b=>b.addEventListener('click',()=>{S.tab=b.dataset.tab;S.selPacket=null;renderPanel();drawPackets();}));326 327/* ---------------- STATS ---------------- */328function renderStats(){ const el=document.getElementById('stats'); const open=NODES.reduce((a,n)=>a+qOpen(n),0), routed=NODES.reduce((a,n)=>a+(n.cond||[]).filter(c=>c.to).length,0), res=NODES.reduce((a,n)=>a+(n.cond||[]).filter(c=>c.r).length,0);329 const dyn={chapter:`${S.ch+1} / ${CH.length}`, shown:`${revealedIds().length} / ${NODES.length}`, questions:`${open} open · ${routed} routed · ${res} resolved`};330 const all=[...(STATS||[]), {k:'Chapter',v:dyn.chapter},{k:'Structures shown',v:dyn.shown},{k:'Open questions',v:dyn.questions,grow:true}];331 el.innerHTML=all.map(s=>`<div class="stat${s.grow?' grow':''}"><div class="k">${s.k}</div><div class="v">${s.v}</div></div>`).join(''); }332 333/* ---------------- INDEX + RAIL ---------------- */334function renderIndex(){335 const ix=document.getElementById('index'); ix.innerHTML=''; const rev=new Set(revealedIds());336 GROUPS.forEach(g=>{ const h=document.createElement('h4'); h.textContent=g.title; ix.appendChild(h);337 NODES.filter(n=>n.group===g.id).forEach(n=>{ const b=document.createElement('button'); const shown=rev.has(n.id);338 b.className='ix'+(n.ghost&&shown?' ghost':'')+(!shown?' later':'')+(S.selected===n.id?' on':'')+(isNew(n.id)?' new':''); b.dataset.id=n.id;339 b.innerHTML=`<span class="code">${n.code}</span><span class="nm">${esc(n.name)}</span><span class="n" title="${shown?'open questions':'revealed in chapter'}">${shown?qOpen(n):'ch '+(revealChapterOf(n.id)+1)}</span>`;340 b.addEventListener('click',()=>{ if(!shown){ gotoCh(revealChapterOf(n.id)); select(n.id); return; } if(S.view!=='world')comeOut(); select(n.id); focusNode(n); });341 b.addEventListener('mouseenter',()=>{ if(S.view==='world'&&shown){S.hover=n.id;renderPanel();render();} });342 b.addEventListener('mouseleave',()=>{ if(S.view==='world'){S.hover=null;renderPanel();render();} });343 ix.appendChild(b); }); });344 renderStats();345}346function renderRail(){ const r=document.getElementById('rail'); if(S.view!=='world'){ r.innerHTML=`<span class="title">THE SYSTEM › <b>${esc(byId[S.inside].code)} ${esc(byId[S.inside].name)}</b> · steps in execution</span>`; document.getElementById('flowSel').style.display='none'; return; }347 r.innerHTML=CH.map((c,i)=>`<button class="ch ${i===S.ch?'on':i<S.ch?'done':''} ${i>S.maxCh+1?'locked':''}" title="${esc(c.title)}" onclick="gotoCh(${i})">${i+1}</button>`).join('')+`<span class="title">chapter ${S.ch+1} · <b>${esc(CH[S.ch].title)}</b></span>`;348 const fs=document.getElementById('flowSel'); fs.style.display = S.ch===CH.length-1?'flex':'none';349 if(!fs.dataset.built){ fs.innerHTML=[...FLOWS.map(f=>`<button data-f="${f.id}">${f.name}</button>`),'<button data-f="both">All flows</button>'].join(''); fs.dataset.built='1'; fs.querySelectorAll('button').forEach(b=>b.addEventListener('click',()=>{S.flowSel=b.dataset.f;buildWorldScene();initPackets();render();})); }350 fs.querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.f===S.flowSel));351 document.getElementById('btnBack').disabled=S.ch===0; document.getElementById('btnNext').disabled=S.ch===CH.length-1;352}353 354/* ---------------- NAV ---------------- */355function select(id){ S.selected=id; S.selPacket=null; renderIndex(); renderPanel(); render(); }356function gotoCh(i){ i=Math.max(0,Math.min(CH.length-1,i)); if(S.view!=='world'){S.view='world';S.inside=null;} S.ch=i; S.maxCh=Math.max(S.maxCh,i); S.selected=null; S.hover=null; S.tab='what'; buildWorldScene(); initPackets(); S.playing=true; syncFlowBtn(); renderIndex(); render(); renderPanel(); fitView(true); }357function nextCh(){ gotoCh(S.ch+1); } function prevCh(){ gotoCh(S.ch-1); }358function goInside(id){ const n=byId[id]; if(!n||!n.steps)return; S.view='inside'; S.inside=id; S.selected='s0'; buildInsideScene(n); initPackets(); fitView(true); renderIndex(); render(); renderPanel(); }359function comeOut(){ const was=S.inside; S.view='world'; S.inside=null; buildWorldScene(); initPackets(); S.selected=was; fitView(true); renderIndex(); render(); renderPanel(); }360function orderList(){ return S.view==='world'?sceneNodes.map(n=>n.id):sceneNodes.map(n=>n.id); }361document.addEventListener('keydown',e=>{ if(e.target.tagName==='INPUT')return; const L=orderList(); let i=L.indexOf(S.selected);362 if(e.key==='ArrowDown'){e.preventDefault();select(L[Math.min(L.length-1,i+1)]);}363 else if(e.key==='ArrowUp'){e.preventDefault();select(L[Math.max(0,i-1)]);}364 else if(e.key==='ArrowRight'){e.preventDefault(); if(S.view==='world'&&S.selected)goInside(S.selected);}365 else if(e.key==='ArrowLeft'){e.preventDefault(); if(S.view==='inside')comeOut(); else select(null);}366 else if(e.key==='Enter'||e.key===']'){e.preventDefault(); if(S.view==='world')nextCh();}367 else if(e.key==='['){e.preventDefault(); if(S.view==='world')prevCh();}368 else if(e.key===' '){e.preventDefault();toggleFlow();}369 else if(e.key==='Escape'){select(null);} });370 371/* ---------------- VIEW / PAN / ZOOM ---------------- */372let tween=null;373function applyView(){ world.setAttribute('transform',`translate(${S.tx},${S.ty}) scale(${S.k})`); }374function tweenTick(){ if(!tween)return; const now=performance.now(); let u=Math.min(1,(now-tween.t0)/tween.dur); u=1-Math.pow(1-u,3); S.tx=tween.a.tx+(tween.b.tx-tween.a.tx)*u; S.ty=tween.a.ty+(tween.b.ty-tween.a.ty)*u; S.k=tween.a.k+(tween.b.k-tween.a.k)*u; applyView(); if(u>=1)tween=null; }375function setView(tx,ty,k,anim){ tween=null; if(anim&&!matchMedia('(prefers-reduced-motion: reduce)').matches){ tween={a:{tx:S.tx,ty:S.ty,k:S.k},b:{tx,ty,k},t0:performance.now(),dur:520}; } else { S.tx=tx;S.ty=ty;S.k=k;applyView(); } }376function bbox(){ let x0=1e9,y0=1e9,x1=-1e9,y1=-1e9; sceneNodes.forEach(n=>{[P(n.gx,n.gy,n.h),P(n.gx+n.w,n.gy,n.h),P(n.gx+n.w,n.gy+n.d,-24),P(n.gx,n.gy+n.d,0),P(n.gx,n.gy,0)].forEach(p=>{x0=Math.min(x0,p[0]);y0=Math.min(y0,p[1]);x1=Math.max(x1,p[0]);y1=Math.max(y1,p[1]);});}); return {x0,y0,x1,y1}; }377function fitView(anim){ const r=svg.getBoundingClientRect(); if(!sceneNodes.length)return; const b=bbox(); const pad=40; const k=Math.min((r.width-pad*2)/Math.max(1,b.x1-b.x0),(r.height-pad*2)/Math.max(1,b.y1-b.y0)); if(r.width<80||r.height<80||!isFinite(k)||k<=0)return; const kk=Math.max(.2,Math.min(k,1.5)); setView(r.width/2-((b.x0+b.x1)/2)*kk, r.height/2-((b.y0+b.y1)/2)*kk+8, kk, anim); }378function focusNode(n){ const r=svg.getBoundingClientRect(); const c=P(n.gx+n.w/2,n.gy+n.d/2,n.h/2); setView(r.width/2-c[0]*S.k, r.height/2-c[1]*S.k, S.k, true); }379function zoomAt(f,cx,cy){ tween=null; const nk=Math.max(.35,Math.min(3,S.k*f)); S.tx=cx-(cx-S.tx)*(nk/S.k); S.ty=cy-(cy-S.ty)*(nk/S.k); S.k=nk; applyView(); }380svg.addEventListener('wheel',e=>{e.preventDefault(); const r=svg.getBoundingClientRect(); zoomAt(e.deltaY<0?1.1:0.9,e.clientX-r.left,e.clientY-r.top);},{passive:false});381/* Selection runs on pointerdown rather than click. render() and drawPackets() each382 clear their layer, and both run on hover, so the box or dot under the cursor is383 replaced between press and release — the browser then never synthesises a click on384 it and click-to-pin silently did nothing. Pointer capture is deferred until a drag385 actually starts for the same reason: capturing on pointerdown retargets the click386 to the svg, away from the node. */387function selectPacket(i){ S.selPacket=i; S.playing=false; syncFlowBtn(); renderPanel(); drawPackets(); }388let drag=null, lastTap={id:null,t:0};389svg.addEventListener('pointerdown',e=>{ if(e.button!==0)return; tween=null;390 const hit=e.target.closest?e.target.closest('g.node,g.pkt'):null, isPkt=hit&&hit.classList.contains('pkt');391 drag={x:e.clientX,y:e.clientY,tx:S.tx,ty:S.ty,moved:false,pid:e.pointerId,392 pkt:isPkt?+hit.dataset.i:null, id:hit&&!isPkt?hit.dataset.id:null}; });393svg.addEventListener('pointermove',e=>{ if(!drag)return; const dx=e.clientX-drag.x,dy=e.clientY-drag.y;394 if(!drag.moved){ if(Math.hypot(dx,dy)<=3)return; drag.moved=true; svg.classList.add('dragging'); try{svg.setPointerCapture(drag.pid);}catch(_){} }395 S.tx=drag.tx+dx; S.ty=drag.ty+dy; applyView(); });396svg.addEventListener('pointercancel',()=>{ drag=null; svg.classList.remove('dragging'); });397svg.addEventListener('pointerup',()=>{ const d=drag; drag=null; svg.classList.remove('dragging');398 if(!d||d.moved)return; // a pan, not a tap — leave selection alone399 if(d.pkt!=null){ selectPacket(d.pkt); return; }400 if(d.id==null){ select(null); return; } // empty canvas clears the pin401 const n=sceneNodes.find(x=>x.id===d.id), now=performance.now();402 if(lastTap.id===d.id&&now-lastTap.t<400){ lastTap={id:null,t:0}; if(n&&!n.inside&&n.steps){ goInside(d.id); return; } }403 else lastTap={id:d.id,t:now};404 select(d.id); });405document.getElementById('zin').onclick=()=>{const r=svg.getBoundingClientRect();zoomAt(1.2,r.width/2,r.height/2);};406document.getElementById('zout').onclick=()=>{const r=svg.getBoundingClientRect();zoomAt(1/1.2,r.width/2,r.height/2);};407window.addEventListener('resize',()=>fitView(false));408 409/* ---------------- TIP ---------------- */410function showTip(e,n){ tip.style.display='block'; tip.textContent=n.inside?n.desc:(n.ghost?'Later · ':'')+n.name+' — '+(n.one||'').replace(/<[^>]+>/g,''); moveTip(e); }411function moveTip(e){ const r=svg.getBoundingClientRect(); let x=e.clientX-r.left+14,y=e.clientY-r.top+14; if(x+290>r.width)x-=300; if(y+60>r.height)y-=70; tip.style.left=x+'px'; tip.style.top=y+'px'; }412function hideTip(){ tip.style.display='none'; }413 414/* ---------------- CONTROLS ---------------- */415function toggleFlow(){ S.playing=!S.playing; if(S.playing)S.selPacket=null; syncFlowBtn(); renderPanel(); drawPackets(); }416document.getElementById('btnFlow').onclick=toggleFlow;417document.getElementById('btnStep').onclick=stepOnce;418document.getElementById('btnReset').onclick=()=>{ select(null); fitView(true); };419document.getElementById('btnNext').onclick=nextCh; document.getElementById('btnBack').onclick=prevCh;420 421/* ---------------- BOOT ---------------- */422gotoCh(0);423</script>424