1let cleanupLandingMotion = null; 2/* âââ Shared single-RAF scroll scheduler ââââââââââââââââââââââââââââââââââââ 3 All scroll-driven functions register here. One scroll listener â one RAF 4 â all updates in the same frame (reads first, then writes = no thrashing). 5 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 6let _rafId = 0; 7let _lastScrollY = -1; 8const _updateQueue = []; 9function _flushUpdates() { 10 _rafId = 0; 11 const sy = window.scrollY; 12 if (sy === _lastScrollY) 13 return; 14 _lastScrollY = sy; 15 for (const fn of _updateQueue) 16 fn(); 17} 18function _scheduleScroll() { 19 if (_rafId !== 0) 20 return; 21 _rafId = window.requestAnimationFrame(_flushUpdates); 22} 23function _addUpdate(fn) { 24 _updateQueue.push(fn); 25 if (_updateQueue.length === 1) { 26 window.addEventListener('scroll', _scheduleScroll, { passive: true }); 27 } 28 return () => { 29 const idx = _updateQueue.indexOf(fn); 30 if (idx >= 0) 31 _updateQueue.splice(idx, 1); 32 if (_updateQueue.length === 0) { 33 window.removeEventListener('scroll', _scheduleScroll); 34 if (_rafId !== 0) { 35 window.cancelAnimationFrame(_rafId); 36 _rafId = 0; 37 } 38 _lastScrollY = -1; 39 } 40 }; 41} 42/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 43function revealOnScroll() { 44 const revealElements = Array.from(document.querySelectorAll('[data-landing-reveal]')); 45 if (revealElements.length === 0) { 46 return () => { }; 47 } 48 revealElements.forEach((element, index) => { 49 const delay = element.dataset['revealDelay'] ?? String((index % 4) * 90); 50 element.style.transitionDelay = `${delay}ms`; 51 }); 52 const observer = new IntersectionObserver((entries) => { 53 entries.forEach((entry) => { 54 if (!entry.isIntersecting) 55 return; 56 const target = entry.target; 57 target.classList.remove('opacity-0', 'translate-y-6'); 58 target.classList.add('opacity-100', 'translate-y-0'); 59 observer.unobserve(target); 60 }); 61 }, { 62 threshold: 0.18, 63 rootMargin: '0px 0px -8% 0px', 64 }); 65 revealElements.forEach((element) => observer.observe(element)); 66 return () => observer.disconnect(); 67} 68function initStoryScroller() { 69 const scrollRoot = document.querySelector('[data-story-scroll-root]'); 70 const panels = Array.from(document.querySelectorAll('[data-story-panel]')); 71 const visuals = Array.from(document.querySelectorAll('[data-story-visual]')); 72 const progressContainer = document.querySelector('[data-story-progress]'); 73 const progressFill = document.querySelector('[data-story-progress-fill]'); 74 const progressDot = document.querySelector('[data-story-progress-dot]'); 75 const isDesktop = window.matchMedia('(min-width: 1024px)'); 76 if (!scrollRoot || panels.length === 0 || visuals.length === 0 || !isDesktop.matches) { 77 return () => { }; 78 } 79 const maxStage = Math.max(1, panels.length - 1); 80 let progress = 0; 81 let target = 0; 82 let animRafId = 0; 83 let scrollableDistance = 1; 84 // Cache element position â avoids getBoundingClientRect in hot scroll path 85 let cachedRootTop = 0; 86 let cachedProgressHeight = 0; 87 const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); 88 const updateProgress = (value) => { 89 if (!progressContainer || !progressFill || !progressDot) 90 return; 91 const snapped = Math.round(value); 92 const ratio = snapped / maxStage; 93 const padding = cachedProgressHeight * 0.06; 94 const trackHeight = Math.max(0, cachedProgressHeight - padding * 2); 95 const top = padding + ratio * trackHeight; 96 progressDot.style.top = `${top.toFixed(1)}px`; 97 progressFill.style.height = '100%'; 98 }; 99 const setStageState = (element, offset) => { 100 const absOffset = Math.abs(offset); 101 const opacity = Math.max(0.18, 1 - absOffset * 0.55); 102 const translateY = offset * 22; 103 const scale = 1 - Math.min(absOffset, 1) * 0.02; 104 element.style.opacity = opacity.toFixed(3); 105 element.style.transform = `translate3d(0, ${translateY.toFixed(1)}px, 0) scale(${scale.toFixed(3)})`; 106 element.style.pointerEvents = opacity > 0.6 ? 'auto' : 'none'; 107 }; 108 const paint = () => {
109 panels.forEach((panel) => { 110 panel.style.opacity = '1'; 111 panel.style.transform = 'translate3d(0, 0, 0) scale(1)'; 112 panel.style.pointerEvents = 'auto'; 113 }); 114 visuals.forEach((visual, visualIndex) => { 115 setStageState(visual, visualIndex - progress); 116 visual.style.zIndex = String(10 - Math.round(Math.abs(visualIndex - progress) * 2)); 117 }); 118 updateProgress(progress); 119 }; 120 const animate = () => { 121 const delta = target - progress; 122 progress += delta * 0.18; 123 if (Math.abs(delta) < 0.001) 124 progress = target; 125 paint(); 126 if (Math.abs(target - progress) < 0.001) { 127 animRafId = 0; 128 return; 129 } 130 animRafId = window.requestAnimationFrame(animate); 131 }; 132 const requestRender = () => { 133 if (animRafId === 0) 134 animRafId = window.requestAnimationFrame(animate); 135 }; 136 const updateLayout = () => { 137 scrollableDistance = Math.max(1, scrollRoot.offsetHeight - window.innerHeight * 0.6); 138 // Re-cache position on resize (offsetTop avoids forced layout during scroll) 139 cachedRootTop = scrollRoot.getBoundingClientRect().top + window.scrollY; 140 if (progressContainer) 141 cachedProgressHeight = progressContainer.offsetHeight; 142 }; 143 // Scroll update: uses cached position â no getBoundingClientRect 144 const handleScroll = () => { 145 const raw = (window.scrollY - cachedRootTop) / scrollableDistance; 146 target = clamp(raw, 0, 1) * maxStage; 147 requestRender(); 148 }; 149 const handleResize = () => { updateLayout(); handleScroll(); }; 150 updateLayout(); 151 handleScroll(); 152 window.addEventListener('resize', handleResize); 153 const removeScrollUpdate = _addUpdate(handleScroll); 154 return () => { 155 if (animRafId !== 0) 156 window.cancelAnimationFrame(animRafId); 157 removeScrollUpdate(); 158 window.removeEventListener('resize', handleResize); 159 }; 160} 161function initParallax() { 162 const parallaxElements = Array.from(document.querySelectorAll('[data-landing-parallax]')); 163 if (parallaxElements.length === 0) 164 return () => { }; 165 let items = []; 166 const cachePositions = () => { 167 items = parallaxElements.map((el) => ({ 168 el, 169 speed: Number(el.dataset['landingParallax'] ?? '0'), 170 offsetTop: el.getBoundingClientRect().top + window.scrollY, 171 height: el.offsetHeight, 172 })); 173 }; 174 const update = () => { 175 const sy = window.scrollY; 176 const viewportCenter = window.innerHeight / 2; 177 for (const { el, speed, offsetTop, height } of items) { 178 const elementCenter = offsetTop - sy + height / 2; 179 const offset = Math.max(-28, Math.min(28, (viewportCenter - elementCenter) * speed)); 180 el.style.transform = `translate3d(0, ${offset.toFixed(1)}px, 0)`; 181 } 182 }; 183 const handleResize = () => { cachePositions(); }; 184 cachePositions(); 185 update(); 186 window.addEventListener('resize', handleResize); 187 const removeScrollUpdate = _addUpdate(update); 188 return () => { 189 removeScrollUpdate(); 190 window.removeEventListener('resize', handleResize); 191 }; 192} 193function initStrengthSectionExpand() { 194 const section = document.querySelector('[data-landing-strength-section]'); 195 const background = section?.querySelector('[data-landing-strength-bg]'); 196 if (!section || !background) 197 return () => { }; 198 const isDesktop = window.matchMedia('(min-width: 768px)'); 199 const COLLAPSED_SCALE = 0.68; 200 let cachedTop = 0; 201 let cachedHeight = 0; 202 let lastScale = -1; 203 const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); 204 const easeOutCubic = (value) => 1 - Math.pow(1 - value, 3); 205 const cachePosition = () => { 206 const rect = section.getBoundingClientRect(); 207 cachedTop = rect.top + window.scrollY; 208 cachedHeight = rect.height; 209 }; 210 const paint = () => { 211 if (!isDesktop.matches) { 212 if (lastScale !== 1) { 213 background.style.transform = 'translate3d(0, 0, 0) scaleX(1)'; 214 background.style.borderRadius = '0px'; 215 lastScale = 1; 216 } 217 return; 218 } 219 const top = cachedTop - window.scrollY; 220 const bottom = top + cachedHeight; 221 const entryProgress = clamp((window.innerHeight - top) / (window.innerHeight * 0.62), 0, 1); 222 const exitProgress = clamp((bottom - window.innerHeight * 0.18) / (window.innerHeight * 0.5), 0, 1); 223 const progress = easeOutCubic(Math.min(entryProgress, exitProgress)); 224 const scale = COLLAPSED_SCALE + (1 - COLLAPSED_SCALE) * progress; 225 const roundedScale = Number(scale.toFixed(3)); 226 if (roundedScale === lastScale) 227 return; 228 lastScale = roundedScale; 229 background.style.borderRadius = '32px'; 230 background.style.transform = `translate3d(0, 0, 0) scaleX(${roundedScale})`; 231 }; 232 const handleResize = () => { 233 cachePosition(); 234 lastScale = -1;
235 paint(); 236 }; 237 cachePosition(); 238 paint(); 239 window.addEventListener('resize', handleResize); 240 const removeScrollUpdate = _addUpdate(paint); 241 return () => { 242 removeScrollUpdate(); 243 window.removeEventListener('resize', handleResize); 244 background.style.removeProperty('transform'); 245 background.style.removeProperty('border-radius'); 246 background.style.removeProperty('will-change'); 247 }; 248} 249function initStickyMediaCta() { 250 const root = document.querySelector('[data-sticky-cta-root]'); 251 if (!root) 252 return () => { }; 253 const steps = Array.from(root.querySelectorAll('[data-sticky-cta-step]')); 254 const media = Array.from(root.querySelectorAll('[data-sticky-cta-media]')); 255 const sticky = root.querySelector('[data-sticky-cta-sticky]'); 256 const track = root.querySelector('[data-sticky-cta-track]'); 257 const isDesktop = window.matchMedia('(min-width: 1024px)'); 258 if (!isDesktop.matches || steps.length === 0 || media.length === 0) 259 return () => { }; 260 const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); 261 const primeMediaImages = () => { 262 root.querySelectorAll('img').forEach((image) => { 263 image.loading = 'eager'; 264 image.decoding = 'async'; 265 if (typeof image.decode === 'function') { 266 void image.decode().catch(() => { }); 267 } 268 }); 269 }; 270 const state = { 271 segmentHeight: 0, 272 rootTop: 0, 273 numItems: Math.min(steps.length, media.length), 274 stepCenters: [], 275 maxBlockShift: 0, 276 lastPaintKey: '', 277 }; 278 const updateLayout = () => { 279 state.numItems = Math.min(steps.length, media.length); 280 if (state.numItems === 0) 281 return; 282 // All reads first 283 const rootRect = root.getBoundingClientRect(); 284 state.rootTop = window.scrollY + rootRect.top; 285 state.segmentHeight = root.offsetHeight / state.numItems; 286 state.stepCenters = steps.slice(0, state.numItems).map((step) => { 287 const stepRect = step.getBoundingClientRect(); 288 return stepRect.top - rootRect.top + stepRect.height / 2; 289 }); 290 const firstCenter = state.stepCenters[0] ?? 0; 291 const lastCenter = state.stepCenters[state.stepCenters.length - 1] ?? firstCenter; 292 state.maxBlockShift = Math.max(0, lastCenter - firstCenter); 293 if (state.maxBlockShift < 8) { 294 state.maxBlockShift = state.segmentHeight * Math.max(0, state.numItems - 1) * 0.58; 295 } 296 // All writes after reads 297 if (sticky && state.segmentHeight > 0) { 298 sticky.style.height = `${state.segmentHeight.toFixed(1)}px`; 299 sticky.style.top = `calc(50vh - ${(state.segmentHeight / 2).toFixed(1)}px)`; 300 } 301 if (track) 302 track.style.willChange = 'transform'; 303 state.lastPaintKey = ''; 304 }; 305 const update = () => { 306 if (state.numItems === 0 || state.segmentHeight <= 0) 307 return; 308 // All reads first 309 const scrollAnchor = window.scrollY + window.innerHeight * 0.5; 310 // Calculations (pure math, no DOM reads) 311 const rawProgress = (scrollAnchor - state.rootTop - state.segmentHeight * 0.5) / state.segmentHeight; 312 const progress = clamp(rawProgress, 0, state.numItems - 1); 313 const activeIndex = clamp(Math.round(progress), 0, state.numItems - 1); 314 const normalizedProgress = state.numItems > 1 ? progress / (state.numItems - 1) : 0; 315 const trackShift = (normalizedProgress * 2 - 1) * 92; 316 const lowerIndex = Math.floor(progress); 317 const upperIndex = Math.min(state.numItems - 1, lowerIndex + 1); 318 const mix = progress - lowerIndex; 319 const lowerCenter = state.stepCenters[lowerIndex] ?? 0; 320 const upperCenter = state.stepCenters[upperIndex] ?? lowerCenter; 321 const interpolatedCenter = lowerCenter + (upperCenter - lowerCenter) * mix; 322 const baseCenter = state.stepCenters[0] ?? interpolatedCenter; 323 const blockShift = clamp(interpolatedCenter - baseCenter, 0, state.maxBlockShift); 324 const paintKey = [ 325 activeIndex, 326 progress.toFixed(3),
327 blockShift.toFixed(1), 328 trackShift.toFixed(1), 329 ].join('|'); 330 if (paintKey === state.lastPaintKey) 331 return; 332 state.lastPaintKey = paintKey; 333 // All writes last 334 if (track) 335 track.style.transform = `translate3d(0, ${blockShift.toFixed(1)}px, 0)`; 336 media.forEach((item, i) => { 337 const distance = i - progress; 338 const absDistance = Math.abs(distance); 339 const opacity = i === activeIndex ? 1 : 0; 340 const translateY = distance * 168 + trackShift; 341 const scale = 1 - Math.min(absDistance, 1.2) * 0.08; 342 item.style.opacity = opacity.toFixed(3); 343 item.style.transform = `translate3d(0, ${translateY.toFixed(1)}px, 0) scale(${scale.toFixed(3)})`; 344 item.style.pointerEvents = i === activeIndex ? 'auto' : 'none'; 345 item.style.zIndex = String(30 - Math.round(Math.abs(distance) * 10)); 346 }); 347 steps.forEach((step, i) => { 348 step.style.opacity = i === activeIndex ? '1' : '0.55'; 349 }); 350 }; 351 const handleResize = () => { updateLayout(); update(); }; 352 updateLayout(); 353 primeMediaImages(); 354 update(); 355 window.addEventListener('resize', handleResize); 356 const removeScrollUpdate = _addUpdate(update); 357 return () => { 358 removeScrollUpdate(); 359 window.removeEventListener('resize', handleResize); 360 }; 361} 362export function initLandingMotion() { 363 cleanupLandingMotion?.(); 364 const cleanups = [ 365 revealOnScroll(), 366 initStrengthSectionExpand(), 367 initStoryScroller(), 368 initStickyMediaCta(), 369 initParallax(), 370 ]; 371 cleanupLandingMotion = () => { 372 cleanups.forEach((cleanup) => cleanup()); 373 cleanupLandingMotion = null; 374 }; 375}
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.