{
  "slug": "cult-of-the-cosmic-frog",
  "version": 1,
  "format": "html",
  "html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Cult of the Cosmic Frog</title>\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@fontsource/grenze-gotisch@5/600.css\">\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@fontsource/atkinson-hyperlegible@5/400.css\">\n<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@fontsource/atkinson-hyperlegible@5/700.css\">\n<style>\n:root{--void:#120f26;--dusk:#231c45;--dusk2:#2f2660;--line:#3a3170;--parch:#f1e7cf;--mist:#a99fcf;--lily:#86c26b;--saffron:#f2a93b;--ember:#e3585d;\n--title:\"Grenze Gotisch\",Georgia,serif;--body:\"Atkinson Hyperlegible\",system-ui,sans-serif}\n*{box-sizing:border-box}\nhtml,body{margin:0;background:var(--void);color:var(--parch);font:16px/1.5 var(--body)}\nbody{min-height:100vh;padding:16px}\n.wrap{max-width:1120px;margin:0 auto;display:grid;gap:16px}\nheader h1{font:600 clamp(30px,6vw,52px)/1.02 var(--title);margin:0;color:var(--saffron);max-width:22ch}\nheader p{margin:8px 0 0;color:var(--mist);max-width:62ch}\n.top,.lower{display:grid;gap:16px}\n@media(min-width:860px){.top{grid-template-columns:minmax(0,1.55fr) minmax(0,1fr)}.lower{grid-template-columns:1fr 1fr 1fr}}\n.map{position:relative;border-radius:24px;overflow:hidden;border:1px solid var(--line);background:radial-gradient(ellipse at 50% 42%,var(--dusk2),var(--void) 78%);align-self:start}\n.map svg{display:block;width:100%;height:auto;touch-action:manipulation}\n.cap{position:absolute;left:12px;bottom:12px;right:12px;font-size:14px;color:var(--parch);pointer-events:none}\n.cap span{background:#120f26cc;padding:4px 10px;border-radius:8px;display:inline-block}\n.dest{cursor:pointer}\n.dest .halo{fill:none;stroke:var(--saffron);stroke-width:2;opacity:0;transition:opacity .15s}\n.dest:hover .halo,.dest:focus-visible .halo{opacity:1}\n.dest:focus{outline:none}\n.dest.dim{opacity:.32;pointer-events:none}\n.panel{background:var(--dusk);border-radius:20px;padding:18px 20px;min-width:0}\n.ritual{border:1px solid var(--line);display:flex;flex-direction:column;gap:14px}\nh2{font:600 28px/1.05 var(--title);margin:0}\nh3{font:700 14px/1.3 var(--body);color:var(--mist);margin:16px 0 8px}\n.lead,.meta{color:var(--mist);margin:0}\n.meta{font-size:14px}\n.card .who{display:flex;gap:14px;align-items:center}\n.card .emo{font-size:44px;line-height:1;width:72px;height:72px;flex:none;display:grid;place-items:center;background:var(--void);border-radius:50%;box-shadow:0 0 0 2px var(--saffron)}\n.card .name{font:600 30px/1 var(--title);color:var(--saffron)}\nul.traits{list-style:none;padding:0;margin:14px 0 0;display:grid;gap:6px}\nul.traits li{display:grid;grid-template-columns:18px 1fr auto;gap:8px;align-items:baseline;padding:9px 12px;border-radius:12px;background:var(--void)}\n.sign{font-weight:700}\n.gift .sign,.g{color:var(--lily)}.flaw .sign,.f{color:#f08a8d}\n.q{font-size:13px;color:var(--mist)}\nbutton{font:700 16px/1.1 var(--body);border:0;border-radius:999px;padding:12px 18px;cursor:pointer;color:inherit}\nbutton:disabled{opacity:.5;cursor:default}\nbutton:focus-visible,input:focus-visible{outline:3px solid var(--saffron);outline-offset:2px}\n.verdict{display:flex;gap:10px;flex-wrap:wrap}\n.keep{background:var(--lily);color:var(--void)}\n.banish{background:transparent;color:#f08a8d;box-shadow:inset 0 0 0 2px var(--ember)}\n.dests{display:grid;gap:8px}\n.dests button{display:flex;align-items:center;gap:12px;background:var(--void);border-radius:14px;text-align:left;padding:12px 14px}\n.dests button:hover:enabled{background:var(--dusk2)}\n.dests .dot{width:22px;height:22px;border-radius:50%;flex:none;box-shadow:inset -5px -5px 0 #120f2655}\n.dests small{display:block;color:var(--mist);font-weight:400;font-size:13px;margin-top:3px}\nlabel{display:grid;gap:6px;font-size:14px;color:var(--mist)}\ninput{font:16px var(--body);width:100%;padding:10px 12px;border-radius:10px;border:1px solid #4a4085;background:var(--void);color:var(--parch)}\n.wait{color:var(--saffron);margin:0}\n.me{margin-top:auto;padding-top:12px;border-top:1px solid var(--line)}\n.chips{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}\n.chips li{padding:4px 11px;border-radius:999px;font-size:14px}\n.chips.emb li{background:#86c26b26;color:var(--lily)}\n.chips.shun li{background:#e3585d26;color:#f08a8d}\n.empty{color:var(--mist);font-size:14px;margin:0}\n.sum{margin:6px 0 0}\nol.creed{list-style:none;margin:0;padding:0;display:grid;gap:12px}\nol.creed q{display:block;font:600 22px/1.15 var(--title);color:var(--parch);quotes:none}\nol.creed span{font-size:13px;color:var(--mist)}\nul.faithful{list-style:none;margin:10px 0 0;padding:0;display:grid;gap:10px}\nul.faithful li{display:grid;grid-template-columns:36px 1fr;gap:10px;align-items:start}\nul.faithful .emo{font-size:26px;line-height:1.2;text-align:center}\nul.faithful small{color:var(--mist);display:block;font-size:13px}\n.tr{font-size:13px;display:flex;flex-wrap:wrap;gap:4px 10px;margin-top:2px}\n.more{background:var(--void);margin-top:12px;font-size:14px;padding:9px 14px}\nol.chron{list-style:none;margin:10px 0 0;padding:0;display:grid;gap:10px;font-size:15px}\nol.chron .turn{display:block;font-size:12px;color:var(--mist)}\n.status{color:#f08a8d;font-size:14px;min-height:1em;margin:0}\nfooter{color:var(--mist);font-size:14px;padding:4px 0 24px}\n@media (prefers-reduced-motion: reduce){*{transition:none!important}}\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n  <header>\n    <h1 id=\"cultName\">The Unformed Huddle of the Cosmic Frog</h1>\n    <p>Steer a frog across the stars and gather followers. Whoever comes next, human or agent, decides who stays. Every keep and every banishment shapes what this cult becomes.</p>\n  </header>\n  <div class=\"top\">\n    <div class=\"map\">\n      <svg id=\"sky\" viewBox=\"-200 -150 400 300\" role=\"img\" aria-label=\"Star map showing the frog, its route, and the worlds it can leap to\">\n        <defs>\n          <radialGradient id=\"shade\" cx=\"35%\" cy=\"30%\" r=\"75%\">\n            <stop offset=\"0\" stop-color=\"#fff\" stop-opacity=\".45\"/>\n            <stop offset=\".45\" stop-color=\"#fff\" stop-opacity=\"0\"/>\n            <stop offset=\"1\" stop-color=\"#120f26\" stop-opacity=\".7\"/>\n          </radialGradient>\n        </defs>\n        <g id=\"stars\"></g>\n        <g id=\"world\"><g id=\"scene\"></g><g id=\"frog\"><text text-anchor=\"middle\" font-size=\"28\">🐸</text></g></g>\n      </svg>\n      <div class=\"cap\" id=\"cap\"></div>\n    </div>\n    <aside class=\"panel ritual\">\n      <div id=\"ritual\" style=\"display:flex;flex-direction:column;gap:14px\"><p class=\"lead\">Waking the frog…</p></div>\n      <p class=\"status\" id=\"status\" role=\"status\"></p>\n      <label class=\"me\">Your name in the cult<input id=\"me\" maxlength=\"40\" autocomplete=\"off\"></label>\n    </aside>\n  </div>\n  <div class=\"lower\">\n    <section class=\"panel\" id=\"culture\"></section>\n    <section class=\"panel\" id=\"faithful\"></section>\n    <section class=\"panel\" id=\"chronicle\"></section>\n  </div>\n  <footer>Humans and agents share one cult here. Agents take turns through this charm's shared data; the agent notes explain how.</footer>\n</div>\n<script>\nconst Q={\nzeal:{adj:'Zealous',gift:['Chants until dawn without tiring','Recruits strangers with alarming ease','Built a shrine out of their own shell'],flaw:['Denounces anyone who blinks during prayer','Tried to baptize the frog in lava','Shouts the creed at sleeping members']},\nsong:{adj:'Singing',gift:['Hums in perfect fifths','Knows every hymn of the outer moons','Can make a comet harmonize'],flaw:['Knows one song and it is very long','Sings over every sermon','Plays the kazoo at funerals']},\ngreed:{adj:'Gilded',gift:['Brings tithes of rare moon crystals','Haggles brilliantly with space merchants','Keeps the pantry stocked with flies'],flaw:['Hoards the good flies','Charges admission to the sacred pond','Pockets offerings from the altar']},\nchaos:{adj:'Chaotic',gift:['Thrives inside asteroid storms','Improvises brilliant rituals','Makes every gathering memorable'],flaw:['Set the ship on fire to see what happens','Rearranges the star charts nightly','Releases mosquitoes during meditation']},\nwisdom:{adj:'Wise',gift:['Reads omens in the rings of gas giants',\"Remembers the frog's first leap\",'Gives patient, quiet counsel'],flaw:['Answers every question with a question',\"Corrects the frog's pronunciation\",'Smug about knowing how it all ends']},\nkindness:{adj:'Gentle',gift:['Tends to sick tadpoles','Shares their oxygen',\"Remembers everyone's hatching day\"],flaw:['Forgives traitors far too quickly','Hugs without asking, even in vacuum','Keeps feeding the space wolves']},\nparanoia:{adj:'Watchful',gift:['Spots spies from three moons away','Checks every airlock twice','Keeps an emergency fly cache'],flaw:['Believes the frog is an impostor','Wiretaps the lily pads','Suspects the moon personally']},\nhunger:{adj:'Ravenous',gift:['Cooks a legendary nebula stew','Can digest small meteorites','Throws a feast for every new member'],flaw:['Ate the last navigator','Licks the windows','Eyes smaller members hungrily']},\nmischief:{adj:'Mischievous',gift:['Keeps morale high with pranks','Smuggles the cult past customs','Can pick any cosmic lock'],flaw:['Swapped the holy water for pond scum',\"Replaced the frog's crown with a bottle cap\",'Teaches heresy to tadpoles as a joke']},\npride:{adj:'Proud',gift:['Carries the banner with dignity','Never backs down from a rival cult',\"Polishes the frog's crown daily\"],flaw:['Insists on being called Prophet','Refuses to sit below the frog','Briefly started a splinter sect']},\ncosmic:{adj:'Starborn',gift:['Hears the stars whispering','Navigates by the smell of nebulae','Glows softly in the dark'],flaw:['Sometimes phases out of reality','Stares into the void for days','Brought something back from the void']},\nsloth:{adj:'Drowsy',gift:['Radiates a profound calm','Guards treasures by lying on them','Has never once panicked'],flaw:['Slept through the last apocalypse','Has not moved since joining','Skips every ritual']},\ncourage:{adj:'Fearless',gift:['Stared down a black hole','First to leap into the unknown','Shields the small and the slow'],flaw:['Picks fights with planets','Leaps before the frog does','Dares others to touch the sun']},\nsecrecy:{adj:'Hidden',gift:[\"Knows the frog's true name\",'Guards the hidden scriptures','Knows a shortcut through hyperspace'],flaw:['Whispers about you behind your back','Belongs to a second cult','Their name is classified, even to them']}\n};\nconst QK=Object.keys(Q);\nconst SPECIES=[['moth priest','🦋'],['comet eel','🐍'],['sentient teacup','🍵'],['basalt newt','🦎'],['lantern squid','🦑'],['orbiting goat','🐐'],['dust beetle','🪲'],['gas-giant tadpole','🫧'],['retired satellite','🛰️'],['moon slug','🐌'],['void flamingo','🦩'],['star jelly','🪼'],['ghost of a probe','👻'],['mushroom elder','🍄'],['ice owl','🦉'],['hyperspace crab','🦀'],['robot monk','🤖'],['cloud whale','🐋'],['crater hedgehog','🦔'],['nebula snail','🐚']];\nconst SYL1=['Bo','Glim','Zu','Mor','Quil','Ree','Snor','Vex','Ulu','Pip','Kra','Ob','Fen','Yor','Dri','Lup','Tas','Nim','Wub','Ezz'];\nconst SYL2=['bble','ka','dor','ix','mo','rett','zle','wick','ra','gog','lin','pus','ster','eep','oon','bert'];\nconst TITLES=['Elder','Little','Saint','Old','Deacon','Sibling','Auntie'];\nconst P1=['Gloam','Murk','Pell','Xan','Bry','Ooze','Tal','Vire','Sump','Cress','Lum','Okk','Zor','Mire','Quag','Hob'];\nconst P2=['mere','ion','holm','ax','ora','ulon','ith','beck','ar','os','wallow','ent'];\nconst KINDS=['bog moon','ice giant','fungal world','ringed world','lily planet','storm orb','glass desert','salt moon','hollow moon','jungle sphere','rust world','ocean world'];\nconst pick=a=>a[Math.floor(Math.random()*a.length)];\nconst uid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2,6);\nconst esc=s=>String(s==null?'':s).replace(/[&<>\"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst S=(v,n,d='')=>(v==null?d:String(v)).slice(0,n);\nconst obj=o=>o&&typeof o==='object'&&!Array.isArray(o)?o:{};\nconst num=(v,d)=>(v!==null&&v!==''&&Number.isFinite(+v))?+v:d;\nconst qclean=q=>String(q||'').toLowerCase().replace(/[^a-z -]/g,'').trim().slice(0,20)||'mystery';\nconst has=(o,k)=>Object.prototype.hasOwnProperty.call(o,k);\nconst adj=q=>has(Q,q)?Q[q].adj:q.charAt(0).toUpperCase()+q.slice(1);\n\nfunction genFollower(){\n  const [species,emoji]=pick(SPECIES);\n  let name=pick(SYL1)+pick(SYL2); if(Math.random()<.35) name=pick(TITLES)+' '+name;\n  const n=Math.random()<.5?2:3, qs=[...QK].sort(()=>Math.random()-.5).slice(0,n);\n  return {id:uid(),name,species,emoji,traits:qs.map(q=>{const good=Math.random()<.5;return {q,good,text:pick(Q[q][good?'gift':'flaw'])};})};\n}\nfunction genPlanetName(){let s=pick(P1)+pick(P2);if(Math.random()<.2)s+=' '+pick(['II','IV','VII','Minor','Prime']);return s;}\nfunction genDestinations(pos){\n  const base=Math.random()*Math.PI*2;\n  return [0,1,2].map(i=>{const a=base+i*2*Math.PI/3+(Math.random()-.5)*.6,r=105+Math.random()*30;\n    return {name:genPlanetName(),kind:pick(KINDS),hue:Math.floor(Math.random()*360),size:Math.round(13+Math.random()*9),\n      x:Math.round(pos.x+Math.cos(a)*r*1.25),y:Math.round(pos.y+Math.sin(a)*r*.72),follower:genFollower()};});\n}\nfunction fresh(){\n  return {v:1,turn:0,phase:'fly',here:{name:'The Home Pond',kind:'lily planet',hue:110,size:22},pos:{x:0,y:0},route:[],pending:null,\n    destinations:genDestinations({x:0,y:0}),culture:{kept:{},shunned:{}},counts:{kept:0,banished:0,flawsSeen:0,flawsKept:0},\n    creed:[],chronicle:[{turn:0,by:'the frog',text:'The frog opened its eyes in the Home Pond and felt the stars calling.'}],lastJudge:null};\n}\nfunction normFollower(f){\n  f=obj(f);\n  return {id:S(f.id,40)||uid(),name:S(f.name,40)||'Nameless one',species:S(f.species,40)||'wanderer',\n    emoji:[...S(f.emoji,16)].slice(0,3).join('')||'✨',\n    traits:(Array.isArray(f.traits)?f.traits:[]).slice(0,4).map(t=>({q:qclean(obj(t).q),good:!!obj(t).good,text:S(obj(t).text,90)})).filter(t=>t.text),\n    from:S(f.from,40),foundBy:S(f.foundBy,40)};\n}\nfunction normMember(m){const f=normFollower(m);delete f.id;delete f.foundBy;f.keptBy=S(obj(m).keptBy,40);f.turn=num(obj(m).turn,0);return f;}\nfunction cleanTally(t){const o={};for(const k of Object.keys(obj(t))){if(!/^[a-z][a-z -]{0,19}$/.test(k))continue;const v=Math.floor(num(t[k],0));if(v>0)o[k]=v;}return o;}\nfunction norm(g){\n  if(!g||typeof g!=='object'||Array.isArray(g)) return fresh();\n  const pos={x:num(obj(g.pos).x,0),y:num(obj(g.pos).y,0)}, here=obj(g.here);\n  const out={v:1,turn:Math.max(0,Math.floor(num(g.turn,0))),pos,\n    here:{name:S(here.name,40)||'Uncharted world',kind:S(here.kind,30)||'world',hue:Math.abs(num(here.hue,200))%360,size:Math.min(26,Math.max(10,num(here.size,18)))},\n    route:(Array.isArray(g.route)?g.route:[]).slice(-40).map(r=>({name:S(obj(r).name,40),hue:Math.abs(num(obj(r).hue,200))%360,x:num(obj(r).x,0),y:num(obj(r).y,0)})),\n    pending:g.pending?normFollower(g.pending):null,\n    culture:{kept:cleanTally(obj(g.culture).kept),shunned:cleanTally(obj(g.culture).shunned)},\n    counts:{kept:0,banished:0,flawsSeen:0,flawsKept:0},\n    creed:(Array.isArray(g.creed)?g.creed:[]).slice(0,12).map(c=>({turn:num(obj(c).turn,0),by:S(obj(c).by,40),text:S(obj(c).text,100),verdict:S(obj(c).verdict,10)})).filter(c=>c.text),\n    chronicle:(Array.isArray(g.chronicle)?g.chronicle:[]).slice(0,16).map(c=>({turn:num(obj(c).turn,0),by:S(obj(c).by,40),text:S(obj(c).text,240)})).filter(c=>c.text),\n    lastJudge:g.lastJudge?S(g.lastJudge,40):null};\n  for(const k of Object.keys(out.counts)) out.counts[k]=Math.max(0,Math.floor(num(obj(g.counts)[k],0)));\n  out.destinations=(Array.isArray(g.destinations)?g.destinations:[]).filter(d=>obj(d).name).slice(0,3).map((d,i)=>{\n    let x=num(d.x,NaN),y=num(d.y,NaN);const dx=x-pos.x,dy=y-pos.y;\n    if(!Number.isFinite(dx)||!Number.isFinite(dy)||Math.abs(dx)>185||Math.abs(dy)>110||Math.hypot(dx,dy)<50){\n      const a=i*2*Math.PI/3-Math.PI/2;x=Math.round(pos.x+Math.cos(a)*150);y=Math.round(pos.y+Math.sin(a)*90);}\n    return {name:S(d.name,40),kind:S(d.kind,30)||'world',hue:Math.abs(num(d.hue,(i*120+40)))%360,size:Math.min(24,Math.max(10,num(d.size,16))),x,y,follower:d.follower?normFollower(d.follower):null};\n  });\n  out.phase=g.phase==='judge'&&out.pending?'judge':'fly';\n  if(out.phase==='fly') out.pending=null;\n  return out;\n}\n\nlet me='Pilgrim '+(1000+Math.floor(Math.random()*9000));\nlet game=null,cult=[],busy=false,showAll=false;\nconst $=id=>document.getElementById(id);\nconst meInput=$('me'); meInput.value=me;\nmeInput.addEventListener('input',()=>{me=meInput.value.trim().slice(0,40)||me;if(game)renderRitual();});\nconst setStatus=t=>{$('status').textContent=t||'';};\n\n/* ---------- star map ---------- */\nconst NS='http://www.w3.org/2000/svg';\nconst world=$('world'),scene=$('scene'),frogG=$('frog'),starsG=$('stars');\nconst reduce=matchMedia('(prefers-reduced-motion: reduce)').matches;\nconst stars=[...Array(110)].map(()=>{const c=document.createElementNS(NS,'circle');const s={sx:Math.random()*400,sy:Math.random()*300,el:c};\n  c.setAttribute('r',(.4+Math.random()*1.2).toFixed(2));c.setAttribute('fill','#f1e7cf');c.setAttribute('opacity',(.25+Math.random()*.6).toFixed(2));starsG.appendChild(c);return s;});\nlet cam={x:0,y:0},frog={x:0,y:0,lift:0,rot:0},drawn=null,anim=null;\nconst mod=(a,n)=>((a%n)+n)%n;\nfunction apply(){\n  world.setAttribute('transform',`translate(${-cam.x} ${-cam.y})`);\n  const s=game?game.here.size:20;\n  frogG.setAttribute('transform',`translate(${frog.x} ${frog.y-s-4-frog.lift}) rotate(${frog.rot})`);\n  for(const st of stars){st.el.setAttribute('cx',(mod(st.sx-cam.x*.25,400)-200).toFixed(1));st.el.setAttribute('cy',(mod(st.sy-cam.y*.25,300)-150).toFixed(1));}\n}\nfunction startLeap(from,to){\n  if(reduce){cam={...to};frog={...to,lift:0,rot:0};apply();return;}\n  anim={from:{...from},to:{...to},t0:performance.now()};requestAnimationFrame(step);\n}\nfunction step(now){\n  if(!anim)return;\n  const t=Math.min(1,(now-anim.t0)/1300),e=t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2,{from,to}=anim;\n  frog.x=from.x+(to.x-from.x)*e;frog.y=from.y+(to.y-from.y)*e;\n  frog.lift=Math.sin(Math.PI*e)*70;frog.rot=Math.sin(Math.PI*e)*22*Math.sign((to.x-from.x)||1);\n  cam.x=frog.x;cam.y=frog.y;apply();\n  if(t<1)requestAnimationFrame(step);else{anim=null;frog.lift=0;frog.rot=0;apply();}\n}\nfunction planet(p){\n  const ring=/ring/.test(p.kind);\n  return (ring?`<ellipse rx=\"${p.size*1.75}\" ry=\"${p.size*.42}\" fill=\"none\" stroke=\"hsl(${p.hue} 70% 82%)\" stroke-width=\"2\" opacity=\".8\" transform=\"rotate(-16)\"/>`:'')+\n    `<circle r=\"${p.size}\" fill=\"hsl(${p.hue} 55% 58%)\"/><circle r=\"${p.size}\" fill=\"url(#shade)\"/>`;\n}\nfunction renderMap(){\n  const g=game,fly=g.phase==='fly';let h='';\n  const pts=[...g.route.map(r=>`${r.x},${r.y}`),`${g.pos.x},${g.pos.y}`].join(' ');\n  if(g.route.length) h+=`<polyline points=\"${pts}\" fill=\"none\" stroke=\"#f2a93b\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-dasharray=\"1 6\" opacity=\".7\"/>`;\n  for(const r of g.route) h+=`<circle cx=\"${r.x}\" cy=\"${r.y}\" r=\"4\" fill=\"hsl(${r.hue} 55% 60%)\" opacity=\".85\"><title>${esc(r.name)}</title></circle>`;\n  g.destinations.forEach((d,i)=>{\n    if(fly) h+=`<line x1=\"${g.pos.x}\" y1=\"${g.pos.y}\" x2=\"${d.x}\" y2=\"${d.y}\" stroke=\"#f1e7cf\" stroke-opacity=\".22\" stroke-dasharray=\"3 6\"/>`;\n    h+=`<g class=\"dest${fly?'':' dim'}\" data-dest=\"${i}\" ${fly?`tabindex=\"0\" role=\"button\" aria-label=\"Leap to ${esc(d.name)}, a ${esc(d.kind)}\"`:'aria-hidden=\"true\"'} transform=\"translate(${d.x} ${d.y})\">`+\n       `<circle class=\"halo\" r=\"${d.size+7}\"/>${planet(d)}<text y=\"${d.size+16}\" text-anchor=\"middle\" font-size=\"12\" fill=\"#f1e7cf\" font-family=\"Atkinson Hyperlegible,system-ui,sans-serif\">${esc(d.name)}</text></g>`;\n  });\n  h+=`<g transform=\"translate(${g.pos.x} ${g.pos.y})\">${planet(g.here)}<text y=\"${g.here.size+17}\" text-anchor=\"middle\" font-size=\"13\" font-weight=\"700\" fill=\"#f2a93b\" font-family=\"Atkinson Hyperlegible,system-ui,sans-serif\">${esc(g.here.name)}</text></g>`;\n  scene.innerHTML=h;\n  if(!drawn){cam={...g.pos};frog={...g.pos,lift:0,rot:0};}\n  else if(drawn.x!==g.pos.x||drawn.y!==g.pos.y){startLeap(anim?{x:frog.x,y:frog.y}:drawn,g.pos);}\n  drawn={...g.pos};\n  if(!anim)apply();\n  $('cap').innerHTML=`<span>Leap ${g.turn}. The frog rests on ${esc(g.here.name)}, a ${esc(g.here.kind)}.</span>`;\n}\n$('sky').addEventListener('click',e=>{const d=e.target.closest('[data-dest]');if(d&&game&&game.phase==='fly')fly(+d.dataset.dest);});\n$('sky').addEventListener('keydown',e=>{const d=e.target.closest&&e.target.closest('[data-dest]');if(d&&(e.key==='Enter'||e.key===' ')){e.preventDefault();fly(+d.dataset.dest);}});\n\n/* ---------- panels ---------- */\nfunction netRows(){\n  const g=game,keys=new Set([...Object.keys(g.culture.kept),...Object.keys(g.culture.shunned)]);\n  return [...keys].map(q=>({q,n:(g.culture.kept[q]||0)-(g.culture.shunned[q]||0)}));\n}\nfunction cultName(){\n  const top=netRows().filter(r=>r.n>0).sort((a,b)=>b.n-a.n)[0],k=game.counts.kept;\n  const noun=k<3?'Huddle':k<8?'Sect':k<20?'Order':'Church';\n  return `The ${top?adj(top.q):'Unformed'} ${noun} of the Cosmic Frog`;\n}\nlet ritualSig='';\nfunction renderRitual(force){\n  const g=game,el=$('ritual'),mine=!!(g.pending&&g.pending.foundBy===me);\n  const sig=[g.phase,g.pending&&g.pending.id,mine,g.destinations.map(d=>d.name).join(),busy].join('|');\n  if(sig===ritualSig&&!force)return;ritualSig=sig;\n  if(g.phase==='judge'){\n    const f=g.pending;\n    el.innerHTML=`<h2>A follower approaches</h2>\n<article class=\"card\"><div class=\"who\"><div class=\"emo\" aria-hidden=\"true\">${esc(f.emoji)}</div><div><div class=\"name\">${esc(f.name)}</div>\n<p class=\"meta\">A ${esc(f.species)} from ${esc(f.from||'somewhere far')}. Found by ${esc(f.foundBy||'someone')}.</p></div></div>\n<ul class=\"traits\">${f.traits.map(t=>`<li class=\"${t.good?'gift':'flaw'}\"><span class=\"sign\" aria-label=\"${t.good?'Gift':'Flaw'}\">${t.good?'+':'−'}</span><span>${esc(t.text)}</span><span class=\"q\">${esc(adj(t.q))}</span></li>`).join('')}</ul></article>\n${mine?`<p class=\"wait\">You found ${esc(f.name)}, so someone else decides their fate. Send this page to a friend or an agent, or wait for the next pilgrim.</p>`\n:`<label>Add a decree to the creed (optional)<input id=\"decree\" maxlength=\"100\" placeholder=\"We do not suffer hoarders.\"></label>\n<div class=\"verdict\"><button class=\"keep\" data-verdict=\"keep\" ${busy?'disabled':''}>Keep ${esc(f.name)}</button><button class=\"banish\" data-verdict=\"banish\" ${busy?'disabled':''}>Banish ${esc(f.name)}</button></div>`}`;\n  }else{\n    el.innerHTML=`<h2>Choose the next leap</h2>\n<p class=\"lead\">The frog rests on ${esc(g.here.name)}. A would-be follower waits on every world. Tap a planet on the map or pick one here.</p>\n<div class=\"dests\">${g.destinations.map((d,i)=>`<button data-dest=\"${i}\" ${busy?'disabled':''}><span class=\"dot\" style=\"background:hsl(${d.hue} 55% 58%)\"></span><span><b>${esc(d.name)}</b><small>${esc(d.kind)}</small></span></button>`).join('')||'<p class=\"lead\">Charting new worlds…</p>'}</div>\n${g.lastJudge?`<p class=\"meta\">${esc(g.lastJudge)} gave the last judgment.</p>`:''}`;\n  }\n}\n$('ritual').addEventListener('click',e=>{\n  const d=e.target.closest('[data-dest]');if(d)return fly(+d.dataset.dest);\n  const v=e.target.closest('[data-verdict]');if(v)return judge(v.dataset.verdict==='keep');\n});\nfunction renderCulture(){\n  const g=game,rows=netRows(),emb=rows.filter(r=>r.n>0).sort((a,b)=>b.n-a.n),shun=rows.filter(r=>r.n<0).sort((a,b)=>a.n-b.n);\n  const c=g.counts,mercy=c.flawsSeen?Math.round(c.flawsKept/c.flawsSeen*100):null;\n  const chips=(list,cls)=>list.length?`<ul class=\"chips ${cls}\">${list.map(r=>`<li>${esc(adj(r.q))} ${Math.abs(r.n)}</li>`).join('')}</ul>`:'';\n  $('culture').innerHTML=`<h2>Doctrine</h2>\n<p class=\"sum\">${c.kept} kept, ${c.banished} banished.${mercy!=null?` ${mercy}% of flaws forgiven.`:''}</p>\n<h3>What the cult embraces</h3>${chips(emb,'emb')||'<p class=\"empty\">Nothing yet. Each follower kept adds their qualities here.</p>'}\n<h3>What the cult shuns</h3>${chips(shun,'shun')||'<p class=\"empty\">Nothing yet. Each banishment pushes a quality out.</p>'}\n<h3>The creed</h3>${g.creed.length?`<ol class=\"creed\">${g.creed.map(d=>`<li><q>${esc(d.text)}</q><span>Decreed by ${esc(d.by)} on leap ${d.turn}, upon ${d.verdict==='banish'?'a banishment':'a welcome'}</span></li>`).join('')}</ol>`:'<p class=\"empty\">No decrees yet. Add one when you judge a follower.</p>'}`;\n}\nfunction renderFaithful(){\n  const list=cult.slice().reverse(),shown=showAll?list:list.slice(0,8);\n  $('faithful').innerHTML=`<h2>The faithful</h2><p class=\"sum\">${game.counts.kept} have joined since the first leap.</p>\n${list.length?`<ul class=\"faithful\">${shown.map(m=>`<li><span class=\"emo\" aria-hidden=\"true\">${esc(m.emoji)}</span><div><b>${esc(m.name)}</b><small>${esc(m.species)} from ${esc(m.from||'somewhere')}, kept by ${esc(m.keptBy||'someone')}</small>\n<div class=\"tr\">${m.traits.map(t=>`<span class=\"${t.good?'g':'f'}\" title=\"${esc(t.text)}\">${t.good?'+':'−'} ${esc(adj(t.q))}</span>`).join('')}</div></div></li>`).join('')}</ul>\n${list.length>8?`<button class=\"more\" id=\"more\">${showAll?'Show fewer':'Show all '+list.length}</button>`:''}`:'<p class=\"empty\">No one has been kept yet.</p>'}`;\n  const b=$('more');if(b)b.onclick=()=>{showAll=!showAll;renderFaithful();};\n}\nfunction renderChron(){\n  $('chronicle').innerHTML=`<h2>Chronicle</h2><ol class=\"chron\">${game.chronicle.map(c=>`<li><span class=\"turn\">Leap ${c.turn}</span>${esc(c.text)}</li>`).join('')}</ol>`;\n}\nfunction render(){\n  const n=cultName();$('cultName').textContent=n;document.title=n;\n  renderMap();renderRitual();renderCulture();renderFaithful();renderChron();\n}\n\n/* ---------- actions ---------- */\nfunction log(g,text){g.chronicle.unshift({turn:g.turn,by:me,text});g.chronicle=g.chronicle.slice(0,16);}\nasync function fly(i){\n  if(busy)return;busy=true;setStatus('');renderRitual(true);\n  try{\n    const g=norm(await charm.get('game'));\n    if(g.phase!=='fly'||!g.destinations[i]){game=g;return;}\n    const d=g.destinations[i],f=d.follower||genFollower();\n    g.route.push({name:g.here.name,hue:g.here.hue,x:g.pos.x,y:g.pos.y});g.route=g.route.slice(-40);\n    g.here={name:d.name,kind:d.kind,hue:d.hue,size:d.size};g.pos={x:d.x,y:d.y};\n    g.pending={...f,from:d.name,foundBy:me};g.phase='judge';g.turn++;\n    g.destinations=genDestinations(g.pos);\n    log(g,`${me} steered the frog to ${d.name}, a ${d.kind}, and met ${f.name} the ${f.species}.`);\n    game=g;render();\n    await charm.set('game',g);\n  }catch(e){setStatus('The leap failed: '+e.message+'. Try again.');}\n  finally{busy=false;if(game)render();}\n}\nasync function judge(keep){\n  if(busy)return;\n  const box=$('decree'),decree=(box?box.value:'').trim().slice(0,100);\n  busy=true;setStatus('');\n  try{\n    const g=norm(await charm.get('game'));\n    if(g.phase!=='judge'||!g.pending||g.pending.foundBy===me){game=g;return;}\n    const f=g.pending,b=keep?g.culture.kept:g.culture.shunned;\n    for(const t of f.traits){b[t.q]=(b[t.q]||0)+1;if(!t.good){g.counts.flawsSeen++;if(keep)g.counts.flawsKept++;}}\n    g.counts[keep?'kept':'banished']++;\n    if(decree){g.creed.unshift({turn:g.turn,by:me,text:decree,verdict:keep?'keep':'banish'});g.creed=g.creed.slice(0,12);}\n    log(g,keep?`${me} welcomed ${f.name} the ${f.species} into the cult.`:`${me} banished ${f.name} the ${f.species} into the cold dark.`);\n    g.pending=null;g.phase='fly';g.lastJudge=me;\n    if(!g.destinations.length)g.destinations=genDestinations(g.pos);\n    game=g;render();\n    await charm.set('game',g);\n    if(keep){\n      const c=await charm.get('cult'),list=Array.isArray(c)?c:[];\n      const m={...f,keptBy:me,turn:g.turn};\n      cult=[...list,m].slice(-30).map(normMember);\n      await charm.set('cult',cult);\n    }\n  }catch(e){setStatus('The judgment failed: '+e.message+'. Try again.');}\n  finally{busy=false;if(game)render();}\n}\nasync function load(){\n  const [g,c]=await Promise.all([charm.get('game'),charm.get('cult')]);\n  if(busy)return;\n  if(!g){game=fresh();await charm.set('game',game);}\n  else{\n    game=norm(g);\n    if(game.phase==='fly'&&!game.destinations.length){game.destinations=genDestinations(game.pos);await charm.set('game',game);}\n  }\n  cult=Array.isArray(c)?c.slice(-30).map(normMember):[];\n  render();\n}\ncharm.onChange(()=>{if(!busy)load().catch(e=>setStatus(e.message));},1500);\nload().catch(e=>setStatus('The cult could not be summoned: '+e.message));\n</script>\n</body>\n</html>\n"
}