Spaces:
Running
Running
Deploy mission-control diorama preview with loop+fullscreen autoplay
Browse files- .gitattributes +5 -0
- assets/flight.mp4 +3 -0
- assets/scene_uc1.webp +3 -0
- assets/scene_uc_eng.webp +3 -0
- assets/scene_uc_gtm.webp +3 -0
- assets/scene_uc_vendor.webp +3 -0
- flight-timeline.js +23 -0
- index.html +185 -18
- scroll-world.js +422 -0
- styles.css +507 -0
.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
|
assets/scene_uc_eng.webp
ADDED
|
Git LFS Details
|
assets/scene_uc_gtm.webp
ADDED
|
Git LFS Details
|
assets/scene_uc_vendor.webp
ADDED
|
Git LFS Details
|
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 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|