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,181 lines · 76.4 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). The hero and postcard stamps
6 * use photographic renders from /img/space/ (mars-orbit, mars-surface); the
7 * drawings are inline SVG and CSS. No web fonts, no network.
8 *
9 * Motion:
10 * - Hero: a 330vh pinned stage. Scrolling is a descent: the orbit image zooms
11 * toward the planet, an entry-glow flashes, then the surface image fades in
12 * and settles. A live sol clock / temperature panel runs on real Mars time.
13 * - One scroll loop writes `--p` (0..1) onto every [data-ares-p] section.
14 * Pure CSS reads it: floor-plan strokes draw room by room, the dome
15 * cross-section draws itself layer by layer, Earth and Mars orbit while
16 * the supply ship flies its transfer arc, manifesto words light up.
17 * - Postcards drift at different speeds, passage tiers toggle between pay
18 * once / lease, the FAQ is a mission-control log that types answers.
19 *
20 * Reduced motion: no autoplay; the hero is a static touchdown
21 * composition and every drawing is shown complete.
22 * SSR-safe: no window/document access during render.
23 */
24import { useEffect, useRef, useState, type CSSProperties, type FormEvent, type ReactNode } from "react";
25
26/* ------------------------------------------------------------------ */
27/* Pure helpers (module scope is deterministic: SSR == client) */
28/* ------------------------------------------------------------------ */
29const clamp = (x: number, a = 0, b = 1) => Math.min(b, Math.max(a, x));
30const sm = (a: number, b: number, x: number) => {
31 const t = clamp((x - a) / (b - a));
32 return t * t * (3 - 2 * t);
33};
34const frac = (x: number) => x - Math.floor(x);
35const v = (o: Record<string, string | number>) => o as CSSProperties;
36
37/* ------------------------------------------------------------------ */
38/* Mars time */
39/* ------------------------------------------------------------------ */
40type MarsNow = { sol: string; lmst: string; temp: number; wind: number };
41function marsNow(): MarsNow {
42 const ms = Date.now();
43 const jd = ms / 86400000 + 2440587.5;
44 const jdTT = jd + (37 + 32.184) / 86400;
45 const msd = (jdTT - 2451549.5) / 1.0274912517 + 44796.0 - 0.0009626;
46 const mtc = frac(msd) * 24;
47 const lmst = (mtc + 77.45 / 15 + 24) % 24;
48 const hh = Math.floor(lmst);
49 const mm = Math.floor((lmst - hh) * 60);
50 const ss = Math.floor((((lmst - hh) * 60 - mm) * 60) % 60);
51 const f = lmst / 24;
52 const temp = -63 + 36 * Math.sin(2 * Math.PI * (f - 0.3)) + Math.sin(ms / 4000) * 0.6;
53 return {
54 sol: Math.floor(msd).toLocaleString("en-US"),
55 lmst: [hh, mm, ss].map((n) => String(n).padStart(2, "0")).join(":"),
56 temp,
57 wind: 6 + 4 * Math.sin(ms / 9000) + 3 * Math.sin(f * 12),
58 };
59}
60
61const PHASES: [number, string][] = [
62 [0.08, "Low Mars orbit"],
63 [0.26, "Deorbit burn"],
64 [0.4, "Entry interface"],
65 [0.54, "Peak heating"],
66 [0.68, "Parachute deploy"],
67 [0.86, "Powered descent"],
68 [2, "Touchdown, Jezero"],
69];
70
71/* ------------------------------------------------------------------ */
72/* Hero */
73/* ------------------------------------------------------------------ */
74function Hero() {
75 const heroRef = useRef<HTMLElement>(null);
76 const altRef = useRef<HTMLSpanElement>(null);
77 const velRef = useRef<HTMLSpanElement>(null);
78 const phaseRef = useRef<HTMLSpanElement>(null);
79 const railRef = useRef<HTMLSpanElement>(null);
80 const [mn, setMn] = useState<MarsNow | null>(null);
81
82 useEffect(() => {
83 const tick = () => setMn(marsNow());
84 tick();
85 const id = window.setInterval(tick, 1000);
86 return () => window.clearInterval(id);
87 }, []);
88
89 useEffect(() => {
90 const hero = heroRef.current;
91 if (!hero) return;
92 const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
93 let pp = reduce ? 0.93 : 0;
94 let target = pp;
95 let raf = 0;
96 let lastPhase = "";
97 let lastAlt = "";
98 const hud = (p: number) => {
99 const d = sm(0.04, 0.62, p);
100 const alt = p > 0.93 ? 0 : 410 * Math.pow(1 - d, 1.6);
101 const altS = alt >= 100 ? Math.round(alt).toString() : alt.toFixed(1);
102 if (altS !== lastAlt && altRef.current) {
103 altRef.current.textContent = altS;
104 lastAlt = altS;
105 }
106 if (velRef.current) velRef.current.textContent = (p > 0.93 ? 0 : 3.42 * (1 - sm(0.1, 0.86, p) * 0.97)).toFixed(2);
107 const ph = PHASES.find((x) => p < x[0])![1];
108 if (ph !== lastPhase && phaseRef.current) {
109 phaseRef.current.textContent = ph;
110 lastPhase = ph;
111 }
112 if (railRef.current) railRef.current.style.transform = `translateY(${(p * 100).toFixed(1)}%)`;
113 hero.style.setProperty("--hp", p.toFixed(3));
114 };
115 const frame = () => {
116 raf = 0;
117 if (!reduce) {
118 const r = hero.getBoundingClientRect();
119 target = clamp(-r.top / Math.max(1, r.height - window.innerHeight));
120 pp += (target - pp) * 0.18;
121 if (Math.abs(target - pp) < 0.0007) pp = target;
122 else raf = requestAnimationFrame(frame);
123 }
124 hud(pp);
125 };
126 const kick = () => {
127 if (!raf) raf = requestAnimationFrame(frame);
128 };
129 kick();
130 window.addEventListener("scroll", kick, { passive: true });
131 window.addEventListener("resize", kick);
132 return () => {
133 cancelAnimationFrame(raf);
134 window.removeEventListener("scroll", kick);
135 window.removeEventListener("resize", kick);
136 };
137 }, []);
138
139 return (
140 <header className="ares-hero" ref={heroRef} id="top" style={v({ "--hp": 0 })}>
141 <div className="ares-hero-stage">
142 <div className="ares-photo ares-photo-orbit">
143 <img
144 src="/img/space/mars-orbit-1200.webp"
145 srcSet="/img/space/mars-orbit-640.webp 640w, /img/space/mars-orbit-1200.webp 1200w, /img/space/mars-orbit.webp 1374w"
146 sizes="100vw"
147 width={1374}
148 height={784}
149 alt="Mars from low orbit: curved limb, canyons and craters under the Milky Way"
150 fetchPriority="high"
151 loading="eager"
152 decoding="async"
153 />
154 </div>
155 <div className="ares-entry" aria-hidden="true" />
156 <div className="ares-photo ares-photo-surf">
157 <img
158 src="/img/space/mars-surface-1200.webp"
159 srcSet="/img/space/mars-surface-640.webp 640w, /img/space/mars-surface-1200.webp 1200w, /img/space/mars-surface.webp 1405w"
160 sizes="100vw"
161 width={1405}
162 height={752}
163 alt="Mars surface at sunset: orange dunes, a rover, a butte and Phobos in a butterscotch sky"
164 decoding="async"
165 loading="lazy"
166 />
167 </div>
168 <div className="ares-scrim" aria-hidden="true" />
169 <div className="ares-grain" aria-hidden="true" />
170 <div className="ares-vig" aria-hidden="true" />
171
172 <div className="ares-hud ares-hud-l" aria-hidden="true">
173 <div className="ares-hud-k">Descent telemetry</div>
174 <div className="ares-hud-row"><span>ALT</span><b><span ref={altRef}>410</span> km</b></div>
175 <div className="ares-hud-row"><span>VEL</span><b><span ref={velRef}>3.42</span> km/s</b></div>
176 <div className="ares-hud-row"><span>PHASE</span><b ref={phaseRef}>Low Mars orbit</b></div>
177 </div>
178
179 <div className="ares-hud ares-hud-r" role="status" aria-label="Live Mars time at Jezero Crater">
180 <div className="ares-hud-k">Jezero Crater · live</div>
181 <div className="ares-hud-row"><span>SOL</span><b>{mn ? mn.sol : "-----"}</b></div>
182 <div className="ares-hud-row"><span>LMST</span><b className="ares-big">{mn ? mn.lmst : "--:--:--"}</b></div>
183 <div className="ares-hud-row"><span>TEMP</span><b>{mn ? `${mn.temp.toFixed(1)} °C` : "--"}</b></div>
184 <div className="ares-hud-row ares-hide-s"><span>WIND</span><b>{mn ? `${mn.wind.toFixed(1)} m/s` : "--"}</b></div>
185 </div>
186
187 <div className="ares-hero-a">
188 <p className="ares-eyebrow">Ares Habitat Company · Passage opens Jan 2029</p>
189 <h1 className="ares-h1">
190 A home on <em>Mars.</em>
191 </h1>
192 <p className="ares-lede">Pressurised, shielded, furnished and waiting. Scroll to ride the descent.</p>
193 </div>
194
195 <div className="ares-hero-b">
196 <p className="ares-eyebrow">Sol 0 · Touchdown confirmed</p>
197 <h2 className="ares-h2 ares-h2-hero">Welcome home. Mind the dust.</h2>
198 <div className="ares-cta-row">
199 <a className="ares-btn ares-btn-solid" href="#passage">Reserve your passage</a>
200 <a className="ares-btn ares-btn-ghost" href="#habitat">Tour the Meridian home</a>
201 </div>
202 </div>
203
204 <div className="ares-rail" aria-hidden="true"><span ref={railRef} /></div>
205 <div className="ares-scrollcue" aria-hidden="true"><span>Descend</span><i /></div>
206 </div>
207 </header>
208 );
209}
210
211/* ------------------------------------------------------------------ */
212/* Content */
213/* ------------------------------------------------------------------ */
214const MANIFESTO =
215 "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(" ");
216
217const STATS = [
218 { n: "148", u: "m²", l: "Private living space in a Meridian home" },
219 { n: "3.0", u: "m", l: "Of packed regolith over every pressure shell" },
220 { n: "94", u: "%", l: "Of household water recovered and reused" },
221 { n: "0.38", u: "g", l: "Gravity, so the stairs are a pleasure" },
222];
223
224type Room = { name: string; area: number; x: number; y: number; w: number; h: number; note: string };
225const ROOMS: Room[] = [
226 { 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." },
227 { 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." },
228 { 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." },
229 { 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." },
230 { 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." },
231 { 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." },
232 { 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." },
233];
234
235const DOME_LAYERS = [
236 { t: "Regolith shield", d: "Three metres of sintered Martian soil over the shell. Cuts radiation to a fraction of surface dose." },
237 { t: "Aerogel-ETFE shell", d: "A bonded pressure envelope with aerogel insulation. Holds 70 kPa and shrugs off dust storms." },
238 { t: "Living deck", d: "Two levels, 148 m², floors cantilevered off a basalt-fibre spine. Normal air, normal doors." },
239 { t: "Greenhouse ring", d: "A band of beds hugging the sunlit curve. Dinner, oxygen and a view of something green." },
240 { t: "Ice and water store", d: "Mined ice melts into a buried tank that doubles as thermal mass and a second radiation wall." },
241 { t: "Airlock and rover port", d: "A pressurised tunnel to the vehicle bay, so a trip to the ridge never starts with a spacewalk." },
242];
243
244const WINDOWS = [
245 { 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 },
246 { 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 },
247 { 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 },
248 { 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 },
249];
250
251const LOOK: Record<string, string> = { p1: "a woman with a gray bob", p2: "a bearded man with dark hair", p3: "a woman with braided hair", p4: "a man with short curly hair", p5: "a smiling man with a short beard", p6: "a woman with long wavy dark hair", p7: "a bearded man in round glasses", p8: "a smiling man with swept-back hair", p9: "a woman with natural curly hair" };
252const portraitAlt = (n: string, r: string, src: string) => `${n}, ${r}: portrait of ${LOOK[src.match(/p\d/)?.[0] ?? ""] ?? "a person"} in a black sweater`;
253
254const STORIES = [
255 {
256 n: "Ines Okafor-Lindqvist",
257 img: "/img/people/p3.webp",
258 r: "Hydroponics agronomist · Sol 1,204",
259 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.",
260 h: "Meridian home 0417",
261 },
262 {
263 n: "Tomasz Reyes",
264 img: "/img/people/p5.webp",
265 r: "Structural welder · Sol 842",
266 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.",
267 h: "Founder ring, plot 12",
268 },
269 {
270 n: "Dr. Mireille Anand",
271 img: "/img/people/p9.webp",
272 r: "Retired GP, now the clinic · Sol 1,977",
273 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.",
274 h: "Meridian home 0102",
275 },
276];
277
278const TIERS = [
279 { 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"] },
280 { 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 },
281 { 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"] },
282];
283
284const LOG = [
285 { 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." },
286 { 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." },
287 { 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." },
288 { 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." },
289 { 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." },
290 { 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." },
291 { 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." },
292 { 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." },
293];
294
295/* ------------------------------------------------------------------ */
296/* Section components */
297/* ------------------------------------------------------------------ */
298function Mark() {
299 return (
300 <svg viewBox="0 0 32 32" width="28" height="28" aria-hidden="true">
301 <circle cx="16" cy="16" r="13" fill="none" stroke="currentColor" strokeWidth="2" />
302 <path d="M3 20 Q10 14 16 19 T29 17 V29 H3Z" fill="currentColor" />
303 <circle cx="22" cy="9" r="2.4" fill="currentColor" />
304 </svg>
305 );
306}
307
308function Nav({ small }: { small: boolean }) {
309 return (
310 <nav className={`ares-nav${small ? " is-small" : ""}`} aria-label="Primary">
311 <a className="ares-brand" href="#top" aria-label="Ares home"><Mark /><span>ARES</span></a>
312 <div className="ares-nav-links">
313 <a href="#habitat">Habitat</a>
314 <a href="#dome">Dome</a>
315 <a href="#windows">Windows</a>
316 <a href="#residents">Residents</a>
317 <a href="#log">Mission log</a>
318 </div>
319 <a className="ares-btn ares-btn-solid ares-btn-sm" href="#passage">Reserve passage</a>
320 </nav>
321 );
322}
323
324function FloorPlan() {
325 const [sel, setSel] = useState(4);
326 const furn: Record<number, ReactNode> = {
327 0: (
328 <g className="ares-furn">
329 {[0, 1, 2, 3].map((r) => (
330 <g key={r}>
331 <path d={`M80 ${100 + r * 34} H370`} strokeDasharray="4 5" />
332 {Array.from({ length: 12 }, (_, c) => <circle key={c} cx={92 + c * 24} cy={100 + r * 34} r="4.5" />)}
333 </g>
334 ))}
335 </g>
336 ),
337 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>,
338 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>,
339 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>,
340 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>,
341 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>,
342 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>,
343 };
344 return (
345 <section className="ares-sec ares-plan ares-pin" id="habitat" data-ares-p="pin" style={v({ "--pin": "320vh" })}>
346 <div className="ares-pin-stage">
347 <div className="ares-wrap ares-plan-grid">
348 <div className="ares-plan-copy">
349 <p className="ares-eyebrow ares-eyebrow-ink">01 · The Meridian home</p>
350 <h2 className="ares-h2">One level, seven rooms, <em>zero compromises</em> on light.</h2>
351 <ul className="ares-roomlist">
352 {ROOMS.map((r, i) => (
353 <li key={r.name} style={v({ "--s": 0.06 + i * 0.1, "--d": 0.12 })}>
354 <button type="button" className={sel === i ? "is-sel" : ""} onMouseEnter={() => setSel(i)} onFocus={() => setSel(i)} onClick={() => setSel(i)}>
355 <span className="ares-rn">{String(i + 1).padStart(2, "0")}</span>
356 <span className="ares-rt">{r.name}</span>
357 <span className="ares-ra">{r.area} m²</span>
358 </button>
359 </li>
360 ))}
361 </ul>
362 <p className="ares-roomnote" aria-live="polite"><b>{ROOMS[sel].name}.</b> {ROOMS[sel].note}</p>
363 </div>
364 <div className="ares-plan-fig">
365 <svg viewBox="0 0 800 520" role="img" aria-label="Floor plan of the Meridian home with seven rooms">
366 <defs>
367 <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>
368 </defs>
369 <rect x="0" y="0" width="800" height="520" fill="url(#ares-grid)" />
370 <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" />
371 {ROOMS.map((r, i) => {
372 const s = 0.06 + i * 0.1;
373 return (
374 <g key={r.name} className={`ares-room${sel === i ? " is-sel" : ""}`} style={v({ "--s": s, "--d": 0.12 })} onMouseEnter={() => setSel(i)} onClick={() => setSel(i)}>
375 <path className="ares-room-fill" d={`M${r.x} ${r.y} h${r.w} v${r.h} h${-r.w}z`} />
376 <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`} />
377 <g className="ares-room-in">{furn[i]}</g>
378 <circle className="ares-pn" cx={r.x + 24} cy={r.y + 24} r="15" />
379 <text className="ares-pnt" x={r.x + 24} y={r.y + 30} textAnchor="middle">{i + 1}</text>
380 <text className="ares-rname" x={r.x + 46} y={r.y + 29}>{r.name.toUpperCase()}</text>
381 <text className="ares-rarea" x={r.x + 46} y={r.y + 47}>{r.area} M²</text>
382 </g>
383 );
384 })}
385 <g className="ares-dim" style={v({ "--s": 0.8, "--d": 0.1 })}>
386 <path d="M40 505 H760 M40 498 V512 M760 498 V512" />
387 <text x="400" y="500" textAnchor="middle">24.0 M</text>
388 </g>
389 </svg>
390 </div>
391 </div>
392 </div>
393 </section>
394 );
395}
396
397function Dome() {
398 return (
399 <section className="ares-sec ares-dome ares-pin" id="dome" data-ares-p="pin" style={v({ "--pin": "340vh" })}>
400 <div className="ares-pin-stage">
401 <div className="ares-wrap ares-dome-grid">
402 <div className="ares-dome-copy">
403 <p className="ares-eyebrow">02 · Cross-section</p>
404 <h2 className="ares-h2">A pressure vessel with <em>a garden inside.</em></h2>
405 <ol className="ares-legend">
406 {DOME_LAYERS.map((l, i) => (
407 <li key={l.t} style={v({ "--s": 0.52 + i * 0.07, "--d": 0.06 })}>
408 <span className="ares-ln">{i + 1}</span>
409 <span><b>{l.t}</b><i>{l.d}</i></span>
410 </li>
411 ))}
412 </ol>
413 </div>
414 <div className="ares-dome-fig">
415 <svg viewBox="0 0 900 540" role="img" aria-label="Cross-section of a pressurised dome buried under regolith">
416 <defs>
417 <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>
418 <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>
419 </defs>
420 <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" />
421 {[430, 462, 494].map((y, i) => (
422 <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}`} />
423 ))}
424 <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)" />
425 <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" />
426 <path className="ares-fade" style={v({ "--s": 0.26, "--d": 0.1 })} d="M150 381 A300 270 0 0 1 750 381Z" fill="#26160f" />
427 <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" />
428 <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" />
429 <path className="ares-d" pathLength="1" style={v({ "--s": 0.44, "--d": 0.1, stroke: "#f2d3a0" })} d="M165 381 H735" />
430 <g className="ares-fade" style={v({ "--s": 0.46, "--d": 0.1 })}>
431 <path className="ares-line" d="M230 300 H670 M300 300 V381 M600 300 V381 M450 300 V381" />
432 <path className="ares-line" d="M262 262 h78 v38 h-78z M360 270 h60 v30 h-60z M470 266 h70 v34 h-70z" />
433 <path className="ares-line" d="M250 340 h130 v41 h-130z M410 340 h110 v41 h-110z" />
434 </g>
435 <g className="ares-fade" style={v({ "--s": 0.54, "--d": 0.1 })}>
436 {Array.from({ length: 11 }, (_, i) => {
437 const a = Math.PI * (0.18 + i * 0.064);
438 return <circle key={i} className="ares-plant" cx={450 + 262 * Math.cos(a + 0)} cy={381 - 232 * Math.sin(a)} r="8" />;
439 })}
440 <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" />
441 </g>
442 <g className="ares-fade" style={v({ "--s": 0.6, "--d": 0.1 })}>
443 <ellipse cx="450" cy="458" rx="130" ry="30" fill="url(#ares-ice)" stroke="#7ba3c6" strokeWidth="2" />
444 <path className="ares-line" d="M450 428 V381" stroke="#7ba3c6" />
445 </g>
446 <g className="ares-fade" style={v({ "--s": 0.66, "--d": 0.1 })}>
447 <path className="ares-line" d="M735 345 H850 V381 H735 M850 345 L870 381" />
448 <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" />
449 </g>
450 <path className="ares-air" d="M290 250 C370 170 530 170 610 250" />
451 <path className="ares-air ares-air2" d="M610 275 C530 215 370 215 290 275" />
452 {[
453 [630, 104], [300, 146], [450, 300], [645, 322], [450, 458], [790, 360],
454 ].map(([x, y], i) => (
455 <g key={i} className="ares-mk" style={v({ "--s": 0.52 + i * 0.07, "--d": 0.06 })}>
456 <circle cx={x} cy={y} r="19" />
457 <text x={x} y={y + 7} textAnchor="middle">{i + 1}</text>
458 </g>
459 ))}
460 </svg>
461 </div>
462 </div>
463 </div>
464 </section>
465 );
466}
467
468function Windows() {
469 return (
470 <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))" })}>
471 <div className="ares-pin-stage">
472 <div className="ares-wrap ares-win-grid">
473 <div className="ares-win-copy">
474 <p className="ares-eyebrow">03 · Launch windows</p>
475 <h2 className="ares-h2">Earth and Mars line up <em>every 26 months.</em> Be on the pad.</h2>
476 <div className="ares-readouts">
477 <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>
478 <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>
479 </div>
480 <ul className="ares-winlist">
481 {WINDOWS.map((w) => (
482 <li key={w.id} style={v({ "--s": w.a * 0.7, "--d": 0.08 })}>
483 <span className="ares-wid">WIN {w.id}</span>
484 <span className="ares-wmain"><b>{w.date}</b> · {w.ship}<i>{w.load} · {w.arr}</i></span>
485 <span className="ares-wst">{w.st}</span>
486 </li>
487 ))}
488 </ul>
489 </div>
490 <div className="ares-orbit">
491 <svg viewBox="0 0 600 600" role="img" aria-label="Earth and Mars orbiting the Sun while a supply ship flies a transfer arc">
492 <defs>
493 <radialGradient id="ares-sun"><stop offset="0" stopColor="#fff0cf" /><stop offset=".35" stopColor="#f2d3a0" stopOpacity=".7" /><stop offset="1" stopColor="#d99a38" stopOpacity="0" /></radialGradient>
494 </defs>
495 <circle cx="300" cy="300" r="46" fill="url(#ares-sun)" />
496 <circle cx="300" cy="300" r="130" className="ares-orb" />
497 <circle cx="300" cy="300" r="198" className="ares-orb" />
498 <path className="ares-xfer-ghost" d="M430 300 A164 160.4 0 0 0 102 300" />
499 <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" />
500 <g className="ares-spin" style={v({ "--rot": "calc(var(--tau) * -360deg)" })}>
501 <circle cx="430" cy="300" r="11" fill="#7ba3c6" />
502 <circle cx="430" cy="300" r="16" fill="none" stroke="#7ba3c6" strokeOpacity=".4" />
503 <text x="430" y="337" textAnchor="middle" className="ares-orb-t">EARTH</text>
504 </g>
505 <g className="ares-spin" style={v({ "--rot": "calc(var(--tau) * -191.4deg)" })}>
506 <g className="ares-spin" style={v({ "--rot": "-44.3deg" })}>
507 <circle cx="498" cy="300" r="9" fill="#c0502c" />
508 <text x="498" y="278" textAnchor="middle" className="ares-orb-t">MARS</text>
509 </g>
510 </g>
511 <circle className="ares-ship" r="5" />
512 </svg>
513 </div>
514 </div>
515 </div>
516 </section>
517 );
518}
519
520function Postcards() {
521 return (
522 <section className="ares-sec ares-res" id="residents" data-ares-p="pass">
523 <div className="ares-wrap">
524 <p className="ares-eyebrow ares-eyebrow-ink" data-ares-reveal>04 · Residents</p>
525 <h2 className="ares-h2 ares-h2-ink" data-ares-reveal>Postcards from <em>Sol 2,114.</em></h2>
526 <div className="ares-cards">
527 {STORIES.map((s, i) => (
528 <figure key={s.n} className="ares-card" data-ares-reveal style={v({ "--i": i, "--rot": `${(i - 1) * 2.6}deg`, "--par": 36 + i * 30 })}>
529 <div className="ares-card-in">
530 <div className="ares-stamp" aria-hidden="true"><img src="/img/space/mars-surface-640.webp" alt="" width={640} height={343} loading="lazy" decoding="async" style={{ objectPosition: ["66% 63%", "22% 72%", "24% 28%"][i % 3], transformOrigin: ["66% 63%", "22% 72%", "24% 28%"][i % 3] }} /></div>
531 <div className="ares-postmark" aria-hidden="true"><span>JEZERO</span><span>SOL {2114 - i * 31}</span></div>
532 <blockquote>{s.q}</blockquote>
533 <figcaption><img src={s.img} alt={portraitAlt(s.n, s.r.split(" · ")[0].toLowerCase(), s.img)} width={52} height={52} loading="lazy" decoding="async" /><b>{s.n}</b><span>{s.r}</span><span>{s.h}</span></figcaption>
534 </div>
535 </figure>
536 ))}
537 </div>
538 </div>
539 </section>
540 );
541}
542
543function Passage() {
544 const [lease, setLease] = useState(false);
545 const fmt = (n: number) => n.toLocaleString("en-US");
546 return (
547 <section className="ares-sec ares-pass" id="passage">
548 <div className="ares-wrap">
549 <p className="ares-eyebrow" data-ares-reveal>05 · Passage tiers</p>
550 <div className="ares-pass-head" data-ares-reveal>
551 <h2 className="ares-h2">Pick a seat on the <em>Meridian.</em></h2>
552 <div className="ares-toggle" role="group" aria-label="Payment plan">
553 <button type="button" aria-pressed={!lease} className={!lease ? "is-on" : ""} onClick={() => setLease(false)}>Pay once</button>
554 <button type="button" aria-pressed={lease} className={lease ? "is-on" : ""} onClick={() => setLease(true)}>Mars lease · 20 yr</button>
555 </div>
556 </div>
557 <div className="ares-tiers">
558 {TIERS.map((t, i) => (
559 <article key={t.name} className={`ares-tier${t.hot ? " is-hot" : ""}`} data-ares-reveal style={v({ "--i": i })}>
560 {t.hot && <span className="ares-hot">Most chosen</span>}
561 <header>
562 <span className="ares-tn">BOARDING PASS · 0{i + 1}</span>
563 <h3>{t.name}</h3>
564 <p>{t.tag}</p>
565 </header>
566 <div className="ares-price">
567 <b>${lease ? fmt(Math.round((t.base * 1.22) / 240 / 100) * 100) : fmt(t.base)}</b>
568 <span>{lease ? "per month · 240 months" : "one payment, Earth-side"}</span>
569 </div>
570 <ul>{t.perks.map((p) => <li key={p}>{p}</li>)}</ul>
571 <div className="ares-tear" aria-hidden="true"><span /><i /></div>
572 <a className={`ares-btn ${t.hot ? "ares-btn-solid" : "ares-btn-ghost"}`} href="#hold">Request {t.name}</a>
573 </article>
574 ))}
575 </div>
576 </div>
577 </section>
578 );
579}
580
581function Faq() {
582 const [open, setOpen] = useState(0);
583 const [typed, setTyped] = useState(9999);
584 useEffect(() => {
585 if (typed >= 9999) return;
586 const id = window.setInterval(() => setTyped((n) => n + 3), 16);
587 return () => window.clearInterval(id);
588 }, [typed]);
589 const pick = (i: number) => {
590 if (i === open) return;
591 setOpen(i);
592 setTyped(window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 9999 : 0);
593 };
594 return (
595 <section className="ares-sec ares-log" id="log">
596 <div className="ares-wrap">
597 <p className="ares-eyebrow" data-ares-reveal>06 · Mission log</p>
598 <h2 className="ares-h2" data-ares-reveal>Questions we get <em>on the uplink.</em></h2>
599 <div className="ares-console" data-ares-reveal>
600 <div className="ares-console-bar"><span><i /> CHANNEL 07 · LOS ANGELES ⇄ JEZERO</span><span>LIGHT DELAY 12:41</span></div>
601 <div className="ares-console-body">
602 {LOG.map((l, i) => {
603 const on = open === i;
604 const full = typed >= l.a.length;
605 return (
606 <div key={l.t} className={`ares-logrow${on ? " is-open" : ""}`}>
607 <button type="button" aria-expanded={on} onClick={() => pick(i)}>
608 <span className="ares-lt">{l.t}</span>
609 <span className="ares-lq">{l.q}</span>
610 <span className="ares-lp" aria-hidden="true">{on ? "−" : "+"}</span>
611 </button>
612 {on && (
613 <p className="ares-la" aria-live="polite">
614 <span className="ares-lrx">RX&gt;</span> {full ? l.a : l.a.slice(0, typed)}
615 {!full && <span className="ares-caret" />}
616 </p>
617 )}
618 </div>
619 );
620 })}
621 </div>
622 </div>
623 </div>
624 </section>
625 );
626}
627
628function Hold() {
629 const [left, setLeft] = useState<number | null>(null);
630 const [done, setDone] = useState<string | null>(null);
631 useEffect(() => {
632 const target = Date.UTC(2029, 0, 14, 9, 0, 0);
633 const tick = () => setLeft(Math.max(0, target - Date.now()));
634 tick();
635 const id = window.setInterval(tick, 1000);
636 return () => window.clearInterval(id);
637 }, []);
638 const parts = (() => {
639 if (left === null) return ["---", "--", "--", "--"];
640 const s = Math.floor(left / 1000);
641 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")];
642 })();
643 const submit = (e: FormEvent<HTMLFormElement>) => {
644 e.preventDefault();
645 const em = String(new FormData(e.currentTarget).get("email") || "");
646 setDone(`B-${String(100 + ((em.length * 37) % 800))}`);
647 };
648 return (
649 <section className="ares-sec ares-hold" id="hold">
650 <div className="ares-wrap">
651 <p className="ares-eyebrow ares-eyebrow-ink" data-ares-reveal>Next departure · Window 01</p>
652 <h2 className="ares-h2 ares-h2-ink" data-ares-reveal>Window 01 closes <em>the manifest</em> soon.</h2>
653 <div className="ares-count" data-ares-reveal role="timer" aria-label="Time until Window 01">
654 {["days", "hrs", "min", "sec"].map((u, i) => (
655 <div key={u}><b>{parts[i]}</b><span>{u}</span></div>
656 ))}
657 </div>
658 {done ? (
659 <p className="ares-thanks" role="status">Seat held. You are boarding group <b>{done}</b>. A passage advisor will write within two sols.</p>
660 ) : (
661 <form className="ares-form" onSubmit={submit}>
662 <label htmlFor="ares-email" className="ares-sr">Email address</label>
663 <input id="ares-email" name="email" type="email" required placeholder="you@earth.example" autoComplete="email" />
664 <button className="ares-btn ares-btn-solid" type="submit">Hold my seat</button>
665 </form>
666 )}
667 <p className="ares-fine">No deposit to hold a seat. We will send the medical pre-screen and the full manifest.</p>
668 </div>
669 </section>
670 );
671}
672
673function Footer() {
674 return (
675 <footer className="ares-foot">
676 <div className="ares-wrap ares-foot-grid">
677 <div>
678 <a className="ares-brand" href="#top"><Mark /><span>ARES</span></a>
679 <p>Habitats, passage and the long patience of settling a new world.</p>
680 </div>
681 <div><h4>Company</h4><a href="#habitat">The Meridian home</a><a href="#dome">Dome engineering</a><a href="#windows">Launch windows</a></div>
682 <div><h4>Residents</h4><a href="#residents">Postcards</a><a href="#passage">Passage tiers</a><a href="#log">Mission log</a></div>
683 <div><h4>Mission control</h4><a href="#hold">Hold a seat</a><a href="#top">Back to orbit</a></div>
684 </div>
685 <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>
686 </footer>
687 );
688}
689
690/* ------------------------------------------------------------------ */
691/* Page */
692/* ------------------------------------------------------------------ */
693export function Page() {
694 const rootRef = useRef<HTMLDivElement>(null);
695 const [small, setSmall] = useState(false);
696
697 useEffect(() => {
698 const root = rootRef.current;
699 if (!root) return;
700 const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
701 const items = Array.from(root.querySelectorAll<HTMLElement>("[data-ares-p]")).map((el) => ({
702 el,
703 mode: el.dataset.aresP,
704 counts: Array.from(el.querySelectorAll<HTMLElement>("[data-ares-count]")),
705 last: -1,
706 }));
707 const setCounts = (it: (typeof items)[number], p: number) => {
708 for (const c of it.counts) {
709 const max = Number(c.dataset.aresCount);
710 const a = Number(c.dataset.from);
711 const b = Number(c.dataset.to);
712 const val = String(Math.round(max * clamp((p - a) / (b - a)))).padStart(Number(c.dataset.pad) || 1, "0");
713 if (c.textContent !== val) c.textContent = val;
714 }
715 };
716 const rv = Array.from(root.querySelectorAll<HTMLElement>("[data-ares-reveal]"));
717 if (reduce) {
718 for (const it of items) {
719 it.el.style.setProperty("--p", "1");
720 setCounts(it, 1);
721 }
722 rv.forEach((e) => e.classList.add("ares-in"));
723 return;
724 }
725 root.classList.add("ares-js");
726 const io = new IntersectionObserver(
727 (es) => {
728 for (const e of es) if (e.isIntersecting) {
729 e.target.classList.add("ares-in");
730 io.unobserve(e.target);
731 }
732 },
733 { threshold: 0.15, rootMargin: "0px 0px -6% 0px" },
734 );
735 rv.forEach((e) => io.observe(e));
736 let raf = 0;
737 let wasSmall = false;
738 const update = () => {
739 raf = 0;
740 const vh = window.innerHeight;
741 for (const it of items) {
742 const r = it.el.getBoundingClientRect();
743 if (r.bottom < -vh * 0.5 || r.top > vh * 1.5) continue;
744 const p = it.mode === "pin" ? clamp(-r.top / Math.max(1, r.height - vh)) : clamp((vh - r.top) / (vh + r.height));
745 if (Math.abs(p - it.last) < 0.0008) continue;
746 it.last = p;
747 it.el.style.setProperty("--p", p.toFixed(4));
748 if (it.counts.length) setCounts(it, p);
749 }
750 const sm2 = window.scrollY > vh * 0.12;
751 if (sm2 !== wasSmall) {
752 wasSmall = sm2;
753 setSmall(sm2);
754 }
755 };
756 const onScroll = () => {
757 if (!raf) raf = requestAnimationFrame(update);
758 };
759 update();
760 window.addEventListener("scroll", onScroll, { passive: true });
761 window.addEventListener("resize", onScroll);
762 return () => {
763 window.removeEventListener("scroll", onScroll);
764 window.removeEventListener("resize", onScroll);
765 cancelAnimationFrame(raf);
766 io.disconnect();
767 };
768 }, []);
769
770 return (
771 <div className="ares" ref={rootRef}>
772 <style>{CSS}</style>
773 <Nav small={small} />
774 <Hero />
775
776 <div className="ares-ticker" aria-hidden="true">
777 <div className="ares-ticker-in">
778 {[0, 1].map((k) => (
779 <span key={k}>
780 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 ✦{" "}
781 </span>
782 ))}
783 </div>
784 </div>
785
786 <section className="ares-sec ares-manifesto" data-ares-p="pass">
787 <div className="ares-wrap">
788 <p className="ares-eyebrow">The idea</p>
789 <p className="ares-words">
790 {MANIFESTO.map((w, i) => (
791 <span key={i} style={v({ "--w": (i / MANIFESTO.length).toFixed(3) })}>{w} </span>
792 ))}
793 </p>
794 <div className="ares-stats">
795 {STATS.map((s) => (
796 <div key={s.l} data-ares-reveal><b>{s.n}<u>{s.u}</u></b><span>{s.l}</span></div>
797 ))}
798 </div>
799 </div>
800 </section>
801
802 <FloorPlan />
803 <Dome />
804 <Windows />
805 <Postcards />
806 <Passage />
807 <Faq />
808 <Hold />
809 <Footer />
810 </div>
811 );
812}
813
814export default Page;
815
816/* ------------------------------------------------------------------ */
817/* Styles */
818/* ------------------------------------------------------------------ */
819const CSS = `
820.ares {
821 --ares-void: #0a0605; --ares-basalt: #1a100c; --ares-basalt-2: #26160f; --ares-rust: #b4502a; --ares-oxide: #8c2f1b;
822 --ares-ochre: #d99a38; --ares-butter: #f2d3a0; --ares-dust: #e9b98c; --ares-pink: #f3c8b0; --ares-copper: #c97b48;
823 --ares-bone: #f6e9d5; --ares-ink: #2a160e; --ares-blue: #7ba3c6; --ares-line: rgba(242,211,160,.22);
824 --ares-display: "Space Grotesk", "Futura", "Avenir Next", "Century Gothic", "Trebuchet MS", ui-sans-serif, sans-serif;
825 --ares-serif: "Fraunces", "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
826 --ares-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
827 --p: 0;
828 box-sizing: border-box; margin: 0; padding: 0; position: relative;
829 font-family: var(--ares-display); font-size: 17px; line-height: 1.6; color: var(--ares-butter);
830 background: var(--ares-void); overflow-x: clip; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
831}
832:where(.ares) *, :where(.ares) *::before, :where(.ares) *::after { box-sizing: border-box; }
833: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; }
834:where(.ares) a { color: inherit; text-decoration: none; }
835:where(.ares) :is(button, input) { font: inherit; color: inherit; margin: 0; border: 0; background: none; border-radius: 0; appearance: none; -webkit-appearance: none; }
836:where(.ares) button { cursor: pointer; text-align: left; }
837:where(.ares) svg { display: block; }
838.ares :focus-visible { outline: 2px solid var(--ares-ochre); outline-offset: 3px; }
839.ares-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
840.ares-wrap { width: 100%; max-width: 1240px; margin: 0 auto; padding: 0 32px; }
841.ares-sec { position: relative; }
842:where(.ares) em { font-family: var(--ares-serif); font-style: italic; font-weight: 400; color: var(--ares-ochre); letter-spacing: -0.01em; }
843
844.ares-eyebrow { font-family: var(--ares-mono); font-size: 12px; letter-spacing: .2em; text-transform: uppercase; color: var(--ares-copper); margin-bottom: 18px; }
845.ares-eyebrow-ink { color: var(--ares-oxide); }
846.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); }
847.ares-h1 em { text-transform: none; color: var(--ares-ochre); letter-spacing: -.03em; }
848.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; }
849.ares-h2 em { text-transform: none; }
850.ares-h2-ink { color: var(--ares-ink); }
851.ares-h2-ink em { color: var(--ares-oxide); }
852.ares-lede { font-size: 19px; max-width: 28em; margin-top: 22px; color: var(--ares-bone); text-shadow: 0 1px 16px rgba(10,6,5,.9), 0 0 3px rgba(10,6,5,.5); }
853.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; }
854.ares-btn-solid { background: var(--ares-ochre); color: var(--ares-void); box-shadow: 0 0 0 0 rgba(217,154,56,.5); }
855.ares-btn-solid:hover { background: var(--ares-butter); transform: translateY(-2px); box-shadow: 0 8px 30px rgba(217,154,56,.35); }
856.ares-btn-ghost { box-shadow: inset 0 0 0 1.5px currentColor; color: inherit; }
857.ares-btn-ghost:hover { background: rgba(242,211,160,.12); transform: translateY(-2px); }
858.ares-btn-sm { min-height: 40px; padding: 0 18px; font-size: 11.5px; }
859
860/* nav */
861.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); }
862.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); }
863.ares-brand { display: inline-flex; align-items: center; gap: 10px; font-weight: 700; letter-spacing: .32em; font-size: 15px; color: var(--ares-bone); }
864.ares-brand svg { color: var(--ares-ochre); }
865.ares-nav-links { display: flex; gap: 30px; font-family: var(--ares-mono); font-size: 11.5px; letter-spacing: .16em; text-transform: uppercase; }
866.ares-nav-links a { opacity: .8; transition: opacity .2s, color .2s; padding: 6px 0; }
867.ares-nav-links a:hover { opacity: 1; color: var(--ares-ochre); }
868
869/* hero */
870.ares-hero { position: relative; height: 330vh; background: var(--ares-void); --hp: 0; }
871.ares-hero-stage { position: sticky; top: 0; height: 100vh; height: 100svh; overflow: hidden; color: var(--ares-bone); touch-action: pan-y; }
872.ares-photo { position: absolute; inset: 0; overflow: hidden; background: #07040a; will-change: transform, opacity; }
873.ares-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
874.ares-photo-orbit { transform-origin: 50% 85%; transform: scale(calc(1 + var(--hp) * 1.5)); opacity: calc(1 - clamp(0, (var(--hp) - .3) / .22, 1)); }
875.ares-photo-orbit img { object-position: 50% 70%; }
876.ares-photo-surf { transform-origin: 30% 60%; transform: scale(calc(1.45 - clamp(0, (var(--hp) - .45) / .5, 1) * .45)); opacity: clamp(0, (var(--hp) - .38) / .22, 1); }
877.ares-photo-surf img { object-position: 30% 60%; }
878.ares-entry { position: absolute; inset: 0; pointer-events: none; mix-blend-mode: screen; background: radial-gradient(ellipse 70% 55% at 50% 105%, rgba(255,150,70,.85), rgba(210,80,30,.45) 38%, transparent 72%); opacity: min(clamp(0, (var(--hp) - .28) / .12, 1), clamp(0, (.72 - var(--hp)) / .12, 1)); }
879.ares-scrim { position: absolute; inset: 0; pointer-events: none; background: linear-gradient(180deg, rgba(10,6,5,.42) 0, rgba(10,6,5,.16) 38%, rgba(10,6,5,0) 60%), linear-gradient(0deg, rgba(10,6,5,.72) 0, rgba(10,6,5,0) 38%), linear-gradient(90deg, rgba(10,6,5,.45), rgba(10,6,5,0) 55%); }
880.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>"); }
881.ares-vig { position: absolute; inset: 0; pointer-events: none; background: radial-gradient(ellipse at 50% 55%, transparent 55%, rgba(10,6,5,.55) 100%); }
882.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; }
883.ares-hud::before, .ares-hud::after { content: ""; position: absolute; width: 10px; height: 10px; border: 2px solid var(--ares-ochre); }
884.ares-hud::before { top: -2px; left: -2px; border-right: 0; border-bottom: 0; }
885.ares-hud::after { bottom: -2px; right: -2px; border-left: 0; border-top: 0; }
886.ares-hud-l { left: 32px; }
887.ares-hud-r { right: 32px; text-align: right; }
888.ares-hud-k { color: var(--ares-copper); margin-bottom: 6px; font-size: 10.5px; letter-spacing: .2em; }
889.ares-hud-row { display: flex; justify-content: space-between; gap: 18px; padding: 2px 0; }
890.ares-hud-row span { opacity: .6; }
891.ares-hud-row b { font-weight: 600; color: var(--ares-bone); }
892.ares-hud-row .ares-big { font-size: 19px; color: var(--ares-ochre); }
893.ares-hero-a, .ares-hero-b { position: absolute; z-index: 2; left: 32px; right: 32px; will-change: transform, opacity; }
894.ares-hero-a { top: 27vh; max-width: 980px; opacity: calc(1 - clamp(0, (var(--hp) - .1) / .22, 1)); transform: translateY(calc(var(--hp) * -90px)); }
895.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; }
896.ares-hero-b .ares-cta-row { pointer-events: auto; }
897.ares-hero-a .ares-eyebrow, .ares-hero-b .ares-eyebrow { color: var(--ares-butter); text-shadow: 0 1px 12px rgba(10,6,5,.8); }
898.ares-h2-hero { font-size: clamp(34px, 5.4vw, 80px); text-shadow: 0 2px 24px rgba(10,6,5,.7); }
899.ares-cta-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 26px; }
900.ares-hero-b .ares-btn-ghost { color: var(--ares-bone); background: rgba(10,6,5,.35); }
901.ares-rail { position: absolute; z-index: 3; right: 14px; top: 28vh; bottom: 22vh; width: 2px; background: rgba(242,211,160,.22); }
902.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; }
903.ares-rail span { top: 0; }
904.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); }
905.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; }
906@keyframes ares-cue { 0%, 100% { transform: scaleY(.3); opacity: .5; } 50% { transform: scaleY(1); opacity: 1; } }
907
908/* ticker */
909.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); }
910.ares-ticker-in { display: inline-block; animation: ares-marq 55s linear infinite; }
911@keyframes ares-marq { to { transform: translateX(-50%); } }
912
913/* manifesto */
914.ares-manifesto { background: radial-gradient(ellipse at 50% 0%, #3a1a10 0%, var(--ares-basalt) 60%); padding: 150px 0 120px; }
915.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); }
916.ares-words span { opacity: calc(.17 + .83 * clamp(0, (var(--p) - .1 - var(--w) * .42) / .1, 1)); transition: opacity .15s linear; }
917.ares-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; margin-top: 110px; border-top: 1px solid var(--ares-line); }
918.ares-stats > div { padding: 28px 24px 0 0; border-right: 1px solid var(--ares-line); display: flex; flex-direction: column; gap: 10px; }
919.ares-stats > div + div { padding-left: 24px; }
920.ares-stats > div:last-child { border-right: 0; }
921.ares-stats b { font-size: clamp(46px, 5.6vw, 84px); line-height: 1; font-weight: 700; letter-spacing: -.04em; color: var(--ares-ochre); }
922.ares-stats u { font-size: .38em; text-decoration: none; margin-left: 6px; color: var(--ares-copper); letter-spacing: 0; }
923.ares-stats span { font-size: 15px; line-height: 1.45; color: var(--ares-dust); max-width: 17em; }
924
925/* reveal */
926.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); }
927.ares-js [data-ares-reveal].ares-in { opacity: 1; transform: none; }
928
929/* pinned sections */
930.ares-pin { height: var(--pin, 300vh); }
931.ares-pin-stage { position: sticky; top: 0; height: 100vh; height: 100svh; display: flex; align-items: center; padding-top: 70px; overflow: hidden; }
932.ares-draw { fill: none; stroke-dasharray: 1; stroke-dashoffset: calc(1 - clamp(0, (var(--p) - var(--s)) / var(--d, .1), 1)); }
933.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); }
934.ares-d.ares-thin { stroke-width: 1.2; stroke: rgba(201,123,72,.5); }
935.ares-d.ares-thick { stroke-width: 4; }
936.ares-fade { opacity: clamp(0, (var(--p) - var(--s)) / var(--d, .1), 1); }
937
938/* floor plan */
939.ares-plan { background: var(--ares-bone); color: var(--ares-ink); }
940.ares-plan .ares-h2 { color: var(--ares-ink); font-size: clamp(30px, 3.9vw, 56px); }
941.ares-plan .ares-h2 em { color: var(--ares-oxide); }
942.ares-plan-grid { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 48px; align-items: center; }
943.ares-roomlist { margin-top: 28px; border-top: 1px solid rgba(42,22,14,.2); }
944.ares-roomlist li { opacity: clamp(.28, .28 + 1.5 * (var(--p) - var(--s)) / var(--d), 1); }
945.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; }
946.ares-roomlist button:hover, .ares-roomlist button.is-sel { background: var(--ares-oxide); color: var(--ares-bone); padding-left: 14px; }
947.ares-rn, .ares-ra { font-family: var(--ares-mono); font-size: 12px; letter-spacing: .08em; }
948.ares-rt { font-size: 18px; font-weight: 600; letter-spacing: -.01em; }
949.ares-roomnote { margin-top: 20px; font-size: 15.5px; line-height: 1.55; min-height: 5.2em; color: #4a2a1c; }
950.ares-roomnote b { font-family: var(--ares-serif); font-style: italic; font-weight: 400; color: var(--ares-oxide); font-size: 1.1em; }
951.ares-plan-fig svg { width: 100%; height: auto; max-height: calc(100svh - 150px); }
952.ares-hull { stroke: var(--ares-ink); stroke-width: 5; stroke-linejoin: round; }
953.ares-room { cursor: pointer; }
954.ares-room-fill { fill: rgba(140,47,27,.0); transition: fill .25s; opacity: clamp(0, (var(--p) - var(--s) - .06) / .08, 1); }
955.ares-room.is-sel .ares-room-fill { fill: rgba(180,80,42,.24); }
956.ares-room-line { stroke: var(--ares-oxide); stroke-width: 2.4; }
957.ares-room.is-sel .ares-room-line { stroke-width: 4; }
958.ares-room-in { opacity: clamp(0, (var(--p) - var(--s) - .08) / .08, 1); }
959.ares-furn path, .ares-furn circle { fill: none; stroke: rgba(42,22,14,.62); stroke-width: 1.5; stroke-linejoin: round; }
960.ares-pn { fill: var(--ares-oxide); opacity: clamp(0, (var(--p) - var(--s) - .08) / .06, 1); }
961.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; }
962.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; }
963.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; }
964.ares-dim { opacity: clamp(0, (var(--p) - var(--s)) / var(--d), 1); }
965.ares-dim path { stroke: var(--ares-oxide); stroke-width: 1.5; fill: none; }
966.ares-dim text { fill: var(--ares-oxide); font-family: var(--ares-mono); font-size: 13px; letter-spacing: .14em; }
967
968/* dome */
969.ares-dome { background: radial-gradient(ellipse at 50% 30%, #3b1b10 0%, var(--ares-basalt) 70%); }
970.ares-dome-grid { display: grid; grid-template-columns: minmax(0, 4.4fr) minmax(0, 7fr); gap: 40px; align-items: center; }
971.ares-dome .ares-h2 { font-size: clamp(30px, 3.8vw, 54px); }
972.ares-legend { margin-top: 26px; display: grid; gap: 4px; }
973.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); }
974.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; }
975.ares-legend b { display: block; font-size: 15.5px; color: var(--ares-bone); line-height: 1.3; }
976.ares-legend i { display: block; font-style: normal; font-size: 13.5px; line-height: 1.45; color: var(--ares-dust); }
977.ares-dome-fig svg { width: 100%; height: auto; max-height: calc(100svh - 140px); }
978.ares-line { fill: none; stroke: var(--ares-dust); stroke-width: 1.6; stroke-linejoin: round; stroke-linecap: round; }
979.ares-plant { fill: rgba(143,170,90,.8); stroke: #c9d890; stroke-width: 1; }
980.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; }
981.ares-air2 { animation-direction: reverse; stroke: var(--ares-ochre); }
982@keyframes ares-air { to { stroke-dashoffset: -28; } }
983.ares-mk circle { fill: var(--ares-ochre); stroke: var(--ares-void); stroke-width: 3; }
984.ares-mk text { fill: var(--ares-void); font-family: var(--ares-mono); font-weight: 700; font-size: 22px; }
985.ares-mk { opacity: clamp(0, (var(--p) - var(--s)) / var(--d), 1); }
986
987/* windows */
988.ares-win { background: linear-gradient(#0a0605, #130b0c 60%, #0a0605); }
989.ares-win-grid { display: grid; grid-template-columns: minmax(0, 6fr) minmax(0, 6fr); gap: 40px; align-items: center; }
990.ares-win .ares-h2 { font-size: clamp(30px, 3.8vw, 54px); }
991.ares-readouts { display: flex; gap: 40px; margin: 24px 0 14px; font-family: var(--ares-mono); }
992.ares-readouts span { display: block; font-size: 10.5px; letter-spacing: .2em; text-transform: uppercase; color: var(--ares-copper); }
993.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; }
994.ares-readouts i { font-style: normal; }
995.ares-readouts u { text-decoration: none; font-size: 14px; color: var(--ares-dust); margin-left: 6px; }
996.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); }
997.ares-wid { font-family: var(--ares-mono); font-size: 12px; letter-spacing: .1em; color: var(--ares-copper); }
998.ares-wmain { font-size: 16px; color: var(--ares-bone); line-height: 1.35; }
999.ares-wmain b { color: var(--ares-ochre); font-weight: 600; }
1000.ares-wmain i { display: block; font-style: normal; font-size: 13px; color: var(--ares-dust); }
1001.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; }
1002.ares-orbit svg { width: 100%; height: auto; max-height: calc(100svh - 120px); margin: 0 auto; }
1003.ares-orb { fill: none; stroke: rgba(242,211,160,.22); stroke-dasharray: 3 6; }
1004.ares-xfer-ghost { fill: none; stroke: rgba(242,211,160,.14); stroke-width: 1.5; }
1005.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)); }
1006.ares-spin { transform-box: view-box; transform-origin: 300px 300px; transform: rotate(var(--rot)); }
1007.ares-orb-t { fill: var(--ares-dust); font-family: var(--ares-mono); font-size: 12px; letter-spacing: .2em; }
1008.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); }
1009
1010/* residents */
1011.ares-res { background: linear-gradient(#e9b98c, #f3c8b0 55%, #f6e9d5); color: var(--ares-ink); padding: 140px 0 160px; }
1012.ares-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; margin-top: 64px; align-items: start; }
1013.ares-card { transform: translateY(calc((var(--p) - .5) * var(--par) * -1px)); will-change: transform; }
1014.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); }
1015.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); }
1016.ares-card:nth-child(2) { margin-top: 46px; }
1017.ares-stamp { position: absolute; top: 14px; right: 14px; width: 76px; height: 58px; padding: 3px; background: #f6e9d5; outline: 2px dashed var(--ares-oxide); outline-offset: -1px; transform: rotate(4deg); overflow: hidden; }
1018.ares-stamp img { width: 100%; height: 100%; object-fit: cover; display: block; transform: scale(1.8); }
1019.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; }
1020.ares-card blockquote { font-family: var(--ares-serif); font-style: italic; font-size: 19px; line-height: 32px; margin-top: 56px; color: #3a1f14; }
1021.ares-card figcaption { margin-top: 24px; display: grid; grid-template-columns: 52px minmax(0, 1fr); column-gap: 14px; gap: 1px 14px; font-size: 13.5px; color: #6a4030; }
1022.ares-card figcaption img { grid-row: 1 / span 3; width: 52px; height: 52px; border-radius: 50%; object-fit: cover; object-position: center top; border: 2px solid rgba(140,47,27,.35); align-self: center; }
1023.ares-card figcaption b { font-size: 16px; color: var(--ares-ink); }
1024.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; }
1025
1026/* passage */
1027.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%); }
1028.ares-pass-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 30px; flex-wrap: wrap; }
1029.ares-toggle { display: inline-flex; padding: 4px; border-radius: 999px; border: 1px solid var(--ares-line); background: rgba(10,6,5,.4); }
1030.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; }
1031.ares-toggle button.is-on { background: var(--ares-ochre); color: var(--ares-void); }
1032.ares-tiers { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 60px; align-items: stretch; }
1033.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; }
1034.ares-tier:hover { transform: translateY(-8px); border-color: var(--ares-ochre); }
1035.ares-tier.is-hot { background: linear-gradient(160deg, #c05a30, #8c2f1b); border-color: var(--ares-butter); color: var(--ares-bone); }
1036.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; }
1037.ares-tn { font-family: var(--ares-mono); font-size: 10.5px; letter-spacing: .22em; color: var(--ares-copper); }
1038.is-hot .ares-tn { color: var(--ares-butter); }
1039.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; }
1040.ares-tier header p { font-family: var(--ares-serif); font-style: italic; color: var(--ares-dust); }
1041.is-hot header p { color: var(--ares-butter); }
1042.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; }
1043.is-hot .ares-price b { color: var(--ares-bone); }
1044.ares-price span { font-family: var(--ares-mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--ares-dust); }
1045.ares-tier ul { display: grid; gap: 9px; font-size: 15px; flex: 1; }
1046.ares-tier li { padding-left: 22px; position: relative; color: var(--ares-butter); }
1047.ares-tier li::before { content: ""; position: absolute; left: 0; top: .55em; width: 10px; height: 2px; background: var(--ares-ochre); }
1048.is-hot li::before { background: var(--ares-bone); }
1049.ares-tear { position: relative; height: 12px; display: flex; align-items: center; }
1050.ares-tear span { flex: 1; border-top: 2px dashed rgba(242,211,160,.35); }
1051.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; }
1052.ares-tier .ares-btn-ghost { color: var(--ares-butter); }
1053
1054/* mission log */
1055.ares-log { background: var(--ares-void); padding: 140px 0 150px; }
1056.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); }
1057.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); }
1058.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; }
1059@keyframes ares-blink { 50% { opacity: .2; } }
1060.ares-console-body { background: repeating-linear-gradient(0deg, rgba(242,211,160,.025) 0 1px, transparent 1px 3px); }
1061.ares-logrow + .ares-logrow { border-top: 1px solid rgba(242,211,160,.12); }
1062.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; }
1063.ares-logrow button:hover { background: rgba(217,154,56,.07); }
1064.ares-lt { font-size: 12px; letter-spacing: .12em; color: var(--ares-copper); }
1065.ares-lq { font-size: 16px; color: var(--ares-bone); }
1066.ares-logrow.is-open .ares-lq { color: var(--ares-ochre); }
1067.ares-lp { color: var(--ares-ochre); font-size: 20px; text-align: right; }
1068.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; }
1069.ares-lrx { color: #7bd88f; margin-right: 6px; }
1070.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; }
1071
1072/* hold */
1073.ares-hold { background: linear-gradient(#f6e9d5, #e9b98c); color: var(--ares-ink); padding: 130px 0 120px; text-align: center; }
1074.ares-hold .ares-h2 { margin: 0 auto; max-width: 13em; }
1075.ares-count { display: flex; justify-content: center; gap: 14px; margin: 46px 0; flex-wrap: wrap; }
1076.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); }
1077.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; }
1078.ares-count span { font-family: var(--ares-mono); font-size: 11px; letter-spacing: .24em; text-transform: uppercase; color: var(--ares-dust); }
1079.ares-form { display: flex; justify-content: center; gap: 10px; max-width: 560px; margin: 0 auto; flex-wrap: wrap; }
1080.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; }
1081.ares-form input::placeholder { color: #8a6a58; }
1082.ares-form input:focus { border-color: var(--ares-oxide); outline: none; box-shadow: 0 0 0 4px rgba(180,80,42,.25); }
1083.ares-form .ares-btn-solid { background: var(--ares-oxide); color: var(--ares-bone); min-height: 52px; }
1084.ares-form .ares-btn-solid:hover { background: var(--ares-rust); }
1085.ares-thanks { font-size: 20px; max-width: 30em; margin: 0 auto; }
1086.ares-thanks b { font-family: var(--ares-mono); color: var(--ares-oxide); }
1087.ares-fine { margin-top: 22px; font-size: 13.5px; color: #6a4030; }
1088
1089/* footer */
1090.ares-foot { background: var(--ares-void); padding: 80px 0 40px; color: var(--ares-dust); }
1091.ares-foot-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 40px; }
1092.ares-foot-grid p { margin-top: 16px; max-width: 22em; font-size: 15px; }
1093.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; }
1094.ares-foot-grid a:not(.ares-brand) { display: block; padding: 4px 0; font-size: 15px; transition: color .2s; }
1095.ares-foot-grid a:not(.ares-brand):hover { color: var(--ares-ochre); }
1096.ares-legal { margin-top: 60px; padding-top: 22px; border-top: 1px solid var(--ares-line); font-size: 12.5px; color: #9a7a68; }
1097
1098/* responsive */
1099@media (max-width: 980px) {
1100 .ares-plan-grid, .ares-dome-grid, .ares-win-grid { grid-template-columns: 1fr; gap: 14px; }
1101 .ares-tiers, .ares-cards { grid-template-columns: 1fr; }
1102 .ares-card:nth-child(2) { margin-top: 0; }
1103 .ares-stats { grid-template-columns: repeat(2, 1fr); }
1104 .ares-stats > div:nth-child(2) { border-right: 0; }
1105 .ares-stats > div:nth-child(3) { padding-left: 0; }
1106 .ares-stats > div:nth-child(n+3) { border-top: 1px solid var(--ares-line); margin-top: 28px; }
1107 .ares-foot-grid { grid-template-columns: 1fr 1fr; }
1108 .ares-nav-links { display: none; }
1109}
1110@media (max-width: 700px) {
1111 .ares { font-size: 16px; }
1112 .ares-wrap { padding: 0 16px; }
1113 .ares-nav { padding: 12px 16px; }
1114 .ares-hud { top: 70px; min-width: 0; padding: 8px 10px; font-size: 9.5px; }
1115 .ares-hud-l { left: 12px; }
1116 .ares-hud-r { right: 12px; }
1117 .ares-hud-row { gap: 8px; }
1118 .ares-hud-row .ares-big { font-size: 14px; }
1119 .ares-hide-s { display: none; }
1120 .ares-hero-a, .ares-hero-b { left: 16px; right: 16px; }
1121 .ares-hero-a { top: 28vh; }
1122 .ares-lede { font-size: 16px; }
1123 .ares-rail { right: 6px; }
1124 .ares-manifesto, .ares-pass, .ares-log { padding: 90px 0; }
1125 .ares-res { padding: 90px 0 100px; }
1126 .ares-pin-stage { padding-top: 64px; align-items: flex-start; }
1127 .ares-plan .ares-h2, .ares-dome .ares-h2, .ares-win .ares-h2 { font-size: 24px; }
1128 .ares-plan .ares-eyebrow, .ares-dome .ares-eyebrow, .ares-win .ares-eyebrow { margin-bottom: 8px; }
1129 .ares-plan-fig { order: -1; }
1130 .ares-plan-fig svg { max-height: 34svh; }
1131 .ares-rname, .ares-rarea { display: none; }
1132 .ares-roomlist { margin-top: 12px; display: grid; grid-template-columns: 1fr 1fr; column-gap: 12px; }
1133 .ares-roomlist button { padding: 6px 4px; grid-template-columns: 24px 1fr auto; }
1134 .ares-rt { font-size: 13px; }
1135 .ares-ra { font-size: 10px; }
1136 .ares-roomnote { font-size: 13px; min-height: 0; margin-top: 10px; }
1137 .ares-dome-fig { order: -1; }
1138 .ares-dome-fig svg { max-height: 34svh; }
1139 .ares-legend { grid-template-columns: 1fr 1fr; column-gap: 10px; margin-top: 10px; gap: 0 10px; }
1140 .ares-legend li { padding: 5px 0; grid-template-columns: 24px 1fr; gap: 6px; }
1141 .ares-legend i { display: none; }
1142 .ares-legend b { font-size: 12.5px; }
1143 .ares-ln { width: 20px; height: 20px; font-size: 10px; }
1144 .ares-orbit svg { max-height: 36svh; }
1145 .ares-orbit { order: -1; }
1146 .ares-readouts { gap: 20px; margin: 8px 0; }
1147 .ares-readouts b { font-size: 24px; }
1148 .ares-winlist li { grid-template-columns: 1fr; gap: 0; padding: 6px 0; }
1149 .ares-wid, .ares-wst, .ares-wmain i { display: none; }
1150 .ares-wmain { font-size: 13.5px; }
1151 .ares-stats b { font-size: 44px; }
1152 .ares-stats span { font-size: 13px; }
1153 .ares-logrow button { grid-template-columns: 1fr 24px; padding: 16px 14px; }
1154 .ares-lt { grid-column: 1 / -1; }
1155 .ares-la { padding: 0 14px 18px; font-size: 13px; }
1156 .ares-count > div { min-width: 70px; padding: 12px 8px 10px; }
1157 .ares-pass-head { align-items: flex-start; }
1158 .ares-foot-grid { grid-template-columns: 1fr 1fr; gap: 28px; }
1159 .ares-foot-grid > div:first-child { grid-column: 1 / -1; }
1160}
1161@media (max-width: 700px) and (max-height: 700px) {
1162 .ares-roomnote { display: none; }
1163}
1164
1165@media (prefers-reduced-motion: reduce) {
1166 .ares *, .ares *::before, .ares *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; transition-delay: 0ms !important; }
1167 .ares-hero { height: 100vh; height: 100svh; }
1168 .ares-hero-a { opacity: 0; }
1169 .ares-hero-b { opacity: 1; transform: none; }
1170 .ares-scrollcue, .ares-rail { display: none; }
1171 .ares-ticker-in { animation: none; }
1172 .ares-pin { height: auto; }
1173 .ares-pin-stage { position: relative; height: auto; min-height: 0; padding: 100px 0; overflow: visible; }
1174 .ares-card { transform: none; }
1175 .ares-air { animation: none; }
1176}
1177@media (prefers-contrast: more) {
1178 .ares-nav.is-small, .ares-hud { background: var(--ares-void); -webkit-backdrop-filter: none; backdrop-filter: none; }
1179}
1180`;

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 stage, smoothed progress --hp. The mars-orbit image zooms toward the limb and fades out, an orange entry-glow layer peaks mid-scroll, then mars-surface fades in and settles from a 1.45x zoom. Gradient scrims keep the headline, lede and HUD at AA contrast.
  • 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 image stack, 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.

6. Assets

Images live in public/img/space/: mars-orbit and mars-surface, each as .webp (about 1400w), -1200.webp and -640.webp. Copy the whole img/space/ folder next to the page so root-relative /img/space/... URLs resolve. The orbit image is the LCP (fetchPriority="high"); the surface image and the postcard stamp crops (rover, dunes, sun and Phobos via object-position) are lazy-loaded.

Resident postcard portraits: Placeholder portraits live in public/img/people/ (p1.webp to p9.webp, 328×328). Copy the folder next to the page so root-relative /img/people/... URLs resolve, or swap in your own photos. They are lazy-loaded and cropped with object-fit: cover; object-position: center top.

Questions about Ares

A home on Mars — scroll down from low orbit to the surface of Jezero Crater. It is a free space & habitat landing page for React with a matching DESIGN.md brief.

Open the page, click Copy full source or Download .tsx, and drop the single Page file into your React project. Copy the public/img folder next to it for the demo photography, or swap in your own images.

Yes. It is MIT-licensed: free for personal and commercial projects, no signup and no attribution required.

Yes. The DESIGN.md describes tokens, type scale, layout, components and motion so Claude Code, Cursor, Lovable or v0 can extend the page without drifting from its look.