PageSourceSearch

https://www.marq.com/wp-content/themes/marq/js/marq.js?ver=1790238864

js marq.com collected 2026-09-24 08:37:25 UTC 69,561 bytes, 1,924 lines download raw bytes

1/**
2 * Table of Contents:
3 * 1. Custom Videos
4 * 2. Sticky Menu(On Scrolling Up) 
5 * 3. Back To Top
6 * 4. Media Slider
7 * 5. Mansory Slider
8 * 6. Logos Slider
9 * 7. FAQs Accordion
10 * 8. Testimonials Slider
11 * 9. Tabs Accordion
12 * 10. Back To Top
13 * 11. Team Member Lightbox
14 * 12. Load More Press Release
15 * 13. Load More Reviews
16 * 14. Load More Case Studies
17 * 15. Counters
18 * 16. Post Load More
19 * 17. Post Sticky Sidebar
20 * 18. Compare Table Slider
21 * 19. Sticky Pricing Table
22 */
23  
24/*--------------------------------------------------------------
25# Custom Videos
26--------------------------------------------------------------*/
27
28document.addEventListener('DOMContentLoaded', function () {
29    // Initialize Plyr for regular video, YouTube, and Vimeo players
30    document.querySelectorAll('.player').forEach((p) => {
31        const player = new Plyr(p, {
32            fullscreen: { enabled: true, fallback: true, iosNative: true }
33        });
34
35        // Enable fullscreen button manually if needed
36        p.addEventListener('dblclick', () => {
37            player.fullscreen.enter();
38        });
39    });
40
41    // Initialize Wistia players with Plyr
42    document.querySelectorAll('.wistia-player').forEach((element) => {
43        const wistiaId = element.dataset.wistiaId;
44        
45        // Create iframe for Wistia
46        const iframe = document.createElement('iframe');
47        iframe.src = `https://fast.wistia.net/embed/iframe/${wistiaId}`;
48        iframe.allowFullscreen = true;
49        iframe.allow = 'autoplay; fullscreen';
50        
51        // Add poster if available
52        if (element.dataset.poster) {
53            iframe.dataset.poster = element.dataset.poster;
54        }
55        
56        element.appendChild(iframe);
57        
58        // Initialize Plyr on the Wistia iframe
59        const player = new Plyr(iframe, {
60            fullscreen: { enabled: true, fallback: true, iosNative: true }
61        });
62
63        // Enable fullscreen on double-click
64        iframe.addEventListener('dblclick', () => {
65            player.fullscreen.enter();
66        });
67    });
68});
69
70/*--------------------------------------------------------------
71# Sticky Menu(On Scrolling Up) 
72--------------------------------------------------------------*/
73jQuery(function () {
74    var header = document.querySelector('.site-header');
75    var ticking = false;
76
77    window.addEventListener('scroll', function() {
78        if (!ticking) {
79            requestAnimationFrame(function() {
80                var st = window.scrollY || window.pageYOffset;
81                if (st > 80) {
82                    header.classList.add('header-scrolled');
83                } else {
84                    header.classList.remove('header-scrolled');
85                }
86                ticking = false;
87            });
88            ticking = true;
89        }
90    }, { passive: true });
91});
92
93
94/*--------------------------------------------------------------
95# Back To Top
96--------------------------------------------------------------*/
97document.addEventListener('DOMContentLoaded', function () {
98    "use strict";
99
100    var progressPath = document.querySelector('.progress-wrap path');
101    if (!progressPath) return;
102
103    var pathLength = progressPath.getTotalLength();
104    progressPath.style.transition = 'none';
105    progressPath.style.strokeDasharray = pathLength + ' ' + pathLength;
106    progressPath.style.strokeDashoffset = pathLength;
107    requestAnimationFrame(function() {
108        progressPath.style.transition = 'stroke-dashoffset 10ms linear';
109    });
110
111    var cachedScrollHeight = document.documentElement.scrollHeight - window.innerHeight;
112
113    function updateProgress() {
114        var scroll = window.scrollY || window.pageYOffset;
115        var progress = pathLength - (scroll * pathLength / cachedScrollHeight);
116        progressPath.style.strokeDashoffset = progress;
117    }
118
119    updateProgress();
120    window.addEventListener('scroll', updateProgress, { passive: true });
121
122   
123    window.addEventListener('resize', function() {
124        cachedScrollHeight = document.documentElement.scrollHeight - window.innerHeight;
125        updateProgress();
126    }, { passive: true });
127
128    var offset = 50;
129    var progressWrap = document.querySelector('.progress-wrap');
130
131    window.addEventListener('scroll', function () {
132        if (!progressWrap) return;
133        if ((window.scrollY || window.pageYOffset) > offset) {
134            progressWrap.classList.add('active-progress');
135        } else {
136            progressWrap.classList.remove('active-progress');
137        }
138    }, { passive: true });
139
140    if (progressWrap) {
141        progressWrap.addEventListener('click', function (event) {
142            event.preventDefault();
143            window.scrollTo({ top: 0, behavior: 'smooth' });
144            return false;
145        });
146    }
147});
148
149/*--------------------------------------------------------------
150# Media Slider
151--------------------------------------------------------------*/
152document.addEventListener('DOMContentLoaded', function() {
153    
154    // Initialize Owl Carousel for media sliders
155    const mediaSliders = document.querySelectorAll('.media-slider');
156    
157    mediaSliders.forEach(function(sliderElement) {
158        const $slider = jQuery(sliderElement);
159        
160        $slider.owlCarousel({
161            loop: true,
162            margin: 24,
163            nav: true,
164            dots: false,
165            autoplay: false,
166            autoplayHoverPause: true,
167            smartSpeed: 600,
168            navText: [
169                '<span aria-label="Previous slide" class="prev-slide"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="19" viewBox="0 0 25 19" fill="none"><path d="M23.25 7.95508C23.9404 7.95508 24.5 8.51472 24.5 9.20508C24.5 9.89543 23.9404 10.4551 23.25 10.4551V9.20508V7.95508ZM0.366116 10.089C-0.122038 9.60081 -0.122038 8.80935 0.366116 8.32119L8.32107 0.366243C8.80922 -0.121912 9.60068 -0.121912 10.0888 0.366243C10.577 0.854399 10.577 1.64585 10.0888 2.13401L3.01777 9.20508L10.0888 16.2761C10.577 16.7643 10.577 17.5558 10.0888 18.0439C9.60068 18.5321 8.80922 18.5321 8.32107 18.0439L0.366116 10.089ZM23.25 9.20508V10.4551L1.25 10.4551V9.20508V7.95508L23.25 7.95508V9.20508Z" fill="#00191B" fill-opacity="0.3"/></svg></span>', 
170                '<span aria-label="Next slide" class="next-slide"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="19" viewBox="0 0 25 19" fill="none"><path d="M1.25 7.95508C0.559644 7.95508 0 8.51472 0 9.20508C0 9.89543 0.559644 10.4551 1.25 10.4551L1.25 9.20508L1.25 7.95508ZM24.1339 10.089C24.622 9.60081 24.622 8.80935 24.1339 8.32119L16.1789 0.366243C15.6908 -0.121912 14.8993 -0.121912 14.4112 0.366243C13.923 0.854399 13.923 1.64585 14.4112 2.13401L21.4822 9.20508L14.4112 16.2761C13.923 16.7643 13.923 17.5558 14.4112 18.0439C14.8993 18.5321 15.6908 18.5321 16.1789 18.0439L24.1339 10.089ZM1.25 9.20508L1.25 10.4551L23.25 10.4551V9.20508V7.95508L1.25 7.95508L1.25 9.20508Z" fill="#00191B"/></svg></span>'
171            ],
172            responsive: {
173                0: {
174                    items: 2,
175                    center: true,
176                    stagePadding: 40,
177                    margin: 15,
178                    nav: true,
179                    loop: true,
180                },
181                576: {
182                    items: 2,
183                    center: true,
184                    stagePadding: 30,
185                    margin: 20,
186                    nav: true,
187                    loop: true,
188                },
189                768: {
190                    items: 3,
191                    center: false,
192                    stagePadding: 0,
193                    margin: 30,
194                    nav: true,
195                    loop: true,
196                },
197                992: {
198                    items: 4,
199                    center: false,
200                    stagePadding: 0,
201                    margin: 40,
202                    nav: true,
203                    loop: true,
204                },
205                1200: {
206                    items: 5,
207                    center: false,
208                    stagePadding: 0,
209                    margin: 50,
210                    nav: true,
211                    loop: true,
212                }
213            },
214            onInitialized: function(event) {
215                addAccessibilityLabels(sliderElement, event);
216            },
217            onChanged: function(event) {
218                addAccessibilityLabels(sliderElement, event);
219            }
220        });
221        
222        function addAccessibilityLabels(sliderElement, event) {
223            const navButtons = sliderElement.querySelectorAll('.owl-nav button');
224            navButtons.forEach(function(button, index) {
225                if (index === 0) {
226                    button.setAttribute('aria-label', 'Previous slide');
227                } else {
228                    button.setAttribute('aria-label', 'Next slide');
229                }
230            });
231            
232            const dotButtons = sliderElement.querySelectorAll('.owl-dots button');
233            dotButtons.forEach(function(button, index) {
234                const slideNumber = index + 1;
235                button.setAttribute('aria-label', 'Go to slide ' + slideNumber);
236                
237                if (button.classList.contains('active')) {
238                    button.setAttribute('aria-label', 'Go to slide ' + slideNumber + ' (current)');
239                    button.setAttribute('aria-current', 'true');
240                } else {
241                    button.removeAttribute('aria-current');
242                }
243            });
244        }
245        
246        const sliderLinks = sliderElement.querySelectorAll('.slider-image-link');
247        
248        sliderLinks.forEach(function(link) {
249            link.addEventListener('mouseenter', function() {
250                $slider.trigger('stop.owl.autoplay');
251            });
252            
253            link.addEventListener('mouseleave', function() {
254                $slider.trigger('play.owl.autoplay');
255            });
256        });
257    });
258    
259    const plusIcons = document.querySelectorAll('.slider-plus-icon');
260    
261    plusIcons.forEach(function(icon) {
262        icon.addEventListener('keydown', function(e) {
263            if (e.key === 'Enter' || e.key === ' ') {
264                e.preventDefault();
265                window.location.href = icon.getAttribute('href');
266            }
267        });
268    });
269});
270
271/*--------------------------------------------------------------
272# Mansory Slider
273--------------------------------------------------------------*/
274document.addEventListener('DOMContentLoaded', function() {
275    
276    const masonrySliders = document.querySelectorAll('.masonry-slider');
277    
278    masonrySliders.forEach(function(sliderElement) {
279        const $slider = jQuery(sliderElement);
280        
281        $slider.owlCarousel({
282            loop: true,
283            margin: 24,
284            nav: true,
285            dots: true,
286            autoplay: false,
287            autoplayHoverPause: true,
288            smartSpeed: 600,
289            navText: [
290                '<span aria-label="Previous slide">‹</span>', 
291                '<span aria-label="Next slide">›</span>'
292            ],
293            responsive: {
294                0: {
295                    items: 1,
296                    center: true,
297                    stagePadding: 40,
298                    margin: 15,
299                    nav: true,
300                    loop: true,
301                    dots: false
302                },
303                576: {
304                    items: 2,
305                    center: true,
306                    stagePadding: 30,
307                    margin: 20,
308                    nav: true,
309                    loop: true,
310                    dots: false
311                },
312                768: {
313                    items: 3,
314                    center: true,
315                    stagePadding: 0,
316                    margin: 20,
317                    nav: true,
318                    loop: true,
319                    dots: false
320                },
321                992: {
322                    items: 4,
323                    center: true,
324                    stagePadding: 0,
325                    margin: 24,
326                    nav: true,
327                    loop: true,
328                },
329                1200: {
330                    items: 5,
331                    center: false,
332                    stagePadding: 0,
333                    margin: 24,
334                    nav: true,
335                    loop: true,
336                }
337            },
338            onInitialized: function() {
339                fixARIAIssues(sliderElement);
340            },
341            onTranslated: function() {
342                fixARIAIssues(sliderElement);
343            },
344            onRefreshed: function() {
345                fixARIAIssues(sliderElement);
346            },
347            onChanged: function(event) {
348                const dots = sliderElement.querySelectorAll('.owl-dot');
349                dots.forEach(function(dot, index) {
350                    if (dot.classList.contains('active')) {
351                        dot.setAttribute('aria-label', 'Go to slide ' + (index + 1) + ' (current)');
352                        dot.setAttribute('aria-current', 'true');
353                    } else {
354                        dot.setAttribute('aria-label', 'Go to slide ' + (index + 1));
355                        dot.removeAttribute('aria-current');
356                    }
357                });
358            }
359        });
360        
361        const sliderLinks = sliderElement.querySelectorAll('.slider-image-link, .slider-image-wrapper');
362        
363        sliderLinks.forEach(function(element) {
364            element.addEventListener('mouseenter', function() {
365                $slider.trigger('stop.owl.autoplay');
366            });
367            
368            element.addEventListener('mouseleave', function() {
369                $slider.trigger('play.owl.autoplay');
370            });
371        });
372        
373        const navButtons = sliderElement.querySelectorAll('.owl-nav button');
374        navButtons.forEach(function(button) {
375            button.addEventListener('keydown', function(e) {
376                if (e.key === 'Enter' || e.key === ' ') {
377                    e.preventDefault();
378                    button.click();
379                }
380            });
381        });
382        
383        const dotButtons = sliderElement.querySelectorAll('.owl-dot');
384        dotButtons.forEach(function(button) {
385            button.addEventListener('keydown', function(e) {
386                if (e.key === 'Enter' || e.key === ' ') {
387                    e.preventDefault();
388                    button.click();
389                }
390            });
391        });
392    });
393    
394    function fixARIAIssues(sliderElement) {
395        const $slider = jQuery(sliderElement);
396        
397        $slider.removeAttr('role aria-live aria-atomic');
398        $slider.attr({
399            'role': 'region',
400            'aria-label': 'Gallery carousel'
401        });
402        
403        const navButtons = sliderElement.querySelectorAll('.owl-nav button');
404        navButtons.forEach(function(button) {
405            button.removeAttribute('role');
406            button.setAttribute('tabindex', '0');
407            
408            if (button.classList.contains('owl-prev')) {
409                button.setAttribute('aria-label', 'Previous slide');
410            } else if (button.classList.contains('owl-next')) {
411                button.setAttribute('aria-label', 'Next slide');
412            }
413        });
414        
415        const dotButtons = sliderElement.querySelectorAll('.owl-dots button');
416        dotButtons.forEach(function(button, index) {
417            button.removeAttribute('role');
418            button.setAttribute('tabindex', '0');
419            button.setAttribute('aria-label', 'Go to slide ' + (index + 1));
420        });
421    }
422});
423
424/*--------------------------------------------------------------
425# Logos Slider
426--------------------------------------------------------------*/
427(function() {
428    'use strict';
429    
430    function initLogosSlider() {
431        const logosSliders = document.querySelectorAll('.logos-slider');
432        
433        if (logosSliders.length === 0) return;
434        
435        logosSliders.forEach(function(sliderElement) {
436            const $slider = jQuery(sliderElement);
437            
438            if ($slider.hasClass('owl-loaded')) {
439                return;
440            }
441            
442            const isMobile = window.innerWidth < 768;
443            
444            $slider.owlCarousel({
445                slideTransition: 'linear',
446                autoplaySpeed: 6000,
447                autoWidth: !isMobile,
448                center: false,
449                loop: true,
450                autoplay: !isMobile,
451                autoplayTimeout: 6000,
452                autoplayHoverPause: true,
453                dots: false,
454                smartSpeed: isMobile ? 800 : 6000,
455                mouseDrag: true,
456                touchDrag: true,
457                nav: isMobile,
458                slideBy: 1,
459                navText: [
460                    '<span aria-label="Previous slide">‹</span>', 
461                    '<span aria-label="Next slide">›</span>'
462                ],
463                responsiveClass: true,
464                responsive: {
465                    0: {
466                        items: 2,
467                        margin: 10,
468                        autoWidth: false,
469                        autoplay: false,
470                        nav: true,
471                        slideTransition: 'linear',
472                        smartSpeed: 800,
473                        slideBy: 1,
474                    },
475                    545: {
476                        items: 3,
477                        margin: 30,
478                        autoWidth: false,
479                        autoplay: false,
480                        nav: true,
481                        slideTransition: 'linear',
482                        smartSpeed: 800,
483                        slideBy: 1,
484                    },
485                    768: {
486                        items: 4,
487                        margin: 40,
488                        autoWidth: true,
489                        autoplay: true,
490                        nav: false,
491                        slideTransition: 'linear',
492                        smartSpeed: 6000,
493                    },
494                    992: {
495                        items: 5,
496                        margin: 50,
497                        autoWidth: true,
498                        autoplay: true,
499                        nav: false,
500                        slideTransition: 'linear',
501                        smartSpeed: 6000,
502                    },
503                    1200: {
504                        items: 6,
505                        margin: 60,
506                        autoWidth: true,
507                        autoplay: true,
508                        nav: false,
509                        slideTransition: 'linear',
510                        smartSpeed: 6000,
511                    }
512                },
513                onInitialized: function(event) {
514                    fixClonedLinks($slider);
515                    fixARIAIssues(sliderElement);
516                },
517                onTranslated: function(event) {
518                    fixClonedLinks($slider);
519                    fixARIAIssues(sliderElement);
520                },
521                onRefreshed: function(event) {
522                    fixARIAIssues(sliderElement);
523                }
524            });
525            
526            let isDragging = false;
527            
528            $slider.on('drag.owl.carousel', function(event) {
529                isDragging = true;
530            });
531            
532            $slider.on('dragged.owl.carousel', function(event) {
533                setTimeout(function() {
534                    isDragging = false;
535                }, 100);
536            });
537            
538            $slider.on('click', '.logo-link', function(e) {
539                if (isDragging) {
540                    e.preventDefault();
541                    return false;
542                }
543            });
544        });
545    }
546    
547    function fixARIAIssues(sliderElement) {
548        const $slider = jQuery(sliderElement);
549        
550        $slider.removeAttr('role aria-live aria-atomic');
551        $slider.attr({
552            'role': 'region',
553            'aria-label': 'Logo carousel'
554        });
555        
556        const navButtons = sliderElement.querySelectorAll('.owl-nav button');
557        navButtons.forEach(function(button) {
558            button.removeAttribute('role');
559            
560            if (button.classList.contains('owl-prev')) {
561                button.setAttribute('aria-label', 'Previous slide');
562            } else if (button.classList.contains('owl-next')) {
563                button.setAttribute('aria-label', 'Next slide');
564            }
565        });
566    }
567    
568    function fixClonedLinks($slider) {
569        const allItems = $slider.find('.owl-item');
570        
571        allItems.each(function() {
572            const $item = jQuery(this);
573            const $link = $item.find('.logo-link');
574            
575            if ($item.hasClass('cloned') && $link.length > 0) {
576                const href = $link.attr('href');
577                const title = $link.attr('title');
578                const target = $link.attr('target');
579                const rel = $link.attr('rel');
580                const $img = $link.find('img').clone();
581                
582                $link.replaceWith(
583                    jQuery('<a></a>')
584                        .attr('href', href)
585                        .attr('title', title)
586                        .attr('target', target)
587                        .attr('rel', rel)
588                        .attr('aria-label', title)
589                        .addClass('logo-link')
590                        .append($img)
591                );
592            }
593        });
594    }
595    
596    if (document.readyState === 'loading') {
597        document.addEventListener('DOMContentLoaded', initLogosSlider);
598    } else {
599        initLogosSlider();
600    }
601    
602    let resizeTimer;
603    window.addEventListener('resize', function() {
604        clearTimeout(resizeTimer);
605        resizeTimer = setTimeout(function() {
606            const sliders = document.querySelectorAll('.logos-slider.owl-loaded');
607            sliders.forEach(function(slider) {
608                jQuery(slider).trigger('destroy.owl.carousel');
609                jQuery(slider).removeClass('owl-loaded owl-drag');
610            });
611            initLogosSlider();
612        }, 250);
613    });
614    
615})();
616
617/*--------------------------------------------------------------
618# FAQs Accordion
619--------------------------------------------------------------*/
620document.addEventListener('DOMContentLoaded', function() {
621    
622    const faqSections = document.querySelectorAll('.faq-accordion');
623    
624    faqSections.forEach(function(accordion) {
625        const faqButtons = accordion.querySelectorAll('.faq-question');
626        
627        faqButtons.forEach(function(button) {
628            button.addEventListener('click', function() {
629                const answer = this.nextElementSibling;
630                const isOpen = this.getAttribute('aria-expanded') === 'true';
631                
632                faqButtons.forEach(function(otherButton) {
633                    const otherAnswer = otherButton.nextElementSibling;
634                    otherButton.setAttribute('aria-expanded', 'false');
635                    otherAnswer.setAttribute('aria-hidden', 'true');
636                    otherAnswer.setAttribute('hidden', '');
637                });
638                
639                if (!isOpen) {
640                    this.setAttribute('aria-expanded', 'true');
641                    answer.setAttribute('aria-hidden', 'false');
642                    answer.removeAttribute('hidden');
643                }
644            });
645        });
646    });
647});
648
649/*--------------------------------------------------------------
650# Testimonials Slider
651--------------------------------------------------------------*/
652document.addEventListener('DOMContentLoaded', function() {
653    
654    const testimonialsSliders = document.querySelectorAll('.testimonials-slider');
655    
656    testimonialsSliders.forEach(function(sliderElement) {
657        const $slider = jQuery(sliderElement);
658        
659        $slider.owlCarousel({
660            loop: true,
661            margin: 30,
662            nav: true,
663            dots: true,
664            autoplay: false,
665            autoplayTimeout: 6000,
666            autoplayHoverPause: true,
667            smartSpeed: 600,
668            items: 1,
669            navText: [
670                '<span aria-label="Previous">‹</span>', 
671                '<span aria-label="Next">›</span>'
672            ],
673            responsive: {
674                0: {
675                    nav: false,
676                    margin: 15
677                },
678                768: {
679                    nav: true,
680                    margin: 30
681                }
682            },
683            onInitialized: function() {
684                const navButtons = sliderElement.querySelectorAll('.owl-nav button');
685                const dotButtons = sliderElement.querySelectorAll('.owl-dots button');
686                
687                navButtons.forEach(function(button) {
688                    button.setAttribute('role', 'button');
689                    button.setAttribute('tabindex', '0');
690                });
691                
692                dotButtons.forEach(function(button, index) {
693                    button.setAttribute('role', 'button');
694                    button.setAttribute('tabindex', '0');
695                    button.setAttribute('aria-label', 'Go to testimonial ' + (index + 1));
696                });
697            },
698            onChanged: function(event) {
699                const dots = sliderElement.querySelectorAll('.owl-dot');
700                dots.forEach(function(dot, index) {
701                    if (dot.classList.contains('active')) {
702                        dot.setAttribute('aria-label', 'Current testimonial ' + (index + 1));
703                    } else {
704                        dot.setAttribute('aria-label', 'Go to testimonial ' + (index + 1));
705                    }
706                });
707            }
708        });
709    });
710});
711
712/*--------------------------------------------------------------
713# Tabs Accordion
714--------------------------------------------------------------*/
715document.addEventListener('DOMContentLoaded', function() {
716  initTabsAccordion();
717});
718
719function initTabsAccordion() {
720  const sections = document.querySelectorAll('.tabs-accordion-section');
721  
722  sections.forEach(section => {
723    initDesktopTabs(section);
724    initMobileAccordion(section);
725  });
726}
727
728function initDesktopTabs(section) {
729  const tabButtons = section.querySelectorAll('.tab-button');
730  const tabPanels = section.querySelectorAll('.tab-panel');
731
732  tabButtons.forEach(button => {
733    button.addEventListener('click', () => {
734      const tabId = button.getAttribute('data-tab');
735      const tabIndex = tabId.split('-')[1];
736      const targetPanel = section.querySelector('#tabpanel-' + tabIndex);
737
738      if (button.classList.contains('active')) {
739        return;
740      }
741
742      tabButtons.forEach(btn => {
743        btn.classList.remove('active');
744        btn.setAttribute('aria-selected', 'false');
745      });
746      
747      tabPanels.forEach(panel => {
748        panel.classList.remove('active');
749        panel.setAttribute('hidden', 'hidden');
750      });
751
752      button.classList.add('active');
753      button.setAttribute('aria-selected', 'true');
754      targetPanel.classList.add('active');
755      targetPanel.removeAttribute('hidden');
756
757      announceToScreenReader('Tab ' + button.querySelector('.tab-title').textContent + ' selected');
758    });
759
760    button.addEventListener('keydown', (e) => {
761      const buttonsArray = Array.from(tabButtons);
762      const currentIndex = buttonsArray.indexOf(button);
763      let nextIndex;
764
765      switch(e.key) {
766        case 'ArrowDown':
767        case 'ArrowRight':
768          e.preventDefault();
769          nextIndex = (currentIndex + 1) % buttonsArray.length;
770          buttonsArray[nextIndex].focus();
771          buttonsArray[nextIndex].click();
772          break;
773        case 'ArrowUp':
774        case 'ArrowLeft':
775          e.preventDefault();
776          nextIndex = (currentIndex - 1 + buttonsArray.length) % buttonsArray.length;
777          buttonsArray[nextIndex].focus();
778          buttonsArray[nextIndex].click();
779          break;
780        case 'Home':
781          e.preventDefault();
782          buttonsArray[0].focus();
783          buttonsArray[0].click();
784          break;
785        case 'End':
786          e.preventDefault();
787          buttonsArray[buttonsArray.length - 1].focus();
788          buttonsArray[buttonsArray.length - 1].click();
789          break;
790        case 'Enter':
791        case ' ':
792          e.preventDefault();
793          button.click();
794          break;
795      }
796    });
797  });
798}
799
800function initMobileAccordion(section) {
801  const accordionButtons = section.querySelectorAll('.accordion-button');
802  const accordionContents = section.querySelectorAll('.accordion-content');
803
804  function updateFocusableElements(content, makesFocusable) {
805    const focusableElements = content.querySelectorAll('a, button, input, select, textarea, [tabindex]:not
805([tabindex="-1"])');
806    
807    focusableElements.forEach(element => {
808      if (makesFocusable) {
809        const originalTabindex = element.getAttribute('data-original-tabindex') || '0';
810        element.setAttribute('tabindex', originalTabindex);
811      } else {
812        if (!element.hasAttribute('data-original-tabindex')) {
813          element.setAttribute('data-original-tabindex', element.getAttribute('tabindex') || '0');
814        }
815        element.setAttribute('tabindex', '-1');
816      }
817    });
818  }
819
820  accordionContents.forEach(content => {
821    if (content.getAttribute('aria-hidden') === 'true') {
822      updateFocusableElements(content, false);
823    }
824  });
825
826  accordionButtons.forEach(button => {
827    button.addEventListener('click', () => {
828      const index = button.getAttribute('data-index');
829      const content = section.querySelector('#accordion-content-' + index);
830      const isExpanded = button.getAttribute('aria-expanded') === 'true';
831
832      accordionButtons.forEach(btn => {
833        btn.classList.add('collapsed');
834        btn.setAttribute('aria-expanded', 'false');
835      });
836      accordionContents.forEach(cnt => {
837        cnt.classList.remove('show');
838        cnt.setAttribute('aria-hidden', 'true');
839        updateFocusableElements(cnt, false);
840      });
841
842      if (!isExpanded) {
843        button.classList.remove('collapsed');
844        button.setAttribute('aria-expanded', 'true');
845        content.classList.add('show');
846        content.setAttribute('aria-hidden', 'false');
847        updateFocusableElements(content, true);
848        announceToScreenReader(button.querySelector('.accordion-title').textContent + ' expanded');
849      } else {
850        announceToScreenReader(button.querySelector('.accordion-title').textContent + ' collapsed');
851      }
852    });
853
854    button.addEventListener('keydown', (e) => {
855      const buttonsArray = Array.from(accordionButtons);
856      const currentIndex = buttonsArray.indexOf(button);
857
858      switch(e.key) {
859        case 'Enter':
860        case ' ':
861          e.preventDefault();
862          button.click();
863          break;
864        case 'ArrowDown':
865          e.preventDefault();
866          const nextIndex = (currentIndex + 1) % buttonsArray.length;
867          buttonsArray[nextIndex].focus();
868          break;
869        case 'ArrowUp':
870          e.preventDefault();
871          const prevIndex = (currentIndex - 1 + buttonsArray.length) % buttonsArray.length;
872          buttonsArray[prevIndex].focus();
873          break;
874        case 'Home':
875          e.preventDefault();
876          buttonsArray[0].focus();
877          break;
878        case 'End':
879          e.preventDefault();
880          buttonsArray[buttonsArray.length - 1].focus();
881          break;
882      }
883    });
884  });
885}
886
887function announceToScreenReader(message) {
888  let liveRegion = document.getElementById('aria-live-region');
889  
890  if (!liveRegion) {
891    liveRegion = document.createElement('div');
892    liveRegion.id = 'aria-live-region';
893    liveRegion.setAttribute('aria-live', 'polite');
894    liveRegion.setAttribute('aria-atomic', 'true');
895    liveRegion.className = 'sr-only';
896    document.body.appendChild(liveRegion);
897  }
898  
899  liveRegion.textContent = message;
900  
901  setTimeout(() => {
902    liveRegion.textContent = '';
903  }, 1000);
904}
905
906/*--------------------------------------------------------------
907# Team Member Lightbox
908--------------------------------------------------------------*/
909const TeamLightbox = {
910    open: function(index, blockId) {
911        const lightbox = document.getElementById('team-lightbox-' + blockId);
912        const teamMembers = window.teamMembersData[blockId];
913        
914        if (!lightbox || !teamMembers || !teamMembers[index]) {
915            console.error('Team member data not found');
916            return;
917        }
918        
919        const member = teamMembers[index];
920        
921        const imageEl = document.getElementById('lightbox-image-' + blockId);
922        const titleEl = document.getElementById('lightbox-title-' + blockId);
923        const positionEl = lightbox.querySelector('.team-lightbox-position');
924        const descriptionEl = lightbox.querySelector('.team-lightbox-description');
925        
926        if (imageEl && member.image) {
927            imageEl.src = member.image;
928            imageEl.alt = member.image_alt || member.name;
929        }
930        
931        if (titleEl) {
932            titleEl.textContent = member.name;
933        }
934        
935        if (positionEl) {
936            if (member.position) {
937                positionEl.textContent = member.position;
938                positionEl.style.display = 'block';
939            } else {
940                positionEl.style.display = 'none';
941            }
942        }
943        
944        if (descriptionEl) {
945            descriptionEl.innerHTML = member.content || '';
946        }
947        
948        lightbox.style.display = 'flex';
949        requestAnimationFrame(function() {
950            requestAnimationFrame(function() {
951                lightbox.classList.add('active');
952            });
953        });
954        
955        document.body.classList.add('team-lightbox-open');
956        
957        const closeBtn = lightbox.querySelector('.team-lightbox-close');
958        if (closeBtn) {
959            closeBtn.focus();
960        }
961    },
962    
963    close: function(blockId) {
964        const lightbox = document.getElementById('team-lightbox-' + blockId);
965        
966        if (!lightbox) {
967            return;
968        }
969        
970        lightbox.classList.remove('active');
971        
972        setTimeout(() => {
973            lightbox.style.display = 'none';
974            document.body.classList.remove('team-lightbox-open');
975        }, 300);
976    },
977    
978    init: function() {
979        document.querySelectorAll('.team-lightbox').forEach(lightbox => {
980            lightbox.addEventListener('click', function(e) {
981                if (e.target === this) {
982                    const blockId = this.id.replace('team-lightbox-', '');
983                    TeamLightbox.close(blockId);
984                }
985            });
986        });
987        
988        document.addEventListener('keydown', function(e) {
989            if (e.key === 'Escape') {
990                const activeLightbox = document.querySelector('.team-lightbox.active');
991                if (activeLightbox) {
992                    const blockId = activeLightbox.id.replace('team-lightbox-', '');
993                    TeamLightbox.close(blockId);
994                }
995            }
996        });
997    }
998};
999
1000if (document.readyState === 'loading') {
1001    document.addEventListener('DOMContentLoaded', function() {
1002        TeamLightbox.init();
1003    });
1004} else {
1005    TeamLightbox.init();
1006}
1007
1008/*--------------------------------------------------------------
1009# Load More Press Releases
1010--------------------------------------------------------------*/
1011document.addEventListener('DOMContentLoaded', function () {
1012	const postCards = document.querySelectorAll('.press-release-item');
1013	const loadMoreBtn = document.querySelector('.load-more-press-release');
1014	
1015	let currentIndex = 0;
1016	const increment = 10;
1017
1018	function showPosts() {
1019		let lastShownIndex = -1;
1020		for (let i = currentIndex; i < currentIndex + increment && i < postCards.length; i++) {
1021			postCards[i].style.display = '';
1022			lastShownIndex = i;
1023		}
1024		currentIndex += increment;
1025		if (currentIndex >= postCards.length) {
1026			if (loadMoreBtn) loadMoreBtn.style.visibility = 'hidden';
1027		}
1028		return lastShownIndex;
1029	}
1030
1031	postCards.forEach(card => card.style.display = 'none');
1032	showPosts();
1033
1034	if (loadMoreBtn) {
1035		function handleLoadMore(e) {
1036			e.preventDefault();
1037			const lastShownIndex = showPosts();
1038			if (lastShownIndex !== -1) {
1039				const lastCard = postCards[lastShownIndex];
1040				lastCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
1041			}
1042		}
1043		loadMoreBtn.addEventListener('click', handleLoadMore);
1044		loadMoreBtn.addEventListener('touchstart', handleLoadMore);
1045	}
1046});
1047
1048/*--------------------------------------------------------------
1049# Load More Reviews
1050--------------------------------------------------------------*/
1051document.addEventListener('DOMContentLoaded', function () {
1052	const postCards = document.querySelectorAll('.review-item');
1053	const loadMoreBtn = document.querySelector('.load-more-reviews');
1054	
1055	let currentIndex = 0;
1056	const increment = 10;
1057
1058	function showPosts() {
1059		let lastShownIndex = -1;
1060		for (let i = currentIndex; i < currentIndex + increment && i < postCards.length; i++) {
1061			postCards[i].style.display = '';
1062			lastShownIndex = i;
1063		}
1064		currentIndex += increment;
1065		if (currentIndex >= postCards.length) {
1066			if (loadMoreBtn) loadMoreBtn.style.visibility = 'hidden';
1067		}
1068		return lastShownIndex;
1069	}
1070
1071	postCards.forEach(card => card.style.display = 'none');
1072	showPosts();
1073
1074	if (loadMoreBtn) {
1075		function handleLoadMore(e) {
1076			e.preventDefault();
1077			const lastShownIndex = showPosts();
1078			if (lastShownIndex !== -1) {
1079				const lastCard = postCards[lastShownIndex];
1080				lastCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
1081			}
1082		}
1083		loadMoreBtn.addEventListener('click', handleLoadMore);
1084		loadMoreBtn.addEventListener('touchstart', handleLoadMore);
1085	}
1086});
1087
1088/*--------------------------------------------------------------
1089# Reviews Read More Block
1090--------------------------------------------------------------*/
1091(function() {
1092    'use strict';
1093    
1094    let initializedButtons = new Set();
1095    
1096    function initReviewsAccordion(reinit = false) {
1097        if (!reinit) {
1098            initializedButtons.clear();
1099        }
1100        
1101        const readMoreButtons = document.querySelectorAll('.review-read-more');
1102        
1103        if (readMoreButtons.length === 0) {
1104            return;
1105        }
1106        
1107        readMoreButtons.forEach((button, index) => {
1108            const reviewId = button.getAttribute('data-review-id');
1109            
1110            const parentCard = button.closest('.review-card');
1111            const isHidden = parentCard && (
1112                parentCard.classList.contains('hidden') || 
1113                parentCard.style.display === 'none' ||
1114                window.getComputedStyle(parentCard).display === 'none'
1115            );
1116            
1117            if (isHidden) {
1118                return;
1119            }
1120            
1121            if (initializedButtons.has(reviewId) && !reinit) {
1122                return;
1123            }
1124            
1125            const reviewText = document.getElementById(reviewId);
1126            
1127            if (!reviewText) {
1128                return;
1129            }
1130            
1131            const isVisible = !parentCard || window.getComputedStyle(parentCard).display !== 'none';
1132            
1133            if (!isVisible) {
1134                return;
1135            }
1136            
1137            // Batch all reads before any writes
1138            const computedStyle = window.getComputedStyle(reviewText);
1139            const maxHeight = parseFloat(computedStyle.maxHeight);
1140            const scrollHeight = reviewText.scrollHeight;
1141            
1142            const needsTruncation = scrollHeight > (maxHeight + 5);
1143            
1144            if (!needsTruncation) {
1145                button.style.display = 'none';
1146                reviewText.classList.add('no-truncation');
1147                initializedButtons.add(reviewId);
1148                return;
1149            }
1150            
1151            button.reviewTextElement = reviewText;
1152            button.buttonIndex = index;
1153            
1154            const newButton = button.cloneNode(true);
1155            newButton.reviewTextElement = reviewText;
1156            newButton.buttonIndex = index;
1157            button.parentNode.replaceChild(newButton, button);
1158            
1159            newButton.addEventListener('click', function(e) {
1160                e.preventDefault();
1161                e.stopPropagation();
1162                
1163                const currentState = this.getAttribute('aria-expanded');
1164                const isExpanded = currentState === 'true';
1165                const textElement = this.reviewTextElement;
1166                
1167                if (isExpanded) {
1168                    textElement.classList.remove('expanded');
1169                    this.setAttribute('aria-expanded', 'false');
1170                    this.textContent = 'Read More';
1171                    
1172                    const cardEl = textElement.closest('.review-card');
1173                    if (cardEl && cardEl.getBoundingClientRect().top < 0) {
1174                        cardEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
1175                    }
1176                } else {
1177                    textElement.classList.add('expanded');
1178                    this.setAttribute('aria-expanded', 'true');
1179                    this.textContent = 'Read Less';
1180                }
1181            });
1182            
1183            newButton.addEventListener('keydown', function(e) {
1184                if (e.key === 'Enter' || e.key === ' ') {
1185                    e.preventDefault();
1186                    this.click();
1187                }
1188            });
1189            
1190            initializedButtons.add(reviewId);
1191        });
1192    }
1193    
1194    function initLoadMore() {
1195        const loadMoreBtn = document.querySelector('.load-more-reviews');
1196        if (!loadMoreBtn) {
1197            return;
1198        }
1199        
1200        const reviewsGrid = document.querySelector('.reviews-grid');
1201        if (!reviewsGrid) {
1202            return;
1203        }
1204        
1205        const reviewItems = reviewsGrid.querySelectorAll('.review-item');
1206        const itemsPerPage = 6;
1207        let currentPage = 1;
1208        
1209        function showItems(page) {
1210            const end = page * itemsPerPage;
1211            
1212            reviewItems.forEach((item, index) => {
1213                if (index < end) {
1214                    item.classList.remove('hidden');
1215                    item.style.display = 'block';
1216                } else {
1217                    item.classList.add('hidden');
1218                    item.style.display = 'none';
1219                }
1220            });
1221            
1222            if (end >= reviewItems.length) {
1223                loadMoreBtn.style.display = 'none';
1224            }
1225        }
1226        
1227        showItems(currentPage);
1228        
1229        loadMoreBtn.addEventListener('click', function(e) {
1230            e.preventDefault();
1231            
1232            currentPage++;
1233            showItems(currentPage);
1234            
1235            setTimeout(() => {
1236                initReviewsAccordion(true);
1237            }, 300);
1238            
1239            const firstNewItemIndex = (currentPage - 1) * itemsPerPage;
1240            const firstNewItem = reviewItems[firstNewItemIndex];
1241            if (firstNewItem) {
1242                setTimeout(() => {
1243                    firstNewItem.scrollIntoView({ 
1244                        behavior: 'smooth', 
1245                        block: 'nearest' 
1246                    });
1247                }, 400);
1248            }
1249            
1250            const remainingItems = reviewItems.length - (currentPage * itemsPerPage);
1251            if (remainingItems > 0 && remainingItems < itemsPerPage) {
1252                loadMoreBtn.textContent = `Load More (${remainingItems} remaining)`;
1253            }
1254        });
1255    }
1256    
1257    function init() {
1258        function start() {
1259            if (document.fonts && document.fonts.ready) {
1260                document.fonts.ready.then(() => {
1261                    setTimeout(() => {
1262                        initReviewsAccordion();
1263                        initLoadMore();
1264                    }, 150);
1265                });
1266            } else {
1267                setTimeout(() => {
1268                    initReviewsAccordion();
1269                    initLoadMore();
1270                }, 300);
1271            }
1272        }
1273        
1274        if (document.readyState === 'loading') {
1275            document.addEventListener('DOMContentLoaded', start);
1276        } else {
1277            start();
1278        }
1279    }
1280    
1281    init();
1282    
1283    window.ReviewsBlock = {
1284        init: init,
1285        initReviewsAccordion: initReviewsAccordion,
1286        initLoadMore: initLoadMore
1287    };
1288    
1289})();
1290
1291/*--------------------------------------------------------------
1292# Counter Block Animation
1293--------------------------------------------------------------*/
1294(function() {
1295    'use strict';
1296
1297    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
1298
1299
1300    function easeOutQuad(t, b, c, d) {
1301        t /= d;
1302        return -c * t * (t - 2) + b;
1303    }
1304
1305    function formatNumber(num) {
1306        return Math.ceil(num).toLocaleString();
1307    }
1308
1309    function animateCounter(element, targetValue, duration) {
1310        if (prefersReducedMotion) {
1311            element.textContent = formatNumber(targetValue);
1312            element.setAttribute('aria-label', `${formatNumber(targetValue)}`);
1313            return;
1314        }
1315
1316        const startValue = 0;
1317        const startTime = performance.now();
1318        let animationId;
1319
1320        function updateCounter(currentTime) {
1321            const elapsed = currentTime - startTime;
1322            
1323            if (elapsed < duration) {
1324                const currentValue = easeOutQuad(elapsed, startValue, targetValue - startValue, duration);
1325                element.textContent = formatNumber(currentValue);
1326                animationId = requestAnimationFrame(updateCounter);
1327            } else {
1328                element.textContent = formatNumber(targetValue);
1329                element.setAttribute('aria-label', `${formatNumber(targetValue)}`);
1330                element.setAttribute('aria-live', 'off');
1331            }
1332        }
1333
1334        animationId = requestAnimationFrame(updateCounter);
1335        element.animationId = animationId;
1336    }
1337
1338    function startCounter(counter) {
1339        const targetCount = parseInt(counter.getAttribute('data-count'), 10);
1340        const duration    = parseInt(counter.getAttribute('data-duration'), 10) || 3000;
1341        if (isNaN(targetCount)) return;
1342        counter.classList.add('animated');
1343        counter.setAttribute('aria-label', 'Counting to ' + formatNumber(targetCount));
1344        animateCounter(counter, targetCount, duration);
1345    }
1346
1347    function init() {
1348        const counterSections = document.querySelectorAll('.counter-section');
1349        if (counterSections.length === 0) return;
1350
1351        const observer = new IntersectionObserver(function(entries) {
1352            entries.forEach(function(entry) {
1353                if (entry.isIntersecting) {
1354                    entry.target.querySelectorAll('.counter-number:not(.animated)')
1355                        .forEach(startCounter);
1356                    observer.unobserve(entry.target);
1357                }
1358            });
1359        }, { root: null, rootMargin: '0px', threshold: 0.1 });
1360
1361        counterSections.forEach(function(section) { observer.observe(section); });
1362    }
1363
1364    if (document.readyState === 'loading') {
1365        document.addEventListener('DOMContentLoaded', init);
1366    } else {
1367        init();
1368    }
1369
1370})();
1371
1372/*--------------------------------------------------------------
1373# Post Load More — DESHABILITADO: reemplazado por AJAX en js/ajax.js
1374--------------------------------------------------------------*/
1375/*
1376document.addEventListener('DOMContentLoaded', function() {
1377    const loadMoreBtn = document.querySelector('.load-more-posts');
1378
1379    if (loadMoreBtn) {
1380        loadMoreBtn.addEventListener('click', function() {
1381            const hiddenPosts = document.querySelectorAll('.post-item.hidden');
1382            const totalPosts = parseInt(this.getAttribute('data-total-posts'));
1383            const currentPosts = parseInt(this.getAttribute('data-current-posts'));
1384            const postsToShow = 6;
1385            let count = 0;
1386
1387            hiddenPosts.forEach(function(post) {
1388                if (count < postsToShow) {
1389                    post.classList.remove('hidden');
1390                    post.style.animation = 'fadeInUp 0.5s ease';
1391                    count++;
1392                }
1393            });
1394
1395            const newCurrentPosts = currentPosts + count;
1396            this.setAttribute('data-current-posts', newCurrentPosts);
1397
1398            if (newCurrentPosts >= totalPosts) {
1399                this.style.display = 'none';
1400            }
1401
1402            const remainingHidden = document.querySelectorAll('.post-item.hidden');
1403            if (remainingHidden.length === 0) {
1404                this.style.display = 'none';
1405            }
1406        });
1407    }
1408});
1409*/
1410
1411/*--------------------------------------------------------------
1412# Post Sticky Sidebar
1413--------------------------------------------------------------*/
1414document.addEventListener('DOMContentLoaded', function() {
1415	if (window.innerWidth < 992) return;
1416	
1417	const sidebar = document.querySelector('.sidebar-sticky');
1418	if (!sidebar) return;
1419	
1420	const sidebarWrapper = document.querySelector('.sidebar-sticky-wrapper');
1421	const breadcrumbSection = document.querySelector('.section-breadcrumbs');
1422	const postContentSection = document.querySelector('.post-content-section');
1423	
1424	if (!sidebarWrapper || !breadcrumbSection || !postContentSection) return;
1425	
1426	const stickyOffset = 100;
1427	let breadcrumbBottom, contentBottom, sidebarHeight, wrapperWidth;
1428
1429	function cacheLayout() {
1430		breadcrumbBottom = breadcrumbSection.offsetTop + breadcrumbSection.offsetHeight;
1431		contentBottom    = postContentSection.offsetTop + postContentSection.offsetHeight;
1432		sidebarHeight    = sidebar.offsetHeight;
1433		wrapperWidth     = sidebarWrapper.offsetWidth;
1434	}
1435
1436	function handleScroll() {
1437		const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
1438		const stopPoint = contentBottom - sidebarHeight - stickyOffset;
1439		
1440		if (scrollTop > breadcrumbBottom) {
1441			if (scrollTop + stickyOffset > stopPoint) {
1442				sidebar.classList.remove('is-sticky');
1443				sidebar.classList.add('is-bottom');
1444				sidebar.style.cssText = 'position:absolute;top:auto;bottom:0;width:' + wrapperWidth + 'px';
1445			} else {
1446				sidebar.classList.add('is-sticky');
1447				sidebar.classList.remove('is-bottom');
1448				sidebar.style.cssText = 'position:fixed;top:' + stickyOffset + 'px;bottom:auto;width:' + wrapperWidth + 'px';
1449			}
1450		} else {
1451			sidebar.classList.remove('is-sticky', 'is-bottom');
1452			sidebar.style.cssText = 'position:static;width:auto';
1453		}
1454	}
1455	requestAnimationFrame(function() {
1456		cacheLayout();
1457		handleScroll();
1458		window.addEventListener('scroll', handleScroll, { passive: true });
1459	});
1460	
1461	let resizeTimer;
1462	window.addEventListener('resize', function() {
1463		clearTimeout(resizeTimer);
1464		resizeTimer = setTimeout(function() {
1465			if (window.innerWidth >= 992) {
1466				cacheLayout();
1467				handleScroll();
1468			} else {
1469				sidebar.classList.remove('is-sticky', 'is-bottom');
1470				sidebar.style.cssText = 'position:static;width:auto';
1471			}
1472		}, 250);
1473	});
1474});
1475
1476/*--------------------------------------------------------------
1477# Compare Table Slider
1478--------------------------------------------------------------*/
1479(function () {
1480    function initCompareTable() {
1481        if (typeof jQuery === 'undefined' || typeof jQuery.fn.owlCarousel === 'undefined') {
1482            setTimeout(initCompareTable, 100);
1483            return;
1484        }
1485
1486        var $ = jQuery;
1487
1488        var $carousel = $('#comparison-table-slider');
1489        if ($carousel.length) {
1490            $carousel.owlCarousel({
1491                items: 1,
1492                loop: false,
1493                nav: true,
1494                dots: true,
1495                navText: [
1496                    '<span aria-label="Previous" role="button"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 640 640" fill="currentColor" aria-hidden="true"><path d="M41.4 297.4C28.9 309.9 28.9 330.1 41.4 342.6L169.4 470.6C181.9 483.2 202.2 483.2 214.7 470.6C227.2 458.1 227.2 437.8 214.7 425.4L141.3 352L576 352C593.7 352 608 337.7 608 320C608 302.3 593.7 288 576 288L141.3 288L214.6 214.6C227.1 202.1 227.1 181.8 214.6 169.3C202.1 156.8 181.8 156.8 169.3 169.3L41.4 297.4z"/></svg></span>',
1497                    '<span aria-label="Next" role="button"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 640 640" fill="currentColor" aria-hidden="true"><path d="M598.6 342.6C611.1 330.1 611.1 309.8 598.6 297.3L470.6 169.3C458.1 156.8 437.8 156.8 425.3 169.3C412.8 181.8 412.8 202.1 425.3 214.6L498.7 288L64 288C46.3 288 32 302.3 32 320C32 337.7 46.3 352 64 352L498.7 352L425.3 425.4C412.8 437.9 412.8 458.2 425.3 470.7C437.8 483.2 458.1 483.2 470.6 470.7L598.6 342.7z"/></svg></span>'
1498                ],
1499                margin: 0,
1500                stagePadding: 30,
1501                autoHeight: false,
1502                autoplay: false,
1503                smartSpeed: 400,
1504                navSpeed: 400,
1505                dotsSpeed: 400,
1506                responsive: {
1507                    0:   { items: 1, stagePadding: 30 },
1508                    480: { items: 1, stagePadding: 60 }
1509                },
1510                onInitialized: function (event) {
1511                    $carousel.find('.owl-prev, .owl-next').attr('tabindex', '0');
1512                },
1513                onChanged: function (event) {
1514                    var total = event.item.count;
1515                    var current = event.item.index + 1;
1516                    $carousel.attr('aria-label', 'Comparison, item ' + current + ' of ' + total);
1517                }
1518            });
1519
1520            $carousel.on('keydown', '.owl-prev, .owl-next', function (e) {
1521                if (e.key === 'Enter' || e.key === ' ') {
1522                    e.preventDefault();
1523                    $(this).trigger('click');
1524                }
1525            });
1526        }
1527
1528        $(document).on('click keydown', '.ct-tooltip-trigger', function (e) {
1529            if (e.type === 'click' || e.key === 'Enter' || e.key === ' ') {
1530                e.preventDefault();
1531                var $btn = $(this);
1532                var $tip = $btn.next('.ct-tooltip-content');
1533                var isOpen = $tip.hasClass('is-visible');
1534
1535                $('.ct-tooltip-content').removeClass('is-visible').attr('aria-hidden', 'true');
1536                $('.ct-tooltip-trigger').attr('aria-expanded', 'false');
1537
1538                if (!isOpen) {
1539                    $tip.addClass('is-visible').attr('aria-hidden', 'false');
1540                    $btn.attr('aria-expanded', 'true');
1541                }
1542            }
1543        });
1544
1545        $(document).on('click', function (e) {
1546            if (!$(e.target).closest('.ct-row-label').length) {
1547                $('.ct-tooltip-content').removeClass('is-visible').attr('aria-hidden', 'true');
1548                $('.ct-tooltip-trigger').attr('aria-expanded', 'false');
1549            }
1550        });
1551
1552        function syncMobileLabelHeights() {
1553            var labelsEl  = document.querySelector('.ct-mobile-labels');
1554            var firstCol  = $carousel[0] && $carousel[0].querySelector('.ct-mobile-col');
1555            if (!labelsEl || !firstCol) return;
1556
1557        
1558            var headerEl  = firstCol.querySelector('.ct-mobile-col-header');
1559            var headerH   = headerEl ? headerEl.getBoundingClientRect().height : 0;
1560
1561            var cellEls      = firstCol.querySelectorAll('.ct-mobile-cell');
1562            var cellHeights  = [];
1563            cellEls.forEach(function (el) {
1564                cellHeights.push(el.getBoundingClientRect().height);
1565            });
1566
1567            var ctaEl = firstCol.querySelector('.ct-mobile-cta');
1568            var ctaH  = ctaEl ? ctaEl.getBoundingClientRect().height : 0;
1569
1570            // --- All writes after ---
1571            var spacer = labelsEl.querySelector('.ct-mobile-label-logo-spacer');
1572            if (spacer) spacer.style.height = headerH + 'px';
1573
1574            var labelRows = labelsEl.querySelectorAll('.ct-mobile-label-row');
1575            labelRows.forEach(function (row, i) {
1576                if (cellHeights[i] !== undefined) {
1577                    row.style.height = cellHeights[i] + 'px';
1578                }
1579            });
1580
1581            var ctaSpacer = labelsEl.querySelector('.ct-mobile-label-cta-spacer');
1582            if (ctaSpacer) ctaSpacer.style.height = ctaH + 'px';
1583        }
1584
1585        
1586        var syncResizeTimer;
1587        function debouncedSync() {
1588            clearTimeout(syncResizeTimer);
1589            syncResizeTimer = setTimeout(syncMobileLabelHeights, 150);
1590        }
1591
1592     
1593        $carousel.on('initialized.owl.carousel resized.owl.carousel', function () {
1594            requestAnimationFrame(syncMobileLabelHeights);
1595        });
1596        $(window).on('resize', debouncedSync);
1597        setTimeout(syncMobileLabelHeights, 300);
1598    }
1599
1600    if (document.readyState === 'loading') {
1601        document.addEventListener('DOMContentLoaded', initCompareTable);
1602    } else {
1603        initCompareTable();
1604    }
1605})();
1606
1607/*--------------------------------------------------------------
1608# Sticky Pricing Table
1609--------------------------------------------------------------*/
1610(function () {
1611  var footer = document.getElementById('footer-pricing-table');
1612  var table  = document.getElementById('pricing-table'); 
1613
1614  if (!footer || !table) return;
1615
1616 
1617  footer.classList.remove('is-sticky');
1618
1619  var observer = new IntersectionObserver(
1620    function (entries) {
1621      entries.forEach(function (entry) {
1622      
1623        if (!entry.isIntersecting) {
1624          footer.classList.add('is-visible');
1625          footer.removeAttribute('aria-hidden');
1626        } else {
1627          footer.classList.remove('is-visible');
1628          footer.setAttribute('aria-hidden', 'true');
1629        }
1630      });
1631    },
1632    {
1633     
1634      rootMargin: '0px 0px 0px 0px',
1635      threshold: 0
1636    }
1637  );
1638
1639  observer.observe(table);
1640
1641  // Initial state: hidden
1642  footer.setAttribute('aria-hidden', 'true');
1643})();
1644/*--------------------------------------------------------------
1645# Table of Contents — scrollspy + smooth scroll
1646--------------------------------------------------------------*/
1647(function () {
1648    const toc = document.querySelector('.custom-toc');
1649    if (!toc) return;
1650
1651    const links = Array.prototype.slice.call(toc.querySelectorAll('a[href^="#"]'));
1652    if (!links.length) return;
1653
1654    // Map each TOC link to its target heading
1655    const items = links
1656        .map(function (link) {
1657            const id = decodeURIComponent(link.getAttribute('href').slice(1));
1658            const target = id ? document.getElementById(id) : null;
1659            return target ? { link: link, target: target } : null;
1660        })
1661        .filter(Boolean);
1662
1663    if (!items.length) return;
1664
1665    // Offset for a sticky/fixed header (if any)
1666    function getOffset() {
1667        const header = document.querySelector('.site-header, header');
1668        if (header) {
1669            const pos = window.getComputedStyle(header).position;
1670            if (pos === 'fixed' || pos === 'sticky') {
1671                return header.offsetHeight + 20;
1672            }
1673        }
1674        return 20;
1675    }
1676
1677    // Smooth scroll on click
1678    items.forEach(function (item) {
1679        item.link.addEventListener('click', function (e) {
1680            e.preventDefault();
1681            const top = item.target.getBoundingClientRect().top + window.pageYOffset - getOffset();
1682            window.scrollTo({ top: top, behavior: 'smooth' });
1683            history.replaceState(null, '', item.link.getAttribute('href'));
1684        });
1685    });
1686
1687    // Scrollspy: mark the current section active
1688    function setActive(target) {
1689        items.forEach(function (item) {
1690            item.link.classList.toggle('toc-active', item.target === target);
1691        });
1692    }
1693
1694    function onScroll() {
1695        // Activation line: a heading becomes active once it reaches ~70% up the viewport,
1696        // so the section highlights as soon as its heading is well into view (not only at the very top).
1697        const line = window.innerHeight * 0.7;
1698        let current = items[0].target;
1699        for (let i = 0; i < items.length; i++) {
1700            if (items[i].target.getBoundingClientRect().top <= line) {
1701                current = items[i].target;
1702            } else {
1703                break;
1704            }
1705        }
1706        setActive(current);
1707    }
1708
1709    let ticking = false;
1710    window.addEventListener('scroll', function () {
1711        if (!ticking) {
1712            window.requestAnimationFrame(function () {
1713                onScroll();
1714                ticking = false;
1715            });
1716            ticking = true;
1717        }
1718    }, { passive: true });
1719
1720    onScroll();
1721})();
1722
1723/*--------------------------------------------------------------
1724# Case study share — copy link to clipboard
1725--------------------------------------------------------------*/
1726(function () {
1727    document.addEventListener('click', function (e) {
1728        const btn = e.target.closest ? e.target.closest('.cs-share-copy') : null;
1729        if (!btn) return;
1730        const url = btn.getAttribute('data-url') || window.location.href;
1731        if (navigator.clipboard && navigator.clipboard.writeText) {
1732            navigator.clipboard.writeText(url).then(function () {
1733                btn.classList.add('copied');
1734                btn.setAttribute('aria-label', 'Link copied');
1735                setTimeout(function () {
1736                    btn.classList.remove('copied');
1737                    btn.setAttribute('aria-label', 'Copy link');
1738                }, 1500);
1739            });
1740        }
1741    });
1742})();
1743
1744/*--------------------------------------------------------------
1745# Case study floating sidebar — fixed on the left, visible within the content
1746--------------------------------------------------------------*/
1747(function () {
1748    const sidebar = document.querySelector('.cs-floating-sidebar');
1749    if (!sidebar) return;
1750
1751    const startEl = document.querySelector('.about-stats-section') || document.querySelector('.entry-content');
1752    const endEl   = document.querySelector('.entry-content');
1753    if (!startEl || !endEl) return;
1754
1755    const TOP_GAP = 120; // distance from the top when fixed
1756
1757    function isDesktop() {
1758        return window.innerWidth >= 992;
1759    }
1760
1761    function update() {
1762        if (!isDesktop()) {
1763            sidebar.classList.remove('is-visible');
1764            return;
1765        }
1766
1767        // Left position is set in CSS (left: 135px). Here we only handle visibility.
1768        // Visibility bounds: from the start of the content to its end
1769        const y      = window.pageYOffset || document.documentElement.scrollTop;
1770        const startY = startEl.getBoundingClientRect().top + y - TOP_GAP;
1771        const endY   = endEl.getBoundingClientRect().bottom + y - sidebar.offsetHeight - TOP_GAP;
1772
1773        if (y >= startY && y <= endY) {
1774            sidebar.classList.add('is-visible');
1775        } else {
1776            sidebar.classList.remove('is-visible');
1777        }
1778    }
1779
1780    let ticking = false;
1781    function onScroll() {
1782        if (!ticking) {
1783            window.requestAnimationFrame(function () {
1784                update();
1785                ticking = false;
1786            });
1787            ticking = true;
1788        }
1789    }
1790
1791    window.addEventListener('scroll', onScroll, { passive: true });
1792    window.addEventListener('resize', update);
1793    update();
1794})();
1795
1796/*--------------------------------------------------------------
1797# Pricing — billing toggle (sync entre bloques + Annually por defecto)
1798--------------------------------------------------------------*/
1799document.addEventListener('DOMContentLoaded', function () {
1800    var groups = Array.prototype.slice.call(document.querySelectorAll('.billing-toggle-group'));
1801    if (!groups.length) return;
1802
1803    var plans = document.querySelectorAll('.pricingpage #plans .plan');
1804    var syncing = false;
1805
1806    // Marca cada radio con su ciclo; a los grupos secundarios les da id/for/name únicos
1807    groups.forEach(function (group, gi) {
1808        group.querySelectorAll('input[type="radio"]').forEach(function (input) {
1809            input.setAttribute('data-cycle', input.id.indexOf('monthly') === 0 ? 'monthly' : 'annual');
1810            if (gi > 0) {
1811                var oldId = input.id;
1812                var label = group.querySelector('label[for="' + oldId + '"]');
1813                input.id = oldId + '-' + gi;
1814                if (label) label.setAttribute('for', input.id);
1815                input.name = 'billingCycle-' + gi;
1816            }
1817        });
1818    });
1819
1820    // Copia los precios calculados del primer bloque a los demás (ids duplicados)
1821    var currentCycle = 'annual';
1822
1823    function mirrorPricing() {
1824        ['proPrice', 'teamPrice', 'usercount', 'peruserprice', 'calcstorage'].forEach(function (id) {
1825            var els = document.querySelectorAll('[id="' + id + '"]');
1826            for (var i = 1; i < els.length; i++) els[i].textContent = els[0].textContent;
1827        });
1828        ['propricingbreakdown', 'teampricingbreakdown'].forEach(function (id) {
1829            var els = document.querySelectorAll('[id="' + id + '"]');
1830            for (var i = 1; i < els.length; i++) {
1831                els[i].innerHTML = els[0].innerHTML;
1832                els[i].style.display = currentCycle === 'annual' ? 'block' : 'none';
1833            }
1834        });
1835    }
1836
1837    // pricing.js calcula de forma asíncrona (detecta moneda por red); re-espejamos
1838    // cada vez que el primer bloque cambia, para no copiar valores viejos.
1839    if (window.MutationObserver) {
1840        var observer = new MutationObserver(mirrorPricing);
1841        ['proPrice', 'teamPrice', 'usercount', 'peruserprice', 'calcstorage', 'propricingbreakdown', 'teampricingbreakdown'].forEach(function (id) {
1842            var src = document.getElementById(id);
1843            if (src) observer.observe(src, { childList: true, characterData: true, subtree: true });
1844        });
1845    }
1846
1847    // En Annually cambia el período de Pro y Team; en Monthly lo restaura
1848    function updatePeriodText(cycle) {
1849        ['pro', 'team'].forEach(function (planId) {
1850            var blocks = document.querySelectorAll('.pricingpage #plans .plan[id="' + planId + '"]');
1851            blocks.forEach(function (plan) {
1852                var period = plan.querySelector('.pricing-period');
1853                if (period) {
1854                    period.textContent = cycle === 'annual'
1855                        ? 'per month, billed yearly'
1856                        : 'per month, billed monthly';
1857                }
1858            });
1859        });
1860    }
1861
1862    function applyCycle(cycle, propagate) {
1863        currentCycle = cycle;
1864        syncing = true;
1865        groups.forEach(function (group) {
1866            group.querySelectorAll('input[type="radio"]').forEach(function (input) {
1867                input.checked = input.getAttribute('data-cycle') === cycle;
1868            });
1869        });
1870        plans.forEach(function (plan) {
1871            plan.classList.remove('monthly', 'annually');
1872            plan.classList.add(cycle === 'monthly' ? 'monthly' : 'annually');
1873        });
1874        syncing = false;
1875
1876        if (propagate) {
1877            var first = groups[0].querySelector('input[data-cycle="' + cycle + '"]');
1878            if (first) first.dispatchEvent(new Event('change', { bubbles: true }));
1879        }
1880        mirrorPricing();
1881        updatePeriodText(cycle);
1882    }
1883
1884    // Captura: corre antes del stopPropagation() de pricing.js
1885    document.addEventListener('change', function (e) {
1886        if (syncing) return;
1887        var input = e.target;
1888        if (!input || !input.matches || !input.matches('.billing-toggle-group input[type="radio"]')) return;
1889        var fromFirst = groups[0].contains(input);
1890        applyCycle(input.getAttribute('data-cycle'), !fromFirst);
1891    }, true);
1892
1893    // Annually activo por defecto
1894    applyCycle('annual', true);
1895});
1896/*--------------------------------------------------------------
1897# Tablas responsive: etiquetas de cada celda desde el encabezado real
1898--------------------------------------------------------------*/
1899document.addEventListener('DOMContentLoaded', function () {
1900    document.querySelectorAll('.wp-block-table table').forEach(function (table) {
1901        var headerCells = table.querySelectorAll('thead tr:first-child th, thead tr:first-child td');
1902        var bodyRows = Array.prototype.slice.call(table.querySelectorAll('tbody tr'));
1903
1904        // Tablas insertadas sin "Header section": la primera fila del tbody hace de encabezado
1905        if (!headerCells.length && bodyRows.length) {
1906            var headerRow = bodyRows.shift();
1907            headerRow.setAttribute('data-table-header', '');
1908            headerCells = headerRow.querySelectorAll('th, td');
1909        }
1910
1911        if (!headerCells.length) return;
1912
1913        var labels = Array.prototype.map.call(headerCells, function (cell) {
1914            return cell.textContent.replace(/\s+/g, ' ').trim();
1915        });
1916
1917        bodyRows.forEach(function (row) {
1918            row.querySelectorAll('td').forEach(function (cell, index) {
1919                if (labels[index]) cell.setAttribute('data-label', labels[index]);
1920            });
1921        });
1922    });
1923});
1924

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.