Files
portfolio-next/mockups/personal-3.html
T
Lucas WintherandClaude Opus 5.5 843b6d32bc chore: bootstrap agent-ready docs, rules and design-phase artefacts
Add AGENTS.md as the single source of truth (with CLAUDE.md and GEMINI.md
as pointers), permissions for Claude Code, Codex and Gemini, the docs tree
(architecture, decisions, TODO, permissions, specs, plans), PRODUCT.md and
DESIGN.md, plus the design-phase record: the inspiration log, all mockup
rounds with briefs and screenshots, and the owner's work history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 06:47:38 +01:00

340 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lucas Winther · as a person (P3 · The garden)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,300;0,400;0,600;1,400&family=Martian+Mono:wght@300;400&display=swap" rel="stylesheet">
<style>
:root{
--bg:#eef0e6; --bg2:#e4e8da; --ink:#1f2a22; --soft:#5f6b5f; --line:#c3cbb5; --faint:#d6dcc9;
--moss:#4f6b3a; --terra:#c06a43; --ochre:#b4862b; --plum:#7a4f5f;
--serif:"Spectral", Georgia, serif; --mono:"Martian Mono", ui-monospace, monospace;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--serif);font-size:17.5px;line-height:1.6}
a{color:inherit}
.ph{text-decoration:underline dotted var(--soft);text-underline-offset:3px;cursor:help}
.mock-note{position:fixed;top:10px;right:10px;z-index:99;max-width:300px;background:#fff;border:1px solid #bbb;padding:8px 30px 8px 10px;font:12px/1.4 system-ui,sans-serif;color:#333}
.mock-note b{display:block}
.mock-note button{position:absolute;top:4px;right:6px;border:0;background:none;font-size:16px;cursor:pointer;color:#666}
.wrap{max-width:1140px;margin:0 auto;padding:0 30px}
header{display:flex;justify-content:space-between;align-items:baseline;padding:26px 0 16px;border-bottom:1px solid var(--line);gap:20px;flex-wrap:wrap}
header .name{font-size:19px;font-weight:600}
header .name span{font-weight:300;font-style:italic;color:var(--soft)}
nav.spaces{font:11.5px var(--mono);display:flex;gap:16px}
nav.spaces a{text-decoration:none;color:var(--soft)} nav.spaces a[aria-current]{color:var(--ink);border-bottom:1px solid}
.intro{display:grid;grid-template-columns:1fr 330px;gap:50px;padding:28px 0 18px;align-items:end}
.intro h1{font-weight:300;font-size:36px;line-height:1.15;margin:0 0 10px;letter-spacing:-.01em}
.intro p{margin:0;color:var(--soft);max-width:600px}
.legend{font:11px/1.9 var(--mono);color:var(--soft)}
.legend div{display:flex;align-items:center;gap:8px}
.legend svg{flex:none}
.layout{display:grid;grid-template-columns:1fr 400px;gap:44px;align-items:start;padding-bottom:30px}
.filters{display:flex;gap:18px;flex-wrap:wrap;font:11.5px var(--mono);padding:12px 0;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:5}
.filters span{color:var(--soft)}
.filters button{all:unset;cursor:pointer;color:var(--soft);padding:1px 0}
.filters button[aria-pressed="true"]{color:var(--ink);border-bottom:1px solid var(--ink)}
.filters button:focus-visible{outline:2px solid var(--terra)}
.entries{list-style:none;margin:0;padding:0}
.entry{border-bottom:1px solid var(--faint)}
.entry.hide{display:none}
.entry > button{all:unset;box-sizing:border-box;cursor:pointer;display:grid;grid-template-columns:22px 1fr auto;gap:12px;align-items:baseline;width:100%;padding:11px 8px}
.entry > button:hover,.entry.hl > button{background:var(--bg2)}
.entry > button:focus-visible{outline:2px solid var(--terra);outline-offset:-2px}
.entry .t{font-size:18.5px}
.entry .t small{display:block;font-size:14.5px;color:var(--soft);line-height:1.4}
.entry .m{font:10.5px var(--mono);color:var(--soft);text-align:right;white-space:nowrap}
.entry .g{align-self:center}
.entry .body{display:none;padding:2px 8px 20px 42px}
.entry.open .body{display:block}
.entry.open > button .t{font-weight:600}
.body p{margin:0 0 10px;max-width:60ch}
.links{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:12px;font:11px var(--mono);color:var(--soft);align-items:baseline}
.links button{all:unset;cursor:pointer;font-family:var(--serif);font-size:15px;color:var(--ink);border-bottom:1px solid var(--line)}
.links button:hover{border-color:var(--ink)}
.links button:focus-visible{outline:2px solid var(--terra)}
.rec{display:grid;grid-template-columns:auto 1fr;gap:14px;align-items:center;margin:4px 0 14px}
.playbtn{all:unset;cursor:pointer;width:42px;height:42px;border-radius:50%;background:var(--ochre);color:#fff;display:grid;place-items:center;font-size:14px}
.playbtn:hover{background:#9a7120}
.playbtn:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
.vers{list-style:none;margin:6px 0 12px;padding:0;font-size:15.5px}
.vers li{display:grid;grid-template-columns:18px 1fr auto;gap:10px;padding:5px 0;border-bottom:1px dashed var(--line);align-items:baseline}
.vers button{all:unset;cursor:pointer;font:11px var(--mono);color:var(--ochre)}
.vers button:hover{text-decoration:underline}
.vers .on{font:11px var(--mono);color:var(--ink)}
.two{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin:8px 0}
.credits{display:grid;grid-template-columns:auto 1fr;gap:2px 12px;font-size:15px;margin:0}
.credits dt{font:11px var(--mono);color:var(--soft);padding-top:3px}
.credits dd{margin:0}
.photo{height:118px;background:repeating-linear-gradient(-45deg,#d0d7c2 0 9px,#c8d0b9 9px 18px);font:10.5px var(--mono);color:#4e5a4b;display:flex;align-items:flex-end;padding:8px}
.lyric{font-style:italic;border-left:2px solid var(--ochre);padding-left:12px;margin:6px 0 10px}
.set{list-style:none;margin:6px 0 10px;padding:0;counter-reset:n}
.set li button{all:unset;box-sizing:border-box;cursor:pointer;display:grid;grid-template-columns:26px 1fr auto;width:100%;padding:5px 6px;font-size:15.5px}
.set li button::before{counter-increment:n;content:counter(n);font:10.5px var(--mono);color:var(--soft);padding-top:4px}
.set li button span{font:10.5px var(--mono);color:var(--soft)}
.set li button:hover{background:var(--bg2)}
.set li.now button{background:var(--ink);color:var(--bg)}
.set li.now button::before,.set li.now button span{color:var(--line)}
.handcam{height:150px;background:#2a3129;color:#c9d1bd;display:flex;align-items:center;justify-content:center;gap:12px;font:11px var(--mono);margin-bottom:10px}
.handcam i{width:40px;height:40px;border:1.5px solid #c9d1bd;border-radius:50%;display:grid;place-items:center;font-style:normal}
aside{position:sticky;top:16px}
.mapbox{border:1px solid var(--line);background:#f4f5ee}
.maphead{display:flex;justify-content:space-between;font:10.5px var(--mono);color:var(--soft);padding:8px 10px;border-bottom:1px solid var(--line)}
svg.map{display:block;width:100%;height:auto}
.edge{stroke:var(--line);stroke-width:1;transition:stroke .15s, opacity .15s}
.node{cursor:pointer}
.node text{font:9.5px var(--mono);fill:var(--soft);pointer-events:none;transition:fill .15s, opacity .15s;opacity:0}
.node.lab text,.node:hover text,.node:focus-visible text,.map.focus .node.hl text{opacity:1}
.map.focus .node.lab:not(.hl) text{opacity:.5}
.node .s{transition:opacity .15s}
.map.focus .edge{opacity:.25}
.map.focus .node{opacity:.35}
.map.focus .edge.hl{opacity:1;stroke:var(--ink);stroke-width:1.4}
.map.focus .node.hl{opacity:1}
.map.focus .node.hl text{fill:var(--ink)}
.node.open .ring{stroke:var(--ink);stroke-width:1.5;fill:none}
.node:focus-visible{outline:none}
.node:focus-visible .ring{stroke:var(--terra);stroke-width:2;fill:none}
.now{margin-top:12px;border:1px solid var(--line);background:#f4f5ee;padding:12px 14px;font-size:15px}
.now .row{display:flex;align-items:center;gap:12px}
.now .row .playbtn{width:34px;height:34px;font-size:12px}
.now .title{flex:1;line-height:1.3}
.now .title small{display:block;font:10.5px var(--mono);color:var(--soft)}
.now .skip{all:unset;cursor:pointer;font:12px var(--mono);color:var(--soft);padding:4px}
.now .skip:hover{color:var(--ink)}
.bar{height:16px;position:relative;cursor:pointer;margin-top:8px}
.bar::before{content:"";position:absolute;left:0;right:0;top:7px;height:2px;background:var(--line)}
.bar i{position:absolute;left:0;top:7px;height:2px;background:var(--ochre);width:0}
.now .tm{font:10.5px var(--mono);color:var(--soft);display:flex;justify-content:space-between}
.now.idle{color:var(--soft);font-style:italic}
footer{border-top:1px solid var(--line);padding:14px 0 30px;font:10.5px/1.7 var(--mono);color:var(--soft);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
@media (max-width:900px){.layout,.intro{grid-template-columns:1fr} aside{position:static;order:-1} .two{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="mock-note" role="note"><button aria-label="Dismiss note" onclick="this.parentNode.remove()">×</button><b>P3 · The garden</b>Notes, songs, gigs and findings as one tended garden with maturity stages and visible connections. Hover anything to see what it's linked to on the map; songs open with their versions across gigs. Borrows maggieappleton.com's garden and beige.zone's tag filters.</div>
<div class="wrap">
<header>
<div class="name">Lucas Winther <span>· the garden</span></div>
<nav class="spaces" aria-label="Spaces"><a href="#">with AI, now</a><a href="#" aria-current="page">as a person</a><a href="#">by hand, before ↗</a></nav>
</header>
<section class="intro">
<div>
<h1>Half-finished thoughts, songs from <span class="ph" title="placeholder">the band</span>, and things I found, all growing into each other.</h1>
<p>Nothing here is in date order. Some notes are seedlings, some have been rewritten for years. Songs connect to the nights we played them, and those nights connect to whatever I was thinking about at the time.</p>
</div>
<div class="legend" id="legend"></div>
</section>
<div class="layout">
<main>
<div class="filters" role="group" aria-label="Filter">
<span>show</span>
<button data-k="type" data-v="all" aria-pressed="true">everything</button>
<button data-k="type" data-v="note" aria-pressed="false">notes</button>
<button data-k="type" data-v="song" aria-pressed="false">songs</button>
<button data-k="type" data-v="gig" aria-pressed="false">gigs</button>
<button data-k="type" data-v="find" aria-pressed="false">findings</button>
<span style="margin-left:auto">stage</span>
<button data-k="stage" data-v="all" aria-pressed="true">any</button>
<button data-k="stage" data-v="seedling" aria-pressed="false">seedling</button>
<button data-k="stage" data-v="budding" aria-pressed="false">budding</button>
<button data-k="stage" data-v="evergreen" aria-pressed="false">evergreen</button>
</div>
<ul class="entries" id="entries"></ul>
</main>
<aside>
<div class="mapbox">
<div class="maphead"><span>map · hover a dot</span><span id="mapcount"></span></div>
<svg class="map" id="map" viewBox="0 0 400 430" role="img" aria-label="Map of connections between notes, songs, gigs and findings"></svg>
</div>
<div class="now idle" id="now">Nothing playing. Open a song or a gig to listen.</div>
</aside>
</div>
<footer><span>Placeholders (dotted underline, and all music/notes content): band name <span class="ph" title="placeholder">Lav Sol</span>, members, songs, gigs, venues, note and finding titles, photos.</span><span><a href="mailto:lucas@lucaswinther.info">lucas@lucaswinther.info</a> · <a href="#">GitHub</a> · <a href="#">blog</a></span></footer>
</div>
<script>
const N = [
{id:"n1",type:"note",stage:"evergreen",x:55,y:95,title:"Handcam is the honest format",sub:"Why our gigs are filmed on a phone from the third row.",tended:"tended Oct 2026",
body:["Shaky phone footage with the bar chatter left in. I used to want something cleaner, now I think it's the only format that tells the truth about a small gig.","The best live record I own (see the Bill Evans finding) is basically the same thing, in 1961."]},
{id:"n2",type:"note",stage:"budding",x:150,y:30,title:"On playing to eleven people",sub:"Small rooms are harder than big ones.",tended:"tended Oct 2026",
body:["You can see every face, and every face can see your left hand. At Café Retro there were eleven people and a dog.","What helped: telling the story behind a song before playing it."]},
{id:"n3",type:"note",stage:"evergreen",x:345,y:62,title:"Writing code vs. writing songs",sub:"Both are mostly deleting.",tended:"tended Sep 2026",
body:["Both are mostly deleting. One has a test suite.","At work I lean on AI tools (that's the other half of this site). Songwriting is where I deliberately don't."]},
{id:"n4",type:"note",stage:"seedling",x:60,y:250,title:"Learning to sing harmony",sub:"Record yourself, listen once, never again.",tended:"planted Feb 2026",
body:["A seedling. So far: harmonies on Grå morgen and Halvvejs, both shaky. A tuner that doesn't nag helped more than expected."]},
{id:"n5",type:"note",stage:"budding",x:345,y:180,title:"Why I keep a paper notebook",sub:"Half our lyrics started in a meeting margin.",tended:"tended Aug 2026",
body:["Half of the band's lyrics started in the margin of a meeting, the other half on the bus. Kitchen light was written on the back of a receipt."]},
{id:"n6",type:"note",stage:"seedling",x:120,y:400,title:"Things overheard on the 5A",sub:"A list that might become a song.",tended:"planted Jun 2026",
body:["\"He's not a bad person, he just owns a jet ski.\" More to come. Might turn into a verse for Slow ferry's quiet bit."]},
{id:"s1",type:"song",stage:"evergreen",x:140,y:175,title:"Grå morgen",sub:"Our opener. A warm-up riff that became a song.",tended:"played 3 times",
who:[["Lucas","the riff, lead guitar"],["Signe","vocals"],["Mads","bass"],["Jonas","drums, brushes"]],lyric:"Grey morning, the coffee knows before I do.",origin:"Signe wrote the words over Lucas's warm-up riff in one rehearsal. Never changed them.",photo:"photo: Signe, red light"},
{id:"s2",type:"song",stage:"evergreen",x:285,y:150,title:"Kitchen light",sub:"Written at 2am. Slower every year.",tended:"played 3 times + demo",
who:[["Lucas","acoustic, verse 2"],["Signe","vocals, Rhodes"],["Mads","bass"],["Jonas","shaker, then drums"]],lyric:"Leave the kitchen light on, I'll find my way.",origin:"Written on a receipt in a kitchen. Each live version is slower than the last.",photo:"photo: one lamp"},
{id:"s3",type:"song",stage:"budding",x:310,y:330,title:"Slow ferry",sub:"The long one. The bridge still isn't right.",tended:"played 2 times",
who:[["Lucas","guitar, delay"],["Signe","keys"],["Mads","long bass notes"],["Jonas","drums"]],lyric:"Nothing to do but stand at the rail.",origin:"First verse on the back of a ferry ticket.",photo:"photo: grey sea"},
{id:"s4",type:"song",stage:"budding",x:80,y:340,title:"Halvvejs",sub:"Our only Danish song.",tended:"played 3 times",
who:[["Lucas","guitar"],["Signe","vocals"],["Mads","bass"],["Jonas","drums"]],lyric:"Halvvejs hjem, halvvejs væk.",origin:"The title means \"halfway\". Second verse unfinished.",photo:"photo: setlist on the floor"},
{id:"s5",type:"song",stage:"evergreen",x:195,y:130,title:"Paper boats",sub:"Usually the closer.",tended:"played 3 times",
who:[["Lucas","guitar, harmony"],["Signe","vocals"],["Mads","bass"],["Jonas","count-in, too fast"]],lyric:"Fold it twice and let it go.",origin:"Jonas counts it in too fast, every single time.",photo:"photo: blurry crowd"},
{id:"g1",type:"gig",stage:"evergreen",x:190,y:250,title:"Café Retro, 14 Mar 2026",sub:"Five songs, handcam from the bar.",tended:"gig",set:[["s1",252],["s2",208],["s3",301],["s4",196],["s5",244]]},
{id:"g2",type:"gig",stage:"evergreen",x:230,y:205,title:"Studenterhuset basement, Oct 2025",sub:"The loud one. Phone on a beer crate.",tended:"gig",set:[["s5",231],["s1",270],["new",188],["s3",322]]},
{id:"g3",type:"gig",stage:"budding",x:165,y:300,title:"Garden party at Ida's, Aug 2025",sub:"Acoustic. Wasps.",tended:"gig",set:[["s2",190],["s4",205],["s1",240]]},
{id:"f1",type:"find",stage:"evergreen",x:30,y:150,title:"Bill Evans · Sunday at the Village Vanguard",sub:"You can hear the glasses clink.",tended:"found 2024",body:["Handcam in spirit. The record that made me stop caring about clean recordings."]},
{id:"f2",type:"find",stage:"evergreen",x:375,y:105,title:"\"The case for small software\"",sub:"An essay. Applies to bands too.",tended:"found 2025",body:["Things made for a few people can be made with more care."]},
{id:"f3",type:"find",stage:"seedling",x:350,y:395,title:"Every Danish bus shelter, photographed",sub:"Someone did all of them.",tended:"found 2026",body:["There's a map. I've spent too long on it."]},
{id:"f4",type:"find",stage:"budding",x:25,y:285,title:"A tuner that doesn't nag",sub:"No account, no ads.",tended:"found 2026",body:["Just a needle."]},
];
const E = [["n1","f1"],["n1","g1"],["n1","g2"],["n2","g1"],["n2","s5"],["n3","f2"],["n3","n5"],["n4","s1"],["n4","s4"],["n4","f4"],["n5","s2"],["n5","s3"],["n6","f3"],["n6","s3"],["s2","n3"]];
N.filter(n=>n.type==="gig").forEach(g=>g.set.forEach(([s])=>{ if(s!=="new") E.push([g.id,s]); }));
const byId = Object.fromEntries(N.map(n=>[n.id,n]));
const nb = id => E.filter(e=>e.includes(id)).map(e=>e[0]===id?e[1]:e[0]);
const fmt=s=>Math.floor(s/60)+":"+String(Math.floor(s%60)).padStart(2,"0");
const typeName={note:"note",song:"song",gig:"gig",find:"finding"};
const col={note:"var(--moss)",song:"var(--ochre)",gig:"var(--terra)",find:"var(--plum)"};
function glyph(stage,c,sz=14){
if(stage==="seedling") return `<svg width="${sz}" height="${sz}" viewBox="0 0 14 14"><circle cx="7" cy="7" r="3" fill="none" stroke="${c}" stroke-width="1.4"/></svg>`;
if(stage==="budding") return `<svg width="${sz}" height="${sz}" viewBox="0 0 14 14"><circle cx="7" cy="7" r="5" fill="none" stroke="${c}" stroke-width="1.4"/><path d="M7 2a5 5 0 0 1 0 10z" fill="${c}"/></svg>`;
return `<svg width="${sz}" height="${sz}" viewBox="0 0 14 14"><path d="M7 1C11 4 12 9 7 13C2 9 3 4 7 1z" fill="${c}"/></svg>`;
}
document.getElementById("legend").innerHTML =
["seedling","budding","evergreen"].map(s=>`<div>${glyph(s,"var(--ink)")} ${s}</div>`).join("") +
`<div style="margin-top:6px"><svg width="14" height="14"><circle cx="7" cy="7" r="5" fill="var(--moss)"/></svg> note <svg width="14" height="14"><circle cx="7" cy="7" r="5" fill="none" stroke="var(--ochre)" stroke-width="3"/></svg> song <svg width="14" height="14"><rect x="2" y="2" width="10" height="10" fill="var(--terra)"/></svg> gig <svg width="14" height="14"><path d="M7 1L13 7L7 13L1 7z" fill="var(--plum)"/></svg> find</div>`;
const ul=document.getElementById("entries");
ul.innerHTML = N.map(n=>`<li class="entry" id="e-${n.id}" data-id="${n.id}" data-type="${n.type}" data-stage="${n.stage}">
<button aria-expanded="false"><span class="g">${glyph(n.stage,col[n.type])}</span><span class="t">${n.title}<small>${n.sub}</small></span><span class="m">${typeName[n.type]}<br>${n.tended}</span></button>
<div class="body"></div></li>`).join("");
const svg=document.getElementById("map");
const r = n => n.stage==="seedling"?5:n.stage==="budding"?7:9;
function shape(n){
const R=r(n), c=col[n.type];
if(n.type==="gig") return `<rect class="s" x="${n.x-R}" y="${n.y-R}" width="${2*R}" height="${2*R}" fill="${c}"/><rect class="ring" x="${n.x-R-4}" y="${n.y-R-4}" width="${2*R+8}" height="${2*R+8}" fill="none" stroke="none"/>`;
if(n.type==="find") return `<path class="s" d="M${n.x} ${n.y-R-1}L${n.x+R+1} ${n.y}L${n.x} ${n.y+R+1}L${n.x-R-1} ${n.y}z" fill="${c}"/><circle class="ring" cx="${n.x}" cy="${n.y}" r="${R+5}" fill="none" stroke="none"/>`;
if(n.type==="song") return `<circle class="s" cx="${n.x}" cy="${n.y}" r="${R}" fill="var(--bg)" stroke="${c}" stroke-width="3"/><circle class="ring" cx="${n.x}" cy="${n.y}" r="${R+5}" fill="none" stroke="none"/>`;
return `<circle class="s" cx="${n.x}" cy="${n.y}" r="${R}" fill="${c}"/><circle class="ring" cx="${n.x}" cy="${n.y}" r="${R+5}" fill="none" stroke="none"/>`;
}
function short(t){ return t.length>22 ? t.slice(0,21).replace(/[ ,·"]+$/,"")+"…" : t; }
svg.innerHTML = E.map(([a,b])=>`<line class="edge" data-a="${a}" data-b="${b}" x1="${byId[a].x}" y1="${byId[a].y}" x2="${byId[b].x}" y2="${byId[b].y}"/>`).join("")
+ N.map(n=>{ const left = n.x>=260; return `<g class="node${n.type==="song"||n.type==="gig"?" lab":""}" tabindex="0" role="button" aria-label="${n.title}" data-id="${n.id}">${shape(n)}<text x="${left?n.x-r(n)-5:n.x+r(n)+5}" y="${n.y+3}" text-anchor="${left?"end":"start"}">${short(n.title.replace(/^"|"$/g,""))}</text></g>`; }).join("");
document.getElementById("mapcount").textContent = N.length+" things · "+E.length+" links";
function focus(id){
if(!id){ svg.classList.remove("focus"); document.querySelectorAll(".entry.hl").forEach(e=>e.classList.remove("hl")); return; }
const set=new Set([id,...nb(id)]);
svg.classList.add("focus");
svg.querySelectorAll(".edge").forEach(l=>l.classList.toggle("hl", l.dataset.a===id||l.dataset.b===id));
svg.querySelectorAll(".node").forEach(g=>g.classList.toggle("hl", set.has(g.dataset.id)));
document.querySelectorAll(".entry").forEach(e=>e.classList.toggle("hl", e.dataset.id===id));
}
svg.querySelectorAll(".node").forEach(g=>{
g.addEventListener("mouseenter",()=>focus(g.dataset.id)); g.addEventListener("focus",()=>focus(g.dataset.id));
g.addEventListener("mouseleave",()=>focus(openId)); g.addEventListener("blur",()=>focus(openId));
g.addEventListener("click",()=>open(g.dataset.id,true));
g.addEventListener("keydown",e=>{if(e.key==="Enter"||e.key===" "){e.preventDefault();open(g.dataset.id,true);}});
});
ul.querySelectorAll(".entry").forEach(li=>{
const b=li.querySelector("button");
li.addEventListener("mouseenter",()=>focus(li.dataset.id)); li.addEventListener("mouseleave",()=>focus(openId));
b.addEventListener("focus",()=>focus(li.dataset.id));
b.addEventListener("click",()=>open(li.dataset.id===openId?null:li.dataset.id,false));
});
let openId=null;
function open(id,scroll){
openId=id;
ul.querySelectorAll(".entry").forEach(li=>{ const on=li.dataset.id===id; li.classList.toggle("open",on); li.querySelector("button").setAttribute("aria-expanded",on); if(on) li.querySelector(".body").innerHTML=bodyFor(byId[id]); });
svg.querySelectorAll(".node").forEach(g=>g.classList.toggle("open",g.dataset.id===id));
focus(id);
if(id){
const li=document.getElementById("e-"+id);
if(li.classList.contains("hide")){ resetFilters(); }
wire(li);
if(scroll) li.scrollIntoView({block:"nearest"});
}
}
function linksHtml(n){
return `<div class="links"><span>connected to</span>${nb(n.id).map(i=>`<button data-go="${i}">${byId[i].title}</button>`).join("")}</div>`;
}
function bodyFor(n){
if(n.type==="song"){
const vers=N.filter(g=>g.type==="gig"&&g.set.some(s=>s[0]===n.id));
return `<p class="lyric">"${n.lyric}"</p>
<div class="rec"><button class="playbtn" data-play="${vers[0].id}:${vers[0].set.findIndex(s=>s[0]===n.id)}" aria-label="Play ${n.title}">▶</button><span style="font-size:15px">Play it from <b>${vers[0].title}</b>, or pick another night below.</span></div>
<div style="font:10.5px var(--mono);color:var(--soft)">the same song, different nights</div>
<ul class="vers">${vers.map(g=>{const j=g.set.findIndex(s=>s[0]===n.id); return `<li>${glyph(g.stage,col.gig,12)}<span>${g.title} <span style="color:var(--soft)">· ${fmt(g.set[j][1])}</span></span><button data-play="${g.id}:${j}">play this version</button></li>`;}).join("")}</ul>
<div class="two"><div><div style="font:10.5px var(--mono);color:var(--soft);margin-bottom:4px">who plays what</div><dl class="credits">${n.who.map(w=>`<dt>${w[0]}</dt><dd>${w[1]}</dd>`).join("")}</dl><p style="font-size:15px;margin-top:10px">${n.origin}</p></div>
<div class="photo">${n.photo}</div></div>${linksHtml(n)}`;
}
if(n.type==="gig"){
return `<div class="handcam"><i>▶</i>handcam video · ${n.title} · placeholder frame</div>
<div style="font:10.5px var(--mono);color:var(--soft)">setlist · click to jump</div>
<ol class="set">${n.set.map(([s,l],i)=>`<li data-k="${n.id}:${i}"><button data-play="${n.id}:${i}">${s==="new"?"New one (no name yet)":byId[s].title}<span>${fmt(l)}</span></button></li>`).join("")}</ol>${linksHtml(n)}`;
}
return n.body.map(p=>`<p>${p}</p>`).join("")+linksHtml(n);
}
function wire(li){
li.querySelectorAll("[data-go]").forEach(b=>b.onclick=()=>open(b.dataset.go,true));
li.querySelectorAll("[data-play]").forEach(b=>b.onclick=()=>{const [g,i]=b.dataset.play.split(":"); play(g,+i);});
markNow();
}
/* filters */
const F={type:"all",stage:"all"};
document.querySelectorAll(".filters button").forEach(b=>b.addEventListener("click",()=>{
F[b.dataset.k]=b.dataset.v;
document.querySelectorAll(`.filters button[data-k="${b.dataset.k}"]`).forEach(x=>x.setAttribute("aria-pressed",x===b));
applyFilters();
}));
function applyFilters(){
ul.querySelectorAll(".entry").forEach(li=>li.classList.toggle("hide",(F.type!=="all"&&li.dataset.type!==F.type)||(F.stage!=="all"&&li.dataset.stage!==F.stage)));
}
function resetFilters(){ F.type="all";F.stage="all"; document.querySelectorAll(".filters button").forEach(x=>x.setAttribute("aria-pressed",x.dataset.v==="all")); applyFilters(); }
/* player */
let P={g:null,i:0,pos:0,on:false,timer:null};
const now=document.getElementById("now");
function play(g,i){ P.g=byId[g]; P.i=i; P.pos=0; setOn(true); }
function setOn(on){
P.on=on; clearInterval(P.timer);
if(on) P.timer=setInterval(()=>{ P.pos++; if(P.pos>=P.g.set[P.i][1]){ if(P.i<P.g.set.length-1){P.i++;P.pos=0;} else {P.pos=P.g.set[P.i][1]; setOn(false); return;} } renderNow(); },1000);
renderNow();
}
function renderNow(){
if(!P.g) return;
const [s,l]=P.g.set[P.i], name=s==="new"?"New one (no name yet)":byId[s].title;
now.classList.remove("idle");
now.innerHTML=`<div class="row"><button class="playbtn" id="npp" aria-label="${P.on?"Pause":"Play"}">${P.on?"❚❚":"▶"}</button>
<div class="title">${s==="new"?name:`<a href="#" data-go="${s}">${name}</a>`}<small>${P.g.title} · song ${P.i+1}/${P.g.set.length}</small></div>
<button class="skip" id="nprev" aria-label="Previous song">«</button><button class="skip" id="nnext" aria-label="Next song">»</button></div>
<div class="bar" id="nbar"><i style="width:${P.pos/l*100}%"></i></div><div class="tm"><span>${fmt(P.pos)}</span><span>${fmt(l)}</span></div>`;
document.getElementById("npp").onclick=()=>setOn(!P.on);
document.getElementById("nprev").onclick=()=>{P.i=Math.max(0,P.i-1);P.pos=0;renderNow();};
document.getElementById("nnext").onclick=()=>{P.i=Math.min(P.g.set.length-1,P.i+1);P.pos=0;renderNow();};
document.getElementById("nbar").onclick=e=>{const r=e.currentTarget.getBoundingClientRect();P.pos=Math.floor((e.clientX-r.left)/r.width*l);renderNow();};
const a=now.querySelector("[data-go]"); if(a) a.onclick=e=>{e.preventDefault(); open(a.dataset.go,true);};
markNow();
}
function markNow(){ document.querySelectorAll(".set li").forEach(li=>li.classList.toggle("now", P.g && li.dataset.k===P.g.id+":"+P.i)); }
</script>
</body>
</html>