PageSourceSearch

https://www.reputationx.com/wp-content/themes/repx-theme/assets/js/main.js?ver=1.0.48

js reputationx.com collected 2026-10-02 03:43:29 UTC 35,967 bytes, 999 lines download raw bytes

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, '&amp;').replace(/</g, '&lt;');
718          var escapedDetail = detail.replace(/&/g, '&amp;').replace(/</g, '&lt;');
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, '&amp;').replace(/</g, '&lt;') + '</a>';
732        } else if (source) {
733          sourceHtml = source.replace(/&/g, '&amp;').replace(/</g, '&lt;');
734        }
735        if (year) {
736          sourceHtml += (sourceHtml ? ' &middot; ' : '') + 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.