Add Sarah's Monkee Madness static page

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-06-15 14:16:56 -04:00
co-authored by Claude Opus 4.8
parent 606293585c
commit dcf8488ec2
+644
View File
@@ -0,0 +1,644 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Sarah's Monkee Madness</title>
<meta name="referrer" content="strict-origin-when-cross-origin" />
<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=Shrikhand&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" />
<style>
/* ---- tokens: 1967 psychedelic pop ----------------------------------- */
:root{
--night:#1a0f33; /* deep psych indigo base */
--night-2:#271a47; /* raised surfaces */
--cream:#f7ecd2; /* poster cream */
--cream-dim:#c8b48f; /* muted cream */
--pink:#ff2d6b; /* hot pop pink (primary) */
--tang:#ff7a1a; /* tangerine */
--turq:#1ec8b4; /* turquoise */
--lemon:#ffd23f; /* lemon (focus / highlight) */
--line:#46355f; /* hairline on night */
--jam:#ff5a3c;
--disp:'Shrikhand',serif;
--mono:'Space Mono',ui-monospace,monospace;
}
*{box-sizing:border-box}
html{height:100%}
body{margin:0;min-height:100%;background:var(--night);color:var(--cream);
font-family:var(--mono);-webkit-font-smoothing:antialiased;overflow-x:hidden}
button{font-family:inherit;cursor:pointer;color:inherit}
:focus-visible{outline:3px solid var(--lemon);outline-offset:2px}
.screen{display:none}
.screen.on{display:flex;flex-direction:column}
#card.on{min-height:100vh;min-height:100dvh}
#play.on{position:fixed;inset:0;z-index:40}
/* ===== TITLE CARD ==================================================== */
#card{overflow-y:visible;position:relative}
/* op-art ray burst + color wash behind everything */
#card::before{
content:"";position:fixed;inset:-20%;z-index:0;
background:
conic-gradient(from 0deg at 50% 42%,
rgba(255,45,107,.16) 0 12deg, transparent 12deg 24deg,
rgba(30,200,180,.13) 24deg 36deg, transparent 36deg 48deg,
rgba(255,122,26,.13) 48deg 60deg, transparent 60deg 72deg);
animation:spin 90s linear infinite;
transform-origin:50% 42%;
pointer-events:none;
}
#card::after{
content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
background:
radial-gradient(60% 45% at 50% 40%, rgba(255,210,63,.10), transparent 70%),
radial-gradient(90% 70% at 100% 0, rgba(255,45,107,.16), transparent 55%),
radial-gradient(90% 70% at 0 100%, rgba(30,200,180,.14), transparent 55%);
}
@keyframes spin{to{transform:rotate(360deg)}}
.card-wrap{position:relative;z-index:1;max-width:760px;margin:0 auto;
padding:44px 24px 72px;width:100%;text-align:center}
/* guitar-pick emblem (original, not the band's mark) */
.emblem{width:118px;height:132px;margin:0 auto 6px;display:block}
.hero-img{width:132px;height:132px;border-radius:50%;object-fit:cover;margin:0 auto 6px;
display:none;border:4px solid var(--cream);box-shadow:0 0 0 6px var(--pink)}
.eyebrow{font-size:12px;letter-spacing:.34em;text-transform:uppercase;
color:var(--lemon);font-weight:700;margin:14px 0 18px}
h1{margin:0;font-family:var(--disp);font-weight:400;line-height:.92;
text-transform:uppercase;letter-spacing:.01em}
h1 .l1{display:block;font-size:clamp(30px,8vw,60px);color:var(--turq);
transform:rotate(-3deg)}
h1 .l2{display:block;font-size:clamp(52px,15vw,128px);color:var(--pink);
text-shadow:4px 4px 0 var(--night), 8px 8px 0 var(--tang);margin:.02em 0}
h1 .l3{display:block;font-size:clamp(30px,8vw,60px);color:var(--lemon);
transform:rotate(2deg)}
.tagline{margin:30px auto 0;max-width:50ch;font-size:15px;line-height:1.55;
color:var(--cream);text-align:center}
.tagline b{color:var(--lemon);font-weight:400}
.program{margin:40px 0 0;text-align:left}
.program-h{display:flex;align-items:baseline;justify-content:center;gap:10px;margin:0 0 16px;
font-size:11px;letter-spacing:.26em;text-transform:uppercase;color:var(--cream-dim)}
.program-h .shuffle{color:var(--turq);letter-spacing:0;font-size:14px}
.bill{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
.slot{display:flex;align-items:center;gap:14px;background:var(--night-2);
border:2px solid var(--line);border-radius:40px;padding:12px 20px}
.slot.feature{border-color:var(--pink);background:rgba(255,45,107,.10)}
.slot .dot{width:14px;height:14px;border-radius:50%;flex:0 0 auto}
.slot.feature .dot{background:var(--pink)}
.slot:nth-child(2) .dot{background:var(--turq)}
.slot:nth-child(3) .dot{background:var(--tang)}
.slot:nth-child(4) .dot{background:var(--lemon)}
.slot .t{flex:1;min-width:0;font-family:var(--disp);font-size:19px;color:var(--cream);
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:.01em}
.slot .red{font-family:var(--mono);font-size:15px;color:var(--cream-dim);letter-spacing:.1em}
.slot .badge{font-size:10px;letter-spacing:.16em;text-transform:uppercase;font-weight:700;
color:var(--pink);white-space:nowrap}
.slot .badge.dim{color:var(--cream-dim)}
.dedication{margin:30px 0 0;font-size:13px;color:var(--cream-dim);font-style:italic}
.go{margin-top:38px;display:flex;align-items:center;justify-content:center;gap:20px;flex-wrap:wrap}
#roll{font-family:var(--disp);font-weight:400;font-size:clamp(26px,6vw,38px);
text-transform:uppercase;letter-spacing:.02em;background:var(--pink);color:var(--cream);
border:none;border-radius:50px;padding:18px 48px;line-height:1;
box-shadow:5px 5px 0 var(--night),9px 9px 0 var(--turq)}
#roll:hover{background:var(--tang);box-shadow:5px 5px 0 var(--night),9px 9px 0 var(--lemon)}
#roll:active{transform:translate(4px,4px);box-shadow:1px 1px 0 var(--night),3px 3px 0 var(--turq)}
.go-note{font-size:12px;color:var(--cream-dim);max-width:24ch;line-height:1.45;text-align:left}
/* ===== PROJECTION =================================================== */
#play{background:#000}
#stage{position:relative;flex:1;background:#000;overflow:hidden}
#player,#player iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
#player::after{content:"";position:absolute;inset:0}
.topbar{position:absolute;top:0;left:0;right:0;z-index:5;display:flex;align-items:center;
gap:8px;padding:14px;background:linear-gradient(var(--night),rgba(26,15,51,0));pointer-events:none}
.topbar > *{pointer-events:auto}
.icon{background:rgba(26,15,51,.74);border:2px solid var(--line);border-radius:50%;
width:46px;height:46px;font-size:18px;line-height:1;display:grid;place-items:center;
backdrop-filter:blur(4px)}
.icon:hover{border-color:var(--pink)}
.icon[aria-pressed="true"]{background:var(--pink);color:var(--cream);border-color:var(--pink)}
.exit{background:rgba(26,15,51,.74);border:2px solid var(--line);border-radius:30px;height:46px;
padding:0 18px;font-family:var(--disp);font-size:14px;display:inline-flex;align-items:center;
gap:8px;backdrop-filter:blur(4px)}
.exit:hover{border-color:var(--pink)}
.spacer{flex:1}
#slate{position:absolute;left:14px;top:72px;z-index:6;max-width:min(420px,80vw);
background:var(--cream);color:var(--night);border-radius:14px;
border:3px solid var(--pink);padding:14px 18px;display:none;
box-shadow:5px 5px 0 rgba(0,0,0,.35)}
#slate.on{display:block}
#slate .lbl{font-size:10px;letter-spacing:.24em;text-transform:uppercase;color:var(--pink);font-weight:700}
#slate .film{font-family:var(--disp);font-size:23px;line-height:1.05;margin:6px 0 8px}
#slate .tc{font-size:13px;color:#6b5a44}
#slate .tc b{color:var(--night)}
.filmbar{position:absolute;left:0;right:0;bottom:0;z-index:5;
background:linear-gradient(rgba(26,15,51,0),var(--night) 65%);padding:30px 16px 14px}
.counter{display:flex;justify-content:space-between;align-items:flex-end;
font-size:12px;color:var(--cream-dim);margin-bottom:8px}
.counter .clip{font-family:var(--disp);font-size:15px;color:var(--lemon);letter-spacing:.02em}
.counter .tc{font-variant-numeric:tabular-nums}
.strip{position:relative;height:18px;background:var(--night-2);border:2px solid var(--line);
border-radius:10px;overflow:hidden}
.strip .fill{position:absolute;inset:0;width:0;
background:linear-gradient(90deg,var(--pink),var(--tang),var(--lemon));transition:width .25s linear}
.strip .perfs{position:absolute;inset:0;
background:repeating-linear-gradient(90deg,transparent 0 20px,rgba(26,15,51,.7) 20px 26px);
pointer-events:none}
#leader{position:absolute;inset:0;z-index:7;display:none;place-items:center;
background:radial-gradient(circle at 50% 50%, var(--night-2), var(--night))}
#leader.on{display:grid}
#leader .ring{width:min(46vmin,260px);aspect-ratio:1;border-radius:50%;
border:4px solid var(--cream);position:relative;display:grid;place-items:center;overflow:hidden}
#leader .ring::before,#leader .ring::after{content:"";position:absolute;background:var(--cream)}
#leader .ring::before{left:50%;top:0;bottom:0;width:2px;transform:translateX(-50%)}
#leader .ring::after{top:50%;left:0;right:0;height:2px;transform:translateY(-50%)}
#leader .sweep{position:absolute;inset:0;
background:conic-gradient(var(--pink) 0,var(--pink) var(--sweep,0deg),transparent var(--sweep,0deg));
opacity:.6}
#leader .n{font-family:var(--disp);font-size:clamp(70px,22vmin,150px);z-index:1;color:var(--lemon)}
#fin{position:absolute;inset:0;z-index:8;display:none;place-items:center;
background:radial-gradient(circle at 50% 35%, var(--night-2), var(--night));text-align:center;padding:24px}
#fin.on{display:grid}
#fin .inner{max-width:440px}
#fin h2{font-family:var(--disp);font-size:clamp(36px,9vw,62px);text-transform:uppercase;
margin:0 0 12px;line-height:.9;color:var(--pink);
text-shadow:3px 3px 0 var(--night),6px 6px 0 var(--turq)}
#fin p{color:var(--cream-dim);font-size:14px;line-height:1.55;margin:0 0 24px}
#fin .acts{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
#fin button{font-family:var(--disp);font-size:16px;padding:14px 24px;border:none;border-radius:40px}
#fin .again{background:var(--pink);color:var(--cream);box-shadow:4px 4px 0 var(--night)}
#fin .again:hover{background:var(--tang)}
#fin .back{background:var(--night-2);color:var(--cream);border:2px solid var(--line)}
#fin .back:hover{border-color:var(--pink)}
.toast{position:absolute;left:50%;bottom:96px;transform:translateX(-50%) translateY(10px);
z-index:9;background:var(--jam);color:var(--night);font-weight:700;font-size:13px;padding:9px 16px;
border-radius:20px;opacity:0;transition:opacity .3s,transform .3s;pointer-events:none;white-space:nowrap}
.toast.on{opacity:1;transform:translateX(-50%) translateY(0)}
.keys{position:absolute;right:14px;bottom:14px;z-index:5;font-size:11px;color:var(--cream-dim);
text-align:right;line-height:1.7;pointer-events:none}
.keys kbd{background:var(--night-2);border:1px solid var(--line);border-radius:4px;padding:1px 6px;
font-family:var(--mono);color:var(--cream)}
@media (max-width:560px){
.keys{display:none}
.go{flex-direction:column;align-items:stretch}
#roll{width:100%}
.go-note{text-align:center;max-width:none}
}
@media (prefers-reduced-motion:reduce){
#card::before{animation:none}
.strip .fill{transition:none}
#leader .sweep{display:none}
}
</style>
</head>
<body>
<!-- ===================== TITLE CARD ===================== -->
<section id="card" class="screen on" aria-label="Title card">
<div class="card-wrap">
<!-- original guitar-pick emblem; swap for HERO_IMAGE if set -->
<svg class="emblem" id="emblem" viewBox="0 0 118 132" aria-hidden="true">
<path d="M59 4 C92 4 114 26 114 56 C114 88 84 128 59 128 C34 128 4 88 4 56 C4 26 26 4 59 4 Z"
fill="#ff2d6b" stroke="#f7ecd2" stroke-width="5"/>
<g stroke="#f7ecd2" stroke-width="4" fill="none" opacity=".9">
<circle cx="59" cy="60" r="30"/>
<circle cx="59" cy="60" r="18"/>
</g>
<circle cx="59" cy="60" r="7" fill="#ffd23f"/>
<g stroke="#1ec8b4" stroke-width="5" stroke-linecap="round">
<line x1="59" y1="60" x2="59" y2="18"/>
<line x1="59" y1="60" x2="92" y2="84"/>
<line x1="59" y1="60" x2="26" y2="84"/>
</g>
</svg>
<img class="hero-img" id="hero" alt="" />
<p class="eyebrow">A cut-up midnight movie, party of one</p>
<h1>
<span class="l1">Sarah's</span>
<span class="l2">Monkee</span>
<span class="l3">Madness</span>
</h1>
<p class="tagline">
One Monkees movie and three accomplices, chopped into clips of
<b>random length</b> and dealt out in <b>random order</b>. Head turns up about
three times as often as the rest, since it is, after all, the main feature.
You won't know what you're watching until you ask.
</p>
<div class="program">
<p class="program-h"><span>Tonight's program</span><span class="shuffle">&#8646; in no particular order</span></p>
<ul class="bill">
<li class="slot feature">
<span class="dot"></span>
<span class="t">Head (1968)</span>
<span class="badge">main feature &times;3</span>
</li>
<li class="slot">
<span class="dot"></span>
<span class="t"><span class="red">&block;&block;&block;&block;&block;&block;&block;&block;</span></span>
<span class="badge dim">withheld</span>
</li>
<li class="slot">
<span class="dot"></span>
<span class="t"><span class="red">&block;&block;&block;&block;&block;&block;</span></span>
<span class="badge dim">withheld</span>
</li>
<li class="slot">
<span class="dot"></span>
<span class="t"><span class="red">&block;&block;&block;&block;&block;&block;&block;&block;&block;</span></span>
<span class="badge dim">withheld</span>
</li>
</ul>
</div>
<p class="dedication">For Sarah, who pitched this as a joke and accidentally wrote the spec.</p>
<div class="go">
<button id="roll" type="button">Roll it</button>
<p class="go-note">Sound starts on this click, so the browser lets it play.</p>
</div>
</div>
</section>
<!-- ===================== PROJECTION ===================== -->
<section id="play" class="screen" aria-label="Projection">
<div id="stage">
<div id="player"></div>
<div class="topbar">
<button id="exit" class="exit" type="button">&#9664; Title</button>
<span class="spacer"></span>
<button id="reveal" class="icon" type="button" aria-pressed="false" aria-label="Reveal what is playing">?</button>
<button id="skip" class="icon" type="button" aria-label="Skip to next clip">&#9197;</button>
<button id="mute" class="icon" type="button" aria-pressed="false" aria-label="Mute or unmute">&#128266;</button>
</div>
<div id="slate" role="status" aria-live="polite">
<div class="lbl">Now splicing</div>
<div class="film" id="slate-film">&mdash;</div>
<div class="tc">clip in at <b id="slate-tc">0:00</b> &middot; reel <span id="slate-idx">&mdash;</span></div>
</div>
<div id="leader" aria-hidden="true">
<div class="ring"><div class="sweep"></div><div class="n" id="leader-n">3</div></div>
</div>
<div class="filmbar">
<div class="counter">
<span class="clip" id="clip-label">splice 001</span>
<span class="tc"><span id="elapsed">0:00</span> / <span id="cliplen">1:30</span></span>
</div>
<div class="strip"><div class="fill" id="fill"></div><div class="perfs"></div></div>
</div>
<div class="keys" aria-hidden="true">
<div><kbd>N</kbd> next splice</div>
<div><kbd>?</kbd> reveal &middot; <kbd>M</kbd> mute</div>
</div>
<div id="fin">
<div class="inner">
<h2>That's the program</h2>
<p>Every clip from all four reels has now run. The whole thing, technically, has
been watched. Just not in any order a person could reconstruct.</p>
<div class="acts">
<button class="again" id="again" type="button">Reshuffle &amp; run it again</button>
<button class="back" id="fin-back" type="button">Back to title</button>
</div>
</div>
</div>
<div id="toast" class="toast" role="status" aria-live="assertive"></div>
</div>
</section>
<script>
/* =====================================================================
Sarah's Monkee Madness
Four-reel cut-up player. Each clip is a random length between CLIP_MIN
and CLIP_MAX seconds, pulled from a randomly chosen reel that still has
runtime. Reels are weighted, so Head (the main feature) comes up about
three times as often as its three companions. Playback runs entirely
through the YouTube IFrame API, one clip at a time. Nothing downloaded.
===================================================================== */
const $ = (id) => document.getElementById(id);
/* ---------------------------------------------------------------------
OPTIONAL: put any image URL here (your own choice of art / photo) and
it replaces the guitar-pick emblem on the title card. Leave it blank
to keep the original emblem.
--------------------------------------------------------------------- */
const HERO_IMAGE = ''; // e.g. 'https://your-host/whatever.jpg'
const CLIP_MIN = 60; // shortest splice, seconds
const CLIP_MAX = 120; // longest splice, seconds
const REEL_DEFS = [
{ id:'OfYob-y2m7o', title:'Head (1968)', weight:3, main:true },
{ id:'O4o584brzuc', title:null, weight:1, main:false },
{ id:'gu8blmuBFYY', title:null, weight:1, main:false },
{ id:'bQD1lM0WEPM', title:null, weight:1, main:false },
];
const state = {
reels: [], clipLen: 90, player: null, active: null, pendingFirst: null,
clipIndex: 0, tickTimer: null, windowStart: 0,
muted: false, revealed: false, running: false,
};
function freshReels(){
return REEL_DEFS.map(d => ({
id:d.id, title:d.title, weight:d.weight, main:d.main,
duration:null, done:false, jammed:false,
queue:[], qi:0, built:false, probed:false,
}));
}
/* ---- helpers -------------------------------------------------------- */
function fmt(sec){
sec = Math.max(0, Math.floor(sec||0));
const m = Math.floor(sec/60), s = sec%60;
return m + ':' + String(s).padStart(2,'0');
}
function randInt(lo,hi){ return Math.floor(Math.random()*(hi-lo+1))+lo; }
function shuffle(a){
for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; }
return a;
}
/* Once a reel's duration is known, slice the whole film into back-to-back
chunks of random length, then shuffle them. Each chunk plays exactly once,
but in scrambled order. */
function buildQueue(reel){
const d = reel.duration, segs = [];
let t = 0;
while(t < d - 1){
let len = randInt(CLIP_MIN, CLIP_MAX);
if(t + len > d) len = Math.max(20, Math.round(d - t)); // final partial chunk
segs.push({ start: Math.round(t), len });
t += len;
}
if(reel.probed && segs.length) segs.shift(); // opening already shown as the probe
shuffle(segs);
reel.queue = segs; reel.qi = 0; reel.built = true;
}
/* Get the next chunk to play for a reel. Before duration is known we play a
short "probe" from the top so the player can report the runtime. */
function nextClipFor(reel){
if(reel.built){
if(reel.qi >= reel.queue.length){ reel.done = true; return null; }
const seg = reel.queue[reel.qi++];
if(reel.qi >= reel.queue.length) reel.done = true;
return seg;
}
if(reel.probed){ reel.done = true; return null; } // duration never resolved; retire it
reel.probed = true;
return { start: 0, len: randInt(CLIP_MIN, CLIP_MAX) };
}
function label(reel){
if(reel.title) return reel.title;
return reel.main ? 'Head (1968)' : 'Undisclosed reel';
}
/* ---- optional hero image ------------------------------------------- */
if(HERO_IMAGE){
const img = $('hero'); const em = $('emblem');
img.src = HERO_IMAGE; img.style.display = 'block'; em.style.display = 'none';
}
/* ---- YouTube IFrame API -------------------------------------------- */
window.onYouTubeIframeAPIReady = function(){ state.apiReady = true; };
(function loadAPI(){
const t = document.createElement('script');
t.src = 'https://www.youtube.com/iframe_api';
document.head.appendChild(t);
})();
/* ---- start ---------------------------------------------------------- */
$('roll').addEventListener('click', startProgram);
$('again').addEventListener('click', () => { $('fin').classList.remove('on'); startProgram(); });
function startProgram(){
state.reels = freshReels();
state.clipIndex = 0;
state.running = true;
show('play');
$('fin').classList.remove('on');
const first = pickReel();
if(!first){ endProgram(); return; }
const seg = nextClipFor(first);
if(!seg){ endProgram(); return; }
if(state.player){
loadAndRun(first, seg);
} else {
state.pendingFirst = { reel: first, seg };
const httpOrigin = /^https?:$/.test(location.protocol) ? location.origin : undefined;
const vars = { autoplay:1, controls:0, rel:0, modestbranding:1,
playsinline:1, fs:0, disablekb:1, start:0 };
if(httpOrigin) vars.origin = httpOrigin;
state.player = new YT.Player('player', {
host: 'https://www.youtube-nocookie.com',
videoId: first.id,
playerVars: vars,
events: {
onReady: () => { const p = state.pendingFirst; runWindow(p.reel, p.seg); applyMute(); },
onStateChange: onState,
onError: onError,
}
});
}
}
/* weighted random pick among reels that still have runtime */
function pickReel(){
const pool = state.reels.filter(r => !r.done && !r.jammed);
if(!pool.length) return null;
const total = pool.reduce((s,r) => s + r.weight, 0);
let x = Math.random() * total;
for(const r of pool){ x -= r.weight; if(x <= 0) return r; }
return pool[pool.length-1];
}
function loadAndRun(reel, seg){
try{
state.player.loadVideoById({ videoId: reel.id, startSeconds: seg.start });
applyMute();
}catch(e){}
runWindow(reel, seg);
}
function runWindow(reel, seg){
state.active = reel;
state.clipLen = seg.len;
state.clipIndex += 1;
state.windowStart = Date.now();
$('clip-label').textContent = 'splice ' + String(state.clipIndex).padStart(3,'0');
$('cliplen').textContent = fmt(seg.len);
$('elapsed').textContent = '0:00';
$('fill').style.width = '0%';
$('slate-film').textContent = label(reel);
$('slate-tc').textContent = fmt(seg.start);
$('slate-idx').textContent = (state.reels.indexOf(reel)+1) + ' / ' + state.reels.length;
clearInterval(state.tickTimer);
state.tickTimer = setInterval(tick, 250);
setTimeout(captureDuration, 800);
}
function captureDuration(){
if(!state.player || !state.active) return;
let d = 0;
try{ d = state.player.getDuration(); }catch(e){}
if(d && d > 0){
state.active.duration = d;
if(!state.active.built) buildQueue(state.active); // shuffle this film's chunks
maybeTitle();
}
}
function maybeTitle(){
const reel = state.active;
if(reel.title) return;
try{
const data = state.player.getVideoData && state.player.getVideoData();
if(data && data.title){
reel.title = data.title;
if(state.revealed) $('slate-film').textContent = reel.title;
}
}catch(e){}
}
function tick(){
if(!state.running) return;
const elapsed = (Date.now() - state.windowStart) / 1000;
$('fill').style.width = Math.min(100, (elapsed/state.clipLen)*100) + '%';
$('elapsed').textContent = fmt(elapsed);
if(state.active && state.active.duration == null) captureDuration();
if(elapsed >= state.clipLen) advance();
}
function advance(){
clearInterval(state.tickTimer);
let reel, seg;
while((reel = pickReel())){
seg = nextClipFor(reel);
if(seg) break; // got a chunk; otherwise that reel just retired, try another
}
if(!reel || !seg){ endProgram(); return; }
leaderThen(() => loadAndRun(reel, seg));
}
function skip(){ if(state.running) advance(); }
function leaderThen(cb){
if(window.matchMedia('(prefers-reduced-motion:reduce)').matches){ cb(); return; }
const leader = $('leader'), nEl = $('leader-n');
let n = 3, deg = 0;
leader.style.setProperty('--sweep','0deg');
nEl.textContent = n;
leader.classList.add('on');
const sweep = setInterval(() => { deg=(deg+36)%360; leader.style.setProperty('--sweep', deg+'deg'); }, 33);
const step = setInterval(() => {
n -= 1;
if(n <= 0){ clearInterval(step); clearInterval(sweep); leader.classList.remove('on'); cb(); }
else nEl.textContent = n;
}, 360);
}
/* ---- player events -------------------------------------------------- */
function onState(e){
if(e.data === YT.PlayerState.ENDED){
advance(); // a chunk near the end finished early; the reel keeps its other chunks
} else if(e.data === YT.PlayerState.PLAYING){
captureDuration();
}
}
function onError(){
if(state.active){ state.active.jammed = true; toast('Reel jammed. Skipping it.'); }
advance();
}
/* ---- end ------------------------------------------------------------ */
function endProgram(){
state.running = false;
clearInterval(state.tickTimer);
try{ state.player && state.player.stopVideo(); }catch(e){}
$('fin').classList.add('on');
}
/* ---- chrome --------------------------------------------------------- */
$('skip').addEventListener('click', skip);
$('exit').addEventListener('click', toTitle);
$('fin-back').addEventListener('click', toTitle);
$('mute').addEventListener('click', toggleMute);
$('reveal').addEventListener('click', toggleReveal);
function toTitle(){
state.running = false;
clearInterval(state.tickTimer);
try{ state.player && state.player.stopVideo(); }catch(e){}
$('slate').classList.remove('on');
state.revealed = false; $('reveal').setAttribute('aria-pressed','false');
show('card');
}
function applyMute(){ try{ state.muted ? state.player.mute() : state.player.unMute(); }catch(e){} }
function toggleMute(){
state.muted = !state.muted; applyMute();
const b = $('mute');
b.textContent = state.muted ? '\uD83D\uDD07' : '\uD83D\uDD0A';
b.setAttribute('aria-pressed', String(state.muted));
}
function toggleReveal(){
state.revealed = !state.revealed;
if(state.revealed && state.active) $('slate-film').textContent = label(state.active);
$('slate').classList.toggle('on', state.revealed);
$('reveal').setAttribute('aria-pressed', String(state.revealed));
}
let toastTimer = null;
function toast(msg){
const t = $('toast');
t.textContent = msg; t.classList.add('on');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('on'), 1800);
}
/* ---- keyboard ------------------------------------------------------- */
document.addEventListener('keydown', e => {
if(!$('play').classList.contains('on')) return;
const k = e.key.toLowerCase();
if(k === 'n'){ e.preventDefault(); skip(); }
else if(k === 'm'){ toggleMute(); }
else if(k === '?' || k === '/'){ toggleReveal(); }
});
/* ---- screen switch -------------------------------------------------- */
function show(id){
document.querySelectorAll('.screen').forEach(s => s.classList.remove('on'));
$(id).classList.add('on');
}
</script>
</body>
</html>