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

View File

@@ -45,3 +45,6 @@
- **Shortstraw \- Cold Shoulder \[Music Video 720p\] \- YouTube**
- URL: https://www.youtube.com/watch?v=pIGIW3h9LpU
- **Dammit - Dwonlaod \ - Free On File Browser**
- URL: https://files.exopraxist.org/share/KeQTyi9r

769
Music/build.py Normal file
View File

@@ -0,0 +1,769 @@
#!/usr/bin/env python3
"""
Music build script.
For each *-music.md in this folder:
- Parse title, optional first-load lightbox, and track entries
- Two entry types detected by URL:
YouTube → youtube.com/watch?v=VIDEO_ID → video embed card
Audio → files.exopraxist.org/share/TOKEN → audio card + floating player
- Generate a self-contained HTML page
Then update music-hub.html subtitle with current totals.
Output filenames: strip '-music' suffix, lowercase the stem.
OldMusicandMVs-music.md → oldmusicandmvs.html
Run after adding or editing any *-music.md file.
"""
import re
from pathlib import Path
from html import escape
HERE = Path(__file__).parent
HUB = HERE / "music-hub.html"
ALLOW = (
"accelerometer; autoplay; clipboard-write; "
"encrypted-media; gyroscope; picture-in-picture; web-share"
)
# ── MD parsing ────────────────────────────────────────────────────────────────
def slug_from_path(path: Path) -> str:
stem = path.stem
stem = re.sub(r'-music$', '', stem, flags=re.IGNORECASE)
return stem.lower().replace('-', '').replace('_', '').replace(' ', '')
def parse_md(path: Path) -> dict:
text = path.read_text(encoding='utf-8')
m = re.search(r'^#\s+(.+)$', text, re.MULTILINE)
title = m.group(1).strip() if m else path.stem
m = re.search(r'>>on-first-load\s+-\s+lightbox\s+-\s+"([^"]+)"', text)
lightbox = m.group(1).strip() if m else None
entries = []
for m in re.finditer(
r'-\s+\*\*(.+?)\*\*\s*\n\s+-\s+URL:\s+(https://[^\s]+)',
text
):
name = m.group(1).strip()
# Strip trailing \- Source label from name (e.g. "Title \- YouTube")
name = re.sub(r'\s*\\-\s*.+$', '', name).strip()
url = m.group(2).strip()
if 'youtube.com/watch' in url:
vid = re.search(r'[?&]v=([A-Za-z0-9_-]+)', url)
if vid:
entries.append({'type': 'youtube', 'name': name,
'video_id': vid.group(1)})
elif 'files.exopraxist.org/share/' in url:
token = url.rstrip('/').split('/')[-1]
entries.append({'type': 'audio', 'name': name,
'share_url': url,
'direct_url': f"https://files.exopraxist.org/api/public/dl/{token}"})
return {'title': title, 'lightbox': lightbox, 'entries': entries}
# ── HTML blocks ───────────────────────────────────────────────────────────────
def build_lightbox_html(text: str, slug: str) -> tuple[str, str]:
if not text:
return '', ''
# Preserve — JL attribution if present at the end
body = text
attribution = ''
m = re.search(r'\s*-\s*JL\s*$', text)
if m:
body = text[:m.start()].strip()
attribution = '<p class="lightbox-attribution">— JL</p>'
safe_body = escape(body)
div = f"""
<div id="lightbox">
<div class="lightbox-content">
<span class="lightbox-close">&times;</span>
<p class="lightbox-quote">{safe_body}</p>
{attribution}
<span class="lightbox-hint">CLICK ANYWHERE TO DISMISS</span>
</div>
</div>"""
js = f"""
const lightbox = document.getElementById('lightbox');
if (!localStorage.getItem('music-{slug}-seen')) {{
lightbox.style.display = 'flex';
localStorage.setItem('music-{slug}-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();
}});
"""
return div, js
def build_video_card(entry: dict) -> str:
embed_url = f"https://www.youtube.com/embed/{entry['video_id']}"
title_esc = escape(entry['name'])
return f"""
<div class="card">
<div class="card-header"><h2 class="card-title">{title_esc}</h2></div>
<div class="video-container">
<iframe
data-src="{embed_url}"
allow="{ALLOW}"
allowfullscreen>
</iframe>
</div>
</div>"""
def build_audio_card(entry: dict, audio_index: int) -> str:
title_esc = escape(entry['name'])
direct_url = entry['direct_url']
share_url = entry['share_url']
return f"""
<div class="card" id="track-{audio_index}">
<div class="card-header">
<h2 class="card-title">{title_esc}</h2>
</div>
<div class="audio-card">
<div class="audio-controls">
<button class="play-btn"
data-src="{direct_url}"
data-title="{title_esc}">▶</button>
<div class="audio-info">
<a href="{share_url}" 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>"""
def build_entries_html(entries: list) -> str:
parts = []
audio_idx = 1
for e in entries:
if e['type'] == 'youtube':
parts.append(build_video_card(e))
else:
parts.append(build_audio_card(e, audio_idx))
audio_idx += 1
return '\n'.join(parts)
def build_stagger_css(count: int) -> str:
lines = []
for i in range(1, min(count, 12) + 1):
lines.append(
f" .card:nth-child({i}) {{ animation-delay: {(i-1)*50}ms; }}"
)
return '\n'.join(lines)
# ── Full page generation ──────────────────────────────────────────────────────
def generate_html(data: dict, slug: str) -> str:
title = data['title']
entries = data['entries']
count = len(entries)
has_audio = any(e['type'] == 'audio' for e in entries)
title_upper = title.upper()
count_label = f"{count} TRACK{'S' if count != 1 else ''}"
lightbox_div, lightbox_js = build_lightbox_html(data['lightbox'], slug)
entries_html = build_entries_html(entries)
stagger_css = build_stagger_css(count)
lightbox_css = """
/* --- 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);
}
""" if data['lightbox'] else ''
audio_css = """
/* --- 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;
}
""" if has_audio else ''
floating_player_html = """
<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>""" if has_audio else ''
audio_js = """
// --- 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;
});
});
""" if has_audio else ''
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{escape(title)} | 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_css}{audio_css}
/* --- 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;
}}
{stagger_css}
.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>
{lightbox_div}
<header class="sticky-header">
<div class="header-title">{escape(title_upper)}</div>
<button class="menu-toggle">☰</button>
<nav class="header-nav">
<a href="music-hub.html" class="nav-link">← MUSIC</a>
<span class="count">{count_label}</span>
</nav>
</header>
<main>
<h1 class="section-title">{escape(title.upper())}</h1>
{entries_html}
</main>
{floating_player_html}
<script>
document.addEventListener('DOMContentLoaded', () => {{
{lightbox_js}
// --- 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_js}
}});
</script>
</body>
</html>
"""
# ── Hub subtitle update ───────────────────────────────────────────────────────
def update_hub_subtitle(collection_count: int, entry_total: int):
hub_text = HUB.read_text(encoding='utf-8')
subtitle = (
f'<span class="subtitle">'
f'{collection_count} COLLECTION{"S" if collection_count != 1 else ""}'
f' · PERSONAL ARCHIVE'
f' · {entry_total} TRACK{"S" if entry_total != 1 else ""}'
f'</span>'
)
hub_text = re.sub(
r'<!-- SUBTITLE-START -->.*?<!-- SUBTITLE-END -->',
f'<!-- SUBTITLE-START -->{subtitle}<!-- SUBTITLE-END -->',
hub_text,
flags=re.DOTALL
)
HUB.write_text(hub_text, encoding='utf-8')
# ── Main ──────────────────────────────────────────────────────────────────────
def main():
md_files = sorted(HERE.glob('*-music.md'))
if not md_files:
print("No *-music.md files found.")
return
total_entries = 0
for md_path in md_files:
slug = slug_from_path(md_path)
out = HERE / f"{slug}.html"
data = parse_md(md_path)
count = len(data['entries'])
if count == 0:
print(f" SKIP {md_path.name} — no entries found")
continue
audio_count = sum(1 for e in data['entries'] if e['type'] == 'audio')
video_count = count - audio_count
html = generate_html(data, slug)
out.write_text(html, encoding='utf-8')
total_entries += count
print(f" BUILD {out.name}{data['title']} ({video_count} videos, {audio_count} audio)")
update_hub_subtitle(len(md_files), total_entries)
print(f"\nUpdated hub — {len(md_files)} collections, {total_entries} tracks total")
if __name__ == '__main__':
main()

186
Music/music-hub.html Normal file
View File

@@ -0,0 +1,186 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Music Hub — Singular Particular Space</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 !important;
}
body {
background-color: var(--bg-void);
color: var(--text-warm);
font-family: 'Rambla', sans-serif;
line-height: 1.6;
padding: 2rem 1rem;
max-width: 1200px;
margin: 0 auto;
}
header {
margin-bottom: 3rem;
border-bottom: 2px solid var(--mu-red);
padding-bottom: 1.5rem;
}
h1 {
font-family: 'Rancho', cursive;
font-size: clamp(3rem, 10vw, 5rem);
color: var(--mu-orange);
line-height: 1;
margin-bottom: 0.5rem;
}
.subtitle {
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.1em;
color: rgba(232, 213, 184, 0.6);
display: block;
margin-bottom: 1rem;
}
.back-link {
font-weight: 700;
text-transform: uppercase;
font-size: 0.85rem;
color: var(--mu-orange);
text-decoration: none;
display: inline-block;
transition: var(--transition);
}
.back-link:hover {
color: #ff9f6f; /* lighter tint of mu-orange */
}
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2rem;
}
.card {
display: block;
text-decoration: none;
color: inherit;
border: 2px solid var(--mu-red);
transition: var(--transition);
background: rgba(139, 32, 32, 0.05);
}
.card:hover {
border-color: var(--mu-orange);
box-shadow: 0 0 8px var(--mu-orange-glow);
background: rgba(255, 127, 63, 0.05);
}
.card-header {
background: var(--mu-red);
padding: 0.75rem 1rem;
transition: var(--transition);
}
.card:hover .card-header {
background: var(--mu-orange);
}
.card-title {
font-family: 'Rancho', cursive;
font-size: 1.5rem;
color: var(--text-warm);
}
.card:hover .card-title {
color: var(--bg-void);
}
.card-body {
padding: 1.5rem 1rem;
}
.card-meta {
font-weight: 700;
text-transform: uppercase;
font-size: 0.7rem;
letter-spacing: 0.1em;
color: rgba(232, 213, 184, 0.6);
margin-bottom: 0.5rem;
display: block;
}
.card-desc {
font-style: italic;
font-size: 0.95rem;
color: var(--text-warm);
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header>
<a href="../index.html" class="back-link">← SPACE</a>
<h1>MUSIC</h1>
<!-- SUBTITLE-START --><span class="subtitle">1 COLLECTION · PERSONAL ARCHIVE · 14 TRACKS</span><!-- SUBTITLE-END -->
</header>
<main class="grid">
<!-- Real Card -->
<a href="music-template.html" class="card">
<div class="card-header">
<h2 class="card-title">Old Music and Music Videos</h2>
</div>
<div class="card-body">
<span class="card-meta">14 ENTRIES</span>
<p class="card-desc">An archive of old recordings and music videos directed by JL.</p>
</div>
</a>
<!-- Placeholder 1 -->
<a href="#" class="card">
<div class="card-header">
<h2 class="card-title">Live Sessions</h2>
</div>
<div class="card-body">
<span class="card-meta">0 ENTRIES</span>
<p class="card-desc">Upcoming recordings from live performances and studio sessions.</p>
</div>
</a>
<!-- Placeholder 2 -->
<a href="#" class="card">
<div class="card-header">
<h2 class="card-title">Experimental Tapes</h2>
</div>
<div class="card-body">
<span class="card-meta">0 ENTRIES</span>
<p class="card-desc">Found sounds, field recordings, and sonic experiments.</p>
</div>
</a>
</main>
</body>
</html>

703
Music/music-template.html Normal file
View File

@@ -0,0 +1,703 @@
<!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 — Singular Particular Space</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 !important;
}
body {
background-color: var(--bg-void);
color: var(--text-warm);
font-family: 'Rambla', sans-serif;
line-height: 1.6;
margin: 0;
padding-top: 64px;
padding-bottom: 0;
}
body.player-active {
padding-bottom: 64px;
}
/* --- 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;
}
/* --- Main Layout --- */
main {
max-width: 900px;
margin: 3rem auto;
padding: 0 1rem;
}
.section-title {
font-family: 'Rancho', cursive;
font-size: 3rem;
color: var(--mu-orange);
margin-bottom: 2rem;
text-align: center;
}
/* --- Cards --- */
.card {
margin-bottom: 3rem;
border: 2px solid var(--mu-red);
background: rgba(139, 32, 32, 0.05);
transition: var(--transition);
}
.card:hover {
border-color: var(--mu-orange);
box-shadow: 0 0 8px var(--mu-orange-glow);
}
.card-header {
padding: 1rem;
border-bottom: 1px solid var(--mu-red);
}
.card-title {
font-family: 'Rancho', cursive;
font-size: 1.8rem;
color: var(--mu-orange);
}
.video-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
overflow: hidden;
background: #000;
}
.video-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
/* --- Audio Card --- */
.audio-card {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.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;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
cursor: pointer;
transition: var(--transition);
}
.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;
transition: var(--transition);
}
.download-link:hover {
text-decoration: underline;
}
.progress-container {
height: 4px;
background: var(--mu-red);
width: 100%;
position: relative;
cursor: pointer;
}
.progress-bar {
height: 100%;
background: var(--mu-orange);
width: 0%;
}
.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;
}
/* --- Lightbox --- */
#lightbox {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(4, 6, 11, 0.9);
display: none; /* JS toggles flex */
align-items: center;
justify-content: center;
z-index: 1000;
padding: 2rem;
}
.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;
}
.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;
}
.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);
}
/* --- 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; /* JS toggles flex */
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;
}
.fp-progress-wrap {
position: absolute;
top: -2px;
left: 0;
width: 100%;
height: 2px;
}
/* --- Mobile --- */
@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; }
.section-title { font-size: 2.2rem; }
}
</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'm considering it. We'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">THE ARCHIVE</h1>
<!-- Audio Card -->
<div class="card" id="track-1">
<div class="card-header">
<h2 class="card-title">Dammit (Download — Free)</h2>
</div>
<div class="audio-card">
<div class="audio-controls">
<button class="play-btn" data-track-id="1" data-src="https://files.exopraxist.org/api/public/dl/KeQTyi9r" data-title="Dammit (Blink 182 Cover)"></button>
<div class="audio-info">
<a href="https://files.exopraxist.org/share/KeQTyi9r" target="_blank" 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>
<!-- Video Cards -->
<div class="card">
<div class="card-header"><h2 class="card-title">Sixteen by Melanie Kerr — A Music Video</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 — A Music Video</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'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 — Cold Shoulder (Music Video 720p)</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>
</main>
<!-- Floating Player -->
<div class="floating-player">
<div class="fp-progress-wrap">
<div class="progress-container">
<div class="progress-bar"></div>
</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', () => {
// --- Lightbox ---
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 Menu ---
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 iframeObserver = 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').forEach(iframe => {
iframeObserver.observe(iframe);
});
// --- 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 fpProgressBar = floatingPlayer.querySelector('.progress-bar');
let currentTrackBtn = null;
const updateUI = () => {
const isPlaying = !audio.paused;
const icon = isPlaying ? '▐▐' : '▶';
if (currentTrackBtn) currentTrackBtn.textContent = icon;
fpPlayBtn.textContent = icon;
if (isPlaying) {
floatingPlayer.classList.add('active');
document.body.classList.add('player-active');
}
};
const formatTime = (time) => {
const min = Math.floor(time / 60);
const sec = Math.floor(time % 60);
return `${min}:${sec.toString().padStart(2, '0')}`;
};
document.querySelectorAll('.play-btn').forEach(btn => {
btn.addEventListener('click', () => {
const src = btn.dataset.src;
const title = btn.dataset.title;
if (audio.src === src) {
if (audio.paused) audio.play();
else audio.pause();
} else {
// Reset previous btn if any
if (currentTrackBtn) currentTrackBtn.textContent = '▶';
audio.src = src;
fpTitle.textContent = title;
currentTrackBtn = btn;
audio.play();
}
updateUI();
});
});
fpPlayBtn.addEventListener('click', () => {
if (audio.paused) audio.play();
else audio.pause();
updateUI();
});
audio.addEventListener('timeupdate', () => {
const percent = (audio.currentTime / audio.duration) * 100;
document.querySelectorAll('.progress-bar').forEach(bar => {
bar.style.width = percent + '%';
});
// Update local time display if card is visible
if (currentTrackBtn) {
const card = currentTrackBtn.closest('.audio-card');
card.querySelector('.current-time').textContent = formatTime(audio.currentTime);
}
});
audio.addEventListener('loadedmetadata', () => {
if (currentTrackBtn) {
const card = currentTrackBtn.closest('.audio-card');
card.querySelector('.total-time').textContent = formatTime(audio.duration);
}
});
// Sync floating player button on native play/pause events
audio.addEventListener('play', updateUI);
audio.addEventListener('pause', updateUI);
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 allBtns = [...document.querySelectorAll('.play-btn')];
const idx = allBtns.indexOf(currentTrackBtn);
const prev = allBtns[idx - 1];
if (prev) prev.click();
});
document.getElementById('fp-next').addEventListener('click', () => {
const allBtns = [...document.querySelectorAll('.play-btn')];
const idx = allBtns.indexOf(currentTrackBtn);
const next = allBtns[idx + 1];
if (next) next.click();
});
// Seek logic
document.querySelectorAll('.progress-container').forEach(container => {
container.addEventListener('click', (e) => {
const rect = container.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
audio.currentTime = pos * audio.duration;
});
});
});
</script>
</body>
</html>

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>