1/** 2 * Home hero â pin `.home-banner` for scroll distance only: banner copy + row move up while the 3 * video scrubs from card size to 100vwÃ100vh (fixed to viewport). Extra scroll holds fullscreen 4 * before unpin so `.discovery-cta` follows naturally in document order below. 5 * 6 * Desktop only: wide viewport + fine pointer (mouse). Touch devices (hover: none, pointer: coarse) 7 * including iPad keep the static card size from CSS even at large widths. 8 * 9 * ScrollTrigger is created on `window` `load` (after Lenis + assets) so pin-spacer math matches 10 * layout; a short delayed `ScrollTrigger.refresh()` runs after native scroll restoration settles. 11 */ 12(function () { 13 'use strict'; 14 15 var section = document.querySelector('[data-home-banner-expand]'); 16 if (!section) return; 17 18 var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 19 var hasGsap = typeof window.gsap !== 'undefined' && typeof window.ScrollTrigger !== 'undefined'; 20 var mqDesktop = window.matchMedia('(min-width: 1399px) and (hover: hover) and (pointer: fine)'); 21 var ST = hasGsap ? window.ScrollTrigger : null; 22 /** @type {gsap.Context | null} */ 23 var scrollCtx = null; 24 var scrollInstalled = false; 25 26 function playHeroVideo() { 27 var videoEl = section.querySelector('.home-banner__video-el'); 28 if (videoEl && typeof videoEl.play === 'function') { 29 videoEl.play().catch(function () {}); 30 } 31 } 32 33 function scheduleRefresh() { 34 if (!ST || !scrollInstalled) return; 35 if (scheduleRefresh._t) clearTimeout(scheduleRefresh._t); 36 scheduleRefresh._t = setTimeout(function () { 37 scheduleRefresh._t = null; 38 ST.refresh(); 39 }, 100); 40 } 41 42 /** 43 * Ancestors with transform/filter/will-change break `position: fixed` (video card â viewport). 44 * `.fade-effect` uses transform; pin also applies transforms â use transform pin + reparent. 45 */ 46 function pinOptionsForHero() { 47 return { 48 pinType: 'transform', 49 pinReparent: true 50 }; 51 } 52 53 function teardownScrollEffects() { 54 if (scrollCtx) { 55 scrollCtx.revert(); 56 scrollCtx = null; 57 } 58 scrollInstalled = false; 59 60 if (!hasGsap) return; 61 62 var videoWrap = section.querySelector('.home-banner__video-wrap'); 63 var stack = section.querySelector('.home-banner__stack'); 64 var aside = section.querySelector('.home-banner__aside'); 65 if (videoWrap) window.gsap.set(videoWrap, { clearProps: 'all' }); 66 if (stack) window.gsap.set(stack, { clearProps: 'marginTop' }); 67 if (aside) window.gsap.set(aside, { clearProps: 'marginTop' }); 68 if (ST) ST.refresh(); 69 } 70 71 function initScrollEffects() { 72 if (scrollInstalled || !hasGsap || reducedMotion || !mqDesktop.matches) return; 73 74 var videoSlot = section.querySelector('.home-banner__video-slot'); 75 var videoWrap = section.querySelector('.home-banner__video-wrap'); 76 var stack = section.querySelector('.home-banner__stack'); 77 var aside = section.querySelector('.home-banner__aside'); 78 if (!videoSlot || !videoWrap) return; 79 80 playHeroVideo(); 81 window.gsap.registerPlugin(ST); 82 83 function slotRect() { 84 return videoSlot.getBoundingClientRect(); 85 } 86 87 var startBorderRadius = window.getComputedStyle(videoWrap).borderTopLeftRadius || '0px'; 88 89 var expandDur = 1; 90 var holdDur = 0.65; 91 var vh = function () { 92 return window.innerHeight || 800; 93 }; 94 95 function viewportHeightPx() { 96 var vv = window.visualViewport; 97 if (vv && typeof vv.height === 'number' && vv.height > 0) return Math.round(vv.height); 98 var h = window.innerHeight; 99 if (typeof h === 'number' && h > 0) return Math.round(h); 100 return 800; 101 } 102 103 var pinOpts = pinOptionsForHero(); 104 105 scrollCtx = window.gsap.context(function () { 106 var tl = window.gsap.timeline({ 107 defaults: { ease: 'none' } 108 }); 109 110 tl.fromTo( 111 videoWrap, 112 { 113 position: 'fixed', 114 zIndex: 1020, 115 top: function () { 116 return slotRect().top; 117 }, 118 left: function () { 119 return slotRect().left; 120 }, 121 width: function () { 122 return slotRect().width; 123 }, 124 height: function () { 125 return slotRect().height; 126 }, 127 borderRadius: startBorderRadius, 128 immediateRender: false 129 }, 130 { 131 top: 0, 132 left: 0, 133 width: '100vw', 134 height: function () {
135 return viewportHeightPx() + 2 + 'px'; 136 }, 137 borderRadius: 0, 138 immediateRender: false, 139 duration: expandDur 140 }, 141 0 142 ); 143 144 if (stack) { 145 tl.to( 146 stack, 147 { 148 marginTop: function () { 149 return -Math.min(vh() * 0.42, stack.offsetHeight + 40); 150 }, 151 duration: expandDur 152 }, 153 0 154 ); 155 } 156 157 if (aside) { 158 tl.to( 159 aside, 160 { 161 marginTop: function () { 162 return -Math.min(vh() * 0.35, aside.offsetHeight + 24); 163 }, 164 duration: expandDur 165 }, 166 0 167 ); 168 } 169 170 tl.to({}, { duration: holdDur }); 171 172 ST.create({ 173 animation: tl, 174 trigger: section, 175 start: 'top top', 176 end: function () { 177 return '+=' + String(Math.round(vh() * (1.45 + 0.95))); 178 }, 179 scrub: true, 180 pin: true, 181 pinSpacing: true, 182 anticipatePin: 1, 183 invalidateOnRefresh: true, 184 pinType: pinOpts.pinType, 185 pinReparent: pinOpts.pinReparent 186 }); 187 }, section); 188 189 scrollInstalled = true; 190 if (ST) ST.refresh(); 191 } 192 193 function applyLayoutMode() { 194 if (reducedMotion || !hasGsap) { 195 playHeroVideo(); 196 return; 197 } 198 199 if (mqDesktop.matches) { 200 initScrollEffects(); 201 } else { 202 teardownScrollEffects(); 203 playHeroVideo(); 204 } 205 } 206 207 window.addEventListener('resize', scheduleRefresh, { passive: true }); 208 if (window.visualViewport) { 209 window.visualViewport.addEventListener('resize', scheduleRefresh, { passive: true }); 210 } 211 if (typeof ResizeObserver !== 'undefined' && document.body) { 212 var ro = new ResizeObserver(scheduleRefresh); 213 ro.observe(document.body); 214 } 215 if (typeof mqDesktop.addEventListener === 'function') { 216 mqDesktop.addEventListener('change', applyLayoutMode); 217 } else if (typeof mqDesktop.addListener === 'function') { 218 mqDesktop.addListener(applyLayoutMode); 219 } 220 221 function bootOnLoad() { 222 function start() { 223 applyLayoutMode(); 224 setTimeout(function () { 225 if (ST && scrollInstalled) ST.refresh(); 226 }, 220); 227 } 228 229 if (document.readyState === 'complete') { 230 start(); 231 } else { 232 window.addEventListener('load', start, { once: true }); 233 } 234 } 235 236 bootOnLoad(); 237})();
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.