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

334 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 (P1 · The crate)</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=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,600&family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
<style>
:root{
--wall:#f2ebdf; --wall2:#e9e0d0; --ink:#2b241d; --soft:#6d6255; --line:#d6cab6;
--wood:#8a5a36; --wood2:#6e4426;
--rust:#c0562f; --mustard:#d6a441; --olive:#6f7d4f; --pine:#2f4b45; --bone:#e6dccb; --ox:#8a3a3a;
--serif:"Fraunces", Georgia, serif; --mono:"Courier Prime", ui-monospace, monospace;
}
*{box-sizing:border-box}
html{scroll-behavior:auto}
body{margin:0;background:var(--wall);color:var(--ink);font-family:var(--serif);font-size:17px;line-height:1.55;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:#fffdf8;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:1080px;margin:0 auto;padding:0 28px}
header.top{display:flex;align-items:baseline;justify-content:space-between;gap:20px;padding:26px 0 14px;border-bottom:1px solid var(--line)}
.name{font-size:20px;font-weight:600;letter-spacing:-.01em}
.name small{font-weight:300;color:var(--soft);font-size:15px;margin-left:8px;font-style:italic}
nav.spaces{font-family:var(--mono);font-size:13px;display:flex;gap:18px;flex-wrap:wrap}
nav.spaces a{text-decoration:none;color:var(--soft)}
nav.spaces a[aria-current]{color:var(--ink);border-bottom:1px solid var(--ink)}
nav.spaces a:hover{color:var(--ink)}
.intro{display:grid;grid-template-columns:1.3fr 1fr;gap:40px;padding:34px 0 10px}
.intro h1{font-weight:300;font-size:40px;line-height:1.1;margin:0;letter-spacing:-.02em;font-variation-settings:"opsz" 120}
.intro h1 em{font-style:italic;color:var(--rust)}
.intro p{margin:0;color:var(--soft);font-size:16px;align-self:end}
/* shelf */
.shelf-head{display:flex;justify-content:space-between;align-items:baseline;margin:30px 0 8px;font-family:var(--mono);font-size:12.5px;color:var(--soft)}
.shelf-head .filters button{font:inherit;background:none;border:0;color:var(--soft);cursor:pointer;padding:2px 6px}
.shelf-head .filters button[aria-pressed="true"]{color:var(--ink);text-decoration:underline}
.shelf{position:relative;padding:0 18px;display:flex;align-items:flex-end;gap:6px;height:196px;border-bottom:16px solid var(--wood);
background:linear-gradient(var(--wall2),var(--wall2)) bottom/100% 4px no-repeat}
.shelf::after{content:"";position:absolute;left:0;right:0;bottom:-22px;height:6px;background:var(--wood2);opacity:.5}
.spine{all:unset;cursor:pointer;position:relative;width:46px;height:var(--h,168px);background:var(--c);border-radius:2px 2px 0 0;
transition:transform .22s ease, opacity .2s;display:flex;align-items:center;justify-content:center;box-shadow:inset -3px 0 0 rgba(0,0,0,.12), inset 2px 0 0 rgba(255,255,255,.12)}
.spine .lbl{writing-mode:vertical-rl;transform:rotate(180deg);background:#f7f1e4;color:var(--ink);font-family:var(--mono);font-size:12px;padding:10px 4px;
width:28px;height:78%;display:flex;align-items:center;justify-content:space-between;white-space:nowrap;overflow:hidden}
.spine .lbl i{font-style:normal;color:var(--soft);font-size:10.5px}
.spine:hover,.spine:focus-visible{transform:translateY(-10px)}
.spine:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
.spine.out{transform:translateY(-34px)}
.spine.dim{opacity:.28}
.spine.thin{width:30px}
.spine.thin .lbl{width:18px;font-size:10px}
.bookend{width:12px;height:120px;background:var(--wood2);margin-left:auto;border-radius:2px 2px 0 0}
.shelf .card{align-self:flex-end;margin-left:14px;font-family:var(--mono);font-size:11.5px;color:var(--soft);width:150px;line-height:1.4;padding-bottom:8px}
.peek{min-height:22px;margin:30px 0 0;font-family:var(--mono);font-size:13px;color:var(--soft)}
/* deck */
.deck{margin:12px 0 0;border:1px solid var(--line);background:#f7f1e6;display:grid;grid-template-columns:330px 1fr;min-height:260px}
.deck.empty{grid-template-columns:1fr}
.deck .hint{padding:46px 30px;text-align:center;color:var(--soft);font-style:italic}
.face{padding:22px;border-right:1px solid var(--line);display:flex;flex-direction:column;gap:14px}
.cass{position:relative;height:178px;border-radius:8px;background:var(--c,#333);padding:12px 14px}
.cass .paper{background:#f7f1e4;height:58px;padding:6px 10px;font-family:var(--mono);font-size:12px;line-height:1.3}
.cass .paper b{display:block;font-size:14px}
.cass .win{position:absolute;left:50%;transform:translateX(-50%);bottom:24px;width:180px;height:62px;background:#1f1a15;border-radius:30px;display:flex;justify-content:space-between;align-items:center;padding:0 14px}
.reel{width:38px;height:38px;border-radius:50%;background:repeating-conic-gradient(#efe6d6 0 18deg,#8d8273 18deg 60deg);border:5px solid #3a3129}
.reel.l{box-shadow:0 0 0 calc(var(--fl,10px)) #4a3a2c}
.reel.r{box-shadow:0 0 0 calc(var(--fr,3px)) #4a3a2c}
.playing .reel{animation:spin 2.2s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.playing .reel{animation:none}}
.cass .holes{position:absolute;bottom:6px;left:50%;transform:translateX(-50%);width:110px;height:12px;background:rgba(0,0,0,.18);clip-path:polygon(8% 0,92% 0,100% 100%,0 100%)}
.controls{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:13px}
.btn{all:unset;cursor:pointer;width:40px;height:40px;border:1.5px solid var(--ink);border-radius:50%;display:grid;place-items:center}
.btn:hover{background:var(--ink);color:var(--wall)}
.btn:focus-visible{outline:2px solid var(--rust);outline-offset:2px}
.btn.sm{width:30px;height:30px;border-width:1px}
.bar{flex:1;height:22px;position:relative;cursor:pointer}
.bar::before{content:"";position:absolute;left:0;right:0;top:10px;height:2px;background:var(--line)}
.bar .fill{position:absolute;left:0;top:10px;height:2px;background:var(--ink);width:0}
.bar .tick{position:absolute;top:6px;width:1px;height:10px;background:var(--soft)}
.time{min-width:96px;text-align:right;color:var(--soft)}
.meta{font-size:14px;color:var(--soft)}
.meta strong{color:var(--ink);font-weight:600}
.explore{display:grid;grid-template-columns:1fr 1.25fr;min-width:0}
.setlist{padding:18px 0;border-right:1px solid var(--line)}
.setlist h3,.behind h3{font-family:var(--mono);font-weight:400;font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--soft);margin:0 22px 8px}
.setlist ol{list-style:none;margin:0;padding:0;counter-reset:s}
.setlist li button{all:unset;box-sizing:border-box;cursor:pointer;display:grid;grid-template-columns:22px 1fr auto;width:100%;padding:7px 22px;font-size:15.5px;gap:8px}
.setlist li button::before{counter-increment:s;content:counter(s,decimal-leading-zero);font-family:var(--mono);font-size:11px;color:var(--soft);padding-top:4px}
.setlist li button span{font-family:var(--mono);font-size:12px;color:var(--soft);padding-top:3px}
.setlist li button:hover{background:var(--wall2)}
.setlist li button:focus-visible{outline:2px solid var(--ink);outline-offset:-2px}
.setlist li.now button{background:var(--ink);color:var(--wall)}
.setlist li.now button::before,.setlist li.now button span{color:var(--bone)}
.behind{padding:18px 22px}
.behind h4{font-weight:400;font-size:26px;margin:0 0 4px;letter-spacing:-.01em;font-variation-settings:"opsz" 72}
.tabs{display:flex;flex-wrap:wrap;gap:2px;margin:10px 0 12px;font-family:var(--mono);font-size:12.5px}
.tabs button{all:unset;cursor:pointer;padding:4px 8px;white-space:nowrap;border:1px solid var(--line);color:var(--soft)}
.tabs button[aria-selected="true"]{border-color:var(--ink);color:var(--ink);background:#fffaf0}
.tabs button:focus-visible{outline:2px solid var(--rust)}
.pane{font-size:15.5px;min-height:150px}
.who{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin:0}
.who dt{font-family:var(--mono);font-size:13px;color:var(--soft);padding-top:2px}
.who dd{margin:0}
blockquote{margin:0 0 10px;padding-left:14px;border-left:2px solid var(--rust);font-style:italic}
.photo{height:150px;background:repeating-linear-gradient(135deg,#d9cdb8 0 10px,#d2c5ae 10px 20px);display:flex;align-items:flex-end;padding:8px;font-family:var(--mono);font-size:12px;color:#5d5246}
.versions{list-style:none;margin:0;padding:0}
.versions li{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:7px 0;border-bottom:1px dashed var(--line)}
.versions li .sw{width:14px;height:14px;border-radius:2px;background:var(--c);display:inline-block;margin-right:8px;vertical-align:-2px}
.versions button{font:12px var(--mono);background:none;border:1px solid var(--ink);padding:3px 8px;cursor:pointer}
.versions button:hover{background:var(--ink);color:var(--wall)}
.versions .cur{font:12px var(--mono);color:var(--soft)}
.video{margin-top:12px;height:120px;background:#2d2620;color:#cfc3b0;display:flex;align-items:center;gap:14px;padding:0 18px;font-family:var(--mono);font-size:12.5px;cursor:pointer;border:0;width:100%;text-align:left}
.video .pl{width:38px;height:38px;border:1.5px solid #cfc3b0;border-radius:50%;display:grid;place-items:center;flex:none}
.video.on{background:#1b1612}
/* quiet section */
.quiet{margin:70px 0 0;padding-top:30px;border-top:1px solid var(--line);display:grid;grid-template-columns:1fr 1fr;gap:56px}
.quiet h2{font-family:var(--mono);font-weight:400;font-size:13px;letter-spacing:.06em;text-transform:uppercase;color:var(--soft);margin:0 0 14px}
.writing details{border-bottom:1px solid var(--line);padding:10px 0}
.writing summary{cursor:pointer;list-style:none;display:flex;justify-content:space-between;gap:16px;font-size:18px}
.writing summary::-webkit-details-marker{display:none}
.writing summary span{font-family:var(--mono);font-size:12px;color:var(--soft);white-space:nowrap;padding-top:4px}
.writing summary:hover{color:var(--rust)}
.writing details p{margin:8px 0 2px;color:var(--soft);font-size:15.5px}
.finds{list-style:none;padding:0;margin:0}
.finds li{padding:9px 0;border-bottom:1px solid var(--line);display:grid;grid-template-columns:70px 1fr;gap:12px;font-size:15.5px}
.finds li i{font-style:normal;font-family:var(--mono);font-size:11.5px;color:var(--soft);padding-top:3px}
.finds a{text-decoration:none;border-bottom:1px solid var(--line)}
.finds a:hover{border-color:var(--ink)}
.finds small{display:block;color:var(--soft);font-size:14px}
footer{margin:60px 0 30px;padding-top:14px;border-top:1px solid var(--line);font-family:var(--mono);font-size:12px;color:var(--soft);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
@media (max-width:820px){
.intro,.quiet{grid-template-columns:1fr;gap:20px}
.deck{grid-template-columns:1fr}
.face{border-right:0;border-bottom:1px solid var(--line)}
.explore{grid-template-columns:1fr}
.setlist{border-right:0;border-bottom:1px solid var(--line)}
.shelf{overflow-x:auto;gap:4px}
.shelf .card{display:none}
.intro h1{font-size:30px}
}
</style>
</head>
<body>
<div class="mock-note" role="note"><button aria-label="Dismiss note" onclick="this.parentNode.remove()">×</button><b>P1 · The crate</b>Recordings as cassettes on a shelf: pull one out, it plays, and the setlist, behind-the-song notes and other versions open underneath. Borrows mindofakira's "choose to play, then explore"; writing stays quiet below, like marcschuster.</div>
<div class="wrap">
<header class="top">
<div class="name">Lucas Winther <small>as a person</small></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">
<h1>Mostly I write software. In the evenings I play guitar in a <em class="ph" title="placeholder">small band</em> and write things down.</h1>
<p>Everything we've recorded fits on one shelf. Pull a tape out to listen, then dig into it: who's playing, where it came from, how it sounded on other nights.</p>
</section>
<div class="shelf-head">
<span>the shelf · <span id="count"></span></span>
<span class="filters" role="group" aria-label="Filter tapes">
<button aria-pressed="true" data-f="all">all</button>
<button aria-pressed="false" data-f="live">live handcam</button>
<button aria-pressed="false" data-f="demo">demos &amp; projects</button>
</span>
</div>
<div class="shelf" id="shelf" role="list" aria-label="Recordings"></div>
<div class="peek" id="peek" aria-live="polite">Hover a tape to read the label. Click to pull it out.</div>
<section class="deck empty" id="deck" aria-label="Player">
<div class="hint">The deck is empty. Nothing plays until you pick a tape.</div>
</section>
<section class="quiet">
<div class="writing">
<h2>Writing, now and then</h2>
<details><summary><span class="ph" title="placeholder">On playing to eleven people</span><span>Oct 2026</span></summary><p>Small rooms are harder than big ones. You can see every face, and every face can see your left hand. Some notes after the October gig.</p></details>
<details><summary><span class="ph" title="placeholder">Why I still keep a paper notebook</span><span>Aug 2026</span></summary><p>Half of the band's lyrics started in the margin of a meeting. The other half started on the bus.</p></details>
<details><summary><span class="ph" title="placeholder">Handcam is the honest format</span><span>May 2026</span></summary><p>Shaky phone footage from the third row, with the bar chatter left in. I've stopped wanting anything cleaner.</p></details>
<details><summary><span class="ph" title="placeholder">A winter of learning to sing harmony</span><span>Feb 2026</span></summary><p>Short version: record yourself, listen once, never again.</p></details>
</div>
<div>
<h2>Findings</h2>
<ul class="finds">
<li><i>link</i><div><a href="#" class="ph" title="placeholder">A field guide to Danish bus shelters</a><small>Someone photographed all of them. All of them.</small></div></li>
<li><i>record</i><div><a href="#" class="ph" title="placeholder">Bill Evans · Sunday at the Village Vanguard</a><small>Listen for the glasses clinking. That's the handcam feeling, in 1961.</small></div></li>
<li><i>tool</i><div><a href="#" class="ph" title="placeholder">A free tuner that doesn't nag</a><small>Rare.</small></div></li>
<li><i>essay</i><div><a href="#" class="ph" title="placeholder">"The case for small software"</a><small>Read it twice. Applies to bands too.</small></div></li>
<li><i>place</i><div><a href="#" class="ph" title="placeholder">A rehearsal room that rents by the hour, 90 DKK</a><small>Thursday nights, if you need us.</small></div></li>
</ul>
</div>
</section>
<footer>
<span>Placeholders (dotted underline): band name <span class="ph" title="placeholder">Lav Sol</span>, members, songs, venues, dates, writing titles, findings, 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 M = {lucas:"Lucas · guitar, backing vocals", signe:"Signe · vocals, keys", mads:"Mads · bass", jonas:"Jonas · drums"};
const tapes = [
{id:"retro", kind:"live", c:"var(--rust)", h:172, title:"Live at Café Retro", date:"14 Mar 2026", venue:"Café Retro (placeholder)", note:"Filmed by Signe's flatmate from the bar.",
set:[["Grå morgen",252],["Kitchen light",208],["Slow ferry",301],["Halvvejs",196],["Paper boats",244]]},
{id:"kaelder", kind:"live", c:"var(--pine)", h:160, title:"Basement, Studenterhuset", date:"27 Oct 2025", venue:"Studenterhuset basement (placeholder)", note:"Phone on a beer crate, front left.",
set:[["Paper boats",231],["Grå morgen",270],["New one (no name)",188],["Slow ferry",322]]},
{id:"have", kind:"live", c:"var(--mustard)", h:150, title:"Garden party, Ida's", date:"9 Aug 2025", venue:"a friend's garden (placeholder)", note:"Acoustic. Wasps were involved.",
set:[["Kitchen light",190],["Halvvejs",205],["Grå morgen",240]]},
{id:"demo1", kind:"demo", c:"var(--olive)", h:138, thin:true, title:"Kitchen demos", date:"winter 2024", venue:"Lucas's kitchen", note:"One mic, laptop, a lot of fridge hum.",
set:[["Kitchen light (first take)",174],["Slow ferry (sketch)",142]]},
{id:"loop", kind:"demo", c:"var(--ox)", h:146, thin:true, title:"Loop pedal project", date:"spring 2026", venue:"rehearsal room", note:"Ten loops, one rule: every loop under eight bars.",
set:[["Loop 03",96],["Loop 07",121],["Loop 10",88]]},
{id:"spil", kind:"live", c:"var(--bone)", h:166, title:"First gig ever", date:"2 Jun 2024", venue:"a bar's open mic (placeholder)", note:"Three songs, too fast, very nervous.",
set:[["Halvvejs",170],["Paper boats",215],["Kitchen light",180]]},
];
const songs = {
"Grå morgen":{who:{lucas:"lead guitar, the riff",signe:"vocals",mads:"bass",jonas:"drums, brushes in the intro"},
lyric:"\"Grey morning, the coffee knows before I do.\"", origin:"Started as a riff Lucas played to warm up. Signe wrote words over it in one rehearsal and we never changed them.", photo:"photo: Signe at the mic, red light"},
"Kitchen light":{who:{lucas:"acoustic, vocals on verse 2",signe:"vocals, Rhodes",mads:"bass",jonas:"shaker, then drums"},
lyric:"\"Leave the kitchen light on, I'll find my way.\"", origin:"Written in a kitchen at 2am. The demo is on the olive tape; the live versions got slower every year.", photo:"photo: the kitchen, one lamp"},
"Slow ferry":{who:{lucas:"guitar, delay pedal",signe:"keys, vocals",mads:"bass, the long notes",jonas:"drums"},
lyric:"\"Nothing to do but stand at the rail.\"", origin:"About the ferry to Bornholm (placeholder). It's the one song we let run long.", photo:"photo: Mads, eyes closed"},
"Halvvejs":{who:{lucas:"guitar",signe:"vocals",mads:"bass",jonas:"drums"},
lyric:"\"Halvvejs hjem, halvvejs væk.\"", origin:"Our only song in Danish. The title means \"halfway\".", photo:"photo: setlist taped to the floor"},
"Paper boats":{who:{lucas:"guitar, harmony",signe:"vocals",mads:"bass",jonas:"drums, count-in"},
lyric:"\"Fold it twice and let it go.\"", origin:"Usually the last song. Jonas counts in too fast, every time.", photo:"photo: crowd, blurry, happy"},
};
const base = t => t.replace(/ \(.*\)$/,"");
const fmt = s => (Math.floor(s/60))+":"+String(Math.floor(s%60)).padStart(2,"0");
const shelf = document.getElementById("shelf"), peek = document.getElementById("peek"), deck = document.getElementById("deck");
function renderShelf(){
shelf.innerHTML = tapes.map(t=>`<button class="spine${t.thin?" thin":""}" role="listitem" data-id="${t.id}" data-kind="${t.kind}" style="--c:${t.c};--h:${t.h}px" aria-label="${t.title}, ${t.date}"><span class="lbl">${t.title}<i>${t.date.slice(-4)}</i></span></button>`).join("")
+ `<div class="card">${tapes.length} tapes. Live ones are handcam; the thin ones are demos and side projects.</div><div class="bookend"></div>`;
document.getElementById("count").textContent = tapes.length+" tapes";
shelf.querySelectorAll(".spine").forEach(b=>{
const t = tapes.find(x=>x.id===b.dataset.id);
const show = ()=> peek.innerHTML = `<b>${t.title}</b> · ${t.date} · ${t.set.length} songs · ${fmt(t.set.reduce((a,s)=>a+s[1],0))} · <i>${t.note}</i>`;
b.addEventListener("mouseenter",show); b.addEventListener("focus",show);
b.addEventListener("click",()=>load(t.id,0,true));
});
}
document.querySelectorAll(".filters button").forEach(b=>b.addEventListener("click",()=>{
document.querySelectorAll(".filters button").forEach(x=>x.setAttribute("aria-pressed",x===b));
shelf.querySelectorAll(".spine").forEach(s=>s.classList.toggle("dim", b.dataset.f!=="all" && s.dataset.kind!==b.dataset.f));
}));
let cur=null, pos=0, playing=false, timer=null, tab="who", videoOn=false;
const starts = t => {let a=0; return t.set.map(s=>{const st=a; a+=s[1]; return st;});};
const total = t => t.set.reduce((a,s)=>a+s[1],0);
const idxAt = (t,p)=>{const st=starts(t); let i=0; st.forEach((v,k)=>{if(p>=v)i=k}); return i;};
function load(id, songIdx, autoplay){
cur = tapes.find(t=>t.id===id); pos = starts(cur)[songIdx]; videoOn=false;
shelf.querySelectorAll(".spine").forEach(s=>s.classList.toggle("out", s.dataset.id===id));
deck.classList.remove("empty");
const st = starts(cur), T = total(cur);
deck.innerHTML = `
<div class="face" id="face">
<div class="cass" style="--c:${cur.c}"><div class="paper"><b>${cur.title}</b>${cur.date} · side A</div>
<div class="win"><span class="reel l"></span><span class="reel r"></span></div><span class="holes"></span></div>
<div class="controls">
<button class="btn sm" id="prev" aria-label="Previous song">«</button>
<button class="btn" id="pp" aria-label="Play">▶</button>
<button class="btn sm" id="next" aria-label="Next song">»</button>
<span class="time" id="time">0:00 / ${fmt(T)}</span>
</div>
<div class="bar" id="bar" aria-label="Seek">${st.slice(1).map(s=>`<span class="tick" style="left:${s/T*100}%"></span>`).join("")}<span class="fill" id="fill"></span></div>
<div class="meta"><strong>${cur.venue}</strong><br>${cur.note}</div>
</div>
<div class="explore">
<div class="setlist"><h3>Setlist · jump anywhere</h3><ol id="sl">${cur.set.map((s,i)=>`<li><button data-i="${i}">${s[0]}<span>${fmt(st[i])}</span></button></li>`).join("")}</ol></div>
<div class="behind" id="behind"></div>
</div>`;
deck.querySelector("#pp").onclick = ()=> setPlay(!playing);
deck.querySelector("#prev").onclick = ()=> jump(Math.max(0, idxAt(cur,pos)-1));
deck.querySelector("#next").onclick = ()=> jump(Math.min(cur.set.length-1, idxAt(cur,pos)+1));
deck.querySelector("#bar").onclick = e => { const r=e.currentTarget.getBoundingClientRect(); pos = (e.clientX-r.left)/r.width*T; update(true); };
deck.querySelectorAll("#sl button").forEach(b=>b.onclick=()=>jump(+b.dataset.i));
setPlay(!!autoplay); update(true);
}
function jump(i){ pos = starts(cur)[i]; update(true); }
function setPlay(on){
playing = on; clearInterval(timer);
const pp = document.getElementById("pp"); if(!pp) return;
pp.textContent = on ? "❚❚" : "▶"; pp.setAttribute("aria-label", on?"Pause":"Play");
document.getElementById("face").classList.toggle("playing", on);
if(on) timer = setInterval(()=>{ pos+=1; if(pos>=total(cur)){pos=total(cur); setPlay(false);} update(false); },1000);
}
let lastIdx=-1;
function update(force){
const T=total(cur), i=idxAt(cur,pos);
document.getElementById("fill").style.width = (pos/T*100)+"%";
document.getElementById("time").textContent = fmt(pos)+" / "+fmt(T);
const face=document.querySelector(".cass"); face.style.setProperty("--fl",(14-pos/T*12)+"px"); face.style.setProperty("--fr",(2+pos/T*12)+"px");
document.querySelectorAll("#sl li").forEach((li,k)=>li.classList.toggle("now",k===i));
if(force || i!==lastIdx){ lastIdx=i; renderBehind(i); }
}
function renderBehind(i){
const title = cur.set[i][0], s = songs[base(title)];
const versions = tapes.filter(t=>t.set.some(x=>base(x[0])===base(title)));
let pane = "";
if(!s){ pane = `<p style="color:var(--soft)">No notes for this one yet. ${cur.kind==="demo"?"It's a sketch from a side project.":"We only played it once."}</p>`; }
else if(tab==="who"){ pane = `<dl class="who">${Object.entries(s.who).map(([k,v])=>`<dt>${M[k].split(" · ")[0]}</dt><dd>${v}</dd>`).join("")}</dl>`; }
else if(tab==="story"){ pane = `<blockquote>${s.lyric}</blockquote><p style="margin:0">${s.origin}</p>`; }
else if(tab==="photo"){ pane = `<div class="photo">${s.photo} · ${cur.date}</div>`; }
else { pane = `<ul class="versions">${versions.map(t=>{const j=t.set.findIndex(x=>base(x[0])===base(title));
return `<li><span><span class="sw" style="--c:${t.c}"></span>${t.title} <span style="color:var(--soft);font-size:13px">· ${t.date} · ${fmt(t.set[j][1])}</span></span>${t===cur?'<span class="cur">playing</span>':`<button data-t="${t.id}" data-j="${j}">play this one</button>`}</li>`;}).join("")}</ul>`; }
document.getElementById("behind").innerHTML = `
<h3>Behind the song</h3><h4>${title}</h4>
<div class="tabs" role="tablist">${[["who","who plays what"],["story","lyric & origin"],["photo","photo from the night"],["versions",`other nights (${versions.length})`]].map(([k,l])=>`<button role="tab" aria-selected="${tab===k}" data-k="${k}">${l}</button>`).join("")}</div>
<div class="pane">${pane}</div>
${cur.kind==="live"?`<button class="video${videoOn?" on":""}" id="vid"><span class="pl">${videoOn?"❚❚":"▶"}</span><span>handcam video · ${title} · ${cur.venue}<br><span style="opacity:.7">${videoOn?"playing (placeholder frame)":"placeholder frame, click to watch"}</span></span></button>`:""}`;
document.querySelectorAll(".tabs button").forEach(b=>b.onclick=()=>{tab=b.dataset.k; renderBehind(idxAt(cur,pos));});
document.querySelectorAll(".versions button").forEach(b=>b.onclick=()=>load(b.dataset.t,+b.dataset.j,playing));
const v=document.getElementById("vid"); if(v) v.onclick=()=>{videoOn=!videoOn; renderBehind(idxAt(cur,pos));};
}
renderShelf();
</script>
</body>
</html>