1const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 2const canHover = window.matchMedia('(hover: hover)').matches; 3const mobileViewport = window.matchMedia('(max-width: 760px)').matches; 4 5// ----- Scroll: nav state, progress bar, hero parallax ----- 6const nav = document.getElementById('nav'); 7const progress = document.getElementById('nav-progress'); 8const navMenuToggle = document.querySelector('[data-nav-menu-toggle]'); 9const navMobileMenu = document.querySelector('[data-nav-mobile-menu]'); 10const navCompany = document.querySelector('[data-nav-company]'); 11const roleFeatures = Array.from(document.querySelectorAll('#agents .feature')); 12const aurora = document.getElementById('aurora'); 13 14let scrollQueued = false; 15function onScroll() { 16 if (scrollQueued) return; 17 scrollQueued = true; 18 requestAnimationFrame(() => { 19 scrollQueued = false; 20 const y = window.scrollY; 21 if (nav) { 22 nav.classList.toggle('scrolled', y > 8); 23 } 24 const max = document.documentElement.scrollHeight - window.innerHeight; 25 if (progress) { 26 progress.style.transform = `scaleX(${max > 0 ? Math.min(y / max, 1) : 0})`; 27 } 28 if (!reducedMotion && !mobileViewport && aurora) { 29 aurora.style.transform = `translateY(${y * 0.12}px)`; 30 } 31 }); 32} 33window.addEventListener('scroll', onScroll, { passive: true }); 34onScroll(); 35
36function updateActiveRoleFeature() { 37 if (!mobileViewport || !roleFeatures.length) return; 38 const viewportCenter = window.innerHeight * 0.52; 39 let activeFeature = null; 40 let activeDistance = Number.POSITIVE_INFINITY; 41 42 roleFeatures.forEach((feature) => { 43 const rect = feature.getBoundingClientRect(); 44 const visible = rect.bottom > window.innerHeight * 0.18 && rect.top < window.innerHeight * 0.82; 45 const featureCenter = rect.top + rect.height / 2; 46 const distance = Math.abs(featureCenter - viewportCenter); 47 if (visible && distance < activeDistance) { 48 activeDistance = distance; 49 activeFeature = feature; 50 } 51 }); 52 53 roleFeatures.forEach((feature) => { 54 feature.classList.toggle('scroll-active', feature === activeFeature); 55 }); 56} 57 58if (mobileViewport && roleFeatures.length) { 59 let roleFeatureQueued = false; 60 const queueActiveRoleFeatureUpdate = () => { 61 if (roleFeatureQueued) return; 62 roleFeatureQueued = true; 63 requestAnimationFrame(() => { 64 roleFeatureQueued = false;
65 updateActiveRoleFeature(); 66 }); 67 }; 68 window.addEventListener('scroll', queueActiveRoleFeatureUpdate, { passive: true }); 69 window.addEventListener('resize', queueActiveRoleFeatureUpdate); 70 updateActiveRoleFeature(); 71} 72 73if (navMenuToggle && navMobileMenu) { 74 navMenuToggle.addEventListener('click', () => { 75 const isOpen = navMobileMenu.hidden; 76 navMobileMenu.hidden = !isOpen; 77 navMenuToggle.setAttribute('aria-expanded', String(isOpen)); 78 }); 79 navMobileMenu.querySelectorAll('[data-nav-menu-item]').forEach((item) => { 80 item.addEventListener('click', () => { 81 navMobileMenu.hidden = true; 82 navMenuToggle.setAttribute('aria-expanded', 'false'); 83 }); 84 }); 85 document.addEventListener('click', (event) => { 86 if (navMobileMenu.hidden) return; 87 if (navMobileMenu.contains(event.target) || navMenuToggle.contains(event.target)) return; 88 navMobileMenu.hidden = true; 89 navMenuToggle.setAttribute('aria-expanded', 'false'); 90 }); 91} 92 93if (navCompany) { 94 document.addEventListener('click', (event) => { 95 if (navCompany.open && !navCompany.contains(event.target)) navCompany.open = false; 96 }); 97 navCompany.addEventListener('keydown', (event) => { 98 if (event.key !== 'Escape') return; 99 navCompany.open = false; 100 navCompany.querySelector('summary')?.focus(); 101 }); 102} 103 104document.addEventListener('click', (event) => { 105 const link = event.target.closest('a[href^="#"]'); 106 if (!link) return; 107 const hash = link.getAttribute('href') || ''; 108 if (hash.length <= 1) return; 109 const target = document.getElementById(hash.slice(1)); 110 if (!target) return; 111 event.preventDefault(); 112 target.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'start' }); 113 if (window.location.hash) { 114 window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}`); 115 } 116}); 117 118// ----- Reveal on scroll, staggered per container ----- 119const revealTargets = document.querySelectorAll( 120 '.section-head, .command-preview, .command-section-cta, .feature, .step, .stat, .pull-quote, .side-quote, .plan, .compare, .try-card, .blog-cta, .faq-item, .contact-copy, .contact-actions, .team-chip' 121); 122if (reducedMotion) { 123 // leave content visible; no reveal classes at all 124} else { 125 const siblingIndex = new Map(); 126 revealTargets.forEach((el) => { 127 const n = siblingIndex.get(el.parentElement) || 0; 128 siblingIndex.set(el.parentElement, n + 1); 129 el.classList.add('reveal'); 130 el.style.transitionDelay = `${Math.min(n * 70, 350)}ms`; 131 }); 132 const revealIO = new IntersectionObserver((entries) => { 133 entries.forEach((e) => { 134 if (e.isIntersecting) { e.target.classList.add('in'); revealIO.unobserve(e.target); } 135 }); 136 }, { threshold: 0.15, rootMargin: '0px 0px -5% 0px' }); 137 revealTargets.forEach((el) => revealIO.observe(el)); 138} 139 140// ----- Stat count-up ----- 141const statVals = document.querySelectorAll('.stat-val[data-num]'); 142function formatStat(el, value) { 143 const dec = parseInt(el.dataset.decimals || '0', 10); 144 const body = el.dataset.locale 145 ? Math.round(value).toLocaleString('en-US') 146 : value.toFixed(dec); 147 return (el.dataset.prefix || '') + body + (el.dataset.suffix || ''); 148} 149if (reducedMotion) { 150 statVals.forEach((el) => { el.textContent = formatStat(el, parseFloat(el.dataset.num)); }); 151} else { 152 const statIO = new IntersectionObserver((entries) => { 153 entries.forEach((e) => { 154 if (!e.isIntersecting) return; 155 statIO.unobserve(e.target); 156 const el = e.target; 157 const num = parseFloat(el.dataset.num); 158 const t0 = performance.now(); 159 const step = (t) => { 160 const p = Math.min((t - t0) / 900, 1); 161 const eased = 1 - Math.pow(1 - p, 3); 162 el.textContent = formatStat(el, num * eased); 163 if (p < 1) requestAnimationFrame(step); 164 }; 165 requestAnimationFrame(step); 166 }); 167 }, { threshold: 0.6 }); 168 statVals.forEach((el) => statIO.observe(el)); 169} 170 171// ----- Cursor spotlight on cards ----- 172if (canHover && !reducedMotion) { 173 document.querySelectorAll('.plan, .terminal, .try-card, .command-preview').forEach((el) => { 174 el.classList.add('spot'); 175 el.addEventListener('mousemove', (e) => { 176 const r = el.getBoundingClientRect(); 177 el.style.setProperty('--mx', `${e.clientX - r.left}px`); 178 el.style.setProperty('--my', `${e.clientY - r.top}px`); 179 }); 180 }); 181 182 // ----- Magnetic primary buttons ----- 183 document.querySelectorAll('.btn-primary').forEach((btn) => { 184 btn.addEventListener('mousemove', (e) => { 185 const r = btn.getBoundingClientRect(); 186 const dx = Math.max(-3, Math.min(3, (e.clientX - r.left - r.width / 2) * 0.1)); 187 const dy = Math.max(-3, Math.min(3, (e.clientY - r.top - r.height / 2) * 0.3)); 188 btn.style.transform = `translate(${dx}px, ${dy}px)`; 189 }); 190 btn.addEventListener('mouseleave', () => { btn.style.transform = ''; }); 191 }); 192} 193 194// ----- Agent activity feed replay ----- 195const logLines = [ 196 { time: '6:01 am', tag: 'tag-act', label: 'team', text: 'Reviewed the business and <strong>prioritized todayâs work</strong>' }, 197 { time: '6:03 am', tag: 'tag-act', label: 'team', text: 'Moved open customer work forward' }, 198 { time: '6:05 am', tag: 'tag-act', label: 'team', text: 'Prepared the next growth opportunities' }, 199 { time: '6:06 am', tag: 'tag-plan', label: 'you', text: 'One decision needs <strong>your approval</strong>' }, 200 { time: '6:08 am', tag: 'tag-act', label: 'team', text: 'Updated your workspace with the results' }, 201 { time: '6:09 am', tag: 'tag-ok', label: 'done', text: 'The business is ready for <strong>your next move</strong>' }, 202]; 203 204const tbody = document.getElementById('terminal-body'); 205 206function renderLine(l) { 207 const div = document.createElement('div'); 208 div.className = 't-line'; 209 div.innerHTML = `<span class="t-time">${l.time}</span><span class="t-tag ${l.tag}">${l.label}</span><span class="t-text">${l.text}</span>`;
210 tbody.appendChild(div); 211 if (reducedMotion) { 212 div.classList.add('visible'); 213 } else { 214 requestAnimationFrame(() => div.classList.add('visible')); 215 } 216} 217 218function renderStatus() { 219 const div = document.createElement('div'); 220 div.className = 't-line visible'; 221 div.innerHTML = '<span class="t-time">now</span><span class="t-status"><span class="live-dot"></span>your team is working</span>'; 222 tbody.appendChild(div); 223} 224 225if (reducedMotion || mobileViewport) { 226 logLines.forEach(renderLine); 227 renderStatus(); 228} else { 229 let i = 0; 230 const addLine = () => { 231 if (i >= logLines.length) { renderStatus(); return; } 232 renderLine(logLines[i++]); 233 setTimeout(addLine, 650 + Math.random() * 350); 234 }; 235 const io = new IntersectionObserver((entries) => { 236 if (entries[0].isIntersecting) { io.disconnect(); setTimeout(addLine, 500); } 237 }, { threshold: 0.3 }); 238 io.observe(tbody); 239} 240 241// ----- Pricing billing cycle and Business capacity ----- 242const billingCycleButtons = Array.from(document.querySelectorAll('[data-billing-cycle]')); 243const pricingPlans = Array.from(document.querySelectorAll('[data-pricing-plan]')); 244let activeBillingCycle = 'monthly'; 245 246function updatePricingPlan(plan) { 247 const businessTier = plan.querySelector('[data-business-tier]'); 248 const selectedTier = businessTier?.selectedOptions?.[0]; 249 const monthlyPrice = Number(selectedTier?.dataset.monthlyPrice || plan.dataset.monthlyPrice || 0); 250 const credits = selectedTier?.dataset.credits || plan.dataset.credits || '0'; 251 const displayedMonthlyPrice = activeBillingCycle === 'yearly' 252 ? Math.round(monthlyPrice * 0.8) 253 : monthlyPrice; 254 const annualTotal = displayedMonthlyPrice * 12; 255 const price = plan.querySelector('[data-plan-price]'); 256 const billingNote = plan.querySelector('[data-plan-billing-note]'); 257 const creditsLabel = plan.querySelector('[data-plan-credits]'); 258 const activityLabel = plan.querySelector('[data-plan-activity]'); 259 260 if (price) price.textContent = `$${displayedMonthlyPrice}`; 261 if (creditsLabel) creditsLabel.textContent = credits; 262 if (activityLabel) { 263 activityLabel.textContent = selectedTier?.value === 'scale' 264 ? 'Built for your highest-volume agent activity' 265 : 'Built for high-volume activity across your team'; 266 } 267 if (billingNote) { 268 if (monthlyPrice === 0) { 269 billingNote.textContent = 'Free every month'; 270 } else if (activeBillingCycle === 'yearly') { 271 billingNote.textContent = `Billed $${annualTotal.toLocaleString('en-US')} yearly · Save 20%`; 272 } else { 273 billingNote.textContent = 'Billed monthly'; 274 } 275 } 276 277 plan.dataset.trackPrice = `$${displayedMonthlyPrice}`; 278 plan.dataset.trackBillingPeriod = activeBillingCycle; 279 if (selectedTier) plan.dataset.selectedTier = selectedTier.value; 280} 281 282function setBillingCycle(cycle) { 283 activeBillingCycle = cycle === 'yearly' ? 'yearly' : 'monthly'; 284 billingCycleButtons.forEach((button) => { 285 const isActive = button.dataset.billingCycle === activeBillingCycle; 286 button.classList.toggle('active', isActive); 287 button.setAttribute('aria-pressed', String(isActive)); 288 }); 289 pricingPlans.forEach(updatePricingPlan); 290} 291 292billingCycleButtons.forEach((button) => { 293 button.addEventListener('click', () => setBillingCycle(button.dataset.billingCycle)); 294}); 295 296document.querySelectorAll('[data-business-tier]').forEach((select) => { 297 select.addEventListener('change', () => updatePricingPlan(select.closest('[data-pricing-plan]'))); 298}); 299 300// ----- Interactive agent command center preview ----- 301const commandAgentData = Object.freeze({ 302 atlas: { 303 name: 'Invoice Manager', 304 role: 'Accounts receivable operations', 305 team: 'Operations', 306 responseLength: 'Balanced', 307 creativity: 'Precise', 308 reasoning: 'High', 309 credits: 40000, 310 instructions: 'Reconcile invoices, follow up professionally, and require approval before escalating unusual balances.', 311 label: 'Invoice Manager is working', 312 task: 'Reconciling this weekâs outstanding invoices', 313 meta: 'Started 8:42 AM · QuickBooks + Stripe', 314 progress: 78, 315 activity: [ 316 ['â', 'Matched 39 invoices to Stripe payments', '9:18 AM · completed'], 317 ['â', 'Sent 12 polite overdue reminders', '9:16 AM · email'], 318 ['!', 'Flagged two unusual balances for review', '9:12 AM · approval protected'],
319 ['â', 'Imported 47 open QuickBooks invoices', '8:44 AM · synced'], 320 ], 321 }, 322 luma: { 323 name: 'Lead Coordinator', 324 role: 'Customer intake coordinator', 325 team: 'Customer Experience', 326 responseLength: 'Concise', 327 creativity: 'Balanced', 328 reasoning: 'Standard', 329 credits: 40000, 330 instructions: 'Qualify inbound leads, answer from the approved playbook, and book qualified prospects on the team calendar.', 331 label: 'Lead Coordinator is working', 332 task: 'Qualifying six new website inquiries', 333 meta: 'Started 9:03 AM · Website + Calendar', 334 progress: 64, 335 activity: [ 336 ['â', 'Booked Maya Chen for Thursday at 2:30 PM', '9:21 AM · calendar'], 337 ['â', 'Sent intake questions to three new leads', '9:17 AM · SMS'], 338 ['â', 'Added four qualified contacts to the CRM', '9:11 AM · synced'], 339 ['â¢', 'Received six new website inquiries', '9:03 AM · trigger'], 340 ], 341 }, 342 relay: { 343 name: 'Alex', 344 role: 'Sales follow-up specialist', 345 team: 'Revenue', 346 responseLength: 'Concise', 347 creativity: 'Creative', 348 reasoning: 'High', 349 credits: 40000, 350 instructions: 'Research dormant opportunities and prepare personalized follow-ups. Never send outreach without approval.', 351 label: 'Alex is waiting for you', 352 task: 'Prepared 14 personalized lead follow-ups', 353 meta: 'Paused 9:14 AM · CRM + Gmail', 354 progress: 88, 355 activity: [ 356 ['!', 'Waiting for approval on 14 follow-ups', '9:14 AM · action required'], 357 ['â', 'Personalized messages from CRM context', '9:11 AM · drafted'], 358 ['â', 'Removed two opted-out contacts', '9:08 AM · compliance'], 359 ['â¢', 'Found 16 dormant opportunities', '9:05 AM · CRM'], 360 ], 361 }, 362}); 363 364const commandActivity = document.querySelector('[data-command-activity]'); 365const commandAgentLabel = document.querySelector('[data-command-agent-label]'); 366const commandAgentTask = document.querySelector('[data-command-agent-task]'); 367const commandAgentMeta = document.querySelector('[data-command-agent-meta]'); 368const commandProgress = document.querySelector('[data-command-progress]'); 369const commandActivityLabel = document.querySelector('[data-command-activity-label]'); 370const commandApproval = document.querySelector('[data-command-approval]'); 371 372function renderCommandAgent(agentKey) { 373 const agent = commandAgentData[agentKey]; 374 if (!agent || !commandActivity) return; 375 document.querySelectorAll('[data-command-agent]').forEach((button) => { 376 const selected = button.dataset.commandAgent === agentKey; 377 button.classList.toggle('active', selected); 378 button.setAttribute('aria-selected', String(selected)); 379 }); 380 if (commandAgentLabel) commandAgentLabel.textContent = agent.label; 381 if (commandAgentTask) commandAgentTask.textContent = agent.task; 382 if (commandAgentMeta) commandAgentMeta.textContent = agent.meta; 383 if (commandProgress) commandProgress.style.width = `${agent.progress}%`; 384 if (commandActivityLabel) commandActivityLabel.textContent = `${agent.name} · live log`; 385 commandActivity.replaceChildren(...agent.activity.map(([icon, text, meta]) => { 386 const item = document.createElement('div'); 387 item.className = 'command-activity-item'; 388 const iconElement = document.createElement('span'); 389 iconElement.className = 'command-activity-icon'; 390 iconElement.setAttribute('aria-hidden', 'true'); 391 iconElement.textContent = icon; 392 const copy = document.createElement('div'); 393 const title = document.createElement('strong'); 394 title.textContent = text; 395 const detail = document.createElement('small'); 396 detail.textContent = meta; 397 copy.append(title, detail); 398 item.append(iconElement, copy); 399 return item; 400 })); 401} 402 403document.querySelectorAll('[data-command-agent]').forEach((button) => { 404 button.addEventListener('click', () => renderCommandAgent(button.dataset.commandAgent)); 405}); 406 407const commandEditOverlay = document.querySelector('[data-command-edit-overlay]'); 408const commandEditForm = document.querySelector('[data-command-edit-form]'); 409const commandEditTitle = document.querySelector('[data-command-edit-title]'); 410let commandEditingAgentKey = ''; 411 412function setCommandEditField(name, value) { 413 const field = commandEditForm?.elements.namedItem(name);
414 if (field) field.value = String(value ?? ''); 415} 416 417function openCommandAgentEditor(agentKey) { 418 const agent = commandAgentData[agentKey]; 419 if (!agent || !commandEditOverlay || !commandEditForm) return; 420 commandEditingAgentKey = agentKey; 421 if (commandEditTitle) commandEditTitle.textContent = `Edit ${agent.name}`; 422 setCommandEditField('name', agent.name); 423 setCommandEditField('role', agent.role); 424 setCommandEditField('team', agent.team); 425 setCommandEditField('responseLength', agent.responseLength); 426 setCommandEditField('creativity', agent.creativity); 427 setCommandEditField('reasoning', agent.reasoning); 428 setCommandEditField('credits', agent.credits); 429 setCommandEditField('instructions', agent.instructions); 430 commandEditOverlay.hidden = false; 431 commandEditForm.elements.namedItem('name')?.focus(); 432} 433 434function closeCommandAgentEditor() { 435 if (!commandEditOverlay) return; 436 commandEditOverlay.hidden = true; 437 commandEditingAgentKey = ''; 438} 439 440document.querySelectorAll('[data-command-edit-agent]').forEach((button) => { 441 button.addEventListener('click', () => openCommandAgentEditor(button.dataset.commandEditAgent)); 442}); 443document.querySelectorAll('[data-command-edit-close]').forEach((button) => { 444 button.addEventListener('click', closeCommandAgentEditor); 445}); 446commandEditForm?.addEventListener('submit', (event) => { 447 event.preventDefault(); 448 const agent = commandAgentData[commandEditingAgentKey]; 449 if (!agent) return; 450 const formData = new FormData(commandEditForm); 451 const previousName = agent.name; 452 agent.name = String(formData.get('name') || previousName).trim() || previousName; 453 agent.role = String(formData.get('role') || agent.role).trim() || agent.role; 454 agent.team = String(formData.get('team') || agent.team); 455 agent.responseLength = String(formData.get('responseLength') || agent.responseLength); 456 agent.creativity = String(formData.get('creativity') || agent.creativity); 457 agent.reasoning = String(formData.get('reasoning') || agent.reasoning); 458 agent.credits = Math.max(0, Number(formData.get('credits') || 0)); 459 agent.instructions = String(formData.get('instructions') || '').trim(); 460 agent.label = agent.label.replace(previousName, agent.name); 461 462 const overviewRow = document.querySelector(`[data-command-agent="${commandEditingAgentKey}"]`); 463 const overviewName = overviewRow?.querySelector('strong'); 464 const overviewRole = overviewRow?.querySelector('small'); 465 const tableName = document.querySelector(`[data-command-agent-table-name="${commandEditingAgentKey}"]`); 466 const tableRole = document.querySelector(`[data-command-agent-table-role="${commandEditingAgentKey}"]`); 467 if (overviewName) overviewName.textContent = agent.name; 468 if (overviewRole) overviewRole.textContent = agent.role; 469 if (tableName) tableName.textContent = agent.name; 470 if (tableRole) tableRole.textContent = agent.role; 471 if (overviewRow?.classList.contains('active')) renderCommandAgent(commandEditingAgentKey); 472 closeCommandAgentEditor(); 473}); 474document.addEventListener('keydown', (event) => { 475 if (event.key === 'Escape' && commandEditOverlay && !commandEditOverlay.hidden) closeCommandAgentEditor(); 476}); 477 478const commandThreadData = Object.freeze({ 479 maya: { 480 name: 'Maya Chen', 481 context: 'Website lead · Lead Coordinator', 482 status: 'Qualified', 483 messages: [ 484 ['agent', 'Hi Maya â I can help schedule your consultation. Does Thursday afternoon work?'], 485 ['user', 'Thursday at 2:30 works perfectly.'], 486 ['agent', 'Youâre booked. I sent the calendar invitation and intake details.'], 487 ], 488 }, 489 northwind: { 490 name: 'Northwind Co.', 491 context: 'Dormant opportunity · Alex', 492 status: 'Review', 493 messages: [ 494 ['agent', 'I found a relevant reason to reconnect based on Northwindâs new distribution launch.'], 495 ['user', 'Keep it concise and mention our logistics case study.'], 496 ['agent', 'Done. The personalized follow-up is ready for your approval before anything sends.'], 497 ], 498 }, 499 'invoice-1048': { 500 name: 'Invoice #1048', 501 context: 'Accounts receivable · Invoice Manager', 502 status: 'Paid', 503 messages: [ 504 ['agent', 'Stripe recorded the $2,480 payment for invoice #1048.'], 505 ['agent', 'I matched the payment in QuickBooks and closed the outstanding balance.'],
506 ['user', 'Perfect â include it in todayâs reconciliation summary.'], 507 ], 508 }, 509}); 510 511const commandConversationName = document.querySelector('[data-command-conversation-name]'); 512const commandConversationContext = document.querySelector('[data-command-conversation-context]'); 513const commandConversationStatus = document.querySelector('[data-command-conversation-status]'); 514const commandConversationMessages = document.querySelector('[data-command-conversation-messages]'); 515 516function renderCommandThread(threadKey) { 517 const thread = commandThreadData[threadKey]; 518 if (!thread || !commandConversationMessages) return; 519 document.querySelectorAll('[data-command-thread]').forEach((button) => { 520 const selected = button.dataset.commandThread === threadKey; 521 button.classList.toggle('active', selected); 522 button.setAttribute('aria-pressed', String(selected)); 523 }); 524 if (commandConversationName) commandConversationName.textContent = thread.name; 525 if (commandConversationContext) commandConversationContext.textContent = thread.context; 526 if (commandConversationStatus) commandConversationStatus.textContent = thread.status; 527 commandConversationMessages.replaceChildren(...thread.messages.map(([role, message]) => { 528 const bubble = document.createElement('div'); 529 bubble.className = `command-message ${role}`; 530 bubble.textContent = message; 531 return bubble; 532 })); 533} 534 535const commandRunData = Object.freeze({ 536 invoices: { 537 title: 'Invoice reconciliation', 538 label: 'Run timeline · live', 539 timeline: [ 540 ['â', 'Loaded 47 open invoices', 'QuickBooks · 8:44 AM'], 541 ['â', 'Matched 39 Stripe payments', 'Stripe · 9:18 AM'], 542 ['â', 'Sent 12 overdue reminders', 'Gmail · 9:21 AM'], 543 ['â¦', 'Reviewing unmatched balances', 'In progress · 78% complete'], 544 ], 545 }, 546 leads: { 547 title: 'Qualify website inquiries', 548 label: 'Completed · 2m 14s', 549 timeline: [ 550 ['â', 'Received six new website inquiries', 'Website · 9:03 AM'], 551 ['â', 'Enriched company and contact details', 'CRM · 9:04 AM'], 552 ['â', 'Qualified four high-intent leads', 'Scoring rules · 9:05 AM'], 553 ['â', 'Booked Maya Chen for Thursday', 'Calendar · 9:05 AM'], 554 ], 555 }, 556 'follow-ups': { 557 title: 'Dormant lead follow-up', 558 label: 'Waiting for approval', 559 timeline: [ 560 ['â', 'Found 16 dormant opportunities', 'CRM · 9:05 AM'], 561 ['â', 'Removed two opted-out contacts', 'Compliance · 9:08 AM'], 562 ['â', 'Drafted 14 personalized messages', 'Gmail · 9:11 AM'], 563 ['!', 'Paused before sending', 'Approval required · 9:14 AM'], 564 ], 565 }, 566}); 567 568const commandRunTitle = document.querySelector('[data-command-run-title]'); 569const commandRunLabel = document.querySelector('[data-command-run-label]'); 570const commandRunTimeline = document.querySelector('[data-command-run-timeline]'); 571 572function renderCommandRun(runKey) { 573 const run = commandRunData[runKey]; 574 if (!run || !commandRunTimeline) return; 575 document.querySelectorAll('[data-command-run]').forEach((button) => { 576 const selected = button.dataset.commandRun === runKey; 577 button.classList.toggle('active', selected); 578 button.setAttribute('aria-pressed', String(selected)); 579 }); 580 if (commandRunTitle) commandRunTitle.textContent = run.title; 581 if (commandRunLabel) commandRunLabel.textContent = run.label; 582 commandRunTimeline.replaceChildren(...run.timeline.map(([icon, title, detail]) => { 583 const row = document.createElement('p'); 584 const iconElement = document.createElement('i'); 585 iconElement.textContent = icon; 586 const copy = document.createElement('span'); 587 const heading = document.createElement('strong'); 588 heading.textContent = title; 589 const metadata = document.createElement('small'); 590 metadata.textContent = detail; 591 copy.append(heading, metadata); 592 row.append(iconElement, copy); 593 return row; 594 })); 595} 596 597document.querySelectorAll('[data-command-thread]').forEach((button) => { 598 button.addEventListener('click', () => renderCommandThread(button.dataset.commandThread)); 599}); 600document.querySelectorAll('[data-command-run]').forEach((button) => { 601 button.addEventListener('click', () => renderCommandRun(button.dataset.commandRun)); 602}); 603 604function showCommandPanel(panelKey) { 605 document.querySelectorAll('[data-command-panel-content]').forEach((panel) => { 606 panel.hidden = panel.dataset.commandPanelContent !== panelKey; 607 }); 608 document.querySelectorAll('[data-command-panel]').forEach((button) => { 609 const selected = button.dataset.commandPanel === panelKey; 610 button.classList.toggle('active', selected); 611 button.setAttribute('aria-pressed', String(selected)); 612 }); 613} 614 615document.querySelectorAll('[data-command-panel]').forEach((button) => { 616 button.addEventListener('click', () => showCommandPanel(button.dataset.commandPanel)); 617}); 618document.querySelectorAll('[data-preview-subtab]').forEach((button) => { 619 button.addEventListener('click', () => { 620 const panel = button.closest('[data-command-panel-content]'); 621 if (!panel) return; 622 const target = button.dataset.previewSubtab; 623 const group = button.closest('[data-preview-group]'); 624 group?.querySelectorAll('[data-preview-subtab]').forEach((item) => item.classList.toggle('active', item === button)); 625 panel.querySelectorAll('[data-preview-subpanel]').forEach((subpanel) => { 626 subpanel.hidden = subpanel.dataset.previewSubpanel !== target; 627 }); 628 }); 629}); 630document.querySelector('[data-command-review]')?.addEventListener('click', () => renderCommandAgent('relay')); 631document.querySelector('[data-command-approve]')?.addEventListener('click', (event) => { 632 if (!commandApproval || commandApproval.classList.contains('approved')) return; 633 commandApproval.classList.add('approved'); 634 const icon = commandApproval.querySelector('.command-approval-icon'); 635 const label = commandApproval.querySelector('span'); 636 const title = commandApproval.querySelector('strong'); 637 const detail = commandApproval.querySelector('small'); 638 if (icon) icon.textContent = 'â'; 639 if (label) label.textContent = 'Approved and released'; 640 if (title) title.textContent = 'Alex is sending 14 approved follow-ups.'; 641 if (detail) detail.textContent = 'The approval is recorded in the run history.'; 642 event.currentTarget.textContent = 'Approved'; 643 event.currentTarget.disabled = true; 644}); 645 646renderCommandAgent('atlas'); 647renderCommandThread('maya'); 648renderCommandRun('invoices'); 649 650// ----- Ask Nerova sidebar ----- 651let chatOverlayLoadPromise = null; 652let chatPreloadTimer = null; 653let chatPreloadStarted = false;
654function waitForNerovaSidebar() { 655 const startedAt = Date.now(); 656 return new Promise((resolve) => { 657 const tick = () => { 658 const api = window.NerovaSiteChatSidebar; 659 if (api && typeof api.open === 'function') { 660 resolve(api); 661 return; 662 } 663 if (Date.now() - startedAt > 2500) { 664 resolve(null); 665 return; 666 } 667 setTimeout(tick, 50); 668 }; 669 tick(); 670 }); 671} 672 673function ensureChatOverlay() { 674 if (window.NerovaSiteChatSidebar && typeof window.NerovaSiteChatSidebar.open === 'function') { 675 return Promise.resolve(window.NerovaSiteChatSidebar); 676 } 677 if (chatOverlayLoadPromise) return chatOverlayLoadPromise; 678 chatOverlayLoadPromise = new Promise((resolve, reject) => { 679 const existing = document.getElementById('nerova-site-chat-overlay-script'); 680 if (existing) { 681 existing.addEventListener('load', () => waitForNerovaSidebar().then(resolve), { once: true }); 682 existing.addEventListener('error', () => reject(new Error('Unable to load Nerova chat.')), { once: true }); 683 waitForNerovaSidebar().then(resolve); 684 return; 685 } 686 const script = document.createElement('script'); 687 script.id = 'nerova-site-chat-overlay-script'; 688 script.src = '/site-chat-overlay.js'; 689 script.async = true; 690 script.onload = () => waitForNerovaSidebar().then(resolve); 691 script.onerror = () => reject(new Error('Unable to load Nerova chat.')); 692 document.body.appendChild(script); 693 }); 694 return chatOverlayLoadPromise; 695} 696 697function cancelChatPreloadTimer() { 698 if (!chatPreloadTimer) return; 699 clearTimeout(chatPreloadTimer); 700 chatPreloadTimer = null; 701} 702 703function preloadNerovaChatOnce() { 704 if (chatPreloadStarted) return; 705 chatPreloadStarted = true; 706 const preloadChat = () => ensureChatOverlay() 707 .then((api) => { 708 if (api && typeof api.preload === 'function') api.preload(); 709 }) 710 .catch(() => {}); 711 preloadChat(); 712} 713 714function scheduleNerovaChatPreload() { 715 if (mobileViewport && typeof window.requestIdleCallback !== 'function') return; 716 const delayMs = mobileViewport ? 6000 : 2500; 717 chatPreloadTimer = setTimeout(() => { 718 chatPreloadTimer = null; 719 const runPreload = () => preloadNerovaChatOnce(); 720 if (typeof window.requestIdleCallback === 'function') { 721 window.requestIdleCallback(runPreload, { timeout: mobileViewport ? 4000 : 1500 }); 722 return; 723 } 724 runPreload(); 725 }, delayMs); 726} 727 728window.openNerovaChat = function (mode) { 729 cancelChatPreloadTimer(); 730 const promptByMode = { 731 audit: 'Identify automation opportunities', 732 demo: 'Generate chatbot', 733 'generate-chatbot': 'Generate chatbot', 734 ask: '', 735 }; 736 ensureChatOverlay() 737 .then((api) => { 738 if (!api || typeof api.open !== 'function') return; 739 if (mode === 'ask' && typeof api.isOpen === 'function' && api.isOpen() && typeof api.close === 'function') { 740 api.close(); 741 return; 742 } 743 api.open({ 744 selectedAgent: mode === 'audit' ? 'Scope' : (mode === 'demo' || mode === 'generate-chatbot' ? 'Genie' : 'Nerova'), 745 initialMessage: promptByMode[mode] || '', 746 autoSend: mode === 'audit' || mode === 'demo' || mode === 'generate-chatbot', 747 sendMessage: mode === 'audit' || mode === 'demo' || mode === 'generate-chatbot', 748 forceFreshThread: mode === 'audit' || mode === 'demo' || mode === 'generate-chatbot', 749 }); 750 }) 751 .catch(() => {}); 752}; 753 754document.querySelectorAll('[data-open-chat]').forEach((el) => { 755 el.addEventListener('click', (event) => { 756 event.preventDefault(); 757 window.openNerovaChat(el.dataset.openChat || 'ask'); 758 }); 759}); 760 761const landingChatUrl = new URL(window.location.href); 762if (landingChatUrl.searchParams.get('chat') === 'generate-assistant') { 763 landingChatUrl.searchParams.delete('chat'); 764 window.history.replaceState(null, '', `${landingChatUrl.pathname}${landingChatUrl.search}${landingChatUrl.hash}`); 765 window.openNerovaChat('generate-chatbot'); 766} 767 768scheduleNerovaChatPreload(); 769 770// ----- Public API, tracking, and purchase ----- 771const TRACKER_VISITOR_KEY = 'nrv_vid_public'; 772const TRACKER_LEAD_KEY = 'nrv_lid_public'; 773const TRACKER_SESSION_KEY = 'nrv_sid'; 774const TRACKER_SESSION_LAST_ACTIVE_KEY = 'nrv_sid_last_active_at'; 775const TRACKER_SESSION_STARTED_KEY = 'nrv_sid_started_at'; 776const TRACKER_DEDUPE_KEY = 'nrv_tracking_dedupe_v2'; 777const TRACKER_SESSION_TTL_MS = 2 * 60 * 60 * 1000; 778const TRACKER_HEARTBEAT_INTERVALS_MS = Object.freeze({ 779 initial: 15 * 1000, 780 afterThirtyMinutes: 45 * 1000, 781 afterOneHour: 60 * 1000, 782 afterNinetyMinutes: 150 * 1000, 783 hidden: 150 * 1000, 784}); 785const TRACKER_SCROLL_THRESHOLDS = [25, 50, 75, 100]; 786const TRACKER_SECTION_VISIBLE_RATIO = 0.6; 787const TRACKER_SECTION_CONTINUOUS_MS = 6000; 788const TRACKER_SECTION_CUMULATIVE_MS = 10000; 789const TRACKER_SECTION_TICK_MS = 500; 790const TRACKER_DEDUPE_LIMIT = 250; 791const TRACKER_EVENT_DEDUPE_MS = { 792 page_view: 5000, 793 modal_opened: 1200, 794 modal_closed: 1200, 795 cta_button_clicked: 1200, 796 section_engaged: 30000, 797 form_event: 1200, 798 scroll_depth_reached: 1200, 799 page_exit_summary: 5000, 800 client_error: 30000, 801 performance_issue: 60000, 802}; 803const LOCAL_APP_BASE_PORT = 3000; 804const LOCAL_SIGN_IN_APP_PORT = 4000; 805const LOCAL_SITE_BASE_PORT = 5173; 806const LIVE_APP_ORIGIN = 'https://app.nerova.ai'; 807const DEFAULT_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || 'America/Ne
807w_York'; 808 809function isLocalHost(hostname) { 810 return hostname === 'localhost' || hostname === '127.0.0.1' || /^10\./.test(hostname) || /^192\.168\./.test(hostname) || /^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname); 811} 812 813function getLocalPortOffset() { 814 const port = Number(window.location.port || 0); 815 if (!Number.isFinite(port)) return 0; 816 if (port >= 5173 && port <= 5199) return port - LOCAL_SITE_BASE_PORT; 817 if (port >= 3000 && port <= 3026) return port - LOCAL_APP_BASE_PORT; 818 return 0; 819} 820 821function getPublicApiUrl(path) { 822 const normalizedPath = String(path || '').startsWith('/') ? String(path || '') : `/${path || ''}`; 823 if (isLocalHost(window.location.hostname)) { 824 return `${window.location.protocol}//${window.location.hostname}:${LOCAL_APP_BASE_PORT + getLocalPortOffset()}${normalizedPath}`; 825 } 826 return `${LIVE_APP_ORIGIN}${normalizedPath}`; 827} 828 829function getPublicAppLink(path) { 830 const normalizedPath = String(path || '').startsWith('/') ? String(path || '') : `/${path || ''}`; 831 if (isLocalHost(window.location.hostname)) { 832 return `${window.location.protocol}//${window.location.hostname}:${LOCAL_SIGN_IN_APP_PORT}${normalizedPath}`; 833 } 834 return `${LIVE_APP_ORIGIN}${normalizedPath}`; 835} 836 837document.querySelectorAll('[data-app-link]').forEach((link) => { 838 link.href = getPublicAppLink(link.getAttribute('data-app-link') || '/login'); 839}); 840 841function readStoredIdentity() { 842 return { 843 visitorId: String(window.localStorage.getItem(TRACKER_VISITOR_KEY) || '').trim(), 844 leadId: String(window.localStorage.getItem(TRACKER_LEAD_KEY) || '').trim(), 845 sessionId: String(window.sessionStorage.getItem(TRACKER_SESSION_KEY) || '').trim(), 846 }; 847} 848 849function safeRandomId(prefix) { 850 try { 851 return `${prefix}_${window.crypto?.randomUUID?.() || `${Date.now()}_${Math.random().toString(36).slice(2, 12)}`}`; 852 } catch { 853 return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`; 854 } 855} 856 857function readStorage(storage, key) { 858 try { 859 return String(storage.getItem(key) || '').trim(); 860 } catch { 861 return ''; 862 } 863} 864 865function writeStorage(storage, key, value) { 866 try { 867 storage.setItem(key, String(value || '')); 868 } catch {} 869} 870 871function readJsonStorage(storage, key) { 872 try { 873 return JSON.parse(storage.getItem(key) || '{}') || {}; 874 } catch { 875 return {}; 876 } 877} 878 879function writeJsonStorage(storage, key, value) { 880 try { 881 storage.setItem(key, JSON.stringify(value || {})); 882 } catch {} 883} 884 885function getOrCreateVisitorId() { 886 const existing = readStorage(window.localStorage, TRACKER_VISITOR_KEY); 887 if (existing) return existing; 888 const next = safeRandomId('vis'); 889 writeStorage(window.localStorage, TRACKER_VISITOR_KEY, next); 890 return next; 891} 892 893function getOrCreateSessionId() { 894 const now = Date.now(); 895 let startedAt = Number(readStorage(window.sessionStorage, TRACKER_SESSION_STARTED_KEY) || 0); 896 let sessionId = readStorage(window.sessionStorage, TRACKER_SESSION_KEY); 897 if (!sessionId || !Number.isFinite(startedAt) || startedAt <= 0 || now - startedAt >= TRACKER_SESSION_TTL_MS) { 898 sessionId = safeRandomId('sess'); 899 startedAt = now; 900 writeStorage(window.sessionStorage, TRACKER_SESSION_KEY, sessionId); 901 writeStorage(window.sessionStorage, TRACKER_SESSION_STARTED_KEY, String(startedAt)); 902 } 903 writeStorage(window.sessionStorage, TRACKER_SESSION_LAST_ACTIVE_KEY, Str
903ing(now)); 904 return sessionId; 905} 906 907function getHeartbeatIntervalMs() { 908 getOrCreateSessionId(); 909 if (document.hidden) return TRACKER_HEARTBEAT_INTERVALS_MS.hidden; 910 const now = Date.now(); 911 const startedAt = Number(readStorage(window.sessionStorage, TRACKER_SESSION_STARTED_KEY) || now); 912 const elapsedMs = Math.max(0, now - startedAt); 913 if (elapsedMs >= 90 * 60 * 1000) return TRACKER_HEARTBEAT_INTERVALS_MS.afterNinetyMinutes; 914 if (elapsedMs >= 60 * 60 * 1000) return TRACKER_HEARTBEAT_INTERVALS_MS.afterOneHour; 915 if (elapsedMs >= 30 * 60 * 1000) return TRACKER_HEARTBEAT_INTERVALS_MS.afterThirtyMinutes; 916 return TRACKER_HEARTBEAT_INTERVALS_MS.initial; 917} 918 919function persistTrackingIdentity(payload) { 920 if (!payload || typeof payload !== 'object') return; 921 if (payload.visitorId) writeStorage(window.localStorage, TRACKER_VISITOR_KEY, payload.visitorId); 922 if (payload.leadId) writeStorage(window.localStorage, TRACKER_LEAD_KEY, payload.leadId); 923 if (payload.sessionId) writeStorage(window.sessionStorage, TRACKER_SESSION_KEY, payload.sessionId); 924} 925 926function compactObject(value) { 927 const out = {}; 928 Object.entries(value || {}).forEach(([key, entry]) => { 929 if (entry === undefined || entry === null || entry === '') return; 930 out[key] = entry; 931 }); 932 return out; 933} 934 935function normalizeTrackerEventName(value) { 936 return String(value || '') 937 .trim() 938 .replace(/([a-z0-9])([A-Z])/g, '$1_$2') 939 .replace(/[^a-zA-Z0-9]+/g, '_') 940 .replace(/^_+|_+$/g, '') 941 .toLowerCase() 942 .slice(0, 120); 943} 944 945function normalizeTrackerPath(value) { 946 const raw = String(value || '').trim(); 947 if (!raw) return '/'; 948 try { 949 const parsed = new URL(raw, window.location.origin); 950 return `${parsed.pathname || '/'}${parsed.search || ''}`; 951 } catch { 952 const noHash = raw.split('#')[0] || '/'; 953 return noHash.startsWith('/') ? noHash : `/${noHash}`; 954 } 955} 956 957function stableSerialize(value) { 958 if (value == null) return ''; 959 if (Array.isArray(value)) return `[${value.map((item) => stableSerialize(item)).join(',')}]`; 960 if (typeof value === 'object') { 961 return `{${Object.keys(value).sort().map((key) => `${key}:${stableSerialize(value[key])}`).join(',')}}`; 962 } 963 return String(value); 964} 965 966function shouldSuppressDuplicate(eventName, path, properties, options) { 967 const normalized = normalizeTrackerEventName(eventName); 968 const dedupeMs = Number.isFinite(Number(options?.dedupeMs)) 969 ? Number(options.dedupeMs) 970 : (TRACKER_EVENT_DEDUPE_MS[normalized] || 0); 971 if (!dedupeMs || dedupeMs <= 0) return false; 972 const dedupeKey = String(options?.dedupeKey || `${normalized}:${path}:${stableSerialize(properties || {})}`); 973 const now = Date.now(); 974 const state = readJsonStorage(window.sessionStorage, TRACKER_DEDUPE_KEY); 975 Object.entries(state).forEach(([key, seenAt]) => { 976 if (now - Number(seenAt || 0) > Math.max(dedupeMs * 3, 15000)) delete state[key]; 977 }); 978 const lastSeenAt = Number(state[dedupeKey] || 0); 979 if (lastSeenAt && now - lastSeenAt < dedupeMs) { 980 writeJsonStorage(window.sessionStorage, TRACKER_DEDUPE_KEY, state); 981 return true; 982 } 983 state[dedupeKey] = now; 984 const entries = Object.entries(state) 985 .sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0)) 986 .slice(0, TRACKER_DEDUPE_LIMIT); 987 writeJsonStorage(window.sessionStorage, TRACKER_DEDUPE_KEY, Object.fromEntries(entries)); 988 return false; 989} 990 991function collectClientContext() { 992 const navInfo = navigator || {}; 993 const connection = navInfo.connection || navInfo.mozConnection || navInfo.webkitConnection || {}; 994 const automation = { 995 webdriver: navInfo.webdriver === true, 996 playwright_binding: typeof window.__playwright__binding__ !== 'undefined', 997 playwright_init_scripts: typeof window.__pwInitScripts !== 'undefined', 998 puppeteer: typeof window.__puppeteer_evaluation_script__ !== 'undefined', 999 selenium: typeof document?.$cdc_asdjflasutopfhvcZLmcfl_ !== 'undefined', 1000 phantom: typeof window._phantom !== 'undefined' || typeof window.callPhantom !== 'undefined', 1001 dom_automation: typeof window.domAutomation !== 'undefined' || typeof window.domAutomationController !== 'undefined', 1002 plugins_length: Number(navInfo.plugins?.length || 0), 1003 languages_length: Array.isArray(navInfo.languages) ? navInfo.languages.length : 0, 1004 cookies_enabled: navInfo.cookieEnabled === true, 1005 }; 1006 return compactObject({ 1007 screen_width: window.screen?.width || null, 1008 screen_height: window.screen?.height || null, 1009 viewport_width: window.innerWidth || null, 1010 viewport_height: window.innerHeight || null, 1011 color_depth: window.screen?.colorDepth || null, 1012 pixel_ratio: window.devicePixelRatio || null, 1013 touch_enabled: 'ontouchstart' in window, 1014 max_touch_points: navInfo.maxTouchPoints || 0, 1015 hardware_concurrency: navInfo.hardwareConcurrency || null, 1016 device_memory: navInfo.deviceMemory || null, 1017 connection_type: connection.type || null, 1018 effective_connection_type: connection.effectiveType || null, 1019 downlink: connection.downlink || null, 1020 rtt: connection.rtt || null, 1021 save_data: connection.saveData === true, 1022 cookies_enabled: navInfo.cookieEnabled, 1023 storage_enabled: true, 1024 do_not_track: navInfo.doNotTrack || window.doNotTrack || '', 1025 timezone_offset: new Date().getTimezoneOffset(), 1026 client_timezone: DEFAULT_TIMEZONE, 1027 automation, 1028 }); 1029} 1030 1031function collectAttribution(urlValue) { 1032 const url = new URL(String(urlValue || window.location.href), window.location.origin); 1033 const search = url.searchParams; 1034 return { 1035 attribution: compactObject({ 1036 utm_source: search.get('utm_source') || '', 1037 utm_medium: search.get('utm_medium') || '', 1038 utm_campaign: search.get('utm_campaign') || '',
1039 utm_content: search.get('utm_content') || '', 1040 utm_term: search.get('utm_term') || '', 1041 }), 1042 clickIds: compactObject({ 1043 gclid: search.get('gclid') || '', 1044 msclkid: search.get('msclkid') || '', 1045 ttclid: search.get('ttclid') || '', 1046 }), 1047 }; 1048} 1049 1050function getTrackingIdentity() { 1051 return { 1052 visitorId: getOrCreateVisitorId(), 1053 leadId: readStorage(window.localStorage, TRACKER_LEAD_KEY), 1054 sessionId: getOrCreateSessionId(), 1055 }; 1056} 1057 1058function buildTrackingPayload(extra) { 1059 const identity = getTrackingIdentity(); 1060 const url = String(extra?.url || window.location.href); 1061 const attribution = collectAttribution(url); 1062 return compactObject({ 1063 mode: 'website', 1064 visitorId: identity.visitorId, 1065 leadId: identity.leadId || undefined, 1066 sessionId: identity.sessionId, 1067 path: normalizeTrackerPath(extra?.path || window.location.pathname || '/'), 1068 url, 1069 referrer: document.referrer || undefined, 1070 ts: Date.now(), 1071 attribution: attribution.attribution, 1072 clickIds: attribution.clickIds, 1073 clientHints: collectClientContext(), 1074 ...(extra || {}), 1075 }); 1076} 1077 1078async function sendTrackingRequest(path, body, keepalive) { 1079 const response = await fetch(getPublicApiUrl(path), { 1080 method: 'POST', 1081 credentials: 'include', 1082 keepalive: keepalive !== false, 1083 headers: { 'Content-Type': 'application/json' }, 1084 body: JSON.stringify(body), 1085 }); 1086 if (!response.ok) return null; 1087 return response.json().catch(() => null); 1088} 1089 1090let trackingStartPromise = null; 1091function ensureTrackingStarted(extra) { 1092 if (!trackingStartPromise) { 1093 trackingStartPromise = sendTrackingRequest('/c/sync-a', buildTrackingPayload({ 1094 eventName: 'page_view', 1095 eventId: safeRandomId('page_view'), 1096 properties: compactObject({ 1097 current_url: window.location.href, 1098 source: 'new_landing_page', 1099 title: document.title || 'Nerova', 1100 ...(extra?.properties || {}), 1101 }), 1102 ...(extra || {}), 1103 }), true) 1104 .then((payload) => { 1105 persistTrackingIdentity(payload); 1106 return payload; 1107 }) 1108 .catch(() => null); 1109 } 1110 return trackingStartPromise; 1111} 1112 1113function trackLandingEvent(eventName, properties, options) { 1114 const normalized = normalizeTrackerEventName(eventName); 1115 if (!normalized) return Promise.resolve(null); 1116 const path = normalizeTrackerPath(options?.path || window.location.pathname || '/'); 1117 const url = String(options?.url || window.location.href); 1118 const eventProperties = compactObject({ 1119 current_url: url, 1120 source: 'new_landing_page', 1121 ...(properties || {}), 1122 }); 1123 if (shouldSuppressDuplicate(normalized, path, eventProperties, options || {})) return Promise.resolve(null); 1124 return ensureTrackingStarted() 1125 .then(() => sendTrackingRequest('/c/sync-a', buildTrackingPayload({ 1126 eventName: normalized, 1127 eventId: options?.eventId || safeRandomId(normalized), 1128 properties: eventProperties, 1129 path, 1130 url, 1131 }), options?.keepalive !== false)) 1132 .then((payload) => { 1133 persistTrackingIdentity(payload); 1134 return payload; 1135 }) 1136 .catch(() => null); 1137} 1138 1139window.NerovaTracker = { 1140 start: ensureTrackingStarted, 1141 track: trackLandingEvent, 1142 heartbeat: () => sendTrackingRequest('/c/hrtbt-b', buildTrackingPayload({ properties: { current_url: window.location.href, source: 'new_landing_page' } }), true).catch(() => null), 1143 getIdentity: getTrackingIdentity, 1144}; 1145 1146ensureTrackingStarted(); 1147let trackerHeartbeatTimer = null; 1148const scheduleTrackerHeartbeat = () => { 1149 if (trackerHeartbeatTimer !== null) window.clearTimeout(trackerHeartbeatTimer); 1150 trackerHeartbeatTimer = window.setTimeout(() => { 1151 void window.NerovaTracker.heartbeat(); 1152 scheduleTrackerHeartbeat(); 1153 }, getHeartbeatIntervalMs()); 1154}; 1155const handleTrackerVisibilityChange = () => { 1156 void window.NerovaTracker.heartbeat(); 1157 scheduleTrackerHeartbeat(); 1158}; 1159scheduleTrackerHeartbeat(); 1160document.addEventListener('visibilitychange', handleTrackerVisibilityChange); 1161window.addEventListener('pagehide', () => { 1162 if (trackerHeartbeatTimer !== null) window.clearTimeout(trackerHeartbeatTimer); 1163 void window.NerovaTracker.heartbeat(); 1164}, { once: true }); 1165 1166function readData(element, name) { 1167 if (!element) return ''; 1168 return String(element.dataset?.[name] || element.getAttribute(`data-${name.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)}`) || '').trim(); 1169} 1170 1171function textFromElement(element, maxLen) { 1172 return String(element?.textContent || '').replace(/\s+/g, ' ').trim().slice(0, maxLen || 120); 1173} 1174 1175function slugify(value, fallback) { 1176 const slug = String(value || '') 1177 .trim() 1178 .toLowerCase() 1179 .replace(/[^a-z0-9]+/g, '_') 1180 .replace(/^_+|_+$/g, '') 1181 .slice(0, 80); 1182 return slug || fallback; 1183} 1184 1185function getSourceSection(element) { 1186 const section = element?.closest('[data-track-section], section[id], section'); 1187 if (!section) return ''; 1188 return readData(section, 'trackSectionId') || section.id || slugify(textFromElement(section.querySelector('h1,h2,h3'), 80), 'section'); 1189} 1190 1191const pageSummary = { 1192 startedAt: Date.now(), 1193 maxScrollDepth: 0, 1194 sectionsEngaged: new Set(), 1195 pricingCardsEngaged: new Set(), 1196 ctaClickCount: 0, 1197 formsStarted: new Set(), 1198 formsSubmitted: new Set(), 1199 conversionCompleted: false, 1200 sent: false, 1201}; 1202 1203function getScrollDepthPercent() { 1204 const scrollable = Math.max( 1205 document.documentElement.scrollHeight || 0, 1206 document.body.scrollHeight || 0, 1207 ) - window.innerHeight; 1208 if (scrollable <= 0) return 100; 1209 return Math.min(100, Math.max(0, ((window.scrollY || window.pageYOffset || 0) / scrollable) * 100)); 1210} 1211 1212function getCtaElement(target) { 1213 const element = target?.closest?.('[data-track-cta], [data-track-cta-id], a[data-track-label], button[data-track-label]'); 1214 if (!element) return null; 1215 if (element instanceof HTMLButtonElement && element.disabled) return null; 1216 return element; 1217} 1218 1219function getElementLabel(element) { 1220 return readData(element, 'trackLabel') 1221 || element.getAttribute('aria-label') 1222 || element.getAttribute('title') 1223 || textFromElement(element, 120) 1224 || ''; 1225} 1226 1227function normalizeDestinationUrl(raw) { 1228 try { 1229 return new URL(raw, window.location.href); 1230 } catch { 1231 return null; 1232 } 1233} 1234 1235document.addEventListener('click', (event) => { 1236 const cta = getCtaElement(event.target instanceof Element ? event.target : null); 1237 if (!cta) return; 1238 const destination = cta instanceof HTMLAnchorElement && cta.href ? normalizeDestinationUrl(cta.href) : null; 1239 const ctaId = readData(cta, 'trackCtaId') || readData(cta, 'trackCta') || slugify(getElementLabel(cta), 'cta'); 1240 pageSummary.ctaClickCount += 1; 1241 void trackLandingEvent('ctaButtonClicked', { 1242 event_family: 'cta', 1243 cta_id: ctaId, 1244 cta_label: getElementLabel(cta), 1245 cta_location: readData(cta, 'trackLocation') || readData(cta, 'trackCtaLocation') || getSourceSection(cta), 1246 cta_position: readData(cta, 'trackPosition') || undefined, 1247 source_section: readData(cta, 'trackSourceSection') || getSourceSection(cta), 1248 destination_url: destination?.toString(), 1249 destination_host: destination?.host, 1250 plan: readData(cta, 'trackPlan') || readData(cta, 'plan') || undefined, 1251 agent: readData(cta, 'trackAgent') || undefined, 1252 }, { 1253 dedupeKey: `cta_button_clicked:${window.location.pathname}:${ctaId}:${destination?.toString() || ''}`, 1254 dedupeMs: 1200, 1255 keepalive: true, 1256 }); 1257}, true); 1258 1259const firedScrollDepths = new Set(); 1260function trackScrollDepth() { 1261 const depth = Math.floor(getScrollDepthPercent()); 1262 pageSummary.maxScrollDepth = Math.max(pageSummary.maxScrollDepth, depth); 1263 TRACKER_SCROLL_THRESHOLDS.forEach((threshold) => { 1264 if (depth < threshold || firedScrollDepths.has(threshold)) return; 1265 firedScrollDepths.add(threshold); 1266 void trackLandingEvent('scrollDepthReached', { 1267 event_family: 'attention', 1268 scroll_depth: threshold, 1269 max_scroll_depth: depth, 1270 }, {
1271 dedupeKey: `scroll_depth_reached:${window.location.pathname}:${threshold}`, 1272 dedupeMs: 1200, 1273 keepalive: true, 1274 }); 1275 }); 1276} 1277window.addEventListener('scroll', trackScrollDepth, { passive: true }); 1278trackScrollDepth(); 1279 1280function initSectionTracking() { 1281 const sections = Array.from(document.querySelectorAll('[data-track-section]')); 1282 if (!sections.length || typeof IntersectionObserver === 'undefined') return; 1283 const sectionStates = new Map(); 1284 sections.forEach((section, index) => { 1285 const heading = section.querySelector('h1,h2,h3'); 1286 const label = readData(section, 'trackSectionLabel') 1287 || readData(section, 'trackLabel') 1288 || section.getAttribute('aria-label') 1289 || textFromElement(heading) 1290 || section.id 1291 || `Section ${index + 1}`; 1292 const type = readData(section, 'trackSectionType') 1293 || (readData(section, 'trackPricingPlan') ? 'pricing_plan' : '') 1294 || (section.id === 'pricing' || section.id?.includes('pricing') ? 'pricing' : 'section'); 1295 const id = readData(section, 'trackSectionId') 1296 || section.id 1297 || `${slugify(type, 'section')}_${slugify(label, `section_${index + 1}`)}`; 1298 sectionStates.set(section, { 1299 id, 1300 type, 1301 label, 1302 position: Number(readData(section, 'trackPosition')) || index + 1, 1303 visibleRatio: 0, 1304 visibleSince: 0, 1305 cumulativeMs: 0, 1306 fired: false, 1307 plan: readData(section, 'trackPricingPlan') || readData(section, 'trackPlan') || undefined, 1308 price: readData(section, 'trackPrice') || undefined, 1309 billingPeriod: readData(section, 'trackBillingPeriod') || undefined, 1310 }); 1311 }); 1312 const observer = new IntersectionObserver((entries) => { 1313 const now = Date.now(); 1314 entries.forEach((entry) => { 1315 const state = sectionStates.get(entry.target); 1316 if (!state || state.fired) return; 1317 state.visibleRatio = entry.intersectionRatio || 0; 1318 if (state.visibleRatio >= TRACKER_SECTION_VISIBLE_RATIO) { 1319 state.visibleSince = state.visibleSince || now; 1320 } else if (state.visibleSince) { 1321 state.cumulativeMs += now - state.visibleSince; 1322 state.visibleSince = 0; 1323 } 1324 }); 1325 }, { threshold: [0, 0.25, 0.5, 0.6, 0.75, 1] }); 1326 sections.forEach((section) => observer.observe(section)); 1327 setInterval(() => { 1328 const now = Date.now(); 1329 sectionStates.forEach((state) => { 1330 if (state.fired || state.visibleRatio < TRACKER_SECTION_VISIBLE_RATIO) return; 1331 const visibleMs = state.visibleSince ? now - state.visibleSince : 0; 1332 const cumulativeMs = state.cumulativeMs + visibleMs; 1333 if (visibleMs < TRACKER_SECTION_CONTINUOUS_MS && cumulativeMs < TRACKER_SECTION_CUMULATIVE_MS) return; 1334 state.fired = true; 1335 pageSummary.sectionsEngaged.add(state.id); 1336 if (state.type === 'pricing_plan') pageSummary.pricingCardsEngaged.add(state.id); 1337 void trackLandingEvent('sectionEngaged', { 1338 event_family: 'attention', 1339 section_id: state.id, 1340 section_type: state.type, 1341 section_label: state.label, 1342 section_position: state.position, 1343 visible_ms: Math.round(visibleMs), 1344 cumulative_visible_ms: Math.round(cumulativeMs), 1345 plan: state.plan, 1346 price: state.price, 1347 billing_period: state.billingPeriod, 1348 }, { 1349 dedupeKey: `section_engaged:${window.location.pathname}:${state.id}`, 1350 dedupeMs: 30000, 1351 keepalive: true, 1352 }); 1353 }); 1354 }, TRACKER_SECTION_TICK_MS); 1355} 1356initSectionTracking(); 1357 1358function getFormId(form) { 1359 return readData(form, 'trackForm') 1360 || form.id 1361 || form.getAttribute('name') 1362 || form.getAttribute('aria-label') 1363 || slugify(textFromElement(form.querySelector('h1,h2,h3,label'), 80), 'form'); 1364} 1365 1366function countFilledFields(form) { 1367 return Array.from(form.elements).filter((field) => { 1368 if (!(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement || field instanceof HTMLSelectElement)) return false; 1369 if (field instanceof HTMLInputElement) { 1370 const type = String(field.type || '').toLowerCase(); 1371 if (['hidden', 'button', 'submit', 'reset', 'file', 'password'].includes(type)) return false;
1372 if (['checkbox', 'radio'].includes(type)) return field.checked; 1373 } 1374 return String(field.value || '').trim().length > 0; 1375 }).length; 1376} 1377 1378document.addEventListener('focusin', (event) => { 1379 const form = event.target instanceof Element ? event.target.closest('form') : null; 1380 if (!(form instanceof HTMLFormElement)) return; 1381 const formId = getFormId(form); 1382 if (pageSummary.formsStarted.has(formId)) return; 1383 pageSummary.formsStarted.add(formId); 1384 void trackLandingEvent('formEvent', { 1385 event_family: 'form', 1386 action: 'started', 1387 form_id: formId, 1388 form_type: readData(form, 'trackFormType') || undefined, 1389 source_section: readData(form, 'trackSourceSection') || getSourceSection(form), 1390 }, { 1391 dedupeKey: `form_event:started:${window.location.pathname}:${formId}`, 1392 dedupeMs: 1200, 1393 keepalive: true, 1394 }); 1395}, true); 1396 1397document.addEventListener('submit', (event) => { 1398 const form = event.target instanceof HTMLFormElement ? event.target : null; 1399 if (!form) return; 1400 const formId = getFormId(form); 1401 pageSummary.formsSubmitted.add(formId); 1402}, true); 1403 1404function sendPageExitSummary() { 1405 if (pageSummary.sent) return; 1406 pageSummary.sent = true; 1407 const abandonedForms = Array.from(pageSummary.formsStarted).filter((formId) => !pageSummary.formsSubmitted.has(formId)); 1408 abandonedForms.forEach((formId) => { 1409 let form = document.getElementById(formId); 1410 if (!form && window.CSS && typeof window.CSS.escape === 'function') { 1411 form = document.querySelector(`[data-track-form="${window.CSS.escape(formId)}"]`); 1412 } 1413 void trackLandingEvent('formEvent', { 1414 event_family: 'form', 1415 action: 'abandoned', 1416 form_id: formId, 1417 form_type: form ? readData(form, 'trackFormType') || undefined : undefined, 1418 source_section: form ? readData(form, 'trackSourceSection') || getSourceSection(form) : undefined, 1419 filled_field_count: form instanceof HTMLFormElement ? countFilledFields(form) : undefined, 1420 }, { 1421 dedupeKey: `form_event:abandoned:${window.location.pathname}:${formId}`, 1422 dedupeMs: 1200, 1423 keepalive: true, 1424 }); 1425 }); 1426 void trackLandingEvent('pageExitSummary', { 1427 event_family: 'attention', 1428 duration_ms: Date.now() - pageSummary.startedAt, 1429 max_scroll_depth: pageSummary.maxScrollDepth, 1430 sections_engaged: Array.from(pageSummary.sectionsEngaged), 1431 pricing_cards_engaged: Array.from(pageSummary.pricingCardsEngaged), 1432 cta_click_count: pageSummary.ctaClickCount, 1433 forms_started: Array.from(pageSummary.formsStarted), 1434 forms_submitted: Array.from(pageSummary.formsSubmitted), 1435 abandoned_forms: abandonedForms, 1436 conversion_completed: pageSummary.conversionCompleted, 1437 }, { 1438 dedupeKey: `page_exit_summary:${window.location.pathname}`, 1439 dedupeMs: 5000, 1440 keepalive: true, 1441 }); 1442} 1443window.addEventListener('pagehide', sendPageExitSummary, { capture: true }); 1444 1445function trackClientError(kind, detail) { 1446 void trackLandingEvent('clientError', { 1447 event_family: 'reliability', 1448 error_kind: kind, 1449 ...compactObject(detail || {}), 1450 }, { 1451 dedupeKey: `client_error:${kind}:${detail?.message || detail?.source || ''}`, 1452 dedupeMs: 30000, 1453 keepalive: true, 1454 }); 1455} 1456 1457window.addEventListener('error', (event) => { 1458 const target = event.target; 1459 if (target && target !== window && target instanceof HTMLElement) { 1460 trackClientError('resource_error', { 1461 tag_name: target.tagName, 1462 source: target.getAttribute('src') || target.getAttribute('href') || '', 1463 }); 1464 return; 1465 } 1466 trackClientError('window_error', { 1467 message: event.message || '', 1468 source: event.filename || '', 1469 line: event.lineno || undefined, 1470 column: event.colno || undefined, 1471 }); 1472}, true); 1473 1474window.addEventListener('unhandledrejection', (event) => { 1475 trackClientError('unhandled_rejection', { 1476 message: String(event.reason?.message || event.reason || ''), 1477 }); 1478}); 1479 1480window.addEventListener('load', () => { 1481 setTimeout(() => { 1482 const navEntry = performance.getEntriesByType?.('navigation')?.[0]; 1483 const loadMs = Number(navEntry?.loadEventEnd || 0); 1484 const ttfbMs = Number(navEntry?.responseStart || 0); 1485 if (loadMs > 8000 || ttfbMs > 1800) { 1486 void trackLandingEvent('performanceIssue', { 1487 event_family: 'reliability', 1488 metric: loadMs > 8000 ? 'load_ms' : 'ttfb_ms', 1489 load_ms: Math.round(loadMs), 1490 ttfb_ms: Math.round(ttfbMs), 1491 }, { 1492 dedupeKey: `performance_issue:${window.location.pathname}:navigation`, 1493 dedupeMs: 60000, 1494 keepalive: true, 1495 }); 1496 } 1497 }, 0); 1498}); 1499 1500if (typeof PerformanceObserver !== 'undefined') { 1501 try { 1502 let longTaskReports = 0; 1503 const longTaskObserver = new PerformanceObserver((list) => {
1504 list.getEntries().forEach((entry) => { 1505 const duration = Number(entry.duration || 0); 1506 if (duration < 120 || longTaskReports >= 3) return; 1507 longTaskReports += 1; 1508 void trackLandingEvent('performanceIssue', { 1509 event_family: 'reliability', 1510 metric: 'long_task', 1511 duration_ms: Math.round(duration), 1512 start_ms: Math.round(Number(entry.startTime || 0)), 1513 mobile_viewport: mobileViewport, 1514 reduced_motion: reducedMotion, 1515 }, { 1516 dedupeKey: `performance_issue:${window.location.pathname}:long_task:${longTaskReports}`, 1517 dedupeMs: 60000, 1518 keepalive: true, 1519 }); 1520 }); 1521 }); 1522 longTaskObserver.observe({ entryTypes: ['longtask'] }); 1523 } catch {} 1524} 1525 1526function setButtonPending(button, pending, pendingLabel) { 1527 if (!button) return; 1528 button.dataset.pending = pending ? 'true' : 'false'; 1529 if (pending) { 1530 button.dataset.originalText = button.textContent || ''; 1531 button.textContent = pendingLabel; 1532 button.disabled = true; 1533 return; 1534 } 1535 button.textContent = button.dataset.originalText || button.textContent || ''; 1536 button.disabled = false; 1537} 1538 1539function setInlineError(anchor, message) { 1540 if (!anchor) return; 1541 let el = anchor.querySelector('.form-error'); 1542 if (!message) { 1543 if (el) el.remove(); 1544 return; 1545 } 1546 if (!el) { 1547 el = document.createElement('p'); 1548 el.className = 'form-error'; 1549 anchor.appendChild(el); 1550 } 1551 el.textContent = message; 1552} 1553 1554const purchaseModal = document.getElementById('purchase-modal'); 1555const purchaseForm = document.getElementById('purchase-form'); 1556const purchaseSubmit = document.getElementById('purchase-submit'); 1557const agentRoleTabs = document.getElementById('agent-role-tabs'); 1558const purchaseRoleInput = document.getElementById('purchase-role'); 1559const purchasePromotionBanner = document.getElementById('purchase-promotion-banner'); 1560const purchaseTermsCheckbox = purchaseForm?.elements?.termsAccepted || null; 1561 1562function syncPurchaseSubmitAvailability() { 1563 if (!purchaseSubmit || !purchaseTermsCheckbox) return; 1564 purchaseSubmit.disabled = purchaseSubmit.dataset.pending === 'true' || purchaseTermsCheckbox.checked !== true; 1565} 1566 1567if (purchaseTermsCheckbox) { 1568 purchaseTermsCheckbox.addEventListener('change', syncPurchaseSubmitAvailability); 1569 syncPurchaseSubmitAvailability(); 1570} 1571 1572let selectedPlan = 'one'; 1573let selectedAgentRoleIndex = 0; 1574let singleAgentRole = ''; 1575let purchaseTurnstileToken = ''; 1576let purchaseTurnstileWidgetId = null; 1577let purchaseTurnstilePromise = null; 1578let purchaseCheckoutRequestId = ''; 1579const promotionUrl = new URL(window.location.href); 1580const requestedPromotionCode = promotionUrl.searchParams.get('promotionCode')?.trim().toUpperCase() || ''; 1581const promotionPlans = Object.freeze({ LABORDAY15: 'one', SWARM30: 'team' }); 1582let selectedPromotionPlan = promotionPlans[requestedPromotionCode] || ''; 1583let selectedPromotionCode = selectedPromotionPlan ? requestedPromotionCode : ''; 1584const teamAgentRoles = ['', '', '', '', '']; 1585const planDealPopovers = new Map( 1586 Array.from(document.querySelectorAll('[data-plan-deal]')) 1587 .map((deal) => [deal, deal.querySelector('[data-plan-deal-popover]')]) 1588 .filter((entry) => entry[1]), 1589); 1590 1591function positionPlanDealPopover(deal) { 1592 const popover = planDealPopovers.get(deal); 1593 const summary = deal.querySelector('summary'); 1594 const planOption = deal.closest('.plan-option-wrap'); 1595 if (!deal.open || !popover || !summary || !planOption || !popover.classList.contains('plan-deal-popover-floating')) return; 1596 1597 const viewportMargin = 12; 1598 const gap = 12; 1599 const width = Math.min(250, Math.max(0, window.innerWidth - viewportMargin * 2)); 1600 popover.style.width = `${width}px`; 1601 popover.style.visibility = 'hidden'; 1602 1603 const summaryRect = summary.getBoundingClientRect(); 1604 const optionRect = planOption.getBoundingClientRect(); 1605 const popoverHeight = popover.offsetHeight; 1606 const maxLeft = Math.max(viewportMargin, window.innerWidth - width - viewportMargin); 1607 const preferredLeft = deal.dataset.planDeal === 'team' ? optionRect.right - width : optionRect.left; 1608 const left = Math.min(Math.max(viewportMargin, preferredLeft), maxLeft); 1609 const availableAbove = summaryRect.top - viewportMargin - gap; 1610 const availableBelow = window.innerHeight - summaryRect.bottom - viewportMargin - gap; 1611 const placement = availableAbove >= popoverHeight || availableAbove >= availableBelow ? 'above' : 'below'; 1612 const preferredTop = placement === 'above' 1613 ? summaryRect.top - popoverHeight - gap 1614 : summaryRect.bottom + gap; 1615 const maxTop = Math.max(viewportMargin, window.innerHeight - popoverHeight - viewportMargin); 1616 const top = Math.min(Math.max(viewportMargin, preferredTop), maxTop); 1617 const arrowLeft = Math.min(Math.max(12, summaryRect.left + summaryRect.width / 2 - left - 4), width - 20); 1618 1619 popover.dataset.placement = placement; 1620 popover.style.left = `${left}px`; 1621 popover.style.top = `${top}px`; 1622 popover.style.setProperty('--plan-deal-arrow-left', `${arrowLeft}px`); 1623 popover.style.visibility = ''; 1624} 1625 1626function detachPlanDealPopover(deal) { 1627 const popover = planDealPopovers.get(deal); 1628 if (!popover) return; 1629 popover.classList.add('plan-deal-popover-floating');
1630 document.body.appendChild(popover); 1631 positionPlanDealPopover(deal); 1632} 1633 1634function restorePlanDealPopover(deal) { 1635 const popover = planDealPopovers.get(deal); 1636 if (!popover || popover.parentElement === deal) return; 1637 popover.classList.remove('plan-deal-popover-floating'); 1638 popover.removeAttribute('data-placement'); 1639 popover.removeAttribute('style'); 1640 deal.appendChild(popover); 1641} 1642 1643function closePlanDealPopovers(except = null) { 1644 document.querySelectorAll('[data-plan-deal][open]').forEach((deal) => { 1645 if (deal === except) return; 1646 deal.open = false; 1647 restorePlanDealPopover(deal); 1648 }); 1649} 1650 1651function syncPurchasePromotionState() { 1652 const promotionApplies = Boolean(selectedPromotionCode && selectedPlan === selectedPromotionPlan); 1653 if (purchasePromotionBanner) { 1654 purchasePromotionBanner.hidden = !promotionApplies; 1655 purchasePromotionBanner.textContent = promotionApplies 1656 ? `${selectedPromotionCode} is applied for ${selectedPlan === 'team' ? 'Agent Team' : 'Agent'}` 1657 : ''; 1658 } 1659 document.querySelectorAll('[data-plan-deal]').forEach((deal) => { 1660 deal.hidden = Boolean(selectedPromotionCode && deal.dataset.planDeal === selectedPromotionPlan); 1661 if (deal.hidden) { 1662 deal.open = false; 1663 restorePlanDealPopover(deal); 1664 } 1665 }); 1666 document.querySelectorAll('[data-plan-price]').forEach((price) => { 1667 const promotionIsApplied = Boolean(selectedPromotionCode && price.dataset.planPrice === selectedPromotionPlan); 1668 price.classList.toggle('promotion-applied', promotionIsApplied); 1669 price.closest('.plan-option')?.classList.toggle('promotion-applied', promotionIsApplied); 1670 const discountedPrice = price.querySelector('[data-plan-price-discount]'); 1671 if (discountedPrice) discountedPrice.hidden = !promotionIsApplied; 1672 }); 1673} 1674 1675async function ensurePurchaseTurnstile() { 1676 if (!purchaseForm || purchaseTurnstileWidgetId != null) return; 1677 if (purchaseTurnstilePromise) return purchaseTurnstilePromise; 1678 purchaseTurnstilePromise = (async () => {
1679 let container = document.getElementById('purchase-turnstile'); 1680 if (!container) { 1681 container = document.createElement('div'); 1682 container.id = 'purchase-turnstile'; 1683 container.style.minHeight = '65px'; 1684 container.style.display = 'flex'; 1685 container.style.justifyContent = 'center'; 1686 purchaseForm.insertBefore(container, purchaseSubmit); 1687 } 1688 const configResponse = await fetch(getPublicApiUrl('/api/public-config'), { cache: 'no-store' }); 1689 const config = await configResponse.json(); 1690 const sitekey = String(config?.turnstileSiteKey || '').trim(); 1691 if (!configResponse.ok || !sitekey) throw new Error('Secure checkout verification is unavailable.'); 1692 if (!window.turnstile) { 1693 await new Promise((resolve, reject) => { 1694 const existing = document.getElementById('cf-turnstile-api'); 1695 if (existing) { 1696 existing.addEventListener('load', resolve, { once: true }); 1697 existing.addEventListener('error', reject, { once: true }); 1698 return; 1699 } 1700 const script = document.createElement('script'); 1701 script.id = 'cf-turnstile-api'; 1702 script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; 1703 script.async = true; 1704 script.defer = true; 1705 script.onload = resolve; 1706 script.onerror = reject; 1707 document.head.appendChild(script); 1708 }); 1709 } 1710 if (!window.turnstile) throw new Error('Secure checkout verification is unavailable.'); 1711 purchaseTurnstileWidgetId = window.turnstile.render(container, { 1712 sitekey, 1713 theme: 'dark', 1714 callback: (token) => { purchaseTurnstileToken = token || ''; }, 1715 'expired-callback': () => { purchaseTurnstileToken = ''; }, 1716 'error-callback': () => { purchaseTurnstileToken = ''; }, 1717 }); 1718 })().catch((error) => { 1719 purchaseTurnstilePromise = null; 1720 throw error; 1721 }); 1722 return purchaseTurnstilePromise; 1723} 1724 1725function saveCurrentPurchaseRole() { 1726 if (!purchaseRoleInput) return; 1727 if (selectedPlan === 'team') { 1728 teamAgentRoles[selectedAgentRoleIndex] = purchaseRoleInput.value.trim(); 1729 } else { 1730 singleAgentRole = purchaseRoleInput.value.trim(); 1731 } 1732} 1733 1734function setActiveAgentRoleTab(index) { 1735 if (!purchaseRoleInput) return; 1736 saveCurrentPurchaseRole(); 1737 selectedAgentRoleIndex = Math.max(0, Math.min(4, Number(index) || 0)); 1738 document.querySelectorAll('.agent-role-tab').forEach((btn) => { 1739 const on = Number(btn.dataset.agentRoleTab || 0) === selectedAgentRoleIndex; 1740 btn.classList.toggle('selected', on); 1741 btn.setAttribute('aria-selected', on ? 'true' : 'false'); 1742 }); 1743 purchaseRoleInput.value = teamAgentRoles[selectedAgentRoleIndex] || ''; 1744 purchaseRoleInput.placeholder = `Describe agent ${selectedAgentRoleIndex + 1}'s role`; 1745} 1746 1747function updatePurchaseRoleMode(previousPlan) { 1748 if (!purchaseRoleInput) return; 1749 if (agentRoleTabs) agentRoleTabs.hidden = selectedPlan !== 'team'; 1750 if (selectedPlan === 'team') { 1751 if (previousPlan !== 'team' && singleAgentRole && !teamAgentRoles.some(Boolean)) { 1752 teamAgentRoles[0] = singleAgentRole; 1753 } 1754 setActiveAgentRoleTab(selectedAgentRoleIndex); 1755 return; 1756 } 1757 purchaseRoleInput.value = singleAgentRole || teamAgentRoles.find(Boolean) || ''; 1758 purchaseRoleInput.placeholder = "Describe the role you're hiring for, like you would to a new hire"; 1759} 1760 1761function getPurchaseRoles() { 1762 saveCurrentPurchaseRole(); 1763 const roles = selectedPlan === 'team' 1764 ? teamAgentRoles.map((role, index) => ({ index: index + 1, role: role.trim() })).filter((item) => item.role) 1765 : [{ index: 1, role: singleAgentRole.trim() }].filter((item) => item.role); 1766 return roles; 1767} 1768 1769function setPlan(plan) { 1770 const previousPlan = selectedPlan; 1771 saveCurrentPurchaseRole(); 1772 selectedPlan = plan === 'team' ? 'team' : 'one'; 1773 document.querySelectorAll('.plan-option').forEach((btn) => { 1774 const on = btn.dataset.planOption === selectedPlan; 1775 btn.classList.toggle('selected', on); 1776 btn.setAttribute('aria-pressed', on ? 'true' : 'false'); 1777 }); 1778 updatePurchaseRoleMode(previousPlan); 1779 syncPurchasePromotionState(); 1780} 1781document.querySelectorAll('.plan-option').forEach((btn) => { 1782 btn.addEventListener('click', () => { 1783 closePlanDealPopovers(); 1784 setPlan(btn.dataset.planOption); 1785 }); 1786}); 1787document.querySelectorAll('[data-plan-deal]').forEach((deal) => { 1788 deal.addEventListener('toggle', () => { 1789 if (deal.open) { 1790 closePlanDealPopovers(deal); 1791 detachPlanDealPopover(deal); 1792 return; 1793 } 1794 restorePlanDealPopover(deal); 1795 }); 1796}); 1797document.querySelectorAll('[data-apply-plan-deal]').forEach((button) => { 1798 button.addEventListener('click', () => { 1799 const promotionCode = button.dataset.promotionCode?.trim().toUpperCase() || ''; 1800 const promotionPlan = button.dataset.applyPlanDeal === 'team' ? 'team' : 'one'; 1801 if (promotionPlans[promotionCode] !== promotionPlan) return; 1802 selectedPromotionCode = promotionCode; 1803 selectedPromotionPlan = promotionPlan; 1804 closePlanDealPopovers(); 1805 setPlan(promotionPlan); 1806 }); 1807}); 1808document.querySelectorAll('.agent-role-tab').forEach((btn) => { 1809 btn.addEventListener('click', () => setActiveAgentRoleTab(btn.dataset.agentRoleTab)); 1810}); 1811 1812window.openPurchase = function (plan) { 1813 if (!purchaseModal) return; 1814 setPlan(plan || 'one'); 1815 syncPurchaseSubmitAvailability(); 1816 purchaseCheckoutRequestId = window.crypto?.randomUUID?.() || `${Date.now()}_${Math.random().toString(36).slice(2)}_${Math.random().toString(36).slice(2)}`; 1817 trackLandingEvent('modalOpened', { modal_id: 'purchase_agent', modal_type: 'purchase', plan: selectedPlan }, { dedupeKey: `modal_opened:purchase_agent:${Date.now()}` }); 1818 purchaseModal.hidden = false;
1819 void ensurePurchaseTurnstile().catch((error) => { 1820 setInlineError(purchaseForm, error instanceof Error ? error.message : 'Secure checkout verification is unavailable.'); 1821 }); 1822 requestAnimationFrame(() => purchaseModal.classList.add('open')); 1823 document.body.style.overflow = 'hidden'; 1824}; 1825 1826window.closePurchase = function () { 1827 if (!purchaseModal) return; 1828 trackLandingEvent('modalClosed', { modal_id: 'purchase_agent', modal_type: 'purchase', plan: selectedPlan }); 1829 closePlanDealPopovers(); 1830 purchaseModal.classList.remove('open'); 1831 document.body.style.overflow = ''; 1832 setTimeout(() => { purchaseModal.hidden = true; }, 250); 1833}; 1834 1835if (purchaseModal) purchaseModal.addEventListener('click', (e) => { if (e.target === purchaseModal) window.closePurchase(); }); 1836document.querySelectorAll('[data-close-modal="purchase"]').forEach((el) => { 1837 el.addEventListener('click', () => window.closePurchase()); 1838}); 1839document.addEventListener('keydown', (event) => { 1840 if (event.key !== 'Escape' || !purchaseModal || purchaseModal.hidden) return; 1841 const openDeal = document.querySelector('[data-plan-deal][open]'); 1842 if (openDeal) { 1843 openDeal.open = false; 1844 restorePlanDealPopover(openDeal); 1845 return; 1846 } 1847 window.closePurchase(); 1848}); 1849document.addEventListener('click', (event) => { 1850 document.querySelectorAll('[data-plan-deal][open]').forEach((deal) => { 1851 const popover = planDealPopovers.get(deal); 1852 if (!deal.contains(event.target) && !popover?.contains(event.target)) { 1853 deal.open = false; 1854 restorePlanDealPopover(deal); 1855 } 1856 }); 1857}); 1858window.addEventListener('resize', () => { 1859 document.querySelectorAll('[data-plan-deal][open]').forEach(positionPlanDealPopover); 1860}); 1861window.addEventListener('scroll', () => { 1862 document.querySelectorAll('[data-plan-deal][open]').forEach(positionPlanDealPopover); 1863}, true); 1864document.querySelectorAll('[data-purchase]').forEach((el) => { 1865 el.addEventListener('click', (e) => { 1866 e.preventDefault(); 1867 const plan = el.dataset.plan || 'one'; 1868 window.openPurchase(plan); 1869 }); 1870}); 1871 1872if (selectedPromotionCode) { 1873 const cleanUrl = new URL(window.location.href); 1874 cleanUrl.searchParams.delete('promotionCode'); 1875 cleanUrl.searchParams.delete('plan'); 1876 window.history.replaceState(null, '', `${cleanUrl.pathname}${cleanUrl.search}${cleanUrl.hash}`); 1877 window.openPurchase(selectedPromotionPlan); 1878} 1879 1880if (purchaseForm) purchaseForm.addEventListener('submit', async (e) => { 1881 e.preventDefault(); 1882 const form = e.target; 1883 if (!form.reportValidity()) return; 1884 if (form.elements.termsAccepted?.checked !== true) { 1885 setInlineError(form, 'Accept the Terms and Privacy Policy to continue.'); 1886 form.elements.termsAccepted?.focus(); 1887 syncPurchaseSubmitAvailability(); 1888 return; 1889 } 1890 setInlineError(form, ''); 1891 const identity = readStoredIdentity(); 1892 const name = form.elements.name.value.trim(); 1893 const email = form.elements.email.value.trim(); 1894 const website = form.elements.website.value.trim(); 1895 const roles = getPurchaseRoles(); 1896 if (!roles.length) { 1897 setInlineError(form, selectedPlan === 'team' ? 'Describe at least one agent role.' : 'Describe the role you want built.'); 1898 purchaseRoleInput?.focus(); 1899 return; 1900 } 1901 if (!purchaseTurnstileToken) { 1902 setInlineError(form, 'Complete the security check to continue.'); 1903 return; 1904 } 1905 const role = selectedPlan === 'team' 1906 ? roles.map((item) => `Agent ${item.index}: ${item.role}`).join('\n\n') 1907 : roles[0].role; 1908 const offerLabel = selectedPlan === 'team' ? 'Agent Team' : 'Agent'; 1909 setButtonPending(purchaseSubmit, true, 'Opening checkout...'); 1910 try { 1911 const res = await fetch(getPublicApiUrl('/api/checkout'), { 1912 method: 'POST', 1913 credentials: 'include', 1914 headers: { 'Content-Type': 'application/json' }, 1915 body: JSON.stringify({ 1916 source: 'new_landing_purchase', 1917 offer: selectedPlan, 1918 plan: selectedPlan, 1919 promotionCode: selectedPlan === selectedPromotionPlan ? selectedPromotionCode || undefined : undefined, 1920 email, 1921 termsAccepted: form.elements.termsAccepted.checked === true, 1922 termsVersion: form.elements.termsVersion.value, 1923 privacyVersion: form.elements.privacyVersion.value, 1924 turnstileToken: purchaseTurnstileToken, 1925 checkoutRequestId: purchaseCheckoutRequestId, 1926 leadId: identity.leadId || undefined, 1927 externalId: identity.visitorId || undefined, 1928 eventSourceUrl: window.location.href, 1929 path: window.location.pathname || '/', 1930 agentConfig: { 1931 name: offerLabel, 1932 role, 1933 agentRoles: selectedPlan === 'team' ? roles : undefined, 1934 plan: 'Agent', 1935 agentType: 'agent', 1936 offerKey: selectedPlan, 1937 companyName: name, 1938 website, 1939 source: 'Nerova landing page', 1940 sourceIntent: 'direct_purchase', 1941 visitor_id: identity.visitorId || undefined, 1942 tracking_session_id: identity.sessionId || undefined, 1943 }, 1944 }), 1945 }); 1946 const data = await res.json().catch(() => null); 1947 const checkoutUrl = String(data?.url || '').trim();
1948 if (!res.ok || !checkoutUrl) throw new Error(String(data?.error || 'Failed to create checkout.')); 1949 trackLandingEvent('formEvent', { 1950 action: 'submitted', 1951 form_id: 'purchase-form', 1952 form_type: 'purchase_agent', 1953 source_section: 'pricing', 1954 plan: selectedPlan, 1955 }); 1956 pageSummary.conversionCompleted = true; 1957 window.location.href = checkoutUrl; 1958 } catch (error) { 1959 setInlineError(form, error instanceof Error ? error.message : 'Failed to create checkout.'); 1960 if (window.turnstile && purchaseTurnstileWidgetId != null) window.turnstile.reset(purchaseTurnstileWidgetId); 1961 purchaseTurnstileToken = ''; 1962 setButtonPending(purchaseSubmit, false); 1963 syncPurchaseSubmitAvailability(); 1964 } 1965});
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.