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