Files
portfolio-next/mockups/personal-5.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

289 lines
25 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 (P5 · The zine)</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=Instrument+Serif:ital@0;1&family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,700&display=swap" rel="stylesheet">
<style>
:root{
--paper:#f3f0e8; --ink:#151412; --soft:#66625a; --rule:#151412; --hair:#cfc9bc; --spot:#e3432b; --spot2:#f2c6b9;
--disp:"Instrument Serif", Georgia, serif; --sans:"Bricolage Grotesque", system-ui, sans-serif;
}
*{box-sizing:border-box}
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.5;font-variation-settings:"opsz" 14}
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:1180px;margin:0 auto;padding:0 32px}
.topline{display:flex;justify-content:space-between;font-size:12px;padding:14px 0 8px;letter-spacing:.02em;gap:16px;flex-wrap:wrap}
.topline nav{display:flex;gap:16px}
.topline a{text-decoration:none;color:var(--soft)} .topline a[aria-current]{color:var(--ink);text-decoration:underline}
.mast{border-top:2px solid var(--rule);border-bottom:1px solid var(--rule);display:grid;grid-template-columns:1fr auto;align-items:end;gap:20px;padding:4px 0 8px}
.mast h1{font:400 128px/0.85 var(--disp);margin:0;letter-spacing:-.025em}
.mast h1 i{color:var(--spot)}
.issue{text-align:right;font-size:13px;padding-bottom:10px}
.issue .no{font:italic 40px/1 var(--disp);display:block}
.issues{display:flex;border-bottom:1px solid var(--rule);font-size:12.5px}
.issues button{all:unset;cursor:pointer;padding:7px 14px;border-right:1px solid var(--hair);color:var(--soft)}
.issues button[aria-pressed="true"]{background:var(--ink);color:var(--paper)}
.issues button:hover:not([aria-pressed="true"]){color:var(--ink)}
.issues button:focus-visible{outline:2px solid var(--spot);outline-offset:-2px}
.issues .by{margin-left:auto;padding:7px 0;color:var(--soft)}
.label{font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:500}
.spread{display:grid;grid-template-columns:repeat(12,1fr);gap:0 28px;padding:26px 0;border-bottom:1px solid var(--rule)}
/* contents */
.contents{grid-column:span 2;font-size:13.5px;border-right:1px solid var(--hair);padding-right:16px}
.contents ol{list-style:none;margin:8px 0 0;padding:0}
.contents li{display:grid;grid-template-columns:24px 1fr;padding:6px 0;border-bottom:1px solid var(--hair)}
.contents li span{font:italic 18px/1 var(--disp);color:var(--spot)}
.contents a{text-decoration:none}
.contents a:hover{text-decoration:underline}
/* cover */
.cover{grid-column:span 6}
.kicker{color:var(--spot)}
.cover h2{font:400 58px/0.98 var(--disp);margin:6px 0 12px;letter-spacing:-.015em}
.cover h2 i{color:var(--spot)}
.stand{font-size:19px;line-height:1.4;margin:0 0 16px;font-variation-settings:"opsz" 40}
.cols{columns:2;column-gap:24px;font-size:15.5px;column-rule:1px solid var(--hair)}
.cols p{margin:0 0 10px}
.cols p:first-child::first-letter{font:400 64px/0.8 var(--disp);float:left;margin:6px 8px 0 0;color:var(--spot)}
.byline{font-size:12px;color:var(--soft);margin-bottom:12px}
/* player: tracklist */
.side{grid-column:span 4;border-left:2px solid var(--rule);padding-left:20px;display:flex;flex-direction:column}
.sidesw{display:flex;gap:0;margin:8px 0 4px;font-size:12px}
.sidesw button{all:unset;cursor:pointer;padding:4px 10px;border:1px solid var(--ink);margin-right:-1px}
.sidesw button[aria-pressed="true"]{background:var(--ink);color:var(--paper)}
.sidesw button:focus-visible{outline:2px solid var(--spot)}
.sidehead{font:400 30px/1.05 var(--disp);margin:8px 0 2px}
.sidesub{font-size:12.5px;color:var(--soft);margin-bottom:6px}
.tracks{list-style:none;margin:0;padding:0}
.tracks li{border-top:1px solid var(--hair)}
.tracks li:last-child{border-bottom:1px solid var(--hair)}
.tracks button{all:unset;box-sizing:border-box;cursor:pointer;display:grid;grid-template-columns:64px 1fr auto;align-items:center;width:100%;padding:4px 0;position:relative}
.tracks .n{font:400 46px/1 var(--disp);color:var(--hair);transition:color .15s}
.tracks .tt{font-size:17px;font-weight:500;line-height:1.2}
.tracks .tt small{display:block;font-weight:400;font-size:12px;color:var(--soft)}
.tracks .d{font-size:12px;color:var(--soft);font-variant-numeric:tabular-nums}
.tracks button:hover .n{color:var(--ink)}
.tracks button:focus-visible{outline:2px solid var(--spot);outline-offset:-2px}
.tracks li.now .n{color:var(--spot)}
.tracks li.now button::after{content:"";position:absolute;left:64px;bottom:0;height:2px;width:calc(var(--p,0) * (100% - 64px));background:var(--spot)}
.deck{display:flex;align-items:center;gap:12px;margin-top:14px;padding-top:12px;border-top:2px solid var(--rule)}
.deck button{all:unset;cursor:pointer;font-size:13px;padding:3px 0}
.deck button:hover{color:var(--spot)}
.deck button:focus-visible{outline:2px solid var(--spot)}
.deck .pp{width:54px;height:54px;border-radius:50%;background:var(--spot);color:var(--paper);display:grid;place-items:center;font-size:17px;padding:0}
.deck .pp:hover{background:var(--ink);color:var(--paper)}
.deck .time{margin-left:auto;font:italic 26px/1 var(--disp);font-variant-numeric:tabular-nums}
.deck .state{font-size:12px;color:var(--soft)}
.seek{height:14px;position:relative;cursor:pointer;margin-top:10px}
.seek::before{content:"";position:absolute;left:0;right:0;top:6px;border-top:1px dotted var(--ink)}
.seek i{position:absolute;left:0;top:5px;height:3px;background:var(--spot);width:0}
.film{margin-top:14px;display:flex;gap:12px;align-items:center;font-size:12.5px;color:var(--soft);border:1px solid var(--hair);padding:8px;cursor:pointer;background:none;width:100%;text-align:left;font-family:var(--sans)}
.film .fr{width:84px;height:54px;flex:none;background:radial-gradient(var(--ink) 1px,transparent 1.4px) 0 0/5px 5px,#bdb6a8;display:grid;place-items:center;color:var(--paper);font-size:14px}
.film:hover{border-color:var(--ink);color:var(--ink)}
/* liner notes */
.liner{grid-column:1/-1;display:grid;grid-template-columns:repeat(12,1fr);gap:0 28px}
.liner .head{grid-column:span 12;display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px;flex-wrap:wrap;gap:10px}
.liner .head h3{font:400 40px/1 var(--disp);margin:0}
.liner .head h3 small{font-size:20px;color:var(--soft);font-style:italic}
.quote{grid-column:span 5;font:italic 400 46px/1.02 var(--disp);color:var(--spot);margin:0;letter-spacing:-.01em}
.quote + p{grid-column:span 5}
.origin{font-size:15.5px;margin:12px 0 0}
.credits{grid-column:span 3;font-size:14.5px;border-left:1px solid var(--hair);padding-left:18px}
.credits dl{margin:8px 0 0;display:grid;grid-template-columns:auto 1fr;gap:6px 12px}
.credits dt{font-weight:700}
.credits dd{margin:0;color:var(--soft)}
.halftone{grid-column:span 4;min-height:200px;background:radial-gradient(circle,var(--ink) 1.1px,transparent 1.6px) 0 0/6px 6px, linear-gradient(135deg,#d9d2c4,#a9a294);display:flex;align-items:flex-end;padding:0;position:relative}
.halftone span{background:var(--paper);font-size:11.5px;padding:4px 8px;margin:8px}
.pressings{grid-column:5 / span 8;margin-top:22px;border-top:1px solid var(--rule);padding-top:10px;font-size:14px}
.pressings ul{list-style:none;margin:6px 0 0;padding:0;display:flex;gap:0;flex-wrap:wrap}
.pressings li{flex:1 1 180px;border-right:1px solid var(--hair);padding:4px 14px 4px 0;margin-right:14px}
.pressings li:last-child{border-right:0}
.pressings b{display:block;font:400 22px/1.1 var(--disp)}
.pressings button{all:unset;cursor:pointer;font-size:12px;color:var(--spot);margin-top:4px;display:inline-block}
.pressings button:hover{text-decoration:underline}
.pressings .here{font-size:12px;color:var(--soft);margin-top:4px;display:inline-block}
.liner-empty{grid-column:span 12;font:italic 26px/1.3 var(--disp);color:var(--soft)}
.ln-left{grid-column:span 5}
/* back pages */
.musings{grid-column:span 7}
.musings .row{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;margin-top:10px}
.musings article{border-top:1px solid var(--rule);padding-top:8px}
.musings h4{font:400 26px/1.05 var(--disp);margin:0 0 6px}
.musings p{margin:0;font-size:14.5px}
.musings details summary{cursor:pointer;font-size:12px;color:var(--spot);margin-top:6px;list-style:none}
.musings details summary::-webkit-details-marker{display:none}
.musings details p{margin-top:6px}
.classifieds{grid-column:span 5;border-left:2px solid var(--rule);padding-left:20px}
.classifieds .grid{display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--rule);border-left:1px solid var(--hair);margin-top:10px}
.classifieds .ad{border-right:1px solid var(--hair);border-bottom:1px solid var(--hair);padding:8px 10px;font-size:13.5px;line-height:1.35}
.classifieds .ad b{display:block;font-size:11px;letter-spacing:.12em;text-transform:uppercase;margin-bottom:3px}
.classifieds .ad a{text-decoration:none;font-weight:500}
.classifieds .ad a:hover{color:var(--spot)}
.classifieds .ad.wide{grid-column:span 2;background:var(--spot2)}
.colophon{display:grid;grid-template-columns:2fr 1fr 1fr;gap:28px;padding:18px 0 34px;font-size:12px;color:var(--soft)}
.colophon b{color:var(--ink);font-weight:500}
@media (max-width:980px){
.mast h1{font-size:84px}
.contents{grid-column:1/-1;border-right:0;padding:0 0 16px} .contents ol{columns:2}
.cover{grid-column:span 12} .side{grid-column:span 12;border-left:0;padding:20px 0 0;border-top:2px solid var(--rule);margin-top:20px}
.quote,.ln-left,.credits,.halftone,.pressings,.musings,.classifieds{grid-column:1/-1}
.credits,.classifieds{border-left:0;padding-left:0;margin-top:16px}
.musings .row{grid-template-columns:1fr}
.colophon{grid-template-columns:1fr}
}
@media (max-width:520px){.mast h1{font-size:60px} .cols{columns:1} .cover h2{font-size:40px} .quote{font-size:34px} .classifieds .grid{grid-template-columns:1fr} .classifieds .ad.wide{grid-column:auto}}
</style>
</head>
<body>
<div class="mock-note" role="note"><button aria-label="Dismiss note" onclick="this.parentNode.remove()">×</button><b>P5 · The zine</b>The personal space as a small seasonal magazine. Each issue mixes a cover story, a printed tracklist that plays, liner notes for the current song, musings and classified-style findings. Borrows marcschuster.com's editorial feel and beige.zone's archival typography.</div>
<div class="wrap">
<div class="topline">
<span>Lucas Winther · personal pages</span>
<nav 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>
</div>
<header class="mast">
<h1><span title="placeholder: zine name">Sidespor</span><i>.</i></h1>
<div class="issue"><span class="no" id="no">Nº 03</span><span id="season">Autumn 2026</span> · a zine about the things I do when I'm not working</div>
</header>
<div class="issues" role="group" aria-label="Issues" id="issues"></div>
<section class="spread" id="cover"></section>
<section class="spread"><div class="liner" id="liner"></div></section>
<section class="spread" id="back"></section>
<footer class="colophon">
<div><b>Colophon.</b> Written and put together by Lucas Winther, one issue per season when there's enough to say. Recordings are handcam phone videos from <span class="ph" title="placeholder">Lav Sol</span> gigs. Placeholders (dotted underline): zine name, band name, members, songs, venues, dates, all article and finding titles.</div>
<div><b>Elsewhere.</b><br><a href="mailto:lucas@lucaswinther.info">lucas@lucaswinther.info</a><br><a href="#">GitHub · StereotypicalCat</a><br><a href="#">blog.lucaswinther.info</a></div>
<div><b>Back issues</b> stay up as they were printed. Nothing gets rewritten after the season ends.</div>
</footer>
</div>
<script>
const GIGS={
retro:{name:"Café Retro",date:"14 March 2026",note:"Handcam from the bar, eleven people and a dog.",set:[["Grå morgen",252],["Kitchen light",208],["Slow ferry",301],["Halvvejs",196],["Paper boats",244]]},
rehearsal:{name:"Rehearsal, door open",date:"24 September 2026",note:"Filmed by whoever walked past.",set:[["Slow ferry",344],["Halvvejs",201],["New one (no name)",176]]},
kaelder:{name:"Studenterhuset basement",date:"27 October 2025",note:"Phone on a beer crate, front left.",set:[["Paper boats",231],["Grå morgen",270],["New one (no name)",188],["Slow ferry",322]]},
have:{name:"Garden party at Ida's",date:"9 August 2025",note:"Acoustic. Wasps.",set:[["Kitchen light",190],["Halvvejs",205],["Grå morgen",240]]},
loops:{name:"Loop pedal sketches",date:"summer 2026",note:"Ten loops, each under eight bars.",set:[["Loop 03",96],["Loop 07",121],["Loop 10",88]]},
};
const SONGS={
"Grå morgen":{who:[["Lucas","the riff, lead guitar"],["Signe","vocals"],["Mads","bass"],["Jonas","drums, brushes in the intro"]],lyric:"Grey morning, the coffee knows before I do.",origin:"A warm-up riff that Signe wrote words over in one rehearsal. We've never changed a word.",pic:"photo: Signe under a red bulb"},
"Kitchen light":{who:[["Lucas","acoustic, verse two"],["Signe","vocals, Rhodes"],["Mads","bass"],["Jonas","shaker, then drums"]],lyric:"Leave the kitchen light on, I'll find my way.",origin:"Written at 2am on the back of a receipt. Every live version is slower than the last.",pic:"photo: one lamp over a kitchen table"},
"Slow ferry":{who:[["Lucas","guitar, delay pedal"],["Signe","keys, vocals"],["Mads","the long bass notes"],["Jonas","drums"]],lyric:"Nothing to do but stand at the rail.",origin:"First verse written on a ferry ticket. The one song we let run as long as it wants.",pic:"photo: grey sea from the deck"},
"Halvvejs":{who:[["Lucas","guitar"],["Signe","vocals"],["Mads","bass"],["Jonas","drums"]],lyric:"Halvvejs hjem, halvvejs væk.",origin:"Our only song in Danish. It means \"halfway\". The second verse is still unfinished.",pic:"photo: setlist gaffer-taped to the floor"},
"Paper boats":{who:[["Lucas","guitar, harmony"],["Signe","vocals"],["Mads","bass"],["Jonas","count-in, always too fast"]],lyric:"Fold it twice and let it go.",origin:"Usually the closer. Usually counted in too fast.",pic:"photo: blurry crowd, someone waving"},
};
const ISSUES=[
{no:"01",season:"Spring 2026",sides:["retro","kaelder"],
kicker:"Cover story",title:"Eleven people <i>and a dog</i>",stand:"Our first gig of the year was in a café that seats thirty. Notes on small rooms, nerves, and why the handcam stays.",
body:["Small rooms are harder than big ones. You can see every face, and every face can see your left hand. At Café Retro there were eleven people and a dog, and I counted them twice during the first song.","What helped was telling the story behind each song before we played it. People listen differently when they know where a song came from. It's why every track in this issue has liner notes.","The recording is a phone propped on the bar. You can hear the espresso machine in Halvvejs. I wouldn't change it."],
contents:["Eleven people and a dog","Side A: Café Retro","Learning harmony","Found: a tuner"],
musings:[["Learning harmony","Record yourself, listen once, never again.","So far: harmonies on two songs, both shaky."],["Paper notebooks","Half our lyrics started in a meeting margin.","The other half on the bus."],["New strings","Every March. Small rituals.","89 DKK a set, if you're wondering."]],
finds:[["tool","A tuner that doesn't nag","No account, no ads."],["essay","On small rooms","A musician's notes on playing cafés."],["place","A rehearsal room at 90 DKK/h","Thursdays are ours."],["record","Nick Drake, Pink Moon","Twenty-eight minutes. Perfect."]]},
{no:"02",season:"Summer 2026",sides:["loops","have"],
kicker:"Cover story",title:"Ten loops, <i>eight bars each</i>",stand:"A summer project with one rule: no loop longer than eight bars. Most were bad. Three are on side A.",
body:["The loop pedal was a birthday present that sat in a drawer for a year. This summer I gave myself one rule: every loop under eight bars, one loop per rehearsal.","Loop 07 turned into the bridge of a new song. Loop 10 is just a nice noise. Both are on side A.","Side B is last summer's garden party, acoustic, with wasps. It felt right to put them next to each other."],
contents:["Ten loops, eight bars each","Side A: loop sketches","Bus shelters","Found: Pink Moon"],
musings:[["Bus shelters","Someone photographed all of them.","I've looked at most."],["Summer off","No gigs in July, on purpose.","Best decision of the year."],["Writing on the ferry","Where Slow ferry came from.","Wind, ticket, pen."]],
finds:[["link","Every Danish bus shelter","There's a map."],["record","Pink Moon, again","Still twenty-eight minutes."],["tool","A free loop-pedal manual","Nobody reads them. Read it."],["place","Ferry deck, any ferry","Best writing desk I know."]]},
{no:"03",season:"Autumn 2026",sides:["rehearsal","kaelder"],
kicker:"Cover story",title:"Playing with <i>the door open</i>",stand:"In September we rehearsed with the door to the corridor open, and someone filmed it. Then a year-old basement gig turned up on a friend's phone.",
body:["The rehearsal room gets hot in September, so we left the door open. People walking past stopped, and one of them filmed two songs on their phone and sent them to us the next day. That's side A.","Side B is older: the Studenterhuset basement gig from last October, which I thought nobody had recorded. A friend found it on her phone while clearing space. It's loud, it's crooked, and Slow ferry runs to five and a half minutes.","Put them side by side and you can hear a song change over a year. Click a track, then look at the liner notes below for the other nights it was played."],
contents:["Playing with the door open","Side A: rehearsal, side B: basement","Code vs. songs","Found: small software"],
musings:[["Code vs. songs","Both are mostly deleting. One has a test suite.","At work I use AI tools all day. Songwriting is where I deliberately don't."],["On eleven people","Small rooms, revisited.","Still counting the audience during song one."],["Thursday nights","Rehearsal is the only fixed thing in my week.","It's also the best thing."]],
finds:[["essay","\"The case for small software\"","Read it twice. Applies to bands too."],["record","Bill Evans · Sunday at the Village Vanguard","The glasses clink. Handcam in 1961."],["tool","A setlist app that's just a list","No cloud. Good."],["place","The ferry café","Coffee, 25 DKK, sea included."]]},
];
const fmt=s=>Math.floor(s/60)+":"+String(Math.floor(s%60)).padStart(2,"0");
let I=2, side=0, P={g:null,i:0,pos:0,on:false,timer:null}, tab="who";
const issuesEl=document.getElementById("issues");
function renderIssues(){
issuesEl.innerHTML=ISSUES.map((x,k)=>`<button data-k="${k}" aria-pressed="${k===I}">Nº ${x.no} · ${x.season}</button>`).join("")+`<span class="by">older issues stay as printed</span>`;
issuesEl.querySelectorAll("button").forEach(b=>b.onclick=()=>{I=+b.dataset.k; side=0; stop(); renderIssue();});
}
function renderIssue(){
const x=ISSUES[I]; renderIssues();
document.getElementById("no").textContent="Nº "+x.no; document.getElementById("season").textContent=x.season;
document.getElementById("cover").innerHTML=`
<aside class="contents"><div class="label">In this issue</div><ol>${x.contents.map((c,k)=>`<li><span>${k+1}</span><a href="#" class="ph" title="placeholder">${c}</a></li>`).join("")}</ol></aside>
<article class="cover"><div class="label kicker">${x.kicker}</div><h2 class="ph" title="placeholder">${x.title}</h2><p class="stand">${x.stand}</p><div class="byline">Words by Lucas Winther · ${x.season}</div><div class="cols">${x.body.map(p=>`<p>${p}</p>`).join("")}</div></article>
<div class="side" id="side"></div>`;
loadSide(side,false);
renderBack();
}
function loadSide(s,keepPlaying){
side=s; const x=ISSUES[I], g=GIGS[x.sides[s]];
if(!keepPlaying || P.g!==g){ stop(); P.g=g; P.i=0; P.pos=0; }
renderSide(); renderLiner();
}
function renderSide(){
const x=ISSUES[I], g=P.g, el=document.getElementById("side");
el.innerHTML=`<div class="label">The record in this issue</div>
<div class="sidesw" role="group" aria-label="Side">${x.sides.map((id,k)=>`<button data-s="${k}" aria-pressed="${GIGS[id]===g}">Side ${"AB"[k]}</button>`).join("")}</div>
<div class="sidehead ph" title="placeholder">${g.name}</div><div class="sidesub">${g.date} · ${g.note}</div>
<ol class="tracks">${g.set.map((t,k)=>`<li class="${k===P.i&&(P.on||P.pos>0)?"now":""}"><button data-i="${k}" aria-label="Play ${t[0]}"><span class="n">${String(k+1).padStart(2,"0")}</span><span class="tt">${t[0]}<small>${SONGS[t[0]]?SONGS[t[0]].who.length+" players · liner notes":"no notes yet"}</small></span><span class="d">${fmt(t[1])}</span></button></li>`).join("")}</ol>
<div class="deck"><button class="pp" id="pp" aria-label="${P.on?"Pause":"Play"}">${P.on?"❚❚":"▶"}</button><div><button id="prev">← prev</button> · <button id="next">next →</button><div class="state">${P.on?"playing":P.pos>0?"paused":"press play, nothing starts on its own"}</div></div><span class="time" id="time">${fmt(P.pos)}</span></div>
<div class="seek" id="seek"><i id="sfill" style="width:${P.pos/g.set[P.i][1]*100}%"></i></div>
<button class="film"><span class="fr">▶</span><span>Watch the handcam video of this song<br><i>placeholder frame · ${g.name}</i></span></button>`;
el.querySelectorAll(".sidesw button").forEach(b=>b.onclick=()=>loadSide(+b.dataset.s,false));
el.querySelectorAll(".tracks button").forEach(b=>b.onclick=()=>{ const k=+b.dataset.i; if(k===P.i && P.on){ setOn(false); return; } P.i=k; P.pos=0; setOn(true); renderLiner(); });
el.querySelector("#pp").onclick=()=>setOn(!P.on);
el.querySelector("#prev").onclick=()=>{P.i=Math.max(0,P.i-1);P.pos=0;renderSide();renderLiner();};
el.querySelector("#next").onclick=()=>{P.i=Math.min(g.set.length-1,P.i+1);P.pos=0;renderSide();renderLiner();};
el.querySelector("#seek").onclick=e=>{const r=e.currentTarget.getBoundingClientRect(); P.pos=Math.floor((e.clientX-r.left)/r.width*g.set[P.i][1]); tickUI();};
tickUI();
}
function stop(){ clearInterval(P.timer); P.on=false; }
function setOn(on){
clearInterval(P.timer); P.on=on;
if(on) P.timer=setInterval(()=>{ P.pos++; const L=P.g.set[P.i][1]; if(P.pos>=L){ if(P.i<P.g.set.length-1){P.i++;P.pos=0;renderSide();renderLiner();return;} P.pos=L; setOn(false); return; } tickUI(); },1000);
renderSide();
}
function tickUI(){
const L=P.g.set[P.i][1], t=document.getElementById("time"); if(!t) return;
t.textContent=fmt(P.pos); document.getElementById("sfill").style.width=(P.pos/L*100)+"%";
const li=document.querySelectorAll(".tracks li")[P.i]; if(li){ li.classList.add("now"); li.style.setProperty("--p",P.pos/L); }
}
function renderLiner(){
const name=P.g.set[P.i][0], s=SONGS[name], el=document.getElementById("liner");
const where=Object.entries(GIGS).map(([id,g])=>({id,g,j:g.set.findIndex(t=>t[0]===name)})).filter(o=>o.j>-1);
if(!s){ el.innerHTML=`<div class="head"><div class="label">Liner notes · track ${String(P.i+1).padStart(2,"0")}</div></div><h3 class="liner-empty">${name}: no liner notes yet. Some songs only exist for one night.</h3>`; return; }
el.innerHTML=`<div class="head"><div class="label">Liner notes · track ${String(P.i+1).padStart(2,"0")} of side ${"AB"[side]}</div><h3>${name} <small>· ${P.g.name}</small></h3></div>
<div class="ln-left"><p class="quote">“${s.lyric}”</p><p class="origin">${s.origin}</p></div>
<div class="credits"><div class="label">Who plays what</div><dl>${s.who.map(w=>`<dt class="ph" title="placeholder">${w[0]}</dt><dd>${w[1]}</dd>`).join("")}</dl></div>
<div class="halftone"><span>${s.pic} · ${P.g.date}</span></div>
<div class="pressings"><div class="label">The same song on other nights</div><ul>${where.map(o=>`<li><b>${o.g.name}</b>${o.g.date} · ${fmt(o.g.set[o.j][1])}<br>${o.g===P.g?`<span class="here">you're hearing this one</span>`:`<button data-g="${o.id}" data-j="${o.j}">play this version →</button>`}</li>`).join("")}</ul></div>`;
el.querySelectorAll(".pressings button").forEach(b=>b.onclick=()=>{
const id=b.dataset.g, x=ISSUES[I], k=x.sides.indexOf(id), wasOn=P.on;
stop(); P.g=GIGS[id]; P.i=+b.dataset.j; P.pos=0; if(k>-1) side=k;
renderSide(); renderLiner(); if(wasOn) setOn(true);
});
}
function renderBack(){
const x=ISSUES[I];
document.getElementById("back").innerHTML=`
<div class="musings"><div class="label">Musings, short</div><div class="row">${x.musings.map(m=>`<article><h4 class="ph" title="placeholder">${m[0]}</h4><p>${m[1]}</p><details><summary>read the rest +</summary><p>${m[2]}</p></details></article>`).join("")}</div></div>
<div class="classifieds"><div class="label">Found &amp; kept · classifieds</div><div class="grid">${x.finds.map((f,k)=>`<div class="ad${k===0?" wide":""}"><b>${f[0]}</b><a href="#" class="ph" title="placeholder">${f[1]}</a><br>${f[2]}</div>`).join("")}</div></div>`;
}
renderIssue();
</script>
</body>
</html>