1/**
2 * Selene — a lunar base for research and quiet.
3 *
4 * A cold, silent, spacious landing page for a fictional lunar-hospitality
5 * operator. Everything is procedural (canvas + inline SVG + CSS) — no images,
6 * no web fonts, no network.
7 *
8 * Motion (one rAF loop reads scroll progress of each `[data-sel-p]` wrapper and
9 * writes a `--p` CSS variable; visuals are transform/opacity only):
10 * - Hero: canvas lunar horizon in three parallax layers with true raking-light
11 * cast shadows, a procedural Earth that rises with scroll, a trail of
12 * footprints that prints itself as you scroll, and a 1/6-g cursor: the
13 * title letters drift away from the pointer on underdamped springs and the
14 * pointer kicks up regolith that arcs slowly back down.
15 * - Statement: words light up like sunrise creeping across a plain.
16 * - Crater rim: a pinned descent over a crater lip, depth/temperature counters.
17 * - Lunar day: 29.5 days scrubbed by scroll — the sun walks its arc, shadows
18 * swing and stretch, panels glow gold, Earth cycles through its phases.
19 * - Configurator: modules drop in on a slow 1/6-g ease, puff dust, link with tubes.
20 * - Earth window: a porthole with a live-rotating, phase-lit Earth.
21 * - Crew cards that float up and settle slowly; a seat-manifest booking form.
22 *
23 * Reduced motion: no pinning, no physics, no autoplay; the page renders a
24 * complete static composition. SSR-safe: window/document only inside effects.
25 */
26import { useEffect, useMemo, useRef, useState, type CSSProperties, type FormEvent } from "react";
27
28/* ------------------------------------------------------------------ */
29/* Pure helpers (module scope is deterministic: SSR == client) */
30/* ------------------------------------------------------------------ */
31function rng(seed: number) {
32 let a = seed >>> 0;
33 return () => {
34 a = (a + 0x6d2b79f5) >>> 0;
35 let t = a;
36 t = Math.imul(t ^ (t >>> 15), t | 1);
37 t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
38 return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
39 };
40}
41const clamp = (v: number, a = 0, b = 1) => Math.min(b, Math.max(a, v));
42const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
43const smooth = (t: number) => t * t * (3 - 2 * t);
44
45function h1(i: number) {
46 let h = Math.imul(i | 0, 374761393) ^ 0x9e3779b1;
47 h = Math.imul(h ^ (h >>> 13), 1274126177);
48 return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
49}
50function vn1(x: number) {
51 const i = Math.floor(x);
52 const f = x - i;
53 const s = smooth(f);
54 return h1(i) * (1 - s) + h1(i + 1) * s;
55}
56function h3(x: number, y: number, z: number) {
57 let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(z, 1274126177);
58 h = Math.imul(h ^ (h >>> 13), 1103515245);
59 return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
60}
61function vn3(x: number, y: number, z: number) {
62 const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z);
63 const fx = smooth(x - xi), fy = smooth(y - yi), fz = smooth(z - zi);
64 const c = (a: number, b: number, d: number) => h3(xi + a, yi + b, zi + d);
65 const x00 = lerp(c(0, 0, 0), c(1, 0, 0), fx), x10 = lerp(c(0, 1, 0), c(1, 1, 0), fx);
66 const x01 = lerp(c(0, 0, 1), c(1, 0, 1), fx), x11 = lerp(c(0, 1, 1), c(1, 1, 1), fx);
67 return lerp(lerp(x00, x10, fy), lerp(x01, x11, fy), fz);
68}
69function fbm3(x: number, y: number, z: number, o: number) {
70 let a = 0.5, f = 1, s = 0, n = 0;
71 for (let i = 0; i < o; i++) {
72 s += a * vn3(x * f, y * f, z * f);
73 n += a;
74 a *= 0.5;
75 f *= 2.03;
76 }
77 return s / n;
78}
79
80type RGB = [number, number, number];
81const mix = (a: RGB, b: RGB, t: number) =>
82 `rgb(${(a[0] + (b[0] - a[0]) * t) | 0},${(a[1] + (b[1] - a[1]) * t) | 0},${(a[2] + (b[2] - a[2]) * t) | 0})`;
83
84/* ------------------------------------------------------------------ */
85/* Lunar terrain: heightfield + true raking-light shadows */
86/* ------------------------------------------------------------------ */
87type TCfg = {
88 seed: number;
89 base: number; // surface line as fraction of canvas height
90 amp: number;
91 f: number;
92 rough: number;
93 craters: number;
94 rMin: number;
95 rMax: number;
96 dMin: number;
97 dMax: number;
98 lit: RGB;
99 shade: RGB;
100 tan: number; // sun elevation slope used for cast shadows
101 sl: number; // slope-facing sensitivity
102 rocks?: number;
103 speck?: number;
104};
105
106function makeSurface(cfg: TCfg) {
107 const r = rng(cfg.seed);
108 const o1 = r() * 50, o2 = r() * 50;
109 const cr = Array.from({ length: cfg.craters }, () => ({
110 x: r() * 1.1 - 0.05,
111 r: cfg.rMin + r() * (cfg.rMax - cfg.rMin),
112 d: cfg.dMin + r() * (cfg.dMax - cfg.dMin),
113 }));
114 return (u: number) => {
115 let y =
116 cfg.base +
117 (Math.sin(u * cfg.f * 6.283 + o1) * 0.5 + Math.sin(u * cfg.f * 2.3 * 6.283 + o2) * 0.3) * cfg.amp +
118 (vn1(u * cfg.f * 14 + o1) - 0.5) * cfg.rough;
119 for (const c of cr) {
120 const q = (u - c.x) / c.r;
121 const a = Math.abs(q);
122 if (a < 1) y += c.d * Math.pow(1 - q * q, 0.7);
123 if (a < 2.2) y -= c.d * 0.34 * Math.exp(-Math.pow((a - 1.04) * 3, 2));
124 }
125 return y;
126 };
127}
128
129function paintTerrain(cv: HTMLCanvasElement, cfg: TCfg) {
130 const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
131 const W = Math.max(1, Math.round(cv.clientWidth * dpr));
132 const H = Math.max(1, Math.round(cv.clientHeight * dpr));
133 cv.width = W;
134 cv.height = H;
135 const ctx = cv.getContext("2d");
136 if (!ctx) return;
137 const f = makeSurface(cfg);
138 const ys = new Float32Array(W);
139 for (let x = 0; x < W; x++) ys[x] = f(x / W) * H;
140 let M = -1e9;
141 for (let x = W - 1; x >= 0; x--) {
142 const y = ys[x];
143 const key = -y - x * cfg.tan;
144 const sh = clamp((M - key) / (7 * dpr));
145 if (key > M) M = key;
146 const xl = Math.max(0, x - 2), xr = Math.min(W - 1, x + 2);
147 const slope = (ys[xr] - ys[xl]) / (xr - xl);
148 const facing = clamp(0.5 + slope * cfg.sl);
149 const light = clamp(facing * (1 - sh * 0.85) + (h1(x) - 0.5) * 0.05);
150 const fade = (18 + 55 * Math.min(1, Math.abs(slope))) * dpr;
151 const gg = ctx.createLinearGradient(0, y, 0, y + fade);
152 gg.addColorStop(0, mix(cfg.shade, cfg.lit, 0.07 + 0.93 * light));
153 gg.addColorStop(1, mix(cfg.shade, cfg.lit, 0.3 + 0.2 * (1 - sh)));
154 ctx.fillStyle = gg;
155 ctx.fillRect(x, y, 1, fade);
156 ctx.fillStyle = mix(cfg.shade, cfg.lit, 0.3 + 0.2 * (1 - sh));
157 ctx.fillRect(x, y + fade - 1, 1, H - y - fade + 2);
158 }
159 ctx.globalCompositeOperation = "source-atop";
160 const g = ctx.createLinearGradient(0, H * cfg.base, 0, H);
161 g.addColorStop(0, "rgba(0,0,0,0)");
162 g.addColorStop(1, "rgba(4,5,7,.62)");
163 ctx.fillStyle = g;
164 ctx.fillRect(0, 0, W, H);
165 const rr = rng(cfg.seed + 7);
166 for (let i = 0; i < (cfg.speck || 0); i++) {
167 const x = Math.floor(rr() * W);
168 const y = ys[x] + 3 + rr() * (H - ys[x]);
169 ctx.fillStyle = rr() > 0.5 ? "rgba(255,255,255,.07)" : "rgba(0,0,0,.2)";
170 ctx.fillRect(x, y, dpr * (1 + rr()), dpr);
171 }
172 ctx.globalCompositeOperation = "source-over";
173 for (let i = 0; i < (cfg.rocks || 0); i++) {
174 const x = rr() * W;
175 const size = (6 + rr() * 22) * dpr * (0.6 + (ys[Math.floor(x)] / H) * 0.9);
176 const gy = ys[Math.floor(clamp(x, 0, W - 1))] + size * 0.25;
177 const L = size * (3 + rr() * 3);
178 ctx.fillStyle = "rgba(2,3,4,.55)";
179 ctx.beginPath();
180 ctx.moveTo(x, gy - size * 0.15);
181 ctx.lineTo(x - L, gy + size * 0.05);
182 ctx.lineTo(x - L * 0.9, gy + size * 0.22);
183 ctx.lineTo(x + size * 0.3, gy + size * 0.25);
184 ctx.closePath();
185 ctx.fill();
186 const rg = ctx.createLinearGradient(x - size * 0.55, 0, x + size * 0.7, 0);
187 rg.addColorStop(0, mix(cfg.shade, cfg.lit, 0.08));
188 rg.addColorStop(1, mix(cfg.shade, cfg.lit, 0.92));
189 ctx.fillStyle = rg;
190 ctx.beginPath();
191 ctx.moveTo(x - size * 0.55, gy);
192 ctx.lineTo(x - size * 0.3, gy - size * 0.7);
193 ctx.lineTo(x + size * 0.15, gy - size * 0.95);
194 ctx.lineTo(x + size * 0.6, gy - size * 0.45);
195 ctx.lineTo(x + size * 0.7, gy);
196 ctx.closePath();
197 ctx.fill();
198 }
199}
200
201function paintStars(cv: HTMLCanvasElement, seed: number, n: number) {
202 const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
203 const W = Math.max(1, Math.round(cv.clientWidth * dpr));
204 const H = Math.max(1, Math.round(cv.clientHeight * dpr));
205 cv.width = W;
206 cv.height = H;
207 const ctx = cv.getContext("2d");
208 if (!ctx) return;
209 const r = rng(seed);
210 for (let i = 0; i < n; i++) {
211 const band = r() < 0.35;
212 let x = r() * W;
213 let y = r() * H;
214 if (band) {
215 const t = r();
216 x = t * W;
217 y = H * (0.75 - t * 0.6) + (r() - 0.5) * H * 0.22;
218 }
219 const s = r();
220 const size = (s > 0.97 ? 1.8 : s > 0.85 ? 1.2 : 0.8) * dpr;
221 const a = band ? 0.18 + r() * 0.35 : 0.3 + r() * 0.7;
222 const hue = r();
223 ctx.fillStyle =
224 hue > 0.9 ? `rgba(190,215,255,${a})` : hue > 0.8 ? `rgba(255,236,205,${a})` : `rgba(236,238,239,${a})`;
225 ctx.fillRect(x, y, size, size);
226 }
227}
228
229function TerrainCanvas({ cfg, className }: { cfg: TCfg; className?: string }) {
230 const ref = useRef<HTMLCanvasElement>(null);
231 useEffect(() => {
232 const cv = ref.current;
233 if (!cv) return;
234 let t = 0;
235 const run = () => paintTerrain(cv, cfg);
236 run();
237 const ro = new ResizeObserver(() => {
238 window.clearTimeout(t);
239 t = window.setTimeout(run, 120);
240 });
241 ro.observe(cv);
242 return () => {
243 ro.disconnect();
244 window.clearTimeout(t);
245 };
246 }, [cfg]);
247 return <canvas ref={ref} className={className} aria-hidden="true" />;
248}
249
250function StarCanvas({ seed, n, className }: { seed: number; n: number; className?: string }) {
251 const ref = useRef<HTMLCanvasElement>(null);
252 useEffect(() => {
253 const cv = ref.current;
254 if (!cv) return;
255 let t = 0;
256 const run = () => paintStars(cv, seed, n);
257 run();
258 const ro = new ResizeObserver(() => {
259 window.clearTimeout(t);
260 t = window.setTimeout(run, 120);
261 });
262 ro.observe(cv);
263 return () => {
264 ro.disconnect();
265 window.clearTimeout(t);
266 };
267 }, [seed, n]);
268 return <canvas ref={ref} className={className} aria-hidden="true" />;
269}
270
271const FAR: TCfg = {
272 seed: 11, base: 0.6, amp: 0.025, f: 1.4, rough: 0.02, craters: 7, rMin: 0.02, rMax: 0.05, dMin: 0.012, dMax: 0.03,
273 lit: [128, 134, 142], shade: [38, 42, 50], tan: 0.1, sl: 1.0,
274};
275const MID: TCfg = {
276 seed: 29, base: 0.71, amp: 0.022, f: 1.1, rough: 0.018, craters: 6, rMin: 0.04, rMax: 0.09, dMin: 0.025, dMax: 0.05,
277 lit: [150, 150, 148], shade: [30, 33, 38], tan: 0.12, sl: 1.5, speck: 700,
278};
279const NEAR: TCfg = {
280 seed: 47, base: 0.82, amp: 0.018, f: 0.9, rough: 0.012, craters: 4, rMin: 0.06, rMax: 0.14, dMin: 0.04, dMax: 0.075,
281 lit: [186, 178, 164], shade: [26, 27, 30], tan: 0.15, sl: 1.6, rocks: 16, speck: 2600,
282};
283const RIM_A: TCfg = {
284 seed: 5, base: 0.4, amp: 0.07, f: 1.1, rough: 0.04, craters: 3, rMin: 0.05, rMax: 0.12, dMin: 0.02, dMax: 0.05,
285 lit: [150, 162, 178], shade: [30, 36, 46], tan: 0.08, sl: 1.0,
286};
287const RIM_B: TCfg = {
288 seed: 61, base: 0.6, amp: 0.04, f: 1.6, rough: 0.03, craters: 7, rMin: 0.03, rMax: 0.08, dMin: 0.02, dMax: 0.05,
289 lit: [96, 116, 138], shade: [18, 24, 33], tan: 0.1, sl: 1.5, speck: 900,
290};
291const RIM_C: TCfg = {
292 seed: 83, base: 0.76, amp: 0.07, f: 0.8, rough: 0.05, craters: 3, rMin: 0.08, rMax: 0.15, dMin: 0.03, dMax: 0.06,
293 lit: [182, 172, 156], shade: [24, 24, 27], tan: 0.14, sl: 1.5, rocks: 14, speck: 2200,
294};
295
296/* ------------------------------------------------------------------ */
297/* Procedural Earth (sphere-sampled from a seamless noise texture) */
298/* ------------------------------------------------------------------ */
299const TW = 384, TH = 192;
300let EARTH_TEX: { r: Uint8Array; g: Uint8Array; b: Uint8Array; c: Uint8Array } | null = null;
301function earthTex() {
302 if (EARTH_TEX) return EARTH_TEX;
303 const r = new Uint8Array(TW * TH), g = new Uint8Array(TW * TH), b = new Uint8Array(TW * TH), c = new Uint8Array(TW * TH);
304 for (let j = 0; j < TH; j++) {
305 const lat = (0.5 - (j + 0.5) / TH) * Math.PI;
306 const cl = Math.cos(lat), sl = Math.sin(lat);
307 for (let i = 0; i < TW; i++) {
308 const lon = ((i + 0.5) / TW) * Math.PI * 2;
309 const x = cl * Math.cos(lon), y = sl, z = cl * Math.sin(lon);
310 const n = fbm3(x * 2.1 + 9, y * 2.1 + 3, z * 2.1 + 5, 4);
311 const t2 = fbm3(x * 4 + 1, y * 4 + 8, z * 4 + 2, 3);
312 let R: number, G: number, B: number;
313 if (Math.abs(lat) > 1.2 + (n - 0.5) * 0.35) {
314 R = 232; G = 238; B = 244;
315 } else if (n > 0.53) {
316 const k = clamp((t2 - 0.35) / 0.3);
317 R = lerp(62, 150, k); G = lerp(112, 134, k); B = lerp(72, 92, k);
318 if (Math.abs(lat) < 0.38 && t2 > 0.6) { R = 168; G = 148; B = 96; }
319 } else {
320 const d = clamp((0.53 - n) / 0.18);
321 R = lerp(52, 12, d); G = lerp(128, 52, d); B = lerp(150, 112, d);
322 }
323 const cn = fbm3(x * 3.1 + 40, y * 3.6 + 12, z * 3.1 + 21, 4);
324 const cc = smooth(clamp((cn - 0.5) / 0.2));
325 const idx = j * TW + i;
326 r[idx] = R; g[idx] = G; b[idx] = B; c[idx] = cc * 235;
327 }
328 }
329 EARTH_TEX = { r, g, b, c };
330 return EARTH_TEX;
331}
332
333type EGeo = { S: number; ty: Uint16Array; lf: Float32Array; nx: Float32Array; nz: Float32Array; rim: Float32Array; al: Uint8Array };
334const EGEO = new Map<number, EGeo>();
335function earthGeo(S: number): EGeo {
336 const have = EGEO.get(S);
337 if (have) return have;
338 const N = S * S;
339 const geo: EGeo = {
340 S, ty: new Uint16Array(N), lf: new Float32Array(N), nx: new Float32Array(N), nz: new Float32Array(N),
341 rim: new Float32Array(N), al: new Uint8Array(N),
342 };
343 const R = S / 2;
344 for (let py = 0; py < S; py++) {
345 for (let px = 0; px < S; px++) {
346 const nx = (px + 0.5) / R - 1, ny = 1 - (py + 0.5) / R;
347 const r2 = nx * nx + ny * ny;
348 const k = py * S + px;
349 if (r2 >= 1) continue;
350 const nz = Math.sqrt(1 - r2);
351 geo.nx[k] = nx; geo.nz[k] = nz;
352 geo.lf[k] = Math.atan2(nz, nx) / (Math.PI * 2);
353 geo.ty[k] = Math.min(TH - 1, Math.floor((0.5 - Math.asin(ny) / Math.PI) * TH));
354 geo.rim[k] = Math.pow(1 - nz, 3);
355 geo.al[k] = clamp((1 - Math.sqrt(r2)) * R * 1.1, 0, 1) * 255;
356 }
357 }
358 EGEO.set(S, geo);
359 return geo;
360}
361
362/** alpha = phase angle: 0 full, PI/2 half, PI new. Lit side is on the right while alpha<PI. */
363function drawEarth(cv: HTMLCanvasElement, S: number, rot: number, alpha: number) {
364 const tex = earthTex();
365 const geo = earthGeo(S);
366 if (cv.width !== S || cv.height !== S) { cv.width = S; cv.height = S; }
367 const ctx = cv.getContext("2d");
368 if (!ctx) return;
369 const holder = cv as HTMLCanvasElement & { __img?: ImageData };
370 const img = holder.__img && holder.__img.width === S ? holder.__img : (holder.__img = ctx.createImageData(S, S));
371 const d = img.data;
372 const Lx = Math.sin(alpha), Lz = Math.cos(alpha);
373 const off = rot / (Math.PI * 2);
374 for (let k = 0; k < S * S; k++) {
375 const a = geo.al[k];
376 const o = k * 4;
377 if (a === 0) { d[o + 3] = 0; continue; }
378 let u = geo.lf[k] + off;
379 u -= Math.floor(u);
380 const ti = geo.ty[k] * TW + Math.min(TW - 1, (u * TW) | 0);
381 const cc = tex.c[ti] / 255;
382 let R = tex.r[ti], G = tex.g[ti], B = tex.b[ti];
383 R += (238 - R) * cc; G += (242 - G) * cc; B += (248 - B) * cc;
384 const dl = geo.nx[k] * Lx + geo.nz[k] * Lz;
385 const t = smooth(clamp((dl + 0.08) / 0.5));
386 const sh = 0.035 + 0.965 * t;
387 const rim = geo.rim[k] * (0.22 + 0.95 * t);
388 d[o] = Math.min(255, R * sh + rim * 120);
389 d[o + 1] = Math.min(255, G * sh + rim * 170);
390 d[o + 2] = Math.min(255, B * sh + rim * 255);
391 d[o + 3] = a;
392 }
393 ctx.putImageData(img, 0, 0);
394}
395
396/** Earth phase angle seen from the near side, given days since local sunrise. */
397const earthAlpha = (day: number) => (((7.4 + day) / 29.53) % 1) * Math.PI * 2;
398
399/* ------------------------------------------------------------------ */
400/* Content */
401/* ------------------------------------------------------------------ */
402const NAV = [
403 ["Rim", "#sel-rim"],
404 ["Lunar day", "#sel-day"],
405 ["Modules", "#sel-modules"],
406 ["Crew", "#sel-crew"],
407 ["Manifest", "#sel-manifest"],
408] as const;
409
410const PRINTS = Array.from({ length: 30 }, (_, i) => {
411 const at = (t: number) => ({ x: 20 + t * 48 + Math.sin(t * 6.2) * 6, y: 96 - t * 24 });
412 const t = i / 29;
413 const p = at(t), q = at(Math.min(1, t + 0.04));
414 const ang = (Math.atan2((q.x - p.x) * 16, -(q.y - p.y) * 9) * 180) / Math.PI;
415 const side = i % 2 ? 1 : -1;
416 const sc = 1 - t * 0.84;
417 return { i, x: p.x, y: p.y, ang, off: side * 17 * sc, sc };
418});
419
420const STATEMENT =
421 "There is no weather here. No wind to carry sound, no cloud to hide the stars. Selene is a station built for stillness, for research and for the long view — four hundred thousand kilometres from the nearest inbox.";
422
423const STEPS_RIM = [
424 { k: "01", t: "The landing rim", d: "Pads are cut into the lip of the crater, in sunlight for 89 percent of the year. Guests arrive in daylight, the descent is felt as a slow exhale.", depth: "0 m", temp: "+54 °C" },
425 { k: "02", t: "Over the lip", d: "A heated gallery runs 400 metres inside the rim, where the temperature holds near −20 °C and the Earth never leaves the window.", depth: "−140 m", temp: "−20 °C" },
426 { k: "03", t: "The cold trap", d: "On the floor, sunlight has not arrived in two billion years. Our crews cut water ice at 40 K, the quietest place we know of.", depth: "−412 m", temp: "−233 °C" },
427];
428
429const PHASES = [
430 { n: "Dawn", r: "D+00 to D+02", d: "Sunlight arrives sideways. Shadows run twenty times the height of a person and the array unfolds, panel by panel." },
431 { n: "Long day", r: "D+03 to D+12", d: "The regolith reaches +106 °C. Work happens in the shaded galleries; the greenhouse is fed by light piped down from the mast." },
432 { n: "Dusk", r: "D+13 to D+15", d: "The sun slides along the horizon for two days. Everyone gathers at the west window for the last light." },
433 { n: "Long night", r: "D+16 to D+29", d: "−173 °C and the stars all day. Earth hangs full overhead, about forty-three times brighter than a full Moon over your head at home." },
434];
435
436type ModKey = "hab" | "lab" | "gh" | "mast";
437const MODS: { k: ModKey; name: string; x: number; line: string; beds: number; racks: number; food: number; kw: number }[] = [
438 { k: "hab", name: "Habitat", x: 170, line: "Four private berths, a galley and the long window.", beds: 4, racks: 0, food: 0, kw: -6 },
439 { k: "lab", name: "Laboratory", x: 345, line: "Eight instrument racks, a glovebox and a regolith bench.", beds: 0, racks: 8, food: 0, kw: -11 },
440 { k: "gh", name: "Greenhouse", x: 535, line: "Forty kilos of fresh greens a week under piped sunlight.", beds: 0, racks: 0, food: 40, kw: -9 },
441 { k: "mast", name: "Solar mast", x: 715, line: "A 26 kW gold-film array on a self-tracking mast.", beds: 0, racks: 0, food: 0, kw: 26 },
442];
443
444const CREW = [
445 { n: "Dr. Imani Okafor", r: "Station director, geochemist", q: "People think the quiet is empty. It is the opposite: you can hear the whole station think.", s: "Moonwalks 63", tilt: -14 },
446 { n: "Jonas Reiter", r: "Hospitality and life support", q: "I set the table for guests four hundred thousand kilometres from the nearest restaurant. Nobody has complained.", s: "Moonwalks 22", tilt: 8 },
447 { n: "Ayaka Mori", r: "Greenhouse lead", q: "The first tomato of the month always goes to whoever has been looking at Earth the longest.", s: "Moonwalks 31", tilt: -4 },
448 { n: "Cpt. Luis Ferreira", r: "Surface operations", q: "Walking is a kind of slow flying here. You learn to land softly and to take your time.", s: "Moonwalks 118", tilt: 16 },
449];
450
451const FLIGHTS = [
452 { id: "SEL-2801", date: "08 MAR 2028", from: "Cislunar Gate A", transit: "3 d 06 h" },
453 { id: "SEL-2802", date: "21 APR 2028", from: "Cislunar Gate A", transit: "3 d 04 h" },
454 { id: "SEL-2803", date: "02 JUN 2028", from: "Cislunar Gate B", transit: "3 d 08 h" },
455];
456const STAYS = [
457 { d: 7, name: "Seven days", note: "A short, bright week", price: 2400000 },
458 { d: 14, name: "One lunar day", note: "Sunrise to sunset", price: 3900000 },
459 { d: 29, name: "A full lunation", note: "Day, dusk and the long night", price: 6800000 },
460];
461const ROWS = [1, 2, 3, 4];
462const COLS = ["A", "B", "C", "D"];
463const TAKEN = new Set(["1B", "2A", "2D", "3C", "4A"]);
464const eur = (n: number) => "€" + new Intl.NumberFormat("en-GB").format(n);
465
466/* ------------------------------------------------------------------ */
467/* Small components */
468/* ------------------------------------------------------------------ */
469function StationClock() {
470 const [t, setT] = useState("--:--:--");
471 useEffect(() => {
472 const f = () => setT(new Date().toISOString().slice(11, 19));
473 f();
474 const id = window.setInterval(f, 1000);
475 return () => window.clearInterval(id);
476 }, []);
477 return <span>{t}</span>;
478}
479
480function Visor({ tilt, id }: { tilt: number; id: number }) {
481 return (
482 <svg viewBox="0 0 160 160" className="sel-visor" aria-hidden="true">
483 <defs>
484 <radialGradient id={`sv-h${id}`} cx="35%" cy="28%" r="80%">
485 <stop offset="0" stopColor="#f4f5f6" />
486 <stop offset=".6" stopColor="#b9bdc2" />
487 <stop offset="1" stopColor="#5b6066" />
488 </radialGradient>
489 <linearGradient id={`sv-v${id}`} x1="0" y1="0" x2="1" y2="1">
490 <stop offset="0" stopColor="#1b222b" />
491 <stop offset="1" stopColor="#05070a" />
492 </linearGradient>
493 <radialGradient id={`sv-e${id}`} cx="40%" cy="35%" r="65%">
494 <stop offset="0" stopColor="#8ec3e8" />
495 <stop offset=".5" stopColor="#3f7fb8" />
496 <stop offset="1" stopColor="#1f5c63" />
497 </radialGradient>
498 <clipPath id={`sv-c${id}`}><ellipse cx="80" cy="82" rx="44" ry="36" /></clipPath>
499 </defs>
500 <circle cx="80" cy="80" r="62" fill={`url(#sv-h${id})`} />
501 <ellipse cx="80" cy="82" rx="44" ry="36" fill={`url(#sv-v${id})`} />
502 <g clipPath={`url(#sv-c${id})`}>
503 <g transform={`rotate(${tilt} 80 82)`}>
504 <circle cx="102" cy="68" r="15" fill={`url(#sv-e${id})`} opacity=".92" />
505 <path d="M92 66q6-5 12 0q3 4 8 2" stroke="#e9f1f5" strokeWidth="2" fill="none" opacity=".7" />
506 </g>
507 <ellipse cx="62" cy="68" rx="14" ry="5" fill="#fff" opacity=".12" transform="rotate(-20 62 68)" />
508 </g>
509 <path d="M26 104q54 40 108 0" stroke="#e3b24a" strokeWidth="2" fill="none" opacity=".9" />
510 </svg>
511 );
512}
513
514/* ------------------------------------------------------------------ */
515/* Page */
516/* ------------------------------------------------------------------ */
517export function Page() {
518 const rootRef = useRef<HTMLDivElement>(null);
519 const navRef = useRef<HTMLElement>(null);
520 const heroEarth = useRef<HTMLCanvasElement>(null);
521 const cycEarth = useRef<HTMLCanvasElement>(null);
522 const cfgEarth = useRef<HTMLCanvasElement>(null);
523 const cfgScene = useRef<HTMLCanvasElement>(null);
524 const winEarth = useRef<HTMLCanvasElement>(null);
525 const winBox = useRef<HTMLDivElement>(null);
526 const dayRef = useRef(7);
527 const [reduced, setReduced] = useState(false);
528 const [mods, setMods] = useState<Record<ModKey, boolean>>({ hab: true, lab: false, gh: false, mast: true });
529 const [winDay, setWinDay] = useState(19);
530 const [phaseIdx, setPhaseIdx] = useState(0);
531 const [rimIdx, setRimIdx] = useState(0);
532 const [flight, setFlight] = useState(0);
533 const [stay, setStay] = useState(1);
534 const [seat, setSeat] = useState<string | null>("2C");
535 const [name, setName] = useState("");
536 const [cleared, setCleared] = useState(false);
537
538 const words = useMemo(() => STATEMENT.split(" "), []);
539 const stars = useMemo(() => {
540 const r = rng(77);
541 return Array.from({ length: 90 }, () => ({ x: r() * 800, y: r() * 300, s: 0.5 + r() * 1.2, o: 0.3 + r() * 0.7 }));
542 }, []);
543 const ridge = useMemo(() => {
544 const pts: string[] = [];
545 for (let x = 0; x <= 800; x += 16) pts.push(`${x},${(322 - vn1(x / 90 + 3) * 22 - Math.sin(x / 130) * 5).toFixed(1)}`);
546 return `M0,346 L${pts.join(" L")} L800,346 Z`;
547 }, []);
548 const cfgStars = useMemo(() => {
549 const r = rng(9);
550 return Array.from({ length: 70 }, () => ({ x: r() * 800, y: r() * 250, s: 0.5 + r() * 1.1 }));
551 }, []);
552 const rimGlints = useMemo(() => {
553 const fa = makeSurface(RIM_A), fb = makeSurface(RIM_B);
554 return {
555 gold: [0.62, 0.655, 0.69].map((u) => ({ l: u * 100, t: fa(u) * 100 - 0.9 })),
556 ice: [0.18, 0.27, 0.36, 0.55, 0.8].map((u, i) => ({ l: u * 100, t: fb(u) * 100 + 4 + (i % 3) * 4 })),
557 };
558 }, []);
559 const outpostTop = useMemo(() => makeSurface(MID)(0.7) * 100, []);
560
561 /* ---- reduced motion flag ---- */
562 useEffect(() => {
563 const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
564 setReduced(mq.matches);
565 const on = () => setReduced(mq.matches);
566 mq.addEventListener("change", on);
567 return () => mq.removeEventListener("change", on);
568 }, []);
569
570 /* ---- Earths that are drawn once ---- */
571 useEffect(() => {
572 if (heroEarth.current) drawEarth(heroEarth.current, 360, 2.3, 0.7);
573 }, []);
574
575 /* ---- scroll engine ---- */
576 useEffect(() => {
577 const root = rootRef.current;
578 if (!root) return;
579 const q = (s: string) => root.querySelector<HTMLElement>(`[data-sel-out="${s}"]`);
580 const pins = Array.from(root.querySelectorAll<HTMLElement>("[data-sel-p]"));
581 const o = {
582 elev: q("elev"), rise: q("rise"),
583 depth: q("depth"), rtemp: q("rtemp"),
584 day: q("day"), sun: q("sun"), temp: q("temp"), kw: q("kw"), kwbar: q("kwbar"), ephase: q("ephase"), elevd: q("elevd"),
585 fill: q("fill"), thumb: q("thumb"),
586 };
587 const sunG = root.querySelector<SVGGElement>('[data-sel-svg="sun"]');
588 const sh1 = root.querySelector<SVGPathElement>('[data-sel-svg="sh1"]');
589 const sh2 = root.querySelector<SVGPathElement>('[data-sel-svg="sh2"]');
590 const night = root.querySelector<SVGRectElement>('[data-sel-svg="night"]');
591 const panels = root.querySelector<SVGGElement>('[data-sel-svg="panels"]');
592 const last: Record<string, number> = {};
593 let lastDay = -1, lastPhase = -1, lastRim = -1;
594 let raf = 0;
595
596 const cycle = (p: number) => {
597 const day = p * 29.5;
598 const isDay = day < 14.75;
599 const a = isDay ? day / 14.75 : 0;
600 const phi = Math.PI * a;
601 const el = isDay ? Math.sin(phi) : 0;
602 const elevAng = isDay ? (Math.PI / 2 - Math.abs(phi - Math.PI / 2)) : 0;
603 if (sunG) {
604 sunG.style.transform = `translate(${400 - Math.cos(phi) * 340}px, ${340 - Math.sin(phi) * 250}px)`;
605 sunG.style.opacity = isDay ? "1" : "0";
606 }
607 const dir = Math.cos(phi) > 0 ? 1 : -1;
608 const cot = 1 / Math.max(0.05, Math.tan(Math.max(0.03, elevAng)));
609 if (sh1) {
610 sh1.setAttribute("transform", `translate(250 346) scale(${dir * clamp(58 * cot, 6, 380)} 1)`);
611 sh1.style.opacity = isDay ? String(clamp(el * 6, 0, 0.85)) : "0";
612 }
613 if (sh2) {
614 sh2.setAttribute("transform", `translate(600 346) scale(${dir * clamp(104 * cot, 6, 420)} 1)`);
615 sh2.style.opacity = isDay ? String(clamp(el * 6, 0, 0.8)) : "0";
616 }
617 const bright = isDay ? 0.3 + 0.7 * Math.pow(el, 0.6) : 0.09;
618 if (night) night.style.opacity = String((1 - bright) * 0.9);
619 if (panels) panels.style.opacity = String(isDay ? 0.25 + 0.75 * el : 0.12);
620 const T = isDay ? -140 + 246 * Math.pow(Math.max(el, 0), 0.45) : -140 - 33 * smooth((day - 14.75) / 14.75);
621 const kw = isDay ? 48 * Math.pow(el, 0.8) : 0;
622 const dI = Math.min(29, Math.floor(day));
623 if (o.day) o.day.textContent = "D+" + String(dI).padStart(2, "0");
624 if (o.sun) o.sun.textContent = isDay ? "Rising" : "Below horizon";
625 if (o.elevd) o.elevd.textContent = ((elevAng * 180) / Math.PI).toFixed(0) + "°";
626 if (o.temp) o.temp.textContent = (T > 0 ? "+" : "−") + Math.abs(T).toFixed(0) + " °C";
627 if (o.kw) o.kw.textContent = kw.toFixed(1) + " kW";
628 if (o.kwbar) o.kwbar.style.transform = `scaleX(${(kw / 48).toFixed(3)})`;
629 if (o.fill) o.fill.style.transform = `scaleX(${p.toFixed(4)})`;
630 if (o.thumb) o.thumb.style.transform = `translateX(${(p * 100).toFixed(2)}cqw)`;
631 const al = earthAlpha(day);
632 if (o.ephase) o.ephase.textContent = ((1 + Math.cos(al)) * 50).toFixed(0) + "% lit";
633 if (dI !== lastDay) {
634 lastDay = dI;
635 dayRef.current = dI;
636 if (cycEarth.current) drawEarth(cycEarth.current, 160, 1.6 + dI * 0.2, al);
637 }
638 const ph = day < 2.5 ? 0 : day < 12.3 ? 1 : day < 15.6 ? 2 : 3;
639 if (ph !== lastPhase) { lastPhase = ph; setPhaseIdx(ph); }
640 };
641
642 const frame = () => {
643 raf = 0;
644 const vh = window.innerHeight;
645 const y = window.scrollY;
646 if (navRef.current) navRef.current.classList.toggle("sel-solid", y > 40);
647 for (const el of pins) {
648 const id = el.dataset.selP || "";
649 let p = 0;
650 if (reducedRef.current) p = id === "hero" ? 0.72 : id === "cycle" ? 0.26 : id === "rim" ? 0.5 : 1;
651 else {
652 const r = el.getBoundingClientRect();
653 p = clamp(-r.top / Math.max(1, r.height - vh));
654 }
655 if (last[id] !== undefined && Math.abs(last[id] - p) < 0.0004) continue;
656 last[id] = p;
657 el.style.setProperty("--p", p.toFixed(4));
658 if (id === "hero") {
659 if (o.elev) o.elev.textContent = (1.2 + p * 17.5).toFixed(1) + "°";
660 if (o.rise) o.rise.textContent = Math.round(p * 100) + "%";
661 } else if (id === "rim") {
662 const k = Math.min(2, Math.floor(p * 3));
663 if (k !== lastRim) { lastRim = k; setRimIdx(k); }
664 if (o.depth) o.depth.textContent = "−" + Math.round(p * 412) + " m";
665 if (o.rtemp) o.rtemp.textContent = (p < 0.2 ? "+" + Math.round(54 - p * 370) : "−" + Math.round(p * 233)) + " °C";
666 } else if (id === "cycle") cycle(p);
667 }
668 };
669 const reducedRef = { current: reduced };
670 const kick = () => {
671 if (!raf) raf = requestAnimationFrame(frame);
672 };
673 window.addEventListener("scroll", kick, { passive: true });
674 window.addEventListener("resize", kick);
675 kick();
676 return () => {
677 window.removeEventListener("scroll", kick);
678 window.removeEventListener("resize", kick);
679 if (raf) cancelAnimationFrame(raf);
680 };
681 }, [reduced]);
682
683 /* ---- reveal on view ---- */
684 useEffect(() => {
685 const root = rootRef.current;
686 if (!root) return;
687 root.classList.add("sel-js");
688 const els = Array.from(root.querySelectorAll<HTMLElement>(".sel-rv"));
689 const io = new IntersectionObserver(
690 (es) => es.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } }),
691 { threshold: 0.12 },
692 );
693 els.forEach((e) => io.observe(e));
694 return () => io.disconnect();
695 }, []);
696
697 /* ---- 1/6 g hero: floaty letters + ballistic regolith dust ---- */
698 useEffect(() => {
699 const root = rootRef.current;
700 if (!root || reduced) return;
701 const stick = root.querySelector<HTMLElement>(".sel-hero .sel-stick");
702 const dust = root.querySelector<HTMLCanvasElement>(".sel-dust");
703 if (!stick || !dust) return;
704 const dctx = dust.getContext("2d");
705 const els = Array.from(stick.querySelectorAll<HTMLElement>("[data-sel-float]"));
706 const S = els.map(() => ({ x: 0, y: 0, vx: 0, vy: 0, r: 0, vr: 0 }));
707 const home = els.map(() => ({ x: 0, y: 0 }));
708 let parts: { x: number; y: number; vx: number; vy: number; l: number; s: number }[] = [];
709 let raf = 0, last = 0, lx = -1, ly = -1;
710 const size = () => {
711 const r = stick.getBoundingClientRect();
712 dust.width = Math.round(r.width);
713 dust.height = Math.round(r.height);
714 };
715 size();
716 const measure = () =>
717 els.forEach((el, i) => {
718 const r = el.getBoundingClientRect();
719 home[i].x = r.left + r.width / 2 - S[i].x;
720 home[i].y = r.top + r.height / 2 - S[i].y;
721 });
722 const spawn = (x: number, y: number, n: number, vx: number, vy: number) => {
723 for (let i = 0; i < n; i++)
724 parts.push({ x, y, vx: vx * 6 + (Math.random() - 0.5) * 90, vy: -(40 + Math.random() * 110) - Math.abs(vy) * 3, l: 1, s: 0.8 + Math.random() * 1.8 });
725 if (parts.length > 220) parts = parts.slice(-220);
726 };
727 const tick = (now: number) => {
728 raf = 0;
729 const dt = Math.min(0.033, (now - last) / 1000 || 0.016);
730 last = now;
731 let moving = false;
732 els.forEach((el, i) => {
733 const s = S[i];
734 const ax = -7 * s.x - 1.3 * s.vx;
735 const ay = -7 * s.y - 1.3 * s.vy;
736 const ar = -8 * s.r - 1.4 * s.vr;
737 s.vx += ax * dt; s.vy += ay * dt; s.vr += ar * dt;
738 s.x += s.vx * dt; s.y += s.vy * dt; s.r += s.vr * dt;
739 if (Math.abs(s.x) + Math.abs(s.y) + Math.abs(s.r) + Math.abs(s.vx) + Math.abs(s.vy) > 0.15) {
740 moving = true;
741 el.style.transform = `translate3d(${s.x.toFixed(2)}px,${s.y.toFixed(2)}px,0) rotate(${s.r.toFixed(2)}deg)`;
742 } else if (s.x !== 0 || s.y !== 0 || s.r !== 0) {
743 s.x = s.y = s.r = s.vx = s.vy = s.vr = 0;
744 el.style.transform = "";
745 }
746 });
747 if (dctx) {
748 dctx.clearRect(0, 0, dust.width, dust.height);
749 parts = parts.filter((p) => p.l > 0);
750 for (const p of parts) {
751 p.vy += 150 * dt;
752 p.x += p.vx * dt; p.y += p.vy * dt; p.l -= dt * 0.38;
753 dctx.fillStyle = `rgba(206,200,190,${Math.max(0, p.l) * 0.7})`;
754 dctx.fillRect(p.x, p.y, p.s, p.s);
755 }
756 if (parts.length) moving = true;
757 }
758 if (moving) raf = requestAnimationFrame(tick);
759 };
760 const wake = () => {
761 if (!raf) { last = performance.now(); raf = requestAnimationFrame(tick); }
762 };
763 const onMove = (e: PointerEvent) => {
764 if (lx < 0) { lx = e.clientX; ly = e.clientY; }
765 const dx = e.clientX - lx, dy = e.clientY - ly;
766 lx = e.clientX; ly = e.clientY;
767 const sp = Math.min(60, Math.hypot(dx, dy));
768 if (sp < 1) return;
769 measure();
770 els.forEach((_, i) => {
771 const hx = home[i].x + S[i].x - e.clientX, hy = home[i].y + S[i].y - e.clientY;
772 const dist = Math.hypot(hx, hy) || 1;
773 if (dist > 180) return;
774 const k = Math.pow(1 - dist / 180, 2) * sp;
775 S[i].vx += (hx / dist) * k * 4;
776 S[i].vy += (hy / dist) * k * 4 - k * 2.4;
777 S[i].vr += (hx > 0 ? 1 : -1) * k * 1.2;
778 });
779 const r = stick.getBoundingClientRect();
780 if (e.clientY - r.top > r.height * 0.6) spawn(e.clientX - r.left, e.clientY - r.top, Math.min(4, 1 + ((sp / 10) | 0)), dx * 0.2, dy);
781 wake();
782 };
783 const onDown = (e: PointerEvent) => {
784 const r = stick.getBoundingClientRect();
785 spawn(e.clientX - r.left, e.clientY - r.top, 26, 0, 4);
786 wake();
787 };
788 const onLeave = () => { lx = -1; };
789 stick.addEventListener("pointermove", onMove);
790 stick.addEventListener("pointerdown", onDown);
791 stick.addEventListener("pointerleave", onLeave);
792 window.addEventListener("resize", size);
793 return () => {
794 stick.removeEventListener("pointermove", onMove);
795 stick.removeEventListener("pointerdown", onDown);
796 stick.removeEventListener("pointerleave", onLeave);
797 window.removeEventListener("resize", size);
798 if (raf) cancelAnimationFrame(raf);
799 els.forEach((el) => (el.style.transform = ""));
800 };
801 }, [reduced]);
802
803 /* ---- configurator Earth sprite ---- */
804 useEffect(() => {
805 if (cfgEarth.current) drawEarth(cfgEarth.current, 140, 4.1, 0.5);
806 }, []);
807
808 /* ---- Earth window: rotating, phase-lit ---- */
809 const winDayRef = useRef(19);
810 useEffect(() => {
811 winDayRef.current = winDay;
812 if (reduced && winEarth.current) drawEarth(winEarth.current, 300, 1.2, earthAlpha(winDay));
813 }, [winDay, reduced]);
814 useEffect(() => {
815 const cv = winEarth.current;
816 const box = winBox.current;
817 if (!cv || !box) return;
818 drawEarth(cv, 300, 1.2, earthAlpha(winDayRef.current));
819 if (reduced) return;
820 let vis = false, raf = 0, rot = 1.2, last = 0, acc = 0;
821 const io = new IntersectionObserver((es) => {
822 vis = es[0].isIntersecting;
823 if (vis && !raf) { last = performance.now(); raf = requestAnimationFrame(loop); }
824 });
825 io.observe(box);
826 function loop(now: number) {
827 raf = 0;
828 if (!vis || !cv) return;
829 const dt = (now - last) / 1000;
830 last = now;
831 acc += dt;
832 rot += dt * 0.35;
833 if (acc > 0.05) { acc = 0; drawEarth(cv, 300, rot, earthAlpha(winDayRef.current)); }
834 raf = requestAnimationFrame(loop);
835 }
836 return () => { io.disconnect(); if (raf) cancelAnimationFrame(raf); };
837 }, [reduced]);
838
839 /* ---- derived: configurator totals, booking ---- */
840 const tot = MODS.reduce(
841 (a, m) => (mods[m.k] ? { beds: a.beds + m.beds, racks: a.racks + m.racks, food: a.food + m.food, kw: a.kw + m.kw } : a),
842 { beds: 0, racks: 0, food: 0, kw: 0 },
843 );
844 const supply = mods.mast ? 26 : 0;
845 const demand = MODS.reduce((a, m) => (mods[m.k] && m.kw < 0 ? a - m.kw : a), 0);
846 const present = MODS.filter((m) => mods[m.k]);
847 const toggle = (k: ModKey) => setMods((s) => ({ ...s, [k]: !s[k] }));
848 const preset = (p: "retreat" | "research" | "station") =>
849 setMods(p === "retreat" ? { hab: true, lab: false, gh: true, mast: true } : p === "research" ? { hab: true, lab: true, gh: false, mast: true } : { hab: true, lab: true, gh: true, mast: true });
850
851 const fl = FLIGHTS[flight], st = STAYS[stay];
852 const seatPrice = seat && (seat.endsWith("A") || seat.endsWith("D")) ? 180000 : 0;
853 const total = st.price + seatPrice;
854 const ref = useMemo(() => {
855 let h = 7;
856 for (const ch of name + fl.id + (seat || "")) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
857 return "M-" + h.toString(36).toUpperCase().padStart(6, "0").slice(0, 6);
858 }, [name, fl.id, seat]);
859 const submit = (e: FormEvent) => {
860 e.preventDefault();
861 if (name.trim() && seat) setCleared(true);
862 };
863
864 return (
865 <div ref={rootRef} className={"sel-root" + (reduced ? " sel-rm" : "")}>
866 <style>{CSS}</style>
867
868 {/* nav */}
869 <nav ref={navRef} className="sel-nav" aria-label="Primary">
870 <a href="#sel-top" className="sel-brand">
871 <svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true">
872 <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="1" />
873 <path d="M12 2a10 10 0 0 0 0 20a6.5 10 0 0 1 0-20z" fill="currentColor" />
874 </svg>
875 Selene
876 </a>
877 <div className="sel-links">
878 {NAV.map(([l, h]) => (
879 <a key={h} href={h}>{l}</a>
880 ))}
881 </div>
882 <a className="sel-btn sel-btn-sm" href="#sel-manifest">Reserve a seat</a>
883 </nav>
884
885 {/* ============ HERO ============ */}
886 <header id="sel-top" className="sel-hero sel-pin" data-sel-p="hero">
887 <div className="sel-stick">
888 <StarCanvas seed={3} n={520} className="sel-fill" />
889 <div className="sel-earthwrap">
890 <div className="sel-earthglow" />
891 <canvas ref={heroEarth} className="sel-earth" aria-hidden="true" />
892 </div>
893 <div className="sel-layer sel-l-far"><TerrainCanvas cfg={FAR} className="sel-fill" /></div>
894 <div className="sel-layer sel-l-mid">
895 <TerrainCanvas cfg={MID} className="sel-fill" />
896 <svg className="sel-outpost" style={{ top: `calc(${outpostTop}% - 34px)` }} viewBox="0 0 84 40" aria-hidden="true">
897 <path d="M6 38 A18 18 0 0 1 42 38 Z" fill="#cfd3d6" />
898 <path d="M24 38 A18 18 0 0 1 42 38 Z" fill="#f3f5f6" />
899 <rect x="46" y="26" width="2" height="12" fill="#9aa0a6" />
900 <polygon points="47,14 78,8 80,22 49,28" fill="#e3b24a" />
901 <polygon points="47,14 78,8 79,12 48,18" fill="#fbe29a" />
902 <path d="M0 39 L-30 41 L-30 42 L84 40 Z" fill="rgba(0,0,0,.5)" />
903 </svg>
904 </div>
905 <div className="sel-layer sel-l-near">
906 <TerrainCanvas cfg={NEAR} className="sel-fill" />
907 <div className="sel-prints" aria-hidden="true">
908 {PRINTS.map((p) => (
909 <i
910 key={p.i}
911 className="sel-print"
912 style={{
913 left: `calc(${p.x}% + ${p.off.toFixed(1)}px)`,
914 top: `${p.y}%`,
915 "--i": p.i,
916 transform: `translate(-50%,-50%) rotate(${p.ang.toFixed(1)}deg) scale(${p.sc.toFixed(2)}, ${(p.sc * 0.78).toFixed(2)})`,
917 } as CSSProperties}
918 />
919 ))}
920 </div>
921 </div>
922 <canvas className="sel-dust" aria-hidden="true" />
923
924 <div className="sel-hero-copy">
925 <p className="sel-eyebrow">Lunar hospitality and research · Sinus Medii, near side</p>
926 <h1 className="sel-title" aria-label="Selene">
927 {"SELENE".split("").map((c, i) => (
928 <span key={i} className="sel-letter" data-sel-float aria-hidden="true">{c}</span>
929 ))}
930 </h1>
931 <p className="sel-sub">
932 A quiet place, 384,400 kilometres from anywhere. Stay a week, a lunar day, or the whole lunation.
933 </p>
934 <div className="sel-cta">
935 <a className="sel-btn" href="#sel-manifest" data-sel-float>Reserve a seat</a>
936 <a className="sel-ghost" href="#sel-rim">Descend to the rim</a>
937 </div>
938 </div>
939
940 <div className="sel-rise" aria-hidden="true">Earthrise, 06:12 station time. The planet climbs {` `}
941 <b data-sel-out="rise">0%</b> of the way.</div>
942
943 <dl className="sel-hud" aria-label="Station readout">
944 <div><dt>Station time (UTC)</dt><dd><StationClock /></dd></div>
945 <div><dt>Earth elevation</dt><dd data-sel-out="elev">1.2°</dd></div>
946 <div><dt>Gravity</dt><dd>0.166 g</dd></div>
947 <div><dt>Light delay</dt><dd>1.28 s</dd></div>
948 </dl>
949 <div className="sel-cue" aria-hidden="true"><span>Scroll</span><i /></div>
950 </div>
951 </header>
952
953 {/* ============ STATEMENT ============ */}
954 <section className="sel-statement sel-pin" data-sel-p="statement" aria-label="Why Selene">
955 <div className="sel-stick sel-center">
956 <p className="sel-big">
957 {words.map((w, i) => (
958 <span key={i} className="sel-word" style={{ "--w": (i / words.length).toFixed(3) } as CSSProperties}>{w} </span>
959 ))}
960 </p>
961 <div className="sel-stats">
962 {[
963 ["1.28 s", "light delay to Earth"],
964 ["0.166 g", "six times lighter on your feet"],
965 ["14 days", "of unbroken sunlight"],
966 ["0 m/s", "of wind, ever"],
967 ].map(([a, b]) => (
968 <div key={a} className="sel-stat"><b>{a}</b><span>{b}</span></div>
969 ))}
970 </div>
971 </div>
972 </section>
973
974 {/* ============ CRATER RIM ============ */}
975 <section id="sel-rim" className="sel-rim sel-pin" data-sel-p="rim" aria-label="Crater rim descent">
976 <div className="sel-stick">
977 <StarCanvas seed={21} n={380} className="sel-fill" />
978 <div className="sel-rl sel-rl-a">
979 <TerrainCanvas cfg={RIM_A} className="sel-fill" />
980 {rimGlints.gold.map((g, i) => (
981 <i key={i} className="sel-gold-glint" style={{ left: `${g.l}%`, top: `${g.t}%` }} />
982 ))}
983 </div>
984 <div className="sel-rl sel-rl-b">
985 <TerrainCanvas cfg={RIM_B} className="sel-fill" />
986 {rimGlints.ice.map((g, i) => (
987 <i key={i} className="sel-ice-glint" style={{ left: `${g.l}%`, top: `${g.t}%` }} />
988 ))}
989 </div>
990 <div className="sel-rl sel-rl-c"><TerrainCanvas cfg={RIM_C} className="sel-fill" /></div>
991 <div className="sel-scrim" />
992 <div className="sel-rim-head">
993 <p className="sel-eyebrow">01 · The rim</p>
994 <h2 className="sel-h2">Arrive over a crater lip.<br />Descend into silence.</h2>
995 </div>
996 <ol className="sel-steps">
997 {STEPS_RIM.map((s, i) => (
998 <li key={s.k} className={"sel-step" + (rimIdx === i ? " on" : "")}>
999 <span className="sel-k">{s.k}</span>
1000 <div><h3>{s.t}</h3><p>{s.d}</p></div>
1001 </li>
1002 ))}
1003 </ol>
1004 <dl className="sel-hud sel-hud-r" aria-label="Descent readout">
1005 <div><dt>Depth</dt><dd data-sel-out="depth">−0 m</dd></div>
1006 <div><dt>Surface</dt><dd data-sel-out="rtemp">+54 °C</dd></div>
1007 </dl>
1008 </div>
1009 </section>
1010
1011 {/* ============ LUNAR DAY ============ */}
1012 <section id="sel-day" className="sel-cycle sel-pin" data-sel-p="cycle" aria-label="The lunar day">
1013 <div className="sel-stick sel-cyc-stick">
1014 <div className="sel-cyc-head">
1015 <p className="sel-eyebrow">02 · The lunar day</p>
1016 <h2 className="sel-h2">Fourteen days of light.<br />Fourteen of stars.</h2>
1017 </div>
1018 <div className="sel-cyc-grid">
1019 <div className="sel-scene">
1020 <svg viewBox="0 0 800 450" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Sun crossing a black lunar sky above the station, shadows swinging with it">
1021 <defs>
1022 <linearGradient id="sel-gnd" x1="0" y1="0" x2="0" y2="1">
1023 <stop offset="0" stopColor="#9a9791" />
1024 <stop offset="1" stopColor="#2b2c2f" />
1025 </linearGradient>
1026 <radialGradient id="sel-glow">
1027 <stop offset="0" stopColor="#fff6dc" stopOpacity=".55" />
1028 <stop offset="1" stopColor="#fff6dc" stopOpacity="0" />
1029 </radialGradient>
1030 <linearGradient id="sel-gold" x1="0" y1="0" x2="1" y2="1">
1031 <stop offset="0" stopColor="#fbe29a" />
1032 <stop offset="1" stopColor="#c58f21" />
1033 </linearGradient>
1034 <linearGradient id="sel-dome" x1="0" y1="0" x2="1" y2="0">
1035 <stop offset="0" stopColor="#8d9399" />
1036 <stop offset="1" stopColor="#f4f6f7" />
1037 </linearGradient>
1038 </defs>
1039 <rect width="800" height="450" fill="#040507" />
1040 {stars.map((s, i) => (<circle key={i} cx={s.x} cy={s.y} r={s.s} fill="#eceeef" opacity={s.o} />))}
1041 <g data-sel-svg="sun" style={{ transform: "translate(60px, 340px)" }}>
1042 <circle r="64" fill="url(#sel-glow)" />
1043 <circle r="13" fill="#fff7e0" />
1044 </g>
1045 <path d={ridge} fill="#55595e" />
1046 <rect y="340" width="800" height="110" fill="url(#sel-gnd)" />
1047 <g data-sel-svg="panels" opacity=".6">
1048 {[0, 1, 2, 3].map((i) => (
1049 <polygon key={i} points={`${430 + i * 58},356 ${480 + i * 58},356 ${492 + i * 58},384 ${440 + i * 58},384`} fill="url(#sel-gold)" />
1050 ))}
1051 {[0, 1, 2, 3].map((i) => (
1052 <path key={i} d={`M${455 + i * 58} 356 L${466 + i * 58} 384`} stroke="#8a6416" strokeWidth=".8" opacity=".6" />
1053 ))}
1054 </g>
1055 <path data-sel-svg="sh1" d="M0 -2 L1 2 L1 9 L0 11Z" fill="#000" transform="translate(250 346) scale(120 1)" opacity=".5" />
1056 <path data-sel-svg="sh2" d="M0 -1 L1 1 L1 4 L0 4Z" fill="#000" transform="translate(600 346) scale(180 1)" opacity=".5" />
1057 <path d="M196 346a54 58 0 0 1 108 0z" fill="url(#sel-dome)" />
1058 <path d="M196 346a54 58 0 0 1 108 0" fill="none" stroke="#fff" strokeOpacity=".25" />
1059 <rect x="241" y="320" width="18" height="26" rx="2" fill="#2a2e33" />
1060 <rect x="212" y="322" width="9" height="5" rx="1" fill="#1b222b" />
1061 <rect x="279" y="322" width="9" height="5" rx="1" fill="#1b222b" />
1062 <rect x="598" y="242" width="4" height="104" fill="#b9bdc2" />
1063 <polygon points="570,246 630,236 634,256 574,266" fill="url(#sel-gold)" />
1064 <rect data-sel-svg="night" y="300" width="800" height="150" fill="#030405" opacity=".4" />
1065 </svg>
1066 <canvas ref={cycEarth} className="sel-cyc-earth" aria-hidden="true" />
1067 <span className="sel-cap">Window cam 3 · Earth stays put in the sky; the sun walks around it</span>
1068 </div>
1069 <div className="sel-cyc-side">
1070 <dl className="sel-readouts">
1071 <div><dt>Day</dt><dd data-sel-out="day">D+00</dd></div>
1072 <div><dt>Sun elevation</dt><dd data-sel-out="elevd">0°</dd></div>
1073 <div><dt>Surface</dt><dd data-sel-out="temp">−140 °C</dd></div>
1074 <div><dt>Earth</dt><dd data-sel-out="ephase">50% lit</dd></div>
1075 <div className="sel-wide"><dt>Solar yield <b data-sel-out="kw">0.0 kW</b></dt><dd><span className="sel-meter"><i data-sel-out="kwbar" /></span></dd></div>
1076 </dl>
1077 <ul className="sel-phases">
1078 {PHASES.map((p, i) => (
1079 <li key={p.n} className={"sel-phase" + (phaseIdx === i ? " on" : "")}>
1080 <h3>{p.n} <span>{p.r}</span></h3>
1081 <p>{p.d}</p>
1082 </li>
1083 ))}
1084 </ul>
1085 </div>
1086 </div>
1087 <div className="sel-timeline" aria-hidden="true">
1088 <div className="sel-tl-labels"><span>Dawn</span><span>Long day</span><span>Dusk</span><span>Long night</span></div>
1089 <div className="sel-tl-bar"><i data-sel-out="fill" /><b data-sel-out="thumb" /></div>
1090 </div>
1091 </div>
1092 </section>
1093
1094 {/* ============ MODULES ============ */}
1095 <section id="sel-modules" className="sel-sec" aria-label="Module configurator">
1096 <div className="sel-wrap">
1097 <div className="sel-sec-head sel-rv">
1098 <p className="sel-eyebrow">03 · Build your stay</p>
1099 <h2 className="sel-h2">Modules are lowered, slowly,<br />onto the regolith.</h2>
1100 <p className="sel-lead">Choose what your party needs. Everything is delivered in advance by the cargo lander and linked before you arrive.</p>
1101 </div>
1102 <div className="sel-cfg">
1103 <div className="sel-cfg-scene sel-rv">
1104 <svg viewBox="0 0 800 420" role="img" aria-label="Modules assembled on the lunar surface" preserveAspectRatio="xMidYMax meet">
1105 <defs>
1106 <linearGradient id="sel-g2" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor="#8d8a84" /><stop offset="1" stopColor="#25262a" /></linearGradient>
1107 <linearGradient id="sel-m1" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stopColor="#7d838a" /><stop offset="1" stopColor="#f4f6f7" /></linearGradient>
1108 <linearGradient id="sel-m2" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stopColor="#5f666d" /><stop offset="1" stopColor="#d9dde0" /></linearGradient>
1109 <linearGradient id="sel-gl" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stopColor="#4e8f6a" stopOpacity=".55" /><stop offset="1" stopColor="#3f7fb8" stopOpacity=".35" /></linearGradient>
1110 <linearGradient id="sel-g3" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stopColor="#fbe29a" /><stop offset="1" stopColor="#c58f21" /></linearGradient>
1111 </defs>
1112 <rect width="800" height="420" fill="#040507" />
1113 {cfgStars.map((s, i) => (<circle key={i} cx={s.x} cy={s.y} r={s.s} fill="#eceeef" opacity=".7" />))}
1114 <path d="M0 330 L0 300 Q90 280 170 296 T330 290 T520 298 T700 284 L800 296 L800 330Z" fill="#43474c" />
1115 <rect y="330" width="800" height="90" fill="url(#sel-g2)" />
1116 {present.slice(0, -1).map((m, i) => (
1117 <line key={m.k + present[i + 1].k} className="sel-tube" x1={m.x} y1="316" x2={present[i + 1].x} y2="316" />
1118 ))}
1119 {MODS.map((m) => (
1120 <g key={m.k} className={"sel-mod" + (mods[m.k] ? " on" : "")} style={{ "--x": m.x + "px" } as CSSProperties}>
1121 <polygon className="sel-sh" points="-40,0 -250,3 -258,12 40,14" />
1122 {m.k === "hab" && (
1123 <g>
1124 <path d="M-72 0v-40a72 40 0 0 1 144 0v40z" fill="url(#sel-m1)" />
1125 <path d="M-72 -40a72 40 0 0 1 144 0" fill="none" stroke="#fff" strokeOpacity=".3" />
1126 {[-44, -22, 0, 22, 44].map((x) => (<circle key={x} cx={x} cy="-26" r="4" fill="#12161b" />))}
1127 <rect x="-8" y="-20" width="16" height="20" rx="2" fill="#2a2e33" />
1128 </g>
1129 )}
1130 {m.k === "lab" && (
1131 <g>
1132 <rect x="-62" y="-62" width="124" height="62" rx="3" fill="url(#sel-m2)" />
1133 <rect x="-62" y="-62" width="124" height="8" fill="#fff" opacity=".18" />
1134 <path d="M-6 -62 v-30" stroke="#aeb3b8" strokeWidth="2" />
1135 <path d="M-26 -92 q26 -22 52 0 q-26 -8 -52 0z" fill="#e8ebed" />
1136 {[-40, -16, 8, 32].map((x) => (<rect key={x} x={x} y="-44" width="14" height="22" rx="1" fill="#12161b" />))}
1137 </g>
1138 )}
1139 {m.k === "gh" && (
1140 <g>
1141 <path d="M-78 0v-26a78 56 0 0 1 156 0v26z" fill="url(#sel-gl)" stroke="#a9cfe0" strokeOpacity=".5" />
1142 {[-52, -26, 0, 26, 52].map((x) => (
1143 <g key={x}><path d={`M${x} 0 v-18`} stroke="#4e8f6a" strokeWidth="2" /><ellipse cx={x - 4} cy="-20" rx="6" ry="3" fill="#6fb287" /><ellipse cx={x + 4} cy="-24" rx="6" ry="3" fill="#4e8f6a" /></g>
1144 ))}
1145 <path d="M-78 -26a78 56 0 0 1 156 0" fill="none" stroke="#fff" strokeOpacity=".35" />
1146 </g>
1147 )}
1148 {m.k === "mast" && (
1149 <g>
1150 <rect x="-3" y="-128" width="6" height="128" fill="#b9bdc2" />
1151 <polygon points="-62,-132 -4,-146 -4,-96 -62,-84" fill="url(#sel-g3)" />
1152 <polygon points="4,-146 62,-132 62,-84 4,-96" fill="url(#sel-g3)" />
1153 <polygon points="4,-146 62,-132 62,-126 4,-140" fill="#fff" opacity=".35" />
1154 <rect x="-18" y="-10" width="36" height="10" fill="#7d838a" />
1155 </g>
1156 )}
1157 <ellipse className="sel-puff" cx="0" cy="4" rx="70" ry="8" />
1158 </g>
1159 ))}
1160 </svg>
1161 <canvas ref={cfgEarth} className="sel-cfg-earth" aria-hidden="true" />
1162 <span className="sel-cap">{present.length} of 4 modules on site</span>
1163 </div>
1164 <div className="sel-cfg-panel sel-rv">
1165 <div className="sel-presets" role="group" aria-label="Presets">
1166 <button type="button" onClick={() => preset("retreat")}>Retreat</button>
1167 <button type="button" onClick={() => preset("research")}>Research</button>
1168 <button type="button" onClick={() => preset("station")}>Full station</button>
1169 </div>
1170 <div className="sel-modlist">
1171 {MODS.map((m) => (
1172 <button key={m.k} type="button" className={"sel-modbtn" + (mods[m.k] ? " on" : "")} aria-pressed={mods[m.k]} onClick={() => toggle(m.k)}>
1173 <span className="sel-tick" aria-hidden="true" />
1174 <span><b>{m.name}</b><small>{m.line}</small></span>
1175 </button>
1176 ))}
1177 </div>
1178 <div className="sel-totals" aria-live="polite">
1179 <div><b>{tot.beds}</b><span>berths</span></div>
1180 <div><b>{tot.racks}</b><span>instrument racks</span></div>
1181 <div><b>{tot.food}</b><span>kg greens / week</span></div>
1182 </div>
1183 <div className="sel-power">
1184 <div className="sel-power-row"><span>Demand</span><b>{demand} kW</b></div>
1185 <div className="sel-power-bar"><i style={{ transform: `scaleX(${clamp(demand / 40)})` }} /><u style={{ transform: `scaleX(${clamp(supply / 40)})` }} /></div>
1186 <div className="sel-power-row"><span>Solar supply</span><b className="sel-gold-t">{supply} kW</b></div>
1187 <p className={"sel-note" + (demand > supply ? " warn" : "")}>
1188 {demand > supply ? "Demand exceeds supply. Add the solar mast, or the station runs on fuel cells at night only." : "Power is balanced. The night is carried by the regolith thermal store."}
1189 </p>
1190 </div>
1191 </div>
1192 </div>
1193 </div>
1194 </section>
1195
1196 {/* ============ EARTH WINDOW ============ */}
1197 <section id="sel-window" className="sel-sec sel-win" aria-label="Earth window">
1198 <div className="sel-wrap sel-win-grid">
1199 <div className="sel-rv">
1200 <p className="sel-eyebrow">04 · The window</p>
1201 <h2 className="sel-h2">The only view<br />you will never tire of.</h2>
1202 <p className="sel-lead">From the near side the Earth does not rise and set. It hangs in one place, turning once a day and passing through its phases every month. Move the slider to see the sky from any day of your stay.</p>
1203 <label className="sel-slider">
1204 <span>Lunar day <b>D+{String(winDay).padStart(2, "0")}</b></span>
1205 <input type="range" min={0} max={29} value={winDay} onChange={(e) => setWinDay(+e.target.value)} aria-label="Lunar day of your stay" />
1206 </label>
1207 <p className="sel-fine">Earth lit: {Math.round((1 + Math.cos(earthAlpha(winDay))) * 50)}%. Earthshine on the surface is about 43 times brighter than moonlight at home, enough to read by.</p>
1208 </div>
1209 <div className="sel-port-wrap sel-rv">
1210 <div ref={winBox} className="sel-port">
1211 <div className="sel-port-glass">
1212 <StarCanvas seed={33} n={160} className="sel-fill" />
1213 <canvas ref={winEarth} className="sel-port-earth" aria-label="The Earth, turning" role="img" />
1214 <svg className="sel-port-horizon" viewBox="0 0 400 80" preserveAspectRatio="none" aria-hidden="true">
1215 <path d="M0 80V46q40-14 90-4t100-6q70 8 120-4t90 8V80z" fill="#121418" />
1216 <path d="M0 52q60-6 110 2t120-2q70 2 170-6" stroke="#5b6066" strokeWidth="1" fill="none" opacity=".6" />
1217 </svg>
1218 <i className="sel-glare" />
1219 </div>
1220 <i className="sel-bolt sel-b1" /><i className="sel-bolt sel-b2" /><i className="sel-bolt sel-b3" /><i className="sel-bolt sel-b4" />
1221 </div>
1222 <span className="sel-cap">Window 2 · Earth 7.4° above the horizon · turning once every 24 hours</span>
1223 </div>
1224 </div>
1225 </section>
1226
1227 {/* ============ CREW ============ */}
1228 <section id="sel-crew" className="sel-sec" aria-label="Mission crew">
1229 <div className="sel-wrap">
1230 <div className="sel-sec-head sel-rv">
1231 <p className="sel-eyebrow">05 · Mission crew</p>
1232 <h2 className="sel-h2">Four people who live<br />where you are visiting.</h2>
1233 </div>
1234 <div className="sel-crew">
1235 {CREW.map((c, i) => (
1236 <article key={c.n} className="sel-card sel-rv" style={{ "--d": i * 0.18 + "s" } as CSSProperties} tabIndex={0}>
1237 <Visor tilt={c.tilt} id={i} />
1238 <h3>{c.n}</h3>
1239 <p className="sel-role">{c.r}</p>
1240 <blockquote>{c.q}</blockquote>
1241 <p className="sel-stat-line">{c.s}</p>
1242 </article>
1243 ))}
1244 </div>
1245 </div>
1246 </section>
1247
1248 {/* ============ MANIFEST ============ */}
1249 <section id="sel-manifest" className="sel-sec sel-man" aria-label="Seat manifest booking">
1250 <div className="sel-wrap">
1251 <div className="sel-sec-head sel-rv">
1252 <p className="sel-eyebrow">06 · Seat manifest</p>
1253 <h2 className="sel-h2">Take a seat on the next<br />transfer from Cislunar Gate.</h2>
1254 </div>
1255 <form className="sel-man-grid sel-rv" onSubmit={submit}>
1256 <div className="sel-man-form">
1257 <fieldset>
1258 <legend>1 · Departure</legend>
1259 <div className="sel-opts">
1260 {FLIGHTS.map((f, i) => (
1261 <label key={f.id} className={"sel-opt" + (flight === i ? " on" : "")}>
1262 <input type="radio" name="flight" checked={flight === i} onChange={() => { setFlight(i); setCleared(false); }} />
1263 <b>{f.date}</b><span>{f.id} · {f.from} · transit {f.transit}</span>
1264 </label>
1265 ))}
1266 </div>
1267 </fieldset>
1268 <fieldset>
1269 <legend>2 · Length of stay</legend>
1270 <div className="sel-opts sel-opts-3">
1271 {STAYS.map((s, i) => (
1272 <label key={s.d} className={"sel-opt" + (stay === i ? " on" : "")}>
1273 <input type="radio" name="stay" checked={stay === i} onChange={() => { setStay(i); setCleared(false); }} />
1274 <b>{s.name}</b><span>{s.note}</span>
1275 </label>
1276 ))}
1277 </div>
1278 </fieldset>
1279 <fieldset>
1280 <legend>3 · Seat <small>Window seats (A, D) face Earth</small></legend>
1281 <div className="sel-cabin" role="group" aria-label="Cabin seat map">
1282 {ROWS.map((r) => (
1283 <div key={r} className="sel-seatrow">
1284 <em>{r}</em>
1285 {COLS.map((c, ci) => {
1286 const id = r + c;
1287 const taken = TAKEN.has(id);
1288 return (
1289 <button
1290 key={id}
1291 type="button"
1292 disabled={taken}
1293 aria-pressed={seat === id}
1294 aria-label={`Seat ${id}${taken ? ", taken" : ""}${c === "A" || c === "D" ? ", window" : ""}`}
1295 className={"sel-seat" + (seat === id ? " on" : "") + (ci === 1 ? " aisle" : "")}
1296 onClick={() => { setSeat(id); setCleared(false); }}
1297 >{c}</button>
1298 );
1299 })}
1300 </div>
1301 ))}
1302 </div>
1303 </fieldset>
1304 <label className="sel-field">
1305 <span>4 · Passenger name</span>
1306 <input value={name} onChange={(e) => { setName(e.target.value); setCleared(false); }} placeholder="As printed on your passport" autoComplete="name" required />
1307 </label>
1308 <button className="sel-btn" type="submit" disabled={!name.trim() || !seat}>Request clearance</button>
1309 </div>
1310
1311 <aside className={"sel-pass" + (cleared ? " cleared" : "")} aria-live="polite" aria-label="Boarding manifest">
1312 <div className="sel-pass-top"><span>Selene Transfer Manifest</span><span>{fl.id}</span></div>
1313 <dl>
1314 <div><dt>Passenger</dt><dd>{name.trim() || "— — —"}</dd></div>
1315 <div><dt>Departs</dt><dd>{fl.date}</dd></div>
1316 <div><dt>From</dt><dd>{fl.from}</dd></div>
1317 <div><dt>Transit</dt><dd>{fl.transit}</dd></div>
1318 <div><dt>Seat</dt><dd>{seat || "—"}{seatPrice ? " · window" : ""}</dd></div>
1319 <div><dt>Stay</dt><dd>{st.name} · {st.d} d</dd></div>
1320 <div><dt>Mass allowance</dt><dd>23 kg + 4 kg personal</dd></div>
1321 </dl>
1322 <div className="sel-pass-bar" aria-hidden="true">
1323 {Array.from({ length: 44 }, (_, i) => (<i key={i} style={{ width: 1 + ((i * 7 + (name.length % 5)) % 4) + "px" }} />))}
1324 </div>
1325 <div className="sel-pass-total"><span>Total, all inclusive</span><b>{eur(total)}</b></div>
1326 <div className="sel-stamp" aria-hidden={!cleared}>
1327 <span>Cleared for transit</span>
1328 <small>{ref}</small>
1329 </div>
1330 <p className="sel-fine">{cleared ? `Thank you, ${name.trim().split(" ")[0]}. Your medical briefing arrives within 48 hours.` : "Clearance includes a 6-week conditioning programme and a medical review."}</p>
1331 </aside>
1332 </form>
1333 </div>
1334 </section>
1335
1336 {/* ============ FOOTER ============ */}
1337 <footer className="sel-foot">
1338 <div className="sel-wrap sel-foot-grid">
1339 <div>
1340 <div className="sel-foot-mark" aria-hidden="true">SELENE</div>
1341 <p className="sel-fine">2.4° N · 1.7° E · Sinus Medii, near side<br />Selene Lunar Hospitality is a fictional concept.</p>
1342 </div>
1343 <div className="sel-foot-cols">
1344 <div><h4>Stay</h4><a href="#sel-rim">Crater rim</a><a href="#sel-day">The lunar day</a><a href="#sel-modules">Modules</a></div>
1345 <div><h4>Station</h4><a href="#sel-crew">Crew</a><a href="#sel-window">Earth window</a><a href="#sel-manifest">Manifest</a></div>
1346 <div><h4>Contact</h4><a href="#sel-top">Press</a><a href="#sel-top">Research access</a><a href="#sel-top">Safety</a></div>
1347 </div>
1348 </div>
1349 </footer>
1350 </div>
1351 );
1352}
1353
1354export default Page;
1355
1356/* ------------------------------------------------------------------ */
1357/* Styles (scoped under .sel-root) */
1358/* ------------------------------------------------------------------ */
1359const CSS = `
1360.sel-root{
1361 --sel-void:#06070a; --sel-basalt:#0d0f12; --sel-reg1:#171a1e; --sel-reg2:#252a2f; --sel-reg3:#434950;
1362 --sel-reg4:#767c83; --sel-reg5:#a3a49f; --sel-ano:#eceeef; --sel-warm:#b7ae9c;
1363 --sel-blue:#3f7fb8; --sel-green:#4e8f6a; --sel-gold:#e3b24a; --sel-gold-hi:#fbe29a;
1364 --sel-sans:"Outfit","Jost","Avenir Next","Helvetica Neue",system-ui,-apple-system,"Segoe UI",sans-serif;
1365 --sel-mono:"JetBrains Mono","SF Mono",ui-monospace,Menlo,Consolas,monospace;
1366 --sel-serif:"Iowan Old Style","Palatino Linotype",Georgia,serif;
1367 --sel-slow:cubic-bezier(.16,.84,.3,1);
1368 font:300 16px/1.6 var(--sel-sans); color:var(--sel-ano); background:var(--sel-void);
1369 box-sizing:border-box; overflow-x:clip; -webkit-font-smoothing:antialiased; position:relative;
1370}
1371.sel-root *,.sel-root *::before,.sel-root *::after{box-sizing:border-box}
1372.sel-root a{color:inherit;text-decoration:none}
1373.sel-root h1,.sel-root h2,.sel-root h3,.sel-root h4,.sel-root p,.sel-root dl,.sel-root dd,.sel-root ol,.sel-root ul,.sel-root blockquote,.sel-root fieldset{margin:0;padding:0}
1374.sel-root ol,.sel-root ul{list-style:none}
1375.sel-root button,.sel-root input{font:inherit;color:inherit}
1376.sel-root :focus-visible{outline:2px solid var(--sel-gold);outline-offset:3px}
1377.sel-wrap{width:min(1180px,100% - 48px);margin:0 auto}
1378.sel-fill{position:absolute;inset:0;width:100%;height:100%;display:block}
1379.sel-eyebrow{font:400 11px/1.4 var(--sel-mono);letter-spacing:.2em;text-transform:uppercase;color:var(--sel-reg5)}
1380.sel-h2{font:200 clamp(32px,5.2vw,68px)/1.06 var(--sel-sans);letter-spacing:-.01em;margin-top:14px}
1381.sel-lead{font-size:clamp(16px,1.5vw,19px);color:var(--sel-reg5);max-width:46ch;margin-top:20px}
1382.sel-fine{font:400 12px/1.6 var(--sel-mono);color:var(--sel-reg4);margin-top:16px;letter-spacing:.02em}
1383.sel-cap{display:block;font:400 11px/1.4 var(--sel-mono);color:var(--sel-reg4);letter-spacing:.06em;margin-top:12px}
1384.sel-gold-t{color:var(--sel-gold)}
1385
1386/* buttons */
1387.sel-btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;background:var(--sel-gold);color:#14100a;font:500 13px/1 var(--sel-mono);letter-spacing:.12em;text-transform:uppercase;padding:17px 26px;border:0;border-radius:2px;cursor:pointer;transition:transform 1.4s var(--sel-slow),background .4s,box-shadow 1.4s var(--sel-slow);box-shadow:0 0 0 0 rgba(227,178,74,0)}
1388.sel-btn:hover{transform:translateY(-4px);background:var(--sel-gold-hi);box-shadow:0 18px 40px -18px rgba(227,178,74,.55)}
1389.sel-btn:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
1390.sel-btn-sm{padding:11px 16px;font-size:11px}
1391.sel-ghost{font:400 12px/1 var(--sel-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--sel-ano);padding:17px 8px;border-bottom:1px solid var(--sel-reg3);transition:border-color .4s,color .4s}
1392.sel-ghost:hover{border-color:var(--sel-ano)}
1393
1394/* nav */
1395.sel-nav{position:fixed;inset:0 0 auto 0;z-index:50;display:flex;align-items:center;justify-content:space-between;gap:24px;padding:16px clamp(16px,3vw,40px);transition:background .6s,backdrop-filter .6s,border-color .6s;border-bottom:1px solid transparent}
1396.sel-nav.sel-solid{background:rgba(6,7,10,.72);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-color:rgba(255,255,255,.07)}
1397.sel-brand{display:inline-flex;align-items:center;gap:10px;font:300 18px/1 var(--sel-sans);letter-spacing:.34em;text-transform:uppercase}
1398.sel-links{display:flex;gap:30px;font:400 11px/1 var(--sel-mono);letter-spacing:.16em;text-transform:uppercase;color:var(--sel-reg5)}
1399.sel-links a{transition:color .4s}.sel-links a:hover{color:var(--sel-ano)}
1400
1401/* pinned sections */
1402.sel-pin{position:relative}
1403.sel-stick{position:sticky;top:0;height:100vh;height:100svh;overflow:hidden}
1404.sel-hero{height:260vh}
1405.sel-statement{height:230vh}
1406.sel-rim{height:380vh}
1407.sel-cycle{height:440vh}
1408
1409/* hero */
1410.sel-hero{--p:0}
1411.sel-hero .sel-stick{background:radial-gradient(120% 90% at 70% 10%,#0b0e13,#030406 70%)}
1412.sel-earthwrap{position:absolute;right:15vw;top:11vh;width:clamp(150px,20vw,310px);aspect-ratio:1;transform:translate3d(0,calc((1 - var(--p)) * 54vh),0) scale(calc(.88 + var(--p) * .12));will-change:transform}
1413.sel-earth{width:100%;height:100%;display:block;position:relative}
1414.sel-earthglow{position:absolute;inset:-18%;border-radius:50%;background:radial-gradient(closest-side,rgba(110,170,230,.28),rgba(110,170,230,.07) 60%,transparent)}
1415.sel-layer{position:absolute;inset:0;transform-origin:50% 100%;will-change:transform}
1416.sel-l-far{transform:translate3d(0,calc(var(--p) * 3vh),0)}
1417.sel-l-mid{transform:translate3d(0,calc(var(--p) * 7vh),0)}
1418.sel-l-near{transform:translate3d(0,calc(var(--p) * 13vh),0)}
1419.sel-outpost{position:absolute;left:70%;width:clamp(52px,6.4vw,92px);height:auto;overflow:visible}
1420.sel-prints{position:absolute;inset:0}
1421.sel-print{position:absolute;display:block;width:22px;height:56px;border-radius:48% 48% 44% 44% / 62% 62% 38% 38%;background:rgba(6,7,9,.34);box-shadow:2px 0 0 rgba(255,255,255,.16),-1px 0 2px rgba(0,0,0,.35);opacity:clamp(0,calc(var(--p) * 52 - var(--i)),1)}
1422.sel-print::after{content:"";position:absolute;left:12%;right:12%;bottom:-8px;height:12px;border-radius:40%;background:rgba(6,7,9,.3)}
1423.sel-dust{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
1424.sel-hero-copy{position:absolute;left:clamp(18px,6vw,96px);top:clamp(86px,15vh,150px);z-index:3;max-width:min(680px,88vw);transform:translate3d(0,calc(var(--p) * -9vh),0);opacity:calc(1 - var(--p) * 1.25)}
1425.sel-title{font:200 clamp(66px,16.5vw,250px)/.92 var(--sel-sans);letter-spacing:.07em;margin:18px 0 22px -.05em;display:flex;color:var(--sel-ano);}
1426.sel-letter{display:inline-block;will-change:transform}
1427.sel-sub{font-size:clamp(16px,1.6vw,20px);color:var(--sel-reg5);max-width:38ch}
1428.sel-cta{display:flex;flex-wrap:wrap;align-items:center;gap:20px;margin-top:30px}
1429.sel-rise{position:absolute;left:clamp(18px,6vw,96px);bottom:11vh;z-index:3;font:400 11px/1.6 var(--sel-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--sel-ano);max-width:30ch;opacity:clamp(0,calc((var(--p) - .3) * 5),1);text-shadow:0 1px 8px #000}
1430.sel-rise b{color:var(--sel-gold);font-weight:400}
1431.sel-hud{position:absolute;right:clamp(16px,3vw,40px);bottom:7vh;z-index:3;display:grid;gap:8px;font:400 11px/1.3 var(--sel-mono);letter-spacing:.1em;text-transform:uppercase;text-shadow:0 1px 8px #000}
1432.sel-hud div{display:flex;justify-content:space-between;gap:28px;border-top:1px solid rgba(255,255,255,.18);padding-top:6px;min-width:210px}
1433.sel-hud dt{color:var(--sel-reg5)}.sel-hud dd{color:var(--sel-ano)}
1434.sel-cue{position:absolute;left:50%;bottom:2.6vh;z-index:3;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:8px;font:400 10px/1 var(--sel-mono);letter-spacing:.3em;text-transform:uppercase;color:var(--sel-reg5);opacity:calc(1 - var(--p) * 6)}
1435.sel-cue i{width:1px;height:34px;background:linear-gradient(var(--sel-ano),transparent);transform-origin:top;animation:sel-cue 3.4s var(--sel-slow) infinite}
1436@keyframes sel-cue{0%{transform:scaleY(0)}60%{transform:scaleY(1);opacity:1}100%{transform:scaleY(1);opacity:0}}
1437
1438/* statement */
1439.sel-statement{--p:0;background:var(--sel-void)}
1440.sel-center{display:flex;flex-direction:column;justify-content:center;gap:56px;width:min(1080px,100% - 48px);margin:0 auto}
1441.sel-big{font:200 clamp(26px,4.4vw,56px)/1.22 var(--sel-sans);letter-spacing:-.01em}
1442.sel-word{opacity:clamp(.16,calc((var(--p) * 1.35 - var(--w)) * 9 + .16),1);color:var(--sel-ano)}
1443.sel-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:24px;opacity:clamp(0,calc((var(--p) - .55) * 5),1)}
1444.sel-stat{border-top:1px solid var(--sel-reg3);padding-top:14px;display:flex;flex-direction:column;gap:4px}
1445.sel-stat b{font:200 clamp(26px,3vw,40px)/1.1 var(--sel-sans)}
1446.sel-stat span{font:400 11px/1.5 var(--sel-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--sel-reg4)}
1447
1448/* crater rim */
1449.sel-rim{--p:0}
1450.sel-rim .sel-stick{background:#030406}
1451.sel-rl{position:absolute;inset:0;transform-origin:50% 100%;will-change:transform}
1452.sel-rl-a{transform:translate3d(0,calc(var(--p) * -2vh),0) scale(calc(1 + var(--p) * .16))}
1453.sel-rl-b{transform:translate3d(0,calc(var(--p) * 14vh),0) scale(calc(1 + var(--p) * .3))}
1454.sel-rl-c{transform:translate3d(0,calc(var(--p) * 78vh),0) scale(calc(1 + var(--p) * .45))}
1455.sel-gold-glint{position:absolute;width:7px;height:3px;background:var(--sel-gold);box-shadow:0 0 10px 2px rgba(227,178,74,.7);transform:rotate(-12deg)}
1456.sel-ice-glint{position:absolute;width:3px;height:3px;border-radius:50%;background:#cfe8ff;box-shadow:0 0 12px 3px rgba(120,180,240,.55);opacity:calc(.25 + var(--p) * .75)}
1457.sel-scrim{position:absolute;inset:0;background:linear-gradient(90deg,rgba(3,4,6,.85),rgba(3,4,6,.1) 55%,transparent),linear-gradient(0deg,rgba(3,4,6,.5),transparent 30%);pointer-events:none}
1458.sel-rim-head{position:absolute;left:clamp(18px,6vw,96px);top:clamp(88px,14vh,130px);z-index:2}
1459.sel-steps{position:absolute;left:clamp(18px,6vw,96px);bottom:8vh;z-index:2;width:min(460px,88vw);display:grid;gap:18px}
1460.sel-step{display:grid;grid-template-columns:34px 1fr;gap:12px;opacity:.28;transition:opacity 1.2s var(--sel-slow),transform 1.2s var(--sel-slow);transform:translateX(-6px)}
1461.sel-step.on{opacity:1;transform:none}
1462.sel-k{font:400 11px/1.9 var(--sel-mono);color:var(--sel-gold)}
1463.sel-step h3{font:300 22px/1.2 var(--sel-sans)}
1464.sel-step p{font-size:14px;color:var(--sel-reg5);margin-top:6px;display:none}
1465.sel-step.on p{display:block}
1466.sel-hud-r{bottom:8vh}
1467
1468/* lunar day */
1469.sel-cycle{--p:0;background:var(--sel-basalt)}
1470.sel-cyc-stick{display:flex;flex-direction:column;justify-content:center;gap:clamp(12px,2.4vh,26px);width:min(1180px,100% - 48px);margin:0 auto;padding-top:68px}
1471.sel-cyc-head .sel-h2{font-size:clamp(28px,4vw,52px)}
1472.sel-cyc-grid{display:grid;grid-template-columns:1.55fr 1fr;gap:clamp(18px,3vw,44px);align-items:start;min-height:0}
1473.sel-scene{position:relative;border:1px solid rgba(255,255,255,.08);background:#040507}
1474.sel-scene svg{display:block;width:100%;height:auto;aspect-ratio:16/9;max-height:52vh}
1475.sel-cyc-earth{position:absolute;right:7%;top:7%;width:10.5%;aspect-ratio:1}
1476.sel-scene .sel-cap{position:absolute;left:12px;bottom:6px;margin:0;color:var(--sel-ano);text-shadow:0 1px 6px #000}
1477.sel-readouts{display:grid;grid-template-columns:1fr 1fr;gap:0 20px}
1478.sel-readouts div{border-top:1px solid var(--sel-reg2);padding:9px 0}
1479.sel-readouts dt{font:400 10px/1.4 var(--sel-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--sel-reg4)}
1480.sel-readouts dd{font:200 clamp(20px,2.4vw,30px)/1.2 var(--sel-sans)}
1481.sel-wide{grid-column:1/-1}
1482.sel-wide dt{display:flex;justify-content:space-between}
1483.sel-wide dt b{font-weight:400;color:var(--sel-gold)}
1484.sel-meter{display:block;height:3px;background:var(--sel-reg2);margin-top:8px;overflow:hidden}
1485.sel-meter i{display:block;height:100%;background:var(--sel-gold);transform-origin:left;transform:scaleX(0)}
1486.sel-phases{display:grid;gap:12px;margin-top:14px}
1487.sel-phase{opacity:.3;transition:opacity 1s var(--sel-slow)}
1488.sel-phase.on{opacity:1}
1489.sel-phase h3{font:300 18px/1.3 var(--sel-sans)}
1490.sel-phase h3 span{font:400 10px/1 var(--sel-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--sel-reg4);margin-left:8px}
1491.sel-phase p{font-size:13px;color:var(--sel-reg5);line-height:1.55;margin-top:3px}
1492.sel-phase:not(.on) p{display:none}
1493.sel-timeline{container-type:inline-size}
1494.sel-tl-labels{display:grid;grid-template-columns:10fr 41fr 10fr 39fr;font:400 10px/1 var(--sel-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--sel-reg4);margin-bottom:8px}
1495.sel-tl-bar{position:relative;height:3px;background:repeating-linear-gradient(90deg,var(--sel-reg3) 0 1px,transparent 1px calc(100%/29.5));overflow:visible}
1496.sel-tl-bar::before{content:"";position:absolute;inset:0;background:var(--sel-reg2);z-index:-1}
1497.sel-tl-bar i{position:absolute;inset:0;background:var(--sel-ano);transform-origin:left;transform:scaleX(0);opacity:.85}
1498.sel-tl-bar b{position:absolute;left:0;top:-5px;width:13px;height:13px;margin-left:-6px;border-radius:50%;background:var(--sel-gold);box-shadow:0 0 14px rgba(227,178,74,.8)}
1499
1500/* sections */
1501.sel-sec{padding:clamp(80px,12vw,160px) 0;background:var(--sel-void);position:relative}
1502.sel-sec:nth-of-type(odd){background:var(--sel-basalt)}
1503.sel-sec-head{margin-bottom:clamp(36px,5vw,64px)}
1504.sel-rv{transition:opacity 1.8s var(--sel-slow) var(--d,0s),transform 1.8s var(--sel-slow) var(--d,0s)}
1505.sel-js .sel-rv:not(.in){opacity:0;transform:translate3d(0,34px,0)}
1506
1507/* configurator */
1508.sel-cfg{display:grid;grid-template-columns:1.5fr 1fr;gap:clamp(24px,4vw,56px);align-items:start}
1509.sel-cfg-scene{position:relative;border:1px solid rgba(255,255,255,.08);background:#040507;overflow:hidden}
1510.sel-cfg-scene svg{display:block;width:100%;height:auto}
1511.sel-cfg-earth{position:absolute;left:6%;top:7%;width:9%;aspect-ratio:1}
1512.sel-cfg-scene .sel-cap{position:absolute;right:12px;bottom:4px;margin:0;color:var(--sel-ano)}
1513.sel-mod{transform:translate(var(--x),-120px);opacity:0;transition:transform 2.6s var(--sel-slow),opacity .6s}
1514.sel-mod.on{transform:translate(var(--x),330px);opacity:1}
1515.sel-sh{fill:rgba(0,0,0,.55)}
1516.sel-puff{fill:rgba(210,205,196,.4);opacity:0;transform-box:fill-box;transform-origin:center}
1517.sel-mod.on .sel-puff{animation:sel-puff 2.2s ease-out 1.5s both}
1518@keyframes sel-puff{0%{opacity:.6;transform:scale(.2)}100%{opacity:0;transform:scale(1.8,1.4)}}
1519.sel-tube{stroke:#aeb3b8;stroke-width:5;stroke-linecap:round;opacity:.85;animation:sel-fadein 1.4s ease both}
1520@keyframes sel-fadein{from{opacity:0}to{opacity:.85}}
1521.sel-presets{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}
1522.sel-presets button{background:transparent;border:1px solid var(--sel-reg3);padding:8px 14px;font:400 11px/1 var(--sel-mono);letter-spacing:.12em;text-transform:uppercase;cursor:pointer;transition:border-color .4s,background .4s}
1523.sel-presets button:hover{border-color:var(--sel-ano);background:rgba(255,255,255,.04)}
1524.sel-modlist{display:grid;gap:8px}
1525.sel-modbtn{display:grid;grid-template-columns:22px 1fr;gap:14px;text-align:left;background:rgba(255,255,255,.02);border:1px solid var(--sel-reg2);padding:14px 16px;cursor:pointer;transition:border-color .5s,background .5s}
1526.sel-modbtn:hover{border-color:var(--sel-reg4)}
1527.sel-modbtn.on{border-color:var(--sel-ano);background:rgba(255,255,255,.05)}
1528.sel-modbtn b{display:block;font-weight:400;font-size:17px}
1529.sel-modbtn small{display:block;font-size:13px;color:var(--sel-reg4);margin-top:2px}
1530.sel-tick{width:16px;height:16px;border:1px solid var(--sel-reg4);margin-top:4px;position:relative;border-radius:50%}
1531.sel-modbtn.on .sel-tick{border-color:var(--sel-ano)}
1532.sel-modbtn.on .sel-tick::after{content:"";position:absolute;inset:3px;background:var(--sel-ano);border-radius:50%}
1533.sel-totals{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:22px}
1534.sel-totals div{border-top:1px solid var(--sel-reg3);padding-top:10px}
1535.sel-totals b{display:block;font:200 36px/1.1 var(--sel-sans)}
1536.sel-totals span{font:400 10px/1.3 var(--sel-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--sel-reg4)}
1537.sel-power{margin-top:22px}
1538.sel-power-row{display:flex;justify-content:space-between;font:400 11px/1 var(--sel-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--sel-reg5)}
1539.sel-power-row b{font-weight:400;color:var(--sel-ano)}
1540.sel-power-bar{position:relative;height:6px;background:var(--sel-reg2);margin:10px 0;overflow:hidden}
1541.sel-power-bar i,.sel-power-bar u{position:absolute;left:0;right:0;transform-origin:left;transition:transform 1.4s var(--sel-slow)}
1542.sel-power-bar i{top:0;height:3px;background:var(--sel-ano)}
1543.sel-power-bar u{bottom:0;height:3px;background:var(--sel-gold);text-decoration:none}
1544.sel-note{font-size:13px;color:var(--sel-reg5);margin-top:12px}
1545.sel-note.warn{color:var(--sel-warm)}
1546
1547/* earth window */
1548.sel-win-grid{display:grid;grid-template-columns:1fr 1fr;gap:clamp(28px,6vw,90px);align-items:center}
1549.sel-slider{display:block;margin-top:30px}
1550.sel-slider span{display:flex;justify-content:space-between;font:400 11px/1 var(--sel-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--sel-reg5);margin-bottom:12px}
1551.sel-slider b{font-weight:400;color:var(--sel-ano)}
1552.sel-slider input{width:100%;accent-color:var(--sel-gold);height:24px;cursor:pointer}
1553.sel-port-wrap{display:flex;flex-direction:column;align-items:center}
1554.sel-port{position:relative;width:min(100%,460px);aspect-ratio:1;border-radius:50%;padding:7%;background:conic-gradient(from 210deg,#6b7078,#c9ccd0,#4b5056,#aab0b6,#6b7078);box-shadow:0 40px 80px -30px rgba(0,0,0,.9),inset 0 0 0 2px rgba(255,255,255,.2)}
1555.sel-port::before{content:"";position:absolute;inset:5%;border-radius:50%;background:#101215;box-shadow:inset 0 6px 18px rgba(0,0,0,.9)}
1556.sel-port-glass{position:relative;width:100%;height:100%;border-radius:50%;overflow:hidden;background:#020305;isolation:isolate}
1557.sel-port-earth{position:absolute;left:50%;top:44%;width:62%;aspect-ratio:1;transform:translate(-50%,-50%)}
1558.sel-port-horizon{position:absolute;left:0;bottom:0;width:100%;height:22%}
1559.sel-glare{position:absolute;inset:0;background:linear-gradient(130deg,rgba(255,255,255,.14),transparent 34%);pointer-events:none}
1560.sel-bolt{position:absolute;width:12px;height:12px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#e7e9eb,#5b6066);box-shadow:0 1px 2px #000}
1561.sel-b1{left:50%;top:1.6%;margin-left:-6px}.sel-b2{left:50%;bottom:1.6%;margin-left:-6px}.sel-b3{top:50%;left:1.6%;margin-top:-6px}.sel-b4{top:50%;right:1.6%;margin-top:-6px}
1562
1563/* crew */
1564.sel-crew{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}
1565.sel-card{background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.01));border:1px solid var(--sel-reg2);padding:22px 20px 24px;display:flex;flex-direction:column;gap:6px;outline-offset:2px}
1566.sel-card.in{transition:transform 3s var(--sel-slow),border-color 1.5s,box-shadow 3s var(--sel-slow)}
1567.sel-card.in:hover,.sel-card.in:focus-visible{transform:translate3d(0,-16px,0) rotate(-.8deg);border-color:var(--sel-reg4);box-shadow:0 40px 50px -30px rgba(0,0,0,.9)}
1568.sel-visor{width:112px;height:112px;margin-bottom:10px}
1569.sel-card h3{font:300 20px/1.2 var(--sel-sans)}
1570.sel-role{font:400 11px/1.5 var(--sel-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--sel-reg4)}
1571.sel-card blockquote{font:italic 400 15px/1.55 var(--sel-serif);color:var(--sel-reg5);margin-top:14px}
1572.sel-stat-line{font:400 11px/1 var(--sel-mono);letter-spacing:.1em;color:var(--sel-gold);margin-top:auto;padding-top:18px}
1573
1574/* manifest */
1575.sel-man-grid{display:grid;grid-template-columns:1.2fr 1fr;gap:clamp(24px,4vw,60px);align-items:start}
1576.sel-man-form{display:grid;gap:26px}
1577.sel-man-form fieldset{border:0;min-width:0}
1578.sel-man-form legend{font:400 11px/1 var(--sel-mono);letter-spacing:.16em;text-transform:uppercase;color:var(--sel-reg5);padding:0;margin-bottom:12px}
1579.sel-man-form legend small{color:var(--sel-reg4);margin-left:10px;letter-spacing:.06em;text-transform:none}
1580.sel-opts{display:grid;gap:8px}
1581.sel-opts-3{grid-template-columns:repeat(3,1fr)}
1582.sel-opt{position:relative;display:flex;flex-direction:column;gap:3px;border:1px solid var(--sel-reg2);padding:14px 16px;cursor:pointer;transition:border-color .5s,background .5s}
1583.sel-opt:hover{border-color:var(--sel-reg4)}
1584.sel-opt.on{border-color:var(--sel-ano);background:rgba(255,255,255,.05)}
1585.sel-opt input{position:absolute;opacity:0;inset:0;cursor:pointer}
1586.sel-opt:focus-within{outline:2px solid var(--sel-gold);outline-offset:3px}
1587.sel-opt b{font:400 15px/1.3 var(--sel-mono);letter-spacing:.04em}
1588.sel-opt span{font-size:13px;color:var(--sel-reg4)}
1589.sel-cabin{display:inline-grid;gap:8px;padding:18px 22px;border:1px solid var(--sel-reg2);border-radius:60px 60px 10px 10px / 40px 40px 10px 10px}
1590.sel-seatrow{display:flex;gap:8px;align-items:center}
1591.sel-seatrow em{width:16px;font:400 10px/1 var(--sel-mono);color:var(--sel-reg4);font-style:normal}
1592.sel-seat{width:44px;height:40px;background:transparent;border:1px solid var(--sel-reg3);border-radius:8px 8px 4px 4px;cursor:pointer;font:400 11px/1 var(--sel-mono);color:var(--sel-reg5);transition:background .4s,border-color .4s,color .4s}
1593.sel-seat.aisle{margin-right:22px}
1594.sel-seat:hover:not(:disabled){border-color:var(--sel-ano)}
1595.sel-seat.on{background:var(--sel-ano);color:#101215;border-color:var(--sel-ano)}
1596.sel-seat:disabled{background:repeating-linear-gradient(45deg,var(--sel-reg2) 0 3px,transparent 3px 6px);color:var(--sel-reg3);border-color:var(--sel-reg2);cursor:not-allowed}
1597.sel-field{display:grid;gap:10px}
1598.sel-field span{font:400 11px/1 var(--sel-mono);letter-spacing:.16em;text-transform:uppercase;color:var(--sel-reg5)}
1599.sel-field input{background:transparent;border:0;border-bottom:1px solid var(--sel-reg3);padding:10px 0;font-size:20px;font-weight:200;border-radius:0;transition:border-color .4s}
1600.sel-field input:focus{outline:none;border-color:var(--sel-gold)}
1601.sel-field input::placeholder{color:var(--sel-reg3)}
1602.sel-man-form .sel-btn{justify-self:start}
1603.sel-pass{position:sticky;top:90px;background:#e9ebec;color:#14161a;padding:24px 26px 22px;border-radius:4px;font-family:var(--sel-mono);overflow:hidden;box-shadow:0 40px 80px -40px rgba(0,0,0,.9)}
1604.sel-pass::before,.sel-pass::after{content:"";position:absolute;top:96px;width:22px;height:22px;border-radius:50%;background:var(--sel-basalt)}
1605.sel-pass::before{left:-11px}.sel-pass::after{right:-11px}
1606.sel-pass-top{display:flex;justify-content:space-between;font:500 10px/1 var(--sel-mono);letter-spacing:.18em;text-transform:uppercase;padding-bottom:16px;border-bottom:1px dashed #9aa0a6}
1607.sel-pass dl{display:grid;gap:10px;margin-top:20px}
1608.sel-pass dl div{display:flex;justify-content:space-between;gap:14px;font-size:12px}
1609.sel-pass dt{color:#767c83;text-transform:uppercase;letter-spacing:.1em;font-size:10px;padding-top:2px}
1610.sel-pass dd{text-align:right;font-weight:500;word-break:break-word}
1611.sel-pass-bar{display:flex;gap:2px;height:34px;margin:22px 0 14px;align-items:stretch}
1612.sel-pass-bar i{background:#14161a;display:block}
1613.sel-pass-total{display:flex;justify-content:space-between;align-items:baseline;border-top:1px dashed #9aa0a6;padding-top:14px}
1614.sel-pass-total span{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:#767c83}
1615.sel-pass-total b{font:300 28px/1 var(--sel-sans)}
1616.sel-pass .sel-fine{color:#5a6068;margin-top:14px}
1617.sel-stamp{position:absolute;right:20px;top:128px;border:2px solid #b4801a;color:#b4801a;padding:6px 12px;text-transform:uppercase;font:500 11px/1.3 var(--sel-mono);letter-spacing:.14em;text-align:center;opacity:0;transform:rotate(-12deg) scale(1.6);transition:opacity .4s,transform 1s var(--sel-slow);pointer-events:none}
1618.sel-stamp small{display:block;font-size:10px}
1619.sel-pass.cleared .sel-stamp{opacity:.92;transform:rotate(-12deg) scale(1)}
1620
1621/* footer */
1622.sel-foot{background:var(--sel-void);padding:clamp(60px,9vw,120px) 0 40px;border-top:1px solid var(--sel-reg2);overflow:hidden}
1623.sel-foot-grid{display:grid;grid-template-columns:1.6fr 1fr;gap:40px;align-items:end}
1624.sel-foot-mark{font:200 clamp(60px,15vw,210px)/.9 var(--sel-sans);letter-spacing:.07em;color:transparent;-webkit-text-stroke:1px var(--sel-reg3)}
1625.sel-foot-cols{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}
1626.sel-foot-cols h4{font:400 10px/1 var(--sel-mono);letter-spacing:.2em;text-transform:uppercase;color:var(--sel-reg4);margin-bottom:14px}
1627.sel-foot-cols a{display:block;font-size:14px;color:var(--sel-reg5);padding:4px 0;transition:color .4s}
1628.sel-foot-cols a:hover{color:var(--sel-ano)}
1629
1630/* reduced motion */
1631.sel-rm .sel-hero{height:100vh;height:100svh}
1632.sel-rm .sel-statement{height:auto;padding:120px 0}
1633.sel-rm .sel-statement .sel-stick{position:relative;height:auto}
1634.sel-rm .sel-rim,.sel-rm .sel-cycle{height:auto}
1635.sel-rm .sel-rim .sel-stick{height:100vh}
1636.sel-rm .sel-cycle .sel-stick{position:relative;height:auto;padding:90px 0}
1637.sel-rm .sel-step,.sel-rm .sel-phase{opacity:1;transform:none}
1638.sel-rm .sel-step p,.sel-rm .sel-phase p{display:block}
1639.sel-rm .sel-mod,.sel-rm .sel-card,.sel-rm .sel-rv{transition:none}
1640.sel-rm .sel-mod.on .sel-puff,.sel-rm .sel-cue i,.sel-rm .sel-tube{animation:none}
1641.sel-rm .sel-rv{opacity:1!important;transform:none!important}
1642.sel-rm .sel-cue{display:none}
1643@media (prefers-reduced-motion:reduce){
1644 .sel-root *,.sel-root *::before,.sel-root *::after{animation-duration:.001s!important;transition-duration:.001s!important}
1645}
1646
1647/* responsive */
1648@media (max-width:960px){
1649 .sel-links{display:none}
1650 .sel-cyc-grid,.sel-cfg,.sel-win-grid,.sel-man-grid{grid-template-columns:1fr}
1651 .sel-crew{grid-template-columns:repeat(2,1fr)}
1652 .sel-stats{grid-template-columns:repeat(2,1fr)}
1653 .sel-pass{position:relative;top:0}
1654 .sel-foot-grid{grid-template-columns:1fr}
1655}
1656@media (max-width:640px){
1657 .sel-wrap{width:calc(100% - 32px)}
1658 .sel-nav{padding:12px 16px}
1659 .sel-brand{font-size:15px;letter-spacing:.26em}
1660 .sel-earthwrap{right:6vw;top:8vh;width:42vw}
1661 .sel-hero-copy{top:auto;bottom:34vh}
1662 .sel-title{font-size:22vw;margin:12px 0 14px -.05em}
1663 .sel-sub{font-size:15px}
1664 .sel-cta{margin-top:18px;gap:10px}
1665 .sel-btn{padding:15px 20px}
1666 .sel-hud{display:none}
1667 .sel-rise{bottom:5vh;font-size:10px}
1668 .sel-cue{display:none}
1669 .sel-rim-head{top:84px}
1670 .sel-steps{bottom:5vh}
1671 .sel-step:not(.on){display:none}
1672 .sel-hud-r{display:none}
1673 .sel-cyc-stick{width:calc(100% - 32px);padding-top:60px;gap:10px}
1674 .sel-cyc-head .sel-h2{font-size:26px}
1675 .sel-cyc-head .sel-eyebrow{display:none}
1676 .sel-scene svg{max-height:none}
1677 .sel-phase:not(.on){display:none}
1678 .sel-phase p{font-size:12.5px}
1679 .sel-readouts dd{font-size:18px}
1680 .sel-readouts div{padding:6px 0}
1681 .sel-timeline{display:none}
1682 .sel-opts-3{grid-template-columns:1fr}
1683 .sel-crew{grid-template-columns:1fr}
1684 .sel-seat{width:40px}
1685 .sel-seat.aisle{margin-right:14px}
1686 .sel-foot-cols{grid-template-columns:repeat(2,1fr)}
1687 .sel-rm .sel-hero-copy{bottom:34vh}
1688}
1689`;