PageSourceSearch

https://cprtoday.com/public/js/corporate-hub-page.1777097327.js

js cprtoday.com collected 2026-10-05 11:06:30 UTC 16,008 bytes, 441 lines download raw bytes

1
2/* fade-in of stat band */
3(function(){
4  var el = document.getElementById('heroStatbar');
5  if(!el) return;
6  var activated = false;
7  function inView(n){ var r = n.getBoundingClientRect(); return r.top < (window.innerHeight*0.9) && r.bottom > 0; }
8  function onScroll(){ if(activated) return; if(inView(el)){ el.classList.add('is-in'); activated = true; window.removeEventListener('scroll', onScroll); } }
9  document.addEventListener('DOMContentLoaded', onScroll, {passive:true});
10  window.addEventListener('scroll', onScroll, {passive:true});
11})();
12
13
14
15(function () {
16  const steps = document.querySelectorAll('.how-step');
17
18  const observer = new IntersectionObserver(entries => {
19    entries.forEach(entry => {
20      if (entry.isIntersecting) {
21        steps.forEach(s => s.classList.add('is-dimmed'));
22        entry.target.classList.remove('is-dimmed');
23        entry.target.classList.add('is-active');
24      }
25    });
26  }, { threshold: 0.6 });
27
28  steps.forEach(step => observer.observe(step));
29})();
30
31
32
33document.addEventListener("DOMContentLoaded", () => {
34  const rows = document.querySelectorAll(".full-table-container .price-table tr");
35  if (!rows.length) return;
36
37  const io = new IntersectionObserver(entries => {
38    entries.forEach(entry => {
39      if (entry.isIntersecting) {
40        entry.target.classList.add("is-visible");
41        io.unobserve(entry.target);
42      }
43    });
44  }, { threshold: 0.15 });
45
46  rows.forEach((r, i) => {
47    r.style.transitionDelay = `${i * 0.08}s`;
48    io.observe(r);
49  });
50});
51
52
53
54(function () {
55  const courseInfo = {
56    'Adult CPR + AED': 'One-rescuer CPR and AED use for adults. Ideal for workplace and community training.',
57    'Adult & Pediatric CPR + AED': 'Comprehensive CPR and AED training for adults, children, and infants.',
58    'Healthcare Provider BLS': 'Professional-level CPR for healthcare providers, includes 2-rescuer skills and bag-mask use.',
59    'Universal First Aid': 'Covers wound care, bleeding control, burns, shock, and common first aid emergencies.',
60    'Bloodborne Pathogens': 'OSHA-compliant infection control and exposure prevention for workplace safety.',
61    'Comprehensive BLS Bundle': 'Combined certification covering CPR, AED, First Aid, and Bloodborne Pathogens.',
62    'ACLS': 'Advanced Cardiovascular Life Support — rhythm recognition, airway, and emergency pharmacology.',
63    'PALS': 'Pediatric Advanced Life Support — emergency care for infants and children in critical conditions.'
64  };
65
66  const tooltip = document.createElement('div');
67  tooltip.className = 'tooltip';
68  document.body.appendChild(tooltip);
69
70  const table = document.querySelector('.price-table');
71  if (!table) return;
72
73  let activeCell = null;
74
75table.addEventListener('mouseover', e => {
76  const cell = e.target.closest('td, th');
77  if (!cell) return;
78  activeCell = cell;
79
80  // highlight col + row
81  const colIndex = [...cell.parentNode.children].indexOf(cell);
82  table.querySelectorAll('tr').forEach(tr => tr.classList.remove('hover-row'));
83  table.querySelectorAll('td, th').forEach(c => c.classList.remove('hover-col'));
84  cell.parentNode.classList.add('hover-row');
85  table.querySelectorAll(`tr > *:nth-child(${colIndex + 1})`).forEach(c => c.classList.add('hover-col'));
86
87  let text = '';
88
89  /* ——— 1️⃣ COURSE HEADER (th) ——— */
90  if (cell.tagName === 'TH' && !cell.classList.contains('tier-cell')) {
91    const span = cell.querySelector('span');
92    const title = span ? span.textContent.trim() : cell.textContent.trim();
93    const desc = courseInfo[title] || 'This course includes full online certification with instant results and a printable card.';
94
95    text = `
96      <div style="font-size:16px;font-weight:700;color:#243282;margin-bottom:8px;border-bottom:1px solid #eef2ff;padding-bottom:4px;">
97        ${title}
98      </div>
99      <div style="font-size:14px;color:#4b5272;line-height:1.55;">
100        ${desc}
101      </div>
102      <div style="margin-top:10px;font-size:13.5px;color:#6b7393;">
103        Hover over group rates below to view per-seat and total savings.
104      </div>
105    `;
106  }
107
108  /* ——— 2️⃣ GROUP SIZE COLUMN (tier-cell) ——— */
109  else if (cell.classList.contains('tier-cell')) {
110    const tier = cell.querySelector('.tier-badge')?.textContent.trim() || '';
111    const discount = cell.querySelector('.discount-pill')?.textContent.trim() || '';
112
113    text = `
114      <div style="font-size:16px;font-weight:700;color:#243282;margin-bottom:8px;border-bottom:1px solid #eef2ff;padding-bottom:4px;">
115        ${tier} Members
116      </div>
117      <div style="font-size:14px;color:#4b5272;line-height:1.55;margin-bottom:8px;">
118        Perfect for teams and departments of ${tier} participants. 
119        Larger enrollments automatically qualify for greater savings.
120      </div>
121      <div style="font-size:15px;font-weight:600;color:#ff5800;text-align:right;">
122        ${discount}
123      </div>
124    `;
125  }
126
127  /* ——— 3️⃣ PRICE CELL (update-form-config) ——— */
128  else if (cell.classList.contains('update-form-config')) {
129    const base = parseFloat(cell.dataset.base || 0);
130    const discount = parseFloat(cell.dataset.discount || 0);
131    const final = parseFloat(cell.textContent.replace('$', '').trim());
132    const qty = parseInt(cell.dataset.qty || 0, 10);
133    const tier = cell.parentElement.querySelector('.tier-badge')?.textContent.trim() || '';
134    const minSeats = parseInt(tier.split('–')[0] || qty);
135    const maxSeats = parseInt(tier.split('–')[1] || qty);
136
137    const fmt = n => n ? `$${n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '';
138    const totalMin = final * minSeats;
139    const totalMax = final * maxSeats;
140    const savingsPerSeat = base - final;
141
142    // get course name
143    const headerRow = table.querySelector('tr:first-child');
144    const headerCell = headerRow ? headerRow.querySelectorAll('th')[cell.cellIndex] : null;
145    const courseName = headerCell ? headerCell.textContent.trim() : 'Course';
146
147    text = `
148      <div style="font-size:16px;font-weight:700;color:#243282;margin-bottom:10px;border-bottom:1px solid #eef2ff;padding-bottom:4px;line-height:1.3;">
149        ${courseName}
150      </div>
151
152      <div style="display:flex;align-items:center;justify-content:space-between;font-size:15px;font-weight:600;color:#243282;margin-bottom:10px;">
153        <span>Group Size: ${tier}</span>
154        <span style="background:#fff3eb;color:#ff5800;font-size:15px;font-weight:700;padding:4px 9px;border-radius:8px;margin-left:12px;">
155          ${discount}% OFF
156        </span>
157      </div>
158
159      <div style="display:flex;justify-content:space-between;font-size:14px;margin:6px 0;">
160        <span style="color:#6b7393;">Standard Rate:</span>
161        <span style="text-decoration:line-through;color:#9ba2c1;">${fmt(base)}/seat</span>
162      </div>
163
164      <div style="display:flex;justify-content:space-between;font-size:15px;font-weight:600;margin:4px 0;">
165        <span style="color:#4b5272;">Your Group Rate:</span>
166        <span style="color:#243282;">${fmt(final)}/seat</span>
167      </div>
168
169      <div style="display:flex;justify-content:space-between;font-size:13.5px;margin:4px 0 10px;">
170        <span style="color:#6b7393;">You Save:</span>
171        <span style="color:#2e7d32;font-weight:600;">${fmt(savingsPerSeat)}/seat</span>
172      </div>
173
174      <div style="font-size:13.5px;color:#4b5272;font-weight:500;margin-bottom:8px;">
175        <div style="color:#6b7393;">Total For This Group Size:</div>
176        <div style="text-align:right;color:#243282;font-weight:700;">
177          ${fmt(totalMin)} – ${fmt(totalMax)}
178        </div>
179      </div>
180
181      <div style="font-size:13.5px;color:#4b5272;margin-bottom:8px;">
182        <div style="color:#6b7393;">Total Group Savings:</div>
183        <div style="text-align:right;color:#2e7d32;font-weight:700;">
184          ${fmt(base * minSeats - totalMin)} – ${fmt(base * maxSeats - totalMax)}
185        </div>
186      </div>
187
188      <div style="font-size:13.5px;color:#4b5272;line-height:1.55;">
189        Registering together unlocks this group rate — larger teams save even more.
190      </div>
191    `;
192  }
193
194  if (text) {
195    tooltip.innerHTML = text;
196    positionTooltip(cell);
197
198    // Always restart fade transition cleanly.
199    tooltip.style.transition = 'none';
200    tooltip.classList.remove('show');
201
202    // Force reflow, then restore transition.
203    void tooltip.offsetWidth;
204    tooltip.style.transition = 'opacity 0.5s ease-in-out';
205    tooltip.classList.add('show');
206  }
207  });
208
209  table.addEventListener('mousemove', e => {
210    if (tooltip.classList.contains('show') && activeCell) {
211      positionTooltip(activeCell);
212    }
213  });
214
215  table.addEventListener('mouseout', () => {
216    tooltip.classList.remove('show');
217    table.querySelectorAll('tr').forEach(tr => tr.classList.remove('hover-row'));
218    table.querySelectorAll('td, th').forEach(c => c.classList.remove('hover-col'));
219    activeCell = null;
220  });
221
222  function positionTooltip(cell) {
223    const cellRect = cell.getBoundingClientRect();
224    const tooltipRect = tooltip.getBoundingClientRect();
225    const offset = 14;
226
227    tooltip.classList.remove("flip-left", "flip-right");
228
229    // default: right side of cell
230    let left = cellRect.right + window.scrollX + offset;
231    let top = cellRect.top + window.scrollY + (cellRect.height / 2 - tooltipRect.height / 2);
232
233    // flip left if overflow
234    if (left + tooltipRect.width > window.scrollX + window.innerWidth - 20) {
235      left = cellRect.left + window.scrollX - tooltipRect.width - offset;
236      tooltip.classList.add("flip-left");
237    } else {
238      tooltip.classList.add("flip-right");
239    }
240
241    // keep vertically within viewport
242    if (top < window.scrollY + 10) top = window.scrollY + 10;
243    if (top + tooltipRect.height > window.scrollY + window.innerHeight - 10) {
244      top = window.scrollY + window.innerHeight - tooltipRect.height - 10;
245    }
246
247    tooltip.style.left = left + "px";
248    tooltip.style.top = top + "px";
249  }
250})();
251
252
253
254/* reveal-on-scroll */
255(function(){
256  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
257  const els = document.querySelectorAll('.reveal');
258  if (!els.length) return;
259  const io = new IntersectionObserver((entries)=>{
260    entries.forEach(e=>{
261      if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); }
262    });
263  }, { rootMargin: '0px 0px -10% 0px', threshold: 0.15 });
264  els.forEach(el=>io.observe(el));
265})();
266
267/* connector (anchors OUTSIDE but clamped inside the wrap) */
268(function(){
269  const wrap = document.querySelector('.hiw-stagger .hs-wrap');
270  const svg  = wrap?.querySelector('.hs-connector');
271  const path = svg?.querySelector('.hs-connector__path');
272  if (!wrap || !svg || !path) return;
273
274  function sideAnchor(step, offset = 24){
275    const content = step.querySelector('.hs-content');
276    const visual  = step.querySelector('.hs-visual');
277    if(!content || !visual) return null;
278
279    const wr = wrap.getBoundingClientRect();
280    const cr = content.getBoundingClientRect();
281    const vr = visual.getBoundingClientRect();
282    const sr = step.getBoundingClientRect();
283
284    const visualOnRight = vr.left > cr.left;
285
286    // propose an outside gutter x, but CLAMP it into the wrap so it won't clip
287    let x = visualOnRight ? (vr.right + offset) : (vr.left - offset);
288    const gutterPadding = 8; // keep a little breathing room from the edge
289    x = Math.max(wr.left + gutterPadding, Math.min(x, wr.right - gutterPadding));
290
291    const y = sr.top + sr.height/2;
292    return { x, y };
293  }
294
295  function draw(){
296    const stacked = window.matchMedia('(max-width: 900px)').matches;
297    svg.style.display = stacked ? 'none' : 'block';
298    if (stacked) return;
299
300    const wr = wrap.getBoundingClientRect();
301    svg.setAttribute('viewBox', `0 0 ${wr.width} ${wr.height}`);
302    svg.setAttribute('preserveAspectRatio','none');
303
304    const steps = wrap.querySelectorAll('.hs-step');
305    if (steps.length < 3) return;
306
307    const pts = Array.from(steps).map(s => sideAnchor(s, 24)).filter(Boolean);
308    if (pts.length < 3) return;
309
310    // translate into the wrap's local coords
311    pts.forEach(p => { p.x -= wr.left; p.y -= wr.top; });
312
313    const [p1, p2, p3] = pts;
314    const k = 0.35;
315    const c1 = { x: p1.x + (p2.x - p1.x)*k, y: p1.y };
316    const c2 = { x: p2.x - (p2.x - p1.x)*k, y: p2.y };
317    const c3 = { x: p2.x + (p3.x - p2.x)*k, y: p2.y };
318    const c4 = { x: p3.x - (p3.x - p2.x)*k, y: p3.y };
319
320    const d = `M ${p1.x},${p1.y}
321               C ${c1.x},${c1.y} ${c2.x},${c2.y} ${p2.x},${p2.y}
322               C ${c3.x},${c3.y} ${c4.x},${c4.y} ${p3.x},${p3.y}`;
323    path.setAttribute('d', d);
324  }
325
326  const ro = new ResizeObserver(draw);
327  ro.observe(wrap);
328  window.addEventListener('load', draw, {once:true});
329  window.addEventListener('resize', draw, {passive:true});
330  setTimeout(draw, 300);
331
332  window.addEventListener('load', () => {
333    document.querySelector('.hiw-stagger')?.classList.add('hiws-ready');
334  }, { once:true });
335})();
336
337
338
339function animateNumber(el, newValue) {
340  if (!el) return;
341  el.textContent = newValue;
342  el.classList.add("updated");
343  setTimeout(()=> el.classList.remove("updated"), 600);
344}
345
346
347
348const summary = {
349  courses: [],
350  group: 0,
351  discount: 0,
352  total: 0
353};
354
355function updateSummaryDisplay() {
356  const el = (id) => document.querySelector(id);
357  const sumCourses  = el('#sumCourses');
358  const sumGroup    = el('#sumGroup');
359  const sumDiscount = el('#sumDiscount');
360  const sumTotal    = el('#sumTotal');
361  if (!sumCourses || !sumGroup || !sumDiscount || !sumTotal) return;
362
363  sumCourses.textContent  = summary.courses.join(', ') || '–';
364  sumGroup.textContent    = summary.group || '–';
365  sumDiscount.textContent = summary.discount ? `${summary.discount}%` : '–';
366  sumTotal.textContent    = summary.total ? `$${summary.total.toFixed(2)}` : '–';
367}
368
369document.addEventListener('change', e => {
370  // Course checkboxes
371  if (e.target.matches('.custom-checkbox-input')) {
372    const label = e.target.closest('.custom-checkbox').querySelector('.custom-checkbox-label').textContent.trim();
373    if (e.target.checked) {
374      if (!summary.courses.includes(label)) summary.courses.push(label);
375    } else {
376      summary.courses = summary.courses.filter(c => c !== label);
377    }
378    updateSummaryDisplay();
379  }
380
381  // Group size number input
382  if (e.target.matches('.base-NumberInput-input')) {
383    summary.group = parseInt(e.target.value, 10) || 0;
384    summary.total = summary.group * 19.99;
385    updateSummaryDisplay();
386  }
387});
388
389// Initial render
390updateSummaryDisplay();
391
392// pulse total when updated
393const totalDisplay = document.querySelector('#sumTotal');
394if (totalDisplay) {
395  const observer = new MutationObserver(() => {
396    totalDisplay.animate(
397      [{ transform: 'scale(1)', backgroundColor: 'transparent' },
398       { transform: 'scale(1.1)', backgroundColor: 'rgba(95,122,214,0.08)' },
399       { transform: 'scale(1)', backgroundColor: 'transparent' }],
400      { duration: 500, easing: 'ease-out' }
401    );
402  });
403  observer.observe(totalDisplay, { childList: true });
404}
405
406
407
408
409(function () {
410  const pills = document.querySelectorAll('.hero-lens__pill');
411  if (!pills.length) return;
412
413  const params = new URLSearchParams(window.location.search);
414  const activeSector = params.get('sector');
415
416  // Apply initial state from URL
417  pills.forEach(pill => {
418    const sector = pill.dataset.sector;
419    const isActive = sector === activeSector;
420    pill.setAttribute('aria-selected', isActive ? 'true' : 'false');
421  });
422
423  // Click behavior
424  pills.forEach(pill => {
425    pill.addEventListener('click', () => {
426      const sector = pill.dataset.sector;
427      const isActive = pill.getAttribute('aria-selected') === 'true';
428      const url = new URL(window.location.href);
429
430      if (isActive) {
431        // Clear lens → return to hub
432        url.searchParams.delete('sector');
433      } else {
434        // Activate lens
435        url.searchParams.set('sector', sector);
436      }
437
438      window.location.href = url.toString();
439    });
440  });
441})();

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.