PageSourceSearch

https://tabnav.com/js/index.js

js tabnav.com collected 2026-10-02 02:33:49 UTC 90,648 bytes, 2,721 lines download raw bytes

1// Video Landing Script
2(function () {
3  const vidLandVideo = document.querySelector(".vid-land-video");
4  const vidLandFallbackImg = document.querySelector(".vid-land-fallback-img");
5  const vidLandContainer = document.querySelector(".video-container");
6
7  if (!vidLandVideo || !vidLandFallbackImg || !vidLandContainer) return;
8
9  let started = false; // a real play() attempt is in flight or succeeded
10  let warmedOnce = false; // the initial load-time "play 3s then pause" has run
11
12  function showFallbackImage() {
13    vidLandVideo.style.display = "none";
14    vidLandFallbackImg.style.display = "block";
15  }
16
17  function showVideo() {
18    vidLandFallbackImg.style.display = "none";
19    vidLandVideo.style.display = "block";
20  }
21
22  function startVideo() {
23    if (started) return;
24    started = true;
25
26    const playPromise = vidLandVideo.play();
27    if (playPromise !== undefined) {
28      playPromise.catch((error) => {
29        console.log("Video play failed:", error);
30        showFallbackImage();
31        started = false; // allow a retry (e.g. on user interaction)
32      });
33    }
34  }
35
36  // Warm up immediately on page load, regardless of scroll position: play
37  // for ~3s then pause. This swaps the fallback image out for the video's
38  // real frame right away, so by the time someone scrolls down to it —
39  // even quickly — there is no visible fallback->video jump.
40  function warmUp() {
41    if (warmedOnce) return;
42    warmedOnce = true;
43    startVideo();
44    setTimeout(() => {
45      if (!vidLandVideo.paused) vidLandVideo.pause();
46    }, 3000);
47  }
48
49  // Resume (and keep looping, via the existing `loop` attribute) once the
50  // section actually enters the viewport.
51  function resumeInView() {
52    if (!started) {
53      startVideo();
54      return;
55    }
56    if (vidLandVideo.paused) {
57      vidLandVideo.play().catch(() => {});
58    }
59  }
60
61  function initVideo() {
62    const canPlayMP4 = vidLandVideo.canPlayType("video/mp4");
63    const canPlayWebM = vidLandVideo.canPlayType("video/webm");
64
65    if (canPlayWebM === "" && canPlayMP4 === "") {
66      showFallbackImage();
67      return;
68    }
69
70    // Add error handler
71    vidLandVideo.addEventListener("error", showFallbackImage);
72
73    // The fallback image is shown by default (see CSS); only reveal the
74    // video once it is actually confirmed to be playing, so there is never
75    // a moment where neither is visible (blank space) or both are stacked.
76    vidLandVideo.addEventListener("playing", showVideo);
77
78    warmUp();
79
80    if ("IntersectionObserver" in window) {
81      const observer = new IntersectionObserver(
82        (entries) => {
83          entries.forEach((entry) => {
84            if (entry.isIntersecting) {
85              resumeInView();
86              observer.disconnect();
87            }
88          });
89        },
90        { threshold: 0.25 }
91      );
92      observer.observe(vidLandContainer);
93    }
94
95    // Fallback triggers in case the load-time warm-up was blocked for some reason.
96    vidLandContainer.addEventListener("mouseenter", startVideo, { once: true });
97    vidLandContainer.addEventListener("click", startVideo, { once: true });
98    vidLandContainer.addEventListener("touchstart", startVideo, {
99      once: true,
100      passive: true,
101    });
102  }
103
104  window.addEventListener("DOMContentLoaded", initVideo);
105})();
106
107// Bro Animation Script
108(function () {
109  document.addEventListener("DOMContentLoaded", function () {
110    // Initialize Bro Animation with viewport detection
111    let broAnimation = null;
112    let broAnimationObserver = null;
113
114    function initBroAnimation() {
115      const container = document.getElementById("bro-animation");
116      if (!container) {
117        console.error("Bro animation container not found!");
118        return;
119      }
120
121      // Create the Lottie animation
122      if (typeof lottie !== "undefined") {
123        broAnimation = lottie.loadAnimation({
124          container: container,
125          renderer: "svg",
126          loop: true,
127          autoplay: false, // Don't autoplay until in viewport
128          path: "../jsons/bro.json",
129        });
130
131        // Set up after animation is loaded
132        broAnimation.addEventListener("DOMLoaded", function () {
133          console.log("Bro Lottie animation loaded successfully");
134          setupBroViewportDetection();
135        });
136
137        // Debugging Helpers
138        broAnimation.addEventListener("error", function (err) {
139          console.error("Error loading Bro animation:", err);
140        });
141      }
142    }
143
144    // Set up viewport detection for bro animation
145    function setupBroViewportDetection() {
146      const broSection = document.querySelector(".bro-section");
147      if (!broSection) {
148        console.error("Bro section not found!");
149        return;
150      }
151
152      // Create Intersection Observer
153      broAnimationObserver = new IntersectionObserver(
154        (entries) => {
155          entries.forEach((entry) => {
156            if (entry.isIntersecting) {
157              // Section is in viewport, start animation
158              if (broAnimation && !broAnimation.isPlaying) {
159                broAnimation.play();
160                console.log("Bro animation started - section in viewport");
161              }
162            } else {
163              // Section is out of viewport, pause animation
164              if (broAnimation && broAnimation.isPlaying) {
165                broAnimation.pause();
166                console.log("Bro animation paused - section out of viewport");
167              }
168            }
169          });
170        },
171        {
172          threshold: 0.3, // Start when 30% of the section is visible
173          rootMargin: "0px 0px -50px 0px", // Start slightly before the section is fully visible
174        }
175      );
176
177      // Start observing the bro section
178      broAnimationObserver.observe(broSection);
179    }
180
181    // Initialize bro animation
182    initBroAnimation();
183
184    // Initialize Scanning Animation
185    const scanningContainer = document.getElementById(
186      "scanning-landing-container"
187    );
188    if (scanningContainer && typeof lottie !== "undefined") {
189      const scanningAnimation = lottie.loadAnimation({
190        container: scanningContainer,
191        renderer: "svg",
192        loop: true,
193        autoplay: true,
194        path: "jsons/moniland.json",
195      });
196
197      scanningAnimation.addEventListener("error", function (err) {
198        console.error("Error loading Scanning animation:", err);
199      });
200    }
201  });
202})();
203
204// Solutions tablist content swap
205(function () {
206  document.addEventListener("DOMContentLoaded", () => {
207    const tablist = document.querySelector(".solutions-tablist");
208    const tabs = tablist
209      ? Array.from(tablist.querySelectorAll(".solutions-tab"))
210      : [];
211    const dropdownNative = document.querySelector(".solutions-dropdown-native");
212    const dropdownCombobox = document.getElementById("solutionsDropdownCombobox");
213    const dropdownList = document.getElementById("solutionsDropdownList");
214    const dropdownOptions = dropdownList
215      ? Array.from(dropdownList.querySelectorAll(".solutions-dropdown-custom-option"))
216      : [];
217    if (!tablist || tabs.length === 0) return;
218
219    const updateContent = (key) => {
220      if (!key) return;
221
222      const allContentElements = document.querySelectorAll("[data-tab-content]");
223      
224      allContentElements.forEach((element) => {
225        const elementTab = element.getAttribute("data-tab-content");
226        if (elementTab === key) {
227          element.style.display = "";
228        } else {
229          element.style.display = "none";
230        }
231      });
232    };
233
234    let isInitialLoad = true;
235    let currentTabIndex = 0;
236
237    const setActiveTab = (tab) => {
238      const panel = document.querySelector(".accessibility-solutions-panel");
239      const newTabIndex = tabs.indexOf(tab);
240      const slideDirection =
241        newTabIndex > currentTabIndex ? "slide-right" : "slide-left";
242
243      tabs.forEach((btn) => {
244        const isSelected = btn === tab;
245        btn.setAttribute("aria-selected", isSelected.toString());
246        btn.tabIndex = isSelected ? 0 : -1;
247        btn.classList.toggle("is-active", isSelected);
248      });
249
250      if (dropdownNative && tab.dataset.tabKey) {
251        dropdownNative.value = tab.dataset.tabKey;
252        const changeEvent = new Event("change", { bubbles: true });
253        dropdownNative.dispatchEvent(changeEvent);
254      }
255
256      if (tablist) {
257        const activeTab = tab;
258        const tablistRect = tablist.getBoundingClientRect();
259        const activeTabRect = activeTab.getBoundingClientRect();
260
261        const left = activeTabRect.left - tablistRect.left;
262        const width = activeTabRect.width;
263
264        tablist.style.setProperty("--slider-left", `${left}px`);
265        tablist.style.setProperty("--slider-width", `${width}px`);
266      }
267
268      if (panel && !isInitialLoad) {
269        panel.classList.remove("sliding-in", "slide-left", "slide-right");
270        panel.classList.add("transitioning", slideDirection);
271        setTimeout(() => {
272          updateContent(tab.dataset.tabKey);
273          panel.classList.remove("transitioning");
274          panel.classList.add("sliding-in", slideDirection);
275          setTimeout(() => {
276            panel.classList.remove("sliding-in", "slide-left", "slide-right");
277          }, 400);
278        }, 400);
279      } else {
280        updateContent(tab.dataset.tabKey);
281        isInitialLoad = false;
282      }
283
284      currentTabIndex = newTabIndex;
285    };
286
287    tabs.forEach((tab, index) => {
288      const defaultKey = index === 0 ? "owners" : "partners";
289      if (!tab.dataset.tabKey) {
290        tab.dataset.tabKey = defaultKey;
291      }
292      tab.addEventListener("click", () => setActiveTab(tab));
293      tab.addEventListener("keydown", (event) => {
294        if (event.key === "Enter" || event.key === " ") {
295          event.preventDefault();
296          setActiveTab(tab);
297        }
298        if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
299          event.preventDefault();
300          const direction = event.key === "ArrowRight" ? 1 : -1;
301          const nextIndex =
302            (tabs.indexOf(tab) + direction + tabs.length) % tabs.length;
303          setActiveTab(tabs[nextIndex]);
304          tabs[nextIndex].focus();
305        }
306      });
307    });
308
309    const initiallySelected =
310      tabs.find((tab) => tab.getAttribute("aria-selected") === "true") ||
311      tabs[0];
312    currentTabIndex = tabs.indexOf(initiallySelected);
313    setActiveTab(initiallySelected);
314
315    if (dropdownCombobox && dropdownList && dropdownOptions.length > 0 && dropdownNative) {
316      let activeDropdownOption = dropdownOptions.find(function (opt) {
317        return opt.getAttribute("aria-selected") === "true";
318      }) || dropdownOptions[0] || null;
319
320      function setActiveDropdownOption(option, scrollIntoView) {
321        if (!option) return;
322        activeDropdownOption = option;
323        dropdownOptions.forEach(function (opt) {
324          opt.classList.remove("solutions-dropdown-custom-option--active");
325        });
326        option.classList.add("solutions-dropdown-custom-option--active");
327        if (dropdownCombobox) {
328          dropdownCombobox.setAttribute("aria-activedescendant", option.id);
329        }
330        if (scrollIntoView) {
331          option.scrollIntoView({ block: "nearest" });
332        }
333      }
334
335      function getSelectedDropdownOption() {
336        return dropdownOptions.find(function (opt) {
337          return opt.getAttribute("aria-selected") === "true";
338        }) || dropdownOptions[0];
339      }
340
341      function openDropdown() {
342        if (dropdownCombobox.getAttribute("aria-expanded") === "true") {
343          return;
344        }
345        dropdownCombobox.setAttribute("aria-expanded", "true");
346        dropdownList.hidden = false;
347        setActiveDropdownOption(getSelectedDropdownOption(), true);
348      }
349
350      function closeDropdown() {
351        if (dropdownCombobox.getAttribute("aria-expanded") === "false") {
352          return;
353        }
354        dropdownCombobox.setAttribute("aria-expanded", "false");
355        dropdownList.hidden = true;
356        setActiveDropdownOption(getSelectedDropdownOption(), false);
357        dropdownCombobox.focus();
358      }
359
360      function toggleDropdown(forceState) {
361        const isExpanded = dropdownCombobox.getAttribute("aria-expanded") === "true";
362        const shouldOpen = typeof forceState === "boolean" ? forceState : !isExpanded;
363        if (shouldOpen) {
364          openDropdown();
365        } else {
366          closeDropdown();
367        }
368      }
369
370      function moveActiveDropdownOption(step) {
371        if (!dropdownOptions.length) return;
372        const optionsArray = dropdownOptions;
373        const currentIndex = optionsArray.indexOf(activeDropdownOption);
374        let nextIndex = currentIndex + step;
375        if (nextIndex < 0) {
376          nextIndex = 0;
377        } else if (nextIndex >= optionsArray.length) {
378          nextIndex = optionsArray.length - 1;
379        }
380        setActiveDropdownOption(optionsArray[nextIndex], true);
381      }
382
383      function selectDropdownOption(option) {
384        if (!option) return;
385        const value = option.getAttribute("data-value");
386        const selectOption = Array.from(dropdownNative.options).find(function (opt) {
387          return opt.value === value;
388        });
389        if (selectOption) {
390          dropdownNative.selectedIndex = Array.from(dropdownNative.options).indexOf(selectOption);
391          const changeEvent = new Event("change", { bubbles: true });
392          dropdownNative.dispatchEvent(changeEvent);
393        }
394        dropdownOptions.forEach(function (opt) {
395          opt.setAttribute("aria-selected", opt === option ? "true" : "false");
396        });
397        updateDropdownDisplay(option);
398        closeDropdown();
399      }
400
401      function selectActiveDropdownOption() {
402        if (activeDropdownOption) {
403          selectDropdownOption(activeDropdownOption);
404        }
405      }
406
407      function updateDropdownDisplay(option) {
408        const selectedSpan = dropdownCombobox.querySelector(".solutions-dropdown-custom-selected");
409        if (!selectedSpan) return;
410        const text = option ? option.querySelector(".solutions-dropdown-custom-text").textContent : "Select option";
411        selectedSpan.innerHTML = '<span class="solutions-dropdown-custom-text">' + text + "</span>";
412        if (option) {
413          setActiveDropdownOption(option, false);
414        }
415      }
416
417      function syncDropdownToNative() {
418        const selectedIndex = dropdownNative.selectedIndex;
419        const selectedOption = dropdownNative.options[selectedIndex];
420        const customOption = Array.from(dropdownOptions).find(function (opt) {
421          return opt.getAttribute("data-value") === selectedOption.value;
422        });
423        if (customOption) {
424          dropdownOptions.forEach(function (opt) {
425            opt.setAttribute("aria-selected", "false");
426          });
427          customOption.setAttribute("aria-selected", "true");
428          updateDropdownDisplay(customOption);
429        }
430      }
431
432      dropdownCombobox.addEventListener("click", function (e) {
433        const button = e.target.closest(".solutions-dropdown-custom-button");
434        if (!button) {
435          return;
436        }
437        e.stopPropagation();
438        toggleDropdown();
439      });
440
441      document.addEventListener("click", function (e) {
442        if (!dropdownCombobox.contains(e.target) && !dropdownList.contains(e.target)) {
443          closeDropdown();
444        }
445      });
446
447      dropdownOptions.forEach(function (option) {
448        option.addEventListener("mousedown", function (e) {
449          e.preventDefault();
450        });
451        option.addEventListener("click", function () {
452          selectDropdownOption(option);
453        });
454      });
455
456      dropdownCombobox.addEventListener("keydown", function (e) {
457        const isExpanded = dropdownCombobox.getAttribute("aria-expanded") === "true";
458        if (e.key === "Enter" || e.key === " ") {
459          e.preventDefault();
460          if (isExpanded) {
461            selectActiveDropdownOption();
462          } else {
463            openDropdown();
464          }
465          return;
466        }
467        if (e.key === "Escape") {
468          if (isExpanded) {
469            e.preventDefault();
470            e.stopPropagation();
471            closeDropdown();
472          }
473          return;
474        }
475        if (e.key === "ArrowDown") {
476          e.preventDefault();
477          if (!isExpanded) {
478            openDropdown();
479          }
480          moveActiveDropdownOption(1);
481          return;
482        }
483        if (e.key === "ArrowUp") {
484          e.preventDefault();
485          if (!isExpanded) {
486            openDropdown();
487          }
488          moveActiveDropdownOption(-1);
489          return;
490        }
491        if (e.key === "Home") {
492          e.preventDefault();
493          openDropdown();
494          setActiveDropdownOption(dropdownOptions[0], true);
495          return;
496        }
497        if (e.key === "End") {
498          e.preventDefault();
499          openDropdown();
500          setActiveDropdownOption(dropdownOptions[dropdownOptions.length - 1], true);
501          return;
502        }
503        if (e.key === "Tab") {
504          if (isExpanded) {
505            closeDropdown();
506          }
507        }
508      });
509
510      dropdownNative.addEventListener("change", function () {
511        syncDropdownToNative();
512        const selectedValue = dropdownNative.value;
513        const tabToSelect = tabs.find((tab) => tab.dataset.tabKey === selectedValue);
514        if (tabToSelect) {
515          setActiveTab(tabToSelect);
516        }
517      });
518
519      syncDropdownToNative();
520    }
521
522    window.addEventListener("resize", () => {
523      const activeTab = tabs.find(
524        (tab) => tab.getAttribute("aria-selected") === "true"
525      );
526      if (activeTab && tablist) {
527        const tablistRect = tablist.getBoundingClientRect();
528        const activeTabRect = activeTab.getBoundingClientRect();
529
530        const left = activeTabRect.left - tablistRect.left;
531        const width = activeTabRect.width;
532
533        tablist.style.setProperty("--slider-left", `${left}px`);
534        tablist.style.setProperty("--slider-width", `${width}px`);
535      }
536    });
537  });
538})();
539
540// AIZA Animation Script
541(function () {
542  document.addEventListener("DOMContentLoaded", function () {
543    // Initialize variables
544    let aizaAnimation = null;
545    let animationTimeout = null;
546    let hasPlayedOnce = false; // Track if initial 11-second play has happened
547    let isPlaying = false;
548    let initialPlayInProgress = false; // Flag to track if initial 11-second play is in progress
549    const animationDuration = 11000;
549 // 11 seconds in milliseconds
550
551    // Initialize the Lottie animation
552    function initAnimation() {
553      // Make sure the container exists
554      const container = document.getElementById("aiza-lottie-container");
555      if (!container) {
556        console.error("Animation container not found!");
557        return;
558      }
559
560      if (typeof lottie === "undefined") {
561        console.error("Lottie library not loaded!");
562        return;
563      }
564
565      // Create the Lottie animation
566      aizaAnimation = lottie.loadAnimation({
567        container: container,
568        renderer: "svg",
569        loop: true,
570        autoplay: false, // Don't autoplay until in viewport
571        path: "jsons/aiza.json",
572        rendererSettings: {
573          preserveAspectRatio: "xMidYMid slice",
574          className: "aiza-lottie-svg",
575        },
576      });
577
578      // Set up after animation is loaded
579      aizaAnimation.addEventListener("DOMLoaded", function () {
580        console.log("Lottie animation loaded successfully");
581        setupListeners();
582        checkIfInViewport(); // Check initial position
583      });
584    }
585
586    // Set up event listeners for hover and scroll
587    function setupListeners() {
588      const container = document.getElementById("aiza-lottie-container");
589
590      // Hover events
591      container.addEventListener("mouseenter", function () {
592        console.log("Mouse enter");
593
594        // Only respond to hover after initial play has completed and animation is not in initial play
595        if (
596          aizaAnimation &&
597          !isPlaying &&
598          hasPlayedOnce &&
599          !initialPlayInProgress
600        ) {
601          console.log("Playing animation on hover");
602          playAnimationGradually();
603        }
604      });
605
606      container.addEventListener("mouseleave", function () {
607        console.log("Mouse leave");
608
609        // Only respond to unhover after initial play has completed and animation is not in initial play
610        if (
611          aizaAnimation &&
612          isPlaying &&
613          hasPlayedOnce &&
614          !initialPlayInProgress
615        ) {
616          console.log("Pausing animation on unhover");
617          pauseAnimationGradually();
618        }
619      });
620
621      // Set up Intersection Observer
622      setupIntersectionObserver();
623    }
624
625    // Play animation with gradual speed increase
626    function playAnimationGradually() {
627      if (!aizaAnimation) return;
628
629      // Start at low speed and gradually increase
630      aizaAnimation.setSpeed(0.1);
631      aizaAnimation.play();
632      isPlaying = true;
633
634      // Gradually increase speed
635      let speed = 0.1;
636      const interval = setInterval(function () {
637        speed += 0.1;
638        aizaAnimation.setSpeed(speed);
639
640        if (speed >= 1) {
641          clearInterval(interval);
642          aizaAnimation.setSpeed(1);
643        }
644      }, 50);
645    }
646
647    // Pause animation with gradual slowdown
648    function pauseAnimationGradually() {
649      if (!aizaAnimation) return;
650
651      // Gradually decrease speed
652      let speed = 1;
653      const interval = setInterval(function () {
654        speed -= 0.1;
655        aizaAnimation.setSpeed(speed);
656
657        if (speed <= 0.1) {
658          clearInterval(interval);
659          aizaAnimation.pause();
660          isPlaying = false;
661        }
662      }, 50);
663    }
664
665    // Check if element is in viewport
666    function checkIfInViewport() {
667      const container = document.getElementById("aiza-lottie-container");
668      if (!container) return;
669
670      const rect = container.getBoundingClientRect();
671      const isInViewport =
672        rect.top >= 0 &&
673        rect.left >= 0 &&
674        rect.bottom <=
675          (window.innerHeight || document.documentElement.clientHeight) &&
676        rect.right <=
677          (window.innerWidth || document.documentElement.clientWidth);
678
679      if (isInViewport && !hasPlayedOnce && !initialPlayInProgress) {
680        console.log("Animation in viewport - starting initial play");
681        playInitialAnimation();
682      }
683    }
684
685    // Play animation for initial 11 seconds when first in viewport
686    function playInitialAnimation() {
687      if (!aizaAnimation) return;
688
689      // Set flag that initial play is in progress
690      initialPlayInProgress = true;
691
692      // Play the animation
693      playAnimationGradually();
694
695      // Set timeout to stop after 11 seconds
696      clearTimeout(animationTimeout);
697      animationTimeout = setTimeout(function () {
698        console.log("Initial 11 seconds complete - pausing");
699        pauseAnimationGradually();
700
701        // Mark that initial play has completed and is no longer in progress
702        hasPlayedOnce = true;
703        initialPlayInProgress = false;
704      }, animationDuration);
705    }
706
707    // Use Intersection Observer for better performance
708    function setupIntersectionObserver() {
709      if ("IntersectionObserver" in window) {
710        const options = {
711          root: null,
712          rootMargin: "0px",
713          threshold: 0.5, // 50% of element must be visible
714        };
715
716        const observer = new IntersectionObserver((entries) => {
717          entries.forEach((entry) => {
718            if (
719              entry.isIntersecting &&
720              !hasPlayedOnce &&
721              !initialPlayInProgress
722            ) {
723              console.log("Observer detected animation in viewport");
724              playInitialAnimation();
725            }
726          });
727        }, options);
728
729        const container = document.getElementById("aiza-lottie-container");
730        if (container) {
731          observer.observe(container);
732        }
733      } else {
734        // Fallback to scroll event
735        window.addEventListener("scroll", debounce(checkIfInViewport, 100));
736        window.addEventListener("resize", debounce(checkIfInViewport, 100));
737      }
738    }
739
740    // Debounce function to limit how often a function is called
741    function debounce(func, wait) {
742      let timeout;
743      return function () {
744        const context = this;
745        const args = arguments;
746        clearTimeout(timeout);
747        timeout = setTimeout(function () {
748          func.apply(context, args);
749        }, wait);
750      };
751    }
752
753    // Initialize animation
754    initAnimation();
755  });
756})();
757
758// Black Card Scan Script
759(function () {
760  document.addEventListener("DOMContentLoaded", function () {
761    const input = document.querySelector(".black-card-scan .sca-input");
762    const button = document.querySelector(".black-card-scan .sca-button");
763    const helperContainer = document.querySelector(".helper-text-container");
764
765    if (!input || !button) return;
766
767    // Add animation class to button after a short delay
768    setTimeout(() => {
769      button.classList.add("animate");
770    }, 500);
771
772    function isValidUrl(domain) {
773      const originalDomain = domain;
774      const hasWww = originalDomain.match(/^(https?:\/\/)?www\./);
775      domain = domain.replace(/^(https?:\/\/)?(www\.)?/, "");
776      const urlWithProtocol = hasWww
777        ? "https://www." + domain
778        : "https://" + domain;
779      try {
780        const url = new URL(urlWithProtocol);
781        const cleanUrl =
782          url.protocol + "//" + url.hostname + (url.port ? ":" + url.port : "");
783        return { valid: true, url: cleanUrl };
784      } catch (error) {
785        return { valid: false, error: "Invalid domain format." };
786      }
787    }
788
789    function showError(message) {
790      if (input) input.style.borderColor = "#ff3b30";
791      if (helperContainer) {
792        helperContainer.style.visibility = "visible";
793        helperContainer.innerText = message
794          ? message
795          : "Valid formats: example.com, subdomain.example.com";
796      }
797    }
798
799    function clearError() {
800      if (input) input.style.borderColor = "";
801      if (helperContainer) helperContainer.style.visibility = "hidden";
802    }
803
804    function handleDomainInput() {
805      const domain = input.value.trim();
806      if (!domain) {
807        showError();
808        return;
809      }
810      const { valid, error, url } = isValidUrl(domain);
811      if (valid) {
812        clearError();
813        const currentWhitelist = new URLSearchParams(
814          window.location.search
815        ).get("whitelist");
816        const scanPageExt =
817          document.body.getAttribute("data-scan-page-ext") || "";
818        window.location.href = `/accessibility-checker/scan${scanPageExt}?domain=${encodeURIComponent(url)}&tnv_source=${encodeURIComponent(window.location.pathname)}${currentWhitelist ? `&whitelist=${encodeURIComponent(currentWhitelist)}` : ""}`;
819      } else {
820        showError(error);
821      }
822    }
823
824    input.addEventListener("input", clearError);
825    button.addEventListener("click", handleDomainInput);
826    input.addEventListener("keypress", (e) => {
827      if (e.key === "Enter") {
828        handleDomainInput();
829      }
830    });
831    window.addEventListener("resize", function () {
832      if (helperContainer && helperContainer.style.visibility === "visible") {
833        showError(helperContainer.innerText);
834      }
835    });
836  });
837})();
838
839// Scan CTA Script
840(function () {
841  // Trigger animation on page load
842  window.addEventListener("load", function () {
843    const container = document.querySelector(".scancta-input-container");
844    if (container) {
845      setTimeout(() => {
846        container.classList.add("scancta-animated");
847      }, 800);
848    }
849  });
850
851  // Smart domain validation function
852  function scanctaIsValidDomain(domain) {
853    domain = domain.replace(/^https?:\/\//, "").replace(/^www\./, "");
854
855    // Extract just the domain part (before any path)
856    domain = domain.split("/")[0];
857
858    // Remove trailing slash (now redundant but kept for safety)
859    domain = domain.replace(/\/$/, "");
860
861    const domainRegex =
862      /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
863    return domainRegex.test(domain);
864  }
865
866  function scanctaShowError(message) {
867    const errorDiv = document.getElementById("scanctaErrorMessage");
868    const input = document.querySelector(".scancta-input-field");
869    if (errorDiv) errorDiv.textContent = message;
870    if (errorDiv) errorDiv.classList.add("show");
871    if (input) input.classList.add("error");
872    setTimeout(() => {
873      scanctaHideError();
874    }, 5000);
875  }
876
877  function scanctaHideError() {
878    const errorDiv = document.getElementById("scanctaErrorMessage");
879    const input = document.querySelector(".scancta-input-field");
880    if (errorDiv) errorDiv.classList.remove("show");
881    if (input) input.classList.remove("error");
882  }
883
884  function scanctaStartScan() {
885    const button = document.querySelector(".scancta-button");
886    const input = document.querySelector(".scancta-input-field");
887    const container = document.querySelector(".scancta-input-container");
888    const url = input ? input.value.trim() : "";
889
890    scanctaHideError();
891
892    if (!url) {
893      scanctaShowError("Please enter a website URL");
894      if (input) input.focus();
895      return;
896    }
897
898    if (!scanctaIsValidDomain(url)) {
899      scanctaShowError(
900        "Please enter a valid domain (e.g., example.com, sub.example.co.uk)"
901      );
902      if (input) input.focus();
903      return;
904    }
905
906    // Clear input field before redirecting to prevent browser back button cache issues
907    if (input) {
908      input.value = "";
909      input.classList.remove("has-value");
910    }
911    if (container) container.classList.add("scancta-slide-back");
912    if (button) {
913      button.classList.add("loading");
914      button.textContent = "Scanning";
915    }
916
917    setTimeout(() => {
918      // Clean the domain: remove protocol, www, trailing slashes, params, hash, and file extensions
919      let domain = url
920        .replace(/^https?:\/\//, "") // Remove protocol
921        .replace(/^www\./, "") // Remove www
922        .split("?")[0] // Remove URL parameters
923        .split("#")[0] // Remove hash fragments
924        .replace(/\/+$/, "") // Remove trailing slashes (MOVED BEFORE file extension removal)
925        .replace(/\.(html|php|htm|asp|aspx|jsp|xml|json)$/i, "") // Remove file extensions
926        .replace(/\/\/+/g, "/"); // Replace multiple slashes with single slash
927
928      const currentWhitelist = new URLSearchParams(window.location.search).get(
929        "whitelist"
930      );
931      const scanPageExt =
932        document.body.getAttribute("data-scan-page-ext") || "";
933      let scanUrl = `/accessibility-checker/scan${scanPageExt}?domain=${encodeURIComponent("https://" + domain)}&tnv_source=${encodeURIComponent(window.location.pathname)}`;
934
935      if (currentWhitelist) {
936        scanUrl += `&whitelist=${currentWhitelist}`;
937      }
938
939      window.location.href = scanUrl;
940    }, 2000);
941  }
942
943  // Admin scan function for screen recording
944  function adminScanctaStartScan() {
945    const button = document.querySelector(".scancta-button");
946    const input = document.querySelector(".scancta-input-field");
947    const container = document.querySelector(".scancta-input-container");
948    const url = input ? input.value.trim() : "";
949
950    scanctaHideError();
951
952    if (!url) {
953      scanctaShowError("Please enter a website URL");
954      if (input) input.focus();
955      return;
956    }
957
958    if (!scanctaIsValidDomain(url)) {
959      scanctaShowError(
960        "Please enter a valid domain (e.g., example.com, sub.example.co.uk)"
961      );
962      if (input) input.focus();
963      return;
964    }
965
966    // Clear input field before redirecting to prevent browser back button cache issues
967    if (input) {
968      input.value = "";
969      input.classList.remove("has-value");
970    }
971    if (container) container.classList.add("scancta-slide-back");
972    if (button) {
973      button.classList.add("loading");
974      button.textContent = "Scanning";
975    }
976
977    setTimeout(() => {
978      // Clean the domain: remove protocol, www, trailing slashes, params, hash, and file extensions
979      let domain = url
980        .replace(/^https?:\/\//, "") // Remove protocol
981        .replace(/^www\./, "") // Remove www
982        .split("?")[0] // Remove URL parameters
983        .split("#")[0] // Remove hash fragments
984        .replace(/\/+$/, "") // Remove trailing slashes (MOVED BEFORE file extension removal)
985        .replace(/\.(html|php|htm|asp|aspx|jsp|xml|json)$/i, "") // Remove file extensions
986        .replace(/\/\/+/g, "/"); // Replace multiple slashes with single slash
987
988      const currentWhitelist = new URLSearchParams(window.location.search).get(
989        "whitelist"
990      );
991      const scanPageExt =
992        document.body.getAttribute("data-scan-page-ext") || "";
993      let scanUrl = `/accessibility-checker/scan${scanPageExt}?domain=${encodeURIComponent("https://" + domain)}&tnv_source=${encodeURIComponent(window.location.pathname)}&tnvp=true&tnv-bh=true&adminScan=true`;
994
995      if (currentWhitelist) {
996        scanUrl += `&whitelist=${currentWhitelist}`;
997      }
998
999      window.location.href = scanUrl;
1000    }, 2000);
1001  }
1002
1003  // Make adminScanctaStartScan available globally for onclick handlers
1004  window.adminScanctaStartScan = adminScanctaStartScan;
1005
1006  // Form submission handler
1007  document.addEventListener("DOMContentLoaded", function () {
1008    const form = document.querySelector(".scancta-wrapper");
1009    if (form) {
1010      form.addEventListener("submit", function (e) {
1011        e.preventDefault();
1012        scanctaStartScan();
1013      });
1014    }
1015
1016    // Admin mode toggle functionality
1017    let adminMode = false;
1018
1019    // Keyboard shortcut to toggle admin mode (Ctrl+Shift+A)
1020    document.addEventListener("keydown", function (e) {
1021      if (e.ctrlKey && e.shiftKey && e.key === "A") {
1022        e.preventDefault();
1023        adminMode = !adminMode;
1024        toggleAdminButtons();
1025        console.log("Admin mode:", adminMode ? "enabled" : "disabled");
1026      }
1027    });
1028
1029    // Function to toggle admin buttons visibility
1030    function toggleAdminButtons() {
1031      const adminButtons = document.querySelectorAll(".admin-scan-btn");
1032      const originalButtons = document.querySelectorAll(
1033        ".scancta-button:not(.admin-scan-btn):not(.footer-cta-section .scancta-button)"
1034      );
1035
1036      adminButtons.forEach((button) => {
1037        button.style.display = adminMode ? "block" : "none";
1038      });
1039
1040      originalButtons.forEach((button) => {
1041        if (!button.closest(".footer-cta-section")) {
1042          button.style.display = adminMode ? "none" : "block";
1043        }
1044      });
1045    }
1046
1047    // Input handlers
1048    const scanctaInput = document.querySelector(".scancta-input-field");
1049    if (scanctaInput) {
1050      // Add placeholder attribute for proper CSS behavior
1051      scanctaInput.setAttribute("placeholder", " ");
1052
1053      // Handle input changes to manage the has-value class
1054      scanctaInput.addEventListener("input", function () {
1055        scanctaHideError();
1056        if (this.value.trim()) {
1057          this.classList.add("has-value");
1058        } else {
1059          this.classList.remove("has-value");
1060        }
1061      });
1062
1063      // Handle focus styles
1064      scanctaInput.addEventListener("focus", function (e) {
1065        if (e.target.matches(":focus-visible")) {
1066          this.style.boxShadow =
1067            "0 8px 25px rgba(0, 0, 0, 0.15), 0 0 0 3px rgba(139, 92, 246, 0.3)";
1068        }
1069      });
1070
1071      scanctaInput.addEventListener("blur", function () {
1072        // Reset box shadow but maintain border if has value
1073        if (this.classList.contains("has-value")) {
1074          this.style.boxShadow = "0 8px 25px rgba(0, 0, 0, 0.15)";
1075        } else {
1076          this.style.boxShadow = "0 8px 25px rgba(0, 0, 0, 0.15)";
1077        }
1078        // Keep has-value class if input has content
1079        if (this.value.trim()) {
1080          this.classList.add("has-value");
1081        } else {
1082          this.classList.remove("has-value");
1083        }
1084      });
1085    }
1086  });
1087})();
1088
1089// Features Slideshow Script
1090(function () {
1091  let slideIndex = 1;
1092  let slideInterval;
1093  let progressTimeout = null;
1094  let isPlaying = true;
1095  let isTransitioning = false;
1096  const SLIDE_DURATION = 20000;
1097
1098  function showSlide(n) {
1099    if (isTransitioning) return;
1100    isTransitioning = true;
1101
1102    const slides = document.querySelectorAll(".slide");
1103    if (n > slides.length) slideIndex = 1;
1104    else if (n < 1) slideIndex = slides.length;
1105    else slideIndex = n;
1106
1107    // Find current active slide
1108    const currentActiveSlide = document.querySelector(".slide.active");
1109    const newSlide = slides[slideIndex - 1];
1110
1111    // If same slide, just return
1112    if (currentActiveSlide === newSlide) {
1113      isTransitioning = false;
1114      return;
1115    }
1116
1117    // Reset all progress bars first
1118    slides.forEach((slide) => {
1119      const bar = slide.querySelector(".slideshow-progress-bar");
1120      if (bar) {
1121        bar.querySelectorAll(".progress-segment").forEach((seg) => {
1122          seg.classList.remove("active", "completed", "loading");
1123        });
1124      }
1125    });
1126
1127    // If this is the initial load (no active slide), handle it differently
1128    if (!currentActiveSlide) {
1129      // Initial slide setup
1130      newSlide.style.display = "flex";
1131      setTimeout(() => {
1132        // Hide all other slides
1133        slides.forEach((slide) => {
1134          if (slide !== newSlide) {
1135            slide.style.display = "none";
1136            slide.classList.remove("active");
1137          }
1138        });
1139
1140        // Activate the first slide
1141        newSlide.classList.add("active");
1142        updateProgressBar(slideIndex);
1143        // Only start progress bar animation if playing
1144        if (isPlaying) {
1145          animateProgressBar(slideIndex);
1146        }
1147
1148        setTimeout(() => {
1149          isTransitioning = false;
1150        }, 100);
1151      }, 50);
1152      return;
1153    }
1154
1155    // Fade out current slide first
1156    currentActiveSlide.classList.remove("active");
1157
1158    // Prepare new slide (make it visible but transparent)
1159    newSlide.style.display = "flex";
1160
1161    // Small delay to ensure display change is processed
1162    setTimeout(() => {
1163      // Hide all other slides
1164      slides.forEach((slide) => {
1165        if (slide !== newSlide) {
1166          slide.style.display = "none";
1167          slide.classList.remove("active");
1168        }
1169      });
1170
1171      // Fade in new slide
1172      setTimeout(() => {
1173        newSlide.classList.add("active");
1174        // Update and animate progress bar for this slide
1175        updateProgressBar(slideIndex);
1176        // Only start progress bar animation if playing
1177        if (isPlaying) {
1178          animateProgressBar(slideIndex);
1179        }
1180
1181        // Allow new transitions after animation completes
1182        setTimeout(() => {
1183          isTransitioning = false;
1184        }, 800); // Match the CSS transition duration
1185      }, 50);
1186    }, 50);
1187  }
1188
1189  function updateProgressBar(slideIdx) {
1190    // Only update the progress bar in the active slide
1191    const slides = document.querySelectorAll(".slide");
1192    const bar = slides[slideIdx - 1].querySelector(".slideshow-progress-bar");
1193    if (!bar) return;
1194    bar.querySelectorAll(".progress-segment").forEach((segment, idx) => {
1195      segment.classList.remove("active", "completed", "loading");
1196      if (idx + 1 === slideIdx) {
1197        segment.classList.add("active");
1198      } else if (idx + 1 < slideIdx) {
1199        segment.classList.add("completed");
1200      }
1201    });
1202  }
1203
1204  function animateProgressBar(slideIdx) {
1205    clearTimeout(progressTimeout);
1206    // Only animate if playing
1207    if (!isPlaying) return;
1208
1209    // Only animate the progress bar in the active slide
1210    const slides = document.querySelectorAll(".slide");
1211    const bar = slides[slideIdx - 1].querySelector(".slideshow-progress-bar");
1212    if (!bar) return;
1213    const segments = bar.querySelectorAll(".progress-segment");
1214    segments.forEach((segment, idx) => {
1215      segment.classList.remove("loading");
1216      if (idx + 1 === slideIdx) {
1217        // Force a reflow to ensure the animation starts
1218        // This prevents the browser from batching the remove and add operations
1219        void segment.offsetWidth;
1220
1221        // Animate the active segment
1222        setTimeout(() => {
1223          // Double-check if still playing before starting animation
1224          if (isPlaying) {
1225            segment.classList.add("loading");
1226            // Listen for animation end to auto-advance
1227            segment.addEventListener("animationend", handleProgressComplete, {
1228              once: true,
1229            });
1230          }
1231        }, 50);
1232      }
1233    });
1234  }
1235
1236  function handleProgressComplete() {
1237    if (isPlaying) {
1238      // Small delay before transitioning to next slide
1239      setTimeout(() => {
1240        nextSlide();
1241      }, 200);
1242    }
1243  }
1244
1245  // Function to handle arrow clicks
1246  function changeSlide(direction) {
1247    if (direction === 1) {
1248      nextSlide();
1249    } else if (direction === -1) {
1250      previousSlide();
1251    }
1252  }
1253
1254  function nextSlide() {
1255    showSlide(slideIndex + 1);
1256    resetInterval();
1257  }
1258
1259  function previousSlide() {
1260    showSlide(slideIndex - 1);
1261    resetInterval();
1262  }
1263
1264  function resetInterval() {
1265    clearInterval(slideInterval);
1266    clearTimeout(progressTimeout);
1267    if (isPlaying) {
1268      animateProgressBar(slideIndex);
1269    }
1270  }
1271
1272  function togglePlayPause() {
1273    const playIcons = document.querySelectorAll(".play-icon");
1274    const pauseIcons = document.querySelectorAll(".pause-icon");
1275    isPlaying = !isPlaying;
1276
1277    if (isPlaying) {
1278      // Resume playing
1279      playIcons.forEach((icon) => (icon.style.display = "none"));
1280      pauseIcons.forEach((icon) => (icon.style.display = "block"));
1281      // Restart progress bar animation from the beginning
1282      updateProgressBar(slideIndex);
1283      animateProgressBar(slideIndex);
1284    } else {
1285      // Pause playing
1286      playIcons.forEach((icon) => (icon.style.display = "block"));
1287      pauseIcons.forEach((icon) => (icon.style.display = "none"));
1288      clearTimeout(progressTimeout);
1289
1290      // Stop progress bar animation by removing loading class but keep active state
1291      const slides = document.querySelectorAll(".slide");
1292      const bar = slides[slideIndex - 1].querySelector(
1293        ".slideshow-progress-bar"
1294      );
1295      if (bar) {
1296        const activeSegment = bar.querySelector(".progress-segment.active");
1297        if (activeSegment) {
1298          activeSegment.classList.remove("loading");
1299          // Remove any pending animation event listeners
1300          activeSegment.removeEventListener(
1301            "animationend",
1302            handleProgressComplete
1303          );
1304          // Ensure the active segment shows its color when paused
1305          // The CSS rule .progress-segment.active:not(.loading)::before will handle this
1306        }
1307      }
1308    }
1309  }
1310
1311  // Make functions available globally for onclick handlers
1312  window.changeSlide = changeSlide;
1313  window.togglePlayPause = togglePlayPause;
1314
1315  // Progress bar click/keyboard navigation
1316  function setupProgressBarNavigation() {
1317    document.querySelectorAll(".slide").forEach((slide, sidx) => {
1318      const bar = slide.querySelector(".slideshow-progress-bar");
1319      if (!bar) return;
1320      bar.querySelectorAll(".progress-segment").forEach((segment) => {
1321        segment.addEventListener("click", () => {
1322          const slideNum = parseInt(segment.dataset.slide, 10);
1323          showSlide(slideNum);
1324          resetInterval();
1325        });
1326        segment.addEventListener("keydown", (e) => {
1327          if (e.key === "Enter" || e.key === " ") {
1328            e.preventDefault();
1329            const slideNum = parseInt(segment.dataset.slide, 10);
1330            showSlide(slideNum);
1331            resetInterval();
1332          }
1333        });
1334      });
1335    });
1336  }
1337
1338  document.addEventListener("DOMContentLoaded", function () {
1339    // Intersection Observer for viewport detection
1340    const slideshowSection = document.querySelector(".features-slideshow");
1341    let slideshowStarted = false;
1342    let wasPlayingBeforePause = false;
1343
1344    if (!slideshowSection) return;
1345
1346    const observer = new IntersectionObserver(
1347      (entries) => {
1348        entries.forEach((entry) => {
1349          if (entry.isIntersecting) {
1350            // Slideshow is in viewport
1351            if (!slideshowStarted) {
1352              slideshowStarted = true;
1353              // Add fade-in class
1354              slideshowSection.classList.add("in-viewport");
1355
1356              // Start slideshow after fade-in completes
1357              setTimeout(() => {
1358                showSlide(1);
1359                // Ensure autoplay starts for the first slide only if playing
1360                if (isPlaying) {
1361                  const firstSlide = document.querySelector(".slide.active");
1362                  if (firstSlide) {
1363                    updateProgressBar(1);
1364                    animateProgressBar(1);
1365                  }
1366                }
1367              }, 200); // Wait 300ms for fade-in to start
1368            } else {
1369              // Slideshow was already started, resume if it was playing before
1370              if (wasPlayingBeforePause) {
1371                isPlaying = true;
1372                wasPlayingBeforePause = false;
1373                // Resume progress bar animation for current slide
1374                const currentSlide = document.querySelector(".slide.active");
1375                if (currentSlide) {
1376                  animateProgressBar(slideIndex);
1377                }
1378              }
1379            }
1380          } else {
1381            // Slideshow is out of viewport
1382            if (slideshowStarted) {
1383              // Pause the slideshow
1384              if (isPlaying) {
1385                wasPlayingBeforePause = true;
1386                isPlaying = false;
1387                // Stop progress bar animation
1388                clearTimeout(progressTimeout);
1389                const currentSlide = document.querySelector(".slide.active");
1390                if (currentSlide) {
1391                  const bar = currentSlide.querySelector(
1392                    ".slideshow-progress-bar"
1393                  );
1394                  if (bar) {
1395                    bar.querySelectorAll(".progress-segment").forEach((seg) => {
1396                      seg.classList.remove("loading");
1397                    });
1398                  }
1399                }
1400              }
1401            }
1402          }
1403        });
1404      },
1405      {
1406        threshold: 0.3, // Trigger when 30% of the element is visible
1407        rootMargin: "0px 0px 100px 0px", // Trigger a bit before the element is fully visible
1408      }
1409    );
1410
1411    observer.observe(slideshowSection);
1412
1413    setupProgressBarNavigation();
1414    // Keyboard navigation
1415    document.addEventListener("keydown", (e) => {
1416      if (e.key === "ArrowLeft") previousSlide();
1417      if (e.key === "ArrowRight") nextSlide();
1418    });
1419    // Touch support for mobile
1420    let touchStartX = 0;
1421    let touchEndX = 0;
1422    const slideshowContainer = document.querySelector(".slideshow-container");
1423    if (slideshowContainer) {
1424      slideshowContainer.addEventListener("touchstart", (e) => {
1425        touchStartX = e.changedTouches[0].screenX;
1426      });
1427      slideshowContainer.addEventListener("touchend", (e) => {
1428        touchEndX = e.changedTouches[0].screenX;
1429        handleSwipe();
1430      });
1431    }
1432    function handleSwipe() {
1433      if (touchEndX < touchStartX - 50) nextSlide();
1434      if (touchEndX > touchStartX + 50) previousSlide();
1435    }
1436  });
1437})();
1438
1439// Home Guidelines Tabs Script
1440(function () {
1441  document.addEventListener("DOMContentLoaded", function () {
1442    const tabButtons = document.querySelectorAll(".home-guidelines-tab");
1443    const tabsWrapper = document.querySelector(".home-guidelines-tabs-wrapper");
1444    const guidelinesPanel = tabsWrapper
1445      ? tabsWrapper.closest(".home-guidelines-panel")
1446      : null;
1447    const tabsToggle = document.querySelector(".home-guidelines-tabs-toggle");
1448    const tabsCombobox = document.getElementById("homeguidelinestabscombobox");
1449    const tabsToggleLabel = document.querySelector(
1450      ".home-guidelines-tabs-toggle-label"
1451    );
1452    const tabsToggleFlag = document.querySelector(
1453      ".home-guidelines-tabs-toggle-flag"
1454    );
1455    const detailsRegion = document.querySelector(".home-guidelines-details");
1456    const mapImageEl = document.querySelector(".home-guidelines-map-image");
1457    const overlayImageEl = document.querySelector(".home-guidelines-map-overlay");
1458    const defaultMapImage = "/images/world-map.png";
1459    const contentSections = document.querySelectorAll(
1460      ".home-guidelines-content-section"
1461    );
1462    const overlayStyleProperties = [
1463      "top",
1464      "right",
1465      "bottom",
1466      "left",
1467      "width",
1468      "height",
1469      "max-width",
1470      "max-height",
1471      "transform",
1472    ];
1473
1474    let overlayFadeTimeoutId = null;
1475    const OVERLAY_FADE_DURATION_MS = 400;
1476
1477    // Map data for images and overlays only (not text content)
1478    const mapData = {
1479      wcag: {
1480        name: "WCAG 2.2",
1481        mapImage: "/images/world-map.png",
1482      },
1483      ada: {
1484        name: "ADA Titles II & III",
1485        mapImage: "/images/world-map-ADA.png",
1486        overlayImage: "/images/ADA.png",
1487        overlayStyles: {
1488          top: "40%",
1489          left: "41%",
1490          width: "52%",
1491          transform: "translate(-50%, -50%)",
1492        },
1493      },
1494      section508: {
1495        name: "Section 508",
1496        mapImage: "/images/world-map-ADA.png",
1497        overlayImage: "/images/section508.png",
1498        overlayStyles: {
1499          top: "40%",
1500          left: "41%",
1501          width: "52%",
1502          transform: "translate(-50%, -50%)",
1503        },
1504      },
1505      aoda: {
1506        name: "AODA",
1507        mapImage: "/images/world-map-AODA.png",
1508        overlayImage: "/images/AODA.png",
1509        overlayStyles: {
1510          top: "42%",
1511          left: "49%",
1512          width: "52%",
1513          transform: "translate(-50%, -50%)",
1514        },
1515      },
1516      eaa: {
1517        name: "European Accessibility Act",
1518        mapImage: "/images/world-map-EAA.png",
1519        overlayImage: "/images/EAA.png",
1520        overlayStyles: {
1521          top: "38%",
1522          left: "43%",
1523          width: "52%",
1524          transform: "translate(-50%, -50%)",
1525        },
1526      },
1527      bitv: {
1528        name: "BITV 2.0",
1529        mapImage: "/images/world-map-BITV.png",
1530        overlayImage: "/images/BITV.png",
1531        overlayStyles: {
1532          top: "40%",
1533          left: "49%",
1534          width: "52%",
1535          transform: "translate(-50%, -50%)",
1536        },
1537      },
1538      agid: {
1539        name: "AGID Guidelines",
1540        mapImage: "/images/world-map-AGID.png",
1541        overlayImage: "/images/AGID.png",
1542        overlayStyles: {
1543          top: "46%",
1544          left: "55%",
1545          width: "52%",
1546          transform: "translate(-50%, -50%)",
1547        },
1548      },
1549    };
1550
1551    function clearOverlayAnimations() {
1552      if (overlayFadeTimeoutId !== null) {
1553        window.clearTimeout(overlayFadeTimeoutId);
1554        overlayFadeTimeoutId = null;
1555      }
1556    }
1557
1558    function applyOverlayStyles(styles) {
1559      if (!overlayImageEl) {
1560        return;
1561      }
1562
1563      overlayStyleProperties.forEach(function (prop) {
1564        if (prop === "transform") {
1565          overlayImageEl.style.removeProperty("--overlay-transform");
1566          overlayImageEl.style.removeProperty("transform");
1567        } else {
1568          overlayImageEl.style.removeProperty(prop);
1569        }
1570      });
1571
1572      if (!styles) {
1573        return;
1574      }
1575
1576      Object.entries(styles).forEach(function ([prop, value]) {
1577        if (
1578          overlayStyleProperties.includes(prop) &&
1579          typeof value === "string" &&
1580          value.trim() !== ""
1581        ) {
1582          if (prop === "transform") {
1583            overlayImageEl.style.setProperty(
1584              "--overlay-transform",
1585              value.trim()
1586            );
1587          } else {
1588            overlayImageEl.style.setProperty(prop, value.trim());
1589          }
1590        }
1591      });
1592    }
1593
1594    if (!tabButtons.length || !contentSections.length) {
1595      return;
1596    }
1597
1598    const order = Array.prototype.slice.call(tabButtons);
1599    const DEFAULT_DROPDOWN_BREAKPOINT = 820;
1600    const dropdownBreakpointAttr = tabsWrapper
1601      ? parseInt(tabsWrapper.getAttribute("data-dropdown-breakpoint"), 10)
1602      : NaN;
1603    const dropdownBreakpoint = Number.isNaN(dropdownBreakpointAttr)
1604      ? DEFAULT_DROPDOWN_BREAKPOINT
1605      : dropdownBreakpointAttr;
1606    const dropdownMediaQuery =
1607      typeof window.matchMedia === "function"
1608        ? window.matchMedia(`(max-width: ${dropdownBreakpoint}px)`)
1609        : null;
1610
1611    function isDropdownModeEnabled() {
1612      return (
1613        Boolean(tabsWrapper) &&
1614        tabsWrapper.getAttribute("data-tabs-mode") === "dropdown"
1615      );
1616    }
1617
1618    const tabsList = document.querySelector(".home-guidelines-tabs-list");
1619
1620    function updateRolesForMode(dropdownEnabled) {
1621      if (!tabsList || !tabButtons.length) {
1622        return;
1623      }
1624
1625      if (dropdownEnabled) {
1626        // Combobox mode: update roles to combobox/listbox/option
1627        if (tabsToggle) {
1628          tabsToggle.setAttribute("role", "combobox");
1629          tabsToggle.setAttribute("aria-haspopup", "listbox");
1630        }
1631        if (tabsList) {
1632          tabsList.setAttribute("role", "listbox");
1633          // Hide list initially in dropdown mode
1634          if (tabsWrapper.getAttribute("data-expanded") !== "true") {
1635            tabsList.setAttribute("hidden", "true");
1636          }
1637        }
1638        tabButtons.forEach(function (button) {
1639          button.setAttribute("role", "option");
1640          // Keep original tab ID but use option ID for aria-activedescendant
1641          const optionId = button.getAttribute("data-option-id");
1642          if (optionId && button.id !== optionId) {
1643            // Store original ID if not already stored
1644            if (!button.getAttribute("data-original-id")) {
1645              button.setAttribute("data-original-id", button.id);
1646            }
1647            // Use option ID for combobox mode
1648            button.id = optionId;
1649          }
1650        });
1651      } else {
1652        // Tabs mode: update roles to tablist/tab
1653        // The combobox role is on tabsCombobox, no need to remove it from tabsToggle
1654        if (tabsList) {
1655          tabsList.removeAttribute("role");
1656          tabsList.removeAttribute("hidden");
1657        }
1658        tabButtons.forEach(function (button) {
1659          button.setAttribute("role", "tab");
1660          // Restore original tab ID
1661          const originalId = button.getAttribute("data-original-id");
1662          if (originalId) {
1663            button.id = originalId;
1664            button.removeAttribute("data-original-id");
1665          }
1666        });
1667      }
1668    }
1669
1670    function setTabsMode(dropdownEnabled) {
1671      if (!tabsWrapper) {
1672        return;
1673      }
1674
1675      tabsWrapper.setAttribute(
1676        "data-tabs-mode",
1677        dropdownEnabled ? "dropdown" : "tabs"
1678      );
1679      tabsWrapper.setAttribute("data-expanded", "false");
1680
1681      if (guidelinesPanel) {
1682        guidelinesPanel.setAttribute(
1683          "data-tabs-mode",
1684          dropdownEnabled ? "dropdown" : "tabs"
1685        );
1686      }
1687
1688      if (tabsToggle) {
1689        if (tabsCombobox) {
1690          tabsCombobox.setAttribute("aria-expanded", "false");
1691        }
1692
1693        if (dropdownEnabled) {
1694          tabsToggle.hidden = false;
1695          tabsToggle.removeAttribute("aria-hidden");
1696          tabsToggle.removeAttribute("tabindex");
1697        } else {
1698          tabsToggle.hidden = true;
1699          tabsToggle.setAttribute("aria-hidden", "true");
1700          tabsToggle.setAttribute("tabindex", "-1");
1701        }
1702      }
1703
1704      updateRolesForMode(dropdownEnabled);
1705    }
1706
1707    if (dropdownMediaQuery) {
1708      setTabsMode(dropdownMediaQuery.matches);
1709
1710      const handleTabsModeChange = function (event) {
1711        setTabsMode(event.matches);
1712      };
1713
1714      if (typeof dropdownMediaQuery.addEventListener === "function") {
1715        dropdownMediaQuery.addEventListener("change", handleTabsModeChange);
1716      } else if (typeof dropdownMediaQuery.addListener === "function") {
1717        dropdownMediaQuery.addListener(handleTabsModeChange);
1718      }
1719    } else {
1720      setTabsMode(false);
1721    }
1722
1723    function getNextIndex(currentIndex, offset) {
1724      let nextIndex = currentIndex + offset;
1725      if (nextIndex < 0) {
1726        nextIndex = order.length - 1;
1727      } else if (nextIndex >= order.length) {
1728        nextIndex = 0;
1729      }
1730      return nextIndex;
1731    }
1732
1733    function setDropdownLabel(targetButton) {
1734      if (!tabsToggleLabel || !targetButton) {
1735        return;
1736      }
1737
1738      const labelEl = targetButton.querySelector(".home-guidelines-tab-label");
1739      if (labelEl) {
1740        tabsToggleLabel.textContent = labelEl.textContent.trim();
1741      }
1742
1743      if (!tabsToggleFlag) {
1744        return;
1745      }
1746
1747      tabsToggleFlag.innerHTML = "";
1748
1749      const flagEl = targetButton.querySelector(".home-guidelines-tab-flag");
1750      const iconEl = targetButton.querySelector(".home-guidelines-tab-icon");
1751
1752      if (flagEl) {
1753        const flagClone = flagEl.cloneNode(true);
1754        tabsToggleFlag.appendChild(flagClone);
1755      } else if (iconEl) {
1756        const iconClone = iconEl.cloneNode(true);
1757        tabsToggleFlag.appendChild(iconClone);
1758      }
1759    }
1760
1761    function closeTabsDropdown() {
1762      if (!tabsWrapper || !tabsToggle || !isDropdownModeEnabled()) {
1763        return;
1764      }
1765
1766      tabsWrapper.setAttribute("data-expanded", "false");
1767      if (tabsCombobox) {
1768        tabsCombobox.setAttribute("aria-expanded", "false");
1769      }
1770      if (tabsList) {
1771        tabsList.setAttribute("hidden", "true");
1772      }
1773      
1774      // Reset tabindex when dropdown closes - only active option should be tabbable
1775      order.forEach(function (button) {
1776        const isActive = button.classList.contains("is-active");
1777        button.setAttribute("tabindex", isActive ? "0" : "-1");
1778      });
1779    }
1780
1781    function setActiveTab(target, shouldFocus) {
1782      if (!target) {
1783        return;
1784      }
1785
1786      const key = target.getAttribute("data-guideline");
1787      const mapInfo = mapData[key];
1788
1789      if (!mapInfo) {
1790        return;
1791      }
1792
1793      order.forEach(function (button) {
1794        const isActive = button === target;
1795        button.classList.toggle("is-active", isActive);
1796        button.setAttribute("aria-selected", isActive ? "true" : "false");
1797        button.setAttribute("tabindex", isActive ? "0" : "-1");
1798      });
1799
1800      // Update aria-activedescendant for combobox mode
1801      if (isDropdownModeEnabled() && tabsCombobox) {
1802        const optionId = target.getAttribute("data-option-id") || target.id;
1803        if (optionId) {
1804          tabsCombobox.setAttribute("aria-activedescendant", optionId);
1805        }
1806      }
1807
1808      if (shouldFocus) {
1809        target.focus();
1810      }
1811
1812      // Show/hide content sections instead of injecting text
1813      contentSections.forEach(function (section) {
1814        const sectionKey = section.getAttribute("data-guideline-content");
1815        if (sectionKey === key) {
1816          section.removeAttribute("hidden");
1817
1818          // Update aria-label on details region
1819          if (detailsRegion) {
1820            detailsRegion.setAttribute(
1821              "aria-label",
1822              mapInfo.name + " overview and updates"
1823            );
1824          }
1825
1826          // Update info row aria-label
1827          const infoRow = section.querySelector(".home-guidelines-info-row");
1828          if (infoRow) {
1829            infoRow.setAttribute(
1830              "aria-label",
1831              mapInfo.name + " overview and latest update"
1832            );
1833          }
1834        } else {
1835          section.setAttribute("hidden", "true");
1836        }
1837      });
1838
1839      // Update map image
1840      if (mapImageEl) {
1841        const nextSrc = mapInfo.mapImage || defaultMapImage;
1842        if (mapImageEl.getAttribute("src") !== nextSrc) {
1843          mapImageEl.setAttribute("src", nextSrc);
1844        }
1845        mapImageEl.setAttribute("data-guideline-map", key);
1846      }
1847
1848      // Handle overlay image
1849      if (overlayImageEl) {
1850        const overlaySrc = mapInfo.overlayImage || "";
1851        const shouldShowOverlay = Boolean(overlaySrc);
1852
1853        if (shouldShowOverlay) {
1854          const fadeInOverlay = function () {
1855            const needsNewSrc =
1856              overlayImageEl.getAttribute("src") !== overlaySrc;
1857
1858            const startFadeIn = function () {
1859              requestAnimationFrame(function () {
1860                overlayImageEl.classList.add("is-visible");
1861              });
1862            };
1863
1864            if (needsNewSrc) {
1865              const handleLoad = function () {
1866                overlayImageEl.removeEventListener("load", handleLoad);
1867                startFadeIn();
1868              };
1869
1870              overlayImageEl.addEventListener("load", handleLoad);
1871              overlayImageEl.setAttribute("src", overlaySrc);
1872
1873              if (overlayImageEl.complete && overlayImageEl.naturalWidth > 0) {
1874                overlayImageEl.removeEventListener("load", handleLoad);
1875                startFadeIn();
1876              }
1877            } else {
1878              startFadeIn();
1879            }
1880          };
1881
1882          const prepareOverlay = function () {
1883            applyOverlayStyles(mapInfo.overlayStyles || null);
1884            overlayImageEl.removeAttribute("hidden");
1885            overlayImageEl.setAttribute("data-guideline-overlay", key);
1886            fadeInOverlay();
1887          };
1888
1889          clearOverlayAnimations();
1890          overlayImageEl.classList.remove("is-visible");
1891
1892          if (overlayImageEl.getAttribute("data-guideline-overlay")) {
1893            overlayFadeTimeoutId = window.setTimeout(function () {
1894              overlayFadeTimeoutId = null;
1895              prepareOverlay();
1896            }, OVERLAY_FADE_DURATION_MS);
1897          } else {
1898            prepareOverlay();
1899          }
1900        } else {
1901          clearOverlayAnimations();
1902          overlayImageEl.classList.remove("is-visible");
1903          overlayImageEl.setAttribute("src", "");
1904          applyOverlayStyles(null);
1905          overlayImageEl.setAttribute("data-guideline-overlay", "");
1906          overlayImageEl.setAttribute("hidden", "true");
1907        }
1908      }
1909
1910      setDropdownLabel(target);
1911      closeTabsDropdown();
1912    }
1913
1914    let activeOptionIndex = 0;
1915
1916    function setActiveOptionIndex(index, scrollIntoView) {
1917      if (index < 0 || index >= order.length) {
1918        return;
1919      }
1920      activeOptionIndex = index;
1921      const activeOption = order[activeOptionIndex];
1922      order.forEach(function (button) {
1923        button.classList.remove("home-guidelines-tab--active-option");
1924      });
1925      activeOption.classList.add("home-guidelines-tab--active-option");
1926      if (tabsCombobox && isDropdownModeEnabled()) {
1927        const optionId = activeOption.getAttribute("data-option-id") || activeOption.id;
1928        if (optionId) {
1929          tabsCombobox.setAttribute("aria-activedescendant", optionId);
1930        }
1931      }
1932      if (scrollIntoView && activeOption) {
1933        activeOption.scrollIntoView({ block: "nearest" });
1934      }
1935    }
1936
1937    function moveActiveOption(step) {
1938      if (!order.length) {
1939        return;
1940      }
1941      let nextIndex = activeOptionIndex + step;
1942      if (nextIndex < 0) {
1943        nextIndex = 0;
1944      } else if (nextIndex >= order.length) {
1945        nextIndex = order.length - 1;
1946      }
1947      setActiveOptionIndex(nextIndex, true);
1948    }
1949
1950    order.forEach(function (button, index) {
1951      button.addEventListener("click", function (event) {
1952        event.preventDefault();
1953        setActiveTab(button, true);
1954      });
1955
1956      // Update active option when focused via Tab
1957      button.addEventListener("focus", function () {
1958        if (isDropdownModeEnabled() && tabsWrapper && tabsWrapper.getAttribute("data-expanded") === "true") {
1959          activeOptionIndex = index;
1960          setActiveOptionIndex(index, false);
1961        }
1962      });
1963
1964      button.addEventListener("keydown", function (event) {
1965        const key = event.key;
1966        const isDropdown = isDropdownModeEnabled();
1967        const isExpanded = tabsWrapper && tabsWrapper.getAttribute("data-expanded") === "true";
1968
1969        if (isDropdown && isExpanded) {
1970          // Combobox mode keyboard navigation (when dropdown is open)
1971          if (key === "ArrowDown" || key === "Down") {
1972            event.preventDefault();
1973            moveActiveOption(1);
1974            order[activeOptionIndex].focus();
1975          } else if (key === "ArrowUp" || key === "Up") {
1976            event.preventDefault();
1977            moveActiveOption(-1);
1978            order[activeOptionIndex].focus();
1979          } else if (key === "Home") {
1980            event.preventDefault();
1981            setActiveOptionIndex(0, true);
1982            order[0].focus();
1983          } else if (key === "End") {
1984            event.preventDefault();
1985            setActiveOptionIndex(order.length - 1, true);
1986            order[order.length - 1].focus();
1987          } else if (key === "Enter" || key === " ") {
1988            event.preventDefault();
1989            setActiveTab(button, true);
1990          } else if (key === "Escape" || key === "Esc") {
1991            event.preventDefault();
1992            closeTabsDropdown();
1993            if (tabsToggle) {
1994              if (tabsCombobox) {
1995              tabsCombobox.focus();
1996            }
1997            }
1998          }
1999          // Tab key should work naturally - don't prevent default
2000        } else if (isDropdown && !isExpanded) {
2001          // If dropdown is closed but we're in dropdown mode, allow opening with Enter/Space
2002          if (key === "Enter" || key === " ") {
2003            event.preventDefault();
2004            openTabsDropdown();
2005          }
2006        } else {
2007          // Tabs mode keyboard navigation
2008          if (key === "ArrowRight" || key === "Right") {
2009            event.preventDefault();
2010            const next = getNextIndex(index, 1);
2011            setActiveTab(order[next], true);
2012          } else if (key === "ArrowLeft" || key === "Left") {
2013            event.preventDefault();
2014            const prev = getNextIndex(index, -1);
2015            setActiveTab(order[prev], true);
2016          } else if (key === "Home") {
2017            event.preventDefault();
2018            setActiveTab(order[0], true);
2019          } else if (key === "End") {
2020            event.preventDefault();
2021            setActiveTab(order[order.length - 1], true);
2022          } else if (key === " " || key === "Spacebar" || key === "Enter") {
2023            event.preventDefault();
2024            setActiveTab(button, true);
2025          }
2026        }
2027      });
2028    });
2029
2030    const defaultActive =
2031      document.querySelector(".home-guidelines-tab.is-active") || order[0];
2032    setActiveTab(defaultActive, false);
2033    
2034    // Initialize active option index
2035    const defaultIndex = order.indexOf(defaultActive);
2036    if (defaultIndex >= 0) {
2037      activeOptionIndex = defaultIndex;
2038      if (isDropdownModeEnabled()) {
2039        setActiveOptionIndex(defaultIndex, false);
2040      }
2041    }
2042
2043    function openTabsDropdown() {
2044      if (!tabsWrapper || !tabsToggle || !isDropdownModeEnabled()) {
2045        return;
2046      }
2047      tabsWrapper.setAttribute("data-expanded", "true");
2048      if (tabsCombobox) {
2049        tabsCombobox.setAttribute("aria-expanded", "true");
2050      }
2051      if (tabsList) {
2052        tabsList.removeAttribute("hidden");
2053      }
2054      
2055      // Make all options tabbable when dropdown opens
2056      order.forEach(function (button) {
2057        button.setAttribute("tabindex", "0");
2058      });
2059      
2060      const activeTab = document.querySelector(".home-guidelines-tab.is-active") || order[0];
2061      if (activeTab) {
2062        const activeIndex = order.indexOf(activeTab);
2063        if (activeIndex >= 0) {
2064          activeOptionIndex = activeIndex;
2065          setActiveOptionIndex(activeIndex, true);
2066          activeTab.focus();
2067        }
2068      }
2069    }
2070
2071    if (tabsToggle && tabsWrapper) {
2072      tabsToggle.addEventListener("click", function () {
2073        if (!isDropdownModeEnabled()) {
2074          return;
2075        }
2076
2077        const expanded = tabsCombobox ? tabsCombobox.getAttribute("aria-expanded") === "true" : false;
2078        if (expanded) {
2079          closeTabsDropdown();
2080        } else {
2081          openTabsDropdown();
2082        }
2083      });
2084
2085      if (tabsCombobox) {
2086        tabsCombobox.addEventListener("keydown", function (event) {
2087          if (!isDropdownModeEnabled()) {
2088            return;
2089          }
2090
2091          const key = event.key;
2092          const isExpanded = tabsCombobox.getAttribute("aria-expanded") === "true";
2093
2094        if (key === "Enter" || key === " ") {
2095          event.preventDefault();
2096          if (isExpanded) {
2097            setActiveTab(order[activeOptionIndex], true);
2098          } else {
2099            openTabsDropdown();
2100          }
2101          return;
2102        }
2103
2104        if (key === "Escape" || key === "Esc") {
2105          if (isExpanded) {
2106            event.preventDefault();
2107            closeTabsDropdown();
2108            if (tabsCombobox) {
2109              tabsCombobox.focus();
2110            }
2111          }
2112          return;
2113        }
2114
2115        if (key === "ArrowDown" || key === "Down") {
2116          event.preventDefault();
2117          if (!isExpanded) {
2118            openTabsDropdown();
2119          }
2120          moveActiveOption(1);
2121          return;
2122        }
2123
2124        if (key === "ArrowUp" || key === "Up") {
2125          event.preventDefault();
2126          if (!isExpanded) {
2127            openTabsDropdown();
2128          }
2129          moveActiveOption(-1);
2130          return;
2131        }
2132
2133        if (key === "Home") {
2134          event.preventDefault();
2135          if (!isExpanded) {
2136            openTabsDropdown();
2137          }
2138          setActiveOptionIndex(0, true);
2139          return;
2140        }
2141
2142        if (key === "End") {
2143          event.preventDefault();
2144          if (!isExpanded) {
2145            openTabsDropdown();
2146          }
2147          setActiveOptionIndex(order.length - 1, true);
2148          return;
2149        }
2150
2151        // Tab key should work naturally - don't prevent default
2152        // When tabbing away from toggle, dropdown will close via blur/click handlers
2153        });
2154      }
2155
2156      document.addEventListener("click", function (event) {
2157        if (!isDropdownModeEnabled() || !tabsWrapper.contains(event.target)) {
2158          closeTabsDropdown();
2159        }
2160      });
2161
2162      // Close dropdown when focus leaves the component
2163      tabsWrapper.addEventListener("focusout", function (event) {
2164        if (!isDropdownModeEnabled()) {
2165          return;
2166        }
2167        // Check if focus is moving to another element within the component
2168        setTimeout(function () {
2169          if (!tabsWrapper.contains(document.activeElement)) {
2170            closeTabsDropdown();
2171          }
2172        }, 0);
2173      });
2174    }
2175
2176    document.addEventListener("keydown", function (event) {
2177      if (!isDropdownModeEnabled()) {
2178        return;
2179      }
2180
2181      const solutionsDropdownCombobox = document.getElementById("solutionsDropdownCombobox");
2182      if (solutionsDropdownCombobox && (solutionsDropdownCombobox.contains(event.target) || solutionsDropdownCombobox === event.target)) {
2183        return;
2184      }
2185
2186      if (event.key === "Escape" || event.key === "Esc") {
2187        closeTabsDropdown();
2188        if (tabsCombobox) {
2189          tabsCombobox.focus();
2190        }
2191      }
2192    });
2193  });
2194})();
2195
2196// Accessibility image carousel
2197(function () {
2198  document.addEventListener("DOMContentLoaded", () => {
2199    const carousel = document.querySelector(".image-carousel");
2200    if (!carousel) {
2201      return;
2202    }
2203
2204    const viewport = carousel.querySelector(".carousel-viewport");
2205    const prevButton = carousel.querySelector('.carousel-nav[data-direction="prev"]');
2206    const nextButton = carousel.querySelector('.carousel-nav[data-direction="next"]');
2207    const cards = Array.from(carousel.querySelectorAll(".carousel-card"));
2208    let currentIndex = 0;
2209    let isAnimating = false;
2210    let animationTimeout;
2211
2212    const getVisibleCount = () => {
2213      if (window.innerWidth < 640) {
2214        return 1;
2215      }
2216      if (window.innerWidth < 1024) {
2217        return 2;
2218      }
2219      return 3;
2220    };
2221
2222    const getCardMetrics = () => {
2223      if (!cards.length) return { cardWidth: 0, gap: 16 };
2224      const gapVariable = getComputedStyle(document.documentElement).getPropertyValue("--carousel-gap");
2225      const gap = Number.parseInt(gapVariable, 10) || 16;
2226      const cardWidth = cards[0].getBoundingClientRect().width;
2227      return { cardWidth, gap };
2228    };
2229
2230    const updateButtons = () => {
2231      if (!viewport || !prevButton || !nextButton) {
2232        return;
2233      }
2234      const visible = getVisibleCount();
2235      const maxIndex = Math.max(0, cards.length - visible);
2236      prevButton.disabled = currentIndex <= 0;
2237      nextButton.disabled = currentIndex >= maxIndex;
2238    };
2239
2240    const scrollCarousel = (direction) => {
2241      const { cardWidth, gap } = getCardMetrics();
2242      if (!cardWidth || !viewport) {
2243        return;
2244      }
2245      const visible = getVisibleCount();
2246      const maxIndex = Math.max(0, cards.length - visible);
2247      currentIndex = Math.min(Math.max(0, currentIndex + direction), maxIndex);
2248      isAnimating = true;
2249      clearTimeout(animationTimeout);
2250      const targetLeft = currentIndex * (cardWidth + gap);
2251      viewport.scrollTo({ left: targetLeft, behavior: "smooth" });
2252      updateButtons();
2253      animationTimeout = setTimeout(() => {
2254        isAnimating = false;
2255      }, 320);
2256    };
2257
2258    prevButton?.addEventListener("click", () => scrollCarousel(-1));
2259    nextButton?.addEventListener("click", () => scrollCarousel(1));
2260
2261    // Touch/swipe support for mobile - let native scroll-snap handle snapping
2262    let scrollEndTimeout;
2263    let lastScrollLeft = viewport?.scrollLeft || 0;
2264
2265    // Detect when scrolling ends to sync the currentIndex
2266    const handleScrollEnd = () => {
2267      if (!viewport) return;
2268      const { cardWidth, gap } = getCardMetrics();
2269      if (!cardWidth) return;
2270      
2271      // Calculate the closest card index based on scroll position
2272      const newIndex = Math.round(viewport.scrollLeft / (cardWidth + gap));
2273      const visible = getVisibleCount();
2274      const maxIndex = Math.max(0, cards.length - visible);
2275      const clampedIndex = Math.min(Math.max(0, newIndex), maxIndex);
2276      
2277      // Only update if index changed
2278      if (currentIndex !== clampedIndex) {
2279        currentIndex = clampedIndex;
2280        updateButtons();
2281      }
2282    };
2283
2284    viewport?.addEventListener("scroll", () => {
2285      // Clear existing timeout
2286      clearTimeout(scrollEndTimeout);
2287      
2288      // Set a new timeout to detect when scrolling has stopped
2289      scrollEndTimeout = setTimeout(() => {
2290        handleScrollEnd();
2291      }, 150);
2292      
2293      // During scroll, update buttons based on current position
2294      if (!isAnimating) {
2295        const { cardWidth, gap } = getCardMetrics();
2296        if (cardWidth) {
2297          const scrollIndex = Math.round(viewport.scrollLeft / (cardWidth + gap));
2298          const visible = getVisibleCount();
2299          const maxIndex = Math.max(0, cards.length - visible);
2300          const clampedIndex = Math.min(Math.max(0, scrollIndex), maxIndex);
2301          
2302          // Update buttons state
2303          if (prevButton && nextButton) {
2304            prevButton.disabled = clampedIndex <= 0;
2305            nextButton.disabled = clampedIndex >= maxIndex;
2306          }
2307        }
2308      }
2309    });
2310
2311    window.addEventListener("resize", () => {
2312      // Recompute position on resize to stay aligned
2313      const { cardWidth, gap } = getCardMetrics();
2314      const visible = getVisibleCount();
2315      const maxIndex = Math.max(0, cards.length - visible);
2316      currentIndex = Math.min(currentIndex, maxIndex);
2317      const targetLeft = currentIndex * (cardWidth + gap);
2318      viewport?.scrollTo({ left: targetLeft });
2319      updateButtons();
2320    });
2321
2322    updateButtons();
2323  });
2324})();
2325
2326// Reviews carousel - Auto-scroll with columns (continuous infinite scroll)
2327(function() {
2328  document.addEventListener("DOMContentLoaded", () => {
2329    const carousel = document.querySelector(".reviews-carousel-section");
2330    if (!carousel) {
2331      return;
2332    }
2333
2334    const viewport = carousel.querySelector(".reviews-carousel-viewport");
2335    const track = carousel.querySelector(".reviews-carousel-track");
2336    const columns = Array.from(carousel.querySelectorAll(".reviews-carousel-column"));
2337
2338    if (!viewport || !track || !columns.length) {
2339      return;
2340    }
2341
2342    // Collect all review cards from all columns
2343    const allCards = [];
2344    columns.forEach((column) => {
2345      const cards = Array.from(column.querySelectorAll(".reviews-carousel-card"));
2346      cards.forEach((card) => {
2347        const textEl = card.querySelector(".reviews-card-text");
2348        const nameEl = card.querySelector(".reviews-card-name");
2349        const text = textEl ? textEl.textContent.trim() : "";
2350        const name = nameEl ? nameEl.textContent.trim() : "";
2351        const key = text + "||" + name;
2352        allCards.push({ node: card, key });
2353      });
2354    });
2355
2356    if (allCards.length > 1) {
2357      // Fisher-Yates shuffle algorithm
2358      const fisherYatesShuffle = (array) => {
2359        for (let i = array.length - 1; i > 0; i -= 1) {
2360          const j = Math.floor(Math.random() * (i + 1));
2361          const temp = array[i];
2362          array[i] = array[j];
2363          array[j] = temp;
2364        }
2365      };
2366
2367      // Create order where no identical reviews are adjacent
2368      const createNonAdjacentOrder = () => {
2369        const attempts = 50;
2370        const working = allCards.slice();
2371        for (let attempt = 0; attempt < attempts; attempt += 1) {
2372          fisherYatesShuffle(working);
2373          let hasAdjacentDuplicate = false;
2374          for (let i = 1; i < working.length; i += 1) {
2375            if (working[i].key === working[i - 1].key) {
2376              hasAdjacentDuplicate = true;
2377              break;
2378            }
2379          }
2380          if (!hasAdjacentDuplicate) {
2381            return working.slice();
2382          }
2383        }
2384        return working.slice();
2385      };
2386
2387      const shuffled = createNonAdjacentOrder();
2388
2389      // Clear all existing columns
2390      columns.forEach((column) => {
2391        while (column.firstChild) {
2392          column.removeChild(column.firstChild);
2393        }
2394      });
2395
2396      // Distribute reviews in pattern: 3, 2, 3, 2, 3, 2...
2397      // This pattern repeats: column with 3 reviews, then column with 2 reviews
2398      const pattern = [3, 2];
2399      let currentColumnIndex = 0;
2400      let reviewsInCurrentColumn = 0;
2401      let patternIndex = 0;
2402      
2403      // Function to get or create a column at the given index
2404      const getOrCreateColumn = (index) => {
2405        if (index < columns.length) {
2406          return columns[index];
2407        }
2408        // Create a new column if needed
2409        const newColumn = document.createElement("div");
2410        newColumn.className = "reviews-carousel-column";
2411        track.appendChild(newColumn);
2412        columns.push(newColumn);
2413        return newColumn;
2414      };
2415      
2416      shuffled.forEach((item) => {
2417        // Get or create the current column
2418        const currentColumn = getOrCreateColumn(currentColumnIndex);
2419        
2420        // Add review to current column
2421        currentColumn.appendChild(item.node);
2422        reviewsInCurrentColumn += 1;
2423        
2424        // Check if we've reached the target count for current column
2425        const targetCount = pattern[patternIndex];
2426        if (reviewsInCurrentColumn >= targetCount) {
2427          // Move to next column and next pattern value
2428          reviewsInCurrentColumn = 0;
2429          currentColumnIndex += 1;
2430          patternIndex = (patternIndex + 1) % pattern.length;
2431        }
2432      });
2433    }
2434
2435    // Update columns array after shuffle (in case new columns were created)
2436    const finalColumns = Array.from(carousel.querySelectorAll(".reviews-carousel-column"));
2437    
2438    let isPaused = false;
2439    let isDragging = false;
2440    let animationFrameId;
2441    const scrollSpeed = 0.2; // Scroll speed in pixels per frame (slow, smooth movement)
2442    let scrollAccumulator = 0; // Accumulate fractional pixels for smooth scrolling
2443
2444    let cachedColumnWidth = 0;
2445    let cachedOriginalSetWidth = 0;
2446    const gap = 24;
2447    const originalColumnCount = finalColumns.length;
2448
2449    // Clone all columns and append them to create seamless infinite scroll
2450    finalColumns.forEach((column) => {
2451      const clone = column.cloneNode(true);
2452      clone.setAttribute("aria-hidden", "true");
2453      track.appendChild(clone);
2454    });
2455
2456    // Drag state
2457    let dragStartX = 0;
2458    let dragStartScroll = 0;
2459    let lastDragX = 0;
2460    let hasDragged = false;
2461    const dragThreshold = 5; // Minimum pixels to move before considering it a drag
2462
2463    const updateCachedWidths = () => {
2464      if (!finalColumns.length) {
2465        cachedColumnWidth = 0;
2466        cachedOriginalSetWidth = 0;
2467        return;
2468      }
2469      const columnWidth = finalColumns[0].getBoundingClientRect().width;
2470      if (columnWidth > 0) {
2471        cachedColumnWidth = columnWidth + gap;
2472        cachedOriginalSetWidth = originalColumnCount * cachedColumnWidth;
2473      }
2474    };
2475
2476    const scrollToNext = () => {
2477      if (!viewport) return;
2478      
2479      if (isPaused || isDragging) {
2480        animationFrameId = requestAnimationFrame(scrollToNext);
2481        return;
2482      }
2483      
2484      if (!cachedOriginalSetWidth || cachedOriginalSetWidth <= 0) {
2485        updateCachedWidths();
2486        if (!cachedOriginalSetWidth || cachedOriginalSetWidth <= 0) {
2487          animationFrameId = requestAnimationFrame(scrollToNext);
2488          return;
2489        }
2490      }
2491      
2492      // Accumulate scroll for smooth sub-pixel movement
2493      scrollAccumulator += scrollSpeed;
2494      const scrollDelta = Math.floor(scrollAccumulator);
2495      scrollAccumulator -= scrollDelta;
2496      
2497      if (scrollDelta > 0) {
2498        const currentScroll = viewport.scrollLeft;
2499        const newScroll = currentScroll + scrollDelta;
2500        
2501        // Reset to beginning when we have scrolled past the original set
2502        if (newScroll >= cachedOriginalSetWidth) {
2503          viewport.scrollLeft = newScroll - cachedOriginalSetWidth;
2504        } else {
2505          viewport.scrollLeft = newScroll;
2506        }
2507      }
2508      
2509      animationFrameId = requestAnimationFrame(scrollToNext);
2510    };
2511
2512    const startAutoScroll = () => {
2513      if (animationFrameId) {
2514        cancelAnimationFrame(animationFrameId);
2515      }
2516      animationFrameId = requestAnimationFrame(scrollToNext);
2517    };
2518
2519    const stopAutoScroll = () => {
2520      if (animationFrameId) {
2521        cancelAnimationFrame(animationFrameId);
2522        animationFrameId = null;
2523      }
2524    };
2525
2526    const pauseAutoScroll = () => {
2527      isPaused = true;
2528    };
2529
2530    const resumeAutoScroll = () => {
2531      isPaused = false;
2532      // Ensure animation continues even if it was stopped
2533      if (!animationFrameId) {
2534        startAutoScroll();
2535      }
2536    };
2537
2538    // Only pause on focus for accessibility (keyboard navigation)
2539    viewport.addEventListener("focusin", pauseAutoScroll);
2540    viewport.addEventListener("focusout", resumeAutoScroll);
2541
2542    // Drag functionality
2543    const handleDragStart = (clientX) => {
2544      dragStartX = clientX;
2545      dragStartScroll = viewport.scrollLeft;
2546      lastDragX = clientX;
2547      hasDragged = false;
2548    };
2549
2550    const handleDragMove = (clientX) => {
2551      if (isDragging === false && dragStartX !== 0) {
2552        const moveDistance = Math.abs(clientX - dragStartX);
2553        if (moveDistance > dragThreshold) {
2554          isDragging = true;
2555          hasDragged = true;
2556          viewport.classList.add("grabbing");
2557        } else {
2558          return; // Not enough movement to start dragging
2559        }
2560      }
2561      
2562      if (!isDragging) return;
2563      
2564      const deltaX = lastDragX - clientX; // Inverted for natural drag direction
2565      let newScroll = viewport.scrollLeft + deltaX;
2566      
2567      // Handle infinite scroll reset
2568      if (cachedOriginalSetWidth > 0) {
2569        if (newScroll < 0) {
2570          newScroll = cachedOriginalSetWidth + newScroll;
2571        } else if (newScroll >= cachedOriginalSetWidth) {
2572          newScroll = newScroll - cachedOriginalSetWidth;
2573        }
2574      }
2575      
2576      viewport.scrollLeft = newScroll;
2577      lastDragX = clientX;
2578    };
2579
2580    const handleDragEnd = () => {
2581      if (isDragging) {
2582        isDragging = false;
2583        viewport.classList.remove("grabbing");
2584      }
2585      
2586      // Reset drag state
2587      dragStartX = 0;
2588      dragStartScroll = 0;
2589      lastDragX = 0;
2590      hasDragged = false;
2591      
2592      // Always resume auto-scroll after release
2593      isPaused = false;
2594      if (!animationFrameId) {
2595        startAutoScroll();
2596      }
2597    };
2598
2599    // Mouse events
2600    viewport.addEventListener("mousedown", (e) => {
2601      handleDragStart(e.clientX);
2602    });
2603
2604    document.addEventListener("mousemove", (e) => {
2605      if (dragStartX !== 0) {
2606        if (isDragging) {
2607          e.preventDefault();
2608        }
2609        handleDragMove(e.clientX);
2610      }
2611    });
2612
2613    document.addEventListener("mouseup", () => {
2614      handleDragEnd();
2615    });
2616
2617    // Touch events
2618    viewport.addEventListener("touchstart", (e) => {
2619      if (e.touches.length === 1) {
2620        handleDragStart(e.touches[0].clientX);
2621      }
2622    }, { passive: true });
2623
2624    document.addEventListener("touchmove", (e) => {
2625      if (isDragging && e.touches.length === 1) {
2626        e.preventDefault();
2627        handleDragMove(e.touches[0].clientX);
2628      }
2629    }, { passive: false });
2630
2631    document.addEventListener("touchend", () => {
2632      handleDragEnd();
2633    });
2634
2635    const initializeCarousel = () => {
2636      updateCachedWidths();
2637      if (cachedOriginalSetWidth > 0 && viewport.scrollWidth > viewport.clientWidth) {
2638        startAutoScroll();
2639      } else {
2640        requestAnimationFrame(initializeCarousel);
2641      }
2642    };
2643    
2644    // Wait a bit longer for layout to be ready
2645    setTimeout(initializeCarousel, 200);
2646
2647    window.addEventListener("resize", () => {
2648      updateCachedWidths();
2649      if (cachedOriginalSetWidth) {
2650        const currentScroll = viewport.scrollLeft;
2651        if (currentScroll >= cachedOriginalSetWidth) {
2652          viewport.scrollLeft = currentScroll - cachedOriginalSetWidth;
2653        }
2654      }
2655    });
2656
2657    // Arrow navigation for keyboard users
2658    const prevButton = carousel.querySelector('.reviews-carousel-nav[data-direction="prev"]');
2659    const nextButton = carousel.querySelector('.reviews-carousel-nav[data-direction="next"]');
2660
2661    const scrollByColumn = (direction) => {
2662      if (!cachedColumnWidth) {
2663        updateCachedWidths();
2664      }
2665      if (!cachedColumnWidth) return;
2666
2667      const scrollAmount = cachedColumnWidth;
2668      let newScroll = viewport.scrollLeft + (direction === "next" ? scrollAmount : -scrollAmount);
2669
2670      // Handle infinite scroll wrapping
2671      if (cachedOriginalSetWidth > 0) {
2672        if (newScroll < 0) {
2673          newScroll = cachedOriginalSetWidth + newScroll;
2674        } else if (newScroll >= cachedOriginalSetWidth) {
2675          newScroll = newScroll - cachedOriginalSetWidth;
2676        }
2677      }
2678
2679      viewport.scrollTo({
2680        left: newScroll,
2681        behavior: "smooth"
2682      });
2683    };
2684
2685    if (prevButton) {
2686      prevButton.addEventListener("click", () => {
2687        pauseAutoScroll();
2688        scrollByColumn("prev");
2689        // Resume after a delay to let users see the result
2690        setTimeout(resumeAutoScroll, 3000);
2691      });
2692    }
2693
2694    if (nextButton) {
2695      nextButton.addEventListener("click", () => {
2696        pauseAutoScroll();
2697        scrollByColumn("next");
2698        // Resume after a delay to let users see the result
2699        setTimeout(resumeAutoScroll, 3000);
2700      });
2701    }
2702  });
2703})();
2704
2705// Solutions panel background video: slow down the loop
2706(function () {
2707  document.addEventListener("DOMContentLoaded", () => {
2708    const RATE = 0.3;
2709    document.querySelectorAll(".avz .bgvid").forEach((bgvid) => {
2710      const setRate = () => {
2711        try {
2712          bgvid.playbackRate = RATE;
2713        } catch (e) {}
2714      };
2715      setRate();
2716      ["loadedmetadata", "canplay", "play"].forEach((ev) =>
2717        bgvid.addEventListener(ev, setRate)
2718      );
2719    });
2720  });
2721})();

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.