PageSourceSearch

https://nerova.ai/newlandingpage-assets/main.js?v=20260821-ai-team-pricing

js nerova.ai collected 2026-10-05 12:17:09 UTC 77,905 bytes, 1,965 lines download raw bytes

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.