1document.addEventListener('DOMContentLoaded', () => { 2 3 // ===== MOBILE HAMBURGER ===== 4 const hamburger = document.getElementById('hamburger'); 5 const mobileNav = document.getElementById('mobileNav'); 6 7 if (hamburger && mobileNav) { 8 hamburger.addEventListener('click', () => { 9 hamburger.classList.toggle('active'); 10 mobileNav.classList.toggle('open'); 11 }); 12 13 mobileNav.querySelectorAll('a').forEach(link => { 14 link.addEventListener('click', () => { 15 hamburger.classList.remove('active'); 16 mobileNav.classList.remove('open'); 17 }); 18 }); 19 } 20 21 // ===== DROPDOWN TOGGLE ===== 22 const dropdownToggles = document.querySelectorAll('.dropdown-toggle'); 23 24 function closeAllDropdowns() { 25 document.querySelectorAll('.nav-dropdown.open').forEach(d => { 26 d.classList.remove('open'); 27 d.querySelector('.dropdown-toggle').setAttribute('aria-expanded', 'false'); 28 }); 29 } 30 31 dropdownToggles.forEach(toggle => { 32 toggle.addEventListener('click', e => { 33 e.stopPropagation(); 34 const dropdown = toggle.closest('.nav-dropdown'); 35 const isOpen = dropdown.classList.contains('open'); 36 closeAllDropdowns(); 37 if (!isOpen) { 38 dropdown.classList.add('open'); 39 toggle.setAttribute('aria-expanded', 'true'); 40 } 41 }); 42 }); 43 44 document.querySelectorAll('.dropdown-menu a').forEach(link => { 45 link.addEventListener('click', closeAllDropdowns); 46 }); 47 48 document.addEventListener('click', e => { 49 if (!e.target.closest('.nav-dropdown')) { 50 closeAllDropdowns(); 51 } 52 }); 53 54 document.addEventListener('keydown', e => { 55 if (e.key === 'Escape') closeAllDropdowns(); 56 }); 57 58 // ===== HEADER SCROLL SHADOW ===== 59 const header = document.querySelector('.site-header'); 60 61 window.addEventListener('scroll', () => { 62 if (window.scrollY > 50) { 63 header.classList.add('scrolled'); 64 } else { 65 header.classList.remove('scrolled'); 66 } 67 }); 68 69 // ===== HERO DEVICE TYPING EFFECT ===== 70 const typingText = document.querySelector('.hdf-typing-text'); 71 if (typingText) { 72 const isMobile = window.innerWidth < 768; 73 const typeSpeed = isMobile ? 100 : 55; 74 const deleteSpeed = isMobile ? 60 : 28; 75 const phrases = [ 76 { segs: [['lead generation', true], [' machine.', false]] }, 77 { segs: [['call', true], [' magnet.', false]] }, 78 { segs: [['booking', true], [' engine.', false]] }, 79 { segs: [['client conversion', true], [' system.', false]] }, 80 { segs: [['Get found. Get called. ', false], ['Get paid.', true]] }, 81 { segs: [['Turn clicks into ', false], ['customers.', true]] }, 82 { segs: [['Dominate your ', false], ['local market.', true]] }, 83 { segs: [['Turn your site into ', false], ['revenue machine.', true]] }, 84 ]; 85 86 function getFullText(segs) { return segs.map(s => s[0]).join(''); } 87 function buildHTML(segs, count) { 88 let html = '', rem = count; 89 for (const [text, hl] of segs) { 90 if (rem <= 0) break; 91 const take = Math.min(rem, text.length); 92 const part = text.substring(0, take); 93 html += hl ? '<span style="color:var(--orange)">' + part + '</span>' : part; 94 rem -= take; 95 } 96 return html; 97 } 98 99 let phraseIndex = 0; 100 let charIndex = 0; 101 let isDeleting = false; 102 let isPaused = false; 103 104 function typeLoop() { 105 const currentPhrase = phrases[phraseIndex]; 106 const fullText = getFullText(currentPhrase.segs); 107 108 if (isPaused) { 109 setTimeout(typeLoop, 2200); 110 isPaused = false; 111 return; 112 } 113 114 if (!isDeleting) { 115 typingText.innerHTML = buildHTML(currentPhrase.segs, charIndex + 1); 116 charIndex++; 117 if (charIndex === fullText.length) { 118 isPaused = true; 119 isDeleting = true; 120 setTimeout(typeLoop, 2800); 121 return; 122 } 123 setTimeout(typeLoop, typeSpeed); 124 } else { 125 typingText.innerHTML = buildHTML(currentPhrase.segs, charIndex - 1); 126 charIndex--; 127 if (charIndex === 0) { 128 isDeleting = false; 129 phraseIndex = (phraseIndex + 1) % phrases.length; 130 } 131 setTimeout(typeLoop, deleteSpeed); 132 } 133 } 134 135 typeLoop(); 136 } 137 138 // ===== HERO DEVICE LEAD COUNTER ===== 139 const leadCounter = document.querySelector('.hdf-lc-num'); 140 if (leadCounter) { 141 let started = false;
142 143 function startLeadCounter() { 144 if (started) return; 145 started = true; 146 147 const target = parseInt(leadCounter.getAttribute('data-count')); 148 const duration = 1500; 149 const startTime = performance.now(); 150 151 function update(currentTime) { 152 const elapsed = currentTime - startTime; 153 const progress = Math.min(elapsed / duration, 1); 154 const eased = 1 - Math.pow(1 - progress, 3); 155 leadCounter.textContent = Math.floor(eased * target); 156 if (progress < 1) requestAnimationFrame(update); 157 else leadCounter.textContent = target; 158 } 159 160 requestAnimationFrame(update); 161 } 162 163 const device = document.querySelector('.hero-device-frame'); 164 if (device) { 165 const observer = new IntersectionObserver((entries) => { 166 entries.forEach(entry => { 167 if (entry.isIntersecting) { startLeadCounter(); observer.unobserve(entry.target); } 168 }); 169 }, { threshold: 0.3 }); 170 observer.observe(device); 171 } 172 } 173 174 // ===== INTERSECTION OBSERVER (Scroll Animations) ===== 175 const observerOptions = { 176 threshold: 0.08, 177 rootMargin: '0px 0px -40px 0px' 178 }; 179 180 const observer = new IntersectionObserver((entries) => { 181 entries.forEach(entry => { 182 if (entry.isIntersecting) { 183 entry.target.classList.add('is-visible'); 184 } 185 }); 186 }, observerOptions); 187 188 document.querySelectorAll('.reveal').forEach(el => observer.observe(el)); 189 190 // ===== SERVICE ACCORDION ===== 191 const svcItems = document.querySelectorAll('.svc-item'); 192 193 svcItems.forEach(item => { 194 const header = item.querySelector('.svc-header'); 195 header.addEventListener('click', () => { 196 const isOpen = item.classList.contains('open'); 197 198 svcItems.forEach(i => { 199 i.classList.remove('open'); 200 i.querySelector('.svc-header').setAttribute('aria-expanded', 'false'); 201 }); 202 203 if (!isOpen) { 204 item.classList.add('open'); 205 item.querySelector('.svc-header').setAttribute('aria-expanded', 'true'); 206 } 207 }); 208 }); 209 210 // ===== FAQ ACCORDION ===== 211 const faqItems = document.querySelectorAll('.faq-item'); 212 213 faqItems.forEach(item => { 214 const question = item.querySelector('.faq-question'); 215 question.addEventListener('click', () => { 216 const isOpen = item.classList.contains('open'); 217 218 faqItems.forEach(i => { 219 i.classList.remove('open'); 220 i.querySelector('.faq-question').setAttribute('aria-expanded', 'false'); 221 }); 222 223 if (!isOpen) { 224 item.classList.add('open'); 225 item.querySelector('.faq-question').setAttribute('aria-expanded', 'true'); 226 } 227 }); 228 }); 229 230 // ===== SMOOTH SCROLL FOR ANCHOR LINKS ===== 231 document.querySelectorAll('a[href^="#"]').forEach(anchor => { 232 anchor.addEventListener('click', (e) => { 233 const href = anchor.getAttribute('href'); 234 if (href === '#') return; 235 const target = document.querySelector(href); 236 if (target) { 237 e.preventDefault(); 238 const headerHeight = header ? header.offsetHeight : 68; 239 const targetPosition = target.getBoundingClientRect().top + window.scrollY - headerHeight; 240 window.scrollTo({ 241 top: targetPosition, 242 behavior: 'smooth' 243 }); 244 } 245 }); 246 }); 247 248 // ===== ACTIVE NAV LINK ON SCROLL (cached offsets, rAF-throttled) ===== 249 const sections = document.querySelectorAll('section[id]'); 250 const navAnchors = document.querySelectorAll('.desktop-nav a'); 251 let sectionOffsets = []; 252 253 function cacheOffsets() { 254 sectionOffsets = Array.from(sections, s => ({ id: s.getAttribute('id'), top: s.offsetTop - 120 })); 255 } 256 257 if (sections.length && navAnchors.length) { 258 cacheOffsets(); 259 window.addEventListener('resize', cacheOffsets); 260 let ticking = false; 261 window.addEventListener('scroll', () => { 262 if (!ticking) { 263 requestAnimationFrame(() => { 264 let current = ''; 265 const sy = window.scrollY; 266 for (const s of sectionOffsets) { 267 if (sy >= s.top) current = s.id; 268 } 269 navAnchors.forEach(link => { 270 const linkHref = link.getAttribute('href').replace('#', ''); 271 if (linkHref === current) { 272 link.style.color = '#11110f'; 273 link.style.fontWeight = '700'; 274 } else { 275 link.style.color = ''; 276 link.style.fontWeight = ''; 277 } 278 }); 279 ticking = false;
280 }); 281 ticking = true; 282 } 283 }); 284 } 285 286 // ===== SERVICE CARD GLOW ON TOUCH (only one at a time) ===== 287 if (matchMedia('(hover: none)').matches) { 288 const cards = document.querySelectorAll('.service-card'); 289 const ratios = new Map(); 290 const obs = new IntersectionObserver((entries) => { 291 entries.forEach(e => ratios.set(e.target, e.intersectionRatio)); 292 let best = null, bestR = 0; 293 ratios.forEach((r, c) => { if (r > bestR) { bestR = r; best = c; } }); 294 cards.forEach(c => c.classList.toggle('sc-visible', c === best && bestR > 0)); 295 }, { threshold: [0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1] }); 296 cards.forEach(c => obs.observe(c)); 297 } 298 299 // ===== RESULTS COUNTER ===== 300 const resultsNumbers = document.querySelectorAll('.results-number'); 301 if (resultsNumbers.length) { 302 const countUp = (el) => { 303 const raw = el.dataset.target; 304 const target = parseFloat(raw); 305 const prefix = el.dataset.prefix || ''; 306 const suffix = el.dataset.suffix || ''; 307 const duration = 1800; 308 const start = performance.now(); 309 const isMillions = target >= 1000000; 310 const step = (now) => { 311 const p = Math.min((now - start) / duration, 1); 312 const eased = 1 - Math.pow(1 - p, 3); 313 const val = Math.round(eased * target); 314 let display; 315 if (isMillions) { 316 display = (val / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'; 317 } else { 318 display = val; 319 } 320 el.textContent = prefix + display + suffix; 321 if (p < 1) requestAnimationFrame(step); 322 }; 323 requestAnimationFrame(step); 324 }; 325 const resObs = new IntersectionObserver((entries) => { 326 entries.forEach(e => { 327 if (!e.isIntersecting) return; 328 countUp(e.target); 329 resObs.unobserve(e.target); 330 }); 331 }, { threshold: 0.4 }); 332 resultsNumbers.forEach(n => resObs.observe(n)); 333 } 334 335});
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.