Files
singular-particular-space/index.html
JL Kruger fd47895207 Add Playlists section; clean up build artifacts from repo
- Wire Playlists star in index.html → Playlists/playlists.html
- Hub page: Aladin font, dynamic cards via build.py injection, no back-link
- 4 playlist pages: eclectica-experimenti, daydreamsoftime, soosfynwyn, theelfladymadeusdoit
- build.py: scans folder, extracts title+track count, regenerates hub cards in place
- Remove 21 tracked CSVs (moved with PlaylistPirate to ToolsnToys)
- Untrack Images/GEMINI.md, GEMINI-FIX.md, script-guide.md (agent artifacts)
- .gitignore: global rules for GEMINI*.md, HANDOVER.md, script-guide.md; Playlists build dirs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 12:14:34 +02:00

1321 lines
52 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<!--
Integration v13 — THE NIGHT MARKET — 2026-03-27
AUTHOR: Sonnet
ARCHITECTURE:
- Body: 350vh, display:flex, flex-direction:column (no justify-content — items stack top to bottom)
- #sky-layer: position:fixed — nebula + star canvases fill the viewport always (you're always in space)
- #star-map: 100vh, position:relative, flex-shrink:0 — TOP of page, where user starts
Star nodes (main + writing sub-stars) are absolute children of star-map
mode-writings class toggled on star-map to swap nav layers
- [void]: flex:1 — the 150vh of empty space between star-map and scene
- #scene: 100vh, position:relative, flex-shrink:0 — BOTTOM of page, the city
#horizon-fog, #grid-canvas, #skyline-footer are absolute children of scene
All positioned with CSS % — no JS geometry
- #content-wrap: position:fixed — rises into viewport on star click
- #hud: position:fixed — always visible
PRESERVED FROM v5/v8:
- All 8 personality star animations
- Polychromatic billboard CSS
- 6-layer nebulae
- Keyboard spatial nav + visited tracking
- Full accessibility markup
- Welcome modal (every page load)
-->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>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=Noto+Emoji&family=Space+Grotesk:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-void: #04060b;
--bg-deep: #050810;
--bg-warm: #0d1320;
--fire-amber: #e8943a;
--fire-coral: #d4654a;
--neon-green: #32dc8c;
--neon-teal: #2ac4b3;
--deep-red: #8b2020;
--blue-magenta: #6b3fa0;
--cosmic-purple: #4a1d6e;
--star-blue: #a0c4ff;
--text-warm: #e8d5b8;
--text-muted: #6a7a8a;
--orchid: #c558d9;
--paradise: #ff7f3f;
--toucan: #ffcf40;
--mint-glow: #86efac;
--fairy-pink: #f472b6;
--waterfall: #3fbfaf;
--phosphor: #00ff41;
--warm-gold: #c4a24a;
--footer-h: 33vh; /* overridden by JS */
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { overflow-x: hidden; }
body {
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
background: var(--bg-void);
color: var(--text-warm);
height: 350vh;
display: flex;
flex-direction: column;
}
/* Lock outer scroll while content frame is risen; iframe takes over */
body.content-mode { overflow: hidden; }
/* CRT Ghost */
body::after {
content: '';
position: fixed; inset: 0;
background: repeating-linear-gradient(
0deg, transparent, transparent 2px,
rgba(0,0,0,0.06) 2px, rgba(0,0,0,0.06) 4px
);
pointer-events: none;
z-index: 9999;
}
/* ══ STAR MAP — fixed to viewport, always visible ══ */
#star-map {
position: fixed;
top: 0; left: 0;
width: 100%;
height: 100vh;
z-index: 5;
pointer-events: none; /* children opt-in */
}
/* ══ SCENE WRAPPER — normal block at the end of the flex body ══ */
#scene {
position: relative;
width: 100%;
height: 100vh;
flex-shrink: 0;
overflow: hidden;
}
/* ══ SKY LAYER — fixed canvas background, always fills viewport ══ */
#sky-layer {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
background: radial-gradient(ellipse at 50% 35%, #090d1c 0%, var(--bg-void) 100%);
pointer-events: none;
}
#sky-layer::after {
content: '';
position: absolute; inset: 0;
background: radial-gradient(ellipse at 50% 50%, transparent 50%, rgba(0,0,0,0.28) 100%);
pointer-events: none;
z-index: 4;
}
/* Horizon fog — atmospheric gradient between stars and grid */
#horizon-fog {
position: absolute;
top: 44%;
left: 0;
width: 100%;
height: 45%;
z-index: 3;
pointer-events: none;
background: linear-gradient(to bottom,
transparent 0%,
rgba(13,19,32,0.45) 35%,
rgba(13,19,32,0.80) 62%,
rgba(9,13,28,0.92) 80%,
rgba(6,8,16,0.98) 100%);
}
#nebula-canvas, #star-canvas {
position: absolute;
top: 0; left: 0;
width: 100%;
/* height set by JS to window.innerHeight */
}
#nebula-canvas { z-index: 0; }
#star-canvas { z-index: 1; }
/* Grid sits at the horizon inside the scene */
#grid-canvas {
position: absolute;
top: 62%;
left: 0;
width: 100%;
height: 38%;
z-index: 5;
pointer-events: none;
}
/* ══ STAR NODES ══ */
.star-node {
position: absolute;
z-index: 5;
display: flex; flex-direction: column; align-items: center; gap: 8px;
cursor: pointer;
transform: translate(-50%, -50%);
outline: none; border: none; background: none; padding: 0;
font-family: inherit;
pointer-events: auto;
}
.star-visual {
position: relative;
width: 40px; height: 40px;
display: flex; align-items: center; justify-content: center;
}
.star-dot {
width: 7px; height: 7px; border-radius: 50%;
position: relative; z-index: 2;
transition: width 150ms ease, height 150ms ease, background 150ms ease, box-shadow 150ms ease;
}
.star-label {
font-size: clamp(12px, 2vw, 16px);
font-weight: 500; color: var(--text-muted);
letter-spacing: 0.03em; white-space: nowrap;
opacity: 0;
transition: opacity 200ms ease, color 150ms ease;
pointer-events: none; user-select: none;
}
.star-node:hover .star-label,
.star-node:focus .star-label,
.star-node.current .star-label { opacity: 1; color: var(--text-warm); }
.star-node[data-star="writings"] .star-dot { background: var(--star-blue); box-shadow: 0 0 8px var(--star-blue), 0 0 16px rgba(160,196,255,0.4); }
.star-node[data-star="videos"] .star-dot { background: var(--fire-coral); box-shadow: 0 0 8px var(--fire-coral), 0 0 16px rgba(212,101,74,0.4); }
.star-node[data-star="music"] .star-dot { background: var(--neon-teal); box-shadow: 0 0 8px var(--neon-teal), 0 0 16px rgba(42,196,179,0.4); }
.star-node[data-star="images"] .star-dot { background: var(--mint-glow); box-shadow: 0 0 8px var(--mint-glow), 0 0 16px rgba(134,239,172,0.4); }
.star-node[data-star="playlists"] .star-dot { background: var(--toucan); box-shadow: 0 0 8px var(--toucan), 0 0 16px rgba(255,207,64,0.4); }
.star-node[data-star="watchlists"] .star-dot { background: var(--orchid); box-shadow: 0 0 8px var(--orchid), 0 0 16px rgba(197,88,217,0.4); }
.star-node[data-star="toolsntoys"] .star-dot { background: var(--fairy-pink); box-shadow: 0 0 8px var(--fairy-pink), 0 0 16px rgba(244,114,182,0.4); }
.star-node[data-star="creatorlists"] .star-dot { background: var(--warm-gold); box-shadow: 0 0 8px var(--warm-gold), 0 0 16px rgba(196,162,74,0.4); }
.star-node:hover .star-dot,
.star-node:focus .star-dot {
width: 20px; height: 20px;
background: var(--fire-amber) !important;
box-shadow: 0 0 12px rgba(232,148,58,0.95), 0 0 24px rgba(232,148,58,0.5) !important;
}
.star-node.current .star-dot {
width: 40px; height: 40px;
background: var(--fire-amber) !important;
box-shadow: 0 0 20px rgba(232,148,58,1), 0 0 50px rgba(232,148,58,0.65), 0 0 80px rgba(232,148,58,0.2) !important;
}
.star-node.current .star-label { font-size: clamp(22px, 4vw, 32px); color: var(--text-warm); opacity: 1; }
.star-node.visited .star-dot {
background: var(--neon-green) !important;
box-shadow: 0 0 8px rgba(50,220,140,0.9), 0 0 16px rgba(50,220,140,0.4) !important;
}
.star-node.visited .star-label { color: var(--neon-green); }
.star-node.visited:hover .star-dot,
.star-node.visited:focus .star-dot {
background: var(--fire-amber) !important;
box-shadow: 0 0 8px rgba(232,148,58,0.9) !important;
}
.star-node.visited.current .star-dot {
width: 40px; height: 40px;
background: var(--fire-amber) !important;
box-shadow: 0 0 20px rgba(232,148,58,1), 0 0 50px rgba(232,148,58,0.65) !important;
}
.star-node.visited:hover .star-label,
.star-node.visited:focus .star-label,
.star-node.visited.current .star-label { color: var(--text-warm); opacity: 1; }
/* ── Personality 1: Writings — The Beacon ── */
.star-node[data-star="writings"] .star-visual::before {
content: ''; position: absolute; width: 1px; height: 100%;
background: linear-gradient(to bottom, transparent 0%, var(--star-blue) 50%, transparent 100%);
animation: beacon-v 4s ease-in-out infinite;
}
.star-node[data-star="writings"] .star-visual::after {
content: ''; position: absolute; width: 100%; height: 1px;
background: linear-gradient(to right, transparent 0%, var(--star-blue) 50%, transparent 100%);
animation: beacon-h 4s ease-in-out infinite 0.4s;
}
@keyframes beacon-v { 0%, 100% { opacity: 0.08; } 50% { opacity: 0.4; } }
@keyframes beacon-h { 0%, 100% { opacity: 0.08; } 50% { opacity: 0.4; } }
/* ── Personality 2: Videos — The Binary ── */
.star-node[data-star="videos"] .star-visual::before {
content: ''; position: absolute; width: 3px; height: 3px;
border-radius: 50%; background: rgba(255,255,255,0.9);
animation: binary-orbit 3.2s linear infinite;
}
@keyframes binary-orbit {
from { transform: rotate(0deg) translateX(14px); }
to { transform: rotate(360deg) translateX(14px); }
}
/* ── Personality 3: Music — The Pulsar ── */
.star-node[data-star="music"] .star-visual::before {
content: ''; position: absolute; width: 32px; height: 32px;
border-radius: 50%; border: 1px solid var(--neon-teal);
animation: pulsar-ring 2.2s ease-out infinite;
}
@keyframes pulsar-ring { 0% { transform: scale(0.2); opacity: 0.8; } 100% { transform: scale(1.1); opacity: 0; } }
/* ── Personality 4: Images — The Flora ── */
.star-node[data-star="images"] .star-visual::before {
content: ''; position: absolute; width: 22px; height: 22px;
border-radius: 50%; border: 1px solid rgba(134,239,172,0.45);
animation: flora-pulse 3.5s ease-in-out infinite;
}
.star-node[data-star="images"] .star-visual::after {
content: ''; position: absolute; width: 36px; height: 36px;
border-radius: 50%; border: 1px solid rgba(134,239,172,0.22);
animation: flora-pulse 3.5s ease-in-out infinite 0.9s;
}
@keyframes flora-pulse { 0%, 100% { opacity: 0.85; transform: scale(0.9); } 50% { opacity: 0.3; transform: scale(1.05); } }
/* ── Personality 5: Playlists — The Hearth ── */
.star-node[data-star="playlists"] .star-visual::before {
content: ''; position: absolute; width: 30px; height: 30px;
border-radius: 50%;
background: radial-gradient(circle, rgba(232,148,58,0.3) 0%, transparent 68%);
animation: hearth-breathe 2.8s ease-in-out infinite;
}
@keyframes hearth-breathe { 0%, 100% { opacity: 0.4; transform: scale(0.8); } 50% { opacity: 1.0; transform: scale(1.2); } }
/* ── Personality 6: Watchlists — The Nebula Point ── */
.star-node[data-star="watchlists"] .star-visual::before {
content: ''; position: absolute; width: 38px; height: 26px;
border-radius: 50%;
background: radial-gradient(ellipse, rgba(197,88,217,0.22) 0%, transparent 70%);
animation: nebula-breathe 5.5s ease-in-out infinite;
}
@keyframes nebula-breathe { 0%, 100% { opacity: 0.5; transform: scale(0.85) rotate(-6deg); } 50% { opacity: 1.0; transform: scale(1.12) rotate(4deg); } }
/* ── Personality 7: ToolsnToys — The Spark ── */
.star-node[data-star="toolsntoys"] .star-visual::before {
content: '+'; position: absolute;
color: var(--fairy-pink); font-size: 24px; font-weight: 300; line-height: 1;
top: -11px; opacity: 0.5;
animation: spark-flicker 1.6s ease-in-out infinite;
}
@keyframes spark-flicker { 0%, 100% { opacity: 0.3; transform: scale(0.88); } 40% { opacity: 0.75; transform: scale(1.06); } 70% { opacity: 0.4; transform: scale(0.94); } }
/* ── Personality 8: Creatorlists — The Flow ── */
.star-node[data-star="creatorlists"] .star-visual::before {
content: '· · ·'; position: absolute; left: 22px;
color: var(--waterfall); font-size: 12px; letter-spacing: 4px;
white-space: nowrap; opacity: 0.5;
animation: flow-drift 4s ease-in-out infinite;
}
@keyframes flow-drift { 0%, 100% { opacity: 0.28; transform: translateX(0); } 50% { opacity: 0.65; transform: translateX(4px); } }
/* ══ NAV LAYER SWITCHING ══ */
/* Main nav stars fade when writings mode is active */
.main-star-node { transition: opacity 0.7s ease; }
#star-map.mode-writings .main-star-node { opacity: 0; pointer-events: none; }
/* Writing stars: hidden by default, visible in writings mode */
.writing-star-node {
opacity: 0; pointer-events: none;
transition: opacity 0.7s ease;
}
.writing-star-node .star-dot { width: 5px; height: 5px; }
.writing-star-node:hover .star-dot,
.writing-star-node:focus .star-dot {
width: 14px; height: 14px;
background: var(--fire-amber) !important;
box-shadow: 0 0 10px rgba(232,148,58,0.95), 0 0 20px rgba(232,148,58,0.5) !important;
}
.writing-star-node .star-label { font-size: clamp(11px, 1.6vw, 14px); }
#star-map.mode-writings .writing-star-node { opacity: 1; pointer-events: auto; }
/* ══ SKYLINE FOOTER — absolute inside #scene, sits at scene bottom ══ */
#skyline-footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: var(--footer-h);
display: flex;
align-items: flex-end;
z-index: 10;
pointer-events: auto;
/* Ambient ground glow */
background: linear-gradient(to top, rgba(232,148,58,0.07) 0%, transparent 100%);
animation: city-wake 1.4s ease-out 0.3s both;
}
@keyframes city-wake {
from { opacity: 0.2; }
to { opacity: 1; }
}
.sky-col {
flex-shrink: 0;
background: var(--bg-deep);
position: relative;
overflow: visible; /* billboard extends above */
border-top: 1px solid rgba(232,148,58,0.10);
}
/* Edge light — on ~25% of columns */
.sky-col.edge-lit::after {
content: '';
position: absolute;
top: 0; right: 0;
width: 1px; height: 100%;
background: linear-gradient(to bottom, rgba(42,196,179,0.28), rgba(42,196,179,0.04));
}
/* Text window characters inside columns */
.win-char {
position: absolute;
font-family: 'Courier New', Courier, monospace;
line-height: 1;
pointer-events: none;
user-select: none;
}
/* Billboard nav — floats above column */
.billboard-nav {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 2px;
font-family: 'Noto Emoji', 'Space Grotesk', system-ui, sans-serif;
border: 1px solid;
background: rgba(4, 6, 11, 0.92);
white-space: nowrap;
text-decoration: none;
transition: color 150ms ease, border-color 150ms ease;
cursor: pointer;
pointer-events: auto;
}
.billboard-nav.bb-sm { font-size: 14px; font-weight: 400; padding: 3px 8px; }
.billboard-nav.bb-md { font-size: 18px; font-weight: 500; padding: 4px 10px; }
.billboard-nav.bb-lg { font-size: 22px; font-weight: 600; padding: 5px 12px; }
.billboard-nav.bb-teal { color: var(--neon-teal); border-color: rgba(42,196,179,0.28); }
.billboard-nav.bb-amber { color: var(--fire-amber); border-color: rgba(232,148,58,0.28); }
.billboard-nav.bb-green { color: var(--neon-green); border-color: rgba(50,220,140,0.28); }
.billboard-nav.bb-orchid { color: var(--orchid); border-color: rgba(197,88,217,0.28); }
.billboard-nav.bb-coral { color: var(--fire-coral); border-color: rgba(212,101,74,0.28); }
.billboard-nav.bb-pink { color: var(--fairy-pink); border-color: rgba(244,114,182,0.28); }
.billboard-nav.bb-gold { color: var(--toucan); border-color: rgba(255,207,64,0.28); }
.billboard-nav.bb-water { color: var(--waterfall); border-color: rgba(63,191,175,0.28); }
.billboard-nav:hover,
.billboard-nav:focus {
color: var(--fire-amber);
border-color: rgba(232,148,58,0.45);
outline: none;
}
/* ══ CONTENT FRAME (the only thing that rises) ══ */
#content-wrap {
position: fixed;
top: 11vh;
left: 8px; right: 8px;
bottom: 4px;
z-index: 8;
opacity: 0.98;
/* Gradient border: teal top → orchid mid → amber bottom */
border: 2px solid;
border-image: linear-gradient(to bottom,
var(--neon-teal) 0%,
var(--orchid) 50%,
var(--fire-amber) 100%) 1;
background: var(--bg-warm);
/* Starts completely below viewport */
transform: translateY(calc(100vh + 40px));
transition: transform 1.2s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: none;
}
#content-wrap.risen {
transform: translateY(0);
pointer-events: auto;
}
/* iframe fills the frame and handles its own scroll */
#content-frame {
width: 100%; height: 100%;
border: none; display: block;
background: var(--bg-warm);
overflow-y: auto;
opacity: 0;
transition: opacity 200ms ease;
}
#content-frame.loaded { opacity: 1; }
/* ══ HUD ══ */
#hud {
position: fixed; top: 14px; right: 14px; z-index: 100;
display: flex; flex-direction: column; gap: 5px;
padding: 9px 13px 11px;
background: rgba(4,6,11,0.92);
border: 1px solid rgba(0,255,65,0.2);
}
#hud::before {
content: '[ SYS ]';
display: block;
font-family: 'Courier New', Courier, monospace;
font-size: 8px; color: rgba(0,255,65,0.3);
letter-spacing: 0.18em; margin-bottom: 5px;
}
#back-btn {
background: none; border: none; border-radius: 0; padding: 0;
font-family: 'Courier New', Courier, monospace;
font-size: 12px; color: rgba(0,255,65,0.75);
cursor: pointer; text-align: left; letter-spacing: 0.04em;
transition: color 100ms ease;
}
#back-btn::before { content: '> '; color: rgba(0,255,65,0.4); }
#back-btn::after { content: '_'; color: rgba(0,255,65,0.65); animation: cur-blink 1.1s step-end infinite; }
@keyframes cur-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
#back-btn:hover, #back-btn:focus { color: var(--phosphor); outline: none; }
#jl-link {
font-family: 'Courier New', Courier, monospace;
font-size: 10px; color: rgba(0,255,65,0.42);
text-decoration: none; letter-spacing: 0.04em;
transition: color 100ms ease;
}
#jl-link::before { content: '>> '; color: rgba(0,255,65,0.22); }
#jl-link:hover, #jl-link:focus { color: var(--phosphor); outline: none; }
#sr-announce {
position: absolute;
width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0,0,0,0); border: 0;
}
/* ══ LIGHTBOX ══ */
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes slide-up { from { transform: translateY(24px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
#lightbox-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.8);
z-index: 200;
display: flex; align-items: center; justify-content: center;
animation: fade-in 2.5s ease-out;
}
#lightbox-overlay.hidden { display: none; }
#lightbox {
background: var(--bg-warm);
border: 1px solid var(--fire-amber);
border-radius: 4px;
padding: 36px; max-width: 480px; width: 90%;
text-align: center; position: relative;
animation: slide-up 1.2s cubic-bezier(0.16, 1, 0.3, 1);
}
#lightbox::before {
content: ''; position: absolute;
top: 0; left: 8px; right: 8px; height: 1px;
background: linear-gradient(to right, transparent, rgba(0,255,65,0.15), transparent);
pointer-events: none;
}
#lightbox::after {
content: ''; position: absolute; inset: 4px;
border: 1px solid rgba(197,88,217,0.13);
pointer-events: none; border-radius: 2px;
}
#lightbox p { font-size: 16px; line-height: 1.65; color: var(--text-warm); margin-bottom: 20px; }
#lightbox .nav-hint { font-size: 13px; color: var(--text-muted); margin-bottom: 24px; line-height: 1.5; display: block; }
#lightbox-enter {
padding: 8px 28px;
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
font-size: 14px; font-weight: 500;
color: var(--bg-void); background: var(--fire-amber);
border: none; border-radius: 4px; cursor: pointer;
transition: opacity 150ms ease;
}
#lightbox-enter:hover, #lightbox-enter:focus { opacity: 0.85; outline: none; }
/* ══ CLUSTER LABELS (writings mode only) ══ */
.cluster-label {
position: absolute;
transform: translateX(-50%);
font-family: 'Courier New', Courier, monospace;
font-size: 9px;
letter-spacing: 0.22em;
color: rgba(160,196,255,0.22);
pointer-events: none;
user-select: none;
opacity: 0;
transition: opacity 500ms ease;
z-index: 4;
}
#star-map.mode-writings .cluster-label { opacity: 1; }
/* ══ WRITINGS LIGHTBOX ══ */
#writings-lightbox-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.8);
z-index: 200;
display: flex; align-items: center; justify-content: center;
animation: fade-in 0.8s ease-out;
}
#writings-lightbox-overlay.hidden { display: none; }
#writings-lightbox {
background: var(--bg-warm);
border: 1px solid var(--star-blue);
border-radius: 4px;
padding: 36px; max-width: 520px; width: 90%;
text-align: center; position: relative;
animation: slide-up 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
#writings-lightbox::before {
content: ''; position: absolute;
top: 0; left: 8px; right: 8px; height: 1px;
background: linear-gradient(to right, transparent, rgba(160,196,255,0.18), transparent);
pointer-events: none;
}
#writings-lightbox::after {
content: ''; position: absolute; inset: 4px;
border: 1px solid rgba(160,196,255,0.1);
pointer-events: none; border-radius: 2px;
}
#writings-lightbox p { font-size: 15px; line-height: 1.7; color: var(--text-warm); margin-bottom: 24px; }
#writings-lightbox-enter {
padding: 8px 28px;
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
font-size: 14px; font-weight: 500;
color: var(--bg-void); background: var(--star-blue);
border: none; border-radius: 4px; cursor: pointer;
transition: opacity 150ms ease;
}
#writings-lightbox-enter:hover, #writings-lightbox-enter:focus { opacity: 0.85; outline: none; }
/* ══ REDUCED MOTION ══ */
@media (prefers-reduced-motion: reduce) {
.star-node[data-star] .star-visual::before,
.star-node[data-star] .star-visual::after { animation: none; opacity: 0.06; }
.star-dot { transition: none; }
#content-wrap { transition: none; }
#content-frame { transition: none; }
#lightbox-overlay { animation: none; }
#lightbox { animation: none; }
#skyline-footer { animation: none; }
}
/* ══ RESPONSIVE ══ */
@media (max-width: 768px) {
.star-label { font-size: 12px; }
#lightbox { padding: 24px; max-width: 380px; }
.billboard-nav.bb-sm { font-size: 12px; }
.billboard-nav.bb-md { font-size: 15px; }
.billboard-nav.bb-lg { font-size: 18px; }
}
@media (max-width: 480px) {
.star-dot { width: 12px; height: 12px; }
.star-node:hover .star-dot, .star-node:focus .star-dot { width: 22px; height: 22px; }
.star-node.current .star-dot { width: 36px; height: 36px; }
.star-node.current .star-label { font-size: clamp(18px, 6vw, 24px); }
.star-node:active .star-label { opacity: 1; }
#lightbox { padding: 20px 16px; }
#lightbox p { font-size: 15px; }
}
</style>
</head>
<body>
<!-- Fixed sky background — always fills the viewport, canvases only -->
<div id="sky-layer">
<canvas id="nebula-canvas"></canvas>
<canvas id="star-canvas"></canvas>
</div>
<!-- Star map — top 100vh, where the user starts; star nodes appended by JS -->
<div id="star-map" role="navigation" aria-label="Star map navigation">
<div class="cluster-label" style="left:26%;top:0.8%">STORIES</div>
<div class="cluster-label" style="left:72%;top:0.8%">ESSAYS</div>
<div class="cluster-label" style="left:54%;top:68%">MISC.</div>
</div>
<!-- Void — 150vh of deep space between the star map and the city -->
<div id="void" style="flex:1"></div>
<!-- Scene — bottom 100vh, grid + skyline -->
<div id="scene">
<div id="horizon-fog"></div>
<canvas id="grid-canvas"></canvas>
<div id="skyline-footer" aria-label="Section navigation"></div>
</div>
<!-- Content frame: rises when a section is selected -->
<div id="content-wrap" aria-live="polite">
<iframe id="content-frame" title="Section content"></iframe>
</div>
<!-- HUD -->
<div id="hud">
<button id="back-btn" aria-label="Back to stars">stars</button>
<a id="jl-link" href="https://jl-kruger.github.io/introductions" target="_blank" rel="noopener">JL Kruger</a>
</div>
<div id="sr-announce" aria-live="polite" aria-atomic="true"></div>
<!-- Writings section lightbox — hidden until user enters Writings -->
<div id="writings-lightbox-overlay" class="hidden" role="dialog" aria-modal="true" aria-label="Writings">
<div id="writings-lightbox">
<p>Over the years, I have spun many yarns, told many tales, offered many possibly unpopular opinions. I guided some machines to turn old writings into artifacts of educative value, or some other conceit. I quite liked the results and so I have shared them here for public scrutination and discourstion. Peace, Joy, and Good Faith, Forever. Your&#8209;Nomad&#8209;Soul, Myster Wizzard, JL.</p>
<button id="writings-lightbox-enter">Enter</button>
</div>
</div>
<!-- Welcome lightbox — no hidden class on load, animation plays immediately -->
<div id="lightbox-overlay" role="dialog" aria-modal="true" aria-label="Welcome">
<div id="lightbox">
<p>Hello traveller, welcome to a singular, particular space. Feel free to explore this little pocket of the universe. It&rsquo;s an adventure, bring snacks. Happy wanderings, Myster Wizzard</p>
<span class="nav-hint">click stars to explore &middot; arrow keys to navigate &middot; escape to return</span>
<button id="lightbox-enter">Enter</button>
</div>
</div>
<script>
(function () {
'use strict';
var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var STARS = [
{ id: 'writings', label: 'Writings', sign: '文', x: 25, y: 22, href: '' },
{ id: 'videos', label: 'Videos', sign: '映', x: 68, y: 18, href: 'Videos/index.html' },
{ id: 'music', label: 'Music', sign: '♬', x: 12, y: 44, href: '' },
{ id: 'images', label: 'Images', sign: '絵', x: 55, y: 35, href: 'Images/images.html' },
{ id: 'playlists', label: 'Playlists', sign: '≡', x: 78, y: 50, href: 'Playlists/playlists.html' },
{ id: 'watchlists', label: 'Watchlists', sign: '視', x: 22, y: 66, href: '' },
{ id: 'toolsntoys', label: 'ToolsnToys', sign: '⚙', x: 50, y: 72, href: '' },
{ id: 'creatorlists', label: 'Creatorlists', sign: '創', x: 75, y: 70, href: '' }
];
// Writing sub-section stars — each maps to one HTML in /Writings/
// Clustered: Stories (left, x 5-52), Essays (right, x 56-92), Miscellany (bottom, x 26-80)
var WRITINGS = [
// ── STORIES ──
{ id: 'benson', label: 'Benson & Old Man John', sign: '記', href: 'Writings/benson_oldmanjohn.html', x: 8, y: 6 },
{ id: 'butforadream', label: 'But For A Dream', sign: '夢', href: 'Writings/butforadream-annotated.html', x: 24, y: 6 },
{ id: 'daddydidntdoit', label: "Daddy Didn't Do It", sign: '証', href: 'Writings/DaddyDidntDoIt_Annotated.html', x: 40, y: 6 },
{ id: 'doomy', label: 'Doomy McDoomface', sign: '骸', href: 'Writings/doomy-mcdoomface.html', x: 10, y: 15 },
{ id: 'escape2', label: 'Escape', sign: '逃', href: 'Writings/escape-annotated-v2.html', x: 42, y: 15 },
{ id: 'faultywarpcore', label: 'Faulty Warp Core', sign: '核', href: 'Writings/faulty-warp-core.html', x: 6, y: 24 },
{ id: 'gladstone', label: 'Gladstone', sign: '石', href: 'Writings/gladstone-annotated.html', x: 20, y: 24 },
{ id: 'raft', label: 'How To Build A Raft', sign: '舟', href: 'Writings/HowToBuildARaft_Annotated.html', x: 36, y: 24 },
{ id: 'iwashere', label: 'I Was Here', sign: '痕', href: 'Writings/iwashere-annotated.html', x: 50, y: 24 },
{ id: 'johnnyclive', label: 'Johnny and Clive', sign: '友', href: 'Writings/johnny-and-clive.html', x: 12, y: 33 },
{ id: 'lastmen', label: 'Last Men Standing', sign: '残', href: 'Writings/lastmenstanding-annotated.html', x: 28, y: 33 },
{ id: 'himitsu', label: 'My Himitsu', sign: '秘', href: 'Writings/my-himitsu.html', x: 44, y: 33 },
{ id: 'shesmiled', label: 'She Smiled at Me', sign: '笑', href: 'Writings/she_smiled_at_me.html', x: 8, y: 42 },
{ id: 'skyfishing', label: 'Sky Fishing', sign: '釣', href: 'Writings/SkyFishing_Annotated.html', x: 24, y: 42 },
{ id: 'smokerpolitician',label: 'Smoker Politician Beast', sign: '獣', href: 'Writings/smoker-politician-beast.html', x: 40, y: 42 },
{ id: 'snacks', label: 'Snacks', sign: '食', href: 'Writings/snacks-annotated.html', x: 14, y: 51 },
{ id: 'sneaky', label: 'Sneaky Bastards', sign: '影', href: 'Writings/sneaky-bastards.html', x: 30, y: 51 },
{ id: 'thenightyoudied', label: 'The Night You Died', sign: '夜', href: 'Writings/TheNightYouDied_Annotated.html', x: 46, y: 51 },
{ id: 'watchmaker', label: 'The Watchmaker', sign: '時', href: 'Writings/watchmaker.html', x: 18, y: 60 },
{ id: 'whoniverse', label: 'Whoniverse', sign: '宇', href: 'Writings/whoniverse-annotated.html', x: 36, y: 60 },
// ── ESSAYS ──
{ id: 'iamargument', label: 'I Am An Argument', sign: '論', href: 'Writings/i-am-an-argument.html', x: 60, y: 8 },
{ id: 'masterrace', label: 'Master Race', sign: '族', href: 'Writings/master_race_annotated.html', x: 78, y: 8 },
{ id: 'nothingnew', label: 'Nothing New About Normal',sign: '常', href: 'Writings/nothing-new-about-normal.html', x: 58, y: 20 },
{ id: 'pensandswords', label: 'Of Pens and Swords', sign: '剣', href: 'Writings/OfPensandSwordsIntheBellyoftheDragon.html', x: 76, y: 20 },
{ id: 'polemos', label: 'Polemos', sign: '戦', href: 'Writings/polemos-annotated.html', x: 62, y: 32 },
{ id: 'politics', label: 'Politics', sign: '政', href: 'Writings/politics-annotated.html', x: 80, y: 32 },
{ id: 'postscript', label: 'Post Script', sign: '後', href: 'Writings/post-script.html', x: 60, y: 44 },
{ id: 'snakeoil', label: 'Snake Oil', sign: '蛇', href: 'Writings/snake-oil-annotated.html', x: 78, y: 44 },
{ id: 'noteforend', label: 'Note for the End', sign: '末', href: 'Writings/note-for-the-end.html', x: 70, y: 56 },
// ── MISCELLANY ──
{ id: 'threepoems', label: 'Three Poems', sign: '詩', href: 'Writings/three-poems.html', x: 28, y: 76 },
{ id: 'nomad', label: 'Nomad Archive', sign: '旅', href: 'Writings/nomad-archive.html', x: 52, y: 76 },
{ id: 'alexandra', label: 'Life in Alexandra', sign: '街', href: 'Writings/life_in_alexandra.html', x: 76, y: 76 }
];
// Palette cycling for writing star dot colors
var WRITING_PALETTE = [
{ color: 'rgb(160,196,255)', shadow: '160,196,255' },
{ color: 'rgb(212,101,74)', shadow: '212,101,74' },
{ color: 'rgb(42,196,179)', shadow: '42,196,179' },
{ color: 'rgb(134,239,172)', shadow: '134,239,172' },
{ color: 'rgb(255,207,64)', shadow: '255,207,64' },
{ color: 'rgb(197,88,217)', shadow: '197,88,217' },
{ color: 'rgb(244,114,182)', shadow: '244,114,182' },
{ color: 'rgb(196,162,74)', shadow: '196,162,74' }
];
var BB_COLORS = ['bb-teal', 'bb-coral', 'bb-orchid', 'bb-green', 'bb-gold', 'bb-water', 'bb-pink', 'bb-amber'];
var BB_SIZES = ['bb-md', 'bb-sm', 'bb-lg', 'bb-sm', 'bb-lg', 'bb-md', 'bb-sm', 'bb-lg'];
// State
var currentStarIndex = 0;
var inContentMode = false;
var lightboxOpen = false;
var writingsMode = false;
var writingsLightboxShown = false;
var currentWritingIndex = 0;
var writingElements = [];
var visited = JSON.parse(localStorage.getItem('sp-visited') || '{}');
var starElements = [];
var srAnnounce = document.getElementById('sr-announce');
var backBtn = document.getElementById('back-btn');
var contentFrame = document.getElementById('content-frame');
var contentWrap = document.getElementById('content-wrap');
// ── Smooth scroll utility ──
function smoothScrollTo(targetY, onComplete) {
if (prefersReducedMotion) { window.scrollTo(0, targetY); if (onComplete) onComplete(); return; }
var startY = window.scrollY || window.pageYOffset;
var startT = null;
var dur = 700;
function step(t) {
if (!startT) startT = t;
var p = Math.min(1, (t - startT) / dur);
var e = 1 - Math.pow(1 - p, 3); // cubic ease-out
window.scrollTo(0, startY + (targetY - startY) * e);
if (p < 1) requestAnimationFrame(step);
else if (onComplete) onComplete();
}
requestAnimationFrame(step);
}
// ── Layout constants ──
var FOOTER_H;
function computeLayout() {
FOOTER_H = Math.round(window.innerHeight * 0.33);
document.documentElement.style.setProperty('--footer-h', FOOTER_H + 'px');
}
// No parallax — the entire scene sits at the bottom of the 350vh page as one unit.
// ── Star color palette ──
var STAR_COLORS = [
{ r: 200, g: 210, b: 230, w: 35 },
{ r: 240, g: 220, b: 190, w: 15 },
{ r: 140, g: 180, b: 230, w: 12 },
{ r: 255, g: 207, b: 64, w: 10 },
{ r: 244, g: 114, b: 182, w: 8 },
{ r: 197, g: 88, b: 217, w: 8 },
{ r: 134, g: 239, b: 172, w: 7 },
{ r: 212, g: 101, b: 74, w: 5 }
];
function pickStarColor() {
var roll = Math.random() * 100, cum = 0;
for (var i = 0; i < STAR_COLORS.length; i++) { cum += STAR_COLORS[i].w; if (roll < cum) return STAR_COLORS[i]; }
return STAR_COLORS[0];
}
function pickStarSize() {
var r = Math.random() * 100;
if (r < 42) return 0.3 + Math.random() * 0.2;
if (r < 70) return 0.5 + Math.random() * 0.5;
if (r < 90) return 1.0 + Math.random() * 0.5;
return 1.5 + Math.random() * 1.5;
}
// ── Nebula (static, drawn once on resize) ──
var nebulaCanvas = document.getElementById('nebula-canvas');
var nebulaCtx = nebulaCanvas.getContext('2d');
function drawNebulae() {
var isMob = window.innerWidth < 480;
nebulaCanvas.width = window.innerWidth;
nebulaCanvas.height = window.innerHeight;
var w = nebulaCanvas.width, h = nebulaCanvas.height;
nebulaCtx.clearRect(0, 0, w, h);
var isMobile = isMob;
function wash(cx, cy, rad, r, g, b, a) {
var grd = nebulaCtx.createRadialGradient(cx, cy, 0, cx, cy, rad);
grd.addColorStop(0, 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')');
grd.addColorStop(0.5, 'rgba(' + r + ',' + g + ',' + b + ',' + (a * 0.35) + ')');
grd.addColorStop(1, 'rgba(' + r + ',' + g + ',' + b + ',0)');
nebulaCtx.fillStyle = grd;
nebulaCtx.fillRect(0, 0, w, h);
}
wash(w*0.25, h*0.30, w*0.38, 197, 88, 217, 0.08);
wash(w*0.12, h*0.65, w*0.38, 42, 196, 179, 0.07);
wash(w*0.78, h*0.18, w*0.42, 74, 29, 110, 0.08);
if (!isMobile) {
wash(w*0.52, h*0.12, w*0.30, 244, 114, 182, 0.07);
wash(w*0.85, h*0.55, w*0.30, 255, 127, 63, 0.06);
wash(w*0.52, h*0.78, w*0.28, 255, 207, 64, 0.05);
}
}
// ── Star field (throttled ~20fps) ──
var starCanvas = document.getElementById('star-canvas');
var starCtx = starCanvas.getContext('2d');
var bgStars = [];
var lastStarT = 0;
var FRAME_INT = 50;
function generateBgStars() {
// star canvas fills the scene (1× viewport)
var isMob = window.innerWidth < 480;
starCanvas.width = window.innerWidth;
starCanvas.height = window.innerHeight;
var w = starCanvas.width, h = starCanvas.height;
// scale count with canvas area vs single viewport
var count = isMob ? 350 : 900;
bgStars = [];
for (var i = 0; i < count; i++) {
var c = pickStarColor(), r = pickStarSize();
var steady = Math.random() < 0.12;
var spd = steady ? 0 : (r > 1.5 ? 0.15 + Math.random() * 0.25 : r < 0.5 ? 0.6 + Math.random() * 0.6 : 0.3 + Math.random() * 0.5);
bgStars.push({ x: Math.random() * w, y: Math.random() * h, r: r, base: 0.25 + Math.random() * 0.55, phase: Math.random() * 6.283, spd: spd, col: c.r + ',' + c.g + ',' + c.b });
}
}
function drawStars(t) {
if (t - lastStarT < FRAME_INT) { requestAnimationFrame(drawStars); return; }
lastStarT = t;
starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
var ts = t * 0.001;
for (var i = 0; i < bgStars.length; i++) {
var s = bgStars[i];
var a = (s.spd === 0 || prefersReducedMotion) ? s.base : s.base + Math.sin(ts * s.spd + s.phase) * 0.3;
a = Math.max(0.05, Math.min(1, a));
starCtx.beginPath(); starCtx.arc(s.x, s.y, s.r, 0, 6.2832);
starCtx.fillStyle = 'rgba(' + s.col + ',' + a + ')';
starCtx.fill();
}
requestAnimationFrame(drawStars);
}
// ── Grid (animated: v5 verticals + v7 horizontal pulse) ──
var lastGridT = 0;
var gridCanvas = document.getElementById('grid-canvas');
var GRID_PAL = [
[42,196,179], [197,88,217], [244,114,182], [255,207,64],
[134,239,172],[212,101,74], [232,148,58], [63,191,175]
];
function drawGrid(t) {
if (t - lastGridT < FRAME_INT) { requestAnimationFrame(drawGrid); return; }
lastGridT = t;
var ctx = gridCanvas.getContext('2d');
var w = gridCanvas.width = gridCanvas.offsetWidth;
var h = gridCanvas.height = gridCanvas.offsetHeight;
if (!w || !h) { requestAnimationFrame(drawGrid); return; }
ctx.clearRect(0, 0, w, h);
var pulsePos = (t * 0.05) % 100;
var gridTop = 2;
var numH = 22;
// Horizontal lines — quadratic spacing + data pulse
for (var i = 0; i < numH; i++) {
var s = i / (numH - 1);
var y = gridTop + (h - gridTop) * (s * s);
var p = GRID_PAL[i % GRID_PAL.length];
if (Math.abs(s * 100 - pulsePos) < 4) {
ctx.strokeStyle = 'rgba(232,148,58,0.4)';
} else {
ctx.strokeStyle = 'rgba(' + p[0] + ',' + p[1] + ',' + p[2] + ',' + (0.045 + s * 0.19) + ')';
}
ctx.lineWidth = 1.3;
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
}
// Vertical lines — perspective converge (v5)
var vpX = w * 0.5, spread = 2.4, numV = 26;
for (var j = 0; j <= numV; j++) {
var topX = w * (j / numV);
var bottomX = vpX + (topX - vpX) * spread;
var p = GRID_PAL[j % GRID_PAL.length];
var dist = Math.abs(j / numV - 0.5) * 2;
ctx.strokeStyle = 'rgba(' + p[0] + ',' + p[1] + ',' + p[2] + ',' + (0.055 + dist * 0.10) + ')';
ctx.lineWidth = 1.3;
ctx.beginPath(); ctx.moveTo(topX, gridTop); ctx.lineTo(bottomX, h); ctx.stroke();
}
requestAnimationFrame(drawGrid);
}
// ── Skyline footer ──
// Text window characters — monospace, scattered inside columns
var WIN_CHARS = ['▪', '·', '▫', '▬', '·', '·', '▪'];
var WIN_COLORS = ['232,148,58', '42,196,179', '197,88,217', '134,239,172', '106,122,138'];
var WIN_WEIGHTS= [50, 20, 10, 10, 10];
function pickWinColor() {
var r = Math.random() * 100, c = 0;
for (var i = 0; i < WIN_WEIGHTS.length; i++) { c += WIN_WEIGHTS[i]; if (r < c) return WIN_COLORS[i]; }
return WIN_COLORS[0];
}
function addTextWindows(col, colW, colH) {
var count = Math.floor((colW * colH) / 480 * (0.6 + Math.random() * 0.9));
for (var i = 0; i < count; i++) {
var span = document.createElement('span');
span.className = 'win-char';
span.textContent = WIN_CHARS[Math.floor(Math.random() * WIN_CHARS.length)];
var alpha = 0.12 + Math.random() * 0.32;
span.style.cssText =
'font-size:' + (6 + Math.floor(Math.random() * 4)) + 'px;' +
'color:rgba(' + pickWinColor() + ',' + alpha + ');' +
'left:' + (2 + Math.random() * Math.max(1, colW - 10)) + 'px;' +
'top:' + (3 + Math.random() * Math.max(1, colH - 10)) + 'px;';
col.appendChild(span);
}
}
function buildSkyline() {
var container = document.getElementById('skyline-footer');
container.innerHTML = '';
var vw = window.innerWidth;
var N = Math.max(22, Math.min(58, Math.floor(vw / 34)));
// Generate heights — random with mild center-weighting
var heights = [];
for (var i = 0; i < N; i++) {
var distFromCenter = Math.abs(i - N / 2) / (N / 2);
var boost = 1 - distFromCenter * 0.28;
heights.push(Math.max(0.08, Math.min(0.96, (0.12 + Math.random() * 0.84) * boost)));
}
// Pick 8 billboard columns, evenly distributed
var step = Math.floor(N / 8);
var offset = Math.floor(step / 2);
var bbMap = {};
for (var b = 0; b < 8; b++) {
bbMap[offset + b * step] = b;
}
for (var i = 0; i < N; i++) {
var col = document.createElement('div');
col.className = 'sky-col';
var colW = 18 + Math.random() * 52;
var colH = Math.round(heights[i] * FOOTER_H);
col.style.width = colW + 'px';
col.style.height = colH + 'px';
col.style.marginLeft = Math.floor(Math.random() * 3) + 'px';
if (Math.random() < 0.25) col.classList.add('edge-lit');
addTextWindows(col, colW, colH);
if (bbMap[i] !== undefined) {
var starIdx = bbMap[i];
var bb = document.createElement('a');
bb.className = 'billboard-nav ' + BB_COLORS[starIdx] + ' ' + BB_SIZES[starIdx];
bb.textContent = STARS[starIdx].sign;
bb.setAttribute('href', '#');
bb.setAttribute('tabindex', '0');
bb.setAttribute('aria-label', STARS[starIdx].label + ' section');
bb.addEventListener('click', (function (idx) {
return function (e) { e.preventDefault(); selectStar(idx); };
})(starIdx));
col.appendChild(bb);
}
container.appendChild(col);
}
}
// ── Star DOM nodes ──
STARS.forEach(function (star, i) {
var btn = document.createElement('button');
btn.className = 'star-node main-star-node';
btn.setAttribute('aria-label', star.label + ' section');
btn.setAttribute('data-star', star.id);
btn.style.left = star.x + '%';
btn.style.top = star.y + '%';
var visual = document.createElement('div');
visual.className = 'star-visual';
var dot = document.createElement('span');
dot.className = 'star-dot';
visual.appendChild(dot);
var label = document.createElement('span');
label.className = 'star-label';
label.textContent = star.label;
btn.appendChild(visual);
btn.appendChild(label);
document.getElementById('star-map').appendChild(btn);
starElements.push(btn);
if (visited[star.id]) btn.classList.add('visited');
btn.addEventListener('click', function (e) { e.preventDefault(); selectStar(i); });
});
starElements[0].classList.add('current');
// ── Writing star DOM nodes (hidden until writings mode) ──
WRITINGS.forEach(function (writing, i) {
var pal = WRITING_PALETTE[i % WRITING_PALETTE.length];
var btn = document.createElement('button');
btn.className = 'star-node writing-star-node';
btn.setAttribute('aria-label', writing.label);
btn.setAttribute('data-writing', writing.id);
btn.style.left = writing.x + '%';
btn.style.top = writing.y + '%';
var visual = document.createElement('div');
visual.className = 'star-visual';
var dot = document.createElement('span');
dot.className = 'star-dot';
dot.style.background = pal.color;
dot.style.boxShadow = '0 0 6px rgba(' + pal.shadow + ',0.9), 0 0 12px rgba(' + pal.shadow + ',0.4)';
visual.appendChild(dot);
var label = document.createElement('span');
label.className = 'star-label';
label.textContent = writing.label;
btn.appendChild(visual);
btn.appendChild(label);
document.getElementById('star-map').appendChild(btn);
writingElements.push(btn);
btn.addEventListener('click', function (e) { e.preventDefault(); selectWriting(i); });
});
// ── Writings layer management ──
var writingsOverlay = document.getElementById('writings-lightbox-overlay');
var writingsEnterBtn = document.getElementById('writings-lightbox-enter');
function selectWritingsSection() {
writingsMode = true;
backBtn.textContent = 'writings';
var s = window.scrollY || window.pageYOffset;
if (!writingsLightboxShown) {
writingsLightboxShown = true;
lightboxOpen = true;
if (s >= 20) smoothScrollTo(0, null);
writingsOverlay.classList.remove('hidden');
writingsEnterBtn.focus();
} else {
if (s < 20) activateWritingsLayer();
else smoothScrollTo(0, activateWritingsLayer);
}
}
function closeWritingsLightbox() {
writingsOverlay.classList.add('hidden');
lightboxOpen = false;
activateWritingsLayer();
}
writingsEnterBtn.addEventListener('click', closeWritingsLightbox);
writingsOverlay.addEventListener('click', function (e) { if (e.target === writingsOverlay) closeWritingsLightbox(); });
writingsOverlay.addEventListener('keydown', function (e) { if (e.key === 'Tab') { e.preventDefault(); writingsEnterBtn.focus(); } });
function activateWritingsLayer() {
document.getElementById('star-map').classList.add('mode-writings');
srAnnounce.textContent = 'Writings star map. ' + WRITINGS.length + ' pieces.';
}
function returnToMainNav() {
writingsMode = false;
document.getElementById('star-map').classList.remove('mode-writings');
backBtn.textContent = 'stars';
srAnnounce.textContent = 'Returned to star map';
smoothScrollTo(0, function () {
starElements[currentStarIndex].focus();
});
}
// ── Writing selection — frame rises with writing HTML ──
function selectWriting(index) {
if (lightboxOpen) return;
var writing = WRITINGS[index];
currentWritingIndex = index;
srAnnounce.textContent = 'Opening ' + writing.label;
inContentMode = true;
document.body.classList.add('content-mode');
backBtn.textContent = 'back to writings';
writingElements.forEach(function (el) { el.classList.remove('current'); });
writingElements[index].classList.add('current');
contentFrame.classList.remove('loaded');
contentFrame.src = writing.href;
contentFrame.onload = function () { contentFrame.classList.add('loaded'); };
contentWrap.classList.add('risen');
}
// ── Star selection — frame rises (or writings mode activates) ──
function selectStar(index) {
if (lightboxOpen) return;
var star = STARS[index];
// Writings star activates the writings sub-map instead of rising a frame
if (star.id === 'writings') {
visited[star.id] = true;
localStorage.setItem('sp-visited', JSON.stringify(visited));
starElements[index].classList.add('visited');
selectWritingsSection();
return;
}
visited[star.id] = true;
localStorage.setItem('sp-visited', JSON.stringify(visited));
starElements[index].classList.add('visited');
starElements.forEach(function (el) { el.classList.remove('current'); });
starElements[index].classList.add('current');
currentStarIndex = index;
srAnnounce.textContent = 'Opening ' + star.label;
inContentMode = true;
document.body.classList.add('content-mode'); // lock outer scroll; iframe scrolls
backBtn.textContent = 'back to stars';
contentFrame.classList.remove('loaded');
if (star.href) {
contentFrame.src = star.href;
contentFrame.onload = function () { contentFrame.classList.add('loaded'); };
} else {
contentFrame.srcdoc = '<html><body style="background:#0d1320;color:#6a7a8a;' +
'font-family:\'Space Grotesk\',system-ui,sans-serif;display:flex;' +
'align-items:center;justify-content:center;height:100%;margin:0">' +
'<p style="font-size:14px">' + star.label + ' &mdash; coming soon</p>' +
'</body></html>';
contentFrame.onload = function () { contentFrame.classList.add('loaded'); };
}
// Rise
contentWrap.classList.add('risen');
}
function returnToStars() {
inContentMode = false;
document.body.classList.remove('content-mode'); // restore outer scroll
if (writingsMode) {
backBtn.textContent = 'writings';
srAnnounce.textContent = 'Returned to writings';
} else {
backBtn.textContent = 'stars';
srAnnounce.textContent = 'Returning to star map';
}
contentWrap.classList.remove('risen');
contentWrap.addEventListener('transitionend', function handler() {
contentWrap.removeEventListener('transitionend', handler);
contentFrame.classList.remove('loaded');
contentFrame.src = 'about:blank';
contentFrame.removeAttribute('srcdoc');
if (writingsMode) {
writingElements[currentWritingIndex].focus();
} else {
starElements[currentStarIndex].focus();
}
}, { once: true });
}
backBtn.addEventListener('click', function () {
if (inContentMode) returnToStars();
else if (writingsMode) returnToMainNav();
});
// ── Keyboard spatial navigation ──
function findNearestStar(fromIdx, dir) {
var from = STARS[fromIdx], best = -1, bestScore = Infinity;
for (var i = 0; i < STARS.length; i++) {
if (i === fromIdx) continue;
var dx = STARS[i].x - from.x, dy = STARS[i].y - from.y;
var valid = dir === 'up' ? dy < -3 :
dir === 'down' ? dy > 3 :
dir === 'left' ? dx < -3 :
dx > 3;
if (!valid) continue;
var dist = Math.sqrt(dx*dx + dy*dy);
var penalty = (dir === 'up' || dir === 'down') ? Math.abs(dx)*0.5 : Math.abs(dy)*0.5;
var score = dist + penalty;
if (score < bestScore) { bestScore = score; best = i; }
}
return best;
}
document.addEventListener('keydown', function (e) {
if (lightboxOpen) {
if (e.key === 'Escape' || e.key === 'Enter') {
if (!writingsOverlay.classList.contains('hidden')) closeWritingsLightbox();
else closeLightbox();
e.preventDefault();
}
return;
}
if (inContentMode) {
if (e.key === 'Escape') { returnToStars(); e.preventDefault(); }
return;
}
if (writingsMode && !inContentMode) {
if (e.key === 'Escape') { returnToMainNav(); e.preventDefault(); }
return;
}
var dir = null;
if (e.key === 'ArrowUp') dir = 'up';
else if (e.key === 'ArrowDown') dir = 'down';
else if (e.key === 'ArrowLeft') dir = 'left';
else if (e.key === 'ArrowRight') dir = 'right';
else if (e.key === 'Enter' || e.key === ' ') { selectStar(currentStarIndex); e.preventDefault(); return; }
if (dir) {
e.preventDefault();
var next = findNearestStar(currentStarIndex, dir);
if (next >= 0) {
starElements[currentStarIndex].classList.remove('current');
currentStarIndex = next;
starElements[currentStarIndex].classList.add('current');
starElements[currentStarIndex].focus();
srAnnounce.textContent = STARS[currentStarIndex].label;
}
}
});
// ── Lightbox (every page load) ──
var overlay = document.getElementById('lightbox-overlay');
var enterBtn = document.getElementById('lightbox-enter');
function closeLightbox() {
overlay.classList.add('hidden');
lightboxOpen = false;
starElements[currentStarIndex].focus();
}
function showLightbox() {
overlay.classList.remove('hidden');
lightboxOpen = true;
enterBtn.focus();
}
enterBtn.addEventListener('click', closeLightbox);
overlay.addEventListener('click', function (e) { if (e.target === overlay) closeLightbox(); });
overlay.addEventListener('keydown', function (e) { if (e.key === 'Tab') { e.preventDefault(); enterBtn.focus(); } });
// ── Resize ──
var resizeTimer;
function resizeAll() {
computeLayout();
drawNebulae();
generateBgStars();
buildSkyline();
}
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resizeAll, 150);
});
// ── Init ──
computeLayout();
drawNebulae();
generateBgStars();
buildSkyline();
requestAnimationFrame(drawStars);
requestAnimationFrame(drawGrid);
showLightbox();
})();
</script>
</body>
</html>