1'use strict'; 2 3document.addEventListener('DOMContentLoaded', function () { 4 5 // --------------------------------------------------------------------------- 6 // Header scroll handler 7 // Adds 'is-scrolled' class to .site-header after 50px of vertical scroll. 8 // --------------------------------------------------------------------------- 9 var siteHeader = document.querySelector('.site-header'); 10 11 if (siteHeader) { 12 var scrollThreshold = 50; 13 14 function handleHeaderScroll() { 15 if (window.scrollY > scrollThreshold) { 16 siteHeader.classList.add('is-scrolled'); 17 } else { 18 siteHeader.classList.remove('is-scrolled'); 19 } 20 } 21 22 window.addEventListener('scroll', handleHeaderScroll, { passive: true }); 23 handleHeaderScroll(); 24 } 25 26 // --------------------------------------------------------------------------- 27 // Smooth scroll for anchor links 28 // --------------------------------------------------------------------------- 29 var anchorLinks = document.querySelectorAll('a[href^="#"]'); 30 31 anchorLinks.forEach(function (link) { 32 link.addEventListener('click', function (e) { 33 var targetId = this.getAttribute('href'); 34 35 if (targetId === '#' || targetId === '') { 36 return; 37 } 38 39 var targetElement = document.querySelector(targetId); 40 41 if (targetElement) { 42 e.preventDefault(); 43 targetElement.scrollIntoView({ 44 behavior: 'smooth', 45 block: 'start' 46 }); 47 48 // Update URL hash without jumping 49 if (history.pushState) { 50 history.pushState(null, null, targetId); 51 } 52 } 53 }); 54 }); 55 56 // --------------------------------------------------------------------------- 57 // FAQ Accordion 58 // Single-open behavior: opening one item closes others in the same accordion. 59 // --------------------------------------------------------------------------- 60 var accordionTriggers = document.querySelectorAll('.accordion__trigger'); 61 62 accordionTriggers.forEach(function (trigger) { 63 trigger.addEventListener('click', function (e) { 64 e.preventDefault(); 65 66 var parentItem = this.closest('.accordion__item'); 67 var parentAccordion = this.closest('.accordion'); 68 var isCurrentlyOpen = parentItem.classList.contains('is-open'); 69 70 // Determine the content panel â the sibling element controlled by the trigger 71 var contentPanelId = this.getAttribute('aria-controls'); 72 var contentPanel = contentPanelId 73 ? document.getElementById(contentPanelId) 74 : parentItem.querySelector('.accordion__content'); 75 76 // Close all other open items within the same accordion 77 if (parentAccordion) { 78 var openItems = parentAccordion.querySelectorAll('.accordion__item.is-open'); 79 openItems.forEach(function (openItem) { 80 if (openItem !== parentItem) { 81 openItem.classList.remove('is-open'); 82 83 var otherTrigger = openItem.querySelector('.accordion__trigger'); 84 if (otherTrigger) { 85 otherTrigger.setAttribute('aria-expanded', 'false'); 86 } 87 88 var otherPanelId = otherTrigger 89 ? otherTrigger.getAttribute('aria-controls') 90 : null; 91 var otherPanel = otherPanelId 92 ? document.getElementById(otherPanelId) 93 : openItem.querySelector('.accordion__content'); 94 95 if (otherPanel) { 96 otherPanel.setAttribute('aria-hidden', 'true'); 97 otherPanel.setAttribute('hidden', ''); 98 } 99 } 100 }); 101 } 102 103 // Toggle the clicked item 104 if (isCurrentlyOpen) { 105 parentItem.classList.remove('is-open'); 106 this.setAttribute('aria-expanded', 'false'); 107 if (contentPanel) { 108 contentPanel.setAttribute('aria-hidden', 'true'); 109 contentPanel.setAttribute('hidden', ''); 110 } 111 } else { 112 parentItem.classList.add('is-open'); 113 this.setAttribute('aria-expanded', 'true'); 114 if (contentPanel) { 115 contentPanel.setAttribute('aria-hidden', 'false'); 116 contentPanel.removeAttribute('hidden'); 117 } 118 } 119 }); 120 }); 121 122 // --------------------------------------------------------------------------- 123 // Pricing Service Filter 124 // Shows/hides pricing sections based on the selected filter button. 125 // --------------------------------------------------------------------------- 126 var filterBar = document.querySelector('.pricing-filter__bar'); 127 128 if (filterBar) { 129 var filterBtns = filterBar.querySelectorAll('.pricing-filter__bt
129n'); 130 var pricingSections = document.querySelectorAll('.pricing-section[data-service]'); 131 132 filterBtns.forEach(function (btn) { 133 btn.addEventListener('click', function () { 134 var filterValue = this.getAttribute('data-filter'); 135 136 // Update active state on buttons 137 filterBtns.forEach(function (b) { 138 b.classList.remove('is-active'); 139 b.setAttribute('aria-selected', 'false'); 140 }); 141 this.classList.add('is-active'); 142 this.setAttribute('aria-selected', 'true'); 143 144 // Show/hide sections 145 pricingSections.forEach(function (section) { 146 var service = section.getAttribute('data-service'); 147 148 if (filterValue === 'all' || service === filterValue) { 149 section.classList.remove('is-hidden'); 150 } else { 151 section.classList.add('is-hidden'); 152 153 // Close any open accordions in hidden sections 154 var openItems = section.querySelectorAll('.accordion__item.is-open'); 155 openItems.forEach(function (item) { 156 item.classList.remove('is-open'); 157 var trigger = item.querySelector('.accordion__trigger'); 158 if (trigger) trigger.setAttribute('aria-expanded', 'false'); 159 var panel = item.querySelector('.accordion__content'); 160 if (panel) { 161 panel.setAttribute('aria-hidden', 'true'); 162 panel.setAttribute('hidden', ''); 163 } 164 }); 165 } 166 }); 167 168 // Scroll to the first visible section when filtering (not for "all") 169 if (filterValue !== 'all') { 170 var targetSection = document.querySelector('.pricing-section[data-service="' + filterValue + '"]'); 171 if (targetSection) { 172 var filterBarEl = document.querySelector('.pricing-filter'); 173 var filterBarHeight = filterBarEl ? filterBarEl.offsetHeight : 0; 174 var headerOffset = filterBarHeight + 16; 175 var elementPosition = targetSection.getBoundingClientRect().top + window.scrollY; 176 window.scrollTo({ 177 top: elementPosition - headerOffset, 178 behavior: 'smooth' 179 }); 180 } 181 } 182 }); 183 }); 184 } 185 186 // --------------------------------------------------------------------------- 187 // Default page (page.php): contents rail scroll-spy. 188 // The rail itself is rendered server-side, so this only adds the active 189 // marker. No-ops on every other template. 190 // --------------------------------------------------------------------------- 191 var pgToc = document.querySelector('.pg__toc'); 192 193 if (pgToc) { 194 var pgLinks = Array.prototype.slice.call(pgToc.querySelectorAll('a[href^="#"]')); 195 var pgHeadings = pgLinks 196 .map(function (link) { return document.getElementById(link.getAttribute('href').slice(1)); }) 197 .filter(Boolean); 198 199 if (pgHeadings.length > 1 && 'IntersectionObserver' in window) { 200 var pgObserver = new IntersectionObserver(function (entries) { 201 entries.forEach(function (entry) { 202 if (!entry.isIntersecting) return; 203 var idx = pgHeadings.indexOf(entry.target); 204 if (idx === -1) return; 205 pgLinks.forEach(function (link, linkIdx) { 206 link.classList.toggle('is-active', linkIdx === idx); 207 }); 208 }); 209 }, { root: null, rootMargin: '-80px 0px -60% 0px', threshold: 0 }); 210
211 pgHeadings.forEach(function (heading) { pgObserver.observe(heading); }); 212 } 213 } 214 215 // --------------------------------------------------------------------------- 216 // Blog Post: Table of Contents (auto-generated from H2 headings) 217 // Scans #post-content for H2 elements, generates a numbered TOC in 218 // #toc-list, adds slug-based IDs to each heading, and uses 219 // IntersectionObserver for scroll-spy active-state highlighting. 220 // --------------------------------------------------------------------------- 221 var tocList = document.getElementById('toc-list'); 222 var postContent = document.getElementById('post-content'); 223 224 if (tocList && postContent) { 225 var headings = postContent.querySelectorAll('.prose h2'); 226 var tocWidget = document.getElementById('sidebar-toc'); 227 228 if (headings.length === 0) { 229 // Hide TOC widget if no H2 headings found 230 if (tocWidget) { 231 tocWidget.classList.add('is-hidden'); 232 } 233 } else { 234 var tocLinks = []; 235 236 headings.forEach(function (heading, index) { 237 // Create a slug-based ID for the heading 238 var slug = heading.textContent 239 .trim() 240 .toLowerCase() 241 .replace(/[^a-z0-9]+/g, '-') 242 .replace(/^-|-$/g, ''); 243 244 // Ensure unique IDs 245 var baseSlug = slug || 'section'; 246 var id = baseSlug; 247 var suffix = 1; 248 while (document.getElementById(id)) { 249 id = baseSlug + '-' + suffix; 250 suffix++; 251 } 252 heading.setAttribute('id', id); 253 254 // Create TOC list item + link 255 var li = document.createElement('li'); 256 li.className = 'sidebar-toc__item'; 257 258 var link = document.createElement('a'); 259 link.className = 'sidebar-toc__link'; 260 link.href = '#' + id; 261 link.textContent = heading.textContent.trim(); 262 link.setAttribute('data-toc-index', index); 263 264 li.appendChild(link); 265 tocList.appendChild(li); 266 tocLinks.push(link); 267 }); 268 269 // Scroll-spy via IntersectionObserver 270 if ('IntersectionObserver' in window && tocLinks.length > 0) { 271 var observerOptions = { 272 root: null, 273 rootMargin: '-80px 0px -60% 0px', 274 threshold: 0 275 }; 276 277 var activeIndex = -1; 278 279 var tocObserver = new IntersectionObserver(function (entries) { 280 entries.forEach(function (entry) { 281 if (entry.isIntersecting) { 282 var idx = Array.prototype.indexOf.call(headings, entry.target); 283 if (idx !== -1) { 284 activeIndex = idx; 285 tocLinks.forEach(function (tocLink, linkIdx) { 286 if (linkIdx === idx) { 287 tocLink.classList.add('is-active'); 288 } else { 289 tocLink.classList.remove('is-active'); 290 } 291 }); 292 } 293 } 294 }); 295 }, observerOptions); 296
297 headings.forEach(function (heading) { 298 tocObserver.observe(heading); 299 }); 300 } 301 } 302 } 303 304 // --------------------------------------------------------------------------- 305 // Blog Post: Copy Link button 306 // Copies the post URL to clipboard and shows a brief "copied" state. 307 // --------------------------------------------------------------------------- 308 var copyBtns = document.querySelectorAll('.js-copy-link'); 309 310 copyBtns.forEach(function (btn) { 311 btn.addEventListener('click', function (e) { 312 e.preventDefault(); 313 var url = this.getAttribute('data-url') || window.location.href; 314 var button = this; 315 316 if (navigator.clipboard && navigator.clipboard.writeText) { 317 navigator.clipboard.writeText(url).then(function () { 318 showCopiedState(button); 319 }); 320 } else { 321 // Fallback for older browsers 322 var textarea = document.createElement('textarea'); 323 textarea.value = url; 324 textarea.style.position = 'fixed'; 325 textarea.style.left = '-9999px'; 326 document.body.appendChild(textarea); 327 textarea.select(); 328 try { 329 document.execCommand('copy'); 330 showCopiedState(button); 331 } catch (err) { 332 // Silently fail 333 } 334 document.body.removeChild(textarea); 335 } 336 }); 337 }); 338 339 function showCopiedState(button) { 340 var icon = button.querySelector('i'); 341 button.classList.add('is-copied'); 342 if (icon) { 343 icon.className = 'fa-solid fa-check'; 344 } 345 setTimeout(function () { 346 button.classList.remove('is-copied'); 347 if (icon) { 348 icon.className = 'fa-solid fa-link'; 349 } 350 }, 2000); 351 } 352 353 // --------------------------------------------------------------------------- 354 // Reading Progress Bar (Module H) 355 // Thin gradient bar below the global nav, on every long-form template: posts, 356 // guides and pillar pages, and the default page template. It measures the 357 // article column, not the document, so the footer does not count as reading. 358 // Guides used to carry their own .guide-progress bar; that one measured whole 359 // document scroll and sat at z-index 600 under the z-index 1000 header, so it 360 // was never visible. This is the single implementation now. 361 // Uses requestAnimationFrame for smooth, performant scroll tracking. 362 // --------------------------------------------------------------------------- 363 var progressBar = document.querySelector('.reading-progress'); 364 var progressTarget = postContent 365 || document.querySelector('.guide-content') 366 || document.querySelector('.pg__main'); 367 368 if (progressBar && progressTarget) { 369 var progressFill = progressBar.querySelector('.reading-progress__bar'); 370 var ticking = false; 371 372 function updateProgressBar() { 373 var article = progressTarget; 374 var articleTop = article.getBoundingClientRect().top + window.scrollY; 375 var articleHeight = article.offsetHeight; 376 var windowHeight = window.innerHeight; 377 var scrollY = window.scrollY; 378 379 // Calculate how far through the article the user has scrolled 380 var start = articleTop; 381 var end = articleTop + articleHeight - windowHeight; 382 var progress = 0; 383 384 if (scrollY >= start && end > start) { 385 progress = Math.min(((scrollY - start) / (end - start)) * 100, 100); 386 } else if (scrollY > end) { 387 progress = 100; 388 } 389 390 if (progressFill) { 391 progressFill.style.width = progress + '%'; 392 } 393 394 // Show/hide the bar 395 if (scrollY > articleTop - windowHeight) { 396 progressBar.classList.add('is-visible'); 397 } else { 398 progressBar.classList.remove('is-visible'); 399 } 400 401 ticking = false; 402 } 403 404 window.addEventListener('scroll', function () { 405 if (!ticking) { 406 requestAnimationFrame(updateProgressBar); 407 ticking = true; 408 } 409 }, { passive: true }); 410 411 // Initial check 412 updateProgressBar(); 413 } 414 415 // --------------------------------------------------------------------------- 416 // Language Switcher Toggle (WPML) 417 // --------------------------------------------------------------------------- 418 var langToggle = document.querySelector('.language-switcher__toggle'); 419 if (langToggle) { 420 langToggle.addEventListener('click', function (e) { 421 e.stopPropagation(); 422 var expanded = this.getAttribute('aria-expanded') === 'true'; 423 this.setAttribute('aria-expanded', String(!expanded)); 424 }); 425 426 // Close dropdown when clicking outside 427 document.addEventListener('click', function (e) { 428 if (!e.target.closest('.language-switcher')) { 429 langToggle.setAttribute('aria-expanded', 'false'); 430 } 431 }); 432 433 // Close on Escape key 434 document.addEventListener('keydown', function (e) { 435 if (e.key === 'Escape') { 436 langToggle.setAttribute('aria-expanded', 'false'); 437 langToggle.focus(); 438 } 439 }); 440 } 441 442 // --------------------------------------------------------------------------- 443 // Form AJAX placeholder 444 // Ready for Gravity Forms integration. Uncomment and adapt when forms are 445 // configured in the WordPress admin. 446 // --------------------------------------------------------------------------- 447 /* 448 var contactForm = document.querySelector('#gform_wrapper_1'); 449 450 if (contactForm) { 451 // Gravity Forms fires a custom event after successful AJAX submission. 452 // Listen for gform_confirmation_loaded to handle post-submit behavior. 453 // 454 // document.addEventListener('gform_confirmation_loaded', function (event) { 455 // var formId = event.detail.formId; 456 // 457 // // Scroll to confirmation message 458 // var confirmation = document.querySelector('#gform_confirmation_message_' + formId); 459 // if (confirmation) { 460 // confirmation.scrollIntoView({ behavior: 'smooth', block: 'center' }); 461 // } 462 // 463 // // Optional: fire analytics event 464 // // if (typeof gtag === 'function') { 465 // // gtag('event', 'form_submission', { 466 // // event_category: 'Contact', 467 // // event_label: 'Form ' + formId 468 // // }); 469 // // } 470 // }); 471 } 472 */ 473 474 // --------------------------------------------------------------------------- 475 // Testimonials Slider 476 // Horizontal carousel with prev/next arrows, dot indicators, and optional 477 // auto-play. Supports touch swipe via pointer events. 478 // --------------------------------------------------------------------------- 479 var sliders = document.querySelectorAll('.ts-slider'); 480
481 sliders.forEach(function (slider) { 482 var track = slider.querySelector('.ts-slider__track'); 483 var slides = slider.querySelectorAll('.ts-slider__slide'); 484 var dots = slider.querySelectorAll('.ts-slider__dot'); 485 var prevBtn = slider.querySelector('.ts-slider__arrow--prev'); 486 var nextBtn = slider.querySelector('.ts-slider__arrow--next'); 487 var autoplay = slider.getAttribute('data-autoplay') === 'true'; 488 var interval = parseInt(slider.getAttribute('data-interval'), 10) || 6000; 489 var current = 0; 490 var total = slides.length; 491 var timer = null; 492 493 if (total <= 1) return; 494 495 function goTo(index) { 496 if (index < 0) index = total - 1; 497 if (index >= total) index = 0; 498 current = index; 499 500 track.style.transform = 'translateX(-' + (current * 100) + '%)'; 501 502 // Update aria 503 slides.forEach(function (slide, i) { 504 if (i === current) { 505 slide.removeAttribute('aria-hidden'); 506 slide.removeAttribute('tabindex'); 507 } else { 508 slide.setAttribute('aria-hidden', 'true'); 509 slide.setAttribute('tabindex', '-1'); 510 } 511 }); 512 513 // Update dots 514 dots.forEach(function (dot, i) { 515 dot.classList.toggle('is-active', i === current); 516 dot.setAttribute('aria-selected', i === current ? 'true' : 'false'); 517 }); 518 } 519 520 function startAutoplay() { 521 if (autoplay && !timer) { 522 timer = setInterval(function () { goTo(current + 1); }, interval); 523 } 524 } 525 526 function stopAutoplay() { 527 if (timer) { 528 clearInterval(timer); 529 timer = null; 530 } 531 } 532 533 // Arrow listeners 534 if (prevBtn) prevBtn.addEventListener('click', function () { stopAutoplay(); goTo(current - 1); startAutoplay(); }); 535 if (nextBtn) nextBtn.addEventListener('click', function () { stopAutoplay(); goTo(current + 1); startAutoplay(); }); 536 537 // Dot listeners 538 dots.forEach(function (dot) { 539 dot.addEventListener('click', function () { 540 stopAutoplay(); 541 goTo(parseInt(this.getAttribute('data-slide'), 10)); 542 startAutoplay(); 543 }); 544 }); 545 546 // Pause on hover/focus 547 slider.addEventListener('mouseenter', stopAutoplay); 548 slider.addEventListener('mouseleave', startAutoplay); 549 slider.addEventListener('focusin', stopAutoplay); 550 slider.addEventListener('focusout', startAutoplay); 551 552 // Keyboard navigation 553 slider.addEventListener('keydown', function (e) { 554 if (e.key === 'ArrowLeft') { stopAutoplay(); goTo(current - 1); startAutoplay(); } 555 if (e.key === 'ArrowRight') { stopAutoplay(); goTo(current + 1); startAutoplay(); } 556 }); 557 558 // Touch swipe support 559 var startX = 0; 560 var isDragging = false; 561 562 track.addEventListener('pointerdown', function (e) { 563 if (e.pointerType === 'mouse') return; // arrows handle mouse 564 startX = e.clientX; 565 isDragging = true; 566 stopAutoplay(); 567 }); 568 569 track.addEventListener('pointerup', function (e) { 570 if (!isDragging) return; 571 isDragging = false; 572 var diff = startX - e.clientX; 573 if (Math.abs(diff) > 50) { 574 goTo(diff > 0 ? current + 1 : current - 1); 575 } 576 startAutoplay(); 577 }); 578 579 // Init 580 startAutoplay(); 581 }); 582 583 // --------------------------------------------------------------------------- 584 // Stats Counter â Animated Count-Up 585 // Uses IntersectionObserver to trigger a count-up animation when the 586 // element scrolls into view. Numbers animate once, then stop. 587 // --------------------------------------------------------------------------- 588 var counters = document.querySelectorAll('[data-count-to]'); 589 590 if (counters.length > 0 && 'IntersectionObserver' in window) { 591 var counterObserver = new IntersectionObserver(function (entries) { 592 entries.forEach(function (entry) { 593 if (!entry.isIntersecting) return; 594 595 var el = entry.target; 596 var target = parseFloat(el.getAttribute('data-count-to')); 597 var prefix = el.getAttribute('data-count-prefix') || ''; 598 var suffix = el.getAttribute('data-count-suffix') || ''; 599 var duration = 2000; // ms 600 var start = 0; 601 var startTime = null; 602 var isFloat = target % 1 !== 0; 603 604 function animate(timestamp) { 605 if (!startTime) startTime = timestamp; 606 var progress = Math.min((timestamp - startTime) / duration, 1); 607 608 // Ease-out cubic 609 var eased = 1 - Math.pow(1 - progress, 3); 610 var current = eased * target; 611 612 if (isFloat) { 613 el.textContent = prefix + current.toFixed(1) + suffix; 614 } else { 615 el.textContent = prefix + Math.floor(current).toLocaleStr
615ing() + suffix; 616 } 617 618 if (progress < 1) { 619 requestAnimationFrame(animate); 620 } else { 621 // Final value â ensure exact 622 if (isFloat) { 623 el.textContent = prefix + target.toFixed(1) + suffix; 624 } else { 625 el.textContent = prefix + target.toLocaleString() + suffix; 626 } 627 } 628 } 629 630 requestAnimationFrame(animate); 631 counterObserver.unobserve(el); 632 }); 633 }, { 634 threshold: 0.3 635 }); 636 637 counters.forEach(function (counter) { 638 counterObserver.observe(counter); 639 }); 640 } 641 642 // (The guide-specific progress bar was removed: guides now use the shared 643 // .reading-progress bar above, which measures the article column and clears 644 // the fixed header. See "Reading Progress Bar (Module H)".) 645 646 // --------------------------------------------------------------------------- 647 // Guide: TOC Active Link Highlighting 648 // Uses IntersectionObserver to track visible .guide-section or dynamic h2[id] 649 // and apply .is-active to the matching .guide-toc__link. 650 // --------------------------------------------------------------------------- 651 var guideSections = document.querySelectorAll('.guide-section[id], .guide-content h2[id]'); 652 var guideTocLinks = document.querySelectorAll('.guide-toc__link'); 653 if (guideSections.length && guideTocLinks.length) { 654 var activeSectionId = null; 655 var tocObserver = new IntersectionObserver(function (entries) { 656 entries.forEach(function (entry) { 657 if (entry.isIntersecting) { 658 activeSectionId = entry.target.id; 659 guideTocLinks.forEach(function (link) { 660 var isActive = link.getAttribute('href') === '#' + activeSectionId; 661 link.classList.toggle('is-active', isActive); 662 }); 663 } 664 }); 665 }, { rootMargin: '-15% 0px -70% 0px', threshold: 0 }); 666 guideSections.forEach(function (section) { 667 tocObserver.observe(section); 668 }); 669 } 670 671 // --------------------------------------------------------------------------- 672 // Guide: FAQ Accordion 673 // Toggles .is-open on .guide-faq__item when question is clicked. 674 // --------------------------------------------------------------------------- 675 document.querySelectorAll('.guide-faq__question').forEach(function (question) { 676 question.addEventListener('click', function () { 677 var item = this.closest('.guide-faq__item'); 678 if (!item) return; 679 var isOpen = item.classList.contains('is-open'); 680 // Close siblings 681 var siblings = item.parentElement.querySelectorAll('.guide-faq__item'); 682 siblings.forEach(function (s) { s.classList.remove('is-open'); }); 683 if (!isOpen) { 684 item.classList.add('is-open'); 685 } 686 }); 687 }); 688 689 // --------------------------------------------------------------------------- 690 // Stat Card Detail Modal 691 // Single shared modal populated dynamically from data-* attributes on 692 // .stat-card elements. Used by the Statistics blog template. 693 // --------------------------------------------------------------------------- 694 var statModal = document.getElementById('stat-detail-modal'); 695 696 if (statModal) { 697 var modalValue = document.getElementById('stat-modal-value'); 698 var modalLabel = document.getElementById('stat-modal-label'); 699 var modalSource = document.getElementById('stat-modal-source'); 700 var previousFocus = null; 701 702 // Open modal and populate from card data attributes 703 function openStatModal(card) { 704 var value = card.getAttribute('data-stat-value') || ''; 705 var label = card.getAttribute('data-stat-label') || ''; 706 var detail = card.getAttribute('data-stat-detail') || ''; 707 var source = card.getAttribute('data-stat-source') || ''; 708 var sourceUrl = card.getAttribute('data-stat-source-url') || ''; 709 var year = card.getAttribute('data-stat-year') || ''; 710 711 // Populate value 712 if (modalValue) modalValue.textContent = value; 713 714 // Populate label â show rich detail if available, otherwise short label 715 if (modalLabel) { 716 if (detail) { 717 var escapedLabel = label.replace(/&/g, '&').replace(/</g, '<'); 718 var escapedDetail = detail.replace(/&/g, '&').replace(/</g, '<'); 719 modalLabel.innerHTML = '<strong>' + escapedLabel + '</strong>' + 720 '<span class="stat-modal__detail">' + escapedDetail + '</span>'; 721 } else { 722 modalLabel.textContent = label; 723 } 724 } 725 726 // Build source line (subtle, at bottom) 727 if (modalSource) { 728 var sourceHtml = ''; 729 if (source && sourceUrl) { 730 sourceHtml = '<a href="' + sourceUrl + '" target="_blank" rel="noopener noreferrer">' + 731 source.replace(/&/g, '&').replace(/</g, '<') + '</a>'; 732 } else if (source) {
733 sourceHtml = source.replace(/&/g, '&').replace(/</g, '<'); 734 } 735 if (year) { 736 sourceHtml += (sourceHtml ? ' · ' : '') + year; 737 } 738 modalSource.innerHTML = sourceHtml; 739 modalSource.style.display = sourceHtml ? '' : 'none'; 740 } 741 742 // Store current focus for restoration 743 previousFocus = document.activeElement; 744 745 // Show modal 746 statModal.removeAttribute('hidden'); 747 document.body.style.overflow = 'hidden'; 748 749 // Focus the close button 750 var closeBtn = statModal.querySelector('.stat-modal__close'); 751 if (closeBtn) { 752 closeBtn.focus(); 753 } 754 } 755 756 // Close modal and restore state 757 function closeStatModal() { 758 statModal.setAttribute('hidden', ''); 759 document.body.style.overflow = ''; 760 761 // Restore focus to the element that opened the modal 762 if (previousFocus && previousFocus.focus) { 763 previousFocus.focus(); 764 } 765 } 766 767 // Delegate click on all "Detail" buttons 768 document.addEventListener('click', function (e) { 769 var detailBtn = e.target.closest('.js-stat-detail'); 770 if (detailBtn) { 771 e.preventDefault(); 772 var card = detailBtn.closest('.stat-card'); 773 if (card) { 774 openStatModal(card); 775 } 776 } 777 }); 778 779 // Close on backdrop click or close button click 780 var closeTargets = statModal.querySelectorAll('.js-stat-modal-close'); 781 closeTargets.forEach(function (el) { 782 el.addEventListener('click', function (e) { 783 if (e.target === this) { 784 closeStatModal(); 785 } 786 }); 787 }); 788 789 // Close on Escape key 790 document.addEventListener('keydown', function (e) { 791 if (e.key === 'Escape' && !statModal.hasAttribute('hidden')) { 792 closeStatModal(); 793 } 794 }); 795 796 // Trap focus inside modal when open 797 statModal.addEventListener('keydown', function (e) { 798 if (e.key !== 'Tab') return; 799 800 var focusable = statModal.querySelectorAll( 801 'button, [href], [tabindex]:not([tabindex="-1"])' 802 ); 803 if (focusable.length === 0) return; 804 805 var first = focusable[0]; 806 var last = focusable[focusable.length - 1]; 807 808 if (e.shiftKey) { 809 if (document.activeElement === first) { 810 e.preventDefault(); 811 last.focus(); 812 } 813 } else { 814 if (document.activeElement === last) { 815 e.preventDefault(); 816 first.focus(); 817 } 818 } 819 }); 820 821 // --- Share button: build X/Twitter intent URL when modal opens --- 822 var shareBtn = statModal.querySelector('.js-stat-share-x'); 823 824 // Wrap openStatModal to also populate the share button href 825 var _baseOpenStatModal = openStatModal; 826 openStatModal = function (card) { 827 _baseOpenStatModal(card); 828 829 if (shareBtn) { 830 var value = card.getAttribute('data-stat-value') || ''; 831 var label = card.getAttribute('data-stat-label') || ''; 832 var statId = card.getAttribute('data-stat-id') || ''; 833 var pageUrl = window.location.origin + window.location.pathname; 834 if (statId) { 835 pageUrl += '#' + statId; 836 } 837 838 var tweetText = value + ' â ' + label; 839 var intentUrl = 'https://x.com/intent/tweet?text=' + 840 encodeURIComponent(tweetText) + 841 '&url=' + encodeURIComponent(pageUrl); 842 843 shareBtn.setAttribute('href', intentUrl); 844 } 845 }; 846 } 847 848 // --------------------------------------------------------------------------- 849 // Stat Highlight Carousel 850 // Auto-rotates through stat highlight slides with fade transitions. 851 // Pauses on hover/focus, supports dot navigation. 852 // --------------------------------------------------------------------------- 853 var statCarousel = document.querySelector('.stat-highlight-carousel'); 854 855 if (statCarousel) { 856 var carouselSlides = statCarousel.querySelectorAll('.stat-highlight-carousel__slide'); 857 var carouselDots = statCarousel.querySelectorAll('.stat-highlight-carousel__dot'); 858 var carouselCurrent = 0; 859 var carouselTotal = carouselSlides.length; 860 var carouselTimer = null; 861 var carouselInterval = 6000; 862 863 if (carouselTotal > 1) { 864 function goToSlide(index) { 865 if (index < 0) index = carouselTotal - 1; 866 if (index >= carouselTotal) index = 0; 867 carouselCurrent = index; 868
869 carouselSlides.forEach(function (slide, i) { 870 if (i === carouselCurrent) { 871 slide.classList.add('is-active'); 872 } else { 873 slide.classList.remove('is-active'); 874 } 875 }); 876 877 carouselDots.forEach(function (dot, i) { 878 if (i === carouselCurrent) { 879 dot.classList.add('is-active'); 880 } else { 881 dot.classList.remove('is-active'); 882 } 883 }); 884 } 885 886 function startCarousel() { 887 if (!carouselTimer) { 888 carouselTimer = setInterval(function () { 889 goToSlide(carouselCurrent + 1); 890 }, carouselInterval); 891 } 892 } 893 894 function stopCarousel() { 895 if (carouselTimer) { 896 clearInterval(carouselTimer); 897 carouselTimer = null; 898 } 899 } 900 901 // Dot click handlers 902 carouselDots.forEach(function (dot) { 903 dot.addEventListener('click', function () { 904 stopCarousel(); 905 goToSlide(parseInt(this.getAttribute('data-slide'), 10)); 906 startCarousel(); 907 }); 908 }); 909 910 // Pause on hover/focus 911 statCarousel.addEventListener('mouseenter', stopCarousel); 912 statCarousel.addEventListener('mouseleave', startCarousel); 913 statCarousel.addEventListener('focusin', stopCarousel); 914 statCarousel.addEventListener('focusout', startCarousel); 915 916 // Respect prefers-reduced-motion 917 var prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)'); 918 if (!prefersReduced.matches) { 919 startCarousel(); 920 } 921 } 922 } 923 924 // --------------------------------------------------------------------------- 925 // Hash-based Scroll-to-Stat 926 // If the URL has a #stat-N hash, smooth-scroll to that card and briefly 927 // highlight it with an outline pulse. 928 // --------------------------------------------------------------------------- 929 var hash = window.location.hash; 930 if (hash && hash.match(/^#stat-\d+$/)) { 931 var targetStat = document.querySelector(hash); 932 if (targetStat) { 933 // Small delay to ensure page layout is complete 934 setTimeout(function () { 935 targetStat.scrollIntoView({ behavior: 'smooth', block: 'center' }); 936 targetStat.classList.add('stat-card--highlight'); 937 setTimeout(function () { 938 targetStat.classList.remove('stat-card--highlight'); 939 }, 2000); 940 }, 500); 941 } 942 } 943 944 // ââ Hero Mockup State Switcher ââââââââââââââââââââââââââââââââââââââââââââ 945 // Auto-cycles between the Search and Wikipedia states every 5 seconds. 946 // Pauses on hover, resumes on mouse-out. Dots are clickable. 947 (function () { 948 var states = document.querySelectorAll('.hero__mockup-state'); 949 var dots = document.querySelectorAll('.hero__mockup-dot-btn'); 950 var visual = document.querySelector('.hero__visual'); 951 var urlBar = document.querySelector('.hero__mockup-url-text'); 952 if (!states.length) return; 953 954 var stateKeys = ['search', 'wiki']; 955 var urlLabels = { 956 search: 'reputationx.com/dashboard', 957 wiki: 'en.wikipedia.org/wiki/Acme_Corp' 958 }; 959 var current = 0; 960 var timer; 961 962 function goTo(index) { 963 states.forEach(function (s) { s.classList.remove('hero__mockup-state--active'); }); 964 dots.forEach(function (d) { d.classList.remove('active'); }); 965 states[index].classList.add('hero__mockup-state--active'); 966 if (dots[index]) { dots[index].classList.add('active'); } 967 if (visual) { visual.setAttribute('data-active', stateKeys[index]); } 968 // Fade URL bar text: out â change text â back in (synced with CSS 0.15s transition) 969 if (urlBar) { 970 urlBar.style.opacity = '0'; 971 setTimeout(function () { 972 urlBar.textContent = urlLabels[stateKeys[index]]; 973 urlBar.style.opacity = ''; /* clear inline â CSS transition fades back to 1 */ 974 }, 150); 975 } 976 current = index; 977 } 978 979 function advance() { goTo((current + 1) % states.length); } 980 function start() { timer = setInterval(advance, 5000); } 981 function stop() { clearInterval(timer); } 982 983 // Dot clicks
984 dots.forEach(function (dot, i) { 985 dot.addEventListener('click', function () { stop(); goTo(i); start(); }); 986 }); 987 988 // Pause on hover 989 var mockup = document.querySelector('.hero__mockup'); 990 if (mockup) { 991 mockup.addEventListener('mouseenter', stop); 992 mockup.addEventListener('mouseleave', start); 993 } 994 995 goTo(0); 996 start(); 997 })(); 998 999});
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.