PageSourceSearch

https://www.muhlenberg.edu/wp-content/themes/muhlenberg-theme/javascripts/app.js?ver=1790258127

js muhlenberg.edu collected 2026-10-01 09:09:55 UTC 60,120 bytes, 1,707 lines download raw bytes

1/*
2import '../scss/styles.scss';
3import 'bootstrap/dist/js/bootstrap.bundle.min.js';
4
5// Import mmenu CSS and JS
6import 'mmenu-js/dist/mmenu.css';
7import Mmenu from 'mmenu-js';
8*/
9
10//console.log('📄 app.js loaded - file executing');
11
12// Simple console log to check if imports work
13//console.log('🚀 Starting main.js');
14//console.log('📦 Mmenu imported:', typeof Mmenu, Mmenu);
15
16
17//navigation
18document.addEventListener('DOMContentLoaded', function() {
19  const navbar = document.querySelector('.navbar');
20  const navItems = document.querySelectorAll('.nav-item.dropdown-hover');
21  const navLinks = document.querySelectorAll('.navbar .nav-link'); // Only target nav links within the navbar
22  const megaMenus = document.querySelectorAll('.mega-menu');
23
24  let hoverTimeout;
25  let isHoveringNav = false;
26  let isHoveringMega = false;
27  let isFocusWithinNav = false;
28  let currentActiveNavIndex = -1;
29
30  // Hide all mega menus initially
31  megaMenus.forEach(menu => {
32    menu.style.opacity = '0';
33    menu.style.visibility = 'hidden';
34    menu.style.transform = 'translateX(-50%) translateY(-10px)';
35    menu.style.transition = 'all 0.15s ease';
36  });
37
38  function showMegaMenu(menuKey) {
39    clearTimeout(hoverTimeout);
40    megaMenus.forEach(menu => {
41      if (menu.dataset.menu === menuKey) {
42        menu.style.opacity = '1';
43        menu.style.visibility = 'visible';
44        menu.style.transform = 'translateX(-50%) translateY(0)';
45      } else {
46        menu.style.opacity = '0';
47        menu.style.visibility = 'hidden';
48        menu.style.transform = 'translateX(-50%) translateY(-10px)';
49      }
50    });
51    document.querySelector('.site-header')?.classList.add('menu-active');
52  }
53
54  function hideMegaMenus() {
55    hoverTimeout = setTimeout(() => {
56      if (!isHoveringNav && !isHoveringMega && !isFocusWithinNav) {
57        megaMenus.forEach(menu => {
58          menu.style.opacity = '0';
59          menu.style.visibility = 'hidden';
60          menu.style.transform = 'translateX(-50%) translateY(-10px)';
61        });
62        // Only remove active class from navbar nav links, not all nav links on the page
63        navLinks.forEach(link => link.classList.remove('active'));
64        document.querySelector('.site-header')?.classList.remove('menu-active');
65        currentActiveNavIndex = -1;
66      }
67    }, 150);
68  }
69
70  navItems.forEach((navItem, index) => {
71    const navLink = navItem.querySelector('.nav-link');
72    const menuKey = navItem.dataset.menu;
73
74    navItem.addEventListener('mouseenter', function() {
75      isHoveringNav = true;
76      showMegaMenu(menuKey);
77      // Only remove active class from navbar nav links, not all nav links on the page
78      navLinks.forEach(link => link.classList.remove('active'));
79      if (navLink) {
80        navLink.classList.add('active');
81        currentActiveNavIndex = index;
82      }
83    });
84
85    navItem.addEventListener('mouseleave', function() {
86      isHoveringNav = false;
87      hideMegaMenus();
88    });
89  });
90
91  megaMenus.forEach(menu => {
92    menu.addEventListener('mouseenter', function() {
93      isHoveringMega = true;
94      clearTimeout(hoverTimeout);
95    });
96    menu.addEventListener('mouseleave', function() {
97      isHoveringMega = false;
98      hideMegaMenus();
99    });
100  });
101
102  // Keyboard navigation support (optional, similar to your current logic)
103  navLinks.forEach((navLink, index) => {
104    navLink.addEventListener('focus', function() {
105      isFocusWithinNav = true;
106      isHoveringNav = true;
107      const menuKey = navLink.closest('.nav-item').dataset.menu;
108      showMegaMenu(menuKey);
109      // Only remove active class from navbar nav links, not all nav links on the page
110      navLinks.forEach(link => link.classList.remove('active'));
111      this.classList.add('active');
112      currentActiveNavIndex = index;
113    });
114    navLink.addEventListener('blur', function() {
115      setTimeout(() => {
116        isFocusWithinNav = false;
117        hideMegaMenus();
118      }, 50);
119    });
120  });
121});
122
123//hide navigation on scroll
124/*
125document.addEventListener('DOMContentLoaded', function() {
126  const siteHeader = document.querySelector('.site-header');
127  let lastScrollY = window.scrollY;
128  
129  function handleScroll() {
130    // Only on desktop
131    if (window.innerWidth < 992) {
132      siteHeader.classList.remove('hidden', 'scrolled');
133      return;
134    }
135    
136    const currentScrollY = window.scrollY;
137    
138    // Add scrolled class for shadow
139    if (currentScrollY > 10) {
140      siteHeader.classList.add('scrolled');
141    } else {
142      siteHeader.classList.remove('scrolled');
143    }
144    
145    // Hide/show logic
146    if (currentScrollY > 100 && currentScrollY > lastScrollY) {
147      // Scrolling down and past 100px
148      siteHeader.classList.add('hidden');
149    } else if (currentScrollY < lastScrollY) {
150      // Scrolling up
151      siteHeader.classList.remove('hidden');
152    }
153    
154    lastScrollY = currentScrollY;
155  }
156  
157  window.addEventListener('scroll', handleScroll, { passive: true });
158  window.addEventListener('resize', handleScroll);
159});       
160*/
161
162// Video Background Handler
163document.addEventListener('DOMContentLoaded', function() {
164  const heroVideo = document.querySelector('.home-header__bg-video');
165  
166  if (heroVideo) {
167    // Handle video loading and fallback
168    heroVideo.addEventListener('loadeddata', function() {
169      //console.log('✅ Hero video loaded successfully');
170      this.style.opacity = '1';
171    });
172    
173    heroVideo.addEventListener('error', function() {
174      //console.log('❌ Hero video failed to load, showing fallback image');
175      // Hide video and show fallback image
176      this.style.display = 'none';
177      const fallbackImage = this.querySelector('img');
178      if (fallbackImage) {
179        fallbackImage.style.display = 'block';
180      }
181    });
182    
183    // Desktop only: Auto-play + IntersectionObserver for performance
184    const isDesktop = window.innerWidth >= 992;
185    if (isDesktop) {
186      // Pause video when not in viewport (performance optimization)
187      const observer = new IntersectionObserver((entries) => {
188        entries.forEach(entry => {
189          if (entry.isIntersecting) {
190            heroVideo.play();
191          } else {
192            heroVideo.pause();
193          }
194        });
195      }, { threshold: 0.25 });
196      
197      observer.observe(heroVideo);
198      
199      // Ensure video plays on load (some browsers block autoplay)
200      const playVideo = () => {
201        const playPromise = heroVideo.play();
202        if (playPromise !== undefined) {
203          playPromise.catch(error => {
204            //console.log('Video autoplay prevented:', error);
205            // Video autoplay was prevented, but that's okay
206          });
207        }
208      };
209      
210      // Try to play after a short delay
211      setTimeout(playVideo, 100);
212    } else {
213      // Mobile: Keep video paused for performance - user controls with pause button
214      heroVideo.pause();
215      //console.log('✅ Mobile detected - video paused, user can play with pause button');
216    }
217  }
218});
219
220// Video pause/play control button
221document.addEventListener('DOMContentLoaded', function() {
222  const videoControl = document.querySelector('.home-header__video-control');
223  const video = document.querySelector('.home-header__bg-video');
224  
225  if (videoControl && video) {
226    // Initial state based on viewport
227    const isDesktop = window.innerWidth >= 992;
228    let isPlaying = isDesktop; // true on desktop (auto-playing), false on mobile (paused)
229    
230    // Set correct icon on load
231    if (isDesktop) {
232      // Desktop: video is auto-playing, show pause icon
233      videoControl.querySelector('i').classList.remove('fa-circle-play');
234      videoControl.querySelector('i').classList.add('fa-circle-pause');
235    } else {
236      // Mobile: video is paused, show play icon
237      videoControl.querySelector('i').classList.remove('fa-circle-pause');
238      videoControl.querySelector('i').classList.add('fa-circle-play');
239    }
240    
241    videoControl.addEventListener('click', function(e) {
242      e.preventDefault();
243      
244      if (isPlaying) {
245        // Pause the video
246        video.pause();
247        videoControl.querySelector('i').classList.remove('fa-circle-pause');
248        videoControl.querySelector('i').classList.add('fa-circle-play');
249        isPlaying = false;
250        //console.log('✅ Video paused');
251      } else {
252        // Play the video
253        video.play();
254        videoControl.querySelector('i').classList.remove('fa-circle-play');
255        videoControl.querySelector('i').classList.add('fa-circle-pause');
256        isPlaying = true;
257        //console.log('✅ Video playing');
258      }
259    });
260    
261    // Update button when video ends (auto-loop)
262    video.addEventListener('ended', function() {
263      videoControl.querySelector('i').classList.remove('fa-circle-play');
264      videoControl.querySelector('i').classList.add('fa-circle-pause');
265      isPlaying = true;
266    });
267  }
268});
269
270//hide top-bar only on scroll
271document.addEventListener('DOMContentLoaded', function() {
272  const siteHeader = document.querySelector('.site-header');
273  const topBar = document.querySelector('.top-bar');
274  let lastScrollY = window.scrollY;
275  
276  function handleScroll() {
277    // Only on desktop
278    if (window.innerWidth < 992) {
279      siteHeader.classList.remove('scrolled', 'top-bar-hidden');
280      topBar.classList.remove('hidden');
281      return;
282    }
283    
284    const currentScrollY = window.scrollY;
285    
286    // Add scrolled class for any additional styling
287    if (currentScrollY > 10) {
288      siteHeader.classList.add('scrolled');
289    } else {
290      siteHeader.classList.remove('scrolled');
291    }
292    
293    // Hide/show top-bar logic
294    if (currentScrollY > 100 && currentScrollY > lastScrollY) {
295      // Scrolling down and past 100px - hide top-bar only
296      topBar.classList.add('hidden');
297      siteHeader.classList.add('top-bar-hidden');
298    } else if (currentScrollY < lastScrollY) {
299      // Scrolling up - show top-bar
300      topBar.classList.remove('hidden');
301      siteHeader.classList.remove('top-bar-hidden');
302    }
303    
304    lastScrollY = currentScrollY;
305  }
306  
307  window.addEventListener('scroll', handleScroll, { passive: true });
308  window.addEventListener('resize', handleScroll);
309});
310
311// Accessibility: Focus trap and tab navigation for mega menu
312(function() {
313  const navItems = document.querySelectorAll('.nav-item.dropdown-hover');
314  const megaMenus = document.querySelectorAll('.mega-menu');
315  const navLinks = document.querySelectorAll('.navbar .nav-link');
316
317  function getFocusableElements(menu) {
318    return menu.querySelectorAll('a, button, input, [tabindex]:not
318([tabindex="-1"])');
319  }
320
321  function openMegaMenuByIndex(index) {
322    if (navItems[index]) {
323      navItems[index].querySelector('.nav-link').focus();
324      navItems[index].dispatchEvent(new Event('mouseenter'));
325    }
326  }
327
328  function closeAllMegaMenus() {
329    megaMenus.forEach(menu => {
330      menu.style.opacity = '0';
331      menu.style.visibility = 'hidden';
332      menu.style.transform = 'translateX(-50%) translateY(-10px)';
333    });
334    navLinks.forEach(link => link.classList.remove('active'));
335    document.querySelector('.site-header')?.classList.remove('menu-active');
336  }
337
338  function getOpenMegaMenu() {
339    return Array.from(megaMenus).find(menu => menu.style.opacity === '1');
340  }
341
342  // ESC key handler - global listener for when mega menu is open
343  document.addEventListener('keydown', function(e) {
344    if (e.key === 'Escape') {
345      const openMenu = getOpenMegaMenu();
346      if (openMenu) {
347        // Find which nav item owns this menu
348        const menuKey = openMenu.dataset.menu;
349        const ownerNavItem = Array.from(navItems).find(item => item.dataset.menu === menuKey);
350        closeAllMegaMenus();
351        if (ownerNavItem) {
352          ownerNavItem.querySelector('.nav-link').focus();
353        }
354      }
355    }
356  });
357
358  navItems.forEach((navItem, navIndex) => {
359    const menuKey = navItem.dataset.menu;
360    const megaMenu = document.querySelector(`.mega-menu[data-menu="${menuKey}"]`);
361    const navLink = navItem.querySelector('.nav-link');
362
363    // Tab from nav link into mega menu
364    navLink.addEventListener('keydown', function(e) {
365      if (e.key === 'Tab' && !e.shiftKey && megaMenu && megaMenu.style.opacity === '1') {
366        // Focus first focusable in mega menu
367        const focusables = getFocusableElements(megaMenu);
368        if (focusables.length) {
369          e.preventDefault();
370          focusables[0].focus();
371        }
372      } else if (e.key === 'Tab' && e.shiftKey && megaMenu && megaMenu.style.opacity === '1') {
373        // Shift+Tab backwards from nav link - close menu
374        closeAllMegaMenus();
375      }
376    });
377
378    if (megaMenu) {
379      megaMenu.addEventListener('keydown', function(e) {
380        const focusables = Array.from(getFocusableElements(megaMenu));
381        const first = focusables[0];
382        const last = focusables[focusables.length - 1];
383        const active = document.activeElement;
384
385        if (e.key === 'Tab') {
386          if (!e.shiftKey && active === last) {
387            // Tab from last item - move to next nav item
388            e.preventDefault();
389            if (navIndex < navItems.length - 1) {
390              closeAllMegaMenus();
391              openMegaMenuByIndex(navIndex + 1);
392            } else {
393              // Last nav item - exit to page content
394              closeAllMegaMenus();
395              setTimeout(() => {
396                let next = document.querySelector('.site-header').nextElementSibling;
397                if (next) {
398                  let nextFocusable = next.querySelector('a, button, input, [tabindex]:not([tabindex="-1"])');
399                  if (nextFocusable) nextFocusable.focus();
400                }
401              }, 10);
402            }
403          } else if (e.shiftKey && active === first) {
404            // Shift+Tab from first item - close menu and go back to nav link
405            e.preventDefault();
406            closeAllMegaMenus();
407            navLink.focus();
408          }
409        }
410      });
411    }
412  });
413})();
414
415function buildCustomFooterHTML() {
416    // Check if data exists
417    if (typeof mmFooterData === 'undefined') {
418        console.error('WordPress footer data not loaded');
419        return '';
420    }
421    
422    let html = '<div class="mm-custom-footer">';
423    
424    // Top row - Dynamic buttons from ACF repeater
425    html += '<div class="mm-footer-top-actions">';
426    
427    if (mmFooterData.mega_menu_links && mmFooterData.mega_menu_links.length > 0) {
428        mmFooterData.mega_menu_links.forEach(function(link) {
429            html += `<a href="${link.url}" class="mm-footer-btn mm-footer-btn--icon" target="${link.target}">`;
430            html += `<span>${link.title}</span>`;
431            html += '</a>';
432        });
433    }
434    
435    html += '</div>';
436    
437    // Second row - Give CTA (using ACF link field)
438    html += '<div class="mm-footer-give-bg">';
439    html += '<div class="mm-footer-give-section">';
440    html += `<a href="${mmFooterData.give.url}" class="mm-footer-cta" target="${mmFooterData.give.target}">
441               <div class="cta-content">
442                 <h6 class="cta-title">${mmFooterData.give.title}</h6>
443                 <p class="cta-description">${mmFooterData.give.description}</p>
444               </div>
445             </a>`;
446    html += '</div>';
447    html += '</div>';
448    
449    // Third row - "top" menu items from WordPress
450    html += '<div class="mm-footer-links-section">';
451    
452    if (mmFooterData.menu_items && mmFooterData.menu_items.length > 0) {
453        mmFooterData.menu_items.forEach(function(item) {
454            html += `<a href="${item.url}" class="mm-footer-link" target="${item.target}">
455                       <span>${item.title}</span>
456                       <i class="fas fa-arrow-right"></i>
457                     </a>`;
458        });
459    }
460    
461    html += '</div>';
462    html += '</div>';
463    
464    return html;
465}
466
467
468// Mobile Menu - Updated configuration for full width
469document.addEventListener('DOMContentLoaded', function() {
470
471  //console.log('📱 Initializing mobile menu...');
472  
473  const mobileToggle = document.getElementById('mobile-menu-toggle');
474  const mobileMenu = document.getElementById('mobile-menu');
475  
476  /*
477  console.log('🔍 Elements found:', {
478    toggle: !!mobileToggle,
479    menu: !!mobileMenu
480  });
481  */
482
483  if (mobileToggle && mobileMenu) {
484    try {
485      const menu = new Mmenu('#mobile-menu', {
486        // Extensions for positioning and theme
487          offCanvas: {
488            position: 'top'  // Mmenu v9 uses this instead of 'position-top' extension:contentReference[oaicite:5]{index=5}
489          },
490        
491        // Navbar configuration
492        navbar: {
493          title: ' ', // Default title
494        },
495        
496        // Navigation bars
497        navbars: [
498        // First top navbar - just for search
499          {
500            position: 'top',
501            content: [
502              '<div class="mm-custom-search">' +
503                '<form class="mm-search-form" action="/" method="GET">' +
504                  '<div class="mm-search-input-wrapper">' +
505                    '<label for="mm-search-input" class="sr-only">Search Muhlenberg</label>' +
506                    '<input type="text" id="mm-search-input" name="s" placeholder="Search Muhlenberg" class="mm-search-input" autocomplete="off">' +
507                    '<button type="submit" class="mm-search-btn" aria-label="Search">' +
508                      '<i class="fa-light fa-magnifying-glass"></i>' +
509                    '</button>' +
510                  '</div>' +
511                '</form>' +
512              '</div>'
513            ]
514          },
515        ],
516            
517      // Search field
518      /*
519      searchfield: {
520        add: true,
521        search: true,
522        submit: true,
523        placeholder: 'Search Muhlenberg'
524      },
525      */
526      
527        }, 
528        /** Configuration object (integration & accessibility) **/
529        {
530          offCanvas: {
531            // Define the "page" element selector that the menu will push/overlay
532            page: { selector: '#main-content' },
533            // Screen reader texts for accessibility
534            screenReader: {
535              closeMenu: 'Close menu',
536              closeSubmenu: 'Close submenu'
537            }
538          }
539          // No separate screenReader key here; it’s nested under offCanvas in v9 config
540        }
541      );
542
543      
544
545      //console.log('✅ mmenu initialized successfully');
546
547      // Add custom footer to all panels after initialization
548      setTimeout(() => {
549        const customFooterHTML = buildCustomFooterHTML();
550        /*const customFooterHTML = 
551          '<div class="mm-custom-footer">' +
552            // Top row - 3 buttons side by side
553            '<div class="mm-footer-top-actions">' +
554              '<a href="/visit" class="mm-footer-btn mm-footer-btn--icon">' +
555                '<i class="fas fa-map-marker-alt"></i>' +
556                '<span>Visit</span>' +
557              '</a>' +
558              '<a href="/request-info" class="mm-footer-btn mm-footer-btn--icon">' +
559                '<i class="fas fa-info-circle"></i>' +
560                '<span>Request Info</span>' +
561              '</a>' +
562              '<a href="/apply" class="mm-footer-btn mm-footer-btn--icon">' +
563                '<i class="fas fa-clipboard"></i>' +
564                '<span>Apply</span>' +
565              '</a>' +
566            '</div>' +
567            
568            // Second row - Give CTA
569            '<div class="mm-footer-give-bg">' +
570            '<div class="mm-footer-give-section">' +
571              '<a href="/give" class="mm-footer-cta">' +
572                '<div class="cta-content">' +
573                  '<h6 class="cta-title"><i class="fas fa-hand-holding-heart"></i> Give</h6>' +
574                  '<p class="cta-description">Support Muhlenberg students and programs with your gift.</p>' +
575                '</div>' +
576              '</a>' +
577            '</div>' +
578            '</div>' +
579            
580            // Third row - Black background section with arrows
581            '<div class="mm-footer-links-section">' +
582              '<a href="/bergconnect" class="mm-footer-link">' +
583                '<span>BergConnect</span>' +
584                '<i class="fas fa-arrow-right"></i>' +
585              '</a>' +
586              '<a href="/alumni-donors" class="mm-footer-link">' +
587                '<span>Alumni & Donors</span>' +
588                '<i class="fas fa-arrow-right"></i>' +
589              '</a>' +
590              '<a href="/magazine" class="mm-footer-link">' +
591                '<span>Magazine</span>' +
592                '<i class="fas fa-arrow-right"></i>' +
593              '</a>' +
594            '</div>' +
595          '</div>';*/
596
597        // Find all panels and append the footer
598        const panels = document.querySelectorAll('.mm-panel');
599        panels.forEach(panel => {
600          const ulElement = panel.querySelector('ul');
601          if (ulElement) {
602            // Insert the footer HTML after the ul element
603            ulElement.insertAdjacentHTML('afterend', customFooterHTML);
604          }
605        });
606        
607        //console.log('✅ Custom footer added to', panels.length, 'panels');
608      }, 100); // Small delay to ensure panels are fully created
609
610      // Track menu state
611      let isMenuOpen = false;
612
613      // Function to update toggle button icon
614      function updateToggleIcon(isOpen) {
615        const icon = mobileToggle.querySelector('i');
616        if (icon) {
617          if (isOpen) {
618            // Change to X/close icon
619            icon.className = 'fas fa-times'; // or 'fas fa-close' if you prefer
620          } else {
621            // Change back to hamburger
622            icon.className = 'fas fa-bars';
623          }
624        }
625      }
626
627       // Toggle button click handler
628      mobileToggle.addEventListener('click', function(e) {
629        //console.log('🔄 Toggle clicked, menu state:', isMenuOpen);
630        e.preventDefault();
631        e.stopPropagation();
632        
633        if (isMenuOpen) {
634          // Menu is open, close it
635          menu.API.close();
636          isMenuOpen = false;
637          updateToggleIcon(false);
638        } else {
639          // Menu is closed, open it
640          menu.API.open();
641          isMenuOpen = true;
642          updateToggleIcon(true);
643        }
644      });
645
646    // Simple resize checker - add this at the very end of your file
647    window.addEventListener('resize', function() {
648
649      const searchForm = document.getElementById('topBarSearchForm');
650      const searchToggle = document.getElementById('topBarSearchToggle');
651
652      // Check if we're at desktop size (992px or larger)
653      if (window.innerWidth >= 992) {
654        
655        // Hide the mobile menu if it exists
656        if (mobileMenu) {
657          menu.API.close();
658          isMenuOpen = false;
659          updateToggleIcon(false);
660        }
661
662        if (searchForm && searchForm.classList.contains('active')) {
663          searchForm.classList.remove('active');
664          searchToggle.setAttribute('aria-expanded', 'false');
665        }
666        
667        // Reset the toggle button icon if it exists
668        if (mobileToggle) {
669          const icon = mobileToggle.querySelector('i');
670          if (icon) {
671            icon.className = 'fas fa-bars';
672          }
673        }
674        // Remove any mmenu classes from body
675        document.body.classList.remove('mm-menu-opened', 'mm-wrapper--opened');
676      }
677    });
678
679      // Event listeners
680      menu.API.bind('opened', function() {
681        //console.log('📱 Menu opened');
682        document.body.classList.add('mm-menu-opened');
683      });
684
685      menu.API.bind('closed', function() {
686        //console.log('📱 Menu closed');
687        document.body.classList.remove('mm-menu-opened');
688      });
689      
690    } catch (error) {
691      //console.error('❌ Error initializing mmenu:', error);
692    }
693  } else {
694    //console.log('❌ Required elements not found');
695  }
696  
697  
698});
699
700//close announcements
701document.addEventListener('click', function(e) {
702  // Check if clicked element is the close button or within the close button
703  const closeButton = e.target.closest('.alert-dismissible .btn-close');
704  
705  if (closeButton) {
706    //console.log('✅ Announcement close button clicked');
707    
708    // Find the announcement container
709    const announcementBar = closeButton.closest('.announcement-bar');
710    const announcementContainer = closeButton.closest('#announcementContainer');
711    
712    // Remove has-bar class and CSS variable from graphic-header
713    const graphicHeader = document.querySelector('.graphic-header');
714    if (graphicHeader) {
715      graphicHeader.classList.remove('has-bar');
716      graphicHeader.style.removeProperty('--announcement-height');
717      //console.log('✅ Removed has-bar class and height variable');
718    }
719    
720    // Hide the announcement container
721    if (announcementContainer) {
722      announcementContainer.style.display = 'none';
723    }
724  }
725});
726
727// Add has-bar class to graphic-header when announcement bar is present
728document.addEventListener('DOMContentLoaded', function() {
729  const announcementContainer = document.getElementById('announcementContainer');
730  const graphicHeader = document.querySelector('.graphic-header');
731  
732  if (announcementContainer && graphicHeader) {
733    // Check if announcement is visible
734    const isVisible = announcementContainer.style.display !== 'none' && 
735                     announcementContainer.offsetHeight > 0;
736    
737    if (isVisible) {
738      // Measure the actual height of the announcement bar
739      const announcementHeight = announcementContainer.offsetHeight;
740      
741      // Set CSS variable for dynamic height adjustment
742      graphicHeader.style.setProperty('--announcement-height', `${announcementHeight}px`);
743      graphicHeader.classList.add('has-bar');
744      
745      //console.log(`✅ Announcement bar detected - height: ${announcementHeight}px`);
746    }
747  }
748  
749  // Update height on window resize (in case content reflows)
750  window.addEventListener('resize', function() {
751    if (announcementContainer && graphicHeader && graphicHeader.classList.contains('has-bar')) {
752      const announcementHeight = announcementContainer.offsetHeight;
753      graphicHeader.style.setProperty('--announcement-height', `${announcementHeight}px`);
754    }
755  });
756});
757
758
759
760//new listeners
761document.addEventListener('click', function(e) {
762  if (e.target.classList.contains('btn-close')) {
763    const pill = e.target.closest('.filter-type-pill');
764    if (pill) pill.remove();
765  }
766});
767
768
769document.addEventListener('DOMContentLoaded', function() {
770  document.querySelectorAll('select[data-multi-select]').forEach(function(el) {
771    new MultiSelect(el, {
772      placeholder: el.getAttribute('data-placeholder') || 'Select...',
773      search: el.getAttribute('data-search') === 'true',
774      selectAll: el.getAttribute('data-select-all') !== 'false',
775      listAll: el.getAttribute('data-list-all') === 'true'
776    });
777  });
778});
779
780
781
782
783// Dynamic header height calculation & unified anchor / accordion scroll offset
784document.addEventListener('DOMContentLoaded', function () {
785  const BUFFER = 10; // 10px clean breathing room below sticky navbar
786
787  /**
788   * Calculate total fixed/sticky navigation offset dynamically.
789   * Measures the visible sticky navbar height (since top-bar slides offscreen on scroll).
790   */
791  function getHeaderOffset() {
792    const navbar = document.querySelector('.site-header .navbar') || document.querySelector('.navbar');
793    const adminBar = document.getElementById('wpadminbar');
794    const announcementContainer = document.getElementById('announcementContainer');
795
796    let headerHeight = 0;
797    if (navbar && navbar.offsetHeight > 0) {
798      headerHeight = navbar.offsetHeight;
799    } else {
800      headerHeight = window.innerWidth >= 992 ? 90 : 80;
801    }
802
803    let adminBarHeight = 0;
804    if (adminBar && adminBar.offsetHeight > 0) {
805      adminBarHeight = adminBar.offsetHeight;
806    }
807
808    let announcementHeight = 0;
809    if (announcementContainer && announcementContainer.offsetHeight > 0 && window.getComputedStyle(announcementContainer).display !== 'none') {
810      const style = window.getComputedStyle(announcementContainer);
811      if (style.position === 'fixed' || style.position === 'sticky') {
812        announcementHeight = announcementContainer.offsetHeight;
813      }
814    }
815
816    return headerHeight + adminBarHeight + announcementHeight + BUFFER;
817  }
818
819  /**
820   * Synchronize CSS variables for native browser scroll-padding & scroll-margin
821   */
822  function syncScrollOffsetCSS() {
823    const totalOffset = getHeaderOffset();
824    document.documentElement.style.setProperty('--scroll-offset', `${totalOffset}px`);
825
826    const navbar = document.querySelector('.site-header .navbar') || document.querySelector('.navbar');
827    if (navbar && navbar.offsetHeight > 0) {
828      document.documentElement.style.setProperty('--site-header-height', `${navbar.offsetHeight}px`);
829    } else {
830      document.documentElement.style.setProperty('--site-header-height', `${window.innerWidth >= 992 ? 90 : 80}px`);
831    }
832
833    const adminBar = document.getElementById('wpadminbar');
834    if (adminBar) {
835      document.documentElement.style.setProperty('--wp-admin-bar-height', `${adminBar.offsetHeight}px`);
836    }
837  }
838
839  syncScrollOffsetCSS();
840  window.addEventListener('resize', syncScrollOffsetCSS, { passive: true });
841  window.addEventListener('load', syncScrollOffsetCSS);
842
843  /**
844   * Robust element finder supporting IDs with spaces, special characters, and name attributes
845   */
846  function getElementByHashOrId(idString) {
847    if (!idString) return null;
848    const cleanId = idString.replace(/^#/, '');
849    if (!cleanId) return null;
850
851    // 1. Direct getElementById (fast & exact, supports spaces like 'plant operation policies')
852    let el = document.getElementById(cleanId);
853    if (el) return el;
854
855    // Try decoded
856    try {
857      const decoded = decodeURIComponent(cleanId);
858      el = document.getElementById(decoded);
859      if (el) return el;
860    } catch (e) {}
861
862    // 2. Query selector with CSS.escape
863    try {
864      el = document.querySelector(`[id="${cleanId}"]`) ||
865           document.querySelector(`[name="${cleanId}"]`) ||
866           document.querySelector(`[data-id="${cleanId}"]`);
867      if (el) return el;
868    } catch (e) {}
869
870    return null;
871  }
872
873  /**
874   * Checks if target or any ancestor/child is an accordion/tab/collapse, and opens it.
875   * Supports:
876   * 1. Bootstrap 5 Generic Accordions (e.g. accordion_generic, button[data-bs-toggle="collapse"], .collapse)
877   * 2. Elementor Nested Accordions (<details class="e-n-accordion-item">)
878   * 3. Legacy Elementor Accordions / Toggles (.elementor-tab-title)
879   * 4. The Plus Addons Accordions / Tabs
880   * 5. Bootstrap & Elementor Tabs
881   */
882  function checkAndOpenAccordion(target) {
883    if (!target) return false;
884    let opened = false;
885
886    // --- 1. Bootstrap 5 Collapse & Generic Accordion Widgets ---
887    // A. Target contains closed accordion button(s) (e.g. #hrl is on the widget container)
888    const innerBsButtons = target.querySelectorAll?.('.accordion-button.collapsed, [data-bs-toggle="collapse"].collapsed, button[data-bs-toggle="collapse"][aria-expanded="false"]');
889    if (innerBsButtons && innerBsButtons.length > 0) {
890      innerBsButtons.forEach(btn => {
891        btn.click();
892        opened = true;
893      });
894    }
895
896    // B. Target contains closed collapse elements
897    const innerCollapses = target.querySelectorAll?.('.collapse:not(.show), .accordion-collapse:not(.show)');
898    if (innerCollapses && innerCollapses.length > 0) {
899      innerCollapses.forEach(col => {
900        if (typeof bootstrap !== 'undefined' && bootstrap.Collapse) {
901          bootstrap.Collapse.getOrCreateInstance(col, { toggle: false }).show();
902          opened = true;
903        } else {
904          col.classList.add('show');
905          opened = true;
906        }
907      });
908    }
909
910    // C. Target is itself an accordion button that is collapsed
911    if (target.matches?.('.accordion-button.collapsed, [data-bs-toggle="collapse"].collapsed, button[data-bs-toggle="collapse"][aria-expanded="false"]')) {
912      target.click();
913      opened = true;
914    }
915
916    // D. Target is inside a closed collapse element
917    const parentCollapse = target.closest?.('.collapse:not(.show), .accordion-collapse:not(.show)');
918    if (parentCollapse) {
919      if (typeof bootstrap !== 'undefined' && bootstrap.Collapse) {
920        bootstrap.Collapse.getOrCreateInstance(parentCollapse, { toggle: false }).show();
921        opened = true;
922      } else {
923        parentCollapse.classList.add('show');
924        opened = true;
925      }
926      const controllingBtn = document.querySelector(`[data-bs-target="#${parentCollapse.id}"], [href="#${parentCollapse.id}"]`);
927      if (controllingBtn) {
928        controllingBtn.classList.remove('collapsed');
929        controllingBtn.setAttribute('aria-expanded', 'true');
930      }
931    }
932
933    // E. Target has data-accordion-target
934    const accordionTargetAttr = target.getAttribute?.('data-accordion-target');
935    if (accordionTargetAttr) {
936      const collapseEl = document.getElementById('collapse-' + accordionTargetAttr) || document.getElementById(accordionTargetAttr);
937      if (collapseEl && !collapseEl.classList.contains('show')) {
938        if (typeof bootstrap !== 'undefined' && bootstrap.Collapse) {
939          bootstrap.Collapse.getOrCreateInstance(collapseEl, { toggle: false }).show();
940          opened = true;
941        }
942      }
943    }
944
945    // --- 2. Elementor Nested Accordions (details / summary) ---
946    const details = target.closest?.('details') || (target.tagName === 'DETAILS' ? target : null);
947    if (details) {
948      if (!details.open) {
949        details.open = true;
950        const summary = details.querySelector('summary');
951        if (summary) {
952          summary.setAttribute('aria-expanded', 'true');
953        }
954        opened = true;
955      }
956    }
957
958    const innerDetails = target.querySelectorAll?.('details:not([open])');
959    if (innerDetails && innerDetails.length > 0) {
960      innerDetails.forEach(d => {
961        d.open = true;
962        const summary = d.querySelector('summary');
963        if (summary) summary.setAttribute('aria-expanded', 'true');
964      });
965      opened = true;
966    }
967
968    // --- 3. Legacy Elementor & The Plus Addons Accordion/Toggles/Tabs ---
969    const triggerClasses = [
970      '.elementor-tab-title',
971      '.plus-accordion-header',
972      '.elementor-tab-mobile-title',
973      '.elementor-tab-desktop-title',
974      '.plus-tab-header'
975    ];
976
977    for (const cls of triggerClasses) {
978      const innerTriggers = target.querySelectorAll?.(`${cls}:not(.elementor-active):not(.active)`);
979      if (innerTriggers && innerTriggers.length > 0) {
980        innerTriggers.forEach(trig => {
981          trig.click();
982          opened = true;
983        });
984      }
985    }
986
987    let trigger = null;
988    for (const cls of triggerClasses) {
989      trigger = target.closest?.(cls);
990      if (trigger) break;
991    }
992
993    if (!trigger) {
994      const containerClasses = [
995        '.elementor-accordion-item',
996        '.elementor-toggle-item',
997        '.theplus-accordion-item'
998      ];
999      let container = null;
1000      for (const cls of containerClasses) {
1001        container = target.closest?.(cls);
1002        if (container) break;
1003      }
1004      if (container) {
1005        for (const cls of triggerClasses) {
1006          trigger = container.querySelector(cls);
1007          if (trigger) break;
1008        }
1009      }
1010    }
1011
1012    if (!trigger) {
1013      const contentClasses = [
1014        '.elementor-tab-content',
1015        '.plus-accordion-content',
1016        '.plus-tab-content'
1017      ];
1018      let contentArea = null;
1019      for (const cls of contentClasses) {
1020        contentArea = target.closest?.(cls);
1021        if (contentArea) break;
1022      }
1023      if (contentArea) {
1024        let sibling = contentArea.previousElementSibling;
1025        while (sibling) {
1026          for (const cls of triggerClasses) {
1027            if (sibling.matches(cls)) {
1028              trigger = sibling;
1029              break;
1030            }
1031          }
1032          if (trigger) break;
1033          sibling = sibling.previousElementSibling;
1034        }
1035      }
1036    }
1037
1038    if (trigger) {
1039      const isActive = trigger.classList.contains('elementor-active') || trigger.classList.contains('active');
1040      if (!isActive) {
1041        trigger.click();
1042        opened = true;
1043      }
1044    }
1045
1046    // --- 4. Tab Navigation (Bootstrap / Elementor Tabs) ---
1047    const tabPane = target.closest?.('.tab-pane');
1048    if (tabPane && !tabPane.classList.contains('active')) {
1049      const tabId = tabPane.id;
1050      if (tabId) {
1051        const tabTrigger = document.querySelector(`[data-bs-target="#${tabId}"], [href="#${tabId}"]`);
1052        if (tabTrigger) {
1053          tabTrigger.click();
1054          opened = true;
1055        }
1056      }
1057    }
1058
1059    return opened;
1060  }
1061
1062  /**
1063   * Helper to extract anchor target element on current page
1064   */
1065  function getSamePageAnchorTarget(link) {
1066    if (!link || !link.getAttribute) return null;
1067    const href = link.getAttribute('href');
1068    if (!href || href === '#' || href === '#!' || href.startsWith('javascript:')) return null;
1069
1070    let targetId = '';
1071    if (href.startsWith('#')) {
1072      targetId = href.slice(1);
1073    } else {
1074      try {
1075        const url = new URL(link.href, window.location.href);
1076        const isSamePath = (
1077          url.origin === window.location.origin &&
1078          url.pathname.replace(/\/+$/, '') === window.location.pathname.replace(/\/+$/, '') &&
1079          url.search === window.location.search
1080        );
1081        if (isSamePath && url.hash && url.hash.length > 1) {
1082          targetId = url.hash.slice(1);
1083        }
1084      } catch (e) {}
1085    }
1086
1087    if (!targetId) return null;
1088    const element = getElementByHashOrId(targetId);
1089    return {
1090      id: targetId,
1091      element: element
1092    };
1093  }
1094
1095  let isScrolling = false;
1096  let lastProcessedHash = '';
1097
1098  /**
1099   * Smoothly scrolls to target factoring in sticky navigation and accordion animations
1100   */
1101  function scrollToTarget(target, targetId) {
1102    if (!target) return;
1103
1104    // Open any accordions/tabs containing or associated with target
1105    const wasAccordionOpened = checkAndOpenAccordion(target);
1106
1107    function executeScroll(behavior = 'smooth') {
1108      const offset = getHeaderOffset();
1109      const elementPosition = target.getBoundingClientRect().top + window.scrollY;
1110      const targetScrollTop = Math.max(0, elementPosition - offset);
1111
1112      isScrolling = true;
1113      window.scrollTo({
1114        top: targetScrollTop,
1115        behavior: behavior
1116      });
1117
1118      if (targetId) {
1119        lastProcessedHash = '#' + targetId;
1120        if (window.location.hash !== '#' + targetId) {
1121          history.replaceState(null, null, '#' + targetId);
1122        }
1123      }
1124
1125      setTimeout(() => {
1126        isScrolling = false;
1127      }, 600);
1128    }
1129
1130    if (wasAccordionOpened) {
1131      // If accordion opened, allow slight delay for reflow before computing position
1132      setTimeout(() => {
1133        executeScroll('smooth');
1134      }, 50);
1135
1136      // Post-expansion correction passes (accounts for CSS accordion expansion timing)
1137      setTimeout(() => {
1138        const offset = getHeaderOffset();
1139        const currentTop = target.getBoundingClientRect().top;
1140        if (Math.abs(currentTop - offset) > 15) {
1141          executeScroll('smooth');
1142        }
1143      }, 250);
1144
1145      setTimeout(() => {
1146        const offset = getHeaderOffset();
1147        const currentTop = target.getBoundingClientRect().top;
1148        if (Math.abs(currentTop - offset) > 15) {
1149          executeScroll('smooth');
1150        }
1151      }, 450);
1152    } else {
1153      executeScroll('smooth');
1154    }
1155  }
1156
1157  /**
1158   * Click handler for all same-page anchor links
1159   */
1160  document.body.addEventListener('click', function (e) {
1161    const tabToggle = e.target.closest('[data-bs-toggle="tab"], [data-bs-toggle="pill"], .splide__arrow, .splide__pagination__page');
1162    if (tabToggle) return;
1163
1164    const anchor = e.target.closest('a');
1165    if (!anchor) return;
1166
1167    const anchorInfo = getSamePageAnchorTarget(anchor);
1168    if (anchorInfo && anchorInfo.element) {
1169      e.preventDefault();
1170      scrollToTarget(anchorInfo.element, anchorInfo.id);
1171    }
1172  });
1173
1174  /**
1175   * Handles URL hash on initial page load or hashchange
1176   */
1177  function processHashScroll(isInitialLoad = false) {
1178    const hash = window.location.hash;
1179    if (hash && hash.length > 1 && (isInitialLoad || hash !== lastProcessedHash)) {
1180      const decodedId = decodeURIComponent(hash.slice(1));
1181      const target = getElementByHashOrId(decodedId) || getElementByHashOrId(hash.slice(1));
1182
1183      if (target) {
1184        lastProcessedHash = hash;
1185        const delay = isInitialLoad ? 250 : 50;
1186        setTimeout(function () {
1187          scrollToTarget(target, decodedId);
1188        }, delay);
1189      }
1190    }
1191  }
1192
1193  // Handle on DOMContentLoaded (with small timeout), window load, and hashchange
1194  if (window.location.hash) {
1195    setTimeout(function() {
1196      processHashScroll(true);
1197    }, 100);
1198  }
1199
1200  window.addEventListener('load', function () {
1201    processHashScroll(true);
1202  });
1203
1204  window.addEventListener('hashchange', function () {
1205    processHashScroll(false);
1206  });
1207});
1208
1209
1210//gsap animations
1211document.addEventListener('DOMContentLoaded', function () {
1212  if (window.gsap && window.ScrollTrigger) {
1213    const gsap = window.gsap;
1214    const ScrollTrigger = window.ScrollTrigger;
1215    gsap.registerPlugin(ScrollTrigger);
1216
1217    // Disable auto-refresh on resize to prevent recalculation issues
1218    ScrollTrigger.config({
1219      autoRefreshEvents: "visibilitychange,DOMContentLoaded,load"
1220    });
1221
1222    const cards = gsap.utils.toArray(".sj-stack-container .sj-stack-card");
1223
1224    // Force normalize card heights before GSAP calculates
1225    if (cards.length > 0) {
1226      // Wait for images and content to load
1227      window.addEventListener('load', () => {
1228        cards.forEach((card) => {
1229          // Reset any inline height from WordPress/Elementor
1230          card.style.height = 'auto';
1231          card.style.minHeight = '531px';
1232        });
1233        
1234        // Refresh ScrollTrigger after height normalization
1235        ScrollTrigger.refresh();
1236      });
1237    }
1238
1239    cards.forEach((card, index) => {
1240      gsap.to(card, {
1241        scrollTrigger: {
1242          trigger: card,
1243          start: () => `top bottom-=100`,
1244          end: () => `top top+=40`,
1245          scrub: true,
1246          invalidateOnRefresh: true
1247        },
1248        ease: "none",
1249        scale: () => 1 - (cards.length - index) * 0.025
1250      });
1251
1252      ScrollTrigger.create({
1253        trigger: card,
1254        start: "center center-=200",
1255        pin: true,
1256        pinSpacing: false,
1257        endTrigger: ".stop-point",
1258        end: "top center+=500",
1259        id: 'pin',
1260        invalidateOnRefresh: true,
1261        pinType: "transform"
1262      });
1263    });
1264  } else {
1265    console.error('GSAP or ScrollTrigger not loaded!');
1266  }
1267});
1268
1269
1270// Search focus blur effect
1271/*
1272document.addEventListener('DOMContentLoaded', function() {
1273  const searchInput = document.querySelector('.swp-input--search');
1274  const homeHeader = document.querySelector('.home-header');
1275  
1276  if (searchInput && homeHeader) {
1277    // Add blur class on focus
1278    searchInput.addEventListener('focus', function() {
1279      homeHeader.classList.add('search-focused');
1280      //console.log('✅ Search focused - blur activated');
1281    });
1282    
1283    // Remove blur class on blur
1284    searchInput.addEventListener('blur', function() {
1285      homeHeader.classList.remove('search-focused');
1286      //console.log('✅ Search blurred - blur deactivated');
1287    });
1288  }
1289});
1290*/
1291// Search focus blur effect (Architectural Update for Async GCSE)
1292document.addEventListener('DOMContentLoaded', function() {
1293  const homeHeader = document.querySelector('.home-header');
1294  
1295  if (homeHeader) {
1296    // We use 'focusin' and 'focusout' because they bubble up the DOM.
1297    // This allows us to catch focus events on the Google Search input 
1298    // even though it is injected asynchronously after page load.
1299    
1300    homeHeader.addEventListener('focusin', function(e) {
1301      // Check if the element receiving focus is the Google Search input
1302      if (e.target && e.target.classList.contains('gsc-input')) {
1303        homeHeader.classList.add('search-focused');
1304        // console.log('✅ Google Search focused - blur activated');
1305      }
1306    });
1307    
1308    homeHeader.addEventListener('focusout', function(e) {
1309      // Check if the element losing focus is the Google Search input
1310      if (e.target && e.target.classList.contains('gsc-input')) {
1311        homeHeader.classList.remove('search-focused');
1312        // console.log('✅ Google Search blurred - blur deactivated');
1313      }
1314    });
1315  }
1316});
1317
1318function checkZoom() {
1319  const zoom = ((window.outerWidth - 10) / window.innerWidth) * 100;
1320  const header = document.querySelector('.site-header');
1321  
1322  if (zoom >= 400) {
1323    header.classList.add('zoom-override');
1324  } else {
1325    header.classList.remove('zoom-override');
1326  }
1327}
1328
1329window.addEventListener('resize', checkZoom);
1330checkZoom(); // Run on load
1331
1332
1333//smart quotes - recursive processing for all text nodes
1334function processAllText(element = document.body) {
1335  element.childNodes.forEach(node => {
1336    if (node.nodeType === Node.TEXT_NODE) {
1337      // Process all text content
1338      node.textContent = node.textContent
1339        .replace(/\'(\d)/g, '\u2019$1')  // Apostrophe before digits (FIRST to catch all cases)
1340        .replace(/(\W|^)"(\S)/g, '$1\u201C$2')  // Opening double quote
1341        .replace(/(\S)"(\W|$)/g, '$1\u201D$2')  // Closing double quote
1342        .replace(/(\W|^)\'(?!\d)(\S)/g, '$1\u2018$2')  // Opening single quote (but NOT before digits)
1343        .replace(/([a-z])\'([a-z])/gi, '$1\u2019$2')  // Apostrophes in contractions (We're, don't, etc)
1344        .replace(/(\S)\'(\W|$)/g, '$1\u2019$2'); // Closing single quote
1345    } else if (node.nodeType === Node.ELEMENT_NODE) {
1346      // Skip script, style, and noscript tags to avoid breaking code
1347      if (!['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.nodeName)) {
1348        processAllText(node); // Recursively process all child elements
1349      }
1350    }
1351  });
1352}
1353
1354function processSmartQuotes() {
1355  // Process entire document body - catches all text nodes regardless of element type
1356  processAllText(document.body);
1357}
1358
1359// Process on initial page load
1360document.addEventListener('DOMContentLoaded', function() {
1361  processSmartQuotes();
1362
1363  // Skip to Main Content link handler
1364  const skipLink = document.querySelector('.skip-link');
1365  const mainContent = document.getElementById('main-content');
1366
1367  if (skipLink && mainContent) {
1368    skipLink.addEventListener('click', function(e) {
1369      e.preventDefault();
1370      // Find first focusable element within main-content
1371      const focusableElements = mainContent.querySelectorAll(
1372        'a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
1373      );
1374      const firstFocusable = focusableElements[0];
1375      
1376      if (firstFocusable) {
1377        // Focus the first focusable element
1378        firstFocusable.focus();
1379      } else {
1380        // Fallback: focus main-content container
1381        mainContent.setAttribute('tabindex', '-1');
1382        mainContent.focus();
1383      }
1384      
1385      // Scroll to main content
1386      mainContent.scrollIntoView({ behavior: 'smooth', block: 'start' });
1387    });
1388  }
1389});
1390
1391// Process accordion content when it's shown (Bootstrap 5)
1392document.addEventListener('shown.bs.collapse', function(event) {
1393  // Process the newly opened accordion content
1394  processAllText(event.target);
1395});
1396
1397
1398
1399// Fix Swiper "stuck" issue - Ver 1.0
1400// Issue: Swiper carousel arrows work on first click but become unresponsive on clicks 2+
1401// Diagnosis: Check if internal state flags are stuck OR if click events aren't reaching carousel
1402// Solution: Track clicks, monitor state, attach arrow click listeners, and proactively reset state
1403
1404// ===== PAGE CAROUSEL: Initialize on page load =====
1405document.addEventListener('DOMContentLoaded', () => {
1406  // Delay to allow Elementor to finish initializing Swiper instances
1407  setTimeout(() => {
1408    initSwiperMonitoring();
1409  }, 2000); // 2 second delay for Elementor initialization
1410});
1411
1412// Also try on window load (catches late-loading carousels)
1413window.addEventListener('load', () => {
1414  setTimeout(() => {
1415    initSwiperMonitoring();
1416  }, 500);
1417});
1418
1419// ===== LIGHTBOX CAROUSEL: Initialize when lightbox opens =====
1420// Listen for Elementor lightbox events
1421
1422// Direct click listener on lightbox trigger buttons
1423document.addEventListener('click', (event) => {
1424  const lightboxTrigger = event.target.closest('[data-elementor-open-lightbox="yes"]');
1425  if (lightboxTrigger) {
1426    // Wait for lightbox to appear in DOM
1427    setTimeout(() => {
1428      const lightboxModal = document.querySelector('.elementor-lightbox');
1429      const lightboxSwiper = lightboxModal?.querySelector('.swiper');
1430      
1431      if (lightboxSwiper) {
1432        initSwiperMonitoring([lightboxSwiper]);
1433      }
1434    }, 200);
1435  }
1436}, true); // Use capture phase to catch clicks early
1437
1438document.addEventListener('elementor/lightbox/show', (event) => {
1439  // Small delay to ensure lightbox DOM is fully rendered
1440  setTimeout(() => {
1441    const lightboxModal = document.querySelector('.elementor-lightbox');
1442    const lightboxSwiper = lightboxModal?.querySelector('.swiper');
1443    
1444    if (lightboxSwiper) {
1445      // Pass just the lightbox swiper to the same monitoring function
1446      initSwiperMonitoring([lightboxSwiper]);
1447    }
1448  }, 100);
1449});
1450
1451// Also listen for alternative event names in case Elementor uses something different
1452document.addEventListener('elementor-lightbox-open', (event) => {
1453  setTimeout(() => {
1454    const lightboxModal = document.querySelector('.elementor-lightbox');
1455    const lightboxSwiper = lightboxModal?.querySelector('.swiper');
1456    if (lightboxSwiper) {
1457      initSwiperMonitoring([lightboxSwiper]);
1458    }
1459  }, 100);
1460});
1461
1462// Monitor DOM for lightbox appearance as fallback
1463const lightboxObserver = new MutationObserver(() => {
1464  const lightboxModal = document.querySelector('.elementor-lightbox.elementor-lightbox--open');
1465  if (lightboxModal && !lightboxModal.dataset.swiperMonitored) {
1466    const lightboxSwiper = lightboxModal.querySelector('.swiper');
1467    if (lightboxSwiper) {
1468      lightboxModal.dataset.swiperMonitored = 'true';
1469      initSwiperMonitoring([lightboxSwiper]);
1470    }
1471  }
1472});
1473
1474lightboxObserver.observe(document.body, {
1475  childList: true,
1476  subtree: true,
1477  attributes: true,
1478  attributeFilter: ['class']
1479});
1480
1481function initSwiperMonitoring(elements) {
1482  // If elements array passed, use those. Otherwise, scan for all .swiper elements
1483  const swiperElements = elements || document.querySelectorAll('.swiper');
1484  
1485  swiperElements.forEach((swiperEl, carouselIndex) => {
1486    // Prevent duplicate initialization on the same swiper element
1487    if (swiperEl?.dataset?.swiperMonitoringInitialized === 'true') {
1488      return;
1489    }
1490    
1491    // Try to get Swiper instance (could be stored as .swiper or accessed differently)
1492    let swiper = swiperEl?.swiper;
1493    
1494    // If not found, try looking for it in the parent (Elementor might wrap it differently)
1495    if (!swiper && swiperEl.parentElement?.swiper) {
1496      swiper = swiperEl.parentElement.swiper;
1497    }
1498    
1499    // Also check if it's on the element itself as a different property
1500    if (!swiper && swiperEl.swiper !== undefined) {
1501      swiper = swiperEl.swiper;
1502    }
1503    
1504    // Skip if we can't find swiper instance
1505    if (!swiper) {
1506      return;
1507    }
1508    
1509    let clickCount = 0;
1510    let stuckDetectionCount = 0;
1511    
1512    // ===== ARROW CLICK TRACKING =====
1513    // Look for both standard Swiper arrows AND Elementor-specific arrows
1514    const arrows = swiperEl.querySelectorAll(
1515      '.swiper-button-next, .swiper-button-prev, .elementor-swiper-button-next, .elementor-swiper-button-prev'
1516    );
1517    
1518    arrows.forEach(arrow => {
1519      arrow.addEventListener('click', (e) => {
1520        clickCount++;
1521      });
1522    });
1523    
1524    // ===== EVENT LISTENERS =====
1525    // Reset state when transitions complete
1526    swiper.on('slideChangeTransitionEnd', () => {
1527      resetSwiperState(swiper, carouselIndex);
1528    });
1529    
1530    swiper.on('slideChange', () => {
1531      resetSwiperState(swiper, carouselIndex);
1532    });
1533
1534    // Update Elementor lightbox captions when slides change
1535    const isLightboxSwiper = !!swiperEl.closest('.elementor-lightbox');
1536    if (isLightboxSwiper) {
1537      const syncCaption = () => updateLightboxCaption(swiper);
1538      swiper.on('slideChangeTransitionEnd', syncCaption);
1539      swiper.on('slideChange', syncCaption);
1540      setTimeout(syncCaption, 0);
1541    }
1542    
1543    // ===== PROACTIVE STATE MONITOR =====
1544    // Check every 500ms if carousel appears stuck (animating flag locked)
1545    const existingIntervalId = swiperEl.dataset.swiperMonitoringIntervalId;
1546    if (existingIntervalId) {
1547      clearInterval(Number(existingIntervalId));
1548    }
1549
1550    const intervalId = setInterval(() => {
1551      if (swiper.animating) {
1552        setTimeout(() => {
1553          if (swiper.animating) {
1554            // State appears stuck - flag hasn't changed in 400ms
1555            resetSwiperState(swiper, carouselIndex);
1556          }
1557        }, 400);
1558      }
1559    }, 500);
1560
1561    swiperEl.dataset.swiperMonitoringIntervalId = String(intervalId);
1562    swiperEl.dataset.swiperMonitoringInitialized = 'true';
1563    
1564    // Update params for safety
1565    swiper.params.preventInteractionOnTransition = false;
1566    swiper.params.simulateTouch = false;
1567    swiper.params.observer = true;
1568    swiper.params.observeParents = true;
1569  });
1570}
1571
1572// Helper function to reset all stuck state flags
1573function resetSwiperState(swiper, carouselIndex) {
1574  const beforeState = {
1575    animating: swiper.animating,
1576    allowSlideNext: swiper.allowSlideNext,
1577    allowSlidePrev: swiper.allowSlidePrev,
1578    allowTouchMove: swiper.allowTouchMove
1579  };
1580  
1581  // Clear animation flag
1582  swiper.animating = false;
1583  
1584  // Reset transition styles that may be lingering
1585  if (swiper.wrapperEl) {
1586    swiper.wrapperEl.style.transitionDuration = '0ms';
1587    swiper.wrapperEl.style.transform = swiper.wrapperEl.style.transform;
1588  }
1589  
1590  // Re-enable all interaction flags
1591  swiper.allowSlideNext = true;
1592  swiper.allowSlidePrev = true;
1593  swiper.allowTouchMove = true;
1594  
1595  // Trigger lazy loading for current and adjacent slides
1596  if (swiper.lazy) {
1597    try {
1598      swiper.lazy.load();
1599    } catch (e) {
1600    }
1601  } else {
1602    // Try manual approach if .lazy doesn't exist
1603    const images = swiper.el.querySelectorAll('img.swiper-lazy');
1604    images.forEach(img => {
1605      if (img.dataset.src && !img.src) {
1606        img.src = img.dataset.src;
1607      }
1608    });
1609  }
1610  
1611  // Reconcile internal state
1612  swiper.update();
1613}
1614
1615function updateLightboxCaption(swiper) {
1616  const lightboxModal = swiper?.el?.closest('.elementor-lightbox');
1617  if (!lightboxModal) return;
1618
1619  const footer = lightboxModal.querySelector('.elementor-slideshow__footer');
1620  const titleEl = footer?.querySelector('.elementor-slideshow__title');
1621  const descEl = footer?.querySelector('.elementor-slideshow__description');
1622  if (!titleEl && !descEl) return;
1623
1624  let activeSlide = swiper.slides?.[swiper.activeIndex];
1625  const realIndex = typeof swiper.realIndex === 'number' ? swiper.realIndex : null;
1626  if (realIndex !== null && swiper.slides?.length) {
1627    const realSlide = Array.from(swiper.slides).find(slide => {
1628      const slideIndex = slide.getAttribute('data-swiper-slide-index');
1629      return slideIndex !== null && Number(slideIndex) === realIndex;
1630    });
1631    if (realSlide) activeSlide = realSlide;
1632  }
1633
1634  if (!activeSlide) return;
1635
1636  let title = activeSlide.getAttribute('data-title') || activeSlide.dataset?.title || '';
1637  let description = activeSlide.getAttribute('data-description') || activeSlide.dataset?.description || '';
1638
1639  const mediaEl = activeSlide.querySelector('[data-caption], [data-title], [data-description], img, video, iframe');
1640  if (mediaEl) {
1641    title = title || mediaEl.getAttribute('data-title') || mediaEl.getAttribute('title') || mediaEl.getAttribute('alt') || '';
1642    description = description || mediaEl.getAttribute('data-description') || mediaEl.getAttribute('data-caption') || '';
1643  }
1644
1645  if (titleEl) titleEl.textContent = title;
1646  if (descEl) descEl.textContent = description;
1647}
1648
1649
1650// ===== Search Toggle =====
1651document.addEventListener('DOMContentLoaded', () => {
1652  const searchToggle = document.getElementById('topBarSearchToggle');
1653  const searchForm = document.getElementById('topBarSearchForm');
1654  const navbarMain = document.getElementById('navbarMain'); // Target the main nav wrapper
1655
1656  if (searchToggle && searchForm) {
1657    // 1. Toggle Open and Focus
1658    searchToggle.addEventListener('click', (e) => {
1659      e.preventDefault();
1660      
1661      // Check if it's already open
1662      if (searchForm.classList.contains('active')) {
1663        // CLOSE STATE
1664        searchForm.classList.remove('active');
1665        searchToggle.setAttribute('aria-expanded', 'false');
1666        if (navbarMain) navbarMain.classList.remove('no-bg'); 
1667      } else {
1668        // OPEN STATE
1669        searchForm.classList.add('active');
1670        searchToggle.setAttribute('aria-expanded', 'true');
1671        if (navbarMain) navbarMain.classList.add('no-bg'); 
1672        
1673        // Dynamically find Google's injected input field and focus it
1674        setTimeout(() => { 
1675          const googleInput = searchForm.querySelector('input.gsc-input');
1676          if (googleInput) {
1677            googleInput.focus(); 
1678          }
1679        }, 100);
1680      }
1681    });
1682
1683    // 2. The "Click Outside" Closer
1684    document.addEventListener('mousedown', (e) => {
1685      // Check if the click was NOT on the form or the toggle button
1686      const isClickInside = searchForm.contains(e.target) || searchToggle.contains(e.target);
1687      
1688      if (!isClickInside && searchForm.classList.contains('active')) {
1689        searchForm.classList.remove('active');
1690        searchToggle.setAttribute('aria-expanded', 'false');
1691        if (navbarMain) navbarMain.classList.remove('no-bg'); 
1692      }
1693    });
1694
1695    // 3. Accessibility: Close on ESC key 
1696    // ADDED 'true' FOR CAPTURE PHASE TO OVERRIDE GOOGLE'S SCRIPT
1697    document.addEventListener('keydown', (e) => {
1698      // If ESC is pressed AND the search menu is currently open
1699      if (e.key === 'Escape' && searchForm.classList.contains('active')) {
1700        searchForm.classList.remove('active');
1701        searchToggle.setAttribute('aria-expanded', 'false');
1702        if (navbarMain) navbarMain.classList.remove('no-bg'); 
1703        searchToggle.focus(); // Return focus to the magnifying glass
1704      }
1705    }, true); 
1706  }
1707});�

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.