- SkyFishing: Cormorant Garamond → Estonia (display/headings), IBM Plex Mono → Share Tech Mono - Remove escape-annotated-minimalist.html (duplicate of v2); rename Escape II → Escape in nav - .gitignore: unblock Writings/life_in_alexandra.html (large but the one and only exception) - life_in_alexandra.html now tracked Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1321 lines
52 KiB
HTML
1321 lines
52 KiB
HTML
<!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‑Nomad‑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’s an adventure, bring snacks. Happy wanderings, Myster Wizzard</p>
|
||
<span class="nav-hint">click stars to explore · arrow keys to navigate · 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: '' },
|
||
{ 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/
|
||
// 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 + ' — 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>
|