(function () { // // One lead flow for every form on the site. A form opts in with // data-lead-form="
" (and optionally data-industry=""), and must // contain an input named "website" inside a
})(); ; (function () { /* Scroll runtime: each [data-scene] gets its progress written to --p (0 → 1) and CSS does the rest. Pinned scenes are [data-pin=""] (data-pin-m on phones): a tall track around a sticky stage. Only on-screen scenes are measured, in one rAF loop; native scrolling is never hijacked. ?tier=static or prefers-reduced-motion shows every scene in its final, readable state. */ (function(){ const root = document.getElementById('sbm'); // The platform runs this script once while the HTML parses and again after it mounts. Whichever run // sees this element first sets it up; the flag lives on the element, so a re-rendered copy starts fresh. if (!root || root.dataset.ready) return; root.dataset.ready = '1'; const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v)); const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches || matchMedia('(max-width: 999px)').matches || new URLSearchParams(location.search).get('tier') === 'static'; root.dataset.tier = reduce ? 'static' : 'full'; // The tier is chosen once, here. Crossing the 1000px line or flipping the reduced-motion setting while the page // is open would leave the wrong tier running, so the page reloads and chooses again. let retier = 0; const onTierChange = () => { clearTimeout(retier); retier = setTimeout(() => location.reload(), 250); }; matchMedia('(max-width: 999px)').addEventListener('change', onTierChange); matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', onTierChange); root.classList.remove('no-js', 'booting'); root.classList.add('js'); const $ = (s, r = document) => r.querySelector(s); const $$ = (s, r = document) => [...r.querySelectorAll(s)]; const scenes = $$('[data-scene]'); const listeners = new Map(); const on = (el, fn) => { if (reduce) { fn(1); return; } (listeners.get(el) || listeners.set(el, []).get(el)).push(fn); }; function layout() { const phone = innerWidth < 760; scenes.forEach(el => { const steps = reduce ? 0 : parseFloat((phone && el.dataset.pinM !== undefined ? el.dataset.pinM : el.dataset.pin) || 0); if (steps > 0) { el.style.height = (steps + 1) * 100 + 'svh'; el.classList.add('is-pinned'); } else { el.style.height = ''; el.classList.remove('is-pinned'); } }); } layout(); scenes.forEach(el => el.style.setProperty('--p', reduce ? '1' : '0')); /* scroll-lit words: every word and logo in a [data-lit] block gets an index; CSS lights them from --p */ $$('[data-lit]').forEach(box => { let i = 0; (function walk(node, hl) { [...node.childNodes].forEach(n => { if (n.nodeType === 3) { const frag = document.createDocumentFragment(); n.textContent.split(/(\s+)/).forEach(t => { if (!t) return; if (/^\s+$/.test(t)) { frag.appendChild(document.createTextNode(t)); return; } const w = document.createElement('span'); w.className = 'w' + (hl ? ' hl' : ''); w.style.setProperty('--i', i++); w.textContent = t; frag.appendChild(w); }); n.replaceWith(frag); } else if (n.nodeType === 1) { if (n.classList.contains('ic')) n.style.setProperty('--i', i++); else walk(n, hl || n.classList.contains('last')); } }); })(box, false); box.closest('[data-scene]').style.setProperty('--n', i); }); if (!reduce) { const active = new Set(); const io = new IntersectionObserver(es => { es.forEach(e => e.isIntersecting ? active.add(e.target) : active.delete(e.target)); kick(); }, { rootMargin: '20% 0px 20% 0px' }); scenes.forEach(el => io.observe(el)); const progressOf = el => { const r = el.getBoundingClientRect(), vh = innerHeight; if (el.classList.contains('is-pinned')) return clamp(-r.top / Math.max(1, r.height - vh)); return clamp((vh - r.top) / (vh + r.height)); }; let ticking = false; function frame() { ticking = false; active.forEach(el => { const p = progressOf(el); if (el._p === undefined || Math.abs(el._p - p) > 0.0004) { el._p = p; el.style.setProperty('--p', p.toFixed(4)); (listeners.get(el) || []).forEach(f => f(p)); } }); } function kick() { if (!ticking) { ticking = true; requestAnimationFrame(frame); } } addEventListener('scroll', kick, { passive: true }); addEventListener('resize', () => { layout(); kick(); }); kick(); } /* one-shot reveals and count-ups */ const rio = new IntersectionObserver(es => es.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); rio.unobserve(e.target); } }), { threshold: .15 }); $$('[data-reveal]').forEach(e => reduce ? e.classList.add('in') : rio.observe(e)); const ease = t => (t < .5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2); $$('[data-count]').forEach(el => { const to = parseFloat(el.dataset.count), pre = el.dataset.pre || '', suf = el.dataset.suf || ''; if (reduce) { el.textContent = pre + to.toLocaleString('en-US') + suf; return; } el.textContent = pre + '0' + suf; new IntersectionObserver((es, o) => es.forEach(e => { if (!e.isIntersecting) return; o.disconnect(); const t0 = performance.now(); let lastPaint = 0, lastValue = -1; (function tick(now) { const k = ease(clamp((now - t0) / 1500)), value = Math.round(to * k); // Text changes trigger layout. Keep the count-up motion, but cap DOM // writes at 30fps so it cannot compete with touch scrolling on phones. if (k >= 1 || (now - lastPaint >= 32 && value !== lastValue)) { lastPaint = now; lastValue = value; el.textContent = pre + value.toLocaleString('en-US') + suf; } if (k < 1) requestAnimationFrame(tick); })(t0); }), { threshold: .6 }).observe(el); }); /* ring: hold each card at the front for a beat, then turn */ const ringScene = $('.ring-scene'), names = ['Website & hosting', 'SEO on Google', 'AI SEO', 'Readable by Meta Muse', 'Google Business Profile', 'Chat & WhatsApp', 'CRM & follow-ups', 'Reviews & referrals', 'Outbound email']; const N = names.length, rcs = $$('.rc'); let lastIdx = -1, target = 0, shown = 0, raf = 0; /* The rotation eases toward the scroll position instead of snapping to it, so a fast swipe still turns through every card in order. */ function showRing(r) { ringScene.style.setProperty('--r', r.toFixed(4)); const idx = Math.round(r); if (idx !== lastIdx) { lastIdx = idx; rcs.forEach((a, i) => a.classList.toggle('on', i === idx)); } } let lastRingTime = 0; function ringTick(now) { const dt = lastRingTime ? Math.min(50, now - lastRingTime) : 16.67; lastRingTime = now; raf = 0; const d = target - shown; if (Math.abs(d) < .003) shown = target; else { shown += d * (1 - Math.pow(.86, dt / 16.67)); raf = requestAnimationFrame(ringTick); } showRing(shown); } function setRing(p) { const x = clamp(p * (N - 1 + .3) - .15, 0, N - 1), k = Math.min(N - 2, Math.floor(x)), f = x - k, hold = .26; const g = ease(clamp((f - hold) / (1 - hold * 2))); target = x >= N - 1 ? N - 1 : k + g; if (!raf) { lastRingTime = 0; raf = requestAnimationFrame(ringTick); } } if (!reduce) { on(ringScene, setRing); const rect = ringScene.getBoundingClientRect(); setRing(clamp(-rect.top / Math.max(1, rect.height - innerHeight))); shown = target; showRing(shown); } else { rcs.forEach(a => a.classList.add('on')); } // Reserve the original layout, but do not paint half-initialized 3D cards or fallback fonts. const revealRing = () => requestAnimationFrame(() => requestAnimationFrame(() => { if (!ringScene.isConnected) return; if (!reduce) { const rect = ringScene.getBoundingClientRect(); setRing(clamp(-rect.top / Math.max(1, rect.height - innerHeight))); shown = target; showRing(shown); } ringScene.dataset.renderReady = '1'; })); if (document.fonts) { Promise.allSettled([ document.fonts.load('400 36px "Instrument Serif"'), document.fonts.load('400 16px Inter'), document.fonts.load('600 16px Inter') ]).then(revealRing); } else revealRing(); /* FAQ: the first few answers show; the button reveals the rest */ const faqBtn = $('[data-faq-toggle]'), faqMore = $('[data-faq-more]'); if (faqBtn && faqMore) faqBtn.addEventListener('click', () => { const open = faqMore.hidden; faqMore.hidden = !open; faqBtn.setAttribute('aria-expanded', String(open)); faqBtn.textContent = open ? 'Show fewer' : 'Read all FAQs'; }); /* how it works: the timeline fills as it scrolls into view */ const tlTrack = $('#tlTrack'), tlFill = $('#tlFill'), tlStops = $$('.tl-stop'); function howFrame() { if (!tlTrack) return; const r = tlTrack.getBoundingClientRect(); const p = reduce ? 1 : clamp((innerHeight * .8 - r.top) / (r.height + innerHeight * .2)); if (innerWidth < 820 || tlTrack.classList.contains('v')) { tlTrack.style.setProperty('--fill', p * 100 + '%'); tlFill.style.width = ''; } else tlFill.style.width = p * 100 + '%'; tlStops.forEach((st, i) => st.classList.toggle('on', p >= i / (tlStops.length - 1) - .02)); } // Static/mobile mode is already fully filled. Do that once instead of // forcing layout on every touch-scroll event for an off-screen timeline. if (reduce) howFrame(); else { addEventListener('scroll', howFrame, { passive: true }); addEventListener('resize', howFrame); howFrame(); } /* ?review=1 shows the yellow tags marking copy that still needs a real name, photo or permission */ if (new URLSearchParams(location.search).get('review')) root.classList.add('review'); /* customer cards: arrows scroll the track one card at a time */ const vt = $('[data-vtrack]'); if (vt) { const step = () => (vt.querySelector('.vc').offsetWidth + 18) * (innerWidth < 760 ? 1 : 1.5); $('[data-vprev]').addEventListener('click', () => vt.scrollBy({ left: -step(), behavior: 'smooth' })); $('[data-vnext]').addEventListener('click', () => vt.scrollBy({ left: step(), behavior: 'smooth' })); } /* The page demos itself on the visitor's business: [data-tpl] elements hold a template whose {tokens} come from the chosen kind of business and the name typed in the hero. Typing updates every mock live; until the visitor interacts, the hero cycles through example businesses. */ const VERTS = { plumber: { biz: 'Lone Star Plumbing', noun: 'plumber', tag: '24/7 emergency', c: ['Pipe & Drain Co.', 'Capital Plumbing', 'Metro Plumbing', 'Alamo Rooter'], inq: 'Water heater is leaking. Can someone come today?', rep: 'Sorry about that! We have a plumber free tomorrow at 8am or 10am. Which works?' }, dentist: { biz: 'Bright Smile Dental', noun: 'dentist', tag: 'Gentle family care', c: ['City Dental Group', 'Oak Street Dentistry', 'Pearl Dental', 'Sunrise Dental'], inq: 'My tooth is killing me. Any openings today?', rep: "So sorry! We have a slot today at 4pm or tomorrow at 9am. Which works?" }, restaurant: { biz: 'Casa Verde Kitchen', noun: 'restaurant', tag: 'Open late', c: ['Harbor Grill', 'The Corner Table', 'Olive & Fire', 'Bistro 22'], inq: 'Table for 6 on Saturday around 7?', rep: 'Yes! We have 7:00 or 7:30 for six. Want me to hold one?' }, salon: { biz: 'Studio Luna Salon', noun: 'hair salon', tag: 'Walk-ins welcome', c: ['Glow Hair Studio', 'The Style Room', 'Velvet Salon', 'Salon Noir'], inq: 'Any balayage openings this weekend?', rep: 'Yes! Saturday at 10am or 2pm. Which works?' }, lawyer: { biz: 'Reyes Family Law', noun: 'family lawyer', tag: 'Free first consult', c: ['Harper & Cole LLP', 'Lakeside Legal', 'Union Law Group', 'Brooks Family Law'], inq: 'Can someone call me about a custody case?', rep: 'Of course. We have a free consult tomorrow at 10am or 1pm. Which works?' }, travel: { biz: 'Sunset Travel Co.', noun: 'travel agent', tag: 'Honeymoon specialists', c: ['Global Getaways', 'Roma Tours Direct', 'Wanderly Travel', 'Jetset Trips'], inq: 'Planning Italy for our honeymoon in June. Can you help?', rep: 'Congratulations! Want a 20-minute call tomorrow at 11am or 2pm?' }, }; const vertKeys = Object.keys(VERTS); const ctx = { vert: 'plumber', typed: '', city: 'Miami' }; const slug = s => s.toLowerCase().replace(/&/g, 'and').replace(/[^a-z0-9]+/g, '').slice(0, 24) || 'yourbusiness'; function tokens() { const v = VERTS[ctx.vert], typed = ctx.typed; const biz = typed || v.biz, city = ctx.city; return { biz, city, cityl: city.toLowerCase(), noun: v.noun, Noun: v.noun.replace(/^./, c => c.toUpperCase()), tag: v.tag, inq: v.inq, rep: v.rep, c1: v.c[0], c2: v.c[1], c3: v.c[2], c4: v.c[3] || 'Local Pros', c1slug: slug(v.c[0]), c2slug: slug(v.c[1]), c3slug: slug(v.c[2]), c4slug: slug(v.c[3] || 'Local Pros'), slug: slug(biz), rank: 14, q: 'best ' + v.noun + ' in ' + city.toLowerCase(), }; } const fill = (el, t) => { const tpl = el.dataset.tpl; const out = tpl.replace(/\{(\w+)\}/g, (m, k) => (k in t ? t[k] : m)); if (el.textContent !== out) { el.textContent = out; if (!reduce) { el.classList.remove('tick'); void el.offsetWidth; el.classList.add('tick'); } } }; const renderAll = () => { const t = tokens(); $$('[data-tpl]').forEach(el => fill(el, t)); }; function setVert(k) { ctx.vert = k; renderAll(); } // The free-draft form feeds the same tokens, so a business typed there shows in every mock. $$('form[data-lead-form] input[name="website"]').forEach(inp => inp.addEventListener('input', () => { ctx.typed = inp.value.trim().slice(0, 36); renderAll(); })); renderAll(); /* industries: hover opens a tile; a click shows that kind of business in the carousel */ const indTiles = $$('[data-ind]'); indTiles.forEach(t => { t.addEventListener('mouseenter', () => { indTiles.forEach(x => x.classList.toggle('on', x === t)); }); t.addEventListener('focus', () => { indTiles.forEach(x => x.classList.toggle('on', x === t)); }); t.addEventListener('click', () => { const any = t.dataset.ind === 'any'; if (!any) setVert(t.dataset.ind); const c = $(any ? '#free' : '#services'); (c || $('#free')).scrollIntoView({ behavior: reduce ? 'auto' : 'smooth' }); }); }); })(); })(); ; ; (function(){ const root=document.getElementById('sbm'); if(!root||root.dataset.mobileMotionReady||!('IntersectionObserver' in window)||!Element.prototype.animate)return; root.dataset.mobileMotionReady='1'; const query=matchMedia('(max-width:999px) and (prefers-reduced-motion:no-preference)'); // Enhance the ordinary sections only: hero and pinned services have their // own motion, and the pricing offer must stay painted during the pin. const targets=[...root.querySelectorAll('[data-reveal],#proof .dp-card,#proof .race,#industries .ind-t,#voices .vc')] .filter(el=>!el.closest('.hero,#services,#price')); const seen=new WeakSet(),running=new Set(); const observer=new IntersectionObserver(entries=>{ if(!root.isConnected){observer.disconnect();running.forEach(a=>a.cancel());query.removeEventListener('change',configure);return;} entries.forEach(entry=>{ if(!query.matches||!entry.isIntersecting||seen.has(entry.target))return; const el=entry.target; seen.add(el);observer.unobserve(el); // One short compositor-friendly entrance, no scroll loop or hidden state. const animation=el.animate([ {opacity:.6,transform:'translate3d(0,16px,0)'}, {opacity:1,transform:'translate3d(0,0,0)'} ],{duration:520,easing:'cubic-bezier(.22,1,.36,1)',fill:'none'}); running.add(animation); animation.onfinish=animation.oncancel=()=>{running.delete(animation);}; if(el.matches('.ticket')){ const plane=el.querySelector('.tk-route .line i'); const distance=Math.max(0,(plane.parentElement.clientWidth-plane.offsetWidth)/2); const flight=plane.animate([ {transform:'translateX(0)'}, {transform:`translateX(${-distance}px)`,offset:.18}, {transform:`translateX(${distance}px)`,offset:.72}, {transform:'translateX(0)'} ],{duration:2600,delay:200,easing:'ease-in-out',iterations:2,fill:'none'}); running.add(flight); flight.onfinish=flight.oncancel=()=>{running.delete(flight);}; } }); },{threshold:.08,rootMargin:'0px 0px -5% 0px'}); function configure(){ observer.disconnect(); running.forEach(a=>a.cancel());running.clear(); if(query.matches)targets.forEach(el=>{if(!seen.has(el))observer.observe(el);}); } query.addEventListener('change',configure); configure(); })(); ; (function(){ var pictures=document.querySelectorAll('#sbm .dfr-ph[data-bg]'); var motion=document.querySelectorAll('#sbm .hero,#sbm .pf-card,#sbm .ticket'); function load(el){ if(el.classList.contains('bg-ready'))return; el.style.backgroundImage='url("'+el.dataset.bg.replace(/"/g,'\"')+'")'; el.classList.add('bg-ready'); } if(!('IntersectionObserver' in window)){pictures.forEach(load);return;} var observer=new IntersectionObserver(function(entries){ entries.forEach(function(entry){if(entry.isIntersecting){load(entry.target);observer.unobserve(entry.target);}}); },{rootMargin:'150% 0px'}); pictures.forEach(function(el){observer.observe(el);}); var motionObserver=new IntersectionObserver(function(entries){ entries.forEach(function(entry){entry.target.classList.toggle('perf-visible',entry.isIntersecting);}); },{rootMargin:'15% 0px'}); motion.forEach(function(el){motionObserver.observe(el);}); var flowObserver=new IntersectionObserver(function(entries){ entries.forEach(function(entry){entry.target.classList.toggle('flow-visible',entry.isIntersecting);}); }); document.querySelectorAll('#sbm .pf-card .flow').forEach(function(el){flowObserver.observe(el);}); })(); ; /* One-shot enhancements. Each waits for its section to scroll into view and runs once; none listens to scroll. */ (function(){ const root=document.getElementById('sbm'); if(!root||root.dataset.enhanceReady)return; root.dataset.enhanceReady='1'; const reduced=matchMedia('(prefers-reduced-motion:reduce)').matches; const ease=t=>1-Math.pow(1-t,3); const once=(el,fn,threshold)=>{ if(!el)return; if(reduced||!('IntersectionObserver' in window)){fn(true);return;} const io=new IntersectionObserver(entries=>{if(entries[0].isIntersecting){io.disconnect();fn(false);}},{threshold:threshold||.5}); io.observe(el); }; // Proof: 1,000,000+ counts up and the comparison draws itself (strikethrough on "3 months", then "8 days"). const count=root.querySelector('.dp-n'); once(count,instant=>{ if(instant)return; const target=1000000,t0=performance.now(); (function tick(now){ const k=Math.min(1,(now-t0)/1600); count.textContent=k<1?Math.round(target*ease(k)).toLocaleString('en-US'):'1,000,000+'; if(k<1)requestAnimationFrame(tick); })(t0); }); const race=root.querySelector('.race'); once(race,()=>race.classList.add('go')); // Meridian: on phones the three module boxes become tabs, so the diagram is one box tall instead of three. const modules=root.querySelector('.modules'); if(modules){ const panes=[...modules.children],tabs=document.createElement('div'); tabs.className='mod-tabs';tabs.setAttribute('role','tablist'); const select=i=>{ panes.forEach((p,j)=>p.classList.toggle('on',i===j)); [...tabs.children].forEach((b,j)=>{b.classList.toggle('on',i===j);b.setAttribute('aria-selected',String(i===j));}); }; panes.forEach((p,i)=>{ const b=document.createElement('button'); b.type='button';b.setAttribute('role','tab');b.textContent=p.querySelector('small').textContent; b.addEventListener('click',()=>select(i)); tabs.appendChild(b); }); modules.before(tabs); select(0); } // Industries: dots under the swipe row show which tile is in view (phones only; hidden by CSS on desktop). const row=root.querySelector('#industries .ind'); if(row&&'IntersectionObserver' in window){ const tiles=[...row.children],dots=document.createElement('div'); dots.className='ind-dots';dots.setAttribute('aria-hidden','true'); tiles.forEach(()=>dots.appendChild(document.createElement('i'))); row.after(dots); dots.children[0].classList.add('on'); const io=new IntersectionObserver(entries=>entries.forEach(e=>{ if(!e.isIntersecting)return; const i=tiles.indexOf(e.target); [...dots.children].forEach((d,j)=>d.classList.toggle('on',i===j)); }),{root:row,threshold:.6}); tiles.forEach(t=>io.observe(t)); } })();