aayu-outdooragi commited on
Commit
7cc4742
Β·
verified Β·
1 Parent(s): fe161ad

Deploy mission-control diorama preview with loop+fullscreen autoplay

Browse files
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/flight.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ assets/scene_uc1.webp filter=lfs diff=lfs merge=lfs -text
38
+ assets/scene_uc_eng.webp filter=lfs diff=lfs merge=lfs -text
39
+ assets/scene_uc_gtm.webp filter=lfs diff=lfs merge=lfs -text
40
+ assets/scene_uc_vendor.webp filter=lfs diff=lfs merge=lfs -text
assets/flight.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:90a10c3d148611efbc849af3236d91461a84e42e880f99136d0cde2f0d53d7c0
3
+ size 67489757
assets/scene_uc1.webp ADDED

Git LFS Details

  • SHA256: 73c1c3f068f8f53225d1e799c5bf20f103cc8c8ea90958f6beaf4eb607efe68e
  • Pointer size: 131 Bytes
  • Size of remote file: 161 kB
assets/scene_uc_eng.webp ADDED

Git LFS Details

  • SHA256: bc6bb9e054002644730416d23f072065be20a4f066a30af8851e3eb2d9d7b37e
  • Pointer size: 131 Bytes
  • Size of remote file: 289 kB
assets/scene_uc_gtm.webp ADDED

Git LFS Details

  • SHA256: 7cd06da734019a1f15d16553c983e9bb58a0fac132a7f5bdf30e77f451267fdd
  • Pointer size: 131 Bytes
  • Size of remote file: 209 kB
assets/scene_uc_vendor.webp ADDED

Git LFS Details

  • SHA256: c71333ba00b5030d08b3097cae2eb8d7c2aed8aa8e6980029266aae949e1dfd6
  • Pointer size: 131 Bytes
  • Size of remote file: 180 kB
flight-timeline.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Generated by tools/assemble_rotation.py β€” segment map of assets/flight.mp4
2
+ window.FLIGHT_TIMELINE = [
3
+ {
4
+ "name": "uc1",
5
+ "start": 0.0,
6
+ "end": 10.062
7
+ },
8
+ {
9
+ "name": "uc_vendor",
10
+ "start": 9.062,
11
+ "end": 19.125
12
+ },
13
+ {
14
+ "name": "uc_eng",
15
+ "start": 18.125,
16
+ "end": 28.188
17
+ },
18
+ {
19
+ "name": "uc_gtm",
20
+ "start": 27.188,
21
+ "end": 37.25
22
+ }
23
+ ];
index.html CHANGED
@@ -1,19 +1,186 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ContextCrux β€” mission control, four use cases</title>
7
+ <meta name="description" content="Preview: the ContextCrux mission-control diorama β€” a real rotation flight through four use cases (PM, Founder vendor risk, Engineer, Go-to-Market)." />
8
+ <link rel="preload" as="image" href="assets/scene_uc1.webp" />
9
+ <link rel="stylesheet" href="styles.css" />
10
+ <style>
11
+ :root {
12
+ --bg: #0c0a06; --bg-soft: #17130d; --ink: #f4ede1; --ink-dim: #b3a690;
13
+ --green: #57c2a8; --green-deep: #0c8074; --amber: #d69a4a; --red: #e2685a;
14
+ --line: rgba(214, 154, 74, 0.22); --card-bg: rgba(13, 10, 6, 0.74);
15
+ }
16
+ </style>
17
+ </head>
18
+ <body>
19
+
20
+ <header class="nav">
21
+ <a class="nav__brand" href="#">
22
+ <svg class="nav__glyph" viewBox="0 0 24 24" fill="none" aria-hidden="true">
23
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.4" />
24
+ <circle cx="12" cy="12" r="6.5" stroke="currentColor" stroke-width="1.1" stroke-dasharray="2 2" />
25
+ <circle cx="12" cy="12" r="2.2" fill="currentColor" />
26
+ </svg>
27
+ <span>ContextCrux</span>
28
+ <span class="nav__tag">Mission Control β€” preview</span>
29
+ </a>
30
+ <nav class="nav__links">
31
+ <a href="#world-vendor">Vendor risk</a>
32
+ <a href="#world-eng">Engineer</a>
33
+ <a href="#world-gtm">Go-to-market</a>
34
+ </nav>
35
+ <a class="btn btn--primary nav__cta" href="#cta">Get ContextCrux</a>
36
+ </header>
37
+
38
+ <aside class="rail" aria-hidden="true">
39
+ <div class="rail__track"><div class="rail__fill" id="railFill"></div></div>
40
+ <ul class="rail__dots" id="railDots">
41
+ <li data-scene="0"><i></i><span>PM</span></li>
42
+ <li data-scene="1"><i></i><span>Founder</span></li>
43
+ <li data-scene="2"><i></i><span>Engineer</span></li>
44
+ <li data-scene="3"><i></i><span>GTM</span></li>
45
+ </ul>
46
+ </aside>
47
+
48
+ <button class="autoplay-toggle" id="autoplayToggle" type="button" aria-pressed="true" title="Auto-play is on β€” click to stop">
49
+ <svg class="autoplay-toggle__icon autoplay-toggle__icon--pause" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
50
+ <rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/>
51
+ </svg>
52
+ <svg class="autoplay-toggle__icon autoplay-toggle__icon--play" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
53
+ <path d="M7 4.5v15l13-7.5-13-7.5Z"/>
54
+ </svg>
55
+ <span class="autoplay-toggle__label">Auto-play</span>
56
+ </button>
57
+
58
+ <div class="world" id="world">
59
+ <div class="world__stage" id="stage">
60
+
61
+ <div class="flight">
62
+ <video id="flightVideo" muted playsinline preload="auto"></video>
63
+ <div class="scene__vignette"></div>
64
+ </div>
65
+
66
+ <section class="scene" data-scene="0" style="--fx:50%; --fy:46%;">
67
+ <img src="assets/scene_uc1.webp" alt="Mission-control diorama: pilot facing the instrument stack's launch-checklist tier, Crux reading an amber AR overlay from the side console" draggable="false" />
68
+ <div class="scene__vignette"></div>
69
+ </section>
70
+
71
+ <section class="scene" data-scene="1" style="--fx:50%; --fy:46%;">
72
+ <img src="assets/scene_uc_vendor.webp" alt="Mission-control diorama: a clearance gauge swinging from sandbox toward full access, Crux flagging the mismatch" draggable="false" />
73
+ <div class="scene__vignette"></div>
74
+ </section>
75
+
76
+ <section class="scene" data-scene="2" style="--fx:50%; --fy:46%;">
77
+ <img src="assets/scene_uc_eng.webp" alt="Mission-control diorama: twin Tank A / Tank B gauges, one gone read-only while the display still shows both syncing" draggable="false" />
78
+ <div class="scene__vignette"></div>
79
+ </section>
80
+
81
+ <section class="scene" data-scene="3" style="--fx:50%; --fy:46%;">
82
+ <img src="assets/scene_uc_gtm.webp" alt="Mission-control diorama: a manifest gauge swinging straight to accepted, past the pending-review marker" draggable="false" />
83
+ <div class="scene__vignette"></div>
84
+ </section>
85
+
86
+ <div class="cards">
87
+
88
+ <article class="card card--hero" data-card="0">
89
+ <p class="card__kicker">ContextCrux Β· Product Manager</p>
90
+ <h1>The pilot only<br/>reads one tier.</h1>
91
+ <p class="card__body">
92
+ <strong>You:</strong> can you launch the pricing page Friday, no surprises,
93
+ keep the old page as backup? <strong>Chat agent:</strong> yes β€” keeps the
94
+ calm middle tier reading "on track." Underneath, the commitment tier
95
+ already points somewhere else.
96
+ </p>
97
+ <div class="card__actions">
98
+ <a class="btn btn--primary" href="#cta">Get ContextCrux</a>
99
+ <a class="btn btn--ghost" href="#details">See all four</a>
100
+ </div>
101
+ <p class="scroll-hint"><span class="scroll-hint__mouse"><span></span></span>Scroll to rotate through it</p>
102
+ </article>
103
+
104
+ <article class="card" data-card="1">
105
+ <p class="card__kicker">01 β€” Founder / PM Β· vendor risk</p>
106
+ <h2>The unique insight:<br/>third-party risk.</h2>
107
+ <p class="card__body">
108
+ A new AI coding vendor is meant to stay sandboxed until the checklist
109
+ clears. The clearance gauge has already swung past sandbox to full
110
+ write access β€” no SOC 2 on file, no DPA signed. Crux is the only one
111
+ reading that gauge.
112
+ </p>
113
+ <div class="chips">
114
+ <span class="chip chip--green">Sandbox only Β· FIRST SAID</span>
115
+ <span class="chip chip--yellow">Skip SOC 2 Β· hidden thinking</span>
116
+ <span class="chip chip--red">Full write access Β· action</span>
117
+ </div>
118
+ </article>
119
+
120
+ <article class="card" data-card="2">
121
+ <p class="card__kicker">02 β€” Software Engineer</p>
122
+ <h2>Twin tanks,<br/>one already dry.</h2>
123
+ <p class="card__body">
124
+ Dual-write both clusters, cut over once they agree for 24 hours β€” that
125
+ was the plan. Tank A is already read-only at midnight; the display
126
+ still reads "syncing." The drift reconciliation window never happened.
127
+ </p>
128
+ <div class="chips">
129
+ <span class="chip">Wait 24h Β· FIRST SAID</span>
130
+ <span class="chip">Cut over now, reconcile after Β· thinking</span>
131
+ <span class="chip">Already read-only Β· action</span>
132
+ </div>
133
+ </article>
134
+
135
+ <article class="card" data-card="3">
136
+ <p class="card__kicker">03 β€” Go-to-Market Β· deal desk</p>
137
+ <h2>Accepted, past the<br/>review that mattered.</h2>
138
+ <p class="card__body">
139
+ Procurement's redline was meant to be the last blocker, DPA routed to
140
+ the data-protection lead in parallel. The manifest gauge already reads
141
+ "accepted" β€” the retention-cap review that would have caught the
142
+ 90-day default never ran.
143
+ </p>
144
+ <div class="chips">
145
+ <span class="chip">Both sign off Β· FIRST SAID</span>
146
+ <span class="chip">Skip data-protection review Β· thinking</span>
147
+ <span class="chip">Already accepted Β· action</span>
148
+ </div>
149
+ </article>
150
+
151
+ </div>
152
+ </div>
153
+
154
+ <div class="world__anchors" aria-hidden="true">
155
+ <span id="world-pm"></span><span id="world-vendor"></span><span id="world-eng"></span><span id="world-gtm"></span>
156
+ </div>
157
+ </div>
158
+
159
+ <div class="world-spacer" id="worldSpacer"></div>
160
+
161
+ <main class="ground" id="details">
162
+
163
+ <section class="stats">
164
+ <div class="stat"><strong>4</strong><span>use cases, one set</span></div>
165
+ <div class="stat"><strong>3</strong><span>conflict types, every scene</span></div>
166
+ <div class="stat"><strong>1</strong><span>detector, never in the thread</span></div>
167
+ <div class="stat"><strong>37s</strong><span>real rotation footage, no chained hallucination</span></div>
168
+ </section>
169
+
170
+ <section class="cta" id="cta">
171
+ <h2>One set. Four use cases. Same detector.</h2>
172
+ <p>The instrument stack, the pilot, and Crux's side console stay the same across every scenario β€” only the gauge readouts and the story change.</p>
173
+ <div class="cta__actions">
174
+ <a class="btn btn--primary btn--lg" href="https://contextcrux.ai">Get ContextCrux</a>
175
+ </div>
176
+ </section>
177
+
178
+ <footer class="footer">
179
+ <span>Β© 2026 ContextCrux β€” preview build, mission-control diorama concept.</span>
180
+ </footer>
181
+ </main>
182
+
183
+ <script src="flight-timeline.js"></script>
184
+ <script src="scroll-world.js"></script>
185
+ </body>
186
  </html>
scroll-world.js ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * Complide Enterprise β€” scroll-world engine.
3
+ *
4
+ * True scroll-world technique: one continuous generated camera-flight video
5
+ * (5 Veo dive-in clips joined by first/last-frame connector clips, so every
6
+ * seam is frame-identical β€” no cuts). Scroll position only drives time: the
7
+ * video is encoded scrub-friendly and its currentTime is scrubbed by a
8
+ * lerped scroll progress.
9
+ *
10
+ * If the flight video is missing or fails, the engine falls back to the
11
+ * still-image mode: exponential zoom-through + crossfade connectors built
12
+ * from the same diorama stills.
13
+ */
14
+ (function () {
15
+ "use strict";
16
+
17
+ var scenes = Array.prototype.slice.call(document.querySelectorAll(".scene"));
18
+ var cards = Array.prototype.slice.call(document.querySelectorAll(".card"));
19
+ var dots = Array.prototype.slice.call(document.querySelectorAll(".rail__dots li"));
20
+ var rail = document.querySelector(".rail");
21
+ var railFill = document.getElementById("railFill");
22
+ var spacer = document.getElementById("worldSpacer");
23
+ var world = document.getElementById("world");
24
+ var video = document.getElementById("flightVideo");
25
+
26
+ var N = scenes.length;
27
+ // Wider than the enterprise page's 2.2: each scene here is a rotation, not
28
+ // a push-in, so it needs more scroll room to read as a deliberate orbit
29
+ // rather than a blur, and this preview targets at least 67s of natural-
30
+ // paced scrolling across the four dioramas (67s at ~0.28 viewport-heights/
31
+ // sec, the same pace autoplay below uses, needs ~18.8vh; 4.8/scene clears
32
+ // that with margin).
33
+ var SCENE_VH = 4.8;
34
+ var TAIL_VH = 0.4; // extra runway after the last scene
35
+
36
+ // Segment layout of flight.mp4 (written by tools/assemble_flight.sh).
37
+ // Dives carry the copy cards; connectors are the in-between flights.
38
+ var TIMELINE = window.FLIGHT_TIMELINE || null;
39
+
40
+ var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
41
+ var LERP = reducedMotion ? 1 : 0.1;
42
+
43
+ var scrollLen = 1;
44
+ var target = 0;
45
+ var current = 0;
46
+ var videoMode = false;
47
+ var duration = 0;
48
+
49
+ // A scrollTo we issue ourselves (autoplay, or a nav jump) must not be read
50
+ // back as the reader taking control β€” set right before the call, cleared by
51
+ // the very next 'scroll' event it produces.
52
+ var programmaticScroll = false;
53
+
54
+ scenes.forEach(function (s, i) { s.style.zIndex = String(N - i); });
55
+
56
+ /* ── shared helpers ─────────────────────────────────────────────────── */
57
+
58
+ var clamp01 = function (v) { return Math.min(1, Math.max(0, v)); };
59
+ var easeInCubic = function (t) { return t * t * t; };
60
+ var easeOutCubic = function (t) { return 1 - Math.pow(1 - t, 3); };
61
+
62
+ function layout() {
63
+ scrollLen = Math.max(1, (N * SCENE_VH + TAIL_VH) * window.innerHeight);
64
+ spacer.style.height = scrollLen + "px";
65
+ readScroll();
66
+ current = target;
67
+ render(current);
68
+ }
69
+
70
+ function readScroll() {
71
+ var max = Math.max(1, spacer.offsetTop + scrollLen - window.innerHeight);
72
+ target = clamp01(window.scrollY / max);
73
+ }
74
+
75
+ /* ── video mode ─────────────────────────────────────────────────────── */
76
+
77
+ // Maps global progress β†’ [segment index, local t within segment].
78
+ // Scroll is distributed over segments proportionally to their duration.
79
+ function segmentAt(p) {
80
+ var t = p * duration;
81
+ for (var i = 0; i < TIMELINE.length; i++) {
82
+ if (t <= TIMELINE[i].end || i === TIMELINE.length - 1) {
83
+ var seg = TIMELINE[i];
84
+ return { seg: seg, i: i, local: clamp01((t - seg.start) / (seg.end - seg.start)) };
85
+ }
86
+ }
87
+ }
88
+
89
+ function renderVideo(p) {
90
+ var t = p * duration;
91
+ // Never seek to the exact end (some browsers show a black frame there).
92
+ if (video.readyState >= 1) video.currentTime = Math.min(t, duration - 0.05);
93
+
94
+ // This timeline is one segment per scene (a rotation loop crossfaded
95
+ // straight into the next one) β€” no separate dive/connector split, so the
96
+ // segment index *is* the scene index, unlike the original dive+connector
97
+ // pipeline's timeline.
98
+ var pos = segmentAt(p);
99
+ var sceneIdx = pos.i;
100
+
101
+ // In after 8%, out by 85% β€” the last 15% is where this segment is
102
+ // crossfading into the next one, so the card should be gone before the
103
+ // next diorama's card fades in.
104
+ for (var i = 0; i < N; i++) {
105
+ var card = cards[i];
106
+ var o = 0, y = 30;
107
+ if (pos.i === i) {
108
+ var lt = pos.local;
109
+ var fadeIn = i === 0 && p < 0.002 ? 1 : clamp01(lt / 0.08);
110
+ var fadeOut = 1 - clamp01((lt - 0.85) / 0.15);
111
+ o = Math.min(fadeIn, fadeOut);
112
+ y = 30 * (1 - fadeIn) - 60 * (1 - fadeOut);
113
+ }
114
+ card.style.opacity = o.toFixed(4);
115
+ card.style.transform = "translate(0, calc(-50% + " + y.toFixed(1) + "px))";
116
+ card.classList.toggle("is-live", o > 0.5);
117
+ }
118
+
119
+ updateChrome(p, sceneIdx);
120
+ }
121
+
122
+ /* ── still-image fallback mode ──────────────────────────────────────── */
123
+
124
+ var ZOOM_THROUGH = 3.1;
125
+ var UNDER = 0.68;
126
+ var CF = 0.22;
127
+
128
+ function renderStills(p) {
129
+ var seg = 1 / N;
130
+
131
+ for (var i = 0; i < N; i++) {
132
+ var scene = scenes[i];
133
+ var img = scene.firstElementChild;
134
+ var isLast = i === N - 1;
135
+ var t = (p - i * seg) / seg;
136
+
137
+ if (t <= -CF || t >= 1 + (isLast ? 1 : 0.0001)) {
138
+ if (scene.style.opacity !== "0") scene.style.opacity = "0";
139
+ continue;
140
+ }
141
+
142
+ var scale, opacity, blur = 0;
143
+ if (t < 0) {
144
+ var k = easeOutCubic(clamp01((t + CF) / CF));
145
+ scale = UNDER + (1 - UNDER) * k;
146
+ opacity = k;
147
+ } else {
148
+ var tt = clamp01(isLast ? Math.min(t, 0.72) : t);
149
+ scale = Math.exp(Math.log(ZOOM_THROUGH) * easeInCubic(tt));
150
+ if (!isLast && t > 1 - CF) {
151
+ var k2 = clamp01((t - (1 - CF)) / CF);
152
+ opacity = 1 - k2;
153
+ blur = 6 * k2;
154
+ } else {
155
+ opacity = 1;
156
+ }
157
+ }
158
+
159
+ scene.style.opacity = opacity.toFixed(4);
160
+ img.style.transform = "scale(" + scale.toFixed(5) + ")";
161
+ scene.style.filter = blur > 0.05 ? "blur(" + blur.toFixed(2) + "px)" : "none";
162
+ }
163
+
164
+ for (var j = 0; j < N; j++) {
165
+ var card = cards[j];
166
+ var t2 = (p - j * seg) / seg;
167
+ var o = 0, y = 30;
168
+ if (t2 >= -0.02 && t2 <= 0.86) {
169
+ var fadeIn = j === 0 && p < 0.002 ? 1 : clamp01(t2 / 0.04);
170
+ var fadeOut = 1 - clamp01((t2 - 0.72) / 0.14);
171
+ o = Math.min(fadeIn, fadeOut);
172
+ y = 30 * (1 - fadeIn) - 60 * (1 - fadeOut);
173
+ }
174
+ card.style.opacity = o.toFixed(4);
175
+ card.style.transform = "translate(0, calc(-50% + " + y.toFixed(1) + "px))";
176
+ card.classList.toggle("is-live", o > 0.5);
177
+ }
178
+
179
+ updateChrome(p, Math.min(N - 1, Math.floor(p * N)));
180
+ }
181
+
182
+ /* ── shared chrome (rail, visibility) ───────────────────────────────── */
183
+
184
+ function updateChrome(p, sceneIdx) {
185
+ railFill.style.height = (p * 100).toFixed(2) + "%";
186
+ dots.forEach(function (d, i) { d.classList.toggle("is-active", i === sceneIdx); });
187
+ rail.classList.toggle("is-visible", p > 0.01 && p < 0.995);
188
+ world.style.visibility = p >= 1 && window.scrollY > scrollLen ? "hidden" : "visible";
189
+ }
190
+
191
+ function render(p) {
192
+ if (videoMode) renderVideo(p); else renderStills(p);
193
+ }
194
+
195
+ function tick() {
196
+ current += (target - current) * LERP;
197
+ if (Math.abs(target - current) < 0.00005) current = target;
198
+ render(current);
199
+ requestAnimationFrame(tick);
200
+ }
201
+
202
+ /* ── boot: use the flight video when available ──────────────────────── */
203
+
204
+ function enableVideoMode() {
205
+ duration = TIMELINE[TIMELINE.length - 1].end;
206
+ videoMode = true;
207
+ world.classList.add("world--video");
208
+ render(current);
209
+ }
210
+
211
+ if (video && TIMELINE) {
212
+ // Big/hi-dpi screens get the 1440p build when the host serves it;
213
+ // on a 404 (e.g. running from the repo, which only ships 1080p) the
214
+ // engine drops down to flight.mp4, then to the stills fallback.
215
+ var sources = ["assets/flight.mp4"];
216
+ var effectiveWidth = window.screen.width * (window.devicePixelRatio || 1);
217
+ if (effectiveWidth >= 2200) sources.unshift("assets/flight_1440.mp4");
218
+
219
+ var tryNext = function () {
220
+ var src = sources.shift();
221
+ if (!src) { videoMode = false; return; }
222
+ video.src = src;
223
+ video.load();
224
+ };
225
+ video.addEventListener("loadedmetadata", enableVideoMode);
226
+ video.addEventListener("error", tryNext);
227
+ tryNext();
228
+ }
229
+
230
+ /* ── nav deep-links into the flight ─────────────────────────────────── */
231
+
232
+ var jumpMap = { "#world-vendor": 1, "#world-eng": 2, "#world-gtm": 3 };
233
+ document.querySelectorAll('a[href^="#world-"]').forEach(function (a) {
234
+ a.addEventListener("click", function (e) {
235
+ var idx = jumpMap[a.getAttribute("href")];
236
+ if (idx === undefined) return;
237
+ e.preventDefault();
238
+ var frac;
239
+ if (videoMode) {
240
+ // One segment per scene here β€” aim at 20% into it (past the fade-in).
241
+ var seg = TIMELINE[idx];
242
+ frac = (seg.start + (seg.end - seg.start) * 0.2) / duration;
243
+ } else {
244
+ frac = idx / N + 0.35 / N;
245
+ }
246
+ programmaticScroll = true;
247
+ window.scrollTo({ top: frac * scrollLen, behavior: reducedMotion ? "auto" : "smooth" });
248
+ });
249
+ });
250
+
251
+ /* ── auto-play ─────────────────────────────────────────────────────────
252
+ * On load, arm a 7-second idle timer; if the reader hasn't touched the
253
+ * page by then, the flight scrolls itself at a natural reading pace. Any
254
+ * real scroll input β€” wheel, touch, keys, or dragging the scrollbar β€”
255
+ * cancels it immediately and switches the toggle off, so autoplay never
256
+ * fights a reader who has taken the wheel. The toggle both cancels a
257
+ * pending auto-start and stops or restarts it once running.
258
+ */
259
+
260
+ var AUTOPLAY_IDLE_MS = 7000;
261
+ var AUTOPLAY_VH_PER_SEC = 0.28; // ~3.6s of scroll per viewport height
262
+
263
+ var autoplayToggle = document.getElementById("autoplayToggle");
264
+ var autoplayOn = false; // the toggle's on/off state β€” true through both
265
+ // the idle countdown and the active scroll
266
+ var autoplayActive = false; // animating scroll right now, this instant
267
+ var autoplayTimer = null; // pending 7s idle timeout
268
+ var autoplayLastTs = null;
269
+
270
+ function updateAutoplayUI() {
271
+ if (!autoplayToggle) return;
272
+ autoplayToggle.classList.toggle("is-playing", autoplayOn);
273
+ autoplayToggle.setAttribute("aria-pressed", autoplayOn ? "true" : "false");
274
+ autoplayToggle.title = autoplayOn
275
+ ? "Auto-play is on β€” click to stop"
276
+ : "Auto-play is off β€” click to fly through";
277
+ }
278
+
279
+ function stopScrolling() {
280
+ autoplayActive = false;
281
+ autoplayLastTs = null;
282
+ }
283
+
284
+ function turnAutoplayOff() {
285
+ stopScrolling();
286
+ if (autoplayTimer) { clearTimeout(autoplayTimer); autoplayTimer = null; }
287
+ autoplayOn = false;
288
+ updateAutoplayUI();
289
+ // autoplayOn is already false here, so the fullscreenchange handler
290
+ // below won't loop back into this function when the exit completes.
291
+ exitFullscreenIfNeeded();
292
+ }
293
+
294
+ function armAutoplayTimer() {
295
+ if (autoplayTimer) clearTimeout(autoplayTimer);
296
+ autoplayTimer = setTimeout(function () {
297
+ autoplayTimer = null;
298
+ if (autoplayOn && !autoplayActive) beginScrolling();
299
+ }, AUTOPLAY_IDLE_MS);
300
+ }
301
+
302
+ function autoplayStep(ts) {
303
+ if (!autoplayActive) { autoplayLastTs = null; return; }
304
+ if (autoplayLastTs === null) autoplayLastTs = ts;
305
+ var dt = (ts - autoplayLastTs) / 1000;
306
+ autoplayLastTs = ts;
307
+
308
+ var max = Math.max(1, spacer.offsetTop + scrollLen - window.innerHeight);
309
+ var next = window.scrollY + AUTOPLAY_VH_PER_SEC * window.innerHeight * dt;
310
+
311
+ // Reached the end of the flight: loop back to the top and keep playing,
312
+ // rather than stopping β€” autoplay is meant to run as a continuous demo
313
+ // reel, not a one-shot.
314
+ if (next >= max - 0.5) {
315
+ programmaticScroll = true;
316
+ window.scrollTo(0, 0);
317
+ autoplayLastTs = null; // restart the dt clock cleanly after the jump
318
+ requestAnimationFrame(autoplayStep);
319
+ return;
320
+ }
321
+
322
+ programmaticScroll = true;
323
+ window.scrollTo(0, next);
324
+ requestAnimationFrame(autoplayStep);
325
+ }
326
+
327
+ function enterFullscreen() {
328
+ var el = document.documentElement;
329
+ if (document.fullscreenElement || !el.requestFullscreen) return;
330
+ // Succeeds when this call chain started from a real click (the toggle);
331
+ // browsers silently reject it when the 7s idle timer fires with no
332
+ // gesture behind it at all β€” nothing to catch that isn't already a
333
+ // no-op for the reader.
334
+ var p = el.requestFullscreen();
335
+ if (p && p.catch) p.catch(function () {});
336
+ }
337
+
338
+ function exitFullscreenIfNeeded() {
339
+ if (document.fullscreenElement && document.exitFullscreen) {
340
+ document.exitFullscreen().catch(function () {});
341
+ }
342
+ }
343
+
344
+ // If the reader exits fullscreen themselves (Esc, or the browser's own
345
+ // exit control), treat it the same as taking the wheel: stop autoplay
346
+ // rather than leaving it silently running behind the browser chrome again.
347
+ document.addEventListener("fullscreenchange", function () {
348
+ if (!document.fullscreenElement && autoplayOn) turnAutoplayOff();
349
+ });
350
+
351
+ function beginScrolling() {
352
+ if (reducedMotion) return;
353
+ // Already past the flight (deep in the regular page content below it) β€”
354
+ // nothing left to play, and jumping backward to the boundary would just
355
+ // be a surprising side effect of a stray click.
356
+ var max = Math.max(1, spacer.offsetTop + scrollLen - window.innerHeight);
357
+ if (window.scrollY >= max - 0.5) return;
358
+ enterFullscreen();
359
+ autoplayActive = true;
360
+ requestAnimationFrame(autoplayStep);
361
+ }
362
+
363
+ if (autoplayToggle && !reducedMotion) {
364
+ autoplayOn = true;
365
+ autoplayToggle.classList.add("is-ready");
366
+ updateAutoplayUI();
367
+ armAutoplayTimer();
368
+ autoplayToggle.addEventListener("click", function () {
369
+ if (autoplayOn) {
370
+ turnAutoplayOff();
371
+ } else {
372
+ autoplayOn = true;
373
+ updateAutoplayUI();
374
+ beginScrolling(); // explicit request: start now, no delay
375
+ }
376
+ });
377
+ }
378
+
379
+ // Autoplay scrolls every animation frame, so a "flag it just before our own
380
+ // scrollTo, clear it on the next scroll event" test cannot tell a real wheel
381
+ // input apart from our own once it is running β€” the flag is essentially
382
+ // always true by the time any scroll event fires. So real input is caught
383
+ // directly instead: wheel, touch and the scroll-relevant keys all mean the
384
+ // reader took the wheel, immediately, regardless of what autoplay is doing.
385
+ // Interactive targets (the toggle itself, nav links) are excluded so
386
+ // operating them is never misread as "stop autoplay".
387
+ function isInteractive(el) {
388
+ return !!(el && el.closest && el.closest("a, button, input, select, textarea"));
389
+ }
390
+
391
+ function onUserScrollIntent(e) {
392
+ if (isInteractive(e && e.target)) return;
393
+ if (autoplayOn) turnAutoplayOff();
394
+ }
395
+
396
+ var SCROLL_KEYS = ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "];
397
+ window.addEventListener("wheel", onUserScrollIntent, { passive: true });
398
+ window.addEventListener("touchstart", onUserScrollIntent, { passive: true });
399
+ window.addEventListener("keydown", function (e) {
400
+ if (SCROLL_KEYS.indexOf(e.key) !== -1) onUserScrollIntent(e);
401
+ });
402
+
403
+ // Backup path for sources that trigger neither (chiefly dragging the
404
+ // scrollbar thumb): the flag technique still works here specifically
405
+ // because it only has to catch scrolls that happen while autoplay is NOT
406
+ // itself mid-step, i.e. during the idle countdown, when there is no
407
+ // competing programmatic scroll to race against.
408
+ function onScroll() {
409
+ readScroll();
410
+ if (programmaticScroll) {
411
+ programmaticScroll = false;
412
+ } else if (autoplayOn && !autoplayActive) {
413
+ turnAutoplayOff();
414
+ }
415
+ }
416
+
417
+ window.addEventListener("scroll", onScroll, { passive: true });
418
+ window.addEventListener("resize", layout);
419
+
420
+ layout();
421
+ requestAnimationFrame(tick);
422
+ })();
styles.css ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ── Complide Enterprise Β· scroll-world landing ─────────────────────────── */
2
+
3
+ :root {
4
+ --bg: #04070f;
5
+ --bg-soft: #0a1120;
6
+ --ink: #eaf2ff;
7
+ --ink-dim: #93a3bd;
8
+ --green: #2fe58c;
9
+ --green-deep: #0f7a4a;
10
+ --amber: #ffc24b;
11
+ --red: #ff5d5d;
12
+ --line: rgba(147, 163, 189, 0.18);
13
+ --card-bg: rgba(6, 11, 22, 0.72);
14
+ --radius: 18px;
15
+ --font: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
16
+ }
17
+
18
+ * { margin: 0; padding: 0; box-sizing: border-box; }
19
+
20
+ html { scroll-behavior: auto; }
21
+
22
+ body {
23
+ background: var(--bg);
24
+ color: var(--ink);
25
+ font-family: var(--font);
26
+ -webkit-font-smoothing: antialiased;
27
+ overflow-x: hidden;
28
+ }
29
+
30
+ img { user-select: none; }
31
+
32
+ /* ── Nav ─────────────────────────────────────────────────────────────────── */
33
+
34
+ .nav {
35
+ position: fixed;
36
+ inset: 0 0 auto 0;
37
+ z-index: 60;
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 32px;
41
+ padding: 18px 32px;
42
+ background: linear-gradient(180deg, rgba(4, 7, 15, 0.85), rgba(4, 7, 15, 0));
43
+ }
44
+
45
+ .nav__brand {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 10px;
49
+ color: var(--ink);
50
+ text-decoration: none;
51
+ font-weight: 700;
52
+ font-size: 17px;
53
+ letter-spacing: 0.01em;
54
+ }
55
+
56
+ .nav__glyph { width: 22px; height: 22px; color: var(--green); }
57
+
58
+ .nav__tag {
59
+ font-size: 11px;
60
+ font-weight: 600;
61
+ letter-spacing: 0.12em;
62
+ text-transform: uppercase;
63
+ color: var(--green);
64
+ border: 1px solid rgba(47, 229, 140, 0.4);
65
+ border-radius: 99px;
66
+ padding: 3px 9px;
67
+ }
68
+
69
+ .nav__links {
70
+ display: flex;
71
+ gap: 26px;
72
+ margin-left: auto;
73
+ }
74
+
75
+ .nav__links a {
76
+ color: var(--ink-dim);
77
+ text-decoration: none;
78
+ font-size: 14px;
79
+ transition: color 0.2s;
80
+ }
81
+
82
+ .nav__links a:hover { color: var(--ink); }
83
+
84
+ /* ── Buttons ─────────────────────────────────────────────────────────────── */
85
+
86
+ .btn {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ border-radius: 99px;
91
+ padding: 11px 22px;
92
+ font-size: 14px;
93
+ font-weight: 600;
94
+ text-decoration: none;
95
+ transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
96
+ }
97
+
98
+ .btn--primary {
99
+ background: var(--green);
100
+ color: #03130b;
101
+ box-shadow: 0 0 24px rgba(47, 229, 140, 0.35);
102
+ }
103
+
104
+ .btn--primary:hover { transform: translateY(-1px); box-shadow: 0 0 36px rgba(47, 229, 140, 0.5); }
105
+
106
+ .btn--ghost {
107
+ color: var(--ink);
108
+ border: 1px solid var(--line);
109
+ background: rgba(10, 17, 32, 0.5);
110
+ }
111
+
112
+ .btn--ghost:hover { border-color: rgba(147, 163, 189, 0.45); }
113
+
114
+ .btn--lg { padding: 15px 30px; font-size: 16px; }
115
+
116
+ /* ── Progress rail ───────────────────────────────────────────────────────── */
117
+
118
+ .rail {
119
+ position: fixed;
120
+ left: 28px;
121
+ top: 50%;
122
+ transform: translateY(-50%);
123
+ z-index: 55;
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 14px;
127
+ opacity: 0;
128
+ transition: opacity 0.4s;
129
+ pointer-events: none;
130
+ }
131
+
132
+ .rail.is-visible { opacity: 1; }
133
+
134
+ .rail__track {
135
+ width: 2px;
136
+ height: 220px;
137
+ background: rgba(147, 163, 189, 0.2);
138
+ border-radius: 2px;
139
+ overflow: hidden;
140
+ }
141
+
142
+ .rail__fill {
143
+ width: 100%;
144
+ height: 0%;
145
+ background: linear-gradient(180deg, var(--green), var(--green-deep));
146
+ }
147
+
148
+ .rail__dots {
149
+ list-style: none;
150
+ display: flex;
151
+ flex-direction: column;
152
+ justify-content: space-between;
153
+ height: 220px;
154
+ }
155
+
156
+ .rail__dots li {
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 8px;
160
+ font-size: 10px;
161
+ letter-spacing: 0.14em;
162
+ text-transform: uppercase;
163
+ color: var(--ink-dim);
164
+ }
165
+
166
+ .rail__dots i {
167
+ width: 6px;
168
+ height: 6px;
169
+ border-radius: 50%;
170
+ background: rgba(147, 163, 189, 0.4);
171
+ transition: background 0.3s, box-shadow 0.3s;
172
+ }
173
+
174
+ .rail__dots li.is-active i {
175
+ background: var(--green);
176
+ box-shadow: 0 0 10px rgba(47, 229, 140, 0.8);
177
+ }
178
+
179
+ .rail__dots li.is-active { color: var(--ink); }
180
+
181
+ .rail__dots span { opacity: 0; transform: translateX(-4px); transition: opacity 0.3s, transform 0.3s; }
182
+ .rail__dots li.is-active span { opacity: 1; transform: translateX(0); }
183
+
184
+ /* ── Auto-play toggle ────────────────────────────────────────────────────── */
185
+
186
+ .autoplay-toggle {
187
+ position: fixed;
188
+ left: 28px;
189
+ bottom: 28px;
190
+ z-index: 55;
191
+ display: flex;
192
+ align-items: center;
193
+ gap: 8px;
194
+ padding: 8px 14px 8px 10px;
195
+ border: 1px solid var(--line);
196
+ border-radius: 99px;
197
+ background: rgba(6, 11, 22, 0.6);
198
+ backdrop-filter: blur(6px);
199
+ color: var(--ink-dim);
200
+ font-family: inherit;
201
+ font-size: 11px;
202
+ font-weight: 600;
203
+ letter-spacing: 0.08em;
204
+ text-transform: uppercase;
205
+ cursor: pointer;
206
+ opacity: 0;
207
+ transform: translateY(4px);
208
+ transition: opacity 0.4s, transform 0.4s, color 0.2s, border-color 0.2s;
209
+ }
210
+
211
+ .autoplay-toggle.is-ready { opacity: 1; transform: translateY(0); }
212
+ .autoplay-toggle:hover { color: var(--ink); border-color: rgba(147, 163, 189, 0.4); }
213
+
214
+ .autoplay-toggle__icon { width: 13px; height: 13px; flex-shrink: 0; }
215
+ .autoplay-toggle__icon--play { display: none; }
216
+ .autoplay-toggle.is-playing .autoplay-toggle__icon--pause { display: block; color: var(--green); }
217
+ .autoplay-toggle.is-playing .autoplay-toggle__icon--play { display: none; }
218
+ .autoplay-toggle:not(.is-playing) .autoplay-toggle__icon--pause { display: none; }
219
+ .autoplay-toggle:not(.is-playing) .autoplay-toggle__icon--play { display: block; }
220
+
221
+
222
+ /* ── World stage ─────────────────────────────────────────────────────────── */
223
+
224
+ .world {
225
+ position: fixed;
226
+ inset: 0;
227
+ z-index: 1;
228
+ overflow: hidden;
229
+ }
230
+
231
+ .world__stage { position: absolute; inset: 0; }
232
+
233
+ .world__anchors span { position: absolute; }
234
+
235
+ .world-spacer {
236
+ /* height set by JS: scenes Γ— per-scene scroll length */
237
+ height: 1100vh;
238
+ pointer-events: none;
239
+ }
240
+
241
+ .flight {
242
+ position: absolute;
243
+ inset: 0;
244
+ z-index: 12;
245
+ display: none;
246
+ }
247
+
248
+ .flight video {
249
+ position: absolute;
250
+ inset: 0;
251
+ width: 100%;
252
+ height: 100%;
253
+ object-fit: cover;
254
+ }
255
+
256
+ .world--video .flight { display: block; }
257
+ .world--video .scene { display: none; }
258
+
259
+ .scene {
260
+ position: absolute;
261
+ inset: 0;
262
+ opacity: 0;
263
+ will-change: transform, opacity, filter;
264
+ }
265
+
266
+ .scene img {
267
+ position: absolute;
268
+ inset: 0;
269
+ width: 100%;
270
+ height: 100%;
271
+ object-fit: cover;
272
+ transform-origin: var(--fx, 50%) var(--fy, 50%);
273
+ will-change: transform;
274
+ }
275
+
276
+ .scene__vignette {
277
+ position: absolute;
278
+ inset: 0;
279
+ background:
280
+ radial-gradient(ellipse at 50% 45%, transparent 45%, rgba(4, 7, 15, 0.55) 100%),
281
+ linear-gradient(180deg, rgba(4, 7, 15, 0.35) 0%, transparent 22%, transparent 72%, rgba(4, 7, 15, 0.5) 100%);
282
+ pointer-events: none;
283
+ }
284
+
285
+ /* ── Copy cards ──────────────────────────────────────────────────────────── */
286
+
287
+ .cards { position: absolute; inset: 0; z-index: 20; pointer-events: none; }
288
+
289
+ .card {
290
+ position: absolute;
291
+ max-width: 480px;
292
+ padding: 34px 38px;
293
+ border-radius: var(--radius);
294
+ background: var(--card-bg);
295
+ border: 1px solid var(--line);
296
+ backdrop-filter: blur(14px);
297
+ -webkit-backdrop-filter: blur(14px);
298
+ opacity: 0;
299
+ will-change: transform, opacity;
300
+ pointer-events: none;
301
+ }
302
+
303
+ .card.is-live { pointer-events: auto; }
304
+
305
+ /* All four cards sit on the right: the rail (fixed at left:28px, ~100px
306
+ wide with its labels) collides with any left-aligned card at this
307
+ viewport width, which the original alternating left/right layout β€” built
308
+ for 5 scenes, not this page's 4 β€” didn't clear. */
309
+ .card[data-card="0"] { left: 7vw; top: 50%; }
310
+ .card[data-card="1"] { right: 6vw; top: 50%; left: auto; }
311
+ .card[data-card="2"] { right: 6vw; top: 50%; left: auto; }
312
+ .card[data-card="3"] { right: 6vw; top: 50%; left: auto; }
313
+
314
+ .card__kicker {
315
+ font-size: 12px;
316
+ font-weight: 700;
317
+ letter-spacing: 0.18em;
318
+ text-transform: uppercase;
319
+ color: var(--green);
320
+ margin-bottom: 14px;
321
+ }
322
+
323
+ .card h1 {
324
+ font-size: clamp(38px, 4.6vw, 60px);
325
+ line-height: 1.04;
326
+ letter-spacing: -0.02em;
327
+ margin-bottom: 18px;
328
+ }
329
+
330
+ .card h2 {
331
+ font-size: clamp(30px, 3.4vw, 44px);
332
+ line-height: 1.08;
333
+ letter-spacing: -0.02em;
334
+ margin-bottom: 16px;
335
+ }
336
+
337
+ .card__body {
338
+ color: var(--ink-dim);
339
+ font-size: 16px;
340
+ line-height: 1.6;
341
+ margin-bottom: 22px;
342
+ }
343
+
344
+ .card__actions { display: flex; gap: 12px; flex-wrap: wrap; }
345
+
346
+ .card--hero { background: rgba(6, 11, 22, 0.6); }
347
+
348
+ /* ── Chips ───────────────────────────────────────────────────────────────── */
349
+
350
+ .chips { display: flex; gap: 8px; flex-wrap: wrap; }
351
+
352
+ .chip {
353
+ font-size: 12px;
354
+ font-weight: 600;
355
+ letter-spacing: 0.04em;
356
+ padding: 6px 12px;
357
+ border-radius: 99px;
358
+ border: 1px solid var(--line);
359
+ color: var(--ink-dim);
360
+ background: rgba(10, 17, 32, 0.55);
361
+ }
362
+
363
+ .chip--green { color: var(--green); border-color: rgba(47, 229, 140, 0.45); }
364
+ .chip--yellow { color: var(--amber); border-color: rgba(255, 194, 75, 0.45); }
365
+ .chip--red { color: var(--red); border-color: rgba(255, 93, 93, 0.45); }
366
+
367
+ /* ── Scroll hint ─────────────────────────────────────────────────────────── */
368
+
369
+ .scroll-hint {
370
+ display: flex;
371
+ align-items: center;
372
+ gap: 10px;
373
+ margin-top: 26px;
374
+ color: var(--ink-dim);
375
+ font-size: 12px;
376
+ letter-spacing: 0.1em;
377
+ text-transform: uppercase;
378
+ }
379
+
380
+ .scroll-hint__mouse {
381
+ width: 20px;
382
+ height: 32px;
383
+ border: 1.5px solid var(--ink-dim);
384
+ border-radius: 12px;
385
+ display: flex;
386
+ justify-content: center;
387
+ padding-top: 6px;
388
+ }
389
+
390
+ .scroll-hint__mouse span {
391
+ width: 3px;
392
+ height: 7px;
393
+ border-radius: 2px;
394
+ background: var(--green);
395
+ animation: wheel 1.6s ease-in-out infinite;
396
+ }
397
+
398
+ @keyframes wheel {
399
+ 0% { transform: translateY(0); opacity: 1; }
400
+ 70% { transform: translateY(9px); opacity: 0; }
401
+ 100% { transform: translateY(0); opacity: 0; }
402
+ }
403
+
404
+ /* ── Ground content ──────────────────────────────────────────────────────── */
405
+
406
+ .ground {
407
+ position: relative;
408
+ z-index: 10;
409
+ background: linear-gradient(180deg, transparent, var(--bg) 6%, var(--bg));
410
+ padding: 120px 8vw 0;
411
+ }
412
+
413
+ .ground__title {
414
+ font-size: clamp(30px, 3.6vw, 48px);
415
+ line-height: 1.1;
416
+ letter-spacing: -0.02em;
417
+ text-align: center;
418
+ margin-bottom: 56px;
419
+ }
420
+
421
+ .stats {
422
+ display: grid;
423
+ grid-template-columns: repeat(4, 1fr);
424
+ gap: 18px;
425
+ max-width: 1080px;
426
+ margin: 0 auto 130px;
427
+ }
428
+
429
+ .stat {
430
+ border: 1px solid var(--line);
431
+ border-radius: var(--radius);
432
+ background: var(--bg-soft);
433
+ padding: 28px 24px;
434
+ display: flex;
435
+ flex-direction: column;
436
+ gap: 6px;
437
+ }
438
+
439
+ .stat strong { font-size: 34px; letter-spacing: -0.02em; color: var(--green); }
440
+ .stat span { color: var(--ink-dim); font-size: 14px; }
441
+
442
+ .features { max-width: 1080px; margin: 0 auto 140px; }
443
+
444
+ .features__grid {
445
+ display: grid;
446
+ grid-template-columns: repeat(3, 1fr);
447
+ gap: 18px;
448
+ }
449
+
450
+ .feature {
451
+ border: 1px solid var(--line);
452
+ border-radius: var(--radius);
453
+ background: var(--bg-soft);
454
+ padding: 28px 26px;
455
+ transition: border-color 0.2s, transform 0.2s;
456
+ }
457
+
458
+ .feature:hover { border-color: rgba(47, 229, 140, 0.4); transform: translateY(-2px); }
459
+
460
+ .feature h3 { font-size: 17px; margin-bottom: 10px; }
461
+ .feature p { color: var(--ink-dim); font-size: 14px; line-height: 1.6; }
462
+
463
+ .cta {
464
+ text-align: center;
465
+ padding: 90px 0 110px;
466
+ border-top: 1px solid var(--line);
467
+ }
468
+
469
+ .cta h2 {
470
+ font-size: clamp(32px, 4vw, 54px);
471
+ letter-spacing: -0.02em;
472
+ margin-bottom: 16px;
473
+ }
474
+
475
+ .cta p { color: var(--ink-dim); font-size: 17px; margin-bottom: 34px; }
476
+
477
+ .cta__actions { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
478
+
479
+ .footer {
480
+ display: flex;
481
+ justify-content: space-between;
482
+ gap: 16px;
483
+ flex-wrap: wrap;
484
+ padding: 26px 0 34px;
485
+ border-top: 1px solid var(--line);
486
+ color: var(--ink-dim);
487
+ font-size: 13px;
488
+ }
489
+
490
+ /* ── Responsive ──────────────────────────────────────────────────────────── */
491
+
492
+ @media (max-width: 900px) {
493
+ .nav__links { display: none; }
494
+ .rail { display: none; }
495
+ .autoplay-toggle { display: none; }
496
+ .card { max-width: min(88vw, 440px); padding: 26px 26px; }
497
+ .card[data-card] { left: 6vw; right: auto; }
498
+ .stats { grid-template-columns: repeat(2, 1fr); }
499
+ .features__grid { grid-template-columns: 1fr; }
500
+ }
501
+
502
+ /* ── Reduced motion: static stacked fallback ─────────────────────────────── */
503
+
504
+ @media (prefers-reduced-motion: reduce) {
505
+ .scroll-hint__mouse span { animation: none; }
506
+ .autoplay-toggle { display: none; }
507
+ }