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>
337 lines
27 KiB
HTML
337 lines
27 KiB
HTML
<!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 (P4 · The almanac)</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=Cormorant+Garamond:ital,wght@0,500;0,600;1,500&family=Crimson+Pro:ital,wght@0,400;0,600;1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--paper:#efe6d3; --paper2:#e6dbc4; --ink:#2a2620; --soft:#6e6455; --line:#cdbfa3;
|
||
--winter:#6b7c88; --spring:#7a955a; --summer:#cf982e; --autumn:#b4532f;
|
||
--disp:"Cormorant Garamond", Georgia, serif; --serif:"Crimson Pro", Georgia, serif; --mono:"DM Mono", ui-monospace, monospace;
|
||
}
|
||
*{box-sizing:border-box}
|
||
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--serif);font-size:18px;line-height:1.5}
|
||
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:1120px;margin:0 auto;padding:0 30px}
|
||
header{display:flex;justify-content:space-between;align-items:baseline;padding:24px 0 12px;gap:20px;flex-wrap:wrap}
|
||
header .name{font:600 22px var(--disp)}
|
||
nav.spaces{font:12px 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}
|
||
|
||
.title{border-top:3px double var(--ink);border-bottom:1px solid var(--ink);padding:14px 0 12px;display:grid;grid-template-columns:auto 1fr auto;gap:30px;align-items:end}
|
||
.title h1{font:500 58px/0.95 var(--disp);margin:0;letter-spacing:-.01em}
|
||
.title h1 i{font-style:italic;color:var(--soft)}
|
||
.title p{margin:0;color:var(--soft);font-size:17px;max-width:520px}
|
||
.years{display:flex;gap:4px;font:13px var(--mono)}
|
||
.years button{all:unset;cursor:pointer;padding:4px 9px;border:1px solid transparent;color:var(--soft)}
|
||
.years button[aria-pressed="true"]{border-color:var(--ink);color:var(--ink)}
|
||
.years button:hover{color:var(--ink)}
|
||
.years button:focus-visible{outline:2px solid var(--autumn)}
|
||
|
||
.summary{display:flex;gap:26px;font:12px var(--mono);color:var(--soft);padding:10px 0;flex-wrap:wrap}
|
||
.summary b{color:var(--ink);font-weight:500}
|
||
.key{margin-left:auto;display:flex;gap:14px;align-items:center}
|
||
.key span{display:inline-flex;gap:5px;align-items:center}
|
||
|
||
/* strip */
|
||
.seasons{display:grid;grid-template-columns:2fr 3fr 3fr 3fr 1fr;font:11px var(--mono);letter-spacing:.08em;text-transform:uppercase;margin-top:12px}
|
||
.seasons div{padding:0 0 5px 6px;border-bottom:2px solid var(--c)}
|
||
.seasons div:last-child{padding-left:0}
|
||
.strip{display:grid;grid-template-columns:repeat(12,1fr);border-left:1px solid var(--line)}
|
||
.month{all:unset;box-sizing:border-box;cursor:pointer;position:relative;border-right:1px solid var(--line);height:176px;display:flex;flex-direction:column;padding:6px 6px 0;transition:background .15s}
|
||
.month:hover,.month:focus-visible{background:var(--paper2)}
|
||
.month:focus-visible{outline:2px solid var(--ink);outline-offset:-2px}
|
||
.month[aria-pressed="true"]{background:#f7f0e1;box-shadow:inset 0 -3px 0 var(--c)}
|
||
.month .mn{font:600 21px var(--disp);line-height:1}
|
||
.month .mn small{display:block;font:10.5px var(--mono);color:var(--soft);margin-top:3px;font-weight:400}
|
||
.month .days{position:relative;flex:1;margin-top:8px}
|
||
.month .days::before{content:"";position:absolute;left:50%;top:0;bottom:0;border-left:1px dotted var(--line)}
|
||
.mk{position:absolute;left:50%;transform:translate(-50%,-50%);width:13px;height:13px;background:var(--ink)}
|
||
.mk.post{border-radius:50%;background:var(--paper);border:1.5px solid var(--ink)}
|
||
.mk.gig{background:var(--c)}
|
||
.mk.find{transform:translate(-50%,-50%) rotate(45deg);width:10px;height:10px;background:var(--soft)}
|
||
.mk.proj{background:none;width:0;height:0;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:12px solid var(--ink)}
|
||
.mk.off{left:calc(50% + 12px)}
|
||
.mk.off2{left:calc(50% - 12px)}
|
||
.month .q{font:italic 13px var(--serif);color:var(--soft);padding:0 0 8px;line-height:1.2;min-height:30px}
|
||
.month.future{cursor:default;color:#a99c86}
|
||
.month.future .days::before{border-color:#ddd2bc}
|
||
.today{position:absolute;top:0;bottom:0;border-left:1.5px solid var(--autumn)}
|
||
.today::after{content:"today";position:absolute;top:-2px;left:4px;font:10px var(--mono);color:var(--autumn)}
|
||
|
||
.peek{min-height:26px;font:13px var(--mono);color:var(--soft);padding:10px 0 0}
|
||
|
||
/* month page */
|
||
.page{margin-top:18px;border-top:1px solid var(--ink);display:grid;grid-template-columns:280px 1fr;gap:40px;padding:22px 0 10px}
|
||
.page .left h2{font:500 64px/0.9 var(--disp);margin:0}
|
||
.page .left h2 small{display:block;font:12px var(--mono);color:var(--soft);margin-top:10px;letter-spacing:.06em;text-transform:uppercase}
|
||
.alm{margin-top:18px;font-size:16px;border-top:1px solid var(--line)}
|
||
.alm div{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-bottom:1px solid var(--line)}
|
||
.alm span:first-child{font:11.5px var(--mono);color:var(--soft);padding-top:3px}
|
||
.alm span:last-child{text-align:right}
|
||
.weather{font-style:italic;color:var(--soft);margin-top:14px;font-size:17px}
|
||
.items{list-style:none;margin:0;padding:0}
|
||
.items > li{border-bottom:1px solid var(--line);padding:12px 0}
|
||
.items .row{all:unset;box-sizing:border-box;cursor:pointer;display:grid;grid-template-columns:46px 1fr auto;gap:14px;width:100%;align-items:baseline}
|
||
.items .row:focus-visible{outline:2px solid var(--autumn);outline-offset:3px}
|
||
.items .day{font:12px var(--mono);color:var(--soft)}
|
||
.items h3{margin:0;font-weight:600;font-size:21px;line-height:1.2}
|
||
.items .row:hover h3{color:var(--autumn)}
|
||
.items .row p{margin:3px 0 0;color:var(--soft);font-size:16.5px}
|
||
.items .kind{font:11px var(--mono);color:var(--soft);text-transform:uppercase;letter-spacing:.06em}
|
||
.items .x{display:none;padding:12px 0 4px 60px}
|
||
.items li.open .x{display:block}
|
||
.items .x p{margin:0 0 10px;max-width:62ch}
|
||
.quiet{font-style:italic;color:var(--soft);font-size:19px;padding:12px 0;max-width:52ch}
|
||
|
||
/* gig player */
|
||
.gigbox{display:grid;grid-template-columns:1fr 1.15fr;gap:22px}
|
||
.cam{height:160px;background:#2b2722;color:#d7ccb8;display:flex;flex-direction:column;justify-content:flex-end;padding:10px 12px;font:11px var(--mono);position:relative}
|
||
.cam .big{position:absolute;inset:0;margin:auto;width:46px;height:46px;border:1.5px solid #d7ccb8;border-radius:50%;display:grid;place-items:center;background:none;color:#d7ccb8;cursor:pointer;font-size:14px}
|
||
.cam .big:hover{background:#d7ccb8;color:#2b2722}
|
||
.bar{height:18px;position:relative;cursor:pointer;margin-top:8px}
|
||
.bar::before{content:"";position:absolute;left:0;right:0;top:8px;height:2px;background:var(--line)}
|
||
.bar i{position:absolute;left:0;top:8px;height:2px;background:var(--autumn);width:0}
|
||
.bar b{position:absolute;top:5px;width:1px;height:8px;background:var(--soft)}
|
||
.tm{display:flex;justify-content:space-between;font:11.5px var(--mono);color:var(--soft)}
|
||
.set{list-style:none;margin:10px 0 0;padding:0}
|
||
.set button{all:unset;box-sizing:border-box;cursor:pointer;display:flex;justify-content:space-between;width:100%;padding:4px 8px;font-size:16.5px}
|
||
.set button span{font:11.5px var(--mono);color:var(--soft);padding-top:3px}
|
||
.set button:hover{background:var(--paper2)}
|
||
.set li.now button{background:var(--ink);color:var(--paper)}
|
||
.set li.now button span{color:var(--line)}
|
||
.song{background:#f7f0e1;padding:14px 16px;border:1px solid var(--line)}
|
||
.song h4{font:500 30px/1 var(--disp);margin:0 0 8px}
|
||
.song .tabs{display:flex;gap:14px;font:11.5px var(--mono);margin-bottom:10px;flex-wrap:wrap}
|
||
.song .tabs button{all:unset;cursor:pointer;color:var(--soft);border-bottom:1px solid transparent}
|
||
.song .tabs button[aria-selected="true"]{color:var(--ink);border-color:var(--ink)}
|
||
.song .tabs button:focus-visible{outline:2px solid var(--autumn)}
|
||
.song dl{display:grid;grid-template-columns:auto 1fr;gap:3px 14px;margin:0;font-size:16.5px}
|
||
.song dt{font:11.5px var(--mono);color:var(--soft);padding-top:4px}
|
||
.song dd{margin:0}
|
||
.song .pic{height:130px;background:repeating-linear-gradient(90deg,#ddd1b9 0 2px,#e7dcc6 2px 9px);font:11px var(--mono);color:var(--soft);display:flex;align-items:flex-end;padding:8px}
|
||
.others{list-style:none;margin:0;padding:0;font-size:16.5px}
|
||
.others li{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-bottom:1px dashed var(--line)}
|
||
.others button{all:unset;cursor:pointer;font:11.5px var(--mono);color:var(--autumn)}
|
||
.others button:hover{text-decoration:underline}
|
||
.ctrls{display:flex;gap:8px;align-items:center;margin-top:8px}
|
||
.ctrls button{all:unset;cursor:pointer;width:32px;height:32px;border:1px solid var(--ink);border-radius:50%;display:grid;place-items:center;font-size:12px}
|
||
.ctrls button.pp{width:40px;height:40px;background:var(--ink);color:var(--paper)}
|
||
.ctrls button:focus-visible{outline:2px solid var(--autumn);outline-offset:2px}
|
||
|
||
footer{border-top:1px solid var(--line);margin-top:40px;padding:14px 0 30px;font:11.5px/1.7 var(--mono);color:var(--soft);display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||
@media (max-width:860px){
|
||
.title{grid-template-columns:1fr;gap:12px} .title h1{font-size:44px}
|
||
.strip{overflow-x:auto;grid-template-columns:repeat(12,minmax(70px,1fr))} .seasons{display:none}
|
||
.page{grid-template-columns:1fr;gap:16px} .gigbox{grid-template-columns:1fr}
|
||
.items .x{padding-left:0}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="mock-note" role="note"><button aria-label="Dismiss note" onclick="this.parentNode.remove()">×</button><b>P4 · The almanac</b>One year at a time on a seasonal strip. Hover a month to peek, click it to open that month's page: a gig with its setlist and per-song notes, a post, a find, or just a line about the weather. Quiet months are part of the design. Borrows lynnandtonic's yearly archive (the idea, not the look) and beige.zone's archival labels.</div>
|
||
|
||
<div class="wrap">
|
||
<header>
|
||
<div class="name">Lucas Winther</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="title">
|
||
<h1>Almanac <i id="yr">2026</i></h1>
|
||
<p>What happened outside work, a month at a time: gigs with <span class="ph" title="placeholder">Lav Sol</span>, things I wrote, things I found. Some months are full. Most aren't, and that's fine.</p>
|
||
<div class="years" role="group" aria-label="Year" id="years"></div>
|
||
</section>
|
||
|
||
<div class="summary" id="summary"></div>
|
||
|
||
<div class="seasons" aria-hidden="true">
|
||
<div style="--c:var(--winter)">winter</div><div style="--c:var(--spring)">spring</div><div style="--c:var(--summer)">summer</div><div style="--c:var(--autumn)">autumn</div><div style="--c:var(--winter)"></div>
|
||
</div>
|
||
<div class="strip" id="strip" role="group" aria-label="Months"></div>
|
||
<div class="peek" id="peek" aria-live="polite">Hover a month to peek. Click to open its page.</div>
|
||
|
||
<section class="page" id="page" aria-live="polite"></section>
|
||
|
||
<footer><span>Placeholders: band name, members, songs, venues, gig dates, post and finding titles, the almanac lines, 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 MON=["January","February","March","April","May","June","July","August","September","October","November","December"];
|
||
const season=m=>[0,1,11].includes(m)?"winter":m<5?"spring":m<8?"summer":"autumn";
|
||
const TODAY={y:2026,m:10,d:9};
|
||
const GIGS={
|
||
retro:{name:"Café Retro",date:"14 Mar 2026",y:2026,m:2,set:[["Grå morgen",252],["Kitchen light",208],["Slow ferry",301],["Halvvejs",196],["Paper boats",244]]},
|
||
rehearsal:{name:"Rehearsal room, open door",date:"24 Sep 2026",y:2026,m:8,set:[["Slow ferry",344],["Halvvejs",201]]},
|
||
kaelder:{name:"Studenterhuset basement",date:"27 Oct 2025",y:2025,m:9,set:[["Paper boats",231],["Grå morgen",270],["New one (no name)",188],["Slow ferry",322]]},
|
||
have:{name:"Garden party at Ida's",date:"9 Aug 2025",y:2025,m:7,set:[["Kitchen light",190],["Halvvejs",205],["Grå morgen",240]]},
|
||
first:{name:"Open mic, first gig ever",date:"2 Jun 2024",y:2024,m:5,set:[["Halvvejs",170],["Paper boats",215],["Kitchen light",180]]},
|
||
};
|
||
const SONGS={
|
||
"Grå morgen":{who:[["Lucas","the riff, lead guitar"],["Signe","vocals"],["Mads","bass"],["Jonas","drums, brushes"]],lyric:"Grey morning, the coffee knows before I do.",origin:"A warm-up riff Signe wrote words over in one rehearsal.",pic:"photo: Signe under a red bulb"},
|
||
"Kitchen light":{who:[["Lucas","acoustic, verse 2"],["Signe","vocals, Rhodes"],["Mads","bass"],["Jonas","shaker"]],lyric:"Leave the kitchen light on, I'll find my way.",origin:"Written at 2am on a receipt. Slower every year.",pic:"photo: a single kitchen lamp"},
|
||
"Slow ferry":{who:[["Lucas","guitar, delay"],["Signe","keys"],["Mads","long bass notes"],["Jonas","drums"]],lyric:"Nothing to do but stand at the rail.",origin:"First verse written on a ferry ticket. Gets longer every time.",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 Danish song: \"halfway\".",pic:"photo: setlist taped to the floor"},
|
||
"Paper boats":{who:[["Lucas","guitar, harmony"],["Signe","vocals"],["Mads","bass"],["Jonas","count-in, too fast"]],lyric:"Fold it twice and let it go.",origin:"Usually the closer.",pic:"photo: blurry crowd, someone waving"},
|
||
};
|
||
const ITEMS=[
|
||
{y:2026,m:1,d:12,t:"post",title:"A winter of learning to sing harmony",text:"Record yourself, listen once, never again.",more:["Harmonies on Grå morgen and Halvvejs. Both shaky. A tuner that doesn't nag helped."]},
|
||
{y:2026,m:1,d:20,t:"find",title:"A tuner that doesn't nag",text:"No account, no ads. Just a needle.",more:["<a href='#'>link</a>"]},
|
||
{y:2026,m:2,d:14,t:"gig",gig:"retro",title:"Live at Café Retro",text:"Five songs, eleven people and a dog. Handcam from the bar."},
|
||
{y:2026,m:4,d:3,t:"post",title:"Handcam is the honest format",text:"Shaky phone footage, bar chatter left in.",more:["I used to want cleaner recordings. Not anymore."]},
|
||
{y:2026,m:5,d:22,t:"find",title:"Every Danish bus shelter, photographed",text:"Someone did all of them. There's a map.",more:["<a href='#'>link</a>"]},
|
||
{y:2026,m:6,d:10,t:"proj",title:"Loop pedal project",text:"Ten loops, each under eight bars.",more:["Rehearsal-room experiments. Nothing finished, all fun."]},
|
||
{y:2026,m:7,d:15,t:"post",title:"Why I still keep a paper notebook",text:"Half our lyrics started in a meeting margin.",more:["The other half started on the bus."]},
|
||
{y:2026,m:8,d:24,t:"gig",gig:"rehearsal",title:"Rehearsal, door open",text:"Two songs, filmed by whoever walked past."},
|
||
{y:2026,m:9,d:18,t:"post",title:"On playing to eleven people",text:"Small rooms are harder than big ones.",more:["You can see every face, and every face can see your left hand."]},
|
||
{y:2026,m:9,d:29,t:"find",title:"\"The case for small software\"",text:"Read it twice. Applies to bands too.",more:["<a href='#'>link</a>"]},
|
||
{y:2025,m:2,d:4,t:"post",title:"Writing code vs. writing songs",text:"Both are mostly deleting.",more:["One has a test suite."]},
|
||
{y:2025,m:7,d:9,t:"gig",gig:"have",title:"Garden party at Ida's",text:"Acoustic, three songs. Wasps were involved."},
|
||
{y:2025,m:9,d:27,t:"gig",gig:"kaelder",title:"Studenterhuset basement",text:"The loud one. Phone on a beer crate, front left."},
|
||
{y:2025,m:10,d:2,t:"find",title:"Bill Evans · Sunday at the Village Vanguard",text:"You can hear the glasses clink.",more:["The record that made me stop caring about clean recordings."]},
|
||
{y:2024,m:0,d:20,t:"proj",title:"Kitchen demos",text:"One mic, a laptop, fridge hum.",more:["First takes of Kitchen light and Slow ferry."]},
|
||
{y:2024,m:5,d:2,t:"gig",gig:"first",title:"Open mic, first gig ever",text:"Three songs, too fast, very nervous."},
|
||
];
|
||
const ALM={
|
||
2026:["Dark at four. Learning harmonies.","Cold. Practised a lot, posted once.","First gig of the year.","Nothing recorded. New strings.","Light again. Wrote a bit.","Bus shelters, for some reason.","Loops in the rehearsal room.","Notebook season.","Played with the door open.","Small rooms, small software.","Rehearsals Thursdays.",""],
|
||
2025:["Quiet. Work, mostly.","Nothing much.","One essay, about deleting.","Rain. Rehearsals.","Wrote nothing. Played a lot.","A cold that lasted a month.","Summer off.","Garden gig, wasps.","Back to Thursdays.","The loud basement gig.","Found a 1961 record.","Christmas, no music."],
|
||
2024:["Kitchen demos.","","","","Booked our first gig.","First gig ever.","Rested.","","Found a rehearsal room.","","","Decided to keep going."],
|
||
};
|
||
const ROWS={ // small almanac table for each month page
|
||
sun:["08:30","07:40","06:30","06:15","05:10","04:30","04:40","05:30","06:30","07:30","07:40","08:35"],
|
||
set:["15:55","16:55","17:55","20:00","21:00","21:55","21:45","20:50","19:30","18:20","16:20","15:40"],
|
||
};
|
||
const fmt=s=>Math.floor(s/60)+":"+String(Math.floor(s%60)).padStart(2,"0");
|
||
const glyph=(t,m)=>`<span class="mk ${t}" style="position:static;transform:${t==="find"?"rotate(45deg)":"none"};display:inline-block;--c:var(--${season(m||0)})"></span>`;
|
||
|
||
let Y=2026, openM=null;
|
||
const yrs=document.getElementById("years");
|
||
yrs.innerHTML=[2024,2025,2026].map(y=>`<button data-y="${y}" aria-pressed="${y===Y}">${y}</button>`).join("");
|
||
yrs.querySelectorAll("button").forEach(b=>b.onclick=()=>setYear(+b.dataset.y));
|
||
|
||
function setYear(y,m){
|
||
Y=y; document.getElementById("yr").textContent=y;
|
||
yrs.querySelectorAll("button").forEach(b=>b.setAttribute("aria-pressed",+b.dataset.y===y));
|
||
const its=ITEMS.filter(i=>i.y===y), c=t=>its.filter(i=>i.t===t).length;
|
||
const quiet=12-new Set(its.map(i=>i.m)).size - (y===TODAY.y?11-TODAY.m:0);
|
||
document.getElementById("summary").innerHTML=`<span><b>${c("gig")}</b> gig${c("gig")===1?"":"s"}</span><span><b>${c("post")}</b> pieces of writing</span><span><b>${c("find")}</b> finds</span><span><b>${c("proj")}</b> small project${c("proj")===1?"":"s"}</span><span><b>${quiet}</b> quiet months</span>
|
||
<span class="key"><span>${glyph("gig",2)} gig</span><span>${glyph("post")} writing</span><span>${glyph("find")} find</span><span>${glyph("proj")} project</span></span>`;
|
||
const strip=document.getElementById("strip");
|
||
strip.innerHTML=MON.map((mn,m)=>{
|
||
const future = y===TODAY.y && m>TODAY.m;
|
||
const mi=its.filter(i=>i.m===m);
|
||
const marks=mi.map((i,k)=>`<span class="mk ${i.t} ${k%3===1?"off":k%3===2?"off2":""}" style="top:${(i.d/31)*100}%"></span>`).join("");
|
||
const todayLine = (y===TODAY.y && m===TODAY.m) ? `<span class="today" style="left:${TODAY.d/30*100}%"></span>`:"";
|
||
return `<button class="month${future?" future":""}" data-m="${m}" style="--c:var(--${season(m)})" aria-pressed="false" ${future?'aria-disabled="true" tabindex="-1"':""} aria-label="${mn} ${y}, ${mi.length} things">
|
||
<span class="mn">${mn.slice(0,3)}<small>${mi.length?mi.length+" thing"+(mi.length>1?"s":""):"quiet"}</small></span>
|
||
<span class="days">${marks}${todayLine}</span>
|
||
<span class="q">${future?"not yet":(ALM[y][m]||"")}</span></button>`;
|
||
}).join("");
|
||
const peek=document.getElementById("peek");
|
||
strip.querySelectorAll(".month").forEach(b=>{
|
||
const m=+b.dataset.m;
|
||
if(b.classList.contains("future")) return;
|
||
const show=()=>{ const mi=its.filter(i=>i.m===m); peek.innerHTML = `<b style="color:var(--ink)">${MON[m]} ${y}</b> · ` + (mi.length? mi.map(i=>i.title).join(" · ") : `<i>${ALM[y][m]||"a quiet month"}</i>`); };
|
||
b.addEventListener("mouseenter",show); b.addEventListener("focus",show);
|
||
b.addEventListener("click",()=>openMonth(m));
|
||
});
|
||
const first = m!=null ? m : (y===TODAY.y ? ITEMS.filter(i=>i.y===y).slice(-1)[0].m : its[its.length-1].m);
|
||
openMonth(first);
|
||
}
|
||
|
||
let G=null;
|
||
function openMonth(m, gigId, songIdx){
|
||
openM=m;
|
||
document.querySelectorAll(".month").forEach(b=>b.setAttribute("aria-pressed",+b.dataset.m===m));
|
||
const mi=ITEMS.filter(i=>i.y===Y&&i.m===m);
|
||
const page=document.getElementById("page");
|
||
page.style.setProperty("--c",`var(--${season(m)})`);
|
||
page.innerHTML=`<div class="left">
|
||
<h2>${MON[m]}<small>${season(m)} · ${Y}</small></h2>
|
||
<div class="alm"><div><span>sunrise, 1st</span><span>${ROWS.sun[m]}</span></div><div><span>sunset, 1st</span><span>${ROWS.set[m]}</span></div><div><span>things logged</span><span>${mi.length||"none"}</span></div></div>
|
||
<p class="weather">"${ALM[Y][m]||"Nothing to report."}"</p>
|
||
</div>
|
||
<div>${mi.length?`<ul class="items">${mi.map((i,k)=>`<li data-k="${k}"><button class="row" aria-expanded="false"><span class="day">${String(i.d).padStart(2,"0")} ${MON[m].slice(0,3).toLowerCase()}</span><span><h3 class="ph" title="placeholder">${i.title}</h3><p>${i.text}</p></span><span class="kind">${glyph(i.t,m)} ${({gig:"gig",post:"writing",find:"find",proj:"project"})[i.t]}</span></button><div class="x"></div></li>`).join("")}</ul>`
|
||
:`<p class="quiet">Nothing happened this month that made it onto the site. ${ALM[Y][m]?"From the notebook: "+ALM[Y][m].toLowerCase():""} Try ${MON[(m+11)%12]} or ${MON[(m+1)%12]}.</p>`}</div>`;
|
||
page.querySelectorAll(".items > li").forEach(li=>{
|
||
const it=mi[+li.dataset.k], btn=li.querySelector(".row");
|
||
btn.onclick=()=>{
|
||
const on=!li.classList.contains("open");
|
||
page.querySelectorAll(".items > li").forEach(o=>{o.classList.remove("open"); o.querySelector(".row").setAttribute("aria-expanded","false");});
|
||
if(on){ li.classList.add("open"); btn.setAttribute("aria-expanded","true"); fill(li,it); }
|
||
else if(it.t==="gig") stop();
|
||
};
|
||
if(gigId && it.gig===gigId){ li.classList.add("open"); btn.setAttribute("aria-expanded","true"); fill(li,it,songIdx); }
|
||
});
|
||
}
|
||
function fill(li,it,songIdx){
|
||
const x=li.querySelector(".x");
|
||
if(it.t!=="gig"){ stop(); x.innerHTML=(it.more||[]).map(p=>`<p>${p}</p>`).join(""); return; }
|
||
const keep = G && G.id===it.gig && songIdx==null;
|
||
if(!keep){ stop(); G={id:it.gig, g:GIGS[it.gig], pos:0, on:false, timer:null, tab:"who"}; }
|
||
const g=G.g, st=[]; let a=0; g.set.forEach(s=>{st.push(a);a+=s[1];}); G.st=st; G.T=a;
|
||
if(songIdx!=null) G.pos=st[songIdx];
|
||
x.innerHTML=`<div class="gigbox"><div>
|
||
<div class="cam"><button class="big" id="camb" aria-label="Play">▶</button><span>handcam · ${g.name} · ${g.date} (placeholder frame)</span></div>
|
||
<div class="bar" id="gbar">${st.slice(1).map(s=>`<b style="left:${s/a*100}%"></b>`).join("")}<i id="gfill"></i></div>
|
||
<div class="tm"><span id="gt">0:00</span><span>${fmt(a)}</span></div>
|
||
<div class="ctrls"><button id="gprev" aria-label="Previous song">«</button><button class="pp" id="gpp" aria-label="Play">▶</button><button id="gnext" aria-label="Next song">»</button><span style="font:11.5px var(--mono);color:var(--soft);margin-left:6px">the whole set, ${g.set.length} songs</span></div>
|
||
<ol class="set" id="gset">${g.set.map((s,i)=>`<li><button data-i="${i}">${s[0]}<span>${fmt(st[i])}</span></button></li>`).join("")}</ol>
|
||
</div><div class="song" id="gsong"></div></div>`;
|
||
x.querySelector("#gpp").onclick=x.querySelector("#camb").onclick=()=>setOn(!G.on);
|
||
x.querySelector("#gprev").onclick=()=>jump(Math.max(0,cur()-1));
|
||
x.querySelector("#gnext").onclick=()=>jump(Math.min(g.set.length-1,cur()+1));
|
||
x.querySelector("#gbar").onclick=e=>{const r=e.currentTarget.getBoundingClientRect(); G.pos=Math.floor((e.clientX-r.left)/r.width*G.T); upd(true);};
|
||
x.querySelectorAll("#gset button").forEach(b=>b.onclick=()=>jump(+b.dataset.i));
|
||
setOn(G.on); upd(true);
|
||
}
|
||
const cur=()=>{let i=0; G.st.forEach((v,k)=>{if(G.pos>=v)i=k}); return i;};
|
||
function jump(i){ G.pos=G.st[i]; upd(true); }
|
||
function stop(){ if(G){clearInterval(G.timer); G.on=false;} }
|
||
function setOn(on){
|
||
G.on=on; clearInterval(G.timer);
|
||
const label=on?"❚❚":"▶";
|
||
const pp=document.getElementById("gpp"), cb=document.getElementById("camb");
|
||
if(pp){pp.textContent=label; pp.setAttribute("aria-label",on?"Pause":"Play");} if(cb) cb.textContent=label;
|
||
if(on) G.timer=setInterval(()=>{G.pos++; if(G.pos>=G.T){G.pos=G.T; setOn(false);} upd(false);},1000);
|
||
}
|
||
let lastI=-1;
|
||
function upd(force){
|
||
const f=document.getElementById("gfill"); if(!f) return;
|
||
f.style.width=(G.pos/G.T*100)+"%"; document.getElementById("gt").textContent=fmt(G.pos);
|
||
const i=cur();
|
||
document.querySelectorAll("#gset li").forEach((li,k)=>li.classList.toggle("now",k===i));
|
||
if(force||i!==lastI){ lastI=i; song(i); }
|
||
}
|
||
function song(i){
|
||
const name=G.g.set[i][0], s=SONGS[name], box=document.getElementById("gsong");
|
||
const others=Object.entries(GIGS).map(([id,g])=>({id,g,j:g.set.findIndex(x=>x[0]===name)})).filter(o=>o.j>-1);
|
||
let body;
|
||
if(!s) body=`<p style="margin:0;color:var(--soft);font-style:italic">No name, no notes yet. We only played it once.</p>`;
|
||
else if(G.tab==="who") body=`<dl>${s.who.map(w=>`<dt>${w[0]}</dt><dd>${w[1]}</dd>`).join("")}</dl>`;
|
||
else if(G.tab==="story") body=`<p style="margin:0 0 8px;font-style:italic;font-size:19px">"${s.lyric}"</p><p style="margin:0">${s.origin}</p>`;
|
||
else if(G.tab==="pic") body=`<div class="pic">${s.pic} · ${G.g.date}</div>`;
|
||
else body=`<ul class="others">${others.map(o=>`<li><span>${o.g.name} <span style="color:var(--soft);font-size:14px">· ${o.g.date}</span></span>${o.id===G.id?`<span style="font:11.5px var(--mono);color:var(--soft)">this one</span>`:`<button data-g="${o.id}" data-j="${o.j}">go to ${MON[o.g.m].slice(0,3)} ${o.g.y} →</button>`}</li>`).join("")}</ul>`;
|
||
box.innerHTML=`<div style="font:11px var(--mono);color:var(--soft);text-transform:uppercase;letter-spacing:.06em">behind the song</div><h4>${name}</h4>
|
||
<div class="tabs" role="tablist">${[["who","who plays what"],["story","lyric & origin"],["pic","photo"],["other",`on other nights (${others.length})`]].map(([k,l])=>`<button role="tab" data-k="${k}" aria-selected="${G.tab===k}">${l}</button>`).join("")}</div>${body}`;
|
||
box.querySelectorAll(".tabs button").forEach(b=>b.onclick=()=>{G.tab=b.dataset.k; song(cur());});
|
||
box.querySelectorAll(".others button").forEach(b=>b.onclick=()=>{
|
||
const g=GIGS[b.dataset.g], j=+b.dataset.j, wasOn=G.on, tab=G.tab;
|
||
stop(); G=null; if(g.y!==Y) setYear(g.y,g.m);
|
||
openMonth(g.m,b.dataset.g,j); if(G){ G.tab=tab; song(cur()); if(wasOn) setOn(true); }
|
||
});
|
||
}
|
||
setYear(2026);
|
||
</script>
|
||
</body>
|
||
</html>
|