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.