1/* Scroll reveal (single elements + stagger grids) */ 2const io = new IntersectionObserver(entries => { 3 entries.forEach(e => { 4 if (e.isIntersecting) { e.target.classList.add('vis'); io.unobserve(e.target); } 5 }); 6}, {threshold: .12, rootMargin: '0px 0px -40px 0px'}); 7document.querySelectorAll('[data-r], [data-r-stagger]').forEach(el => io.observe(el)); 8 9/* Hero H1 underline + badge entrance trigger */ 10requestAnimationFrame(() => requestAnimationFrame(() => { 11 const h1 = document.querySelector('.hero-h1'); 12 if (h1) h1.classList.add('shown'); 13})); 14 15/* Stat counter */ 16const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 17function animateCounter(el) { 18 if (reducedMotion) return; 19 const target = el.getAttribute('data-count'); 20 if (!target) return; 21 const suffix = el.getAttribute('data-suffix') || ''; 22 const isFloat = target.includes('.'); 23 const end = parseFloat(target); 24 const duration = 1400; 25 const startTime = performance.now(); 26 function tick(now) { 27 const p = Math.min((now - startTime) / duration, 1); 28 const eased = 1 - Math.pow(1 - p, 3); 29 const val = end * eased; 30 el.textContent = (isFloat ? val.toFixed(1) : Math.round(val)) + suffix; 31 if (p < 1) requestAnimationFrame(tick); 32 else el.textContent = (isFloat ? end.toFixed(1) : end) + suffix; 33 } 34 requestAnimationFrame(tick); 35} 36const statIO = new IntersectionObserver(entries => { 37 entries.forEach(e => { 38 if (e.isIntersecting) { animateCounter(e.target); statIO.unobserve(e.target); } 39 }); 40}, {threshold: .5}); 41document.querySelectorAll('.stat-n[data-count]').forEach(el => statIO.observe(el)); 42 43/* Calculator single-select options */ 44document.querySelectorAll('.calc-opt').forEach(btn => { 45 btn.addEventListener('click', () => { 46 const group = btn.dataset.group; 47 document.querySelectorAll('.calc-opt[data-group="'+group+'"]').forEach(b => b.classList.remove('active')); 48 btn.classList.add('active'); 49 }); 50}); 51/* Calculator multi-select checks */ 52document.querySelectorAll('.calc-check-item').forEach(btn => { 53 btn.addEventListener('click', () => btn.classList.toggle('active')); 54}); 55 56/* Tech stack: inject brand-colored logos for each tag */ 57const techLogos = { 58 /* Languages */ 59 'TypeScript':{bg:'#3178C6',letter:'TS'}, 60 'JavaScript':{bg:'#F7DF1E',letter:'JS'}, 61 'Swift':{bg:'#F05138',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M19 6c-1 5-5 11-12 13 2-3 1-4-2-5l1-1c-2-1-3-3-4-5h2c2 3 5 5 9 5-2-3-5-5-7-8 2 2 6 4 9 4-1-2-3-5-6-7 3 1 7 4 10 4z"/></svg>'}, 62 'Kotlin':{bg:'#7F52FF',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M3 3h9l-9 9V3zm0 18V11l9 10H3zm9-18h9L12 12V3z"/></svg>'}, 63 'Objective-C':{bg:'#A2B9CB',letter:'Obj'}, 64 'C++ (JSI)':{bg:'#00599C',letter:'C++'}, 65 66 /* RN Core & UI */ 67 'React Native 0.76':{bg:'#61DAFB',svg:'<svg viewBox="0 0 24 24" fill="#fff"><circle cx="12" cy="12" r="2"/><ellipse cx="12" cy="12" rx="11" ry="4.2" fill="none" stroke="#fff" stroke-width="1.4"/><ellipse cx="12" cy="12" rx="11" ry="4.2" fill="none" stroke="#fff" stroke-width="1.4" transform="rotate(60 12 12)"/><ellipse cx="12" cy="12" rx="11" ry="4.2" fill="none" stroke="#fff" stroke-width="1.4" transform="rotate(120 12 12)"/></svg>'}, 68 'Expo SDK 52':{bg:'#000020',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 3l9 15.5h-3.5L12 9 6.5 18.5H3z"/></svg>'}, 69 'Expo Router':{bg:'#000020',letter:'ER'}, 70 'Reanimated 3':{bg:'#F7672E',letter:'Re'}, 71 'React Native Skia':{bg:'#FE3C45',letter:'Sk'}, 72 'Gesture Handler':{bg:'#61DAFB',letter:'GH'}, 73 'Lottie':{bg:'#00DDB3',svg:'<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2"><path d="M3 12c2-6 6-9 9-9s7 3 9 9-2 9-9 9-11-3-9-9z"/></svg>'}, 74 'React Native Web':{bg:'#61DAFB',letter:'RNW'}, 75 'React Navigation':{bg:'#6B52AE',letter:'RN'}, 76 77 /* Architecture & State */ 78 'Fabric':{bg:'#61DAFB',letter:'Fb'}, 79 'JSI':{bg:'#0F4C81',letter:'JSI'}, 80 'TurboModules':{bg:'#7F52FF',letter:'Tm'}, 81 'Hermes':{bg:'#1C1E21',letter:'Hr'}, 82 'React Query':{bg:'#FF4154',letter:'RQ'}, 83 'Zustand':{bg:'#443E38',letter:'Zu'}, 84 'Redux Toolkit':{bg:'#764ABC',letter:'RT'}, 85 'MMKV':{bg:'#3DDC84',letter:'MM'}, 86 'Axios':{bg:'#5A29E4',letter:'Ax'}, 87 88 /* Backend */ 89 'Node.js':{bg:'#339933',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2L3 7v10l9 5 9-5V7L12 2zm0 2.3l7 3.9v7.6l-7 3.9-7-3.9V8.2l7-3.9z"/></svg>'}, 90 'NestJS':{bg:'#E0234E',letter:'N'},
91 'Python (Django, FastAPI)':{bg:'#3776AB',svg:'<svg viewBox="0 0 24 24"><path fill="#FFD43B" d="M12 2c-3 0-3 1.5-3 3v2h6v1H6c-2 0-3 1.5-3 4s1 4 3 4h2v-2c0-2 1.5-3 4-3h3c1.5 0 3-.5 3-2V5c0-1.5-1.5-3-6-3zm-2 2c.5 0 1 .5 1 1s-.5 1-1 1-1-.5-1-1 .5-1 1-1z"/><path fill="#fff" d="M12 22c3 0 3-1.5 3-3v-2H9v-1h9c2 0 3-1.5 3-4s-1-4-3-4h-2v2c0 2-1.5 3-4 3H9c-1.5 0-3 .5-3 2v3c0 1.5 1.5 4 6 4zm2-2c-.5 0-1-.5-1-1s.5-1 1-1 1 .5 1 1-.5 1-1 1z"/></svg>'}, 92 'Ruby on Rails':{bg:'#CC0000',letter:'R'}, 93 '.NET 8':{bg:'#512BD4',letter:'.N'}, 94 'Go':{bg:'#00ADD8',letter:'Go'}, 95 'GraphQL':{bg:'#E10098',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2l9 5.2v9.6L12 22l-9-5.2V7.2L12 2z" stroke="#fff" stroke-width="1.2" fill="none"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="3" r="1.5"/><circle cx="12" cy="21" r="1.5"/><circle cx="4" cy="7.5" r="1.5"/><circle cx="20" cy="7.5" r="1.5"/><circle cx="4" cy="16.5" r="1.5"/><circle cx="20" cy="16.5" r="1.5"/></svg>'}, 96 'REST':{bg:'#0E2F44',letter:'API'}, 97 'gRPC':{bg:'#247DAB',letter:'g'}, 98 'WebSockets':{bg:'#010101',letter:'WS'}, 99 100 /* Cloud & DevOps */ 101 'AWS':{bg:'#FF9900',svg:'<svg viewBox="0 0 24 24" fill="#232F3E"><path d="M7 11c-.6 0-1 .4-1 1.5 0 .6.2 1 .5 1.5l-.5 1c-.7-.5-1-1.5-1-2.5C5 11 5.7 10 7 10s2 .8 2 2.2v2c0 .3.1.5.2.7l-1 .5c-.1-.1-.2-.3-.3-.5-.5.4-1.1.6-1.7.6-.7 0-1.2-.4-1.2-1 0-.8.7-1.3 2-1.3h.7v-.6c0-.5-.2-.6-.7-.6zm.7 1.7v-.5h-.5c-.5 0-.7.2-.7.5s.2.4.4.4c.3 0 .6-.2.8-.4zM3 17c4 2 9 2 14 0l-.5-1c-4 2-9 2-13 0z"/></svg>'}, 102 'Google Cloud (GCP)':{bg:'#4285F4',svg:'<svg viewBox="0 0 24 24"><path fill="#EA4335" d="M14 7l1.5-1.5C14 4 12.5 3.5 11 3.5c-3 0-5.5 1.5-7 4l2 1.5c.8-1.5 2.5-2.5 5-2.5 1 0 2 .3 3 1z"/><path fill="#4285F4" d="M11.5 11v3h4.5c-.5 1.5-2 2.5-4.5 2.5-2 0-4-1.5-4.5-3l-2 1.5c1 2.5 3.5 4 6.5 4 4 0 7-3 7-7 0-.5 0-1-.2-1.5z"/><path fill="#FBBC04" d="M4 13c-.3-.7-.5-1.5-.5-2.5s.2-1.8.5-2.5l-2-1.5c-.7 1-1 2.5-1 4s.3 3 1 4z"/><path fill="#34A853" d="M11 7c1 0 1.8.3 2.5.8L15.5 6c-1-1-2.5-2-4.5-2-2.5 0-4.5 1-6 2.5L7 8.5c.8-1.2 2-1.5 4-1.5z"/></svg>'}, 103 'Azure':{bg:'#0078D4',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M9 3L2 17l5 4h13L12 7zm1 5l6 11h-9z"/></svg>'}, 104 'Firebase':{bg:'#FFCA28',svg:'<svg viewBox="0 0 24 24"><path fill="#FFA000" d="M4 18l4-15 4 7z"/><path fill="#F57C00" d="M4 18l11-6 5 6-9 4z"/><path fill="#FFCA28" d="M8 3l4 7-3 2z"/></svg>'}, 105 'EAS Build':{bg:'#000020',letter:'EB'}, 106 'EAS Update':{bg:'#000020',letter:'EU'}, 107 'CodePush':{bg:'#0078D4',letter:'CP'}, 108 'Crashlytics':{bg:'#FFA000',letter:'CR'}, 109 'Fastlane':{bg:'#84BD00',letter:'F'}, 110 'GitHub Actions':{bg:'#2088FF',svg:'<svg viewBox="0 0 24 24" fill="#fff"><circle cx="12" cy="12" r="10" fill-opacity=".15"/><path d="M16 12l-5-4v3H7v2h4v3z"/></svg>'}, 111 'CircleCI':{bg:'#161616',letter:'CI'}, 112 'Sentry':{bg:'#362D59',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2l10 18H14l-2-4-2 4H2L12 2zm0 5l5 10h-3l-2-4-2 4H9z" opacity=".95"/></svg>'}, 113 114 /* Databases */ 115 'WatermelonDB':{bg:'#FE3C45',letter:'Wm'}, 116 'SQLite':{bg:'#003B57',letter:'SQ'}, 117 'Firestore':{bg:'#FFCA28',letter:'FS'}, 118 'Realm':{bg:'#39477F',letter:'R'}, 119 'PostgreSQL':{bg:'#336791',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M16 4c-2 0-3 .5-4 1-1-.5-2-1-4-1-3 0-5 2-5 5 0 4 2 13 5 13 1 0 2-1 3-3 0-1 0-2 1-2s1 1 1 2c1 2 2 3 3 3 3 0 5-9 5-13 0-3-2-5-5-5zM7 7c1 0 1 1 1 1s-1 0-1-1zm9 0c0-1 1-1 1 0s-1 1-1 1z"/></svg>'}, 120 'MongoDB':{bg:'#47A248',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2C8 8 8 14 12 22c4-8 4-14 0-20z"/></svg>'}, 121 'Redis':{bg:'#DC382D',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M3 8l9 4 9-4-9-4zm0 4l9 4 9-4M3 16l9 4 9-4" stroke="#fff" stroke-width="1.5" fill="none"/></svg>'}, 122 'DynamoDB':{bg:'#4053D6',letter:'Dy'}, 123 'AsyncStorage':{bg:'#61DAFB',letter:'AS'}, 124 125 /* AI / ML */ 126 'OpenAI API':{bg:'#000',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M22 11c0-3-2-5-5-5-.5-2-2.5-3-5-3s-4.5 1.5-5 3.5C4.5 7 2.5 9 2.5 12c0 1.5 1 3 2 4-1 1.5-.5 4 1 5.5 1.5 1 4 1 5.5 0 .5 2 2.5 3.5 5 3.5s4.5-1.5 5-3.5c2.5-.5 4.5-2.5 4.5-5 0-1.5-1-3-2-4 1-1.5.5-1 .5-1.5z" opacity=".95"/></svg>'}, 127 'Anthropic Claude':{bg:'#D97706',letter:'An'}, 128 'Gemini':{bg:'#1E88E5',svg:'<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2L8 8 2 12l6 4 4 6 4-6 6-4-6-4z"/></svg>'}, 129 'ML Kit':{bg:'#4285F4',letter:'ML'}, 130 'TensorFlow Lite':{bg:'#FF6F00',letter:'TF'}, 131 'Vertex AI':{bg:'#4285F4',letter:'Vx'}, 132 'AWS Bedrock':{bg:'#FF9900',letter:'Br'}, 133 'ARKit / ARCore':{bg:'#4285F4',letter:'AR'}, 134 'LangChain':{bg:'#1C3C3C',letter:'Lc'}, 135 136 /* Testing & QA */
137 'Jest':{bg:'#C21325',letter:'J'}, 138 'React Testing Library':{bg:'#E33332',letter:'RTL'}, 139 'Detox':{bg:'#0066B8',letter:'Dx'}, 140 'Maestro':{bg:'#9333EA',letter:'Ma'}, 141 'Appium':{bg:'#662D91',letter:'Ap'}, 142 'BrowserStack App Live':{bg:'#FF6B00',letter:'BS'}, 143 'Firebase Test Lab':{bg:'#FFCA28',letter:'FTL'}, 144 'MSW (Mock Service Worker)':{bg:'#FF6A33',letter:'MSW'}, 145 'ESLint':{bg:'#4B32C3',letter:'ES'}, 146 'Prettier':{bg:'#F7B93E',letter:'Pr'} 147}; 148function buildTechLogo(name) { 149 const t = techLogos[name]; 150 const fallbackBg = '#5a6068'; 151 const fallbackLetter = name.replace(/[^A-Za-z0-9]/g,'').slice(0,2).toUpperCase() || '·'; 152 if (!t) return '<span class="stack-chip-logo" style="background:'+fallbackBg+'" aria-hidden="true">'+fallbackLetter+'</span>'; 153 const style = 'style="background:'+t.bg+'"'; 154 if (t.svg) return '<span class="stack-chip-logo" '+style+' aria-hidden="true">'+t.svg+'</span>'; 155 if (t.letter) return '<span class="stack-chip-logo" '+style+' aria-hidden="true">'+t.letter+'</span>'; 156 return '<span class="stack-chip-logo" '+style+' aria-hidden="true">'+fallbackLetter+'</span>'; 157} 158document.querySelectorAll('.stack-chip[data-tech]').forEach(chip => { 159 const name = chip.dataset.tech; 160 chip.insertAdjacentHTML('afterbegin', buildTechLogo(name)); 161}); 162 163/* Testimonials rotate */ 164let ti = 0; 165const tCount = 5; 166function showTest(n) { 167 ti = n; 168 document.querySelectorAll('.test-slide').forEach((s, i) => s.classList.toggle('active', i === n)); 169 document.querySelectorAll('.test-dot').forEach((d, i) => d.classList.toggle('active', i === n)); 170} 171setInterval(() => showTest((ti + 1) % tCount), 5500); 172 173/* FAQ accordion */ 174function toggleFAQ(btn) { 175 const item = btn.parentElement; 176 const isOpen = item.classList.contains('open'); 177 item.classList.toggle('open'); 178 btn.setAttribute('aria-expanded', !isOpen); 179} 180 181 182/* Main contact form */ 183function onMainContactSubmit() { 184 const form = document.getElementById('contact-main-form'); 185 alert('Thanks â a Senior Engineering Lead will reply within one business hour during US working hours (MonâFri, 9amâ7pm ET). You\'ll get an auto-acknowledgement email with a reference number in the next few seconds.'); 186 form.reset(); 187} 188 189/* âââââââââââ HERO 3D DEPTH (#v2) â entrance reveal, mouse parallax, app-screen carousel âââââââââââ */ 190(function(){ 191 const v2 = document.getElementById('v2'); 192 if(!v2) return; 193 const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; 194 195 /* entrance reveal via Web Animations API */ 196 if(!reduce && !document.hidden){ 197 v2.querySelectorAll('.reveal').forEach(it => { 198 const d = parseFloat(getComputedStyle(it).getPropertyValue('--d')) || 0; 199 it.animate( 200 [{opacity:0, transform:'translateY(18px)'},{opacity:1, transform:'translateY(0)'}], 201 {duration:750, delay:d*1000, easing:'cubic-bezier(.2,.75,.25,1)', fill:'backwards'} 202 ); 203 }); 204 } 205 206 /* mouse parallax */ 207 const stage = document.getElementById('v2-stage'); 208 const layers = document.querySelectorAll('#v2-scene [data-depth]'); 209 if(stage && !reduce){ 210 stage.addEventListener('mousemove', e => { 211 const r = stage.getBoundingClientRect(); 212 const x = (e.clientX - r.left)/r.width - .5; 213 const y = (e.clientY - r.top)/r.height - .5; 214 layers.forEach(l => { 215 const d = parseFloat(l.dataset.depth); 216 l.style.transform = `translate3d(${-x*d}px, ${-y*d}px, 0)`; 217 }); 218 }); 219 stage.addEventListener('mouseleave', () => { 220 layers.forEach(l => l.style.transform = 'translate3d(0,0,0)'); 221 }); 222 } 223 224 /* Industry app-screen carousel â auto-rotates inside the phone */ 225 (function buildCarousel(){ 226 const car = document.getElementById('v2-car'); 227 const template = document.getElementById('app-screens'); 228 if(!car || !template) return; 229 const track = car.querySelector('.appcar-track'); 230 track.appendChild(template.content.cloneNode(true)); 231 const n = track.children.length; 232 let idx = 0, timer = null; 233 const go = i => { idx = i; track.style.transform = `translateX(-${i*100}%)`; }; 234 const start = () => { if(reduce || timer) return; timer = setInterval(() => go((idx+1)%n), 3400); }; 235 const stop = () => { clearInterval(timer); timer = null; }; 236 car.addEventListener('mouseenter', stop); 237 car.addEventListener('mouseleave', start); 238 start(); 239 })(); 240})(); 241 242/* âââââââââââ UNIFIED CAROUSEL âââââââââââ */ 243(function(){ 244 const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; 245 246 function initCarousel(root){ 247 const viewport = root.querySelector('.tt-car-viewport'); 248 const track = root.querySelector('.tt-car-track'); 249 if(!viewport || !track) return; 250 const slides = Array.from(track.children).filter(el => el.classList.contains('tt-car-slide')); 251 if(slides.length === 0) return; 252 253 const prevBtn = root.querySelector('.tt-car-prev'); 254 const nextBtn = root.querySelector('.tt-car-next'); 255 const dotsHost = root.querySelector('.tt-car-dots'); 256 const counter = root.querySelector('.tt-car-counter'); 257 const baseLabel = root.dataset.label || 'item'; 258 259 function currentPerView(){ 260 const base = parseInt(root.dataset.per || '3', 10); 261 if(matchMedia('(max-width: 640px)').matches) return 1; 262 if(matchMedia('(max-width: 980px)').matches) return Math.min(base, 2); 263 return base; 264 } 265 266 let perView = currentPerView(); 267 let pages = Math.max(1, Math.ceil(slides.length / perView)); 268 let idx = 0; 269 270 function rebuildDots(){ 271 if(!dotsHost) return; 272 dotsHost.innerHTML = ''; 273 for(let i = 0; i < pages; i++){ 274 const b = document.createElement('button'); 275 b.type = 'button'; 276 b.className = 'tt-car-dot' + (i === idx ? ' active' : ''); 277 b.setAttribute('aria-label', `Page ${i+1} of ${pages}`); 278 b.addEventListener('click', () => go(i)); 279 dotsHost.appendChild(b); 280 } 281 } 282 283 function go(i){ 284 idx = Math.max(0, Math.min(pages - 1, i)); 285 const slideW = slides[0] ? slides[0].getBoundingClientRect().width : 0; 286 const gap = parseFloat(getComputedStyle(track).gap) || 0; 287 const offset = (slideW + gap) * (idx * perView); 288 track.style.transform = `translateX(-${offset}px)`; 289 if(dotsHost){
290 [...dotsHost.children].forEach((d, k) => d.classList.toggle('active', k === idx)); 291 } 292 if(prevBtn) prevBtn.disabled = idx === 0; 293 if(nextBtn) nextBtn.disabled = idx === pages - 1; 294 if(counter) counter.textContent = `${idx+1} / ${pages}`; 295 } 296 297 if(prevBtn) prevBtn.addEventListener('click', () => go(idx - 1)); 298 if(nextBtn) nextBtn.addEventListener('click', () => go(idx + 1)); 299 300 /* touch swipe */ 301 let startX = 0, dx = 0, touching = false; 302 viewport.addEventListener('touchstart', e => { 303 startX = e.touches[0].clientX; dx = 0; touching = true; 304 }, {passive: true}); 305 viewport.addEventListener('touchmove', e => { 306 if(!touching) return; 307 dx = e.touches[0].clientX - startX; 308 }, {passive: true}); 309 viewport.addEventListener('touchend', () => { 310 if(!touching) return; 311 touching = false; 312 if(Math.abs(dx) > 50) go(idx + (dx < 0 ? 1 : -1)); 313 }); 314 315 /* keyboard support on focused viewport */ 316 viewport.setAttribute('tabindex', '0'); 317 viewport.setAttribute('role', 'region'); 318 viewport.setAttribute('aria-roledescription', 'carousel'); 319 viewport.setAttribute('aria-label', `${baseLabel} carousel`); 320 viewport.addEventListener('keydown', e => { 321 if(e.key === 'ArrowLeft'){ e.preventDefault(); go(idx - 1); } 322 if(e.key === 'ArrowRight'){ e.preventDefault(); go(idx + 1); } 323 }); 324 325 /* resize handler */ 326 let lastW = window.innerWidth; 327 window.addEventListener('resize', () => { 328 if(Math.abs(window.innerWidth - lastW) < 40) return; 329 lastW = window.innerWidth; 330 perView = currentPerView(); 331 pages = Math.max(1, Math.ceil(slides.length / perView)); 332 idx = Math.min(idx, pages - 1); 333 rebuildDots(); 334 requestAnimationFrame(() => go(idx)); 335 }); 336 337 rebuildDots(); 338 /* defer first layout so flex widths settle */ 339 requestAnimationFrame(() => requestAnimationFrame(() => go(0))); 340 } 341 342 document.querySelectorAll('[data-carousel]').forEach(initCarousel); 343})();
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.