Add Music section: hub, template, build pipeline, and homepage wire

Music/build.py parses *-music.md files and generates collection pages supporting
YouTube video embeds (lazy IntersectionObserver) and FileBrowser audio cards with
floating media player (prev/next/auto-advance). Music star node wired in index.html.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-28 15:23:37 +02:00
parent aec3300e40
commit 1e776bee83
6 changed files with 2367 additions and 1 deletions

705
Music/oldmusicandmvs.html Normal file
View File

@@ -0,0 +1,705 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Old Music and Music Videos - The Archive | MUSIC</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=Rancho&family=Rambla:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<style>
:root {
--bg-void: #04060b;
--text-warm: #e8d5b8;
--mu-red: #8b2020;
--mu-orange: #ff7f3f;
--mu-red-glow: rgba(139, 32, 32, 0.25);
--mu-orange-glow: rgba(255, 127, 63, 0.25);
--transition: 100ms ease;
}
* { box-sizing: border-box; margin: 0; padding: 0; border-radius: 0; }
body {
background-color: var(--bg-void);
color: var(--text-warm);
font-family: 'Rambla', sans-serif;
line-height: 1.6;
padding-top: 64px;
}
body.player-active { padding-bottom: 64px; }
/* --- Sticky header --- */
.sticky-header {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
background: var(--bg-void);
border-bottom: 2px solid var(--mu-red);
padding: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-title {
font-family: 'Rancho', cursive;
font-size: 1.5rem;
color: var(--mu-orange);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 1rem;
}
.header-nav {
display: flex;
align-items: center;
gap: 1.5rem;
}
.nav-link {
font-weight: 700;
text-transform: uppercase;
font-size: 0.85rem;
color: var(--mu-orange);
text-decoration: none;
transition: var(--transition);
}
.nav-link:hover { color: #ff9f6f; }
.count {
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.1em;
color: rgba(232, 213, 184, 0.5);
}
.menu-toggle {
display: none;
background: none;
border: none;
color: var(--mu-orange);
font-size: 1.5rem;
cursor: pointer;
font-family: inherit;
}
@media (max-width: 600px) {
.menu-toggle { display: block; }
.header-nav {
display: none;
position: absolute;
top: 100%; right: 0;
background: var(--bg-void);
border: 2px solid var(--mu-red);
border-top: none;
flex-direction: column;
padding: 1rem;
gap: 1rem;
width: 200px;
}
.header-nav.open { display: flex; }
}
/* --- Lightbox --- */
#lightbox {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(4, 6, 11, 0.9);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 2rem;
cursor: pointer;
}
.lightbox-content {
background: #0c0a08;
border: 2px solid var(--mu-red);
border-top: 8px solid var(--mu-orange);
max-width: 600px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
padding: 2.5rem;
position: relative;
cursor: default;
}
.lightbox-close {
position: absolute;
top: 0.5rem; right: 0.75rem;
font-size: 2rem;
color: var(--mu-orange);
cursor: pointer;
line-height: 1;
}
.lightbox-quote {
font-style: italic;
font-size: 1.1rem;
color: var(--text-warm);
margin-bottom: 1.5rem;
padding-right: 1.5rem;
}
.lightbox-attribution {
font-weight: 700;
text-transform: uppercase;
color: var(--mu-orange);
text-align: right;
}
.lightbox-hint {
display: block;
text-align: center;
margin-top: 2rem;
font-weight: 700;
text-transform: uppercase;
font-size: 0.65rem;
letter-spacing: 0.1em;
color: rgba(232, 213, 184, 0.4);
}
/* --- Audio Card --- */
.audio-card {
padding: 1.5rem;
}
.audio-controls {
display: flex;
align-items: center;
gap: 1.5rem;
}
.play-btn {
background: none;
border: 2px solid var(--mu-orange);
color: var(--mu-orange);
width: 48px;
height: 48px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
cursor: pointer;
transition: var(--transition);
font-family: inherit;
}
.play-btn:hover {
background: var(--mu-orange);
color: var(--bg-void);
}
.audio-info { flex-grow: 1; }
.download-link {
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
color: var(--mu-orange);
text-decoration: none;
display: inline-block;
margin-bottom: 0.5rem;
transition: var(--transition);
}
.download-link:hover { text-decoration: underline; }
.progress-container {
height: 4px;
background: var(--mu-red);
width: 100%;
cursor: pointer;
}
.progress-bar {
height: 100%;
background: var(--mu-orange);
width: 0%;
transition: width 100ms linear;
}
.duration-meta {
font-weight: 700;
text-transform: uppercase;
font-size: 0.7rem;
color: rgba(232, 213, 184, 0.5);
margin-top: 0.25rem;
display: flex;
justify-content: space-between;
}
/* --- Floating Player --- */
.floating-player {
position: fixed;
bottom: 0; left: 0;
width: 100%;
height: 64px;
background: var(--bg-void);
border-top: 2px solid var(--mu-red);
z-index: 500;
display: none;
align-items: center;
padding: 0 1rem;
gap: 1rem;
}
.floating-player.active { display: flex; }
.fp-info { flex: 1; min-width: 0; }
.fp-title {
font-family: 'Rancho', cursive;
font-size: 1.2rem;
color: var(--mu-orange);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fp-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.fp-btn {
background: none;
border: none;
color: var(--mu-orange);
font-family: 'Rambla', sans-serif;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
transition: var(--transition);
}
.fp-btn:hover { color: var(--text-warm); }
.fp-progress-wrap {
position: absolute;
top: -2px; left: 0;
width: 100%;
height: 2px;
background: var(--mu-red);
}
.fp-progress-fill {
height: 100%;
background: var(--mu-orange);
width: 0%;
transition: width 100ms linear;
}
/* --- Cards --- */
@keyframes cardIn {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
main {
max-width: 900px;
margin: 2rem auto;
padding: 0 1rem 4rem;
}
.section-title {
font-family: 'Rancho', cursive;
font-size: 3rem;
color: var(--mu-orange);
margin-bottom: 2rem;
text-align: center;
}
.card {
margin-bottom: 3rem;
border: 2px solid var(--mu-red);
background: rgba(139, 32, 32, 0.05);
transition: border-color var(--transition), box-shadow var(--transition);
animation: cardIn 320ms ease both;
}
.card:nth-child(1) { animation-delay: 0ms; }
.card:nth-child(2) { animation-delay: 50ms; }
.card:nth-child(3) { animation-delay: 100ms; }
.card:nth-child(4) { animation-delay: 150ms; }
.card:nth-child(5) { animation-delay: 200ms; }
.card:nth-child(6) { animation-delay: 250ms; }
.card:nth-child(7) { animation-delay: 300ms; }
.card:nth-child(8) { animation-delay: 350ms; }
.card:nth-child(9) { animation-delay: 400ms; }
.card:nth-child(10) { animation-delay: 450ms; }
.card:nth-child(11) { animation-delay: 500ms; }
.card:nth-child(12) { animation-delay: 550ms; }
.card:hover {
border-color: var(--mu-orange);
box-shadow: 0 0 8px 2px var(--mu-orange-glow);
}
.card-header {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--mu-red);
}
.card-title {
font-family: 'Rancho', cursive;
font-size: 1.6rem;
color: var(--mu-orange);
}
.video-container {
position: relative;
padding-bottom: 56.25%;
height: 0;
overflow: hidden;
background: #000;
}
.video-container iframe {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
border: 0;
}
</style>
</head>
<body>
<div id="lightbox">
<div class="lightbox-content">
<span class="lightbox-close">&times;</span>
<p class="lightbox-quote">I much prefer making music live. But here are some old recordings. Oh and some music videos that I made for better recording artists than me at the time. At time of writing, there are inklings of thinkings about returning to making music. I&#x27;m considering it. We&#x27;ll see how it plays out.</p>
<p class="lightbox-attribution">— JL</p>
<span class="lightbox-hint">CLICK ANYWHERE TO DISMISS</span>
</div>
</div>
<header class="sticky-header">
<div class="header-title">OLD MUSIC AND MUSIC VIDEOS - THE ARCHIVE</div>
<button class="menu-toggle"></button>
<nav class="header-nav">
<a href="music-hub.html" class="nav-link">← MUSIC</a>
<span class="count">14 TRACKS</span>
</nav>
</header>
<main>
<h1 class="section-title">OLD MUSIC AND MUSIC VIDEOS - THE ARCHIVE</h1>
<div class="card">
<div class="card-header"><h2 class="card-title">Sixteen by Melanie Kerr</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/zmwv8NXMO6E"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Good Girl by Melanie Kerr</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/M4KCZr7Xx7A"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">042 Letter to My Sunrise \(Demo Tape\)</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/lbhFd6I_CII"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">038 Trouble Done Bore Me Down</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/fpm0nfMjm7M"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">036 That&#x27;s All Right Mama</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/N_8WL1GhW0s"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">035 Psycho Bch</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/fsE03yW7M9Y"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">034 On the Curious Creatures Called People</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/JJtnygYcs5o"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">033 Ode to Truth Kind of</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/QVQl2uyUBGg"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">032 iYeza</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/1rN4C828vR4"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">031 Dammit Blink 182 Cover</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/YTZqMbIuGKk"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">030 An Irish Airman Forsees his Death</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/wOe-KTlWX1Y"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Trouble Done Bore Me Down</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/BWJrOOYqRuM"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card">
<div class="card-header"><h2 class="card-title">Shortstraw</h2></div>
<div class="video-container">
<iframe
data-src="https://www.youtube.com/embed/pIGIW3h9LpU"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<div class="card" id="track-1">
<div class="card-header">
<h2 class="card-title">Dammit - Dwonlaod \ - Free On File Browser</h2>
</div>
<div class="audio-card">
<div class="audio-controls">
<button class="play-btn"
data-src="https://files.exopraxist.org/api/public/dl/KeQTyi9r"
data-title="Dammit - Dwonlaod \ - Free On File Browser"></button>
<div class="audio-info">
<a href="https://files.exopraxist.org/share/KeQTyi9r" target="_blank" rel="noopener"
class="download-link">⬇ DOWNLOAD</a>
<div class="progress-container">
<div class="progress-bar"></div>
</div>
<div class="duration-meta">
<span class="current-time">0:00</span>
<span class="total-time">--:--</span>
</div>
</div>
</div>
</div>
</div>
</main>
<div class="floating-player">
<div class="fp-progress-wrap"><div class="fp-progress-fill"></div></div>
<div class="fp-info"><div class="fp-title">No Track Selected</div></div>
<div class="fp-controls">
<button class="fp-btn" id="fp-prev">◀◀</button>
<button class="fp-btn" id="fp-play"></button>
<button class="fp-btn" id="fp-next">▶▶</button>
</div>
</div>
<audio id="main-audio"></audio>
<script>
document.addEventListener('DOMContentLoaded', () => {
const lightbox = document.getElementById('lightbox');
if (!localStorage.getItem('music-oldmusicandmvs-seen')) {
lightbox.style.display = 'flex';
localStorage.setItem('music-oldmusicandmvs-seen', '1');
}
const closeLightbox = () => { lightbox.style.display = 'none'; };
lightbox.addEventListener('click', (e) => {
if (e.target === lightbox) closeLightbox();
});
document.querySelector('.lightbox-close').addEventListener('click', closeLightbox);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeLightbox();
});
// --- Mobile nav ---
const menuToggle = document.querySelector('.menu-toggle');
const nav = document.querySelector('.header-nav');
menuToggle.addEventListener('click', (e) => {
e.stopPropagation();
nav.classList.toggle('open');
});
document.addEventListener('click', (e) => {
if (!nav.contains(e.target) && e.target !== menuToggle)
nav.classList.remove('open');
});
// --- Lazy-load iframes ---
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const iframe = entry.target;
if (iframe.dataset.src) {
iframe.src = iframe.dataset.src;
iframe.removeAttribute('data-src');
obs.unobserve(iframe);
}
}
});
}, { rootMargin: '200px' });
document.querySelectorAll('iframe[data-src]').forEach(f => observer.observe(f));
// --- Audio Logic ---
const audio = document.getElementById('main-audio');
const floatingPlayer = document.querySelector('.floating-player');
const fpPlayBtn = document.getElementById('fp-play');
const fpTitle = document.querySelector('.fp-title');
const fpFill = document.querySelector('.fp-progress-fill');
let currentTrackBtn = null;
const formatTime = (t) => {
const m = Math.floor(t / 60);
const s = Math.floor(t % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
};
const updateUI = () => {
const playing = !audio.paused;
const icon = playing ? '▐▐' : '▶';
if (currentTrackBtn) currentTrackBtn.textContent = icon;
fpPlayBtn.textContent = icon;
if (playing) {
floatingPlayer.classList.add('active');
document.body.classList.add('player-active');
}
};
document.querySelectorAll('.play-btn').forEach(btn => {
btn.addEventListener('click', () => {
const src = btn.dataset.src;
const title = btn.dataset.title;
if (audio.getAttribute('data-loaded') === src) {
if (audio.paused) audio.play(); else audio.pause();
} else {
if (currentTrackBtn) currentTrackBtn.textContent = '▶';
audio.src = src;
audio.setAttribute('data-loaded', src);
fpTitle.textContent = title;
currentTrackBtn = btn;
audio.play();
}
});
});
fpPlayBtn.addEventListener('click', () => {
if (audio.paused) audio.play(); else audio.pause();
});
audio.addEventListener('play', updateUI);
audio.addEventListener('pause', updateUI);
audio.addEventListener('timeupdate', () => {
if (!audio.duration) return;
const pct = (audio.currentTime / audio.duration) * 100;
if (fpFill) fpFill.style.width = pct + '%';
if (currentTrackBtn) {
const card = currentTrackBtn.closest('.audio-card');
const bar = card.querySelector('.progress-bar');
const cur = card.querySelector('.current-time');
if (bar) bar.style.width = pct + '%';
if (cur) cur.textContent = formatTime(audio.currentTime);
}
});
audio.addEventListener('loadedmetadata', () => {
if (currentTrackBtn) {
const card = currentTrackBtn.closest('.audio-card');
const tot = card.querySelector('.total-time');
if (tot) tot.textContent = formatTime(audio.duration);
}
});
audio.addEventListener('ended', () => {
const allBtns = [...document.querySelectorAll('.play-btn')];
const idx = allBtns.indexOf(currentTrackBtn);
const next = allBtns[idx + 1];
if (next) {
next.click();
} else {
audio.pause();
if (currentTrackBtn) currentTrackBtn.textContent = '▶';
fpPlayBtn.textContent = '▶';
floatingPlayer.classList.remove('active');
document.body.classList.remove('player-active');
}
});
document.getElementById('fp-prev').addEventListener('click', () => {
const all = [...document.querySelectorAll('.play-btn')];
const prev = all[all.indexOf(currentTrackBtn) - 1];
if (prev) prev.click();
});
document.getElementById('fp-next').addEventListener('click', () => {
const all = [...document.querySelectorAll('.play-btn')];
const next = all[all.indexOf(currentTrackBtn) + 1];
if (next) next.click();
});
document.querySelectorAll('.progress-container').forEach(bar => {
bar.addEventListener('click', (e) => {
const rect = bar.getBoundingClientRect();
audio.currentTime = ((e.clientX - rect.left) / rect.width) * audio.duration;
});
});
});
</script>
</body>
</html>