Ares. Space & Habitat landing page.

A retro-future habitat and relocation landing page. The first screen is a 330vh pinned canvas: scrolling is a descent from Mars orbit through entry heat to touchdown, where layered dunes, Olympus Mons, Phobos and Deimos, drifting dust and a blue Martian sunset resolve. A live sol clock runs on real Mars time with a temperature readout, and the pointer steers a dust devil that swirls the dust. Below: words that light up as you read, a pinned floor plan that draws itself room by room, a dome cross-section that builds layer by layer, an orbital launch-window diagram where Earth and Mars move and a supply ship flies its transfer arc, drifting postcards from residents, boarding-pass pricing with a pay-once or lease toggle, and an FAQ styled as a typing mission-control log. Basalt, oxide red, ochre, butterscotch dust and copper; no images or web fonts.

  • Orbit-to-surface descent hero
  • Dust-devil pointer
  • Live Mars sol clock
  • Scroll-scrubbed word reveal
  • Floor plan draws itself
  • Dome cross-section build
  • Orbital transfer scrub
  • Typing mission log
Full screen

The preview is interactive — scroll inside it to run the animations.

The whole page. One file, nothing left out.

1,494 lines · 85.3 KB · imports React only. This is the exact file behind the preview above — every section, interaction and style.

  1. 01Save it as ares-colony.tsx in any React 18+ project.
  2. 02Render <Page /> on a route of its own — it brings its own nav and footer.
  3. 03Swap the copy and tokens. Keep the DESIGN.md next to it so your AI agent follows the system.
ares-colony.tsx
1/**
2 * Ares — a home on Mars. Relocation and habitat company landing page.
3 *
4 * Retro-future rust: basalt blacks, oxide reds, ochre, butterscotch dust,
5 * copper and one cold blue (the Martian sunset). Everything is procedural —
6 * canvas, inline SVG and CSS gradients. No images, no web fonts, no network.
7 *
8 * Motion:
9 * - Hero: a 330vh pinned canvas. Scrolling is a descent from low Mars orbit:
10 * curved limb -> entry glow -> flat horizon with layered dunes, Olympus
11 * Mons, Phobos and Deimos, drifting dust and a sunset that turns the
12 * horizon blue. A live sol clock / temperature panel runs on real Mars
13 * time. Moving the pointer steers a dust devil that swirls the dust.
14 * - One scroll loop writes `--p` (0..1) onto every [data-ares-p] section.
15 * Pure CSS reads it: floor-plan strokes draw room by room, the dome
16 * cross-section draws itself layer by layer, Earth and Mars orbit while
17 * the supply ship flies its transfer arc, manifesto words light up.
18 * - Postcards drift at different speeds, passage tiers toggle between pay
19 * once / lease, the FAQ is a mission-control log that types answers.
20 *
21 * Reduced motion: no pinning, no autoplay; the hero is a static touchdown
22 * composition and every drawing is shown complete.
23 * SSR-safe: no window/document access during render.
24 */
25import { useEffect, useRef, useState, type CSSProperties, type FormEvent, type ReactNode } 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}
40type RGB = [number, number, number];
41const hex = (h: string): RGB => {
42 const n = parseInt(h.slice(1), 16);
43 return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
44};
45const mix = (a: RGB, b: RGB, t: number): RGB => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
46const rgba = (c: RGB, a = 1) => `rgba(${c[0] | 0},${c[1] | 0},${c[2] | 0},${a})`;
47const clamp = (x: number, a = 0, b = 1) => Math.min(b, Math.max(a, x));
48const sm = (a: number, b: number, x: number) => {
49 const t = clamp((x - a) / (b - a));
50 return t * t * (3 - 2 * t);
51};
52const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
53const frac = (x: number) => x - Math.floor(x);
54const v = (o: Record<string, string | number>) => o as CSSProperties;
55
56/* Scene palette */
57const SPACE = hex("#07040a");
58const DAY_TOP = hex("#a85f3c");
59const DAY_HOR = hex("#f4cfae");
60const DUSK_TOP = hex("#1b2140");
61const DUSK_HOR = hex("#8fb4d4");
62const DUNES = ["#d39a72", "#b9663d", "#94432a", "#66291b", "#33140e"].map(hex);
63const NIGHT = hex("#120a14");
64
65type Star = { x: number; y: number; r: number; ph: number };
66type Part = { x: number; y: number; z: number; s: number };
67type Ptr = { x: number; y: number; a: number };
68type Devil = { x: number; vis: number };
69
70const STARS: Star[] = (() => {
71 const r = rng(11);
72 return Array.from({ length: 170 }, () => ({ x: r(), y: r() * 0.85, r: 0.4 + r() * 1.3, ph: r() * 6.28 }));
73})();
74const BLOTCHES = (() => {
75 const r = rng(5);
76 return Array.from({ length: 16 }, () => ({ x: r(), y: r(), rx: 0.05 + r() * 0.16, ry: 0.01 + r() * 0.05, a: 0.18 + r() * 0.3 }));
77})();
78const ROCKS = (() => {
79 const r = rng(21);
80 return Array.from({ length: 9 }, () => ({ x: r(), w: 0.015 + r() * 0.05, h: 0.01 + r() * 0.03 }));
81})();
82
83const ridge = (x: number, ph: number, f: number) => Math.sin(x * f + ph) + 0.45 * Math.sin(x * f * 2.3 + ph * 1.7) + 0.2 * Math.sin(x * f * 5.1 + ph * 0.6);
84
85function drawMoon(c: CanvasRenderingContext2D, x: number, y: number, r: number, rot: number, a: number) {
86 if (a <= 0.01) return;
87 c.save();
88 c.globalAlpha = a;
89 c.translate(x, y);
90 c.rotate(rot);
91 const g = c.createRadialGradient(-r * 0.4, -r * 0.4, r * 0.1, 0, 0, r * 1.3);
92 g.addColorStop(0, "#e2c4a4");
93 g.addColorStop(0.6, "#8a6a58");
94 g.addColorStop(1, "#3a2a26");
95 c.fillStyle = g;
96 c.beginPath();
97 c.ellipse(0, 0, r * 1.25, r * 0.95, 0, 0, Math.PI * 2);
98 c.fill();
99 c.fillStyle = "rgba(40,24,20,.35)";
100 c.beginPath();
101 c.arc(r * 0.3, r * 0.1, r * 0.28, 0, Math.PI * 2);
102 c.fill();
103 c.restore();
104}
105
106function drawScene(c: CanvasRenderingContext2D, W: number, H: number, dpr: number, p: number, t: number, dt: number, ptr: Ptr, dev: Devil, parts: Part[], moving: boolean) {
107 c.setTransform(dpr, 0, 0, dpr, 0, 0);
108 const d = sm(0.04, 0.62, p);
109 const s = sm(0.55, 0.9, p);
110 const hy = H * lerp(0.78, 0.6, d);
111 const k0 = (0.3 * H) / ((W / 2) * (W / 2));
112 const k = k0 * Math.pow(1 - d, 2.2);
113 const yAt = (x: number) => hy + k * (x - W / 2) * (x - W / 2);
114 const small = W < 600 ? 0.75 : 1;
115 const bump = sm(0.26, 0.4, p) * (1 - sm(0.46, 0.62, p));
116
117 c.save();
118 if (bump > 0.05 && moving) c.translate((Math.random() - 0.5) * 3 * bump, (Math.random() - 0.5) * 3 * bump);
119
120 /* space + sky */
121 c.fillStyle = rgba(SPACE);
122 c.fillRect(-10, -10, W + 20, H + 20);
123 const top = mix(DAY_TOP, DUSK_TOP, s);
124 const hor = mix(DAY_HOR, DUSK_HOR, s);
125 const sky = c.createLinearGradient(0, 0, 0, hy + 20);
126 sky.addColorStop(0, rgba(top));
127 sky.addColorStop(0.55, rgba(mix(top, hor, 0.5)));
128 sky.addColorStop(1, rgba(hor));
129 c.globalAlpha = Math.pow(d, 1.15);
130 c.fillStyle = sky;
131 c.fillRect(-10, -10, W + 20, H + 20);
132 c.globalAlpha = 1;
133
134 /* stars */
135 const starA = Math.max(1 - d * 1.15, s * 0.55);
136 if (starA > 0.02) {
137 for (const st of STARS) {
138 const tw = 0.6 + 0.4 * Math.sin(t * 1.3 + st.ph);
139 c.fillStyle = `rgba(255,236,214,${(starA * tw).toFixed(3)})`;
140 c.beginPath();
141 c.arc(st.x * W, st.y * hy * 0.92, st.r * small, 0, 6.283);
142 c.fill();
143 }
144 }
145
146 /* sun (sets in the final third) */
147 const sunA = sm(0.3, 0.7, p);
148 if (sunA > 0.01) {
149 const sx = W * lerp(0.2, 0.3, s);
150 const sy = hy - H * lerp(0.4, 0.012, s);
151 const sr = Math.max(9, W * 0.012);
152 const glowR = sr * lerp(11, 20, s);
153 const gl = c.createRadialGradient(sx, sy, 0, sx, sy, glowR);
154 const gc = mix(hex("#fff0cf"), hex("#8fc0ee"), s);
155 gl.addColorStop(0, rgba(gc, 0.85 * sunA));
156 gl.addColorStop(0.18, rgba(gc, 0.38 * sunA));
157 gl.addColorStop(1, rgba(gc, 0));
158 c.fillStyle = gl;
159 c.fillRect(sx - glowR, sy - glowR, glowR * 2, glowR * 2);
160 c.fillStyle = rgba(mix(hex("#fff6e0"), hex("#d6ecff"), s), sunA);
161 c.beginPath();
162 c.arc(sx, sy, sr, 0, 6.283);
163 c.fill();
164 }
165
166 /* moons */
167 const px = W * (1.06 - frac(t * 0.006 + 0.3 + p * 0.25) * 1.2);
168 drawMoon(c, px, hy - H * (0.3 + 0.05 * Math.sin((px / W) * 3)), lerp(3, 9, d) * small, t * 0.3, 1 - s * 0.15);
169 drawMoon(c, W * frac(0.62 + t * 0.0016 + p * 0.1), hy - H * 0.5, lerp(2, 4.6, d) * small, t * 0.2, 0.9);
170
171 /* planet body */
172 const horizon = new Path2D();
173 horizon.moveTo(-12, yAt(-12));
174 for (let x = -12; x <= W + 12; x += 14) horizon.lineTo(x, yAt(x));
175 const planet = new Path2D(horizon);
176 planet.lineTo(W + 12, H + 12);
177 planet.lineTo(-12, H + 12);
178 planet.closePath();
179 const glowA = (1 - d) * 0.9;
180 if (glowA > 0.02) {
181 c.save();
182 c.shadowColor = "rgba(255,140,80,.9)";
183 c.shadowBlur = 46;
184 c.strokeStyle = `rgba(255,160,100,${glowA})`;
185 c.lineWidth = 5;
186 c.stroke(horizon);
187 c.shadowColor = "rgba(130,180,230,.8)";
188 c.shadowBlur = 24;
189 c.strokeStyle = `rgba(140,190,240,${glowA * 0.45})`;
190 c.lineWidth = 2;
191 c.translate(0, -4);
192 c.stroke(horizon);
193 c.restore();
194 }
195 const pg = c.createLinearGradient(0, hy, 0, H);
196 pg.addColorStop(0, rgba(mix(hex("#c0623a"), hor, d * 0.7)));
197 pg.addColorStop(0.5, "#8a3b24");
198 pg.addColorStop(1, "#3d160f");
199 c.fillStyle = pg;
200 c.fill(planet);
201 if (d < 0.98) {
202 c.save();
203 c.clip(planet);
204 for (const b of BLOTCHES) {
205 const bx = frac(b.x + p * 0.5) * (W * 1.4) - W * 0.2;
206 const by = hy + b.y * (H - hy) * 0.9 + 6;
207 c.fillStyle = `rgba(50,16,10,${(b.a * (1 - d)).toFixed(3)})`;
208 c.beginPath();
209 c.ellipse(bx, by, b.rx * W, b.ry * H * 1.4 + 2, 0, 0, 6.283);
210 c.fill();
211 }
212 const term = c.createLinearGradient(0, 0, W, 0);
213 term.addColorStop(0, `rgba(0,0,0,${0.62 * (1 - d)})`);
214 term.addColorStop(0.55, "rgba(0,0,0,0)");
215 c.fillStyle = term;
216 c.fillRect(0, 0, W, H);
217 c.restore();
218 }
219
220 /* entry heat */
221 if (bump > 0.02) {
222 const eg = c.createRadialGradient(W * 0.5, H * 1.02, 0, W * 0.5, H * 1.02, H * 1.1);
223 eg.addColorStop(0, `rgba(255,170,90,${0.7 * bump})`);
224 eg.addColorStop(0.4, `rgba(220,80,40,${0.35 * bump})`);
225 eg.addColorStop(1, "rgba(120,20,10,0)");
226 c.fillStyle = eg;
227 c.fillRect(0, 0, W, H);
228 c.strokeStyle = `rgba(255,190,130,${0.5 * bump})`;
229 c.lineWidth = 1;
230 for (let i = 0; i < 34; i++) {
231 const sx = frac(i * 0.6180339) * W;
232 const sy = frac(t * (0.9 + (i % 5) * 0.25) + i * 0.17) * H * 1.2 - H * 0.1;
233 c.beginPath();
234 c.moveTo(sx, sy);
235 c.lineTo(sx + (sx - W / 2) * 0.03, sy + 40 + (i % 4) * 22);
236 c.stroke();
237 }
238 }
239
240 /* Olympus Mons */
241 const surf = sm(0.3, 0.7, p);
242 const off = (i: number) => Math.pow(1 - d, 1.5) * H * 0.42 * (1 + i * 0.1);
243 const sh = (ptr.x - 0.5) * 24;
244 if (surf > 0.01) {
245 const ox = W * 0.7 + sh * 0.3 - p * 30;
246 const base = hy + 6 + off(0);
247 const ow = W * 0.62;
248 const oh = Math.min(H * 0.1, ow * 0.11);
249 const oc = mix(mix(DUNES[0], hor, 0.55), NIGHT, s * 0.5);
250 c.globalAlpha = surf;
251 c.fillStyle = rgba(oc);
252 c.beginPath();
253 c.moveTo(ox - ow / 2, base);
254 c.bezierCurveTo(ox - ow * 0.32, base - oh * 0.2, ox - ow * 0.2, base - oh * 0.94, ox - ow * 0.07, base - oh);
255 c.lineTo(ox - ow * 0.035, base - oh * 0.93);
256 c.lineTo(ox + ow * 0.035, base - oh * 0.94);
257 c.lineTo(ox + ow * 0.07, base - oh);
258 c.bezierCurveTo(ox + ow * 0.2, base - oh * 0.94, ox + ow * 0.34, base - oh * 0.2, ox + ow / 2, base);
259 c.closePath();
260 c.fill();
261 c.globalAlpha = 1;
262 }
263
264 /* dunes */
265 for (let i = 0; i < 5; i++) {
266 const baseY = hy + (i + 0.5) * H * 0.055 * (0.4 + d * 0.6) + off(i);
267 const amp = H * (0.012 + 0.011 * i) * d;
268 const par = sh * (i + 1) * 0.5 + p * (i + 1) * 40;
269 let col = DUNES[i];
270 if (i < 2) col = mix(col, hor, i === 0 ? 0.32 : 0.12);
271 col = mix(col, NIGHT, s * (0.45 + i * 0.1));
272 c.fillStyle = rgba(col);
273 c.beginPath();
274 c.moveTo(-10, H + 10);
275 for (let x = -10; x <= W + 10; x += 12) {
276 const y = baseY + amp * ridge(x + par, i * 1.9, 0.004 + i * 0.0014) - (i === 0 ? 0 : 0);
277 c.lineTo(x, Math.max(y, yAt(x) - 2));
278 }
279 c.lineTo(W + 10, H + 10);
280 c.closePath();
281 c.fill();
282 if (i === 4 && d > 0.5) {
283 c.fillStyle = rgba(mix(DUNES[4], NIGHT, 0.5));
284 for (const r of ROCKS) {
285 const rx = r.x * W;
286 const ry = baseY + amp * ridge(rx + par, i * 1.9, 0.004 + i * 0.0014) + 4;
287 c.beginPath();
288 c.moveTo(rx - r.w * W, ry + 4);
289 c.lineTo(rx - r.w * W * 0.3, ry - r.h * H);
290 c.lineTo(rx + r.w * W * 0.5, ry - r.h * H * 0.6);
291 c.lineTo(rx + r.w * W, ry + 4);
292 c.closePath();
293 c.fill();
294 }
295 }
296 }
297
298 /* dust devil + dust */
299 const recent = t * 1000 - ptr.a < 2400;
300 const tx = recent ? ptr.x : 0.5 + 0.36 * Math.sin(t * 0.06 + 1);
301 dev.x += (tx - dev.x) * 0.045;
302 dev.vis += ((recent ? 1 : 0.5) - dev.vis) * 0.04;
303 const dAmt = sm(0.68, 0.92, p);
304 const bx = dev.x * W;
305 const by = hy + H * 0.2;
306 const dvis = dev.vis * dAmt;
307 for (const q of parts) {
308 q.x += (0.012 + 0.05 * q.z * q.z) * dt * (0.6 + dAmt);
309 if (q.x > 1.05) q.x = -0.05;
310 let qx = q.x * W;
311 let qy = H * (0.28 + 0.72 * q.y) + Math.sin(t * 0.6 + q.s * 6) * 5;
312 if (dvis > 0.02) {
313 const dx = qx - bx;
314 const dy = (qy - by) * 2.2;
315 const r = Math.hypot(dx, dy);
316 const R = W * 0.16;
317 if (r < R) {
318 const w = Math.pow(1 - r / R, 1.4) * dvis;
319 const ang = Math.atan2(dy, dx) + w * (t * 3 + q.s * 3);
320 qx = bx + Math.cos(ang) * r;
321 qy = by + (Math.sin(ang) * r) / 2.2 - w * 60 * (0.4 + q.s);
322 }
323 }
324 const a = (0.12 + q.z * 0.42) * (0.25 + dAmt * 0.75) * d;
325 if (a < 0.01) continue;
326 c.fillStyle = `rgba(${mix(hex("#f3c79e"), hex("#8fa6c4"), s)
327 .map((n) => n | 0)
328 .join(",")},${a.toFixed(3)})`;
329 c.beginPath();
330 c.ellipse(qx, qy, (0.7 + q.z * 2.1) * small * (1 + dAmt), 0.6 + q.z * 0.9, 0, 0, 6.283);
331 c.fill();
332 }
333 if (dvis > 0.03) {
334 for (let j = 0; j < 18; j++) {
335 const u = j / 17;
336 const hh = H * 0.26;
337 const cx = bx + Math.sin(t * 3.1 - u * 5) * 16 * u + (dev.x - tx) * 300 * u;
338 const cy = by - u * hh;
339 const rx = (8 + u * u * 46) * small;
340 c.fillStyle = rgba(mix(hex("#d9a070"), hex("#7f8fb0"), s), 0.2 * (1 - u * 0.65) * dvis);
341 c.beginPath();
342 c.ellipse(cx, cy, rx, rx * 0.2, 0, 0, 6.283);
343 c.fill();
344 }
345 }
346 c.restore();
347}
348
349/* ------------------------------------------------------------------ */
350/* Mars time */
351/* ------------------------------------------------------------------ */
352type MarsNow = { sol: string; lmst: string; temp: number; wind: number };
353function marsNow(): MarsNow {
354 const ms = Date.now();
355 const jd = ms / 86400000 + 2440587.5;
356 const jdTT = jd + (37 + 32.184) / 86400;
357 const msd = (jdTT - 2451549.5) / 1.0274912517 + 44796.0 - 0.0009626;
358 const mtc = frac(msd) * 24;
359 const lmst = (mtc + 77.45 / 15 + 24) % 24;
360 const hh = Math.floor(lmst);
361 const mm = Math.floor((lmst - hh) * 60);
362 const ss = Math.floor((((lmst - hh) * 60 - mm) * 60) % 60);
363 const f = lmst / 24;
364 const temp = -63 + 36 * Math.sin(2 * Math.PI * (f - 0.3)) + Math.sin(ms / 4000) * 0.6;
365 return {
366 sol: Math.floor(msd).toLocaleString("en-US"),
367 lmst: [hh, mm, ss].map((n) => String(n).padStart(2, "0")).join(":"),
368 temp,
369 wind: 6 + 4 * Math.sin(ms / 9000) + 3 * Math.sin(f * 12),
370 };
371}
372
373const PHASES: [number, string][] = [
374 [0.08, "Low Mars orbit"],
375 [0.26, "Deorbit burn"],
376 [0.4, "Entry interface"],
377 [0.54, "Peak heating"],
378 [0.68, "Parachute deploy"],
379 [0.86, "Powered descent"],
380 [2, "Touchdown, Jezero"],
381];
382
383/* ------------------------------------------------------------------ */
384/* Hero */
385/* ------------------------------------------------------------------ */
386function Hero() {
387 const heroRef = useRef<HTMLElement>(null);
388 const canvasRef = useRef<HTMLCanvasElement>(null);
389 const altRef = useRef<HTMLSpanElement>(null);
390 const velRef = useRef<HTMLSpanElement>(null);
391 const phaseRef = useRef<HTMLSpanElement>(null);
392 const railRef = useRef<HTMLSpanElement>(null);
393 const ptr = useRef<Ptr>({ x: 0.5, y: 0.5, a: -99999 });
394 const [mn, setMn] = useState<MarsNow | null>(null);
395
396 useEffect(() => {
397 const tick = () => setMn(marsNow());
398 tick();
399 const id = window.setInterval(tick, 1000);
400 return () => window.clearInterval(id);
401 }, []);
402
403 useEffect(() => {
404 const hero = heroRef.current;
405 const cv = canvasRef.current;
406 if (!hero || !cv) return;
407 const ctx = cv.getContext("2d");
408 if (!ctx) return;
409 const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
410 const rr = rng(9);
411 const parts: Part[] = Array.from({ length: 210 }, () => ({ x: rr(), y: rr(), z: rr(), s: rr() }));
412 const dev: Devil = { x: 0.5, vis: 0.5 };
413 let W = 0;
414 let H = 0;
415 let dpr = 1;
416 let pp = reduce ? 0.93 : 0;
417 let visible = true;
418 let raf = 0;
419 let last = performance.now() / 1000;
420 let lastPhase = "";
421 let lastAlt = "";
422
423 const size = () => {
424 const r = cv.getBoundingClientRect();
425 dpr = Math.min(window.devicePixelRatio || 1, W0() < 600 ? 1.5 : 2);
426 W = r.width;
427 H = r.height;
428 cv.width = Math.max(1, Math.round(W * dpr));
429 cv.height = Math.max(1, Math.round(H * dpr));
430 };
431 const W0 = () => window.innerWidth;
432 const hud = (p: number) => {
433 const d = sm(0.04, 0.62, p);
434 const alt = p > 0.93 ? 0 : 410 * Math.pow(1 - d, 1.6);
435 const altS = alt >= 100 ? Math.round(alt).toString() : alt.toFixed(1);
436 if (altS !== lastAlt && altRef.current) {
437 altRef.current.textContent = altS;
438 lastAlt = altS;
439 }
440 if (velRef.current) velRef.current.textContent = (p > 0.93 ? 0 : 3.42 * (1 - sm(0.1, 0.86, p) * 0.97)).toFixed(2);
441 const ph = PHASES.find((x) => p < x[0])![1];
442 if (ph !== lastPhase && phaseRef.current) {
443 phaseRef.current.textContent = ph;
444 lastPhase = ph;
445 }
446 if (railRef.current) railRef.current.style.transform = `translateY(${(p * 100).toFixed(1)}%)`;
447 hero.style.setProperty("--hp", p.toFixed(3));
448 };
449 const frame = (now: number) => {
450 raf = requestAnimationFrame(frame);
451 const t = now / 1000;
452 const dt = Math.min(0.05, t - last);
453 last = t;
454 if (!visible || document.hidden) return;
455 const r = hero.getBoundingClientRect();
456 const total = Math.max(1, r.height - window.innerHeight);
457 const target = clamp(-r.top / total);
458 pp += (target - pp) * 0.1;
459 drawScene(ctx, W, H, dpr, pp, t, dt, ptr.current, dev, parts, true);
460 hud(pp);
461 };
462 size();
463 const ro = new ResizeObserver(() => {
464 size();
465 if (reduce) {
466 drawScene(ctx, W, H, dpr, pp, 0, 0, ptr.current, dev, parts, false);
467 hud(pp);
468 }
469 });
470 ro.observe(cv);
471 const io = new IntersectionObserver((e) => {
472 visible = e[0].isIntersecting;
473 });
474 io.observe(hero);
475 if (reduce) {
476 drawScene(ctx, W, H, dpr, pp, 0, 0, ptr.current, dev, parts, false);
477 hud(pp);
478 } else {
479 raf = requestAnimationFrame(frame);
480 }
481 return () => {
482 cancelAnimationFrame(raf);
483 ro.disconnect();
484 io.disconnect();
485 };
486 }, []);
487
488 const onMove = (e: React.PointerEvent) => {
489 const r = e.currentTarget.getBoundingClientRect();
490 ptr.current = { x: (e.clientX - r.left) / r.width, y: (e.clientY - r.top) / r.height, a: performance.now() };
491 };
492
493 return (
494 <header className="ares-hero" ref={heroRef} id="top" style={v({ "--hp": 0 })}>
495 <div className="ares-hero-stage" onPointerMove={onMove}>
496 <canvas ref={canvasRef} className="ares-hero-cv" aria-hidden="true" />
497 <div className="ares-grain" aria-hidden="true" />
498 <div className="ares-vig" aria-hidden="true" />
499
500 <div className="ares-hud ares-hud-l" aria-hidden="true">
501 <div className="ares-hud-k">Descent telemetry</div>
502 <div className="ares-hud-row"><span>ALT</span><b><span ref={altRef}>410</span> km</b></div>
503 <div className="ares-hud-row"><span>VEL</span><b><span ref={velRef}>3.42</span> km/s</b></div>
504 <div className="ares-hud-row"><span>PHASE</span><b ref={phaseRef}>Low Mars orbit</b></div>
505 </div>
506
507 <div className="ares-hud ares-hud-r" role="status" aria-label="Live Mars time at Jezero Crater">
508 <div className="ares-hud-k">Jezero Crater · live</div>
509 <div className="ares-hud-row"><span>SOL</span><b>{mn ? mn.sol : "-----"}</b></div>
510 <div className="ares-hud-row"><span>LMST</span><b className="ares-big">{mn ? mn.lmst : "--:--:--"}</b></div>
511 <div className="ares-hud-row"><span>TEMP</span><b>{mn ? `${mn.temp.toFixed(1)} °C` : "--"}</b></div>
512 <div className="ares-hud-row ares-hide-s"><span>WIND</span><b>{mn ? `${mn.wind.toFixed(1)} m/s` : "--"}</b></div>
513 </div>
514
515 <div className="ares-hero-a">
516 <p className="ares-eyebrow">Ares Habitat Company · Passage opens Jan 2029</p>
517 <h1 className="ares-h1">
518 A home on <em>Mars.</em>
519 </h1>
520 <p className="ares-lede">Pressurised, shielded, furnished and waiting. Scroll to ride the descent.</p>
521 </div>
522
523 <div className="ares-hero-b">
524 <p className="ares-eyebrow">Sol 0 · Touchdown confirmed</p>
525 <h2 className="ares-h2 ares-h2-hero">Welcome home. Mind the dust.</h2>
526 <div className="ares-cta-row">
527 <a className="ares-btn ares-btn-solid" href="#passage">Reserve your passage</a>
528 <a className="ares-btn ares-btn-ghost" href="#habitat">Tour the Meridian home</a>
529 </div>
530 </div>
531
532 <div className="ares-rail" aria-hidden="true"><span ref={railRef} /></div>
533 <div className="ares-scrollcue" aria-hidden="true"><span>Descend</span><i /></div>
534 </div>
535 </header>
536 );
537}
538
539/* ------------------------------------------------------------------ */
540/* Content */
541/* ------------------------------------------------------------------ */
542const MANIFESTO =
543 "Every settlement begins as one room with a window. We build the room, pressurise the dream, and hand you the key at six millibars.".split(" ");
544
545const STATS = [
546 { n: "148", u: "m²", l: "Private living space in a Meridian home" },
547 { n: "3.0", u: "m", l: "Of packed regolith over every pressure shell" },
548 { n: "94", u: "%", l: "Of household water recovered and reused" },
549 { n: "0.38", u: "g", l: "Gravity, so the stairs are a pleasure" },
550];
551
552type Room = { name: string; area: number; x: number; y: number; w: number; h: number; note: string };
553const ROOMS: Room[] = [
554 { name: "Greenhouse", area: 31, x: 60, y: 60, w: 330, h: 180, note: "Soil-less beds under tuned LEDs: greens, dwarf tomatoes, herbs. It scrubs the air and smells like rain." },
555 { name: "Sleep pods", area: 18, x: 390, y: 60, w: 170, h: 180, note: "Three quiet cells with their own shielded wall. Heavy sleepers wanted; the wind sings on the dome." },
556 { name: "Water & air", area: 14, x: 560, y: 60, w: 180, h: 180, note: "Recycler, electrolysis and the ice-melt loop. The one room you will learn to love being boring." },
557 { name: "Airlock", area: 12, x: 60, y: 260, w: 130, h: 200, note: "Two-stage lock with dust brushing, a suit rack and a bench to sit on when you get back." },
558 { name: "Observation lounge", area: 42, x: 190, y: 260, w: 300, h: 200, note: "A curved four-metre window facing the crater rim. Where every resident ends up at sunset." },
559 { name: "Galley", area: 17, x: 490, y: 260, w: 150, h: 200, note: "Induction hob, steam oven and a long table. Dinner is the social contract of the colony." },
560 { name: "Workshop", area: 14, x: 640, y: 260, w: 100, h: 200, note: "Printer, bench and spares for your own repairs. Everything on Mars is eventually a repair." },
561];
562
563const DOME_LAYERS = [
564 { t: "Regolith shield", d: "Three metres of sintered Martian soil over the shell. Cuts radiation to a fraction of surface dose." },
565 { t: "Aerogel-ETFE shell", d: "A bonded pressure envelope with aerogel insulation. Holds 70 kPa and shrugs off dust storms." },
566 { t: "Living deck", d: "Two levels, 148 m², floors cantilevered off a basalt-fibre spine. Normal air, normal doors." },
567 { t: "Greenhouse ring", d: "A band of beds hugging the sunlit curve. Dinner, oxygen and a view of something green." },
568 { t: "Ice and water store", d: "Mined ice melts into a buried tank that doubles as thermal mass and a second radiation wall." },
569 { t: "Airlock and rover port", d: "A pressurised tunnel to the vehicle bay, so a trip to the ridge never starts with a spacewalk." },
570];
571
572const WINDOWS = [
573 { id: "01", date: "14 Jan 2029", ship: "Meridian Dawn", load: "12 residents · 80 t cargo", arr: "Arrives 30 Sep 2029", st: "Manifest 71% full", a: 0.1 },
574 { id: "02", date: "09 Mar 2031", ship: "Meridian Dusk", load: "16 residents · 110 t cargo", arr: "Arrives 23 Nov 2031", st: "Reservations open", a: 0.34 },
575 { id: "03", date: "21 Apr 2033", ship: "Perihelion", load: "24 residents · 160 t cargo", arr: "Arrives 05 Jan 2034", st: "Waitlist only", a: 0.58 },
576 { id: "04", date: "03 Jun 2035", ship: "Perihelion II", load: "30 residents · 220 t cargo", arr: "Arrives 17 Feb 2036", st: "Pre-registration", a: 0.82 },
577];
578
579const STORIES = [
580 {
581 n: "Ines Okafor-Lindqvist",
582 r: "Hydroponics agronomist · Sol 1,204",
583 q: "I grew up on a Swedish potato farm and now I grow basil under a dome. My daughter thinks the dust is just a very patient dog. The first tomato took 61 sols and I did not share it.",
584 h: "Meridian home 0417",
585 },
586 {
587 n: "Tomasz Reyes",
588 r: "Structural welder · Sol 842",
589 q: "Gravity is kind to the knees and brutal to the ego, because you will jump too hard for a month. I weld seams that hold a house together. The sunset is the one blue thing here and it is worth the trip.",
590 h: "Founder ring, plot 12",
591 },
592 {
593 n: "Dr. Mireille Anand",
594 r: "Retired GP, now the clinic · Sol 1,977",
595 q: "I am 71 and my bones were the main worry. We have a centrifuge, a routine and an excellent view. People ask if I miss Lyon. I miss the bread. Bakery rota is Thursday.",
596 h: "Meridian home 0102",
597 },
598];
599
600const TIERS = [
601 { name: "Surveyor", tag: "Share a pod, shape the plan", base: 310000, perks: ["Seat on a Meridian cruiser", "Shared residence pod, 18 m²", "Return passage included", "Two-year survey contract"] },
602 { name: "Settler", tag: "Your own front door", base: 780000, perks: ["Private Meridian home, 148 m²", "Greenhouse bed and water quota", "Optional return passage", "Rover share, 12 days a month"], hot: true },
603 { name: "Founder", tag: "Build the next dome", base: 2400000, perks: ["Founder ring plot and expansion rights", "Priority cargo on every window", "Two guest passages a decade", "Seat on the colony council"] },
604];
605
606const LOG = [
607 { t: "T-0214", q: "Is it actually survivable?", a: "Yes, inside the dome. Outside it is 6 millibars, minus 63 °C on average and unbreathable. Everything we sell is designed to be boring to survive: redundant seals, three independent life-support loops and a shelter you can reach in under two minutes from any room." },
608 { t: "T-0187", q: "How long is the journey?", a: "259 days on a minimum-energy transfer, roughly eight and a half months. The cruiser spins for 0.38 g, has a private cabin per resident and a gym you will resent. Windows open every 26 months when Earth and Mars line up." },
609 { t: "T-0162", q: "Can I come back?", a: "Surveyor passage includes a return seat. Settler and Founder tiers can add one. The return window is also every 26 months, and your body needs about eight weeks of centrifuge work to readapt before you board." },
610 { t: "T-0139", q: "What will I eat?", a: "Mostly what the greenhouse ring grows: potatoes, soy, greens, dwarf wheat, tomatoes, herbs. Protein is fermented and printed, with imported freeze-dried luxuries on each supply ship. Coffee is the first thing we cargo, every time." },
611 { t: "T-0121", q: "What about radiation?", a: "Three metres of regolith over your home cuts the dose to roughly the level of a frequent flyer on Earth. Cruise is the worst part; the cruiser has a storm shelter lined with water. Your dosimeter badge syncs to your medical record weekly." },
612 { t: "T-0098", q: "How do I talk to people on Earth?", a: "One-way light delay is 4 to 24 minutes, so live calls are out. We provide an asynchronous relay: video letters, messages and shared documents sync every sol. Weekly live windows are possible at closest approach." },
613 { t: "T-0073", q: "Do I own the land?", a: "No one can own Mars; the Outer Space Treaty forbids it. You own or lease your habitat, your equipment and your claim to a share of the shared infrastructure. Founders hold long leases on a plot's use." },
614 { t: "T-0041", q: "Can I bring my family?", a: "Yes. Children over six are accepted on Settler and Founder tiers, subject to medical review. Medical, schooling and a sunny, slightly dusty playground are all inside the dome. Pets are not yet cleared; we are working on it." },
615];
616
617/* ------------------------------------------------------------------ */
618/* Section components */
619/* ------------------------------------------------------------------ */
620function Mark() {
621 return (
622 <svg viewBox="0 0 32 32" width="28" height="28" aria-hidden="true">
623 <circle cx="16" cy="16" r="13" fill="none" stroke="currentColor" strokeWidth="2" />
624 <path d="M3 20 Q10 14 16 19 T29 17 V29 H3Z" fill="currentColor" />
625 <circle cx="22" cy="9" r="2.4" fill="currentColor" />
626 </svg>
627 );
628}
629
630function Nav({ small }: { small: boolean }) {
631 return (
632 <nav className={`ares-nav${small ? " is-small" : ""}`} aria-label="Primary">
633 <a className="ares-brand" href="#top" aria-label="Ares home"><Mark /><span>ARES</span></a>
634 <div className="ares-nav-links">
635 <a href="#habitat">Habitat</a>
636 <a href="#dome">Dome</a>
637 <a href="#windows">Windows</a>
638 <a href="#residents">Residents</a>
639 <a href="#log">Mission log</a>
640 </div>
641 <a className="ares-btn ares-btn-solid ares-btn-sm" href="#passage">Reserve passage</a>
642 </nav>
643 );
644}
645
646function FloorPlan() {
647 const [sel, setSel] = useState(4);
648 const furn: Record<number, ReactNode> = {
649 0: (
650 <g className="ares-furn">
651 {[0, 1, 2, 3].map((r) => (
652 <g key={r}>
653 <path d={`M80 ${100 + r * 34} H370`} strokeDasharray="4 5" />
654 {Array.from({ length: 12 }, (_, c) => <circle key={c} cx={92 + c * 24} cy={100 + r * 34} r="4.5" />)}
655 </g>
656 ))}
657 </g>
658 ),
659 1: <g className="ares-furn">{[0, 1, 2].map((i) => <path key={i} d={`M406 ${76 + i * 55} h78 v40 h-78z M470 ${86 + i * 55} h8 v20 h-8z`} />)}</g>,
660 2: <g className="ares-furn"><circle cx="650" cy="150" r="42" /><circle cx="650" cy="150" r="26" /><path d="M582 84 h40 v26 h-40z M582 196 h40 v26 h-40z" /></g>,
661 3: <g className="ares-furn"><path d="M72 272 h54 v170 h-54z M126 300 a26 26 0 0 1 26 26 M126 410 a26 26 0 0 0 26 -26" /><path d="M72 442 h106" /></g>,
662 4: <g className="ares-furn"><path d="M230 410 h150 v34 h-150z M246 424 h118" /><circle cx="330" cy="344" r="26" /><path d="M200 456 A140 120 0 0 1 480 456" /></g>,
663 5: <g className="ares-furn"><path d="M504 274 h124 v68" /><circle cx="570" cy="400" r="30" /><circle cx="570" cy="400" r="4" /></g>,
664 6: <g className="ares-furn"><path d="M652 280 h76 v26 h-76z M652 420 h76 M652 436 h76 M660 330 l26 0 l0 50 l-26 0z" /></g>,
665 };
666 return (
667 <section className="ares-sec ares-plan ares-pin" id="habitat" data-ares-p="pin" style={v({ "--pin": "320vh" })}>
668 <div className="ares-pin-stage">
669 <div className="ares-wrap ares-plan-grid">
670 <div className="ares-plan-copy">
671 <p className="ares-eyebrow ares-eyebrow-ink">01 · The Meridian home</p>
672 <h2 className="ares-h2">One level, seven rooms, <em>zero compromises</em> on light.</h2>
673 <ul className="ares-roomlist">
674 {ROOMS.map((r, i) => (
675 <li key={r.name} style={v({ "--s": 0.06 + i * 0.1, "--d": 0.12 })}>
676 <button type="button" className={sel === i ? "is-sel" : ""} onMouseEnter={() => setSel(i)} onFocus={() => setSel(i)} onClick={() => setSel(i)}>
677 <span className="ares-rn">{String(i + 1).padStart(2, "0")}</span>
678 <span className="ares-rt">{r.name}</span>
679 <span className="ares-ra">{r.area} m²</span>
680 </button>
681 </li>
682 ))}
683 </ul>
684 <p className="ares-roomnote" aria-live="polite"><b>{ROOMS[sel].name}.</b> {ROOMS[sel].note}</p>
685 </div>
686 <div className="ares-plan-fig">
687 <svg viewBox="0 0 800 520" role="img" aria-label="Floor plan of the Meridian home with seven rooms">
688 <defs>
689 <pattern id="ares-grid" width="20" height="20" patternUnits="userSpaceOnUse"><path d="M20 0H0V20" fill="none" stroke="rgba(140,47,27,.14)" strokeWidth="1" /></pattern>
690 </defs>
691 <rect x="0" y="0" width="800" height="520" fill="url(#ares-grid)" />
692 <path className="ares-draw ares-hull" pathLength="1" style={v({ "--s": 0, "--d": 0.12 })} d="M100 40 H700 A60 60 0 0 1 760 100 V420 A60 60 0 0 1 700 480 H100 A60 60 0 0 1 40 420 V100 A60 60 0 0 1 100 40Z" />
693 {ROOMS.map((r, i) => {
694 const s = 0.06 + i * 0.1;
695 return (
696 <g key={r.name} className={`ares-room${sel === i ? " is-sel" : ""}`} style={v({ "--s": s, "--d": 0.12 })} onMouseEnter={() => setSel(i)} onClick={() => setSel(i)}>
697 <path className="ares-room-fill" d={`M${r.x} ${r.y} h${r.w} v${r.h} h${-r.w}z`} />
698 <path className="ares-draw ares-room-line" pathLength="1" d={`M${r.x} ${r.y} h${r.w} v${r.h} h${-r.w}z`} />
699 <g className="ares-room-in">{furn[i]}</g>
700 <circle className="ares-pn" cx={r.x + 24} cy={r.y + 24} r="15" />
701 <text className="ares-pnt" x={r.x + 24} y={r.y + 30} textAnchor="middle">{i + 1}</text>
702 <text className="ares-rname" x={r.x + 46} y={r.y + 29}>{r.name.toUpperCase()}</text>
703 <text className="ares-rarea" x={r.x + 46} y={r.y + 47}>{r.area} M²</text>
704 </g>
705 );
706 })}
707 <g className="ares-dim" style={v({ "--s": 0.8, "--d": 0.1 })}>
708 <path d="M40 505 H760 M40 498 V512 M760 498 V512" />
709 <text x="400" y="500" textAnchor="middle">24.0 M</text>
710 </g>
711 </svg>
712 </div>
713 </div>
714 </div>
715 </section>
716 );
717}
718
719function Dome() {
720 return (
721 <section className="ares-sec ares-dome ares-pin" id="dome" data-ares-p="pin" style={v({ "--pin": "340vh" })}>
722 <div className="ares-pin-stage">
723 <div className="ares-wrap ares-dome-grid">
724 <div className="ares-dome-copy">
725 <p className="ares-eyebrow">02 · Cross-section</p>
726 <h2 className="ares-h2">A pressure vessel with <em>a garden inside.</em></h2>
727 <ol className="ares-legend">
728 {DOME_LAYERS.map((l, i) => (
729 <li key={l.t} style={v({ "--s": 0.52 + i * 0.07, "--d": 0.06 })}>
730 <span className="ares-ln">{i + 1}</span>
731 <span><b>{l.t}</b><i>{l.d}</i></span>
732 </li>
733 ))}
734 </ol>
735 </div>
736 <div className="ares-dome-fig">
737 <svg viewBox="0 0 900 540" role="img" aria-label="Cross-section of a pressurised dome buried under regolith">
738 <defs>
739 <pattern id="ares-reg" width="12" height="12" patternUnits="userSpaceOnUse"><circle cx="3" cy="3" r="1.2" fill="rgba(217,154,56,.45)" /><circle cx="9" cy="8" r="1" fill="rgba(201,123,72,.4)" /></pattern>
740 <linearGradient id="ares-ice" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor="rgba(123,163,198,.5)" /><stop offset="1" stopColor="rgba(123,163,198,.12)" /></linearGradient>
741 </defs>
742 <path className="ares-d" pathLength="1" style={v({ "--s": 0, "--d": 0.1, stroke: "#d99a38" })} d="M0 384 C80 378 160 388 240 381 S400 376 450 381 S620 388 700 381 S840 376 900 383" />
743 {[430, 462, 494].map((y, i) => (
744 <path key={y} className="ares-d ares-thin" pathLength="1" style={v({ "--s": 0.04 + i * 0.02, "--d": 0.1 })} d={`M0 ${y} C150 ${y - 8} 300 ${y + 8} 450 ${y} S750 ${y - 8} 900 ${y + 4}`} />
745 ))}
746 <path className="ares-fade" style={v({ "--s": 0.1, "--d": 0.14 })} d="M90 381 A360 320 0 0 1 810 381Z" fill="url(#ares-reg)" />
747 <path className="ares-d" pathLength="1" style={v({ "--s": 0.1, "--d": 0.16, stroke: "#c97b48" })} d="M90 381 A360 320 0 0 1 810 381" />
748 <path className="ares-fade" style={v({ "--s": 0.26, "--d": 0.1 })} d="M150 381 A300 270 0 0 1 750 381Z" fill="#26160f" />
749 <path className="ares-d ares-thick" pathLength="1" style={v({ "--s": 0.24, "--d": 0.16, stroke: "#f2d3a0" })} d="M150 381 A300 270 0 0 1 750 381" />
750 <path className="ares-d" pathLength="1" style={v({ "--s": 0.34, "--d": 0.14, stroke: "#e9b98c" })} d="M165 381 A285 255 0 0 1 735 381" />
751 <path className="ares-d" pathLength="1" style={v({ "--s": 0.44, "--d": 0.1, stroke: "#f2d3a0" })} d="M165 381 H735" />
752 <g className="ares-fade" style={v({ "--s": 0.46, "--d": 0.1 })}>
753 <path className="ares-line" d="M230 300 H670 M300 300 V381 M600 300 V381 M450 300 V381" />
754 <path className="ares-line" d="M262 262 h78 v38 h-78z M360 270 h60 v30 h-60z M470 266 h70 v34 h-70z" />
755 <path className="ares-line" d="M250 340 h130 v41 h-130z M410 340 h110 v41 h-110z" />
756 </g>
757 <g className="ares-fade" style={v({ "--s": 0.54, "--d": 0.1 })}>
758 {Array.from({ length: 11 }, (_, i) => {
759 const a = Math.PI * (0.18 + i * 0.064);
760 return <circle key={i} className="ares-plant" cx={450 + 262 * Math.cos(a + 0)} cy={381 - 232 * Math.sin(a)} r="8" />;
761 })}
762 <path className="ares-line" d="M620 381 v-32 M620 349 q-14 -6 -14 -20 M620 349 q14 -6 14 -20 M655 381 v-24 M655 357 q-12 -4 -12 -16 M655 357 q12 -4 12 -16" />
763 </g>
764 <g className="ares-fade" style={v({ "--s": 0.6, "--d": 0.1 })}>
765 <ellipse cx="450" cy="458" rx="130" ry="30" fill="url(#ares-ice)" stroke="#7ba3c6" strokeWidth="2" />
766 <path className="ares-line" d="M450 428 V381" stroke="#7ba3c6" />
767 </g>
768 <g className="ares-fade" style={v({ "--s": 0.66, "--d": 0.1 })}>
769 <path className="ares-line" d="M735 345 H850 V381 H735 M850 345 L870 381" />
770 <path className="ares-line" d="M800 371 h40 M806 371 a6 6 0 1 0 12 0 M826 371 a6 6 0 1 0 12 0" />
771 </g>
772 <path className="ares-air" d="M290 250 C370 170 530 170 610 250" />
773 <path className="ares-air ares-air2" d="M610 275 C530 215 370 215 290 275" />
774 {[
775 [630, 104], [300, 146], [450, 300], [645, 322], [450, 458], [790, 360],
776 ].map(([x, y], i) => (
777 <g key={i} className="ares-mk" style={v({ "--s": 0.52 + i * 0.07, "--d": 0.06 })}>
778 <circle cx={x} cy={y} r="19" />
779 <text x={x} y={y + 7} textAnchor="middle">{i + 1}</text>
780 </g>
781 ))}
782 </svg>
783 </div>
784 </div>
785 </div>
786 </section>
787 );
788}
789
790function Windows() {
791 return (
792 <section className="ares-sec ares-win ares-pin" id="windows" data-ares-p="pin" style={v({ "--pin": "340vh", "--tau": "calc(-0.5 + 1.26 * var(--p))" })}>
793 <div className="ares-pin-stage">
794 <div className="ares-wrap ares-win-grid">
795 <div className="ares-win-copy">
796 <p className="ares-eyebrow">03 · Launch windows</p>
797 <h2 className="ares-h2">Earth and Mars line up <em>every 26 months.</em> Be on the pad.</h2>
798 <div className="ares-readouts">
799 <div><span>Transit day</span><b><i data-ares-count="259" data-from="0.397" data-to="0.96" data-pad="3">000</i><u>/259</u></b></div>
800 <div><span>Distance flown</span><b><i data-ares-count="483" data-from="0.397" data-to="0.96" data-pad="3">000</i><u>M km</u></b></div>
801 </div>
802 <ul className="ares-winlist">
803 {WINDOWS.map((w) => (
804 <li key={w.id} style={v({ "--s": w.a * 0.7, "--d": 0.08 })}>
805 <span className="ares-wid">WIN {w.id}</span>
806 <span className="ares-wmain"><b>{w.date}</b> · {w.ship}<i>{w.load} · {w.arr}</i></span>
807 <span className="ares-wst">{w.st}</span>
808 </li>
809 ))}
810 </ul>
811 </div>
812 <div className="ares-orbit">
813 <svg viewBox="0 0 600 600" role="img" aria-label="Earth and Mars orbiting the Sun while a supply ship flies a transfer arc">
814 <defs>
815 <radialGradient id="ares-sun"><stop offset="0" stopColor="#fff0cf" /><stop offset=".35" stopColor="#f2d3a0" stopOpacity=".7" /><stop offset="1" stopColor="#d99a38" stopOpacity="0" /></radialGradient>
816 </defs>
817 <circle cx="300" cy="300" r="46" fill="url(#ares-sun)" />
818 <circle cx="300" cy="300" r="130" className="ares-orb" />
819 <circle cx="300" cy="300" r="198" className="ares-orb" />
820 <path className="ares-xfer-ghost" d="M430 300 A164 160.4 0 0 0 102 300" />
821 <path className="ares-xfer" pathLength="1" style={v({ "--s": 0.397, "--d": 0.5625 })} d="M430 300 A164 160.4 0 0 0 102 300" />
822 <g className="ares-spin" style={v({ "--rot": "calc(var(--tau) * -360deg)" })}>
823 <circle cx="430" cy="300" r="11" fill="#7ba3c6" />
824 <circle cx="430" cy="300" r="16" fill="none" stroke="#7ba3c6" strokeOpacity=".4" />
825 <text x="430" y="337" textAnchor="middle" className="ares-orb-t">EARTH</text>
826 </g>
827 <g className="ares-spin" style={v({ "--rot": "calc(var(--tau) * -191.4deg)" })}>
828 <g className="ares-spin" style={v({ "--rot": "-44.3deg" })}>
829 <circle cx="498" cy="300" r="9" fill="#c0502c" />
830 <text x="498" y="278" textAnchor="middle" className="ares-orb-t">MARS</text>
831 </g>
832 </g>
833 <circle className="ares-ship" r="5" />
834 </svg>
835 </div>
836 </div>
837 </div>
838 </section>
839 );
840}
841
842function Postcards() {
843 return (
844 <section className="ares-sec ares-res" id="residents" data-ares-p="pass">
845 <div className="ares-wrap">
846 <p className="ares-eyebrow ares-eyebrow-ink" data-ares-reveal>04 · Residents</p>
847 <h2 className="ares-h2 ares-h2-ink" data-ares-reveal>Postcards from <em>Sol 2,114.</em></h2>
848 <div className="ares-cards">
849 {STORIES.map((s, i) => (
850 <figure key={s.n} className="ares-card" data-ares-reveal style={v({ "--i": i, "--rot": `${(i - 1) * 2.6}deg`, "--par": 36 + i * 30 })}>
851 <div className="ares-card-in">
852 <div className="ares-stamp" aria-hidden="true"><svg viewBox="0 0 60 60"><rect x="3" y="3" width="54" height="54" fill="none" stroke="currentColor" strokeWidth="2" strokeDasharray="3 2" /><circle cx="30" cy="34" r="14" fill="currentColor" /><path d="M8 46 Q20 38 30 44 T52 42 V54 H8Z" fill="#f6e9d5" /></svg></div>
853 <div className="ares-postmark" aria-hidden="true"><span>JEZERO</span><span>SOL {2114 - i * 31}</span></div>
854 <blockquote>{s.q}</blockquote>
855 <figcaption><b>{s.n}</b><span>{s.r}</span><span>{s.h}</span></figcaption>
856 </div>
857 </figure>
858 ))}
859 </div>
860 </div>
861 </section>
862 );
863}
864
865function Passage() {
866 const [lease, setLease] = useState(false);
867 const fmt = (n: number) => n.toLocaleString("en-US");
868 return (
869 <section className="ares-sec ares-pass" id="passage">
870 <div className="ares-wrap">
871 <p className="ares-eyebrow" data-ares-reveal>05 · Passage tiers</p>
872 <div className="ares-pass-head" data-ares-reveal>
873 <h2 className="ares-h2">Pick a seat on the <em>Meridian.</em></h2>
874 <div className="ares-toggle" role="group" aria-label="Payment plan">
875 <button type="button" aria-pressed={!lease} className={!lease ? "is-on" : ""} onClick={() => setLease(false)}>Pay once</button>
876 <button type="button" aria-pressed={lease} className={lease ? "is-on" : ""} onClick={() => setLease(true)}>Mars lease · 20 yr</button>
877 </div>
878 </div>
879 <div className="ares-tiers">
880 {TIERS.map((t, i) => (
881 <article key={t.name} className={`ares-tier${t.hot ? " is-hot" : ""}`} data-ares-reveal style={v({ "--i": i })}>
882 {t.hot && <span className="ares-hot">Most chosen</span>}
883 <header>
884 <span className="ares-tn">BOARDING PASS · 0{i + 1}</span>
885 <h3>{t.name}</h3>
886 <p>{t.tag}</p>
887 </header>
888 <div className="ares-price">
889 <b>${lease ? fmt(Math.round((t.base * 1.22) / 240 / 100) * 100) : fmt(t.base)}</b>
890 <span>{lease ? "per month · 240 months" : "one payment, Earth-side"}</span>
891 </div>
892 <ul>{t.perks.map((p) => <li key={p}>{p}</li>)}</ul>
893 <div className="ares-tear" aria-hidden="true"><span /><i /></div>
894 <a className={`ares-btn ${t.hot ? "ares-btn-solid" : "ares-btn-ghost"}`} href="#hold">Request {t.name}</a>
895 </article>
896 ))}
897 </div>
898 </div>
899 </section>
900 );
901}
902
903function Faq() {
904 const [open, setOpen] = useState(0);
905 const [typed, setTyped] = useState(9999);
906 useEffect(() => {
907 if (typed >= 9999) return;
908 const id = window.setInterval(() => setTyped((n) => n + 3), 16);
909 return () => window.clearInterval(id);
910 }, [typed]);
911 const pick = (i: number) => {
912 if (i === open) return;
913 setOpen(i);
914 setTyped(window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 9999 : 0);
915 };
916 return (
917 <section className="ares-sec ares-log" id="log">
918 <div className="ares-wrap">
919 <p className="ares-eyebrow" data-ares-reveal>06 · Mission log</p>
920 <h2 className="ares-h2" data-ares-reveal>Questions we get <em>on the uplink.</em></h2>
921 <div className="ares-console" data-ares-reveal>
922 <div className="ares-console-bar"><span><i /> CHANNEL 07 · LOS ANGELES ⇄ JEZERO</span><span>LIGHT DELAY 12:41</span></div>
923 <div className="ares-console-body">
924 {LOG.map((l, i) => {
925 const on = open === i;
926 const full = typed >= l.a.length;
927 return (
928 <div key={l.t} className={`ares-logrow${on ? " is-open" : ""}`}>
929 <button type="button" aria-expanded={on} onClick={() => pick(i)}>
930 <span className="ares-lt">{l.t}</span>
931 <span className="ares-lq">{l.q}</span>
932 <span className="ares-lp" aria-hidden="true">{on ? "−" : "+"}</span>
933 </button>
934 {on && (
935 <p className="ares-la" aria-live="polite">
936 <span className="ares-lrx">RX&gt;</span> {full ? l.a : l.a.slice(0, typed)}
937 {!full && <span className="ares-caret" />}
938 </p>
939 )}
940 </div>
941 );
942 })}
943 </div>
944 </div>
945 </div>
946 </section>
947 );
948}
949
950function Hold() {
951 const [left, setLeft] = useState<number | null>(null);
952 const [done, setDone] = useState<string | null>(null);
953 useEffect(() => {
954 const target = Date.UTC(2029, 0, 14, 9, 0, 0);
955 const tick = () => setLeft(Math.max(0, target - Date.now()));
956 tick();
957 const id = window.setInterval(tick, 1000);
958 return () => window.clearInterval(id);
959 }, []);
960 const parts = (() => {
961 if (left === null) return ["---", "--", "--", "--"];
962 const s = Math.floor(left / 1000);
963 return [String(Math.floor(s / 86400)), String(Math.floor((s % 86400) / 3600)).padStart(2, "0"), String(Math.floor((s % 3600) / 60)).padStart(2, "0"), String(s % 60).padStart(2, "0")];
964 })();
965 const submit = (e: FormEvent<HTMLFormElement>) => {
966 e.preventDefault();
967 const em = String(new FormData(e.currentTarget).get("email") || "");
968 setDone(`B-${String(100 + ((em.length * 37) % 800))}`);
969 };
970 return (
971 <section className="ares-sec ares-hold" id="hold">
972 <div className="ares-wrap">
973 <p className="ares-eyebrow ares-eyebrow-ink" data-ares-reveal>Next departure · Window 01</p>
974 <h2 className="ares-h2 ares-h2-ink" data-ares-reveal>Window 01 closes <em>the manifest</em> soon.</h2>
975 <div className="ares-count" data-ares-reveal role="timer" aria-label="Time until Window 01">
976 {["days", "hrs", "min", "sec"].map((u, i) => (
977 <div key={u}><b>{parts[i]}</b><span>{u}</span></div>
978 ))}
979 </div>
980 {done ? (
981 <p className="ares-thanks" role="status">Seat held. You are boarding group <b>{done}</b>. A passage advisor will write within two sols.</p>
982 ) : (
983 <form className="ares-form" onSubmit={submit}>
984 <label htmlFor="ares-email" className="ares-sr">Email address</label>
985 <input id="ares-email" name="email" type="email" required placeholder="you@earth.example" autoComplete="email" />
986 <button className="ares-btn ares-btn-solid" type="submit">Hold my seat</button>
987 </form>
988 )}
989 <p className="ares-fine">No deposit to hold a seat. We will send the medical pre-screen and the full manifest.</p>
990 </div>
991 </section>
992 );
993}
994
995function Footer() {
996 return (
997 <footer className="ares-foot">
998 <div className="ares-wrap ares-foot-grid">
999 <div>
1000 <a className="ares-brand" href="#top"><Mark /><span>ARES</span></a>
1001 <p>Habitats, passage and the long patience of settling a new world.</p>
1002 </div>
1003 <div><h4>Company</h4><a href="#habitat">The Meridian home</a><a href="#dome">Dome engineering</a><a href="#windows">Launch windows</a></div>
1004 <div><h4>Residents</h4><a href="#residents">Postcards</a><a href="#passage">Passage tiers</a><a href="#log">Mission log</a></div>
1005 <div><h4>Mission control</h4><a href="#hold">Hold a seat</a><a href="#top">Back to orbit</a></div>
1006 </div>
1007 <p className="ares-wrap ares-legal">Ares Habitat Company is a fictional company; this page is a design template. Prices, dates and residents are illustrative.</p>
1008 </footer>
1009 );
1010}
1011
1012/* ------------------------------------------------------------------ */
1013/* Page */
1014/* ------------------------------------------------------------------ */
1015export function Page() {
1016 const rootRef = useRef<HTMLDivElement>(null);
1017 const [small, setSmall] = useState(false);
1018
1019 useEffect(() => {
1020 const root = rootRef.current;
1021 if (!root) return;
1022 const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1023 const items = Array.from(root.querySelectorAll<HTMLElement>("[data-ares-p]")).map((el) => ({
1024 el,
1025 mode: el.dataset.aresP,
1026 counts: Array.from(el.querySelectorAll<HTMLElement>("[data-ares-count]")),
1027 last: -1,
1028 }));
1029 const setCounts = (it: (typeof items)[number], p: number) => {
1030 for (const c of it.counts) {
1031 const max = Number(c.dataset.aresCount);
1032 const a = Number(c.dataset.from);
1033 const b = Number(c.dataset.to);
1034 const val = String(Math.round(max * clamp((p - a) / (b - a)))).padStart(Number(c.dataset.pad) || 1, "0");
1035 if (c.textContent !== val) c.textContent = val;
1036 }
1037 };
1038 const rv = Array.from(root.querySelectorAll<HTMLElement>("[data-ares-reveal]"));
1039 if (reduce) {
1040 for (const it of items) {
1041 it.el.style.setProperty("--p", "1");
1042 setCounts(it, 1);
1043 }
1044 rv.forEach((e) => e.classList.add("ares-in"));
1045 return;
1046 }
1047 root.classList.add("ares-js");
1048 const io = new IntersectionObserver(
1049 (es) => {
1050 for (const e of es) if (e.isIntersecting) {
1051 e.target.classList.add("ares-in");
1052 io.unobserve(e.target);
1053 }
1054 },
1055 { threshold: 0.15, rootMargin: "0px 0px -6% 0px" },
1056 );
1057 rv.forEach((e) => io.observe(e));
1058 let raf = 0;
1059 let wasSmall = false;
1060 const update = () => {
1061 raf = 0;
1062 const vh = window.innerHeight;
1063 for (const it of items) {
1064 const r = it.el.getBoundingClientRect();
1065 if (r.bottom < -vh * 0.5 || r.top > vh * 1.5) continue;
1066 const p = it.mode === "pin" ? clamp(-r.top / Math.max(1, r.height - vh)) : clamp((vh - r.top) / (vh + r.height));
1067 if (Math.abs(p - it.last) < 0.0008) continue;
1068 it.last = p;
1069 it.el.style.setProperty("--p", p.toFixed(4));
1070 if (it.counts.length) setCounts(it, p);
1071 }
1072 const sm2 = window.scrollY > vh * 0.12;
1073 if (sm2 !== wasSmall) {
1074 wasSmall = sm2;
1075 setSmall(sm2);
1076 }
1077 };
1078 const onScroll = () => {
1079 if (!raf) raf = requestAnimationFrame(update);
1080 };
1081 update();
1082 window.addEventListener("scroll", onScroll, { passive: true });
1083 window.addEventListener("resize", onScroll);
1084 return () => {
1085 window.removeEventListener("scroll", onScroll);
1086 window.removeEventListener("resize", onScroll);
1087 cancelAnimationFrame(raf);
1088 io.disconnect();
1089 };
1090 }, []);
1091
1092 return (
1093 <div className="ares" ref={rootRef}>
1094 <style>{CSS}</style>
1095 <Nav small={small} />
1096 <Hero />
1097
1098 <div className="ares-ticker" aria-hidden="true">
1099 <div className="ares-ticker-in">
1100 {[0, 1].map((k) => (
1101 <span key={k}>
1102 412 homes pressurised ✦ 18,400 sols of habitation ✦ 94% water recovered ✦ 6.1 mbar outside, 70 kPa inside ✦ next window 14 Jan 2029 ✦ 259 days to your front door ✦{" "}
1103 </span>
1104 ))}
1105 </div>
1106 </div>
1107
1108 <section className="ares-sec ares-manifesto" data-ares-p="pass">
1109 <div className="ares-wrap">
1110 <p className="ares-eyebrow">The idea</p>
1111 <p className="ares-words">
1112 {MANIFESTO.map((w, i) => (
1113 <span key={i} style={v({ "--w": (i / MANIFESTO.length).toFixed(3) })}>{w} </span>
1114 ))}
1115 </p>
1116 <div className="ares-stats">
1117 {STATS.map((s) => (
1118 <div key={s.l} data-ares-reveal><b>{s.n}<u>{s.u}</u></b><span>{s.l}</span></div>
1119 ))}
1120 </div>
1121 </div>
1122 </section>
1123
1124 <FloorPlan />
1125 <Dome />
1126 <Windows />
1127 <Postcards />
1128 <Passage />
1129 <Faq />
1130 <Hold />
1131 <Footer />
1132 </div>
1133 );
1134}
1135
1136export default Page;
1137
1138/* ------------------------------------------------------------------ */
1139/* Styles */
1140/* ------------------------------------------------------------------ */
1141const CSS = `
1142.ares {
1143 --ares-void: #0a0605; --ares-basalt: #1a100c; --ares-basalt-2: #26160f; --ares-rust: #b4502a; --ares-oxide: #8c2f1b;
1144 --ares-ochre: #d99a38; --ares-butter: #f2d3a0; --ares-dust: #e9b98c; --ares-pink: #f3c8b0; --ares-copper: #c97b48;
1145 --ares-bone: #f6e9d5; --ares-ink: #2a160e; --ares-blue: #7ba3c6; --ares-line: rgba(242,211,160,.22);
1146 --ares-display: "Space Grotesk", "Futura", "Avenir Next", "Century Gothic", "Trebuchet MS", ui-sans-serif, sans-serif;
1147 --ares-serif: "Fraunces", "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
1148 --ares-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
1149 --p: 0;
1150 box-sizing: border-box; margin: 0; padding: 0; position: relative;
1151 font-family: var(--ares-display); font-size: 17px; line-height: 1.6; color: var(--ares-butter);
1152 background: var(--ares-void); overflow-x: clip; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
1153}
1154:where(.ares) *, :where(.ares) *::before, :where(.ares) *::after { box-sizing: border-box; }
1155:where(.ares) :is(h1, h2, h3, h4, p, ul, ol, li, figure, blockquote, figcaption, dl, dd) { margin: 0; padding: 0; font: inherit; list-style: none; }
1156:where(.ares) a { color: inherit; text-decoration: none; }
1157:where(.ares) :is(button, input) { font: inherit; color: inherit; margin: 0; border: 0; background: none; border-radius: 0; appearance: none; -webkit-appearance: none; }
1158:where(.ares) button { cursor: pointer; text-align: left; }
1159:where(.ares) svg { display: block; }
1160.ares :focus-visible { outline: 2px solid var(--ares-ochre); outline-offset: 3px; }
1161.ares-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
1162.ares-wrap { width: 100%; max-width: 1240px; margin: 0 auto; padding: 0 32px; }
1163.ares-sec { position: relative; }
1164:where(.ares) em { font-family: var(--ares-serif); font-style: italic; font-weight: 400; color: var(--ares-ochre); letter-spacing: -0.01em; }
1165
1166.ares-eyebrow { font-family: var(--ares-mono); font-size: 12px; letter-spacing: .2em; text-transform: uppercase; color: var(--ares-copper); margin-bottom: 18px; }
1167.ares-eyebrow-ink { color: var(--ares-oxide); }
1168.ares-h1 { font-size: clamp(54px, 9.4vw, 148px); line-height: .92; font-weight: 700; letter-spacing: -.045em; text-transform: uppercase; color: var(--ares-bone); text-shadow: 0 2px 30px rgba(10,6,5,.55); }
1169.ares-h1 em { text-transform: none; color: var(--ares-ochre); letter-spacing: -.03em; }
1170.ares-h2 { font-size: clamp(32px, 4.6vw, 68px); line-height: 1.02; font-weight: 700; letter-spacing: -.035em; text-transform: uppercase; color: var(--ares-bone); text-wrap: balance; max-width: 15em; }
1171.ares-h2 em { text-transform: none; }
1172.ares-h2-ink { color: var(--ares-ink); }
1173.ares-h2-ink em { color: var(--ares-oxide); }
1174.ares-lede { font-size: 19px; max-width: 28em; margin-top: 22px; color: var(--ares-pink); text-shadow: 0 1px 14px rgba(10,6,5,.7); }
1175.ares-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 48px; padding: 0 26px; font-family: var(--ares-mono); font-size: 12.5px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; border-radius: 999px; transition: transform .25s, background .25s, color .25s, box-shadow .25s; white-space: nowrap; }
1176.ares-btn-solid { background: var(--ares-ochre); color: var(--ares-void); box-shadow: 0 0 0 0 rgba(217,154,56,.5); }
1177.ares-btn-solid:hover { background: var(--ares-butter); transform: translateY(-2px); box-shadow: 0 8px 30px rgba(217,154,56,.35); }
1178.ares-btn-ghost { box-shadow: inset 0 0 0 1.5px currentColor; color: inherit; }
1179.ares-btn-ghost:hover { background: rgba(242,211,160,.12); transform: translateY(-2px); }
1180.ares-btn-sm { min-height: 40px; padding: 0 18px; font-size: 11.5px; }
1181
1182/* nav */
1183.ares-nav { position: fixed; z-index: 50; top: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 14px 32px; transition: background .35s, padding .35s, box-shadow .35s; color: var(--ares-bone); }
1184.ares-nav.is-small { background: rgba(26,16,12,.82); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); padding-top: 10px; padding-bottom: 10px; box-shadow: 0 1px 0 var(--ares-line); }
1185.ares-brand { display: inline-flex; align-items: center; gap: 10px; font-weight: 700; letter-spacing: .32em; font-size: 15px; color: var(--ares-bone); }
1186.ares-brand svg { color: var(--ares-ochre); }
1187.ares-nav-links { display: flex; gap: 30px; font-family: var(--ares-mono); font-size: 11.5px; letter-spacing: .16em; text-transform: uppercase; }
1188.ares-nav-links a { opacity: .8; transition: opacity .2s, color .2s; padding: 6px 0; }
1189.ares-nav-links a:hover { opacity: 1; color: var(--ares-ochre); }
1190
1191/* hero */
1192.ares-hero { position: relative; height: 330vh; background: var(--ares-void); --hp: 0; }
1193.ares-hero-stage { position: sticky; top: 0; height: 100vh; height: 100svh; overflow: hidden; color: var(--ares-bone); touch-action: pan-y; }
1194.ares-hero-cv { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
1195.ares-grain { position: absolute; inset: 0; opacity: .1; mix-blend-mode: overlay; pointer-events: none; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .9 0'/></filter><rect width='160' height='160' filter='url(%23n)'/></svg>"); }
1196.ares-vig { position: absolute; inset: 0; pointer-events: none; background: radial-gradient(ellipse at 50% 55%, transparent 55%, rgba(10,6,5,.55) 100%); }
1197.ares-hud { position: absolute; z-index: 3; top: 84px; font-family: var(--ares-mono); font-size: 11.5px; letter-spacing: .08em; text-transform: uppercase; padding: 12px 14px; min-width: 210px; background: linear-gradient(rgba(26,16,12,.55), rgba(26,16,12,.55)), repeating-linear-gradient(0deg, rgba(242,211,160,.06) 0 1px, transparent 1px 3px); border: 1px solid var(--ares-line); border-radius: 4px; -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); color: var(--ares-butter); pointer-events: none; }
1198.ares-hud::before, .ares-hud::after { content: ""; position: absolute; width: 10px; height: 10px; border: 2px solid var(--ares-ochre); }
1199.ares-hud::before { top: -2px; left: -2px; border-right: 0; border-bottom: 0; }
1200.ares-hud::after { bottom: -2px; right: -2px; border-left: 0; border-top: 0; }
1201.ares-hud-l { left: 32px; }
1202.ares-hud-r { right: 32px; text-align: right; }
1203.ares-hud-k { color: var(--ares-copper); margin-bottom: 6px; font-size: 10.5px; letter-spacing: .2em; }
1204.ares-hud-row { display: flex; justify-content: space-between; gap: 18px; padding: 2px 0; }
1205.ares-hud-row span { opacity: .6; }
1206.ares-hud-row b { font-weight: 600; color: var(--ares-bone); }
1207.ares-hud-row .ares-big { font-size: 19px; color: var(--ares-ochre); }
1208.ares-hero-a, .ares-hero-b { position: absolute; z-index: 2; left: 32px; right: 32px; will-change: transform, opacity; }
1209.ares-hero-a { top: 27vh; max-width: 980px; opacity: calc(1 - clamp(0, (var(--hp) - .1) / .22, 1)); transform: translateY(calc(var(--hp) * -90px)); }
1210.ares-hero-b { bottom: 11vh; max-width: 760px; opacity: clamp(0, (var(--hp) - .8) / .1, 1); transform: translateY(calc((1 - clamp(0, (var(--hp) - .8) / .1, 1)) * 40px)); pointer-events: none; }
1211.ares-hero-b .ares-cta-row { pointer-events: auto; }
1212.ares-hero-a .ares-eyebrow, .ares-hero-b .ares-eyebrow { color: var(--ares-butter); text-shadow: 0 1px 12px rgba(10,6,5,.8); }
1213.ares-h2-hero { font-size: clamp(34px, 5.4vw, 80px); text-shadow: 0 2px 24px rgba(10,6,5,.7); }
1214.ares-cta-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 26px; }
1215.ares-hero-b .ares-btn-ghost { color: var(--ares-bone); background: rgba(10,6,5,.35); }
1216.ares-rail { position: absolute; z-index: 3; right: 14px; top: 28vh; bottom: 22vh; width: 2px; background: rgba(242,211,160,.22); }
1217.ares-rail span { position: absolute; top: 0; left: -4px; width: 10px; height: 10px; border-radius: 50%; background: var(--ares-ochre); box-shadow: 0 0 14px var(--ares-ochre); margin-top: -5px; }
1218.ares-rail span { top: 0; }
1219.ares-scrollcue { position: absolute; z-index: 3; left: 50%; bottom: 26px; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: 8px; font-family: var(--ares-mono); font-size: 10.5px; letter-spacing: .3em; text-transform: uppercase; color: var(--ares-butter); opacity: clamp(0, 1 - var(--hp) * 18, 1); }
1220.ares-scrollcue i { width: 1px; height: 38px; background: linear-gradient(var(--ares-ochre), transparent); animation: ares-cue 1.8s ease-in-out infinite; transform-origin: top; }
1221@keyframes ares-cue { 0%, 100% { transform: scaleY(.3); opacity: .5; } 50% { transform: scaleY(1); opacity: 1; } }
1222
1223/* ticker */
1224.ares-ticker { background: var(--ares-rust); color: var(--ares-void); overflow: hidden; white-space: nowrap; font-family: var(--ares-mono); font-size: 12.5px; letter-spacing: .14em; text-transform: uppercase; font-weight: 600; padding: 13px 0; border-top: 2px solid var(--ares-oxide); border-bottom: 2px solid var(--ares-oxide); }
1225.ares-ticker-in { display: inline-block; animation: ares-marq 55s linear infinite; }
1226@keyframes ares-marq { to { transform: translateX(-50%); } }
1227
1228/* manifesto */
1229.ares-manifesto { background: radial-gradient(ellipse at 50% 0%, #3a1a10 0%, var(--ares-basalt) 60%); padding: 150px 0 120px; }
1230.ares-words { font-size: clamp(30px, 5.2vw, 76px); line-height: 1.08; font-weight: 600; letter-spacing: -.035em; max-width: 17em; color: var(--ares-bone); }
1231.ares-words span { opacity: calc(.17 + .83 * clamp(0, (var(--p) - .1 - var(--w) * .42) / .1, 1)); transition: opacity .15s linear; }
1232.ares-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; margin-top: 110px; border-top: 1px solid var(--ares-line); }
1233.ares-stats > div { padding: 28px 24px 0 0; border-right: 1px solid var(--ares-line); display: flex; flex-direction: column; gap: 10px; }
1234.ares-stats > div + div { padding-left: 24px; }
1235.ares-stats > div:last-child { border-right: 0; }
1236.ares-stats b { font-size: clamp(46px, 5.6vw, 84px); line-height: 1; font-weight: 700; letter-spacing: -.04em; color: var(--ares-ochre); }
1237.ares-stats u { font-size: .38em; text-decoration: none; margin-left: 6px; color: var(--ares-copper); letter-spacing: 0; }
1238.ares-stats span { font-size: 15px; line-height: 1.45; color: var(--ares-dust); max-width: 17em; }
1239
1240/* reveal */
1241.ares-js [data-ares-reveal] { opacity: 0; transform: translateY(32px); transition: opacity .8s cubic-bezier(.2,.7,.2,1) calc(var(--i, 0) * .12s), transform .9s cubic-bezier(.2,.7,.2,1) calc(var(--i, 0) * .12s); }
1242.ares-js [data-ares-reveal].ares-in { opacity: 1; transform: none; }
1243
1244/* pinned sections */
1245.ares-pin { height: var(--pin, 300vh); }
1246.ares-pin-stage { position: sticky; top: 0; height: 100vh; height: 100svh; display: flex; align-items: center; padding-top: 70px; overflow: hidden; }
1247.ares-draw { fill: none; stroke-dasharray: 1; stroke-dashoffset: calc(1 - clamp(0, (var(--p) - var(--s)) / var(--d, .1), 1)); }
1248.ares-d { fill: none; stroke-width: 2.4; stroke-linecap: round; stroke-dasharray: 1; stroke-dashoffset: calc(1 - clamp(0, (var(--p) - var(--s)) / var(--d, .1), 1)); stroke: var(--ares-copper); }
1249.ares-d.ares-thin { stroke-width: 1.2; stroke: rgba(201,123,72,.5); }
1250.ares-d.ares-thick { stroke-width: 4; }
1251.ares-fade { opacity: clamp(0, (var(--p) - var(--s)) / var(--d, .1), 1); }
1252
1253/* floor plan */
1254.ares-plan { background: var(--ares-bone); color: var(--ares-ink); }
1255.ares-plan .ares-h2 { color: var(--ares-ink); font-size: clamp(30px, 3.9vw, 56px); }
1256.ares-plan .ares-h2 em { color: var(--ares-oxide); }
1257.ares-plan-grid { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 48px; align-items: center; }
1258.ares-roomlist { margin-top: 28px; border-top: 1px solid rgba(42,22,14,.2); }
1259.ares-roomlist li { opacity: clamp(.28, .28 + 1.5 * (var(--p) - var(--s)) / var(--d), 1); }
1260.ares-roomlist button { display: grid; grid-template-columns: 38px 1fr auto; align-items: baseline; width: 100%; padding: 10px 6px; border-bottom: 1px solid rgba(42,22,14,.2); transition: background .2s, padding .25s, color .2s; }
1261.ares-roomlist button:hover, .ares-roomlist button.is-sel { background: var(--ares-oxide); color: var(--ares-bone); padding-left: 14px; }
1262.ares-rn, .ares-ra { font-family: var(--ares-mono); font-size: 12px; letter-spacing: .08em; }
1263.ares-rt { font-size: 18px; font-weight: 600; letter-spacing: -.01em; }
1264.ares-roomnote { margin-top: 20px; font-size: 15.5px; line-height: 1.55; min-height: 5.2em; color: #4a2a1c; }
1265.ares-roomnote b { font-family: var(--ares-serif); font-style: italic; font-weight: 400; color: var(--ares-oxide); font-size: 1.1em; }
1266.ares-plan-fig svg { width: 100%; height: auto; max-height: calc(100svh - 150px); }
1267.ares-hull { stroke: var(--ares-ink); stroke-width: 5; stroke-linejoin: round; }
1268.ares-room { cursor: pointer; }
1269.ares-room-fill { fill: rgba(140,47,27,.0); transition: fill .25s; opacity: clamp(0, (var(--p) - var(--s) - .06) / .08, 1); }
1270.ares-room.is-sel .ares-room-fill { fill: rgba(180,80,42,.24); }
1271.ares-room-line { stroke: var(--ares-oxide); stroke-width: 2.4; }
1272.ares-room.is-sel .ares-room-line { stroke-width: 4; }
1273.ares-room-in { opacity: clamp(0, (var(--p) - var(--s) - .08) / .08, 1); }
1274.ares-furn path, .ares-furn circle { fill: none; stroke: rgba(42,22,14,.62); stroke-width: 1.5; stroke-linejoin: round; }
1275.ares-pn { fill: var(--ares-oxide); opacity: clamp(0, (var(--p) - var(--s) - .08) / .06, 1); }
1276.ares-pnt { fill: var(--ares-bone); font-family: var(--ares-mono); font-weight: 700; font-size: 17px; opacity: clamp(0, (var(--p) - var(--s) - .08) / .06, 1); pointer-events: none; }
1277.ares-rname { fill: var(--ares-ink); font-family: var(--ares-mono); font-weight: 700; font-size: 14px; letter-spacing: .08em; opacity: clamp(0, (var(--p) - var(--s) - .1) / .06, 1); pointer-events: none; }
1278.ares-rarea { fill: var(--ares-oxide); font-family: var(--ares-mono); font-size: 12.5px; letter-spacing: .08em; opacity: clamp(0, (var(--p) - var(--s) - .1) / .06, 1); pointer-events: none; }
1279.ares-dim { opacity: clamp(0, (var(--p) - var(--s)) / var(--d), 1); }
1280.ares-dim path { stroke: var(--ares-oxide); stroke-width: 1.5; fill: none; }
1281.ares-dim text { fill: var(--ares-oxide); font-family: var(--ares-mono); font-size: 13px; letter-spacing: .14em; }
1282
1283/* dome */
1284.ares-dome { background: radial-gradient(ellipse at 50% 30%, #3b1b10 0%, var(--ares-basalt) 70%); }
1285.ares-dome-grid { display: grid; grid-template-columns: minmax(0, 4.4fr) minmax(0, 7fr); gap: 40px; align-items: center; }
1286.ares-dome .ares-h2 { font-size: clamp(30px, 3.8vw, 54px); }
1287.ares-legend { margin-top: 26px; display: grid; gap: 4px; }
1288.ares-legend li { display: grid; grid-template-columns: 34px 1fr; gap: 10px; align-items: start; padding: 7px 0; border-top: 1px solid var(--ares-line); opacity: clamp(.2, .2 + 1.6 * (var(--p) - var(--s)) / var(--d), 1); }
1289.ares-ln { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; background: var(--ares-ochre); color: var(--ares-void); font-family: var(--ares-mono); font-weight: 700; font-size: 12px; margin-top: 2px; }
1290.ares-legend b { display: block; font-size: 15.5px; color: var(--ares-bone); line-height: 1.3; }
1291.ares-legend i { display: block; font-style: normal; font-size: 13.5px; line-height: 1.45; color: var(--ares-dust); }
1292.ares-dome-fig svg { width: 100%; height: auto; max-height: calc(100svh - 140px); }
1293.ares-line { fill: none; stroke: var(--ares-dust); stroke-width: 1.6; stroke-linejoin: round; stroke-linecap: round; }
1294.ares-plant { fill: rgba(143,170,90,.8); stroke: #c9d890; stroke-width: 1; }
1295.ares-air { fill: none; stroke: var(--ares-blue); stroke-width: 2; stroke-dasharray: 5 9; stroke-linecap: round; opacity: clamp(0, (var(--p) - .6) / .08, .8); animation: ares-air 2.2s linear infinite; }
1296.ares-air2 { animation-direction: reverse; stroke: var(--ares-ochre); }
1297@keyframes ares-air { to { stroke-dashoffset: -28; } }
1298.ares-mk circle { fill: var(--ares-ochre); stroke: var(--ares-void); stroke-width: 3; }
1299.ares-mk text { fill: var(--ares-void); font-family: var(--ares-mono); font-weight: 700; font-size: 22px; }
1300.ares-mk { opacity: clamp(0, (var(--p) - var(--s)) / var(--d), 1); }
1301
1302/* windows */
1303.ares-win { background: linear-gradient(#0a0605, #130b0c 60%, #0a0605); }
1304.ares-win-grid { display: grid; grid-template-columns: minmax(0, 6fr) minmax(0, 6fr); gap: 40px; align-items: center; }
1305.ares-win .ares-h2 { font-size: clamp(30px, 3.8vw, 54px); }
1306.ares-readouts { display: flex; gap: 40px; margin: 24px 0 14px; font-family: var(--ares-mono); }
1307.ares-readouts span { display: block; font-size: 10.5px; letter-spacing: .2em; text-transform: uppercase; color: var(--ares-copper); }
1308.ares-readouts b { font-size: clamp(30px, 3.3vw, 46px); font-weight: 600; color: var(--ares-ochre); font-variant-numeric: tabular-nums; line-height: 1.1; }
1309.ares-readouts i { font-style: normal; }
1310.ares-readouts u { text-decoration: none; font-size: 14px; color: var(--ares-dust); margin-left: 6px; }
1311.ares-winlist li { display: grid; grid-template-columns: 74px 1fr auto; gap: 14px; align-items: center; padding: 12px 0; border-top: 1px solid var(--ares-line); opacity: clamp(.22, .22 + 1.6 * (var(--p) - var(--s)) / var(--d), 1); }
1312.ares-wid { font-family: var(--ares-mono); font-size: 12px; letter-spacing: .1em; color: var(--ares-copper); }
1313.ares-wmain { font-size: 16px; color: var(--ares-bone); line-height: 1.35; }
1314.ares-wmain b { color: var(--ares-ochre); font-weight: 600; }
1315.ares-wmain i { display: block; font-style: normal; font-size: 13px; color: var(--ares-dust); }
1316.ares-wst { font-family: var(--ares-mono); font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase; padding: 5px 10px; border: 1px solid var(--ares-line); border-radius: 999px; color: var(--ares-butter); white-space: nowrap; }
1317.ares-orbit svg { width: 100%; height: auto; max-height: calc(100svh - 120px); margin: 0 auto; }
1318.ares-orb { fill: none; stroke: rgba(242,211,160,.22); stroke-dasharray: 3 6; }
1319.ares-xfer-ghost { fill: none; stroke: rgba(242,211,160,.14); stroke-width: 1.5; }
1320.ares-xfer { fill: none; stroke: var(--ares-ochre); stroke-width: 2.5; stroke-dasharray: 1; stroke-dashoffset: calc(1 - clamp(0, (var(--p) - var(--s)) / var(--d), 1)); }
1321.ares-spin { transform-box: view-box; transform-origin: 300px 300px; transform: rotate(var(--rot)); }
1322.ares-orb-t { fill: var(--ares-dust); font-family: var(--ares-mono); font-size: 12px; letter-spacing: .2em; }
1323.ares-ship { fill: var(--ares-bone); offset-path: path("M430 300 A164 160.4 0 0 0 102 300"); offset-rotate: 0deg; offset-distance: calc(clamp(0, (var(--p) - .397) / .5625, 1) * 100%); filter: drop-shadow(0 0 6px #f2d3a0); }
1324
1325/* residents */
1326.ares-res { background: linear-gradient(#e9b98c, #f3c8b0 55%, #f6e9d5); color: var(--ares-ink); padding: 140px 0 160px; }
1327.ares-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; margin-top: 64px; align-items: start; }
1328.ares-card { transform: translateY(calc((var(--p) - .5) * var(--par) * -1px)); will-change: transform; }
1329.ares-card-in { position: relative; background: #fbf3e4; padding: 34px 28px 28px; border-radius: 3px; box-shadow: 0 1px 0 rgba(42,22,14,.25), 0 24px 50px -22px rgba(90,30,14,.55); transform: rotate(var(--rot)); transition: transform .5s cubic-bezier(.2,.8,.2,1), box-shadow .5s; background-image: repeating-linear-gradient(transparent 0 31px, rgba(140,47,27,.08) 31px 32px); }
1330.ares-card:hover .ares-card-in { transform: rotate(0deg) translateY(-6px); box-shadow: 0 1px 0 rgba(42,22,14,.25), 0 34px 60px -24px rgba(90,30,14,.65); }
1331.ares-card:nth-child(2) { margin-top: 46px; }
1332.ares-stamp { position: absolute; top: 14px; right: 14px; width: 54px; color: var(--ares-oxide); transform: rotate(4deg); }
1333.ares-postmark { position: absolute; top: 20px; right: 78px; width: 70px; height: 70px; border: 2px solid rgba(140,47,27,.5); border-radius: 50%; display: grid; place-content: center; text-align: center; font-family: var(--ares-mono); font-size: 9px; letter-spacing: .1em; color: rgba(140,47,27,.7); transform: rotate(-12deg); line-height: 1.5; }
1334.ares-card blockquote { font-family: var(--ares-serif); font-style: italic; font-size: 19px; line-height: 32px; margin-top: 56px; color: #3a1f14; }
1335.ares-card figcaption { margin-top: 24px; display: grid; gap: 1px; font-size: 13.5px; color: #6a4030; }
1336.ares-card figcaption b { font-size: 16px; color: var(--ares-ink); }
1337.ares-card figcaption span:last-child { font-family: var(--ares-mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--ares-oxide); margin-top: 4px; }
1338
1339/* passage */
1340.ares-pass { background: var(--ares-basalt-2); padding: 140px 0 150px; background-image: radial-gradient(ellipse at 80% 0%, rgba(180,80,42,.25), transparent 55%); }
1341.ares-pass-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 30px; flex-wrap: wrap; }
1342.ares-toggle { display: inline-flex; padding: 4px; border-radius: 999px; border: 1px solid var(--ares-line); background: rgba(10,6,5,.4); }
1343.ares-toggle button { padding: 10px 20px; border-radius: 999px; font-family: var(--ares-mono); font-size: 11.5px; letter-spacing: .12em; text-transform: uppercase; transition: background .25s, color .25s; white-space: nowrap; }
1344.ares-toggle button.is-on { background: var(--ares-ochre); color: var(--ares-void); }
1345.ares-tiers { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 60px; align-items: stretch; }
1346.ares-tier { position: relative; display: flex; flex-direction: column; gap: 22px; padding: 30px 28px 28px; background: linear-gradient(160deg, #3a2016, #2a1710); border: 1px solid var(--ares-line); border-radius: 18px; transition: transform .35s cubic-bezier(.2,.8,.2,1), border-color .3s; -webkit-mask: radial-gradient(circle 13px at 0 76%, transparent 98%, #000) left / 51% 100% no-repeat, radial-gradient(circle 13px at 100% 76%, transparent 98%, #000) right / 51% 100% no-repeat; mask: radial-gradient(circle 13px at 0 76%, transparent 98%, #000) left / 51% 100% no-repeat, radial-gradient(circle 13px at 100% 76%, transparent 98%, #000) right / 51% 100% no-repeat; }
1347.ares-tier:hover { transform: translateY(-8px); border-color: var(--ares-ochre); }
1348.ares-tier.is-hot { background: linear-gradient(160deg, #c05a30, #8c2f1b); border-color: var(--ares-butter); color: var(--ares-bone); }
1349.ares-hot { position: absolute; top: 18px; right: 18px; font-family: var(--ares-mono); font-size: 10px; letter-spacing: .16em; text-transform: uppercase; background: var(--ares-void); color: var(--ares-ochre); padding: 4px 10px; border-radius: 999px; }
1350.ares-tn { font-family: var(--ares-mono); font-size: 10.5px; letter-spacing: .22em; color: var(--ares-copper); }
1351.is-hot .ares-tn { color: var(--ares-butter); }
1352.ares-tier h3 { font-size: 40px; font-weight: 700; letter-spacing: -.035em; text-transform: uppercase; color: var(--ares-bone); line-height: 1.05; margin-top: 8px; }
1353.ares-tier header p { font-family: var(--ares-serif); font-style: italic; color: var(--ares-dust); }
1354.is-hot header p { color: var(--ares-butter); }
1355.ares-price b { display: block; font-size: 44px; line-height: 1; font-weight: 700; letter-spacing: -.04em; color: var(--ares-ochre); font-variant-numeric: tabular-nums; }
1356.is-hot .ares-price b { color: var(--ares-bone); }
1357.ares-price span { font-family: var(--ares-mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--ares-dust); }
1358.ares-tier ul { display: grid; gap: 9px; font-size: 15px; flex: 1; }
1359.ares-tier li { padding-left: 22px; position: relative; color: var(--ares-butter); }
1360.ares-tier li::before { content: ""; position: absolute; left: 0; top: .55em; width: 10px; height: 2px; background: var(--ares-ochre); }
1361.is-hot li::before { background: var(--ares-bone); }
1362.ares-tear { position: relative; height: 12px; display: flex; align-items: center; }
1363.ares-tear span { flex: 1; border-top: 2px dashed rgba(242,211,160,.35); }
1364.ares-tear i { position: absolute; right: 0; top: -10px; width: 64px; height: 28px; opacity: .55; background: repeating-linear-gradient(90deg, currentColor 0 2px, transparent 2px 4px, currentColor 4px 5px, transparent 5px 8px); display: none; }
1365.ares-tier .ares-btn-ghost { color: var(--ares-butter); }
1366
1367/* mission log */
1368.ares-log { background: var(--ares-void); padding: 140px 0 150px; }
1369.ares-console { margin-top: 56px; border: 1px solid rgba(217,154,56,.4); border-radius: 10px; overflow: hidden; background: #0e0806; box-shadow: 0 0 80px -30px rgba(217,154,56,.35), inset 0 0 90px rgba(217,154,56,.04); }
1370.ares-console-bar { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 20px; font-family: var(--ares-mono); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; background: #22130d; color: var(--ares-butter); border-bottom: 1px solid rgba(217,154,56,.3); }
1371.ares-console-bar i { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #7bd88f; margin-right: 8px; animation: ares-blink 1.6s steps(2) infinite; }
1372@keyframes ares-blink { 50% { opacity: .2; } }
1373.ares-console-body { background: repeating-linear-gradient(0deg, rgba(242,211,160,.025) 0 1px, transparent 1px 3px); }
1374.ares-logrow + .ares-logrow { border-top: 1px solid rgba(242,211,160,.12); }
1375.ares-logrow button { display: grid; grid-template-columns: 90px 1fr 30px; gap: 12px; align-items: baseline; width: 100%; padding: 18px 20px; font-family: var(--ares-mono); transition: background .2s; }
1376.ares-logrow button:hover { background: rgba(217,154,56,.07); }
1377.ares-lt { font-size: 12px; letter-spacing: .12em; color: var(--ares-copper); }
1378.ares-lq { font-size: 16px; color: var(--ares-bone); }
1379.ares-logrow.is-open .ares-lq { color: var(--ares-ochre); }
1380.ares-lp { color: var(--ares-ochre); font-size: 20px; text-align: right; }
1381.ares-la { padding: 0 20px 22px 122px; font-family: var(--ares-mono); font-size: 14px; line-height: 1.7; color: var(--ares-butter); max-width: 62em; min-height: 3.4em; }
1382.ares-lrx { color: #7bd88f; margin-right: 6px; }
1383.ares-caret { display: inline-block; width: 8px; height: 1.1em; background: var(--ares-ochre); vertical-align: text-bottom; margin-left: 2px; animation: ares-blink .8s steps(2) infinite; }
1384
1385/* hold */
1386.ares-hold { background: linear-gradient(#f6e9d5, #e9b98c); color: var(--ares-ink); padding: 130px 0 120px; text-align: center; }
1387.ares-hold .ares-h2 { margin: 0 auto; max-width: 13em; }
1388.ares-count { display: flex; justify-content: center; gap: 14px; margin: 46px 0; flex-wrap: wrap; }
1389.ares-count > div { min-width: 118px; padding: 18px 14px 14px; background: var(--ares-basalt); color: var(--ares-ochre); border-radius: 10px; border-bottom: 4px solid var(--ares-oxide); }
1390.ares-count b { display: block; font-family: var(--ares-mono); font-size: clamp(34px, 5vw, 58px); line-height: 1; font-weight: 600; font-variant-numeric: tabular-nums; }
1391.ares-count span { font-family: var(--ares-mono); font-size: 11px; letter-spacing: .24em; text-transform: uppercase; color: var(--ares-dust); }
1392.ares-form { display: flex; justify-content: center; gap: 10px; max-width: 560px; margin: 0 auto; flex-wrap: wrap; }
1393.ares-form input { flex: 1 1 260px; min-height: 52px; padding: 0 22px; border-radius: 999px; background: #fff8ea; border: 2px solid rgba(42,22,14,.3); color: var(--ares-ink); font-size: 16px; }
1394.ares-form input::placeholder { color: #8a6a58; }
1395.ares-form input:focus { border-color: var(--ares-oxide); outline: none; box-shadow: 0 0 0 4px rgba(180,80,42,.25); }
1396.ares-form .ares-btn-solid { background: var(--ares-oxide); color: var(--ares-bone); min-height: 52px; }
1397.ares-form .ares-btn-solid:hover { background: var(--ares-rust); }
1398.ares-thanks { font-size: 20px; max-width: 30em; margin: 0 auto; }
1399.ares-thanks b { font-family: var(--ares-mono); color: var(--ares-oxide); }
1400.ares-fine { margin-top: 22px; font-size: 13.5px; color: #6a4030; }
1401
1402/* footer */
1403.ares-foot { background: var(--ares-void); padding: 80px 0 40px; color: var(--ares-dust); }
1404.ares-foot-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 40px; }
1405.ares-foot-grid p { margin-top: 16px; max-width: 22em; font-size: 15px; }
1406.ares-foot-grid h4 { font-family: var(--ares-mono); font-size: 11px; letter-spacing: .2em; text-transform: uppercase; color: var(--ares-copper); margin-bottom: 14px; }
1407.ares-foot-grid a:not(.ares-brand) { display: block; padding: 4px 0; font-size: 15px; transition: color .2s; }
1408.ares-foot-grid a:not(.ares-brand):hover { color: var(--ares-ochre); }
1409.ares-legal { margin-top: 60px; padding-top: 22px; border-top: 1px solid var(--ares-line); font-size: 12.5px; color: #9a7a68; }
1410
1411/* responsive */
1412@media (max-width: 980px) {
1413 .ares-plan-grid, .ares-dome-grid, .ares-win-grid { grid-template-columns: 1fr; gap: 14px; }
1414 .ares-tiers, .ares-cards { grid-template-columns: 1fr; }
1415 .ares-card:nth-child(2) { margin-top: 0; }
1416 .ares-stats { grid-template-columns: repeat(2, 1fr); }
1417 .ares-stats > div:nth-child(2) { border-right: 0; }
1418 .ares-stats > div:nth-child(3) { padding-left: 0; }
1419 .ares-stats > div:nth-child(n+3) { border-top: 1px solid var(--ares-line); margin-top: 28px; }
1420 .ares-foot-grid { grid-template-columns: 1fr 1fr; }
1421 .ares-nav-links { display: none; }
1422}
1423@media (max-width: 700px) {
1424 .ares { font-size: 16px; }
1425 .ares-wrap { padding: 0 16px; }
1426 .ares-nav { padding: 12px 16px; }
1427 .ares-hud { top: 70px; min-width: 0; padding: 8px 10px; font-size: 9.5px; }
1428 .ares-hud-l { left: 12px; }
1429 .ares-hud-r { right: 12px; }
1430 .ares-hud-row { gap: 8px; }
1431 .ares-hud-row .ares-big { font-size: 14px; }
1432 .ares-hide-s { display: none; }
1433 .ares-hero-a, .ares-hero-b { left: 16px; right: 16px; }
1434 .ares-hero-a { top: 28vh; }
1435 .ares-lede { font-size: 16px; }
1436 .ares-rail { right: 6px; }
1437 .ares-manifesto, .ares-pass, .ares-log { padding: 90px 0; }
1438 .ares-res { padding: 90px 0 100px; }
1439 .ares-pin-stage { padding-top: 64px; align-items: flex-start; }
1440 .ares-plan .ares-h2, .ares-dome .ares-h2, .ares-win .ares-h2 { font-size: 24px; }
1441 .ares-plan .ares-eyebrow, .ares-dome .ares-eyebrow, .ares-win .ares-eyebrow { margin-bottom: 8px; }
1442 .ares-plan-fig { order: -1; }
1443 .ares-plan-fig svg { max-height: 34svh; }
1444 .ares-rname, .ares-rarea { display: none; }
1445 .ares-roomlist { margin-top: 12px; display: grid; grid-template-columns: 1fr 1fr; column-gap: 12px; }
1446 .ares-roomlist button { padding: 6px 4px; grid-template-columns: 24px 1fr auto; }
1447 .ares-rt { font-size: 13px; }
1448 .ares-ra { font-size: 10px; }
1449 .ares-roomnote { font-size: 13px; min-height: 0; margin-top: 10px; }
1450 .ares-dome-fig { order: -1; }
1451 .ares-dome-fig svg { max-height: 34svh; }
1452 .ares-legend { grid-template-columns: 1fr 1fr; column-gap: 10px; margin-top: 10px; gap: 0 10px; }
1453 .ares-legend li { padding: 5px 0; grid-template-columns: 24px 1fr; gap: 6px; }
1454 .ares-legend i { display: none; }
1455 .ares-legend b { font-size: 12.5px; }
1456 .ares-ln { width: 20px; height: 20px; font-size: 10px; }
1457 .ares-orbit svg { max-height: 36svh; }
1458 .ares-orbit { order: -1; }
1459 .ares-readouts { gap: 20px; margin: 8px 0; }
1460 .ares-readouts b { font-size: 24px; }
1461 .ares-winlist li { grid-template-columns: 1fr; gap: 0; padding: 6px 0; }
1462 .ares-wid, .ares-wst, .ares-wmain i { display: none; }
1463 .ares-wmain { font-size: 13.5px; }
1464 .ares-stats b { font-size: 44px; }
1465 .ares-stats span { font-size: 13px; }
1466 .ares-logrow button { grid-template-columns: 1fr 24px; padding: 16px 14px; }
1467 .ares-lt { grid-column: 1 / -1; }
1468 .ares-la { padding: 0 14px 18px; font-size: 13px; }
1469 .ares-count > div { min-width: 70px; padding: 12px 8px 10px; }
1470 .ares-pass-head { align-items: flex-start; }
1471 .ares-foot-grid { grid-template-columns: 1fr 1fr; gap: 28px; }
1472 .ares-foot-grid > div:first-child { grid-column: 1 / -1; }
1473}
1474@media (max-width: 700px) and (max-height: 700px) {
1475 .ares-roomnote { display: none; }
1476}
1477
1478@media (prefers-reduced-motion: reduce) {
1479 .ares *, .ares *::before, .ares *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; transition-delay: 0ms !important; }
1480 .ares-hero { height: 100vh; height: 100svh; }
1481 .ares-hero-a { opacity: 0; }
1482 .ares-hero-b { opacity: 1; transform: none; }
1483 .ares-scrollcue, .ares-rail { display: none; }
1484 .ares-ticker-in { animation: none; }
1485 .ares-pin { height: auto; }
1486 .ares-pin-stage { position: relative; height: auto; min-height: 0; padding: 100px 0; overflow: visible; }
1487 .ares-card { transform: none; }
1488 .ares-air { animation: none; }
1489}
1490@media (prefers-contrast: more) {
1491 .ares-nav.is-small, .ares-hud { background: var(--ares-void); -webkit-backdrop-filter: none; backdrop-filter: none; }
1492}
1493`;

The design brief. For you and your AI agent.

Tokens, type, layout, components and motion — written from the real values in the file, so a model can extend the page without drifting.

Ares — DESIGN.md

A home on Mars. Retro-future rust: basalt blacks, oxide reds, ochre, butterscotch dust and copper, with a single cold blue reserved for the Martian sunset and for water. Mono readouts, uppercase grotesque headlines, one italic serif accent. All imagery is procedural (canvas and inline SVG).

All CSS lives in Page.tsx under the .ares prefix, with box-sizing, margins, fonts and list styles set explicitly on the root, so it renders the same under any reset.

1. Palette

RoleTokenHex
Deep space, page base--ares-void#0A0605
Dark sections--ares-basalt / -2#1A100C / #26160F
Ticker, tier highlight--ares-rust#B4502A
Ink accent on light, hot tier--ares-oxide#8C2F1B
Primary accent: CTAs, italics, numerals--ares-ochre#D99A38
Body text on dark--ares-butter#F2D3A0
Secondary text on dark--ares-dust#E9B98C
Eyebrows, hairline accents--ares-copper#C97B48
Warm sky band--ares-pink#F3C8B0
Light sections, headlines on dark--ares-bone#F6E9D5
Ink on light--ares-ink#2A160E
Sunset sky, ice, water--ares-blue#7BA3C6
Hairlines on dark--ares-linergba(242,211,160,.22)

Hero canvas colours: day sky #A85F3C → #F4CFAE, dusk sky #1B2140 → #8FB4D4, dune layers #D39A72 #B9663D #94432A #66291B #33140E, night tint #120A14.

Contrast: butter on basalt is above 12:1, copper on basalt about 5.5:1, oxide on bone about 7:1.

2. Typography

  • Display (--ares-display): "Space Grotesk", "Futura", "Avenir Next", "Century Gothic", "Trebuchet MS", ui-sans-serif, sans-serif. Weight 700, uppercase, tracking -0.035 to -0.045em.
  • Serif accent (--ares-serif): "Fraunces", "Iowan Old Style", Palatino, Georgia, serif, italic 400. Used for emphasis words, postcards and room notes.
  • Mono (--ares-mono): "JetBrains Mono", "IBM Plex Mono", ui-monospace, Menlo, Consolas, monospace. Eyebrows (12px, 0.2em, uppercase), HUD, buttons, FAQ log, counters.
StyleSpec
H1clamp(54px, 9.4vw, 148px) / 0.92, 700, uppercase
H2clamp(32px, 4.6vw, 68px) / 1.02 (pinned sections use 3.8-3.9vw)
Manifestoclamp(30px, 5.2vw, 76px) / 1.08, 600
Body17px / 1.6 (16px under 700px)
Buttonsmono 12.5px, 0.14em, uppercase, pill, 48px tall

3. Spacing and layout

Wrap max 1240px with 32px gutters (16px under 700px). Sections 130-150px vertical padding (90px mobile). Pinned sections are 320-340vh tall with a position: sticky 100svh stage and a two-column grid (copy / drawing) that stacks under 980px with the drawing first. Radii: pills 999px, cards 3-18px. Hairlines 1px --ares-line.

4. Motion

  • Hero descent: 330vh container, sticky canvas, smoothed progress. Orbit limb curvature flattens, atmosphere glow fades, entry heat and streaks peak mid-scroll, dunes rise from below, Olympus Mons and the sun appear, sky turns to blue sunset, stars return. Moons drift continuously.
  • Dust devil: follows the pointer (auto-wanders when idle); dust particles within its radius spin around and lift; a stack of ellipses draws the column.
  • HUD: altitude, velocity and phase update by progress; sol clock, LMST and temperature use real Mars time at Jezero (77.45° E), updated every second.
  • Scroll variable: one rAF-throttled scroll loop writes --p (0-1) on [data-ares-p] (pin = progress through the sticky range, pass = progress across the viewport). CSS reads it with clamp() calc for stroke-dashoffset drawing, opacities, and rotations. Counters use data-ares-count.
  • Reveals: IntersectionObserver adds .ares-in (opacity + 32px rise, staggered by --i).
  • Others: marquee ticker, postcard parallax and straighten on hover, boarding-pass lift, pay once / lease toggle, FAQ typewriter at about 190 chars/s.
  • Reduced motion: no pin, no autoplay; hero is a static touchdown composition; every drawing is shown complete.

5. Sections

  1. Nav: fixed, transparent, turns into blurred basalt after scroll.
  2. Hero: descent canvas, two HUD panels, headline A (orbit) and B (touchdown + CTAs), progress rail.
  3. Ticker: rust band with facts.
  4. Manifesto: word-by-word scrubbed statement and four stats.
  5. Habitat: bone, pinned floor plan of seven rooms drawn sequentially, linked room list.
  6. Dome: pinned cross-section (regolith, shell, deck, greenhouse, ice store, airlock) with numbered legend.
  7. Windows: pinned orbital diagram, transit counters, four departure windows.
  8. Residents: three drifting postcards on a dust-pink gradient.
  9. Passage: three boarding-pass tiers with pay-once / lease toggle.
  10. Mission log: FAQ console with typing answers.
  11. Hold: countdown to Window 01 and seat-hold form.
  12. Footer with fictional-company note.