Files
singular-particular-space/CREATURE-PLAYGROUND/homepage-redesign/sonnet-drafts/integration-merged-v2.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

1012 lines
30 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<!--
Sonnet Integration Draft v2 — Merged — 2026-03-26
Merges Gemini v1 visual design with Sonnet v1 mechanics.
FROM GEMINI v1:
- Star-specific accent colors per node (Writings=blue, Videos=coral, etc.)
- Constellation lines tinted by source star color
- Lightbox double-border campfire note (::after inset)
- Deeper background (#04060b → bg-void)
- Bioluminescent teal stripes on buildings
- Button elements for star nodes (semantic improvement)
FROM SONNET v1:
- Multi-color background star field (5 color classes, 4 size classes)
- Static nebula canvas layer (drawn once, not per frame)
- Throttled RAF at 20fps, pre-computed color strings
- JS-driven smooth scroll with cubic easing
- Parallax: star zone at 0.4x scroll speed
- prefers-reduced-motion support
- aria-live star announcements, role attributes
- Debounced resize, mobile star count reduction
- Billboard nav with varied colors
- Building edge-lights, bio accents, ambient glow
- Content frame fade-in, full keyboard nav, visited tracking
NEW IN V2:
- Star nodes use per-star accent color from Gemini's palette
- Visited state changes to neon-green but retains subtle accent tint
- Constellation lines tinted per source node (from Gemini)
- Constellation pulse animation per line with staggered delays (from Sonnet v1 CSS)
- Lightbox double-border from Gemini
- bg-void (#04060b) as deepest background
- Star-zone has subtle radial gradient center-brighten (from Gemini)
- Combined nebula positioning: Gemini's amber center-low wash added
- Buildings use Gemini's darker tone (#05080f) with Sonnet's edge-lights
-->
<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;
--warm-gold: #c4a24a;
--cool-blue: #4a8bc4;
--soft-rose: #c47a8a;
--pale-teal: #7ae0d4;
}
*, *::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;
}
/* ── 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%);
}
#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.08; }
50% { stroke-opacity: 0.2; }
}
/* ── Star Nodes ── */
.star-node {
position: absolute;
z-index: 3;
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: 6px;
height: 6px;
border-radius: 50%;
transition: background 150ms ease, box-shadow 150ms ease;
}
.star-label {
font-size: 11px;
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 (from Gemini) */
.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(--neon-green); box-shadow: 0 0 5px var(--neon-green); }
.star-node[data-star="playlists"] .star-dot { background: var(--fire-amber); box-shadow: 0 0 4px var(--fire-amber); }
.star-node[data-star="watchlists"] .star-dot { background: var(--cosmic-purple);box-shadow: 0 0 4px var(--cosmic-purple); }
.star-node[data-star="toolsntoys"] .star-dot { background: var(--text-warm); box-shadow: 0 0 4px rgba(232,213,184,0.7); }
.star-node[data-star="creatorlists"] .star-dot{ background: var(--warm-gold); box-shadow: 0 0 4px var(--warm-gold); }
/* Hover/focus/current: amber campfire follows attention */
.star-node:hover .star-dot,
.star-node:focus .star-dot,
.star-node.current .star-dot {
background: var(--fire-amber);
box-shadow: 0 0 6px 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);
}
/* Hover/focus overrides visited */
.star-node.visited:hover .star-dot,
.star-node.visited:focus .star-dot,
.star-node.visited.current .star-dot {
background: var(--fire-amber);
box-shadow: 0 0 6px 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;
background: linear-gradient(to bottom, var(--bg-void) 0%, #080c14 35%, 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;
gap: 0;
}
#skyline::before {
content: '';
position: absolute;
bottom: 0; left: 0;
width: 100%;
height: 50px;
background: linear-gradient(to top, rgba(232, 148, 58, 0.04), 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.12), rgba(42, 196, 179, 0.02));
}
.building.bio-active::before {
content: '';
position: absolute;
left: 0;
width: 2px;
background: var(--neon-teal);
opacity: 0.06;
}
.billboard-nav {
position: absolute;
top: -2px;
left: 50%;
transform: translate(-50%, -100%);
padding: 3px 8px;
font-size: 10px;
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.2); }
.billboard-nav.bb-amber { color: var(--fire-amber); border-color: rgba(232, 148, 58, 0.2); }
.billboard-nav.bb-green { color: var(--neon-green); border-color: rgba(50, 220, 140, 0.2); }
.billboard-nav.bb-coral { color: var(--fire-coral); border-color: rgba(212, 101, 74, 0.2); }
.billboard-nav:hover,
.billboard-nav:focus {
color: var(--fire-amber);
border-color: rgba(232, 148, 58, 0.4);
outline: none;
}
/* ── Zone 3: Content Alley ── */
#content-zone {
width: 100%;
min-height: 100vh;
background: var(--bg-warm);
position: relative;
}
#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: 12px;
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: 11px;
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;
}
/* ── Screen reader ── */
#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;
top: 0; left: 0;
width: 100%; height: 100%;
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;
}
/* Gemini's double-border: subtle inner frame */
#lightbox::after {
content: '';
position: absolute;
inset: 4px;
border: 1px solid rgba(232, 148, 58, 0.1);
pointer-events: none;
border-radius: 2px;
}
#lightbox p {
font-size: 15px;
line-height: 1.65;
color: var(--text-warm);
margin-bottom: 20px;
}
#lightbox .nav-hint {
font-size: 12px;
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; }
}
/* ── Responsive ── */
@media (max-width: 768px) {
.star-label { font-size: 10px; }
#lightbox { padding: 24px; max-width: 360px; }
.billboard-nav { font-size: 9px; padding: 2px 6px; }
}
@media (max-width: 480px) {
.star-dot { width: 12px; height: 12px; }
.star-node:active .star-label { opacity: 1; }
#lightbox { padding: 20px 16px; }
#lightbox p { font-size: 14px; }
.billboard-nav { font-size: 10px; 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;
// ── Star data ──
// Positions from plan spec. Accent colors from Gemini v1.
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: '#32dc8c' },
{ id: 'playlists', label: 'Playlists', x: 78, y: 48, href: '', accent: '#e8943a' },
{ id: 'watchlists', label: 'Watchlists', x: 22, y: 65, href: '', accent: '#4a1d6e' },
{ id: 'toolsntoys', label: 'ToolsnToys', x: 50, y: 72, href: '', accent: '#e8d5b8' },
{ 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']
];
// ── State ──
var currentStarIndex = 0;
var inContentMode = false;
var lightboxOpen = false;
var visited = JSON.parse(localStorage.getItem('sp-visited') || '{}');
var isScrolling = false;
// ── Background star colors ──
var STAR_COLORS = [
{ r: 200, g: 210, b: 230, weight: 50 },
{ r: 240, g: 220, b: 190, weight: 20 },
{ r: 140, g: 180, b: 230, weight: 15 },
{ r: 220, g: 190, b: 120, weight: 10 },
{ r: 210, g: 140, b: 140, 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 < 45) return 0.3 + Math.random() * 0.2;
if (roll < 75) return 0.5 + Math.random() * 0.5;
if (roll < 93) return 1.0 + Math.random() * 0.5;
return 1.5 + Math.random() * 1.0;
}
// ── Nebula canvas (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 (from Gemini positioning)
var g1 = nebulaCtx.createRadialGradient(w * 0.75, h * 0.25, 0, w * 0.75, h * 0.25, w * 0.4);
g1.addColorStop(0, 'rgba(74, 29, 110, 0.06)');
g1.addColorStop(0.5, 'rgba(74, 29, 110, 0.025)');
g1.addColorStop(1, 'rgba(74, 29, 110, 0)');
nebulaCtx.fillStyle = g1;
nebulaCtx.fillRect(0, 0, w, h);
// Wash 2: teal, lower-left
var g2 = nebulaCtx.createRadialGradient(w * 0.2, h * 0.65, 0, w * 0.2, h * 0.65, w * 0.35);
g2.addColorStop(0, 'rgba(42, 196, 179, 0.04)');
g2.addColorStop(0.5, 'rgba(42, 196, 179, 0.015)');
g2.addColorStop(1, 'rgba(42, 196, 179, 0)');
nebulaCtx.fillStyle = g2;
nebulaCtx.fillRect(0, 0, w, h);
if (!isMobile) {
// Wash 3: warm amber, center-low (from Gemini)
var g3 = nebulaCtx.createRadialGradient(w * 0.5, h * 0.7, 0, w * 0.5, h * 0.7, w * 0.35);
g3.addColorStop(0, 'rgba(232, 148, 58, 0.03)');
g3.addColorStop(0.5, 'rgba(232, 148, 58, 0.012)');
g3.addColorStop(1, 'rgba(232, 148, 58, 0)');
nebulaCtx.fillStyle = g3;
nebulaCtx.fillRect(0, 0, w, h);
}
}
// ── Star field canvas (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 ? 180 : (280 + 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.15;
var speed = steady ? 0 :
(r > 1.5 ? 0.15 + Math.random() * 0.2 :
(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.3 + Math.random() * 0.5,
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.25;
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 (tinted per source star) ──
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');
// Tint line by source star accent (from Gemini)
line.setAttribute('stroke', a.accent);
line.setAttribute('stroke-opacity', '0.14');
svg.appendChild(line);
});
}
drawConstellationLines();
// ── Skyline ──
function generateSkyline() {
var skyline = document.getElementById('skyline');
skyline.innerHTML = '';
var totalBuildings = 35;
var step = Math.floor(totalBuildings / STARS.length);
var bbColors = ['bb-teal', 'bb-coral', 'bb-teal', 'bb-green', 'bb-amber', 'bb-teal', 'bb-amber', 'bb-teal'];
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 = 14 + Math.random() * 44;
var h = 25 + Math.random() * 240;
div.style.width = w + 'px';
div.style.height = h + 'px';
div.style.marginLeft = (Math.random() * 2.5) + 'px';
// Edge-light on ~30%
if (Math.random() < 0.3) div.classList.add('edge-lit');
// Bio-active teal stripe on ~20% (from Gemini)
if (Math.random() < 0.2 && h > 80) {
div.classList.add('bio-active');
// Position the bio stripe
var bioTop = 10 + Math.random() * 30;
var bioHeight = 15 + Math.random() * 25;
div.style.setProperty('--bio-top', bioTop + '%');
// Use the ::before with dynamic positioning via inline style
var bioEl = div.querySelector('::before'); // can't query pseudo, use style
// Set via CSS custom properties on the element
div.style.cssText += ';--bio-top:' + bioTop + '%;--bio-h:' + bioHeight + '%;';
}
// Window lights — mixed amber and teal
if (Math.random() > 0.35 && h > 60) {
var windowCount = Math.floor(h / 26);
for (var j = 0; j < windowCount; j++) {
if (Math.random() > 0.4) continue; // sparse windows
var win = document.createElement('span');
var winColor = Math.random() < 0.75
? 'rgba(232,148,58,' + (0.04 + Math.random() * 0.06) + ')'
: 'rgba(42,196,179,' + (0.03 + Math.random() * 0.04) + ')';
win.style.cssText = 'position:absolute;width:2px;height:2px;background:' + winColor +
';left:' + (3 + Math.random() * (w - 6)) + 'px;top:' + (8 + j * 24 + Math.random() * 10) + 'px;';
div.appendChild(win);
}
}
// Billboard nav
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 navigation ──
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 (debounced) ──
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>