1/**
2 * Terra — regenerative agriculture & climate-farming collective.
3 *
4 * The page itself grows. The hero is a pinned vertical cross-section from sky
5 * to root zone: as you scroll, procedural SVG wheat, cover crops and a tree
6 * rise out of the soil, root systems branch downward through five soil
7 * horizons, carbon particles fall from the sky and are banked as green flecks
8 * in the earth, and a live per-hectare counter climbs. A season slider (always
9 * on screen) re-tints the whole site spring -> summer -> autumn -> winter,
10 * moving the sun, thinning the foliage and laying snow on the ground.
11 *
12 * Below: topographic contour lines that draw across the field, a live carbon
13 * ledger with growing bars and a drawn trend line, an interactive field map
14 * (hover / focus / tap a plot and its crop rows are drawn in and its soil
15 * data appears), a mycelium network with travelling signals, grower voices
16 * that grow in, and a wheat field that rises under the sign-up form.
17 *
18 * Everything is procedural SVG + CSS custom properties driven from a single
19 * rAF scroll loop (no React state per frame). No images, no web fonts, no
20 * network. prefers-reduced-motion renders the fully grown, static scene.
21 * SSR-safe: window/document are only touched inside effects.
22 */
23import { useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react";
24
25/* ------------------------------------------------------------------ */
26/* Procedural generation (deterministic) */
27/* ------------------------------------------------------------------ */
28function rng(seed: number) {
29 let a = seed >>> 0;
30 return () => {
31 a = (a + 0x6d2b79f5) >>> 0;
32 let t = a;
33 t = Math.imul(t ^ (t >>> 15), t | 1);
34 t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
35 return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
36 };
37}
38const f1 = (n: number) => Math.round(n * 10) / 10;
39const HZ = 458; // horizon y in the 1200x1000 scene
40
41type Stroke = { d: string; delay: number; w: number; o?: number };
42type Leaf = { x: number; y: number; rx: number; ry: number; rot: number; delay: number; c: 1 | 2 };
43
44function buildScene() {
45 const r = rng(11);
46 const stems: Stroke[] = [];
47 const blades: Stroke[] = [];
48 const ears: { x: number; y: number; delay: number; rot: number }[] = [];
49 const leaves: Leaf[] = [];
50 const flowers: { x: number; y: number; delay: number }[] = [];
51 const roots: Stroke[] = [];
52 const bases: { x: number; delay: number }[] = [];
53
54 const addRoot = (x: number, y: number, ang: number, len: number, depth: number, maxD: number, d0: number, w: number) => {
55 const ex = x + Math.sin(ang) * len;
56 const ey = y + Math.cos(ang) * len;
57 const cx = x + Math.sin(ang + (r() - 0.5) * 0.9) * len * 0.5;
58 const cy = y + Math.cos(ang) * len * 0.45;
59 roots.push({ d: `M${f1(x)} ${f1(y)}Q${f1(cx)} ${f1(cy)} ${f1(ex)} ${f1(ey)}`, delay: d0, w, o: 0.95 - depth * 0.12 });
60 if (depth < maxD) {
61 const n = depth === 0 ? 2 : 2;
62 for (let i = 0; i < n; i++) {
63 const t = 0.35 + r() * 0.45;
64 const px = x + (ex - x) * t;
65 const py = y + (ey - y) * t;
66 const side = i % 2 ? 1 : -1;
67 addRoot(px, py, ang + side * (0.55 + r() * 0.6), len * (0.42 + r() * 0.18), depth + 1, maxD, d0 + 0.1 + depth * 0.04, Math.max(0.8, w * 0.6));
68 }
69 }
70 };
71
72 // wheat
73 for (let i = 0; i < 11; i++) {
74 const x = 110 + i * 92 + r() * 26;
75 const h = 130 + r() * 90;
76 const lean = (r() - 0.5) * 46;
77 const d = r() * 0.28;
78 const qx = x + lean * 0.15;
79 const qy = HZ - h * 0.55;
80 const tx = x + lean;
81 const ty = HZ - h;
82 stems.push({ d: `M${f1(x)} ${HZ}Q${f1(qx)} ${f1(qy)} ${f1(tx)} ${f1(ty)}`, delay: d, w: 3 });
83 [0.28, 0.46, 0.64].forEach((t, k) => {
84 const px = (1 - t) * (1 - t) * x + 2 * (1 - t) * t * qx + t * t * tx;
85 const py = (1 - t) * (1 - t) * HZ + 2 * (1 - t) * t * qy + t * t * ty;
86 const side = k % 2 ? 1 : -1;
87 blades.push({ d: `M${f1(px)} ${f1(py)}q${side * 20} ${-26 - k * 3} ${side * 42} ${-12 - k * 5}`, delay: d + 0.2 + k * 0.07, w: 3.4 });
88 });
89 ears.push({ x: tx, y: ty, delay: d + 0.45, rot: lean * 0.5 });
90 bases.push({ x, delay: d });
91 // fibrous roots
92 for (let k = 0; k < 4; k++) addRoot(x, HZ + 4, (k - 1.5) * 0.42 + (r() - 0.5) * 0.15, 150 + r() * 110, 0, 1, 0.02 + r() * 0.1, 2.6);
93 }
94 // broadleaf cover crop / clover
95 for (let i = 0; i < 6; i++) {
96 const x = 160 + i * 168 + r() * 40;
97 const h = 60 + r() * 55;
98 const d = 0.05 + r() * 0.25;
99 const lean = (r() - 0.5) * 20;
100 stems.push({ d: `M${f1(x)} ${HZ}Q${f1(x + lean)} ${f1(HZ - h * 0.6)} ${f1(x + lean * 1.4)} ${f1(HZ - h)}`, delay: d, w: 2.6 });
101 for (let k = 0; k < 5; k++) {
102 const t = 0.25 + k * 0.17;
103 const side = k % 2 ? 1 : -1;
104 leaves.push({ x: x + lean * t * 1.3 + side * 9, y: HZ - h * t, rx: 13, ry: 7, rot: side * 28 - 90 + 90, delay: d + 0.2 + k * 0.05, c: k % 2 ? 1 : 2 });
105 }
106 flowers.push({ x: x + lean * 1.4, y: HZ - h - 4, delay: d + 0.55 });
107 addRoot(x, HZ + 4, (r() - 0.5) * 0.2, 210 + r() * 40, 0, 2, 0.05, 3);
108 }
109 // hedge on the left + tree on the right
110 for (let k = 0; k < 6; k++) leaves.push({ x: 40 + (k % 3) * 26 + r() * 8, y: HZ - 28 - Math.floor(k / 3) * 26, rx: 28, ry: 22, rot: 0, delay: 0.3 + k * 0.04, c: k % 2 ? 1 : 2 });
111 stems.push({ d: `M1096 ${HZ}Q1092 ${HZ - 70} 1100 ${HZ - 170}`, delay: 0.1, w: 11 });
112 stems.push({ d: `M1098 ${HZ - 100}Q1070 ${HZ - 130} 1052 ${HZ - 168}`, delay: 0.32, w: 5 });
113 stems.push({ d: `M1099 ${HZ - 130}Q1130 ${HZ - 160} 1150 ${HZ - 190}`, delay: 0.36, w: 5 });
114 const canopy: [number, number, number][] = [[1100, 100, 54], [1056, 130, 42], [1150, 128, 44], [1100, 150, 50], [1070, 88, 36], [1138, 90, 36]];
115 canopy.forEach(([x, y, rr], k) => leaves.push({ x, y, rx: rr, ry: rr * 0.82, rot: 0, delay: 0.42 + k * 0.05, c: k % 2 ? 1 : 2 }));
116 for (let k = 0; k < 7; k++) addRoot(1096, HZ + 6, (k - 3) * 0.34 + (r() - 0.5) * 0.1, 280 + r() * 190, 0, 2, 0.05 + r() * 0.1, 5.2);
117
118 // soil
119 const specks: { x: number; y: number; r: number; c: string }[] = [];
120 const cols = ["#8a6a45", "#2b1c12", "#a8855a", "#3b2616", "#c4a572"];
121 for (let i = 0; i < 260; i++) {
122 const y = HZ + 10 + r() * 530;
123 specks.push({ x: r() * 1200, y, r: 0.8 + r() * (y > 780 ? 5 : 2.2), c: cols[Math.floor(r() * cols.length)] });
124 }
125 const store: { x: number; y: number; delay: number }[] = [];
126 for (let i = 0; i < 70; i++) store.push({ x: 40 + r() * 1120, y: HZ + 24 + r() * 330, delay: r() * 0.6 });
127 const falling: { x: number; dx: number; dy: number; dur: number; del: number }[] = [];
128 for (let i = 0; i < 22; i++) falling.push({ x: 60 + r() * 1080, dx: (r() - 0.5) * 80, dy: 300 + r() * 230, dur: 7 + r() * 6, del: -r() * 12 });
129 const worms = [
130 `M180 ${HZ + 70}q30 -16 60 0t60 0t60 0`,
131 `M820 ${HZ + 150}q-30 14 -60 0t-60 0t-60 0t-40 6`,
132 `M420 ${HZ + 230}q26 -14 52 0t52 0`,
133 ];
134 return { stems, blades, ears, leaves, flowers, roots, bases, specks, store, falling, worms };
135}
136const SCENE = buildScene();
137
138const wave = (y: number, amp: number, ph: number) => {
139 let d = `M0 ${y}`;
140 for (let x = 0; x <= 1200; x += 40) d += `L${x} ${f1(y + Math.sin(x * 0.011 + ph) * amp + Math.sin(x * 0.027 + ph * 2) * amp * 0.5)}`;
141 return d + "L1200 1000L0 1000Z";
142};
143const STRATA = [
144 { y: 920, fill: "#4d4a46", amp: 5, ph: 3 },
145 { y: 790, fill: "#8a6a45", amp: 7, ph: 2 },
146 { y: 640, fill: "#6b4a2e", amp: 6, ph: 1 },
147 { y: 500, fill: "#4a3020", amp: 4, ph: 0.4 },
148 { y: HZ + 2, fill: "#2b1c12", amp: 2.5, ph: 5 },
149];
150const STRATA_PATHS = STRATA.map((s) => ({ ...s, d: wave(s.y, s.amp, s.ph) }));
151
152/* Contours */
153function buildContours() {
154 const hills = [
155 { cx: 300, cy: 280, n: 13, step: 24, ax: 1.5, ay: 1, ph: 0.4 },
156 { cx: 820, cy: 430, n: 15, step: 22, ax: 1.2, ay: 1, ph: 2.1 },
157 { cx: 1050, cy: 150, n: 8, step: 20, ax: 1.1, ay: 0.9, ph: 4.2 },
158 ];
159 const out: { d: string; idx: boolean; delay: number; label?: { x: number; y: number; t: string } }[] = [];
160 hills.forEach((h, hi) => {
161 for (let i = 1; i <= h.n; i++) {
162 const base = i * h.step;
163 let d = "";
164 for (let a = 0; a <= 6.2832 + 0.01; a += 0.1) {
165 const k = 1 + 0.16 * Math.sin(2 * a + h.ph + i * 0.08) + 0.1 * Math.sin(3 * a + h.ph * 2) + 0.05 * Math.sin(5 * a + i * 0.3);
166 const x = h.cx + Math.cos(a) * base * h.ax * k;
167 const y = h.cy + Math.sin(a) * base * h.ay * k;
168 d += (a === 0 ? "M" : "L") + f1(x) + " " + f1(y);
169 }
170 const idx = i % 4 === 0;
171 out.push({
172 d: d + "Z",
173 idx,
174 delay: ((h.n - i) / h.n) * 0.7 + hi * 0.06,
175 label: idx ? { x: h.cx + base * h.ax * 1.0 + 4, y: h.cy + 4, t: `${380 + (h.n - i) * 10} m` } : undefined,
176 });
177 }
178 });
179 return out;
180}
181const CONTOURS = buildContours();
182
183/* Plots (field map) */
184type Plot = { id: string; name: string; crop: string; practice: string; since: number; ha: number; som: number; carbon: number; kind: "wheat" | "clover" | "pasture" | "orchard" | "fallow"; poly: string; clip: string; cx: number; cy: number; ang: number };
185const PLOT_DATA: Omit<Plot, "poly" | "clip" | "cx" | "cy" | "ang">[] = [
186 { id: "p1", name: "North Meadow", crop: "Perennial pasture", practice: "Rotational grazing", since: 2019, ha: 14.2, som: 6.8, carbon: 1.9, kind: "pasture" },
187 { id: "p2", name: "Hedge Row", crop: "Spelt + hedgerow", practice: "Alley cropping", since: 2020, ha: 8.6, som: 5.4, carbon: 2.7, kind: "wheat" },
188 { id: "p3", name: "Long Acre", crop: "Winter rye", practice: "No-till, cover crop", since: 2021, ha: 18.0, som: 4.9, carbon: 1.2, kind: "wheat" },
189 { id: "p4", name: "Alder Brook", crop: "Riparian buffer", practice: "Native planting", since: 2018, ha: 5.1, som: 8.2, carbon: 3.4, kind: "orchard" },
190 { id: "p5", name: "Clover Flat", crop: "Red clover ley", practice: "Nitrogen-fixing ley", since: 2022, ha: 11.7, som: 4.1, carbon: 1.1, kind: "clover" },
191 { id: "p6", name: "Stone Wall", crop: "Heritage wheat", practice: "Minimum till, compost", since: 2020, ha: 16.4, som: 5.2, carbon: 1.5, kind: "wheat" },
192 { id: "p7", name: "The Orchard", crop: "Apple + clover", practice: "Silvopasture", since: 2017, ha: 6.9, som: 7.1, carbon: 3.1, kind: "orchard" },
193 { id: "p8", name: "Pond Field", crop: "Mixed vegetables", practice: "Compost, living mulch", since: 2023, ha: 3.4, som: 3.8, carbon: 0.9, kind: "clover" },
194 { id: "p9", name: "South Fallow", crop: "Fallow / rewilding", practice: "Natural succession", since: 2016, ha: 9.8, som: 7.6, carbon: 2.2, kind: "fallow" },
195 { id: "p10", name: "Wind Hill", crop: "Barley + vetch", practice: "Relay intercrop", since: 2021, ha: 20.3, som: 4.6, carbon: 1.4, kind: "wheat" },
196 { id: "p11", name: "Mill Pasture", crop: "Herbal ley", practice: "Mob grazing", since: 2019, ha: 13.5, som: 6.2, carbon: 2.0, kind: "pasture" },
197 { id: "p12", name: "East Terrace", crop: "Oats + peas", practice: "Contour strips", since: 2022, ha: 10.2, som: 4.3, carbon: 1.3, kind: "clover" },
198];
199function buildPlots(): Plot[] {
200 const r = rng(5);
201 const V: [number, number][][] = [];
202 for (let j = 0; j <= 3; j++) {
203 V.push([]);
204 for (let i = 0; i <= 4; i++) {
205 const edgeX = i === 0 || i === 4;
206 const edgeY = j === 0 || j === 3;
207 V[j].push([60 + i * 170 + (edgeX ? (r() - 0.5) * 10 : (r() - 0.5) * 44), 40 + j * 145 + (edgeY ? (r() - 0.5) * 10 : (r() - 0.5) * 40)]);
208 }
209 }
210 return PLOT_DATA.map((p, k) => {
211 const i = k % 4;
212 const j = Math.floor(k / 4);
213 const pts = [V[j][i], V[j][i + 1], V[j + 1][i + 1], V[j + 1][i]];
214 const cx = pts.reduce((s, q) => s + q[0], 0) / 4;
215 const cy = pts.reduce((s, q) => s + q[1], 0) / 4;
216 const poly = pts.map((q) => `${f1(q[0])},${f1(q[1])}`).join(" ");
217 const ang = (r() - 0.5) * 50 + (p.kind === "pasture" ? 90 : 0);
218 return { ...p, poly, clip: poly, cx, cy, ang };
219 });
220}
221const PLOTS = buildPlots();
222
223/* Mycelium */
224function buildMyc() {
225 const r = rng(23);
226 const nodes: { x: number; y: number }[] = [{ x: 300, y: 40 }];
227 for (let i = 0; i < 44; i++) nodes.push({ x: 20 + r() * 560, y: 40 + r() * 330 });
228 const edges: { a: number; b: number; d: string; delay: number }[] = [];
229 nodes.forEach((n, i) => {
230 if (i === 0) return;
231 const near = nodes
232 .map((m, j) => ({ j, dist: Math.hypot(m.x - n.x, m.y - n.y) }))
233 .filter((o) => o.j < i)
234 .sort((a, b) => a.dist - b.dist)
235 .slice(0, i < 3 ? 1 : 2);
236 near.forEach((o) => {
237 const m = nodes[o.j];
238 const mx = (n.x + m.x) / 2 + (r() - 0.5) * 30;
239 const my = (n.y + m.y) / 2 + (r() - 0.5) * 30;
240 edges.push({ a: i, b: o.j, d: `M${f1(m.x)} ${f1(m.y)}Q${f1(mx)} ${f1(my)} ${f1(n.x)} ${f1(n.y)}`, delay: Math.min(0.75, (n.y - 40) / 420 + r() * 0.1) });
241 });
242 });
243 return { nodes, edges };
244}
245const MYC = buildMyc();
246
247/* ------------------------------------------------------------------ */
248/* Seasons */
249/* ------------------------------------------------------------------ */
250const SEASONS = ["Spring", "Summer", "Autumn", "Winter"];
251const STOPS = [
252 { sky1: "#cfe8d8", sky2: "#f6f0d6", leaf: "#6fae3b", leaf2: "#9ccf4a", wheat: "#b8c46a", bg: "#f3ecda", sun: "#ffe08a", bloom: 0.9, snow: 0, sy: 0, hill: "#7fae5a" },
253 { sky1: "#8cc8e8", sky2: "#fbe9b0", leaf: "#3e8f2d", leaf2: "#5fb540", wheat: "#e2b94a", bg: "#f5ecd0", sun: "#ffd24a", bloom: 1, snow: 0, sy: -40, hill: "#5f9b3c" },
254 { sky1: "#e8b98a", sky2: "#f7dcae", leaf: "#b86a2a", leaf2: "#d89a35", wheat: "#c98a3a", bg: "#f1e2c6", sun: "#ff9a4a", bloom: 0.72, snow: 0, sy: 40, hill: "#b9803a" },
255 { sky1: "#b9c6cf", sky2: "#e9ecec", leaf: "#7d8a6a", leaf2: "#a3ad92", wheat: "#b9a98a", bg: "#ebe9e2", sun: "#f1f5f7", bloom: 0.3, snow: 1, sy: 80, hill: "#9aa58f" },
256];
257const hex2 = (h: string) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));
258const mixHex = (a: string, b: string, t: number) => {
259 const A = hex2(a);
260 const B = hex2(b);
261 return "#" + A.map((v, i) => Math.round(v + (B[i] - v) * t).toString(16).padStart(2, "0")).join("");
262};
263function applySeason(el: HTMLElement, s: number) {
264 const i = Math.min(2, Math.floor(s));
265 const t = s - i;
266 const a = STOPS[i];
267 const b = STOPS[i + 1];
268 const set = (k: string, v: string) => el.style.setProperty(k, v);
269 (["sky1", "sky2", "leaf", "leaf2", "wheat", "bg", "sun", "hill"] as const).forEach((k) => set(`--${k}`, mixHex(a[k], b[k], t)));
270 set("--bloom", String(a.bloom + (b.bloom - a.bloom) * t));
271 set("--snow", String(a.snow + (b.snow - a.snow) * t));
272 set("--sy", `${a.sy + (b.sy - a.sy) * t}px`);
273}
274
275/* ------------------------------------------------------------------ */
276/* Hooks */
277/* ------------------------------------------------------------------ */
278function useInView<T extends HTMLElement>(threshold = 0.25): [React.RefObject<T | null>, boolean] {
279 const ref = useRef<T | null>(null);
280 const [seen, setSeen] = useState(false);
281 useEffect(() => {
282 const el = ref.current;
283 if (!el) return;
284 if (typeof IntersectionObserver === "undefined") {
285 setSeen(true);
286 return;
287 }
288 const io = new IntersectionObserver(
289 (es) => {
290 if (es.some((e) => e.isIntersecting)) {
291 setSeen(true);
292 io.disconnect();
293 }
294 },
295 { threshold },
296 );
297 io.observe(el);
298 return () => io.disconnect();
299 }, [threshold]);
300 return [ref, seen];
301}
302const v = (o: Record<string, string | number>) => o as unknown as CSSProperties;
303
304/* ------------------------------------------------------------------ */
305/* Hero scene */
306/* ------------------------------------------------------------------ */
307function Scene() {
308 const S = SCENE;
309 const ticks = [0, 30, 60, 120, 200];
310 const tickY = [HZ, 500, 640, 790, 920];
311 const labels = [
312 ["O", "Litter layer", HZ + 22],
313 ["A", "Topsoil, 6% organic matter", 570],
314 ["B", "Subsoil", 715],
315 ["C", "Parent material", 860],
316 ["R", "Bedrock", 965],
317 ] as const;
318 return (
319 <svg className="tr-scene" viewBox="0 0 1200 1000" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Cross-section from sky to root zone: crops, a tree and their roots growing through five soil layers while carbon is stored in the soil">
320 <defs>
321 <linearGradient id="tr-sky" x1="0" y1="0" x2="0" y2="1">
322 <stop offset="0" stopColor="var(--sky1)" />
323 <stop offset="1" stopColor="var(--sky2)" />
324 </linearGradient>
325 <linearGradient id="tr-fade" x1="0" y1="0" x2="0" y2="1">
326 <stop offset="0" stopColor="#000" stopOpacity="0" />
327 <stop offset="1" stopColor="#000" stopOpacity=".28" />
328 </linearGradient>
329 </defs>
330 <rect width="1200" height={HZ + 4} fill="url(#tr-sky)" />
331 <g transform={`translate(0 ${HZ - 330})`}>
332 <circle className="tr-sun" cx="860" cy="120" r="46" fill="var(--sun)" />
333 <circle className="tr-sun tr-halo" cx="860" cy="120" r="78" fill="var(--sun)" />
334 <g className="tr-clouds" fill="#fff" opacity=".7">
335 <g className="tr-cl a"><ellipse cx="200" cy="90" rx="70" ry="16" /><ellipse cx="240" cy="76" rx="40" ry="14" /></g>
336 <g className="tr-cl b"><ellipse cx="620" cy="150" rx="90" ry="14" /><ellipse cx="660" cy="136" rx="46" ry="13" /></g>
337 <g className="tr-cl c"><ellipse cx="1000" cy="60" rx="60" ry="12" /></g>
338 </g>
339 <path d="M0 300Q150 240 300 280T620 270T900 285T1200 250V340H0Z" fill="var(--hill)" opacity=".28" />
340 <path d="M0 320Q200 280 420 308T800 300T1200 296V340H0Z" fill="var(--hill)" opacity=".5" />
341 </g>
342
343 {/* soil horizons */}
344 {STRATA_PATHS.map((s) => <path key={s.y} d={s.d} fill={s.fill} />)}
345 <rect y={HZ} width="1200" height="542" fill="url(#tr-fade)" />
346 {S.specks.map((p, i) => <circle key={i} cx={f1(p.x)} cy={f1(p.y)} r={f1(p.r)} fill={p.c} opacity=".55" />)}
347 {S.worms.map((d, i) => <path key={i} d={d} fill="none" stroke="#d99a8e" strokeWidth="4" strokeLinecap="round" opacity=".7" />)}
348
349 {/* snow */}
350 <path className="tr-snow" d={`M0 ${HZ - 4}Q60 ${HZ - 12} 120 ${HZ - 4}T240 ${HZ - 4}T360 ${HZ - 4}T480 ${HZ - 4}T600 ${HZ - 4}T720 ${HZ - 4}T840 ${HZ - 4}T960 ${HZ - 4}T1080 ${HZ - 4}T1200 ${HZ - 4}V${HZ + 6}H0Z`} fill="#f7fbfc" />
351
352 {/* roots */}
353 <g fill="none" strokeLinecap="round" stroke="#efdcb4">
354 {S.roots.map((p, i) => <path key={i} className="tr-dr" pathLength={1} d={p.d} strokeWidth={p.w} style={v({ "--d": p.delay, opacity: p.o ?? 1 })} />)}
355 </g>
356
357 {/* stored carbon */}
358 <g className="tr-store">
359 {S.store.map((c, i) => <rect key={i} className="tr-fleck" x={f1(c.x)} y={f1(c.y)} width="5" height="5" style={v({ "--d": c.delay })} />)}
360 </g>
361
362 {/* plants */}
363 <g>
364 {S.stems.map((p, i) => <path key={i} className="tr-ds" pathLength={1} d={p.d} fill="none" stroke={p.w > 8 ? "#5b3d26" : p.w > 4 ? "#6b4a2e" : "var(--stem, #8b9a4a)"} strokeWidth={p.w} strokeLinecap="round" style={v({ "--d": p.delay })} />)}
365 {S.blades.map((p, i) => <path key={i} className="tr-ds" pathLength={1} d={p.d} fill="none" stroke="var(--leaf)" strokeWidth={p.w} strokeLinecap="round" style={v({ "--d": p.delay })} />)}
366 {S.leaves.map((l, i) => (
367 <ellipse key={i} className="tr-sc" cx={f1(l.x)} cy={f1(l.y)} rx={l.rx} ry={l.ry} fill={l.c === 1 ? "var(--leaf)" : "var(--leaf2)"} transform={`rotate(${l.rot})`} style={v({ "--d": l.delay, "--o": "50% 50%" })} />
368 ))}
369 {S.flowers.map((fl, i) => (
370 <g key={i} className="tr-sc" style={v({ "--d": fl.delay })}>
371 {[0, 72, 144, 216, 288].map((a) => <circle key={a} cx={f1(fl.x + Math.cos((a * Math.PI) / 180) * 5)} cy={f1(fl.y + Math.sin((a * Math.PI) / 180) * 5)} r="3.6" fill="var(--wheat)" />)}
372 <circle cx={fl.x} cy={fl.y} r="2.6" fill="#7a4a1c" />
373 </g>
374 ))}
375 {S.ears.map((e, i) => (
376 <g key={i} className="tr-sc" transform={`rotate(${f1(e.rot)})`} style={v({ "--d": e.delay })}>
377 {[0, 1, 2, 3, 4].map((k) => (
378 <g key={k}>
379 <ellipse cx={f1(e.x - 4)} cy={f1(e.y - 4 - k * 7)} rx="2.8" ry="5.6" fill="var(--wheat)" transform={`rotate(-24 ${f1(e.x - 4)} ${f1(e.y - 4 - k * 7)})`} />
380 <ellipse cx={f1(e.x + 4)} cy={f1(e.y - 7 - k * 7)} rx="2.8" ry="5.6" fill="var(--wheat)" transform={`rotate(24 ${f1(e.x + 4)} ${f1(e.y - 7 - k * 7)})`} />
381 </g>
382 ))}
383 <path d={`M${f1(e.x)} ${f1(e.y - 38)}l0 -22`} stroke="var(--wheat)" strokeWidth="1.5" />
384 </g>
385 ))}
386 </g>
387
388 {/* falling carbon */}
389 <g className="tr-fall" fill="var(--sun)">
390 {S.falling.map((c, i) => (
391 <g key={i} className="tr-co" style={v({ "--dx": `${f1(c.dx)}px`, "--dy": `${f1(c.dy)}px`, animationDuration: `${f1(c.dur)}s`, animationDelay: `${f1(c.del)}s` })}>
392 <circle cx={f1(c.x)} cy="150" r="5" fill="#fff" stroke="#2a1d14" strokeWidth="1.2" />
393 <text x={f1(c.x)} y="153" fontSize="7" textAnchor="middle" fill="#2a1d14" fontFamily="ui-monospace,monospace" fontWeight="700">C</text>
394 </g>
395 ))}
396 </g>
397
398 {/* labels */}
399 <g className="tr-lab" fontFamily="ui-monospace,SFMono-Regular,Menlo,monospace" fontSize="13">
400 {labels.map(([k, t, y]) => (
401 <g key={k}>
402 <text x="22" y={y} fill="#f3ecda" opacity=".9"><tspan fontWeight="700" fill="#3be34f">{k}</tspan> {t}</text>
403 </g>
404 ))}
405 {ticks.map((t, i) => (
406 <g key={t} opacity=".7">
407 <path d={`M1140 ${tickY[i]}h38`} stroke="#f3ecda" strokeWidth="1" />
408 <text x="1176" y={tickY[i] - 5} textAnchor="end" fill="#f3ecda">{t} cm</text>
409 </g>
410 ))}
411 </g>
412 </svg>
413 );
414}
415
416/* ------------------------------------------------------------------ */
417/* Sections */
418/* ------------------------------------------------------------------ */
419const STEPS = [
420 ["01", "Seed", "Living roots feed soil life from day one."],
421 ["02", "Shoot", "Cover crops and wheat turn sunlight into sugar."],
422 ["03", "Root", "Roots push carbon down through every horizon."],
423 ["04", "Store", "Fungi lock it into stable humus for decades."],
424];
425
426function Hero({ reduced }: { reduced: boolean }) {
427 return (
428 <header className={`tr-hero${reduced ? " is-static" : ""}`} id="top">
429 <div className="tr-stage" data-hero style={v({ "--p": reduced ? 1 : 0, "--gs": reduced ? 1 : 0, "--gr": reduced ? 1 : 0, "--gc": reduced ? 1 : 0, "--tp": 0 })}>
430 <Scene />
431 <div className="tr-hero-t">
432 <p className="tr-eyebrow">Terra / regenerative farming collective</p>
433 <h1>Grow the <br className="tr-br" /><em>ground</em> back.</h1>
434 <p className="tr-lede">We help farms rebuild living soil, and we pay for the carbon it keeps. Scroll to plant a season.</p>
435 </div>
436 <div className="tr-hint" aria-hidden="true"><span>scroll to grow</span><i /></div>
437 <div className="tr-counter" aria-live="off">
438 <small>carbon banked in this hectare</small>
439 <b><span data-counter>{reduced ? "+3.80" : "+0.00"}</span> <i>t CO₂e / yr</i></b>
440 </div>
441 <ol className="tr-steps" data-steps>
442 {STEPS.map(([n, t, d], i) => (
443 <li key={n} data-i={i}>
444 <span>{n}</span>
445 <b>{t}</b>
446 <em>{d}</em>
447 </li>
448 ))}
449 </ol>
450 </div>
451 </header>
452 );
453}
454
455function Contours() {
456 return (
457 <section className="tr-sec tr-contour" id="land" data-prog>
458 <svg className="tr-contour-svg" viewBox="0 0 1200 560" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
459 {CONTOURS.map((c, i) => (
460 <g key={i}>
461 <path className={`tr-dc${c.idx ? " idx" : ""}`} pathLength={1} d={c.d} fill="none" style={v({ "--d": c.delay })} />
462 {c.label && <text className="tr-dc-l" x={f1(c.label.x)} y={f1(c.label.y)} style={v({ "--d": c.delay + 0.2 })}>{c.label.t}</text>}
463 </g>
464 ))}
465 <path className="tr-dc fence" pathLength={1} d="M120 80L520 60L560 320L180 360Z" fill="none" style={v({ "--d": 0.85 })} />
466 </svg>
467 <div className="tr-wrap tr-contour-in">
468 <div className="tr-card">
469 <p className="tr-eyebrow dark">01 / Read the land</p>
470 <h2>Plant the water <em>before</em> the crop.</h2>
471 <p>
472 Every Terra farm begins with a contour survey. We map slope, flow and
473 infiltration, then place keylines, swales and cover so rain walks
474 slowly through the field instead of running off it.
475 </p>
476 <dl className="tr-stats">
477 <div><dt>-38%</dt><dd>runoff after two seasons</dd></div>
478 <div><dt>2.1x</dt><dd>faster water infiltration</dd></div>
479 <div><dt>0</dt><dd>days of bare soil</dd></div>
480 </dl>
481 </div>
482 </div>
483 </section>
484 );
485}
486
487const PRACTICES = [
488 { n: "Agroforestry", v: 3.1 },
489 { n: "Rotational grazing", v: 1.6 },
490 { n: "Cover crops", v: 1.2 },
491 { n: "Compost & biochar", v: 1.0 },
492 { n: "No-till", v: 0.6 },
493];
494const TREND = [4.1, 12.8, 31, 58, 96, 141, 184.2];
495
496function Ledger() {
497 const [ref, seen] = useInView<HTMLDivElement>(0.2);
498 const [tons, setTons] = useState(184206.4);
499 useEffect(() => {
500 const t0 = Date.now();
501 const id = window.setInterval(() => setTons(184206.4 + ((Date.now() - t0) / 1000) * 0.38), 100);
502 return () => window.clearInterval(id);
503 }, []);
504 const pts = TREND.map((val, i) => `${40 + i * 90},${210 - (val / 190) * 180}`);
505 const line = "M" + pts.join("L");
506 return (
507 <section className="tr-sec tr-ledger" id="carbon">
508 <div className="tr-wrap" ref={ref}>
509 <p className="tr-eyebrow">02 / The ledger</p>
510 <h2>Every tonne, <em>counted in soil.</em></h2>
511 <div className="tr-big" aria-live="off">
512 <b>{tons.toLocaleString("en-US", { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</b>
513 <span>tonnes CO₂e verified in Terra soils since 2019</span>
514 </div>
515 <div className={`tr-ledger-grid${seen ? " in" : ""}`}>
516 <div className="tr-panel">
517 <h3>Per hectare, per year</h3>
518 {PRACTICES.map((p, i) => (
519 <div className="tr-bar" key={p.n}>
520 <span>{p.n}</span>
521 <div><i style={v({ "--w": p.v / 3.2, "--i": i })} /></div>
522 <b>{p.v.toFixed(1)} t</b>
523 </div>
524 ))}
525 </div>
526 <div className="tr-panel">
527 <h3>Cumulative, thousand tonnes</h3>
528 <svg viewBox="0 0 640 240" role="img" aria-label="Carbon banked per year rising from 4 thousand tonnes in 2019 to 184 thousand in 2026">
529 {[0, 1, 2, 3].map((k) => <path key={k} d={`M40 ${30 + k * 60}H620`} stroke="rgba(243,236,218,.14)" />)}
530 <path className="tr-line" pathLength={1} d={line} fill="none" />
531 <path className="tr-area" d={`${line}L${40 + 6 * 90} 210L40 210Z`} />
532 {pts.map((p, i) => {
533 const [x, y] = p.split(",");
534 return (
535 <g key={i}>
536 <circle className="tr-dot" cx={x} cy={y} r="4.5" style={v({ "--i": i })} />
537 <text x={x} y="232" textAnchor="middle" className="tr-ax">{2019 + i}</text>
538 </g>
539 );
540 })}
541 </svg>
542 <p className="tr-note">Verified by deep-core sampling every five years. Credits retire only after verification.</p>
543 </div>
544 </div>
545 </div>
546 </section>
547 );
548}
549
550function FieldMap() {
551 const [active, setActive] = useState<string | null>(null);
552 const [pinned, setPinned] = useState<string | null>(null);
553 const cur = PLOTS.find((p) => p.id === (active ?? pinned)) ?? null;
554 const [ref, seen] = useInView<HTMLDivElement>(0.15);
555 const fillFor = (k: Plot["kind"]) =>
556 ({ wheat: "var(--wheat)", clover: "var(--leaf2)", pasture: "var(--leaf)", orchard: "#5a6b33", fallow: "#a58b63" })[k];
557 return (
558 <section className="tr-sec tr-plots" id="plots">
559 <div className="tr-wrap">
560 <p className="tr-eyebrow dark">03 / The farm map</p>
561 <h2>Twelve plots, <em>each with a story.</em></h2>
562 <div className={`tr-map-grid${seen ? " in" : ""}`} ref={ref}>
563 <div className="tr-map">
564 <svg viewBox="0 0 800 520" role="group" aria-label="Field map of Hollin Farm with twelve plots. Hover, focus or tap a plot for details.">
565 <defs>
566 {PLOTS.map((p) => <clipPath key={p.id} id={`tr-c-${p.id}`}><polygon points={p.clip} /></clipPath>)}
567 </defs>
568 <rect width="800" height="520" fill="rgba(42,29,20,.06)" />
569 {PLOTS.map((p) => {
570 const on = cur?.id === p.id;
571 const lines = Array.from({ length: 16 }, (_, k) => k);
572 return (
573 <g
574 key={p.id}
575 className={`tr-plot${on ? " on" : ""}`}
576 tabIndex={0}
577 role="button"
578 aria-label={`${p.name}, ${p.crop}`}
579 aria-pressed={pinned === p.id}
580 onMouseEnter={() => setActive(p.id)}
581 onMouseLeave={() => setActive(null)}
582 onFocus={() => setActive(p.id)}
583 onBlur={() => setActive(null)}
584 onClick={() => setPinned((s) => (s === p.id ? null : p.id))}
585 onKeyDown={(e) => {
586 if (e.key === "Enter" || e.key === " ") {
587 e.preventDefault();
588 setPinned((s) => (s === p.id ? null : p.id));
589 }
590 }}
591 >
592 <polygon points={p.poly} className="tr-plot-f" fill={fillFor(p.kind)} />
593 <g clipPath={`url(#tr-c-${p.id})`}>
594 <g transform={`rotate(${f1(p.ang)} ${f1(p.cx)} ${f1(p.cy)})`}>
595 {lines.map((k) => (
596 <path key={k} className="tr-row" pathLength={1} d={`M${f1(p.cx - 130)} ${f1(p.cy - 90 + k * 12)}H${f1(p.cx + 130)}`} style={v({ "--i": k })} />
597 ))}
598 </g>
599 </g>
600 <polygon points={p.poly} className="tr-plot-o" fill="none" />
601 <text x={f1(p.cx)} y={f1(p.cy + 4)} textAnchor="middle" className="tr-plot-t">{p.name}</text>
602 </g>
603 );
604 })}
605 <path className="tr-river" d="M-10 262C120 240 190 300 330 270S560 230 650 275 760 300 810 285" fill="none" />
606 <text x="700" y="304" className="tr-river-t" transform="rotate(8 700 304)">Alder Brook</text>
607 </svg>
608 <div className="tr-legend" aria-hidden="true">
609 <span><i style={{ background: "var(--wheat)" }} />Grain</span>
610 <span><i style={{ background: "var(--leaf2)" }} />Ley / veg</span>
611 <span><i style={{ background: "var(--leaf)" }} />Pasture</span>
612 <span><i style={{ background: "#5a6b33" }} />Trees</span>
613 <span><i style={{ background: "#a58b63" }} />Rewilding</span>
614 </div>
615 </div>
616 <aside className="tr-info" aria-live="polite">
617 {cur ? (
618 <>
619 <p className="tr-eyebrow dark">{cur.crop}</p>
620 <h3>{cur.name}</h3>
621 <p className="tr-info-p">{cur.practice}, regenerative since {cur.since}.</p>
622 <dl>
623 <div><dt>Area</dt><dd>{cur.ha} ha</dd></div>
624 <div><dt>Organic matter</dt><dd>{cur.som}%</dd></div>
625 <div><dt>Carbon</dt><dd>{cur.carbon} t/ha/yr</dd></div>
626 </dl>
627 <div className="tr-som"><i style={v({ "--w": Math.min(1, cur.som / 9) })} /></div>
628 <small>Soil organic matter vs. a 9% native-prairie benchmark.</small>
629 </>
630 ) : (
631 <>
632 <p className="tr-eyebrow dark">Hollin Farm / 128 ha</p>
633 <h3>Hover a plot.</h3>
634 <p className="tr-info-p">Crop rows grow into the field and its soil numbers appear here. Tap or press Enter to pin one.</p>
635 </>
636 )}
637 </aside>
638 </div>
639 </div>
640 </section>
641 );
642}
643
644function Mycelium() {
645 const [ref, seen] = useInView<HTMLDivElement>(0.25);
646 const signals = MYC.edges.filter((_, i) => i % 9 === 0).slice(0, 6);
647 return (
648 <section className="tr-sec tr-myc" id="soil">
649 <div className="tr-wrap tr-myc-grid" ref={ref}>
650 <div className={`tr-myc-svg${seen ? " in" : ""}`}>
651 <svg viewBox="0 0 600 400" role="img" aria-label="A procedurally drawn fungal network linking plant roots, with signals travelling along it">
652 {MYC.edges.map((e, i) => <path key={i} className="tr-hy" pathLength={1} d={e.d} fill="none" style={v({ "--d": e.delay })} />)}
653 {MYC.nodes.map((n, i) => <circle key={i} className="tr-nd" cx={f1(n.x)} cy={f1(n.y)} r={i === 0 ? 8 : 3} style={v({ "--d": i === 0 ? 0 : Math.min(0.8, (n.y - 40) / 420) })} />)}
654 {signals.map((e, i) => <circle key={i} className="tr-sig" r="3.4" style={v({ offsetPath: `path("${e.d}")`, animationDelay: `${i * 0.9}s` })} />)}
655 </svg>
656 </div>
657 <div>
658 <p className="tr-eyebrow">04 / Under one footstep</p>
659 <h2>The <em>wood-wide</em> web does the heavy lifting.</h2>
660 <p className="tr-lead">
661 Mycorrhizal fungi trade minerals for sugar and bind carbon into
662 aggregates that outlast any single crop. Tillage tears the web;
663 living cover rebuilds it. We measure both.
664 </p>
665 <div className="tr-count-row">
666 <Count to={1000} suffix="M" label="bacteria in a teaspoon" go={seen} />
667 <Count to={48} suffix=" m" label="fungal hyphae per gram" go={seen} />
668 <Count to={19} suffix="%" label="more water held per point of organic matter" go={seen} />
669 </div>
670 </div>
671 </div>
672 </section>
673 );
674}
675
676function Count({ to, suffix, label, go }: { to: number; suffix: string; label: string; go: boolean }) {
677 const [val, setVal] = useState(to);
678 const mounted = useRef(false);
679 useEffect(() => {
680 if (!mounted.current) {
681 mounted.current = true;
682 if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) setVal(0);
683 return;
684 }
685 if (!go) return;
686 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
687 setVal(to);
688 return;
689 }
690 let raf = 0;
691 const t0 = performance.now();
692 const tick = (now: number) => {
693 const p = Math.min(1, (now - t0) / 1600);
694 setVal(Math.round(to * (1 - Math.pow(1 - p, 3))));
695 if (p < 1) raf = requestAnimationFrame(tick);
696 };
697 raf = requestAnimationFrame(tick);
698 return () => cancelAnimationFrame(raf);
699 }, [go, to]);
700 return (
701 <div className="tr-count">
702 <b>{val.toLocaleString("en-US")}{suffix}</b>
703 <span>{label}</span>
704 </div>
705 );
706}
707
708const VOICES = [
709 { q: "We stopped fighting the field and started feeding it. Year four, the wheat stands in a drought and the earthworms are back in the headlands.", n: "Maren Holt", r: "Long Acre, 62 ha, Lower Saxony" },
710 { q: "Terra paid for the hedgerows before the first harvest from them. That is the only reason my bank said yes.", n: "Joaquin Ferrer", r: "Vall Seca orchards, Catalonia" },
711 { q: "The map showed me my worst corner was my best carbon. I would never have guessed. Now I plan around it.", n: "Aoife Brennan", r: "Mill Pasture, 40 ha, County Meath" },
712];
713
714function Voices() {
715 const [ref, seen] = useInView<HTMLDivElement>(0.2);
716 return (
717 <section className="tr-sec tr-voices" id="growers">
718 <div className="tr-wrap" ref={ref}>
719 <p className="tr-eyebrow dark">05 / Growers</p>
720 <h2>Farmers who <em>left the soil better.</em></h2>
721 <div className={`tr-vgrid${seen ? " in" : ""}`}>
722 {VOICES.map((x, i) => (
723 <figure key={x.n} className="tr-voice" style={v({ "--i": i })}>
724 <svg viewBox="0 0 40 40" width="34" height="34" aria-hidden="true"><path d="M20 38V16" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" /><path d="M20 22C8 22 6 12 6 6c8 0 14 4 14 16ZM20 18c0-9 5-12 14-12 0 7-4 12-14 12Z" fill="var(--leaf)" /></svg>
725 <blockquote>{x.q}</blockquote>
726 <figcaption><b>{x.n}</b><span>{x.r}</span></figcaption>
727 </figure>
728 ))}
729 </div>
730 </div>
731 </section>
732 );
733}
734
735function Field() {
736 const stalks = useMemo(() => {
737 const r = rng(77);
738 return Array.from({ length: 56 }, (_, i) => ({ x: i * 22 + r() * 12, h: 60 + r() * 90, d: r() * 0.4, s: 3 + r() * 3, del: -r() * 5 }));
739 }, []);
740 return (
741 <svg className="tr-field" viewBox="0 0 1240 180" preserveAspectRatio="none" aria-hidden="true">
742 {stalks.map((s, i) => (
743 <g key={i} className="tr-fs" style={v({ "--d": s.d, animationDuration: `${s.s}s`, animationDelay: `${s.del}s`, transformOrigin: `${f1(s.x)}px 180px` })}>
744 <path d={`M${f1(s.x)} 180Q${f1(s.x + 4)} ${180 - s.h / 2} ${f1(s.x + 2)} ${180 - s.h}`} stroke="var(--wheat)" strokeWidth="2.5" fill="none" />
745 <ellipse cx={f1(s.x + 2)} cy={180 - s.h - 8} rx="3.4" ry="9" fill="var(--wheat)" />
746 <path d={`M${f1(s.x)} ${180 - s.h * 0.3}q-14 -10 -20 -26`} stroke="var(--leaf)" strokeWidth="2.5" fill="none" />
747 </g>
748 ))}
749 </svg>
750 );
751}
752
753function Join() {
754 const [done, setDone] = useState(false);
755 const [ha, setHa] = useState("50-200");
756 const [mail, setMail] = useState("");
757 const [err, setErr] = useState("");
758 return (
759 <section className="tr-sec tr-join" id="join" data-prog>
760 <div className="tr-wrap tr-join-in">
761 <p className="tr-eyebrow">06 / Begin</p>
762 <h2>Start with a <em>soil test.</em></h2>
763 <p className="tr-lead">We core, map and model your land for free. If the numbers work, we co-fund the first three years of transition.</p>
764 {done ? (
765 <div className="tr-done" role="status"><b>Seed planted.</b> A Terra agronomist will write to {mail} within two working days.</div>
766 ) : (
767 <form
768 className="tr-form"
769 noValidate
770 onSubmit={(e) => {
771 e.preventDefault();
772 if (!/.+@.+\..+/.test(mail)) return setErr("Please enter a valid e-mail.");
773 setErr("");
774 setDone(true);
775 }}
776 >
777 <label>Farm size
778 <select value={ha} onChange={(e) => setHa(e.target.value)}>
779 {["under 20", "20-50", "50-200", "200-1000", "over 1000"].map((x) => <option key={x} value={x}>{x} ha</option>)}
780 </select>
781 </label>
782 <label>E-mail
783 <input type="email" value={mail} onChange={(e) => setMail(e.target.value)} placeholder="you@farm.example" autoComplete="email" />
784 </label>
785 <button type="submit">Request a soil test</button>
786 {err && <p className="tr-err" role="alert">{err}</p>}
787 </form>
788 )}
789 </div>
790 <Field />
791 <footer className="tr-foot">
792 <span>Terra Regenerative Collective, 2026</span>
793 <span>Soil is the largest living carbon store we can rebuild.</span>
794 </footer>
795 </section>
796 );
797}
798
799/* ------------------------------------------------------------------ */
800/* Page */
801/* ------------------------------------------------------------------ */
802function Nav({ children }: { children?: ReactNode }) {
803 return (
804 <nav className="tr-nav" aria-label="Main">
805 <a href="#top" className="tr-logo">
806 <svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M12 22V9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /><path d="M12 14C5 14 3 8 3 3c6 0 9 3 9 11ZM12 11c0-6 3-8 9-8 0 5-3 8-9 8Z" fill="#3be34f" stroke="currentColor" strokeWidth="1.2" /></svg>
807 Terra
808 </a>
809 <div className="tr-links">
810 <a href="#land">Land</a><a href="#carbon">Carbon</a><a href="#plots">Plots</a><a href="#soil">Soil</a><a href="#growers">Growers</a>
811 </div>
812 <a className="tr-cta" href="#join">Soil test</a>
813 {children}
814 </nav>
815 );
816}
817
818export function Page() {
819 const root = useRef<HTMLDivElement | null>(null);
820 const [season, setSeason] = useState(0.35);
821 const [reduced, setReduced] = useState(false);
822
823 useEffect(() => {
824 if (root.current) applySeason(root.current, season);
825 }, [season]);
826
827 useEffect(() => {
828 const el = root.current;
829 if (!el) return;
830 const rm = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
831 setReduced(rm);
832 if (rm) return;
833 const stage = el.querySelector<HTMLElement>("[data-hero]");
834 const track = stage?.parentElement ?? null;
835 const counter = el.querySelector<HTMLElement>("[data-counter]");
836 const steps = el.querySelector<HTMLElement>("[data-steps]");
837 const progs = Array.from(el.querySelectorAll<HTMLElement>("[data-prog]"));
838 const clamp = (x: number) => Math.max(0, Math.min(1, x));
839 let raf = 0;
840 let lastP = -1;
841 let lastStep = -1;
842 const update = () => {
843 raf = 0;
844 const vh = window.innerHeight;
845 if (stage && track) {
846 const rect = track.getBoundingClientRect();
847 const p = clamp(-rect.top / Math.max(1, rect.height - vh));
848 if (Math.abs(p - lastP) > 0.0004) {
849 lastP = p;
850 const gs = clamp(p / 0.42);
851 const gr = clamp((p - 0.28) / 0.5);
852 const gc = clamp((p - 0.5) / 0.45);
853 stage.style.setProperty("--p", p.toFixed(4));
854 stage.style.setProperty("--gs", gs.toFixed(4));
855 stage.style.setProperty("--gr", gr.toFixed(4));
856 stage.style.setProperty("--gc", gc.toFixed(4));
857 if (counter) counter.textContent = "+" + (gc * 3.8).toFixed(2);
858 const step = p < 0.1 ? 0 : p < 0.38 ? 1 : p < 0.7 ? 2 : 3;
859 if (step !== lastStep && steps) {
860 lastStep = step;
861 steps.dataset.step = String(step);
862 }
863 }
864 }
865 progs.forEach((s) => {
866 const r = s.getBoundingClientRect();
867 if (r.bottom < -100 || r.top > vh + 100) return;
868 s.style.setProperty("--sp", clamp((vh - r.top) / (vh * 0.85 + r.height * 0.5)).toFixed(4));
869 });
870 };
871 const onScroll = () => {
872 if (!raf) raf = requestAnimationFrame(update);
873 };
874 update();
875 window.addEventListener("scroll", onScroll, { passive: true });
876 window.addEventListener("resize", onScroll);
877 return () => {
878 window.removeEventListener("scroll", onScroll);
879 window.removeEventListener("resize", onScroll);
880 if (raf) cancelAnimationFrame(raf);
881 };
882 }, []);
883
884 const sIdx = Math.min(3, Math.round(season));
885 return (
886 <div className={`tr${reduced ? " reduced" : ""}`} ref={root}>
887 <style>{CSS}</style>
888 <Nav />
889 <Hero reduced={reduced} />
890 <div className="tr-edge" aria-hidden="true">
891 <svg viewBox="0 0 1200 60" preserveAspectRatio="none"><path d="M0 0H1200V30Q1100 55 1000 34T800 34T600 26T400 38T200 26T0 36Z" fill="#2b1c12" /></svg>
892 </div>
893 <main>
894 <Contours />
895 <Ledger />
896 <FieldMap />
897 <Mycelium />
898 <Voices />
899 <Join />
900 </main>
901 <div className="tr-season" role="group" aria-label="Season">
902 <span className="tr-season-n">{SEASONS[sIdx]}</span>
903 <input
904 type="range"
905 min={0}
906 max={3}
907 step={0.01}
908 value={season}
909 aria-label="Season, spring to winter"
910 aria-valuetext={SEASONS[sIdx]}
911 onChange={(e) => setSeason(Number(e.target.value))}
912 />
913 <div className="tr-season-t" aria-hidden="true">{SEASONS.map((s) => <span key={s}>{s.slice(0, 3)}</span>)}</div>
914 </div>
915 </div>
916 );
917}
918
919export default Page;
920
921/* ------------------------------------------------------------------ */
922/* Styles */
923/* ------------------------------------------------------------------ */
924const CSS = `
925.tr{--soil:#2b1c12;--soil2:#5b3d26;--moss:#5a6b33;--ink:#2a1d14;--cream:#f3ecda;--chl:#3be34f;
926 --sky1:#cfe8d8;--sky2:#f6f0d6;--leaf:#6fae3b;--leaf2:#9ccf4a;--wheat:#b8c46a;--bg:#f3ecda;--sun:#ffe08a;--bloom:.9;--snow:0;--sy:0px;--hill:#7fae5a;
927 --serif:"Fraunces","Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif;
928 --sans:"Instrument Sans",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
929 --mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
930 box-sizing:border-box;position:relative;font-family:var(--sans);font-size:16px;line-height:1.55;color:var(--ink);background:var(--bg);transition:background-color .3s;overflow-x:clip}
931.tr *,.tr *::before,.tr *::after{box-sizing:border-box}
932.tr h1,.tr h2,.tr h3{font-family:var(--serif);font-weight:500;line-height:1.02;letter-spacing:-.02em;margin:0}
933.tr h2{font-size:clamp(34px,5.4vw,70px);margin-bottom:22px}
934.tr h3{font-size:22px;letter-spacing:-.01em}
935.tr em{font-style:italic;color:var(--moss)}
936.tr p{margin:0 0 1em}
937.tr a{color:inherit}
938.tr-wrap{max-width:1160px;margin:0 auto;padding:0 28px;position:relative}
939.tr-eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.14em;text-transform:uppercase;margin:0 0 16px;color:var(--cream);opacity:.85}
940.tr-eyebrow.dark{color:var(--moss);opacity:1}
941.tr-sec{position:relative;padding:120px 0}
942.tr-lead{font-size:19px;max-width:52ch}
943
944/* nav */
945.tr-nav{position:fixed;top:0;left:0;right:0;z-index:40;display:flex;align-items:center;gap:24px;padding:14px 28px;color:var(--cream);mix-blend-mode:normal}
946.tr .tr-logo{display:flex;align-items:center;gap:8px;font-family:var(--serif);font-size:24px;text-decoration:none;font-weight:600;color:var(--ink);background:rgba(243,236,218,.85);padding:4px 12px 4px 8px;border-radius:99px}
947.tr-links{display:flex;gap:4px;margin-left:auto;background:rgba(243,236,218,.85);padding:4px;border-radius:99px;color:var(--ink)}
948.tr-links a{padding:6px 12px;border-radius:99px;font-size:14px;text-decoration:none}
949.tr-links a:hover{background:var(--ink);color:var(--cream)}
950.tr-cta{background:var(--chl);color:#0c2a10!important;padding:9px 16px;border-radius:99px;font-weight:700;font-size:14px;text-decoration:none}
951.tr a:focus-visible,.tr button:focus-visible,.tr select:focus-visible,.tr input:focus-visible,.tr .tr-plot:focus-visible{outline:3px solid var(--chl);outline-offset:3px}
952
953/* hero */
954.tr-hero{position:relative;height:480vh;background:var(--soil)}
955.tr-hero.is-static{height:100vh;min-height:680px}
956.tr-stage{position:sticky;top:0;height:100vh;min-height:620px;overflow:hidden;background:var(--soil)}
957.tr-scene{position:absolute;inset:0;width:100%;height:100%;display:block}
958.tr-sun{transform:translateY(var(--sy));transition:transform .2s}
959.tr-halo{opacity:.25}
960.tr-cl{animation:tr-cloud 60s linear infinite alternate}
961.tr-cl.b{animation-duration:80s}.tr-cl.c{animation-duration:50s}
962@keyframes tr-cloud{from{transform:translateX(-60px)}to{transform:translateX(80px)}}
963.tr-snow{opacity:var(--snow)}
964.tr-ds,.tr-dr{--q:clamp(0,calc((var(--gs) * 1.4 - var(--d,0)) * 3),1);stroke-dasharray:1;stroke-dashoffset:calc(1 - var(--q))}
965.tr-dr{--q:clamp(0,calc((var(--gr) * 1.4 - var(--d,0)) * 3),1)}
966.tr-ds,.tr-dr{opacity:clamp(0,calc(var(--q) * 60),1)}
967.tr-dr{opacity:clamp(0,calc(var(--q) * 60),1)}
968.tr-sc{--q:clamp(0,calc((var(--gs) * 1.4 - var(--d,0)) * 3),1);transform-box:fill-box;transform-origin:50% 100%}
969ellipse.tr-sc{transform-origin:50% 50%}
970.tr-sc{opacity:clamp(0,calc(var(--q) * 60),1);scale:calc(var(--q) * var(--bloom,1))}
971.tr-fleck{--q:clamp(0,calc((var(--gc) * 1.5 - var(--d,0)) * 3),1);fill:var(--chl);opacity:clamp(0,calc(var(--q) * .95),.95)}
972.tr-fall{opacity:clamp(0,calc(var(--p) * 6 - .1),.95)}
973.tr-co{animation:tr-fall 9s linear infinite}
974@keyframes tr-fall{0%{transform:translate(0,0);opacity:0}10%{opacity:1}60%{opacity:1}100%{transform:translate(var(--dx),var(--dy));opacity:0}}
975.tr-lab{opacity:clamp(0,calc(var(--gr) * 3 - .3),1)}
976.tr-hero-t{position:absolute;left:0;right:0;top:10vh;text-align:center;padding:0 24px;opacity:clamp(0,calc(1 - var(--p) * 5.5),1);transform:translateY(calc(var(--p) * -90px));pointer-events:none;color:var(--ink)}
977.tr-hero.is-static .tr-hero-t{opacity:1;transform:none}
978.tr-hero-t .tr-eyebrow{color:var(--ink);opacity:.75}
979.tr-br{display:none}
980.tr-hero h1{font-size:clamp(46px,7.8vw,118px);white-space:nowrap;font-weight:400;line-height:.92;letter-spacing:-.035em}
981.tr-hero h1 em{color:#2f6b1b;font-weight:300}
982.tr .tr-lede{font-size:clamp(16px,1.7vw,21px);max-width:44ch;margin:20px auto 0}
983.tr-hint{position:absolute;left:50%;bottom:92px;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:6px;font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--cream);opacity:clamp(0,calc(1 - var(--p) * 14),1)}
984.tr-hint i{width:2px;height:34px;background:var(--chl);animation:tr-hint 1.6s ease-in-out infinite;transform-origin:top}
985@keyframes tr-hint{0%{transform:scaleY(0)}60%{transform:scaleY(1)}100%{transform:scaleY(1);opacity:0}}
986.tr-counter{position:absolute;left:28px;bottom:96px;background:rgba(27,16,9,.84);padding:14px 20px 12px;border-radius:4px;color:var(--cream);opacity:clamp(0,calc(var(--p) * 5),1);pointer-events:none}
987.tr-counter small{display:block;font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;opacity:.8}
988.tr-counter b{font-family:var(--serif);font-weight:400;font-size:clamp(40px,6vw,76px);line-height:1;color:var(--chl);font-variant-numeric:tabular-nums}
989.tr-counter i{font-family:var(--mono);font-style:normal;font-size:13px;color:var(--cream)}
990.tr-steps{position:absolute;right:28px;bottom:96px;list-style:none;margin:0;padding:16px 20px;width:min(320px,42vw);height:122px;background:rgba(27,16,9,.84);border-radius:4px;color:var(--cream);opacity:clamp(0,calc(var(--p) * 8),1)}
991.tr-steps li{position:absolute;inset:16px 20px;opacity:0;transform:translateY(14px);transition:opacity .4s,transform .4s;display:flex;flex-direction:column;gap:2px}
992.tr-steps li span{font-family:var(--mono);font-size:12px;color:var(--chl)}
993.tr-steps li b{font-family:var(--serif);font-weight:500;font-size:34px;line-height:1}
994.tr-steps li em{font-style:normal;font-size:14px;color:var(--cream);opacity:.85}
995.tr-steps[data-step="0"] li[data-i="0"],.tr-steps[data-step="1"] li[data-i="1"],.tr-steps[data-step="2"] li[data-i="2"],.tr-steps[data-step="3"] li[data-i="3"]{opacity:1;transform:none}
996.tr-hero.is-static .tr-steps li[data-i="3"]{opacity:1;transform:none}
997.tr-hero.is-static .tr-hint{display:none}
998.tr-edge{height:48px;margin-top:-1px;background:var(--bg);line-height:0;transition:background-color .3s}
999.tr-edge svg{width:100%;height:48px;display:block}
1000
1001/* contours */
1002.tr-contour{overflow:hidden;--sp:0;padding:140px 0 120px}
1003.tr-contour-svg{position:absolute;inset:0;width:100%;height:100%}
1004.tr-dc{--q:clamp(0,calc((var(--sp) * 1.5 - var(--d,0)) * 3),1);stroke:var(--moss);stroke-width:1.2;opacity:.55;stroke-dasharray:1;stroke-dashoffset:calc(1 - var(--q))}
1005.tr-dc.idx{stroke-width:2.4;opacity:.9}
1006.tr-dc.fence{stroke:var(--chl);stroke-width:3;stroke-dasharray:1;opacity:1;filter:drop-shadow(0 0 4px rgba(59,227,79,.5))}
1007.tr-dc-l{--q:clamp(0,calc((var(--sp) * 1.5 - var(--d,0)) * 3),1);font-family:var(--mono);font-size:11px;fill:var(--ink);opacity:var(--q)}
1008.reduced .tr-contour,.reduced .tr-join{--sp:1}
1009.tr-contour-in{display:flex;justify-content:flex-end}
1010.tr-card{width:min(540px,100%);background:rgba(243,236,218,.92);padding:40px;border:1.5px solid var(--ink);border-radius:4px;box-shadow:10px 10px 0 var(--soil2)}
1011.tr-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:26px 0 0;border-top:1.5px solid var(--ink);padding-top:18px}
1012.tr-stats div{display:flex;flex-direction:column-reverse}
1013.tr-stats dt{font-family:var(--serif);font-size:38px;line-height:1;font-weight:500}
1014.tr-stats dd{margin:0;font-size:12.5px;line-height:1.3;opacity:.8}
1015
1016/* ledger */
1017.tr-ledger{background:var(--soil);color:var(--cream)}
1018.tr-ledger em{color:var(--chl)}
1019.tr-big{display:flex;flex-direction:column;gap:4px;margin:0 0 56px}
1020.tr-big b{font-family:var(--serif);font-weight:400;font-size:clamp(46px,9.5vw,132px);line-height:1;letter-spacing:-.03em;color:var(--chl);font-variant-numeric:tabular-nums;text-shadow:0 0 40px rgba(59,227,79,.25)}
1021.tr-big span{font-family:var(--mono);font-size:13px;letter-spacing:.1em;text-transform:uppercase;opacity:.8}
1022.tr-ledger-grid{display:grid;grid-template-columns:1fr 1.15fr;gap:24px}
1023.tr-panel{border:1px solid rgba(243,236,218,.25);padding:28px;border-radius:4px;background:rgba(243,236,218,.04)}
1024.tr-panel h3{margin-bottom:20px;font-size:20px}
1025.tr-bar{display:grid;grid-template-columns:130px 1fr 52px;gap:12px;align-items:center;margin:0 0 14px;font-size:14px}
1026.tr-bar div{height:14px;background:rgba(243,236,218,.1);overflow:hidden}
1027.tr-bar i{display:block;height:100%;background:var(--chl);transform-origin:left;transform:scaleX(0);transition:transform 1.2s cubic-bezier(.2,.7,.2,1);transition-delay:calc(var(--i) * .12s)}
1028.in .tr-bar i{transform:scaleX(var(--w))}
1029.tr-bar b{font-family:var(--mono);font-weight:500;text-align:right;font-size:13px}
1030.tr-ledger svg{width:100%;height:auto;display:block}
1031.tr-line{stroke:var(--chl);stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:1;stroke-dashoffset:1;transition:stroke-dashoffset 2s ease .2s}
1032.in .tr-line{stroke-dashoffset:0}
1033.tr-area{fill:rgba(59,227,79,.12);opacity:0;transition:opacity 1s 1.6s}
1034.in .tr-area{opacity:1}
1035.tr-dot{fill:var(--soil);stroke:var(--chl);stroke-width:2.5;opacity:0;transition:opacity .3s calc(.3s + var(--i) * .26s)}
1036.in .tr-dot{opacity:1}
1037.tr-ax{font-family:var(--mono);font-size:11px;fill:var(--cream);opacity:.7}
1038.tr-note{font-size:13px;opacity:.7;margin:14px 0 0}
1039
1040/* plots */
1041.tr-plots{background:var(--bg);transition:background-color .3s}
1042.tr-map-grid{display:grid;grid-template-columns:1.7fr 1fr;gap:28px;align-items:start}
1043.tr-map{border:1.5px solid var(--ink);background:rgba(255,255,255,.35);padding:12px;border-radius:4px}
1044.tr-map svg{width:100%;height:auto;display:block}
1045.tr-plot{cursor:pointer;outline:none}
1046.tr-plot-f{opacity:.38;transition:opacity .3s}
1047.tr-plot-o{stroke:var(--ink);stroke-width:1.5;stroke-linejoin:round}
1048.tr-plot.on .tr-plot-f{opacity:.85}
1049.tr-plot.on .tr-plot-o{stroke-width:3;stroke:var(--ink)}
1050.tr-row{stroke:#2a1d14;stroke-width:1.6;opacity:.7;stroke-dasharray:1;stroke-dashoffset:1;transition:stroke-dashoffset .8s ease;transition-delay:calc(var(--i) * 30ms)}
1051.tr-plot.on .tr-row{stroke-dashoffset:0}
1052.tr-plot-t{font-family:var(--mono);font-size:11px;font-weight:700;fill:var(--ink);paint-order:stroke;stroke:rgba(243,236,218,.85);stroke-width:3px;pointer-events:none}
1053.tr-river{stroke:#7fb8c4;stroke-width:12;stroke-linecap:round;opacity:.85;pointer-events:none}
1054.tr-river-t{font-family:var(--serif);font-style:italic;font-size:12px;fill:#2c5a66;pointer-events:none}
1055.tr-legend{display:flex;flex-wrap:wrap;gap:6px 16px;font-family:var(--mono);font-size:11px;padding:10px 4px 2px}
1056.tr-legend span{display:flex;align-items:center;gap:6px}.tr-legend i{width:12px;height:12px;border:1px solid var(--ink)}
1057.tr-info{position:sticky;top:90px;border:1.5px solid var(--ink);padding:28px;background:var(--cream);border-radius:4px;box-shadow:8px 8px 0 var(--soil2);min-height:300px}
1058.tr-info h3{font-size:36px;margin-bottom:10px}
1059.tr-info-p{font-size:15px}
1060.tr-info dl{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:18px 0;border-top:1.5px solid var(--ink);padding-top:14px}
1061.tr-info dl div{display:flex;flex-direction:column-reverse}
1062.tr-info dt{font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;opacity:.7}
1063.tr-info dd{margin:0;font-family:var(--serif);font-size:24px;line-height:1.1}
1064.tr-som{height:10px;background:rgba(42,29,20,.12);overflow:hidden}
1065.tr-som i{display:block;height:100%;background:var(--chl);transform-origin:left;transform:scaleX(var(--w));transition:transform .6s cubic-bezier(.2,.7,.2,1)}
1066.tr-info small{display:block;margin-top:8px;font-size:12px;opacity:.7}
1067
1068/* mycelium */
1069.tr-myc{background:#1f150d;color:var(--cream)}
1070.tr-myc em{color:var(--chl)}
1071.tr-myc-grid{display:grid;grid-template-columns:1.1fr 1fr;gap:56px;align-items:center}
1072.tr-myc-svg svg{width:100%;height:auto;display:block}
1073.tr-hy{stroke:rgba(243,236,218,.7);stroke-width:1.2;stroke-dasharray:1;stroke-dashoffset:1;transition:stroke-dashoffset 1.4s ease;transition-delay:calc(var(--d) * 2.2s)}
1074.in .tr-hy{stroke-dashoffset:0}
1075.tr-nd{fill:var(--chl);opacity:0;transition:opacity .4s calc(var(--d) * 2.2s + .8s)}
1076.in .tr-nd{opacity:1}
1077.tr-sig{fill:#fff;offset-distance:0%;opacity:0;filter:drop-shadow(0 0 4px var(--chl))}
1078.in .tr-sig{animation:tr-sig 3.4s linear 2.6s infinite}
1079@keyframes tr-sig{0%{offset-distance:0%;opacity:0}10%{opacity:1}90%{opacity:1}100%{offset-distance:100%;opacity:0}}
1080.tr-count-row{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:34px;border-top:1px solid rgba(243,236,218,.3);padding-top:22px}
1081.tr-count b{display:block;font-family:var(--serif);font-weight:400;font-size:clamp(28px,3.4vw,44px);color:var(--chl);line-height:1;font-variant-numeric:tabular-nums}
1082.tr-count span{font-size:12.5px;line-height:1.3;display:block;margin-top:6px;opacity:.8}
1083
1084/* voices */
1085.tr-voices{background:var(--bg);transition:background-color .3s;padding-bottom:140px}
1086.tr-vgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
1087.tr-voice{margin:0;padding:30px;background:var(--cream);border:1.5px solid var(--ink);border-radius:4px;display:flex;flex-direction:column;gap:16px;clip-path:inset(100% 0 0 0);transition:clip-path 1s cubic-bezier(.2,.7,.2,1);transition-delay:calc(var(--i) * .18s);color:var(--ink)}
1088.in .tr-voice{clip-path:inset(0 0 0 0)}
1089.tr-voice:nth-child(2){margin-top:32px}
1090.tr-voice blockquote{margin:0;font-family:var(--serif);font-size:21px;line-height:1.3;flex:1}
1091.tr-voice figcaption{display:flex;flex-direction:column;font-size:13px}
1092.tr-voice figcaption span{opacity:.7}
1093
1094/* join */
1095.tr-join{background:var(--soil);color:var(--cream);overflow:hidden;--sp:0;padding-bottom:0}
1096.tr-join em{color:var(--chl)}
1097.tr-join-in{padding-bottom:120px;z-index:2}
1098.tr-form{display:grid;grid-template-columns:200px 1fr auto;gap:12px;align-items:end;max-width:760px;margin-top:28px}
1099.tr-form label{display:flex;flex-direction:column;gap:6px;font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase}
1100.tr-form input,.tr-form select{font:inherit;font-family:var(--sans);font-size:16px;letter-spacing:0;text-transform:none;padding:13px 14px;border:1.5px solid var(--cream);background:transparent;color:var(--cream);border-radius:4px;width:100%}
1101.tr-form select option{color:var(--ink)}
1102.tr-form input::placeholder{color:rgba(243,236,218,.5)}
1103.tr-form button{font:inherit;font-weight:700;padding:14px 22px;background:var(--chl);color:#0c2a10;border:0;border-radius:4px;cursor:pointer;transition:transform .15s}
1104.tr-form button:hover{transform:translateY(-2px)}
1105.tr-err{grid-column:1/-1;color:#ff9d8a;margin:0;font-size:14px}
1106.tr-done{margin-top:28px;padding:20px;border:1.5px solid var(--chl);max-width:560px;border-radius:4px}
1107.tr-field{position:absolute;left:0;right:0;bottom:46px;width:100%;height:180px;transform-origin:50% 100%;transform:scaleY(clamp(0,calc(var(--sp) * 1.3 - .2),1));opacity:clamp(0,calc(var(--sp) * 4 - .5),1)}
1108.tr-fs{animation:tr-sway 4s ease-in-out infinite alternate}
1109@keyframes tr-sway{from{transform:rotate(-2.5deg)}to{transform:rotate(2.5deg)}}
1110.tr-foot{position:relative;z-index:2;display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;padding:14px 28px;font-family:var(--mono);font-size:12px;background:#1a110a;margin-top:140px;color:var(--cream)}
1111.tr-join{padding-bottom:0}
1112.tr-join .tr-field{bottom:46px}
1113
1114/* season slider */
1115.tr-season{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:60;width:min(420px,calc(100vw - 32px));background:rgba(243,236,218,.94);border:1.5px solid var(--ink);border-radius:14px;padding:8px 16px 8px;box-shadow:5px 5px 0 var(--soil2);display:grid;grid-template-columns:auto 1fr;gap:2px 14px;align-items:center}
1116.tr-season-n{font-family:var(--serif);font-size:20px;min-width:76px;color:var(--ink)}
1117.tr-season input{width:100%;accent-color:#1fa33a;margin:0;height:24px}
1118.tr-season-t{grid-column:2;display:flex;justify-content:space-between;font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink);opacity:.7}
1119
1120@media (max-width:960px){
1121 .tr-ledger-grid,.tr-map-grid,.tr-myc-grid{grid-template-columns:1fr}
1122 .tr-info{position:static;min-height:0}
1123 .tr-vgrid{grid-template-columns:1fr}.tr-voice:nth-child(2){margin-top:0}
1124 .tr-links{display:none}.tr-nav{padding:12px 16px}.tr-cta{margin-left:auto}
1125 .tr-sec{padding:84px 0}
1126}
1127@media (max-width:900px){.tr-br{display:inline}.tr-hero h1{font-size:clamp(46px,15vw,92px);white-space:normal}}
1128@media (max-width:640px){
1129 .tr-wrap{padding:0 16px}
1130 .tr-lab{display:none}
1131 .tr-counter{left:16px;bottom:100px;padding:10px 14px}
1132 .tr-steps{right:16px;left:16px;width:auto;bottom:206px;height:84px}
1133 .tr-steps li b{font-size:24px}
1134 .tr-steps li em{font-size:13px}
1135 .tr-hero-t{top:11vh}
1136 .tr-card{padding:26px}
1137 .tr-stats{grid-template-columns:1fr 1fr 1fr}.tr-stats dt{font-size:28px}
1138 .tr-bar{grid-template-columns:96px 1fr 44px;font-size:12.5px}
1139 .tr-panel{padding:18px}
1140 .tr-count-row{grid-template-columns:1fr}
1141 .tr-form{grid-template-columns:1fr}
1142 .tr-info h3{font-size:30px}
1143 .tr-foot{margin-top:100px}
1144}
1145@media (prefers-reduced-motion:reduce){
1146 .tr *,.tr *::before,.tr *::after{animation:none!important;transition:none!important}
1147 .tr-hint{display:none}
1148 .tr-voice{clip-path:none}
1149 .tr-bar i{transform:scaleX(var(--w))}
1150 .tr-line{stroke-dashoffset:0}.tr-area,.tr-dot,.tr-nd{opacity:1}
1151 .tr-hy{stroke-dashoffset:0}
1152 .tr-row{stroke-dashoffset:0}
1153 .tr-field{--sp:1}
1154}
1155`;