1/* lisamlum.com homepage experience layer. 2 GSAP + ScrollTrigger + Lenis arrive via deferred CDN tags before this file. 3 Contract: if anything is missing, or reduced motion is set, every element 4 must end up visible with zero animation. */ 5(function () { 6 'use strict'; 7 var root = document.documentElement; 8 9 function showEverything() { 10 root.classList.remove('has-motion'); 11 root.classList.add('wm-ready'); 12 document.querySelectorAll('.fade-in').forEach(function (el) { 13 el.classList.add('visible'); 14 }); 15 } 16 17 var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 18 if (reduced || !window.gsap || !window.ScrollTrigger) { 19 showEverything(); 20 return; 21 } 22 23 gsap.registerPlugin(ScrollTrigger); 24 root.classList.add('has-motion'); 25 26 /* The shared components.js fade-in observer must not fight GSAP on this 27 page: mark its targets visible immediately so it no-ops. */ 28 document.querySelectorAll('.fade-in').forEach(function (el) { 29 el.classList.add('visible'); 30 }); 31 32 var isMobile = window.matchMedia('(max-width: 640px)').matches; 33 var EASE = 'power3.out'; 34 35 /* ââ Lenis smooth scroll: fine pointers only ââ */ 36 var lenis = null; 37 if (window.Lenis && window.matchMedia('(hover: hover) and (pointer: fine)').matches) { 38 lenis = new Lenis({ duration: 1.1, smoothWheel: true }); 39 lenis.on('scroll', ScrollTrigger.update); 40 gsap.ticker.add(function (t) { lenis.raf(t * 1000); }); 41 gsap.ticker.lagSmoothing(0); 42 document.addEventListener('click', function (e) { 43 var a = e.target.closest ? e.target.closest('a[href^="#"]') : null; 44 if (!a) return; 45 var hash = a.getAttribute('href'); 46 if (hash.length < 2) return; 47 var target = document.querySelector(hash); 48 if (!target) return; 49 e.preventDefault(); 50 lenis.scrollTo(target, { offset: -80 }); 51 }); 52 } 53 54 /* ââ Hero cinematic entrance ââ */ 55 function heroEntrance() { 56 var tagline = document.querySelector('.hero .h1-tagline'); 57 if (tagline) { 58 var html = ''; 59 Array.prototype.forEach.call(tagline.childNodes, function (n) { 60 if (n.nodeType === 3) { 61 n.textContent.split(/\s+/).forEach(function (w) { 62 if (w) html += '<span class="wm"><span class="w">' + w + '</span></span> '; 63 }); 64 } else if (n.nodeType === 1) { 65 html += '<span class="wm"><span class="w">' + n.outerHTML + '</span></span> '; 66 } 67 }); 68 tagline.innerHTML = html; 69 } 70 var words = document.querySelectorAll('.hero .h1-tagline .w'); 71 var tl = gsap.timeline({ defaults: { ease: EASE } }); 72 tl.fromTo('.hero-bg', { scale: 1.06 }, 73 { scale: 1, duration: isMobile ? 1.2 : 1.8, ease: 'power2.out' }, 0) 74 .fromTo('.hero .h1-eyebrow', { opacity: 0, y: 12 }, 75 { opacity: 1, y: 0, duration: 0.5 }, 0.15) 76 .to('.h1-rule', { scaleX: 1, duration: 0.5, ease: 'power2.inOut' }, 0.3) 77 .set('.hero .h1-tagline', { opacity: 1 }, 0.35) 78 .fromTo(words, { yPercent: 130 }, 79 { yPercent: 0, duration: isMobile ? 0.7 : 1.0, stagger: 0.05 }, 0.35) 80 .fromTo(['.hero .h1-geo', '.hero-rating'], { opacity: 0, y: 16 }, 81 { opacity: 1, y: 0, duration: 0.5, stagger: 0.08 }, isMobile ? 0.65 : 0.95) 82 .fromTo(['.hero-ctas', '.hero-proof'], { opacity: 0, y: 16 }, 83 { opacity: 1, y: 0, duration: 0.5, stagger: 0.08 }, isMobile ? 0.75 : 1.1); 84 } 85 86 /* ââ Shared reveal helpers ââ */ 87 function rise(targets, trigger, stagger, y) { 88 /* Never hide anything already in the viewport: late repaints become the 89 LCP and read as jank. Reveals apply only below the current viewport. */ 90 var els = gsap.utils.toArray(targets).filter(function (el) { 91 return el.getBoundingClientRect().top > window.innerHeight * 0.9; 92 }); 93 if (!els.length) return; 94 gsap.fromTo(els, { opacity: 0, y: y || (isMobile ? 18 : 28) }, 95 { opacity: 1, y: 0, duration: 0.8, ease: EASE, stagger: stagger || 0, 96 scrollTrigger: { trigger: trigger || els[0], start: 'top 85%', once: true } }); 97 } 98 99 function animateCount(el) { 100 var m = el.textContent.trim().match(/^(\$)?([\d][\d.,]*)([A-Za-z+%]*)$/); 101 if (!m) return; 102 var prefix = m[1] || ''; 103 var clean = m[2].replace(/,/g, ''); 104 var target = parseFloat(clean); 105 var suffix = m[3] || ''; 106 var decimals = (clean.split('.')[1] || '').length; 107 var state = { v: 0 }; 108 gsap.to(state, { v: target, duration: 1.2, ease: 'power1.out', 109 onUpdate: function () { el.textContent = prefix + state.v.toFixed(decimals) + suffix; }, 110 onComplete: function () { el.textContent = prefix + m[2] + suffix; } }); 111 } 112 113 function countOnEnter(el) { 114 ScrollTrigger.create({ trigger: el, start: 'top 88%', once: true, 115 onEnter: function () { animateCount(el); } }); 116 } 117 118 function drawRules() { 119 /* Same viewport rule as reveals: never animate a rule already on screen. */ 120 gsap.utils.toArray('.rule-draw').filter(function (el) { 121 return el.getBoundingClientRect().top > window.innerHeight * 0.9; 122 }
122).forEach(function (el) { 123 gsap.fromTo(el, { scaleX: 0 }, { scaleX: 1, duration: 0.6, ease: 'power2.inOut', 124 scrollTrigger: { trigger: el, start: 'top 88%', once: true } }); 125 }); 126 } 127 128 function featherChapter() { 129 var section = document.querySelector('.recent-result'); 130 if (!section) return; 131 132 function compactReveal() { 133 rise('.recent-result-panel', '.recent-result', 0, 36); 134 if (document.querySelector('.recent-result-bg')) { 135 gsap.fromTo('.recent-result-bg', { yPercent: -6 }, { yPercent: 6, ease: 'none', 136 scrollTrigger: { trigger: '.recent-result', start: 'top bottom', end: 'bottom top', scrub: true } }); 137 } 138 } 139 140 /* If the section is already in view at init (deep-scroll restore), never 141 pin or hide it: standing viewport rule. */ 142 if (section.getBoundingClientRect().top <= window.innerHeight * 0.9) { 143 return; 144 } 145 146 /* Non-pinned, once-only reveal for all viewports. The previous desktop 147 path pinned + scrubbed this section; combined with Lenis smooth scroll 148 that pin could desync and trap the timeline at progress 0, leaving the 149 navy panel visible with its text stuck at opacity 0. The compact reveal 150 can't get stuck: the panel rises in once and stays visible. */ 151 compactReveal(); 152 } 153 154 /* ââ Section choreography ââ */ 155 function sections() { 156 rise('.numbers-strip p', '.numbers-strip'); 157 rise('.intro h2', '.intro'); 158 rise('.intro-body p', '.intro-body', 0.12); 159 rise('.intro-stats > div', '.intro-stats', 0.1, 20); 160 document.querySelectorAll('.intro-stat-num').forEach(countOnEnter); 161 162 gsap.utils.toArray('.services-grid').forEach(function (grid) { 163 var cards = grid.querySelectorAll('.service-card'); 164 gsap.fromTo(cards, { opacity: 0, y: 32 }, 165 { opacity: 1, y: 0, duration: 0.8, ease: EASE, stagger: 0.12, 166 scrollTrigger: { trigger: grid, start: 'top 85%', once: true } }); 167 gsap.fromTo(grid.querySelectorAll('.service-num'), { x: -20, opacity: 0 }, 168 { x: 0, opacity: 1, duration: 0.6, ease: EASE, stagger: 0.12, 169 scrollTrigger: { trigger: grid, start: 'top 85%', once: true } }); 170 }); 171 172 rise('.hr-feature-inner > *', '.hr-feature', 0.1); 173 gsap.fromTo('.hr-feature .pillars span', { opacity: 0, y: 14 }, 174 { opacity: 1, y: 0, duration: 0.5, ease: EASE, stagger: 0.08, 175 scrollTrigger: { trigger: '.hr-feature .pillars', start: 'top 90%', once: true } }); 176 var hrBg = document.querySelector('.hr-feature'); 177 if (hrBg && getComputedStyle(hrBg).backgroundImage !== 'none') { 178 gsap.fromTo(hrBg, { backgroundPositionY: '40%' }, { backgroundPositionY: '60%', ease: 'none', 179 scrollTrigger: { trigger: hrBg, start: 'top bottom', end: 'bottom top', scrub: true } }); 180 } 181 182 /* Home Refresh compare slider: sweep the divider in on first view so 183 visitors discover it is draggable. home-cinema.js owns the control. */ 184 var hrcFrame = document.querySelector('.hrc-frame'); 185 if (hrcFrame && hrcFrame.getBoundingClientRect().top > window.innerHeight * 0.9) { 186 ScrollTrigger.create({ trigger: '.hr-compare', start: 'top 75%', once: true, 187 onEnter: function () { 188 if (!hrcFrame.__setPos) return; 189 var state = { v: 88 }; 190 hrcFrame.__setPos(88); 191 gsap.to(state, { v: 50, duration: 1.4, ease: 'power2.inOut', 192 onUpdate: function () { hrcFrame.__setPos(state.v); } }); 193 } }); 194 } 195 196 /* How a sale unfolds */ 197 rise('.sc-header > *', '.sale-chapters', 0.08); 198 var scCards = gsap.utils.toArray('.sc-card'); 199 if (scCards.length) { 200 gsap.fromTo(scCards, { opacity: 0, y: 32 }, 201 { opacity: 1, y: 0, duration: 0.8, ease: EASE, stagger: 0.1, 202 scrollTrigger: { trigger: '.sc-grid', start: 'top 85%', once: true } }); 203 gsap.fromTo('.sc-num', { x: -16, opacity: 0 }, 204 { x: 0, opacity: 1, duration: 0.6, ease: EASE, stagger: 0.1, 205 scrollTrigger: { trigger: '.sc-grid', start: 'top 85%', once: true } }); 206 } 207 rise('.sc-cta > *', '.sc-cta', 0.08, 16); 208 209 /* Community switcher */ 210 rise('.cs-item', '.community-switcher', 0.05, 18); 211 var csStage = document.querySelector('.cs-stage'); 212 if (csStage && csStage.getBoundingClientRect().top > window.innerHeight * 0.9) { 213 gsap.fromTo(csStage, { opacity: 0, clipPath: 'inset(6% 6% 6% 6%)' }, 214 { opacity: 1, clipPath: 'inset(0% 0% 0% 0%)', duration: 0.9, ease: EASE, 215 scrollTrigger: { trigger: '.community-switcher', start: 'top 80%', once: true } }); 216 } 217 218 rise('.communities-header', '.communities'); 219 var tiles = gsap.utils.toArray('.community-grid > *'); 220 if (tiles.length) { 221 gsap.fromTo(tiles, { opacity: 0, clipPath: 'inset(10% 10% 10% 10%)' }, 222 { opacity: 1, clipPath: 'inset(0% 0% 0% 0%)', duration: 0.8, ease: EASE, stagger: 0.07, 223 scrollTrigger: { trigger: '.community-grid', start: 'top 85%', once: true } }); 224 } 225 226 rise('.market-widget-header', '.market-widget'); 227 var mwHero = document.querySelector('[data-mw="hero"]'); 228 if (mwHero) { 229 ScrollTrigger.create({ trigger: '#market-widget', start: 'top 80%', once: true, 230 onEnter: function () { 231 var els = mwHero.querySelectorAll('*'); 232 for (var i = 0; i < els.length; i++) { 233 if (els[i].children.length === 0 && /^\$[\d.,]+[MK]?$/.test(els[i].textContent.trim())) { 234 animateCount(els[i]); 235 return; 236 } 237 } 238 } }); 239 } 240 241 if (document.querySelector('.about-image')) { 242 gsap.fromTo('.about-image', { clipPath: 'inset(0 100% 0 0)' }, 243 { clipPath: 'inset(0 0% 0 0)', duration: 1.0, ease: 'power3.inOut', 244 scrollTrigger: { trigger: '.about', start: 'top 80%', once: true } }); 245 } 246 rise('.about-content > *', '.about-content', 0.08, 20); 247 248 gsap.fromTo('.brand-film-frame', { opacity: 0, scale: 0.97 }, 249 { opacity: 1, scale: 1, duration: 0.9, ease: EASE, 250 scrollTrigger: { trigger: '.brand-film', start: 'top 80%', once: true } }); 251 rise('.brand-film .intro-label, .brand-film-title', '.brand-film', 0.08); 252 253 rise('.testimonials-header', '.testimonials');
254 rise('.testi-col', '.testi-marquee', 0.12, 24); 255 256 rise('.blog-header', '.blog'); 257 rise('.blog-card', '.blog-grid', 0.1, 30); 258 259 rise('.cta-inner > *', '.cta', 0.08, 20); 260 } 261 262 function init() { 263 drawRules(); 264 featherChapter(); 265 heroEntrance(); 266 sections(); 267 window.addEventListener('load', function () { 268 ScrollTrigger.refresh(); 269 root.classList.add('wm-ready'); 270 }); 271 var lazyImg = document.querySelector('.recent-result-bg'); 272 if (lazyImg && !lazyImg.complete) { 273 lazyImg.addEventListener('load', function () { ScrollTrigger.refresh(); }, { once: true }); 274 } 275 } 276 277 if (document.readyState === 'loading') { 278 document.addEventListener('DOMContentLoaded', init); 279 } else { 280 init(); 281 } 282})();
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.