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>
This commit is contained in:
2026-03-27 12:09:22 +02:00
commit 5422131782
359 changed files with 117437 additions and 0 deletions

View File

@@ -0,0 +1,992 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!--
Sonnet Integration Draft v3 — BOLD — 2026-03-26
Responds to JL's "bolder" feedback: vaporwave + mad max + rainforest + fairies.
KEY CHANGES FROM v2:
- CRT GHOST: subtle static scanlines (0.04 opacity) + phosphor vignette
- EXPANDED STAR COLORS: orchid, fairy-pink, toucan-gold, mint-green, coral
- BOLDER NEBULAE: 5 washes at higher opacity (0.05-0.08), more color variety
- STAR BLOOM: hover/focus grows from 6px to 18px with amber glow (CSS only)
- BIGGER TEXT: labels 13px, billboards 12px, lightbox 16px
- LUSH SKYLINE: more bio accents, polychromatic windows and billboards
- LIGHTBOX: orchid inner border, phosphor-green top line
- Nav mechanics UNTOUCHED from v2
References studied:
- soviet-cyberpunk: CRT scanlines, phosphor glow, brutalist neon
- solarpunk-sundae: pastel warmth deepened for dark palette
- faerie-fire: prismatic violet/pink/cyan/green/gold
- solarpunk-rainforest: orchid, paradise orange, toucan yellow, mint
- nothing-new-about-normal: JL's own CRT aesthetic (#00ff41 phosphor, Soviet grid)
-->
<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: #060a14;
--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;
/* new bold palette */
--orchid: #c558d9;
--paradise: #ff7f3f;
--toucan: #ffcf40;
--mint-glow: #86efac;
--fairy-pink: #f472b6;
--waterfall: #3fbfaf;
--phosphor: #00ff41;
--warm-gold: #c4a24a;
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: auto; }
body {
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
background: var(--bg-void);
color: var(--text-warm);
overflow: hidden;
min-height: 100vh;
}
body.content-mode { overflow-y: auto; }
/* ── CRT Ghost ── */
/* Static scanlines — NOT animated, just texture */
body::after {
content: '';
position: fixed;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.04) 2px,
rgba(0, 0, 0, 0.04) 4px
);
pointer-events: none;
z-index: 9998;
}
/* ── Zone 1: Star Map ── */
#star-zone {
position: relative;
width: 100%; height: 100vh;
overflow: hidden;
will-change: transform;
background: radial-gradient(ellipse at 50% 50%, #080c18 0%, var(--bg-void) 100%);
}
/* Phosphor vignette — faint edge darkening */
#star-zone::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at 50% 50%, transparent 55%, rgba(0,0,0,0.3) 100%);
pointer-events: none;
z-index: 4;
}
#nebula-canvas {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 0;
}
#star-canvas {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
}
#constellation-svg {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 2;
pointer-events: none;
}
.constellation-line {
stroke-width: 1;
stroke-linecap: round;
animation: line-pulse 6s ease-in-out infinite;
}
.constellation-line:nth-child(2) { animation-delay: -1s; }
.constellation-line:nth-child(3) { animation-delay: -2.2s; }
.constellation-line:nth-child(4) { animation-delay: -3.5s; }
.constellation-line:nth-child(5) { animation-delay: -0.8s; }
.constellation-line:nth-child(6) { animation-delay: -4.1s; }
@keyframes line-pulse {
0%, 100% { stroke-opacity: 0.06; }
50% { stroke-opacity: 0.22; }
}
/* ── Star Nodes ── */
.star-node {
position: absolute;
z-index: 5;
display: flex;
flex-direction: column;
align-items: center;
gap: 7px;
text-decoration: none;
cursor: pointer;
transform: translate(-50%, -50%);
outline: none;
border: none;
background: none;
padding: 0;
font-family: inherit;
}
.star-dot {
width: 7px; height: 7px;
border-radius: 50%;
transition: width 150ms ease, height 150ms ease, background 150ms ease, box-shadow 150ms ease;
}
.star-label {
font-size: clamp(12px, 1.4vw, 13px);
font-weight: 400;
color: var(--text-muted);
letter-spacing: 0.02em;
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; }
/* Per-star accent colors */
.star-node[data-star="writings"] .star-dot { background: var(--star-blue); box-shadow: 0 0 4px var(--star-blue); }
.star-node[data-star="videos"] .star-dot { background: var(--fire-coral); box-shadow: 0 0 4px var(--fire-coral); }
.star-node[data-star="music"] .star-dot { background: var(--neon-teal); box-shadow: 0 0 4px var(--neon-teal); }
.star-node[data-star="images"] .star-dot { background: var(--mint-glow); box-shadow: 0 0 5px var(--mint-glow); }
.star-node[data-star="playlists"] .star-dot { background: var(--toucan); box-shadow: 0 0 4px var(--toucan); }
.star-node[data-star="watchlists"] .star-dot { background: var(--orchid); box-shadow: 0 0 4px var(--orchid); }
.star-node[data-star="toolsntoys"] .star-dot { background: var(--fairy-pink); box-shadow: 0 0 4px var(--fairy-pink); }
.star-node[data-star="creatorlists"] .star-dot{ background: var(--warm-gold); box-shadow: 0 0 4px var(--warm-gold); }
/* BLOOM: hover/focus/current — star grows to 18px */
.star-node:hover .star-dot,
.star-node:focus .star-dot,
.star-node.current .star-dot {
width: 18px; height: 18px;
background: var(--fire-amber);
box-shadow: 0 0 8px rgba(255, 160, 50, 0.95);
}
.star-node:hover .star-label,
.star-node:focus .star-label,
.star-node.current .star-label { color: var(--text-warm); }
/* Visited: neon green */
.star-node.visited .star-dot {
background: var(--neon-green);
box-shadow: 0 0 4px rgba(50, 220, 140, 0.8);
}
.star-node.visited .star-label { color: var(--neon-green); }
/* Visited + hover: bloom overrides */
.star-node.visited:hover .star-dot,
.star-node.visited:focus .star-dot,
.star-node.visited.current .star-dot {
width: 18px; height: 18px;
background: var(--fire-amber);
box-shadow: 0 0 8px rgba(255, 160, 50, 0.95);
}
.star-node.visited:hover .star-label,
.star-node.visited:focus .star-label,
.star-node.visited.current .star-label { color: var(--text-warm); opacity: 1; }
/* ── Transition Zone: Skyline ── */
#transition-zone {
position: relative;
width: 100%; height: 70vh;
/* Deep space → purple atmosphere → warm city */
background: linear-gradient(to bottom,
var(--bg-void) 0%,
#0a0818 25%,
#0e0c1a 45%,
var(--bg-warm) 100%);
overflow: hidden;
}
#skyline {
position: absolute;
bottom: 0; left: 0;
width: 100%; height: 70%;
display: flex;
align-items: flex-end;
justify-content: center;
}
#skyline::before {
content: '';
position: absolute;
bottom: 0; left: 0;
width: 100%; height: 60px;
background: linear-gradient(to top, rgba(232, 148, 58, 0.06), transparent);
z-index: 1;
pointer-events: none;
}
.building {
background: #05080f;
flex-shrink: 0;
position: relative;
border-top: 1px solid rgba(232, 148, 58, 0.04);
}
.building.edge-lit::after {
content: '';
position: absolute;
top: 0; right: 0;
width: 1px; height: 100%;
background: linear-gradient(to bottom, rgba(42, 196, 179, 0.14), rgba(42, 196, 179, 0.02));
}
.building .bio-stripe {
position: absolute;
width: 2px;
background: var(--mint-glow);
opacity: 0.08;
}
.building .bio-stripe.orchid {
background: var(--orchid);
opacity: 0.06;
}
.billboard-nav {
position: absolute;
top: -2px; left: 50%;
transform: translate(-50%, -100%);
padding: 3px 10px;
font-size: clamp(11px, 1.3vw, 12px);
font-weight: 400;
font-family: inherit;
border: 1px solid;
background: rgba(4, 6, 11, 0.9);
white-space: nowrap;
text-decoration: none;
transition: color 150ms ease, border-color 150ms ease;
cursor: pointer;
}
.billboard-nav.bb-teal { color: var(--neon-teal); border-color: rgba(42,196,179,0.25); }
.billboard-nav.bb-amber { color: var(--fire-amber); border-color: rgba(232,148,58,0.25); }
.billboard-nav.bb-green { color: var(--neon-green); border-color: rgba(50,220,140,0.25); }
.billboard-nav.bb-orchid { color: var(--orchid); border-color: rgba(197,88,217,0.25); }
.billboard-nav.bb-coral { color: var(--fire-coral); border-color: rgba(212,101,74,0.25); }
.billboard-nav.bb-pink { color: var(--fairy-pink); border-color: rgba(244,114,182,0.25); }
.billboard-nav.bb-gold { color: var(--toucan); border-color: rgba(255,207,64,0.25); }
.billboard-nav.bb-water { color: var(--waterfall); border-color: rgba(63,191,175,0.25); }
.billboard-nav:hover,
.billboard-nav:focus {
color: var(--fire-amber);
border-color: rgba(232, 148, 58, 0.4);
outline: none;
}
/* ── Zone 3: Content ── */
#content-zone {
width: 100%; min-height: 100vh;
background: var(--bg-warm);
}
#content-frame {
width: 100%; height: 100vh;
border: none; display: block;
background: var(--bg-warm);
opacity: 0;
transition: opacity 200ms ease;
}
#content-frame.loaded { opacity: 1; }
/* ── Persistent UI ── */
#back-btn {
position: fixed;
top: 16px; right: 16px; z-index: 100;
padding: 6px 14px;
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
font-size: 13px; font-weight: 400;
color: var(--text-muted);
background: rgba(4, 6, 11, 0.85);
border: 1px solid rgba(106, 122, 138, 0.2);
border-radius: 4px;
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease;
}
#back-btn:hover, #back-btn:focus {
color: var(--fire-amber);
border-color: rgba(232, 148, 58, 0.3);
outline: none;
}
#jl-link {
position: fixed;
bottom: 16px; right: 16px; z-index: 100;
font-size: 12px; font-weight: 400;
color: var(--text-muted);
text-decoration: none;
opacity: 0.6;
transition: opacity 150ms ease, color 150ms ease;
}
#jl-link:hover, #jl-link:focus {
opacity: 1; color: var(--fire-amber); 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: Campfire Note ── */
#lightbox-overlay {
position: fixed; inset: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 200;
display: flex; align-items: center; justify-content: center;
}
#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;
}
/* Phosphor-green top line — ghost of CRT */
#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;
}
/* Orchid inner border — fairy fire around the campfire note */
#lightbox::after {
content: '';
position: absolute;
inset: 4px;
border: 1px solid rgba(197, 88, 217, 0.08);
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) {
.constellation-line { animation: none; stroke-opacity: 0.12; }
#star-zone { will-change: auto; }
#content-frame { transition: none; }
.star-dot { transition: none; }
}
/* ── Responsive ── */
@media (max-width: 768px) {
.star-label { font-size: 12px; }
#lightbox { padding: 24px; max-width: 380px; }
.billboard-nav { font-size: 10px; padding: 3px 8px; }
}
@media (max-width: 480px) {
.star-dot { width: 12px; height: 12px; }
.star-node:hover .star-dot,
.star-node:focus .star-dot,
.star-node.current .star-dot { width: 22px; height: 22px; }
.star-node:active .star-label { opacity: 1; }
#lightbox { padding: 20px 16px; }
#lightbox p { font-size: 15px; }
.billboard-nav { font-size: 11px; padding: 6px 12px; }
}
</style>
</head>
<body>
<div id="star-zone" role="navigation" aria-label="Star map navigation">
<canvas id="nebula-canvas"></canvas>
<canvas id="star-canvas"></canvas>
<svg id="constellation-svg" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div id="transition-zone">
<div id="skyline"></div>
</div>
<div id="content-zone" role="region" aria-label="Section content">
<iframe id="content-frame" title="Section content"></iframe>
</div>
<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 id="sr-announce" aria-live="polite" aria-atomic="true"></div>
<div id="lightbox-overlay" class="hidden" 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'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', x: 25, y: 18, href: '', accent: '#a0c4ff' },
{ id: 'videos', label: 'Videos', x: 68, y: 15, href: '', accent: '#d4654a' },
{ id: 'music', label: 'Music', x: 12, y: 42, href: '', accent: '#2ac4b3' },
{ id: 'images', label: 'Images', x: 55, y: 35, href: '', accent: '#86efac' },
{ id: 'playlists', label: 'Playlists', x: 78, y: 48, href: '', accent: '#ffcf40' },
{ id: 'watchlists', label: 'Watchlists', x: 22, y: 65, href: '', accent: '#c558d9' },
{ id: 'toolsntoys', label: 'ToolsnToys', x: 50, y: 72, href: '', accent: '#f472b6' },
{ id: 'creatorlists', label: 'Creatorlists', x: 75, y: 70, href: '', accent: '#c4a24a' }
];
var CONSTELLATION_LINES = [
['writings', 'videos'],
['music', 'playlists'],
['images', 'videos'],
['watchlists', 'toolsntoys'],
['toolsntoys', 'creatorlists'],
['music', 'watchlists']
];
var currentStarIndex = 0;
var inContentMode = false;
var lightboxOpen = false;
var visited = JSON.parse(localStorage.getItem('sp-visited') || '{}');
var isScrolling = false;
// ── Expanded star color palette ──
// 35% cool white, 15% warm white, 12% pale blue, 10% toucan gold,
// 8% fairy pink, 8% orchid, 7% mint green, 5% coral
var STAR_COLORS = [
{ r: 200, g: 210, b: 230, weight: 35 },
{ r: 240, g: 220, b: 190, weight: 15 },
{ r: 140, g: 180, b: 230, weight: 12 },
{ r: 255, g: 207, b: 64, weight: 10 },
{ r: 244, g: 114, b: 182, weight: 8 },
{ r: 197, g: 88, b: 217, weight: 8 },
{ r: 134, g: 239, b: 172, weight: 7 },
{ r: 212, g: 101, b: 74, weight: 5 }
];
function pickStarColor() {
var roll = Math.random() * 100, cum = 0;
for (var i = 0; i < STAR_COLORS.length; i++) {
cum += STAR_COLORS[i].weight;
if (roll < cum) return STAR_COLORS[i];
}
return STAR_COLORS[0];
}
function pickStarSize() {
var roll = Math.random() * 100;
if (roll < 42) return 0.3 + Math.random() * 0.2;
if (roll < 70) return 0.5 + Math.random() * 0.5;
if (roll < 90) return 1.0 + Math.random() * 0.5;
return 1.5 + Math.random() * 1.5; // brights go up to 3px
}
// ── Nebula (static, drawn once) ──
var nebulaCanvas = document.getElementById('nebula-canvas');
var nebulaCtx = nebulaCanvas.getContext('2d');
function drawNebulae() {
nebulaCanvas.width = nebulaCanvas.offsetWidth;
nebulaCanvas.height = nebulaCanvas.offsetHeight;
nebulaCtx.clearRect(0, 0, nebulaCanvas.width, nebulaCanvas.height);
var w = nebulaCanvas.width, h = nebulaCanvas.height;
var isMobile = w < 480;
// Wash 1: cosmic purple, upper-right
var g1 = nebulaCtx.createRadialGradient(w*0.75, h*0.2, 0, w*0.75, h*0.2, w*0.4);
g1.addColorStop(0, 'rgba(74, 29, 110, 0.08)');
g1.addColorStop(0.5, 'rgba(74, 29, 110, 0.03)');
g1.addColorStop(1, 'rgba(74, 29, 110, 0)');
nebulaCtx.fillStyle = g1;
nebulaCtx.fillRect(0, 0, w, h);
// Wash 2: teal-waterfall, lower-left
var g2 = nebulaCtx.createRadialGradient(w*0.15, h*0.7, 0, w*0.15, h*0.7, w*0.35);
g2.addColorStop(0, 'rgba(63, 191, 175, 0.06)');
g2.addColorStop(0.5, 'rgba(63, 191, 175, 0.02)');
g2.addColorStop(1, 'rgba(63, 191, 175, 0)');
nebulaCtx.fillStyle = g2;
nebulaCtx.fillRect(0, 0, w, h);
// Wash 3: orchid, center-left
var g3 = nebulaCtx.createRadialGradient(w*0.3, h*0.4, 0, w*0.3, h*0.4, w*0.3);
g3.addColorStop(0, 'rgba(197, 88, 217, 0.05)');
g3.addColorStop(0.6, 'rgba(197, 88, 217, 0.015)');
g3.addColorStop(1, 'rgba(197, 88, 217, 0)');
nebulaCtx.fillStyle = g3;
nebulaCtx.fillRect(0, 0, w, h);
if (!isMobile) {
// Wash 4: fairy-pink, upper-center
var g4 = nebulaCtx.createRadialGradient(w*0.5, h*0.15, 0, w*0.5, h*0.15, w*0.25);
g4.addColorStop(0, 'rgba(244, 114, 182, 0.04)');
g4.addColorStop(0.6, 'rgba(244, 114, 182, 0.012)');
g4.addColorStop(1, 'rgba(244, 114, 182, 0)');
nebulaCtx.fillStyle = g4;
nebulaCtx.fillRect(0, 0, w, h);
// Wash 5: warm amber, lower-center
var g5 = nebulaCtx.createRadialGradient(w*0.55, h*0.75, 0, w*0.55, h*0.75, w*0.3);
g5.addColorStop(0, 'rgba(232, 148, 58, 0.04)');
g5.addColorStop(0.5, 'rgba(232, 148, 58, 0.015)');
g5.addColorStop(1, 'rgba(232, 148, 58, 0)');
nebulaCtx.fillStyle = g5;
nebulaCtx.fillRect(0, 0, w, h);
}
}
// ── Star field (animated) ──
var starCanvas = document.getElementById('star-canvas');
var starCtx = starCanvas.getContext('2d');
var bgStars = [];
var lastFrame = 0;
var FRAME_INTERVAL = 50;
function generateBgStars() {
var w = starCanvas.width, h = starCanvas.height;
var isMobile = w < 480;
var count = isMobile ? 200 : (300 + Math.floor(Math.random() * 80));
bgStars = [];
for (var i = 0; i < count; i++) {
var color = pickStarColor();
var r = pickStarSize();
var layer = r < 0.5 ? 0 : (r < 1.0 ? (Math.random() < 0.6 ? 0 : 1) :
(r < 1.5 ? (Math.random() < 0.5 ? 1 : 2) : (Math.random() < 0.3 ? 1 : 2)));
var steady = Math.random() < 0.12;
var speed = 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() * Math.PI * 2,
speed: speed,
color: color.r + ',' + color.g + ',' + color.b,
layer: layer
});
}
}
function resizeCanvases() {
starCanvas.width = starCanvas.offsetWidth;
starCanvas.height = starCanvas.offsetHeight;
drawNebulae();
generateBgStars();
}
function drawStars(time) {
if (time - lastFrame < FRAME_INTERVAL) {
requestAnimationFrame(drawStars);
return;
}
lastFrame = time;
starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
var t = time * 0.001;
for (var i = 0; i < bgStars.length; i++) {
var s = bgStars[i];
var a = (s.speed === 0 || prefersReducedMotion)
? s.base
: s.base + Math.sin(t * s.speed + s.phase) * 0.3;
if (a < 0.05) a = 0.05;
if (a > 1) a = 1;
starCtx.beginPath();
starCtx.arc(s.x, s.y, s.r, 0, 6.2832);
starCtx.fillStyle = 'rgba(' + s.color + ',' + a + ')';
starCtx.fill();
}
requestAnimationFrame(drawStars);
}
// ── Parallax ──
var starZone = document.getElementById('star-zone');
function updateParallax() {
if (prefersReducedMotion || !inContentMode) {
starZone.style.transform = '';
return;
}
var scrollY = window.scrollY || window.pageYOffset;
var offset = Math.min(scrollY * 0.4, window.innerHeight * 0.5);
starZone.style.transform = 'translateY(-' + offset + 'px)';
}
window.addEventListener('scroll', function() {
if (inContentMode) requestAnimationFrame(updateParallax);
}, { passive: true });
// ── Star DOM nodes ──
var starElements = [];
var srAnnounce = document.getElementById('sr-announce');
STARS.forEach(function(star, i) {
var btn = document.createElement('button');
btn.className = '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 dot = document.createElement('span');
dot.className = 'star-dot';
var label = document.createElement('span');
label.className = 'star-label';
label.textContent = star.label;
btn.appendChild(dot);
btn.appendChild(label);
starZone.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');
// ── Constellation lines ──
var svg = document.getElementById('constellation-svg');
function drawConstellationLines() {
while (svg.firstChild) svg.removeChild(svg.firstChild);
var starMap = {};
STARS.forEach(function(s) { starMap[s.id] = s; });
CONSTELLATION_LINES.forEach(function(pair) {
var a = starMap[pair[0]], b = starMap[pair[1]];
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', a.x + '%');
line.setAttribute('y1', a.y + '%');
line.setAttribute('x2', b.x + '%');
line.setAttribute('y2', b.y + '%');
line.setAttribute('class', 'constellation-line');
line.setAttribute('stroke', a.accent);
line.setAttribute('stroke-opacity', '0.14');
svg.appendChild(line);
});
}
drawConstellationLines();
// ── Skyline — LUSH ──
function generateSkyline() {
var skyline = document.getElementById('skyline');
skyline.innerHTML = '';
var totalBuildings = 38;
var step = Math.floor(totalBuildings / STARS.length);
// Each billboard a different color — polychromatic city
var bbColors = ['bb-teal', 'bb-coral', 'bb-orchid', 'bb-green', 'bb-gold', 'bb-water', 'bb-pink', 'bb-amber'];
var billboardAssign = {};
STARS.forEach(function(s, i) {
billboardAssign[2 + i * step] = { star: s, colorClass: bbColors[i] };
});
for (var i = 0; i < totalBuildings; i++) {
var div = document.createElement('div');
div.className = 'building';
var w = 12 + Math.random() * 48;
var h = 20 + Math.random() * 260;
div.style.width = w + 'px';
div.style.height = h + 'px';
div.style.marginLeft = (Math.random() * 2) + 'px';
// Edge-light on ~30%
if (Math.random() < 0.3) div.classList.add('edge-lit');
// Bioluminescent stripes on ~35% — mint or orchid
if (Math.random() < 0.35 && h > 60) {
var stripe = document.createElement('span');
stripe.className = 'bio-stripe' + (Math.random() < 0.6 ? '' : ' orchid');
var sTop = 10 + Math.random() * 30;
var sH = 12 + Math.random() * 25;
stripe.style.cssText = 'top:' + sTop + '%;height:' + sH + '%;left:0;';
div.appendChild(stripe);
}
// Polychromatic windows
if (Math.random() > 0.3 && h > 50) {
var windowCount = Math.floor(h / 24);
for (var j = 0; j < windowCount; j++) {
if (Math.random() > 0.5) continue;
var win = document.createElement('span');
// 50% amber, 20% teal, 15% orchid, 10% pink, 5% phosphor
var winRoll = Math.random() * 100;
var winColor;
if (winRoll < 50) winColor = '232,148,58';
else if (winRoll < 70) winColor = '42,196,179';
else if (winRoll < 85) winColor = '197,88,217';
else if (winRoll < 95) winColor = '244,114,182';
else winColor = '0,255,65';
var winAlpha = 0.04 + Math.random() * 0.06;
win.style.cssText = 'position:absolute;width:2px;height:2px;background:rgba(' + winColor + ',' + winAlpha + ');left:' +
(3 + Math.random() * (w - 6)) + 'px;top:' + (6 + j * 22 + Math.random() * 10) + 'px;';
div.appendChild(win);
}
}
if (billboardAssign[i]) {
var info = billboardAssign[i];
var bb = document.createElement('a');
bb.className = 'billboard-nav ' + info.colorClass;
bb.textContent = info.star.label;
bb.setAttribute('tabindex', '0');
bb.setAttribute('aria-label', info.star.label + ' section');
bb.href = '#';
bb.addEventListener('click', (function(s) {
return function(e) { e.preventDefault(); selectStar(STARS.indexOf(s)); };
})(info.star));
div.appendChild(bb);
}
skyline.appendChild(div);
}
}
generateSkyline();
// ── Smooth scroll ──
function smoothScrollTo(targetY, duration, callback) {
if (isScrolling) return;
isScrolling = true;
var startY = window.scrollY || window.pageYOffset;
var distance = targetY - startY;
var startTime = null;
function step(time) {
if (!startTime) startTime = time;
var progress = Math.min((time - startTime) / duration, 1);
var ease = 1 - Math.pow(1 - progress, 3);
window.scrollTo(0, startY + distance * ease);
if (progress < 1) requestAnimationFrame(step);
else { isScrolling = false; if (callback) callback(); }
}
requestAnimationFrame(step);
}
// ── Star selection & descent ──
var contentFrame = document.getElementById('content-frame');
var backBtn = document.getElementById('back-btn');
function selectStar(index) {
if (lightboxOpen || isScrolling) return;
var star = STARS[index];
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');
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:100vh;margin:0"><p style="font-size:14px">' + star.label + ' &mdash; coming soon</p></body></html>';
contentFrame.onload = function() { contentFrame.classList.add('loaded'); };
}
var contentTop = document.getElementById('content-zone').offsetTop;
smoothScrollTo(contentTop, 800);
}
function returnToStars() {
if (isScrolling) return;
inContentMode = false;
backBtn.textContent = 'stars';
srAnnounce.textContent = 'Returning to star map';
smoothScrollTo(0, 500, function() {
document.body.classList.remove('content-mode');
starZone.style.transform = '';
contentFrame.classList.remove('loaded');
contentFrame.src = 'about:blank';
contentFrame.removeAttribute('srcdoc');
starElements[currentStarIndex].focus();
});
}
backBtn.addEventListener('click', function() {
if (inContentMode) returnToStars();
else smoothScrollTo(0, 300);
});
// ── Keyboard nav ──
function findNearestStar(fromIndex, direction) {
var from = STARS[fromIndex];
var bestIndex = -1, bestScore = Infinity;
for (var i = 0; i < STARS.length; i++) {
if (i === fromIndex) continue;
var dx = STARS[i].x - from.x, dy = STARS[i].y - from.y;
var valid = false;
switch (direction) {
case 'up': valid = dy < -3; break;
case 'down': valid = dy > 3; break;
case 'left': valid = dx < -3; break;
case 'right': valid = dx > 3; break;
}
if (!valid) continue;
var dist = Math.sqrt(dx*dx + dy*dy);
var penalty = (direction === 'up' || direction === 'down') ? Math.abs(dx)*0.5 : Math.abs(dy)*0.5;
var score = dist + penalty;
if (score < bestScore) { bestScore = score; bestIndex = i; }
}
return bestIndex;
}
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;
}
var dir = null;
switch (e.key) {
case 'ArrowUp': dir = 'up'; break;
case 'ArrowDown': dir = 'down'; break;
case 'ArrowLeft': dir = 'left'; break;
case 'ArrowRight': dir = 'right'; break;
case 'Enter': case ' ':
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 ──
var overlay = document.getElementById('lightbox-overlay');
var enterBtn = document.getElementById('lightbox-enter');
function closeLightbox() {
overlay.classList.add('hidden');
lightboxOpen = false;
localStorage.setItem('sp-welcomed', 'true');
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;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resizeCanvases, 150);
});
// ── Init ──
resizeCanvases();
requestAnimationFrame(drawStars);
if (!localStorage.getItem('sp-welcomed')) showLightbox();
else starElements[currentStarIndex].focus();
})();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff