Files
singular-particular-space/CREATURE-PLAYGROUND/homepage-redesign/sonnet-drafts/integration-v8.html
JL Kruger 5422131782 Initial commit — Singular Particular Space v1
Homepage (site/index.html): integration-v14 promoted, Writings section
integrated with 33 pieces clustered by type (stories/essays/miscellany),
Writings welcome lightbox, content frame at 98% opacity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 12:09:22 +02:00

1216 lines
50 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 v8 — THE NIGHT MARKET — 2026-03-27 (rev 3)
AUTHOR: Sonnet (acting as parent)
ARCHITECTURE:
- Body is 350vh tall — scroll "looks down" through deep space toward the city
- Star/nebula canvases are 4× viewport height; scroll parallax (0.3×) reveals lower depths
- Grid is stationary at the horizon (bottom of sky layer, z-index above scrolling stars)
- Skyline + billboards = fixed footer (DOM columns, text windows) — always at bottom
- Content frame = the ONLY thing that rises (CSS transition, gradient border, 88% opacity)
- When content is risen, body scroll is locked; iframe content scrolls instead
PRESERVED FROM v5:
- 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);
/* 350vh: user scrolls "down through space" toward the city */
height: 350vh;
}
/* 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;
}
/* ══ SKY LAYER ══ */
#sky-layer {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
overflow: hidden; /* clips tall canvases; scroll reveals their lower portions */
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;
}
/* Canvases are 4× viewport tall; translateY on scroll reveals lower depths */
#nebula-canvas, #star-canvas {
position: absolute;
top: 0; left: 0;
width: 100%;
/* height set by JS to 4× window.innerHeight */
will-change: transform;
}
#nebula-canvas { z-index: 0; }
#star-canvas { z-index: 1; }
/* Grid floats below the star field — position:fixed, top set by JS, moves with star parallax */
#grid-canvas {
position: fixed;
left: 0;
width: 100%;
height: 38vh;
z-index: 5;
pointer-events: none;
will-change: transform;
/* top set by computeLayout() */
}
/* ══ 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; }
#sky-layer.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); }
#sky-layer.mode-writings .writing-star-node { opacity: 1; pointer-events: auto; }
/* ══ SKYLINE FOOTER ══ */
#skyline-footer {
position: fixed;
left: 0;
width: 100%;
height: var(--footer-h);
display: flex;
align-items: flex-end;
z-index: 10;
pointer-events: auto;
will-change: transform;
/* top set by computeLayout() — moves with star parallax, not glued to viewport */
/* 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;
/* fills space from 11% down to just above the city footer */
bottom: calc(var(--footer-h) + 4px);
z-index: 8;
opacity: 0.88;
/* 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; }
/* ══ 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>
<!-- Sky layer: stars + nebulae only (grid + skyline are separate, same parallax) -->
<div id="sky-layer" role="navigation" aria-label="Star map navigation">
<canvas id="nebula-canvas"></canvas>
<canvas id="star-canvas"></canvas>
<!-- Star nodes appended by JS -->
</div>
<!-- Grid and skyline: position:fixed but move with star parallax, start below viewport -->
<canvas id="grid-canvas"></canvas>
<div id="skyline-footer" aria-label="Section navigation"></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>
<!-- 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: '' },
{ id: 'music', label: 'Music', sign: '♬', x: 12, y: 44, href: '' },
{ id: 'images', label: 'Images', sign: '絵', x: 55, y: 35, href: '' },
{ id: 'playlists', label: 'Playlists', sign: '≡', x: 78, y: 50, href: '' },
{ 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/
var WRITINGS = [
{ id: 'benson', label: 'Benson & Old Man John', sign: '記', href: '../../../Writings/benson_oldmanjohn.html', x: 15, y: 8 },
{ id: 'butforadream', label: 'But For A Dream', sign: '夢', href: '../../../Writings/butforadream-annotated.html', x: 35, y: 6 },
{ id: 'daddydidntdoit', label: "Daddy Didn't Do It", sign: '証', href: '../../../Writings/DaddyDidntDoIt_Annotated.html', x: 58, y: 11 },
{ id: 'doomy', label: 'Doomy McDoomface', sign: '骸', href: '../../../Writings/doomy-mcdoomface.html', x: 80, y: 7 },
{ id: 'escape', label: 'Escape', sign: '逃', href: '../../../Writings/escape-annotated-minimalist.html', x: 90, y: 16 },
{ id: 'escape2', label: 'Escape II', sign: '二', href: '../../../Writings/escape-annotated-v2.html', x: 22, y: 18 },
{ id: 'faultywarpcore', label: 'Faulty Warp Core', sign: '核', href: '../../../Writings/faulty-warp-core.html', x: 50, y: 14 },
{ id: 'gladstone', label: 'Gladstone', sign: '石', href: '../../../Writings/gladstone-annotated.html', x: 12, y: 27 },
{ id: 'raft', label: 'How To Build A Raft', sign: '舟', href: '../../../Writings/HowToBuildARaft_Annotated.html', x: 40, y: 23 },
{ id: 'iamargument', label: 'I Am An Argument', sign: '論', href: '../../../Writings/i-am-an-argument.html', x: 64, y: 27 },
{ id: 'iwashere', label: 'I Was Here', sign: '痕', href: '../../../Writings/iwashere-annotated.html', x: 82, y: 21 },
{ id: 'johnnyclive', label: 'Johnny and Clive', sign: '友', href: '../../../Writings/johnny-and-clive.html', x: 28, y: 35 },
{ id: 'lastmen', label: 'Last Men Standing', sign: '残', href: '../../../Writings/lastmenstanding-annotated.html', x: 8, y: 39 },
{ id: 'alexandra', label: 'Life in Alexandra', sign: '街', href: '../../../Writings/life_in_alexandra.html', x: 48, y: 37 },
{ id: 'masterrace', label: 'Master Race', sign: '族', href: '../../../Writings/master_race_annotated.html', x: 70, y: 33 },
{ id: 'himitsu', label: 'My Himitsu', sign: '秘', href: '../../../Writings/my-himitsu.html', x: 88, y: 41 },
{ id: 'nomad', label: 'Nomad Archive', sign: '旅', href: '../../../Writings/nomad-archive.html', x: 18, y: 47 },
{ id: 'noteforend', label: 'Note for the End', sign: '末', href: '../../../Writings/note-for-the-end.html', x: 38, y: 51 },
{ id: 'nothingnew', label: 'Nothing New About Normal',sign: '常', href: '../../../Writings/nothing-new-about-normal.html', x: 60, y: 47 },
{ id: 'pensandswords', label: 'Of Pens and Swords', sign: '剣', href: '../../../Writings/OfPensandSwordsIntheBellyoftheDragon.html', x: 82, y: 53 },
{ id: 'polemos', label: 'Polemos', sign: '戦', href: '../../../Writings/polemos-annotated.html', x: 12, y: 59 },
{ id: 'politics', label: 'Politics', sign: '政', href: '../../../Writings/politics-annotated.html', x: 32, y: 57 },
{ id: 'postscript', label: 'Post Script', sign: '後', href: '../../../Writings/post-script.html', x: 54, y: 61 },
{ id: 'shesmiled', label: 'She Smiled at Me', sign: '笑', href: '../../../Writings/she_smiled_at_me.html', x: 72, y: 57 },
{ id: 'skyfishing', label: 'Sky Fishing', sign: '釣', href: '../../../Writings/SkyFishing_Annotated.html', x: 90, y: 63 },
{ id: 'smokerpolitician',label: 'Smoker Politician Beast', sign: '獣', href: '../../../Writings/smoker-politician-beast.html', x: 20, y: 67 },
{ id: 'snacks', label: 'Snacks', sign: '食', href: '../../../Writings/snacks-annotated.html', x: 44, y: 71 },
{ id: 'snakeoil', label: 'Snake Oil', sign: '蛇', href: '../../../Writings/snake-oil-annotated.html', x: 66, y: 69 },
{ id: 'sneaky', label: 'Sneaky Bastards', sign: '影', href: '../../../Writings/sneaky-bastards.html', x: 84, y: 73 },
{ id: 'thenightyoudied', label: 'The Night You Died', sign: '夜', href: '../../../Writings/TheNightYouDied_Annotated.html', x: 14, y: 77 },
{ id: 'threepoems', label: 'Three Poems', sign: '詩', href: '../../../Writings/three-poems.html', x: 36, y: 81 },
{ id: 'watchmaker', label: 'The Watchmaker', sign: '時', href: '../../../Writings/watchmaker.html', x: 58, y: 79 },
{ id: 'whoniverse', label: 'Whoniverse', sign: '宇', href: '../../../Writings/whoniverse-annotated.html', x: 80, y: 83 }
];
// 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 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;
var gridCanvas = document.getElementById('grid-canvas');
var skylineFooter = document.getElementById('skyline-footer');
function computeLayout() {
var H = window.innerHeight;
FOOTER_H = Math.round(H * 0.33);
document.documentElement.style.setProperty('--footer-h', FOOTER_H + 'px');
// Grid occupies the bottom 38% of the viewport — visible as the horizon of the star field
// Skyline sits flush below grid (top = viewport bottom), hidden until you scroll
gridCanvas.style.top = Math.round(H * 0.62) + 'px';
skylineFooter.style.top = H + 'px';
// content-wrap fills top→(footer + 4px gap) via CSS bottom property — no JS height needed
}
// ── Scroll parallax: stars, nebulae, grid, and skyline all drift at 0.3× ──
// Grid and skyline start below viewport but travel with the star layer —
// as user scrolls "down through space", the whole scene moves as one unit.
var parallaxTick = false;
window.addEventListener('scroll', function () {
if (inContentMode) return;
if (!parallaxTick) {
requestAnimationFrame(function () {
var s = window.scrollY || window.pageYOffset;
if (!prefersReducedMotion) {
var dy = -(s * 0.3);
nebulaCanvas.style.transform = 'translateY(' + dy + 'px)';
starCanvas.style.transform = 'translateY(' + dy + 'px)';
gridCanvas.style.transform = 'translateY(' + dy + 'px)';
skylineFooter.style.transform = 'translateY(' + dy + 'px)';
}
parallaxTick = false;
});
parallaxTick = true;
}
}, { passive: true });
// ── 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 * (isMob ? 2 : 4);
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 is 4× tall on desktop, 2× on mobile
var isMob = window.innerWidth < 480;
starCanvas.width = window.innerWidth;
starCanvas.height = window.innerHeight * (isMob ? 2 : 4);
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 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('sky-layer').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('sky-layer').appendChild(btn);
writingElements.push(btn);
btn.addEventListener('click', function (e) { e.preventDefault(); selectWriting(i); });
});
// ── Writings layer management ──
function selectWritingsSection() {
writingsMode = true;
backBtn.textContent = 'writings';
var threshold = window.innerHeight * 0.25;
var s = window.scrollY || window.pageYOffset;
if (s >= threshold) {
activateWritingsLayer();
} else {
smoothScrollTo(threshold, activateWritingsLayer);
}
}
function activateWritingsLayer() {
document.getElementById('sky-layer').classList.add('mode-writings');
srAnnounce.textContent = 'Writings star map. ' + WRITINGS.length + ' pieces.';
}
function returnToMainNav() {
writingsMode = false;
document.getElementById('sky-layer').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') { 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(); // recomputes top positions for grid + skyline too
// reset parallax transforms (top positions already reset by computeLayout)
nebulaCanvas.style.transform = '';
starCanvas.style.transform = '';
gridCanvas.style.transform = '';
skylineFooter.style.transform = '';
drawNebulae(); // sets canvas dimensions internally
generateBgStars(); // sets canvas dimensions internally
buildSkyline();
}
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resizeAll, 150);
});
// ── Init ──
computeLayout();
drawNebulae();
generateBgStars();
buildSkyline();
requestAnimationFrame(drawStars);
requestAnimationFrame(drawGrid);
showLightbox();
})();
</script>
</body>
</html>