1/* ============================================================ 2 AiDASH PreventionFirst homepage â vanilla-JS port of the 3 framer-motion / Lenis behaviours from the Next.js build. 4 ============================================================ */ 5 (function () { 6 "use strict"; 7 var root = document.querySelector(".pf-home"); 8 if (!root) return; 9 10 var mqMobile = window.matchMedia("(max-width:767px)"); 11 var isMobile = function () { return mqMobile.matches; }; 12 13 /* ââ helpers âââââââââââââââââââââââââââââââââââââââââââ */ 14 function clamp(v, a, b) { return Math.min(b, Math.max(a, v)); } 15 // framer useTransform: piecewise-linear, clamped at the ends 16 function mapRange(v, inp, out) { 17 if (v <= inp[0]) return out[0]; 18 if (v >= inp[inp.length - 1]) return out[out.length - 1]; 19 for (var i = 1; i < inp.length; i++) { 20 if (v <= inp[i]) { 21 var t = (v - inp[i - 1]) / (inp[i] - inp[i - 1]); 22 return out[i - 1] + t * (out[i] - out[i - 1]); 23 } 24 } 25 return out[out.length - 1]; 26 } 27 // scroll progress mirroring framer useScroll() 28 // mode 'cover' = offset ['start start','end end'] 29 // mode 'pass' = offset ['start end','end start'] 30 function progress(el, mode) { 31 if (!el || el.offsetParent === null && el.style.position !== "sticky") { 32 var r0 = el && el.getBoundingClientRect(); 33 if (!r0 || (r0.width === 0 && r0.height === 0)) return -1; // hidden 34 } 35 var r = el.getBoundingClientRect(); 36 var vh = window.innerHeight; 37 if (mode === "pass") { 38 return clamp((vh - r.top) / (r.height + vh), 0, 1); 39 } 40 return clamp(-r.top / (r.height - vh), 0, 1); 41 } 42 43 /* ââ Scrolling âââââââââââââââââââââââââââââââââââââââââââââ 44 We intentionally use NATIVE scrolling. The CDN Lenis build 45 damped wheel input to ~10-20% of normal speed (measured), which 46 is why the page felt like it "barely moved". Native scroll is 47 1:1, smooth at the OS level, and drives every animation below 48 via window.scrollY just the same. */ 49 var lenis = null; 50 51 /* ââ Reveal on scroll (framer whileInView) âââââââââââââââ */ 52 var io = new IntersectionObserver(function (entries) { 53 entries.forEach(function (e) { 54 if (e.isIntersecting) { 55 var d = e.target.getAttribute("data-delay"); 56 if (d) e.target.style.transitionDelay = d + "s"; 57 e.target.classList.add("pf-in"); 58 io.unobserve(e.target); 59 } 60 }); 61 }, { threshold: 0.1, rootMargin: "-40px 0px" }); 62 root.querySelectorAll(".pf-reveal").forEach(function (el) { io.observe(el); }); 63 64 // Mockup entrance (platform pillars) 65 var mock = root.querySelector(".pf-mock__inner"); 66 if (mock) { 67 var mio = new IntersectionObserver(function (es) { 68 es.forEach(function (e) { if (e.isIntersecting) { mock.classList.add("pf-in"); mio.disconnect(); } }); 69 }, { threshold: 0.2 }); 70 mio.observe(mock); 71 } 72 73 /* ââ HERO: load fade-ups âââââââââââââââââââââââââââââââââ */ 74 requestAnimationFrame(function () { 75 root.querySelectorAll(".pf-hero .pf-fade").forEach(function (el) { 76 var d = parseFloat(el.getAttribute("data-delay") || "0"); 77 setTimeout(function () { el.classList.add("pf-in"); }, d * 1000); 78 }); 79 }); 80 81 /* ââ HERO: flipboard word rotation (3D split-flap flip) âââ */ 82 (function () { 83 var flip = root.querySelector(".pf-hero__flip-word"); 84 if (!flip) return; 85 var words; 86 try { words = JSON.parse(flip.getAttribute("data-words") || "[]"); } catch (e) { words = []; } 87 if (!words.length) return; 88 var i = 0, HOLD = 2200, HALF = 300; 89 // fast fold-away, then a slightly springy settle as the new flap lands 90 var OUT = "cubic-bezier(.45,0,.85,.35)", IN = "cubic-bezier(.2,.9,.35,1.25)"; 91 var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 92 93 flip.textContent = words[0]; 94 flip.style.willChange = "transform, opacity"; 95 96 if (reduce) { 97 // honour reduced-motion: plain crossfade, no movement 98 setInterval(function () { 99 flip.style.transition = "opacity .25s ease"; 100 flip.style.opacity = "0"; 101 setTimeout(function () { 102 i = (i + 1) % words.length; 103 flip.textContent = words[i]; 104 flip.style.opacity = "1"; 105 }, 260); 106 }, HOLD + 520); 107 return; 108 } 109 110 // Pure vertical slide + fade â no rotation â matching the Next.js flipboard: 111 // outgoing rises up and clips out the top (y: 0 -> -32, fade out); 112 // incoming rises from below (y: 32 -> 0, fade in). IN eases with a small 113 // overshoot to emulate the framer spring (stiffness 300, damping 28). 114 var RISE = 32; // px â matches Next.js desktop (y: 32 / -32) 115 // Animate the container height (no fixed height) so the content below is 116 // pushed down for two-line words and springs back up for one-line ones â 117 // this mirrors framer-motion's `layout` animation in the Next.js hero. 118 // On mobile the box is a fixed two-line slot in CSS instead â the words flip 119 // inside it so the paragraph and CTAs below never move (any inline height is 120 // cleared, otherwise it would fight the reserved slot). 121 var flipBox = flip.parentNode; // .pf-hero__flip 122 flipBox.style.transition = "height " + HALF + "ms ease"; 123 setInterval(function () { 124 var fluid = !isMobile(); 125 // pin current height so the upcoming resize can transition from it 126 if (fluid) flipBox.style.height = flip.offsetHeight + "px"; 127 else flipBox.style.height = ""; 128 flip.style.transition = "transform " + HALF + "ms " + OUT + ", opacity " + HALF + "ms ease-in"; 129 flip.style.transform = "translateY(-" + RISE + "px)"; 130 flip.style.opacity = "0"; 131 setTimeout(function () { 132 i = (i + 1) % words.length; 133 flip.textContent = words[i]; 134 flip.style.transition = "none"; 135 flip.style.transform = "translateY(" + RISE + "px)"; 136 flip.style.opacity = "0"; 137 void flip.offsetHeight; // force reflow before animating in 138 flip.style.transition = "transform " + HALF + "ms " + IN + ", opacity " + HALF + "ms ease-out"; 139 flip.style.transform = "translateY(0)"; 140 flip.style.opacity = "1"; 141 if (fluid) flipBox.style.height = flip.offsetHeight + "px"; // grow/shrink to new word 142 }, HALF); 143 }, HOLD + HALF * 2); 144 })(); 145 146 /* ââ GlowCards: cursor-following spotlight âââââââââââââââââ 147 Drives --x/--y on every .pf-glow so the radial spotlight and 148 border glow track the pointer (Outcomes, SatelliteFirst 149 Advantages, and Cost-of-Reaction cards). Without this the 150 gradients stay frozen at the top-left corner (0,0). */ 151 (function () { 152 var cards = root.querySelectorAll(".pf-glow"); 153 if (!cards.length) return; 154 for (var c = 0; c < cards.length; c++) { 155 (function (card) { 156 card.addEventListener("pointermove", function (e) { 157 var r = card.getBoundingClientRect(); 158 if (!r.width || !r.height) return; 159 var x = e.clientX - r.left, y = e.clientY - r.top; 160 card.style.setProperty("--x", x.toFixed(2)); 161 card.style.setProperty("--y", y.toFixed(2)); 162 card.style.setProperty("--xp", (x / r.width).toFixed(2)); 163 card.style.setProperty("--yp", (y / r.height).toFixed(2)); 164 }); 165 })(cards[c]); 166 } 167 })(); 168 169 /* ââ Cost-of-Reaction: mobile swipe carousel dots âââââââââ 170 Syncs the pagination dots with the horizontal scroll position and 171 lets a dot tap scroll its card into view. Dots are hidden on desktop. */ 172 (function () { 173 var grid = root.querySelector(".pf-stats__grid"); 174 var dotWrap = root.querySelector(".pf-stats__dots"); 175 if (!grid || !dotWrap) return; 176 var dots = dotWrap.querySelectorAll(".pf-stats__dot"); 177 var cards = grid.querySelectorAll(".pf-stat"); 178 if (!dots.length || !cards.length) return; 179 180 function activeIndex() { 181 var g = grid.getBoundingClientRect(); 182 var mid = g.left + g.width / 2; 183 var best = 0, bestD = Infinity; 184 for (var k = 0; k < cards.length; k++) { 185 var r = cards[k].getBoundingClientRect(); 186 var d = Math.abs(r.left + r.width / 2 - mid); 187 if (d < bestD) { bestD = d; best = k; } 188 } 189 return best; 190 } 191 function sync() { 192 var a = activeIndex(); 193 for (var k = 0; k < dots.length; k++) dots[k].classList.toggle("is-active", k === a); 194 } 195 var raf; 196 grid.addEventListener("scroll", function () { 197 if (raf) cancelAnimationFrame(raf); 198 raf = requestAnimationFrame(sync); 199 }, { passive: true }); 200 for (var k = 0; k < dots.length; k++) { 201 (function (idx) { 202 dots[idx].addEventListener("click", function () { 203 var g = grid.getBoundingClientRect(), r = cards[idx].getBoundingClientRect(); 204 grid.scrollTo({ left: grid.scrollLeft + (r.left - g.left) - (g.width - r.width) / 2, behavior: "smooth" }); 205 }); 206 })(k); 207 } 208 sync(); 209 })(); 210
211 /* ââ HERO: cursor spotlight overlay ââââââââââââââââââââââ */ 212 var heroSpot = (function () { 213 var hero = root.querySelector(".pf-hero"); 214 var ov = root.querySelector(".pf-hero__overlay"); 215 if (!hero || !ov) return function () {}; 216 var tx = -9999, ty = -9999, cx = -9999, cy = -9999; 217 hero.addEventListener("mouseenter", function (e) { 218 var r = hero.getBoundingClientRect(); tx = cx = e.clientX - r.left; ty = cy = e.clientY - r.top; 219 }); 220 hero.addEventListener("mousemove", function (e) { 221 var r = hero.getBoundingClientRect(); tx = e.clientX - r.left; ty = e.clientY - r.top; 222 }); 223 hero.addEventListener("mouseleave", function () { tx = -9999; ty = -9999; }); 224 return function () { 225 cx += (tx - cx) * 0.18; cy += (ty - cy) * 0.18; 226 var mask = "radial-gradient(circle 130px at " + cx + "px " + cy + "px, transparent 0%, transparent 70px, #00000040 100px, #000000 130px)"; 227 ov.style.webkitMaskImage = mask; ov.style.maskImage = mask; 228 }; 229 })(); 230 231 /* ââ CLOUD TRANSITION parallax âââââââââââââââââââââââââââ */ 232 var cloud = (function () { 233 var el = root.querySelector(".pf-cloud"); 234 if (!el) return function () {}; 235 var c = el.querySelector(".pf-cloud__c"), l = el.querySelector(".pf-cloud__l"), r = el.querySelector(".pf-cloud__r"); 236 return function () { 237 var p = progress(el, "pass"); if (p < 0) return; 238 if (l) { l.style.transform = "translateX(" + mapRange(p, [0, 1], [0, -30]) + "%)"; l.style.opacity = mapRange(p, [0, 1], [1, 0]); } 239 if (r) { r.style.transform = "translateX(" + mapRange(p, [0, 1], [0, 30]) + "%)"; r.style.opacity = mapRange(p, [0, 1], [1, 0]); } 240 if (c) { c.style.transform = "translateY(" + mapRange(p, [0, 1], [0, -25]) + "%)"; c.style.opacity = mapRange(p, [0, 1], [1, 0]); } 241 }; 242 })(); 243 244 /* ââ ZOOM PARALLAX (desktop) âââââââââââââââââââââââââââââ */ 245 var zoom = (function () { 246 var el = root.querySelector(".pf-zoom"); 247 if (!el) return function () {}; 248 var scalers = el.querySelectorAll(".pf-zoom__scaler"); 249 var box0 = el.querySelector(".pf-zi-0"); 250 var hots = el.querySelector(".pf-zoom__hotspots"); 251 var targets = [5, 7, 8, 7, 9, 10, 11]; 252 return function () { 253 if (el.offsetParent === null) return; 254 var p = progress(el, "cover"); if (p < 0) return; 255 scalers.forEach(function (s, i) { 256 s.style.transform = "scale(" + mapRange(p, [0, 1], [1, targets[i] || 5]) + ")"; 257 }); 258 if (box0) box0.style.borderRadius = mapRange(p, [0, 0.3], [6, 0]) + "px"; 259 if (hots) hots.style.opacity = mapRange(p, [0, 0.70, 0.88, 1], [0, 0, 1, 1]); 260 }; 261 })(); 262 263 /* ââ SATELLITE FIRST (desktop tabs) ââââââââââââââââââââââ */ 264 (function () { 265 var el = root.querySelector(".pf-sat__desktop"); 266 if (!el) return; 267 var tabs = el.querySelectorAll("[data-sat-tab]"); 268 var panels = el.querySelectorAll("[data-sat-panel]"); 269 var wraps = el.querySelectorAll(".pf-sat__vid"); 270 var nexts = el.querySelectorAll("[data-sat-advance]"); 271 if (!tabs.length) return; 272 var AUTO = 8000; /* auto-advance cadence (matches the Next.js build) */ 273 var active = 0, timer = null, primed = false; 274 275 /* the two layouts both ship in the markup â only fetch the clips for the one 276 that is actually displayed, so phones never pull the desktop stack */ 277 function visible() { return el.offsetParent !== null; } 278 function prime() { 279 if (primed || !visible()) return; 280 primed = true; 281 wraps.forEach(function (w) { var v = w.querySelector("video"); if (v) v.preload = "auto"; }); 282 } 283 function render() { 284 prime();
285 tabs.forEach(function (t, i) { 286 t.classList.toggle("pf-active", i === active); 287 t.classList.toggle("pf-filled", i <= active); 288 t.setAttribute("aria-selected", i === active ? "true" : "false"); 289 }); 290 panels.forEach(function (p, i) { 291 var on = i === active; 292 p.classList.toggle("pf-active", on); 293 p.setAttribute("aria-hidden", on ? "false" : "true"); 294 var btn = p.querySelector("[data-sat-advance]"); 295 if (btn) btn.disabled = !on; 296 }); 297 wraps.forEach(function (w, i) { 298 w.classList.toggle("pf-active", i === active); 299 var v = w.querySelector("video"); if (!v) return; 300 if (i === active && visible()) { try { v.currentTime = 0; } catch (e) {} v.play().catch(function () {}); } 301 else { v.pause(); try { v.currentTime = 0; } catch (e) {} } 302 }); 303 } 304 function go(i) { active = (i + tabs.length) % tabs.length; render(); } 305 function stopAuto() { if (timer) { clearInterval(timer); timer = null; } } 306
307 tabs.forEach(function (t, i) { t.addEventListener("click", function () { stopAuto(); go(i); }); }); 308 nexts.forEach(function (b) { b.addEventListener("click", function () { stopAuto(); go(active + 1); }); }); 309 window.addEventListener("resize", function () { if (!primed) render(); }, { passive: true }); 310 311 render(); 312 timer = setInterval(function () { if (visible()) go(active + 1); }, AUTO); 313 })(); 314 315 /* ââ SATELLITE FIRST (mobile tap stepper) ââââââââââââââââ */ 316 (function () { 317 var el = root.querySelector(".pf-satm"); 318 if (!el) return; 319 var vids = el.querySelectorAll(".pf-satm__videos video"); 320 var stepsEls = el.querySelectorAll(".pf-satm__step"); 321 var prevBtns = el.querySelectorAll("[data-sat-prev]"); 322 var nextBtns = el.querySelectorAll("[data-sat-next]"); 323 var active = 0, max = stepsEls.length - 1, primed = false; 324 /* same deal as the desktop tabs: only fetch this stack when it is on screen */ 325 function visible() { return el.offsetParent !== null; } 326 function prime() { 327 if (primed || !visible()) return; 328 primed = true; 329 vids.forEach(function (v) { v.preload = "auto"; }); 330 } 331 function render() { 332 prime(); 333 vids.forEach(function (v, i) { 334 if (i === active) { v.classList.add("pf-active"); if (visible()) { v.currentTime = 0; v.play().catch(function () {}); } } 335 else { v.classList.remove("pf-active"); v.pause(); } 336 }); 337 stepsEls.forEach(function (s, i) { s.classList.toggle("pf-active", i === active); }); 338 prevBtns.forEach(function (b) { b.disabled = active === 0; }); 339 nextBtns.forEach(function (b) { b.disabled = active === max; }); 340 } 341 prevBtns.forEach(function (b) { b.addEventListener("click", function () { active = Math.max(0, active - 1); render(); }); }); 342 nextBtns.forEach(function (b) { b.addEventListener("click", function () { active = Math.min(max, active + 1); render(); }); }); 343 window.addEventListener("resize", function () { if (!primed) render(); }, { passive: true }); 344 render(); 345 })(); 346 347 /* ââ SHIFT mobile hotspot toggles ââââââââââââââââââââââââ */ 348 root.querySelectorAll(".pf-mhot__pt").forEach(function (pt) { 349 pt.addEventListener("click", function () { 350 var was = pt.classList.contains("pf-open"); 351 root.querySelectorAll(".pf-mhot__pt").forEach(function (p) { p.classList.remove("pf-open"); }); 352 if (!was) pt.classList.add("pf-open"); 353 }); 354 }); 355 356 /* ââ CITYSCAPE (scroll beats + auto-scroll) ââââââââââââââ */ 357 var city = (function () { 358 var el = root.querySelector(".pf-city"); 359 if (!el) return { frame: function () {} }; 360 var lit = el.querySelector(".pf-city__lit"); 361 var shade = el.querySelector(".pf-city__shade"); 362 var b1 = el.querySelector('[data-beat="1"]'); 363 var b2 = el.querySelector('[data-beat="2"]'); 364 var end = el.querySelector(".pf-city__end"); 365 var prompt = el.querySelector(".pf-city__prompt"); 366 var cur = 0, showPrompt = false, auto = false, autoRAF = null, idleT = null; 367 368 function stopAuto() { auto = false; if (autoRAF) { cancelAnimationFrame(autoRAF); autoRAF = null; } } 369 function startAuto() { 370 if (auto) return; auto = true; 371 var step = function () { 372 if (!auto) return; 373 if (lenis) lenis.scrollTo(window.scrollY + 1.5, { immediate: true }); 374 else window.scrollBy(0, 1.5); 375 autoRAF = requestAnimationFrame(step); 376 }; 377 autoRAF = requestAnimationFrame(step); 378 } 379 function resetIdle() { 380 stopAuto(); showPrompt = false; if (prompt) prompt.style.opacity = "0"; 381 if (idleT) clearTimeout(idleT); 382 // Show the "keep scrolling" chevron hint after idle. (No auto-scroll: 383 // hijacking the scroll position fought the user and felt broken.) 384 idleT = setTimeout(function () { showPrompt = true; }, 5000); 385 } 386 ["scroll", "wheel", "touchmove", "keydown"].forEach(function (ev) { 387 window.addEventListener(ev, function () { stopAuto(); resetIdle(); }, { passive: true }); 388 }); 389 resetIdle(); 390 391 return { 392 frame: function () { 393 if (el.offsetParent === null) return; 394 var p = progress(el, "cover"); if (p < 0) return; 395 cur = p; 396 var litOp = clamp(mapRange(p, [0.30, 0.46], [0, 1]), 0, 1); 397 if (lit) lit.style.opacity = litOp; 398 if (shade) shade.style.opacity = litOp; 399 if (b1) b1.style.opacity = mapRange(p, [0, 0.06, 0.32, 0.40], [0, 1, 1, 0]); 400 if (b2) b2.style.opacity = mapRange(p, [0.38, 0.46, 0.68, 0.76], [0, 1, 1, 0]); 401 var e3 = clamp(mapRange(p, [0.76, 0.84], [0, 1]), 0, 1); 402 if (end) end.style.opacity = e3; 403 if (p >= 0.98) { stopAuto(); showPrompt = false; if (idleT) clearTimeout(idleT); } 404 if (prompt) prompt.style.opacity = (showPrompt && p < 0.92) ? "1" : "0"; 405 } 406 }; 407 })(); 408 409 /* ââ master rAF loop âââââââââââââââââââââââââââââââââââââ */ 410 function loop() { 411 heroSpot(); 412 cloud(); 413 zoom(); 414 city.frame(); 415 requestAnimationFrame(loop); 416 } 417 requestAnimationFrame(loop); 418 })(); 419
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.