PageSourceSearch

https://www.jesuiten.org/_assets/b4d01d375a6833188ae8909cf4264f83/Js/scripts.js?1790690182

js jesuiten.org collected 2026-10-02 08:21:59 UTC 80,640 bytes, 2,228 lines download raw bytes

1
2const body=document.body;
3
4// --- Sticky Header
5(function(){
6  const header = document.querySelector('.site-header');
7  const spacer = document.getElementById('header-spacer');
8  if (!header || !spacer) return;
9  
10  const navRow = header.querySelector('.nav-row');
11  const normalHeight = navRow ? navRow.offsetHeight : 180;
12  const stickyHeight = 60;
13  let isSticky = false;
14  
15  function update() {
16    const scrollY = window.scrollY || document.documentElement.scrollTop;
17    const shouldBeSticky = scrollY > 150;
18    
19    if (shouldBeSticky !== isSticky) {
20      isSticky = shouldBeSticky;
21      if (shouldBeSticky) {
22        header.classList.add('is-sticky');
23        spacer.style.height = normalHeight + 'px';
24      } else {
25        header.classList.remove('is-sticky');
26        spacer.style.height = '0px';
27      }
28    }
29  }
30  
31  let raf = null;
32  window.addEventListener('scroll', function() {
33    if (!raf) {
34      raf = requestAnimationFrame(function() {
35        update();
36        raf = null;
37      });
38    }
39  }, {passive: true});
40  
41  update();
42})();
43
44// --- Position Mitmachen button between primary-nav and actions
45(function(){
46  function init() {
47    const mitmachenBtn = document.querySelector('.nav-mitmachen');
48    const primaryNav = document.querySelector('.primary-nav');
49    const actions = document.querySelector('.header-actions');
50    const navRow = document.querySelector('.nav-row');
51
52    if (!mitmachenBtn || !primaryNav || !actions || !navRow) return;
53
54    function positionButton() {
55      if (window.innerWidth < 1161) {
56        mitmachenBtn.style.left = '';
57        mitmachenBtn.style.right = '';
58        return;
59      }
60
61      const navRowRect = navRow.getBoundingClientRect();
62      const primaryNavRect = primaryNav.getBoundingClientRect();
63      const actionsRect = actions.getBoundingClientRect();
64
65      // Calculate the right edge of primary-nav and left edge of actions
66      const primaryNavRight = primaryNavRect.right - navRowRect.left;
67      const actionsLeft = actionsRect.left - navRowRect.left;
68
69      // Calculate midpoint
70      const midpoint = (primaryNavRight + actionsLeft) / 2;
71
72      // Position button at midpoint, centered on itself
73      const buttonWidth = mitmachenBtn.offsetWidth;
74      mitmachenBtn.style.left = (midpoint - buttonWidth / 2) + 'px';
75      mitmachenBtn.style.right = '';
76    }
77
78    // Position on load and resize
79    positionButton();
80    mitmachenBtn.classList.add('nav-mitmachen--ready');
81    window.addEventListener('resize', positionButton);
82
83    // Async script may execute before stylesheet/fonts are applied — the initial
84    // measurement is then wrong. Re-measure once everything has finished loading.
85    window.addEventListener('load', positionButton);
86    if (document.fonts && document.fonts.ready) {
87      document.fonts.ready.then(positionButton);
88    }
89
90    // Also recalculate when header becomes sticky/unsticky
91    const header = document.querySelector('.site-header');
92    if (header) {
93      const observer = new MutationObserver(positionButton);
94      observer.observe(header, { attributes: true, attributeFilter: ['class'] });
95    }
96  }
97
98  if (document.readyState === 'loading') {
99    document.addEventListener('DOMContentLoaded', init);
100  } else {
101    init();
102  }
103})();
104
105// --- Search overlay
106const searchOpen=document.getElementById('searchOpen');
107const searchCloseBtn=document.getElementById('searchClose');
108const searchOverlay=document.getElementById('searchOverlay');
109function openSearch(){ if(!searchOverlay) return; searchOverlay.setAttribute('aria-hidden','false'); body.classList.add('no-scroll'); }
110function closeSearch(){ if(!searchOverlay) return; searchOverlay.setAttribute('aria-hidden','true'); body.classList.remove('no-scroll'); }
111searchOpen?.addEventListener('click',openSearch,{passive:true});
112searchCloseBtn?.addEventListener('click',closeSearch,{passive:true});
113searchOverlay?.addEventListener('click',e=>{ if(e.target.matches('[data-close="search"]')) closeSearch(); },{passive:true});
114document.addEventListener('keydown',e=>{ if(e.key==='Escape'&&searchOverlay?.getAttribute('aria-hidden')==='false') closeSearch(); });
115
116// --- Basic sliders (multi-instance)
117(function(){
118  document.querySelectorAll('.basic-slider').forEach(section => {
119    const scroller = section.querySelector('.basic-scroll');
120    const track = section.querySelector('.basic-track');
121    const left = section.querySelector('.basic-btn[data-basic-dir="left"]');
122    const right = section.querySelector('.basic-btn[data-basic-dir="right"]');
123    if(!scroller || !track || !left || !right) return;
124
125    let unit = 0;
126    let peek = 0;
127
128    const measure = ()=>{
129      const firstCard = track.querySelector('.basic-card');
130      if(!firstCard) return;
131      const style = window.getComputedStyle(track);
132      const gap = parseFloat(style.columnGap || style.gap || '0');
133      unit = firstCard.getBoundingClientRect().width + gap;
134      peek = Math.max(24, Math.round(unit * 0.25)); // keep a small preview visible
135    };
136
137    const update = ()=>{
138      const max = scroller.scrollWidth - scroller.clientWidth;
139      left.disabled = scroller.scrollLeft <= 0;
140      right.disabled = scroller.scrollLeft >= max - 1;
141    };
142
143    const nudge = dir => {
144      const max = scroller.scrollWidth - scroller.clientWidth;
145      if(!unit){ measure(); }
146
147      // slide by nearly a viewport, leaving a small peek of the next/prev card
148      const stride = Math.max(unit, scroller.clientWidth - peek);
149      const current = scroller.scrollLeft;
150      let target = dir==='right'
151        ? Math.min(max, current + stride)
152        : Math.max(0, current - stride);
153
154      // snap to card boundary while keeping the initial "cut on right" behavior
155      if(target !== max && unit){
156        target = Math.max(0, Math.floor(target / unit) * unit);
157      }
158
159      scroller.scrollTo({ left: target, behavior: 'smooth' });
160    };
161
162    left.addEventListener('click', ()=> nudge('left'), {passive:true});
163    right.addEventListener('click', ()=> nudge('right'), {passive:true});
164    scroller.addEventListener('scroll', update, {passive:true});
165    window.addEventListener('resize', ()=>{ measure(); update(); }, {passive:true});
166    // Karten nachträglich eingefügt (z. B. news-slider.js) → neu vermessen
167    section.addEventListener('basic-slider:refresh', ()=>{ measure(); update(); }, {passive:true});
168
169    measure();
170    update();
171  });
172})();
173//
174
175// --- Drawer (mobile)
176(function(){
177  const drawer=document.getElementById('mobileDrawer');
178  const drawerToggle=document.getElementById('drawerToggle');
179  const drawerCloseFloating=document.getElementById('drawerCloseFloating');
180
181  function openDrawer(){
182    if(!drawer) return;
183    drawer.setAttribute('aria-hidden','false');
184    drawerToggle?.setAttribute('aria-expanded','true');
185    body.classList.add('no-scroll');
186    if(window.matchMedia('(max-width:1160px)').matches){ 
187      setTimeout(function(){
188        drawerCloseFloating?.classList.add('is-visible');
189      }, 240);
190    }
191  }
192  function closeDrawer(){
193    if(!drawer) return;
194    drawer.setAttribute('aria-hidden','true');
195    drawerToggle?.setAttribute('aria-expanded','false');
196    body.classList.remove('no-scroll');
197    drawerCloseFloating?.classList.remove('is-visible');
198    drawerToggle?.focus();
199  }
200  const openHandler = ()=>{ const isOpen = drawer?.getAttribute('aria-hidden')==='false'; isOpen ? closeDrawer() : openDrawer(); };
201  drawerToggle?.addEventListener('click', openHandler, {passive:true});
202  drawerToggle?.addEventListener('touchstart', e=>{ e.preventDefault(); openHandler(); }, {passive:false});
203  drawerCloseFloating?.addEventListener('click', closeDrawer, {passive:true});
204  drawer?.addEventListener('click',e=>{ if(e.target.matches('[data-close="drawer"]')) closeDrawer(); },{passive:true});
205  document.addEventListener('keydown',e=>{ if(e.key==='Escape'&&drawer?.getAttribute('aria-hidden')==='false') closeDrawer(); });
206
207  // Auto-close drawer when resizing to desktop
208  const mqDesktop = window.matchMedia('(min-width: 1161px)');
209  mqDesktop.addEventListener('change', (ev)=>{ if(ev.matches && drawer?.getAttribute('aria-hidden')==='false') closeDrawer(); });
210
211  // Drawer menu animations
212  document.querySelectorAll('details.m-acc').forEach(details => {
213    let isAnimating = false;
214
215    details.addEventListener('click', function(e) {
216      // If clicking on a link, allow it to work normally (follow href)
217      if (e.target.closest('a')) {
218        return;
219      }
220      
221      // Only handle clicks on summary element
222      if (!e.target.closest('summary')) {
223        return;
224      }
225      if (isAnimating) {
226       // e.preventDefault();
227       // return;
228      }
229      
230      if (this.open) {
231        // Closing - prevent default and animate out
232        e.preventDefault();
233        isAnimating = true;
234        this.classList.add('closing');
235        
236        // Close after animation
237        setTimeout(() => {
238          this.open = false;
239          this.classList.remove('closing');
240          isAnimating = false;
241        }, 250);
242      } else {
243        // Opening - allow default behavior
244        this.classList.remove('closing');
245      }
246    });
247  });
248})();
249
250// Newsletter card click handlers and sync with form checkboxes
251(function() {
252  // Find the container (nl-grid or form) for an element
253  function findContainer(element) {
254    return element.closest('.nl-grid') || element.closest('form') || element.closest('section.newsletter');
255  }
256  
257  // Find form checkbox by value within a specific container
258  function findFormCheckboxByValue(value, container) {
259    if (!container) return null;
260    return container.querySelector(`input[type="checkbox"][name*="multicheckbox"][value="${value}"]`);
261  }
262  
263  // Find nl-card by form checkbox value within a specific container
264  function findCardByFormValue(formValue, container) {
265    if (!container) return null;
266    return container.querySelector(`.nl-card[data-form-value="${formValue}"]`);
267  }
268  
269  // Sync nl-card checkbox with form checkbox
270  function syncCardToForm(card) {
271    const formValue = card.getAttribute('data-form-value');
272    if (!formValue) return;
273    
274    const container = findContainer(card);
275    const formCheckbox = findFormCheckboxByValue(formValue, container);
276    const cardCheckbox = card.querySelector('input[type="checkbox"]');
277    
278    if (formCheckbox && cardCheckbox) {
279      formCheckbox.checked = cardCheckbox.checked;
280    }
281  }
282  
283  // Sync form checkbox with nl-card checkbox
284  function syncFormToCard(formCheckbox) {
285    const formValue = formCheckbox.value;
286    if (!formValue) return;
287    
288    const container = findContainer(formCheckbox);
289    const card = findCardByFormValue(formValue, container);
290    const cardCheckbox = card?.querySelector('input[type="checkbox"]');
291    
292    if (cardCheckbox) {
293      cardCheckbox.checked = formCheckbox.checked;
294    }
295  }
296  
297  // Set up nl-card checkboxes
298  document.querySelectorAll('.nl-card[data-checkbox]').forEach(card => {
299    const checkboxId = card.getAttribute('data-checkbox');
300    // Find checkbox within the same container as the card to avoid conflicts
301    const container = findContainer(card);
302    const checkbox = container ? container.querySelector(`#${checkboxId}`) : document.getElementById(checkboxId);
303    const label = card.querySelector(`label[for="${checkboxId}"]`);
304    
305    if (checkbox) {
306      // Card click handler - toggle when clicking anywhere on the card (except checkbox/label)
307      card.addEventListener('click', (e) => {
308        // If clicking directly on checkbox or label, let native behavior handle it
309        // The change event will trigger syncCardToForm
310        if (e.target === checkbox || e.target === label || e.target.closest('label') === label) {
311          return;
312        }
313        // Clicking elsewhere on card - manually toggle
314        checkbox.checked = !checkbox.checked;
315        syncCardToForm(card);
316      });
317      
318      // Checkbox change handler - sync whenever checkbox state changes
319      // This fires for: direct checkbox clicks, label clicks, and card clicks
320      checkbox.addEventListener('change', function() {
321        syncCardToForm(card);
322      });
323    }
324  });
325  
326  // Set up form checkboxes
327  document.querySelectorAll('input[type="checkbox"][name*="multicheckbox"]').forEach(formCheckbox => {
328    formCheckbox.addEventListener('change', function() {
329      syncFormToCard(this);
330    });
331  });
332})();
333
334// Newsletter: Validierung bei Klick auf .nl-btn (E-Mail + mind. eine Checkbox)
335function isValidEmail(value) {
336  if (!value || typeof value !== 'string') return false;
337  const trimmed = value.trim();
338  if (!trimmed) return false;
339  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
340  return emailRegex.test(trimmed);
341}
342
343document.addEventListener('submit', function(e) {
344  const form = e.target;
345  if (!form || !form.querySelector || !form.querySelector('.nl-btn')) return;
346  const emailInput = form.querySelector('.nl-input[type="email"]') || form.querySelector('input[name*="email"]');
347  if (emailInput) {
348    const email = (emailInput.value || '').trim();
349    if (!email) {
350      e.preventDefault();
351      alert('Bitte geben Sie Ihre E-Mail-Adresse ein.');
352      emailInput.focus();
353      return;
354    }
355    if (!isValidEmail(email) || (emailInput.checkValidity && !emailInput.checkValidity())) {
356      e.preventDefault();
357      alert('Bitte geben Sie eine gültige E-Mail-Adresse ein.');
358      emailInput.focus();
359      return;
360    }
361  }
362  const checkboxContainer = form.closest('.nl-grid') || form.closest('.newsletter') || form;
363  const selectableCheckboxes = checkboxContainer.querySelectorAll('.nl-card input[type="checkbox"]');
364  if (selectableCheckboxes.length > 0) {
365    const checked = checkboxContainer.querySelectorAll('.nl-card input[type="checkbox"]:checked');
366    if (checked.length === 0) {
367      e.preventDefault();
368      alert('Bitte wählen Sie mindestens einen Newsletter aus.');
369      return;
370    }
371  }
372}, true);
373
374// Newsletter form submission
375const newsletterForm = document.getElementById('newsletterForm');
376if (newsletterForm) {
377  newsletterForm.addEventListener('submit', function(e) {
378    e.preventDefault();
379    const email = this.querySelector('.nl-input[type="email"]').value;
380    const anrede = this.querySelector('select[name="anrede"]') ? this.querySelector('select[name="anrede"]').value : '';
381    const titel = this.querySelector('select[name="titel"]') ? this.querySelector('select[name="titel"]').value : '';
382    const vorname = this.querySelector('input[name="vorname"]') ? this.querySelector('input[name="vorname"]').value : '';
383    const nachname = this.querySelector('input[name="nachname"]') ? this.querySelector('input[name="nachname"]').value : '';
384    const container = this.closest('.nl-grid') || this.closest('.newsletter') || document;
385    const selectableCheckboxes = container.querySelectorAll('.nl-card input[type="checkbox"]');
386    const checked = Array.from(container.querySelectorAll('.nl-card input[type="checkbox"]:checked')).map(cb => cb.id);
387    if (selectableCheckboxes.length > 0 && checked.length === 0) {
388      alert('Bitte wählen Sie mindestens einen Newsletter aus.');
389      return;
390    }
391    let alertText = 'E-Mail: ' + email + '\n\n';
392    if (anrede) alertText += 'Anrede: ' + anrede + '\n';
393    if (titel) alertText += 'Titel: ' + titel + '\n';
394    if (vorname) alertText += 'Vorname: ' + vorname + '\n';
395    if (nachname) alertText += 'Nachname: ' + nachname + '\n';
396    alertText += '\nNewsletter IDs: ' + checked.join(', ');
397    alert(alertText);
398  });
399}
400
401// Newsletter form submission for content form
402const newsletterFormContent = document.getElementById('newsletterFormContent');
403if (newsletterFormContent) {
404  newsletterFormContent.addEventListener('submit', function(e) {
405    e.preventDefault();
406    const email = this.querySelector('.nl-input[type="email"]').value;
407    const anrede = this.querySelector('select[name="anrede"]') ? this.querySelector('select[name="anrede"]').value : '';
408    const titel = this.querySelector('select[name="titel"]') ? this.querySelector('select[name="titel"]').value : '';
409    const vorname = this.querySelector('input[name="vorname"]') ? this.querySelector('input[name="vorname"]').value : '';
410    const nachname = this.querySelector('input[name="nachname"]') ? this.querySelector('input[name="nachname"]').value : '';
411    const container = this.closest('.nl-grid') || this.closest('.newsletter') || document;
412    const selectableCheckboxes = container.querySelectorAll('.nl-card input[type="checkbox"]');
413    const checked = Array.from(container.querySelectorAll('.nl-card input[type="checkbox"]:checked')).map(cb => cb.id);
414    if (selectableCheckboxes.length > 0 && checked.length === 0) {
415      alert('Bitte wählen Sie mindestens einen Newsletter aus.');
416      return;
417    }
418    let alertText = 'E-Mail: ' + email + '\n\n';
419    if (anrede) alertText += 'Anrede: ' + anrede + '\n';
420    if (titel) alertText += 'Titel: ' + titel + '\n';
421    if (vorname) alertText += 'Vorname: ' + vorname + '\n';
422    if (nachname) alertText += 'Nachname: ' + nachname + '\n';
423    alertText += '\nNewsletter IDs: ' + checked.join(', ');
424    alert(alertText);
425  });
426}
427
428// Show additional fields when email input is focused
429const nlEmailInputs = document.querySelectorAll('.nl-input[type="email"]');
430nlEmailInputs.forEach(function(emailInput) {
431  emailInput.addEventListener('focus', function() {
432    const form = this.closest('.nl-form');
433    if (form) {
434      const additionalFields = form.querySelector('.nl-additional-fields');
435      const closeLink = form.querySelector('.nl-close-link');
436      if (additionalFields) {
437        additionalFields.classList.remove('nl-additional-fields--hidden');
438      }
439      if (closeLink) {
440        closeLink.classList.remove('nl-close-link--hidden');
441      }
442    }
443  });
444});
445
446// Hide additional fields when close link is clicked
447const nlCloseLinks = document.querySelectorAll('.nl-close-link');
448nlCloseLinks.forEach(function(closeLink) {
449  closeLink.addEventListener('click', function(e) {
450    e.preventDefault();
451    const form = this.closest('.nl-form');
452    if (form) {
453      const additionalFields = form.querySelector('.nl-additional-fields');
454      if (additionalFields) {
455        additionalFields.classList.add('nl-additional-fields--hidden');
456      }
457      this.classList.add('nl-close-link--hidden');
458    }
459  });
460});
461
462// Handle select color based on selected value (first option = gray, others = black)
463const nlSelects = document.querySelectorAll('select.nl-input');
464nlSelects.forEach(function(select) {
465  function updateSelectColor() {
466    if (select.value === '') {
467      select.classList.add('nl-input--default');
468    } else {
469      select.classList.remove('nl-input--default');
470    }
471  }
472  updateSelectColor(); // Set initial state
473  select.addEventListener('change', updateSelectColor);
474});
475
476// Generic "load more" — .ev-more buttons; config from data-* or section context (no HTML required)
477function getLoadMoreConfig(button) {
478  if (button.dataset.loadMoreHiddenClass) {
479    return {
480      hiddenClass: button.dataset.loadMoreHiddenClass,
481      step: Number.parseInt(button.dataset.loadMoreStep || '0', 10),
482      scopeSelector: button.dataset.loadMoreScope || null,
483      hideMode: button.dataset.loadMoreHide || 'self'
484    };
485  }
486  if (button.closest('.person-profil-articles-news')) {
487    return {
488      hiddenClass: 'person-profil-articles-news__card--hidden',
489      step: 5,
490      scopeSelector: '.person-profil-articles-news',
491      hideMode: 'self'
492    };
493  }
494  if (button.closest('.person-profil-subsection--jesuiten')) {
495    return {
496      hiddenClass: 'person-profil-jesuiten-card--hidden',
497      step: 3,
498      scopeSelector: '.person-profil-subsection--jesuiten',
499      hideMode: 'self'
500    };
501  }
502  return {
503    hiddenClass: 'ev-item--hidden',
504    step: 6,
505    scopeSelector: '.ev',
506    hideMode: 'self'
507  };
508}
509
510document.querySelectorAll('.ev-more').forEach(function(button) {
511  const config = getLoadMoreConfig(button);
512  const hiddenClass = config.hiddenClass;
513  const step = config.step;
514  const hideMode = config.hideMode;
515  const scope = config.scopeSelector
516    ? (button.closest(config.scopeSelector) || document)
517    : (button.closest('section') || document);
518
519  const getHiddenItems = function() {
520    return scope.querySelectorAll('.' + hiddenClass);
521  };
522
523  const getHideTarget = function() {
524    if (hideMode.startsWith('closest:')) {
525      const selector = hideMode.slice('closest:'.length).trim();
526      return selector ? (button.closest(selector) || button) : button;
527    }
528    return button;
529  };
530
531  const hideTarget = getHideTarget();
532  const updateVisibility = function() {
533    const hasHiddenItems = getHiddenItems().length > 0;
534    hideTarget.style.display = hasHiddenItems ? '' : 'none';
535  };
536
537  updateVisibility();
538
539  button.addEventListener('click', function() {
540    const hiddenItems = getHiddenItems();
541    const itemsToShow = step > 0 ? Math.min(step, hiddenItems.length) : hiddenItems.length;
542
543    for (let i = 0; i < itemsToShow; i++) {
544      hiddenItems[i].classList.remove(hiddenClass);
545    }
546
547    updateVisibility();
548  });
549});
550
551// ===== Big Slider =====
552(function(){
553  const slider = document.querySelector('.big-slider');
554  if (!slider) return;
555  
556  const scroll = slider.querySelector('.big-slider__scroll');
557  const cards = Array.from(slider.querySelectorAll('.big-slider__card'));
558  const dots = Array.from(slider.querySelectorAll('.big-dot'));
559  const leftBtn = slider.querySelector('.big-slider-btn[data-big-dir="left"]');
560  const rightBtn = slider.querySelector('.big-slider-btn[data-big-dir="right"]');
561  
562  let currentIndex = 0;
563  let isProgrammaticScroll = false;
564  
565  const updateDots = () => {
566    dots.forEach((dot, index) => {
567      dot.classList.toggle('is-active', index === currentIndex);
568    });
569  };
570  
571  // Calculate button width based on visible slide area
572  const updateButtonWidths = () => {
573    if (!leftBtn || !rightBtn || !scroll || !cards.length) return;
574    
575    const isDesktop = window.matchMedia('(min-width: 900px)').matches;
576    if (!isDesktop) {
577      leftBtn.style.width = '';
578      rightBtn.style.width = '';
579      return;
580    }
581    
582    const track = slider.querySelector('.big-slider__track');
583    if (!track) return;
584    
585    const scrollWidth = scroll.clientWidth;
586    const cardWidth = cards[0].getBoundingClientRect().width;
587    const trackStyles = getComputedStyle(track);
588    const gapValue = trackStyles.columnGap || trackStyles.gap || '0';
589    const gap = parseFloat(gapValue) || 0;
590    
591    // Formula: (scrollWidth - cardWidth - (2 * gap)) / 2
592    const buttonWidth = (scrollWidth - cardWidth - (2 * gap)) / 2;
593    
594    if (buttonWidth > 0) {
595      leftBtn.style.width = buttonWidth + 'px';
596      rightBtn.style.width = buttonWidth + 'px';
597    } else {
598      leftBtn.style.width = '';
599      rightBtn.style.width = '';
600    }
601  };
602  
603  // Simple visibility control for desktop arrows
604  const updateButtons = () => {
605    if (!leftBtn || !rightBtn) return;
606    
607    const isDesktop = window.matchMedia('(min-width: 900px)').matches;
608    
609    if (!isDesktop) {
610      // Hide arrows completely on mobile/tablet
611      leftBtn.style.display = 'none';
612      rightBtn.style.display = 'none';
613      return;
614    }
615    
616    // Update button widths
617    updateButtonWidths();
618    
619    // On desktop: show only when a previous/next slide exists
620    leftBtn.style.display = currentIndex === 0 ? 'none' : 'flex';
621    rightBtn.style.display = currentIndex === cards.length - 1 ? 'none' : 'flex';
622  };
623  
624  const scrollToCard = (index) => {
625    if (cards[index]) {
626      isProgrammaticScroll = true;
627      cards[index].scrollIntoView({
628        behavior: 'smooth',
629        block: 'nearest',
630        inline: 'center'
631      });
632      currentIndex = index;
633      updateDots();
634      updateButtons();
635      // Reset flag after scroll animation completes
636      setTimeout(() => {
637        isProgrammaticScroll = false;
638      }, 500);
639    }
640  };
641  
642  const nextCard = () => {
643    const next = (currentIndex + 1) % cards.length;
644    scrollToCard(next);
645  };
646  
647  const prevCard = () => {
648    const prev = currentIndex === 0 ? cards.length - 1 : currentIndex - 1;
649    scrollToCard(prev);
650  };
651  
652  
653  // Dot navigation
654  dots.forEach((dot, index) => {
655    dot.addEventListener('click', () => {
656      scrollToCard(index);
657    });
658  });
659  
660  // Card click navigation
661  cards.forEach((card, index) => {
662    card.addEventListener('click', () => {
663      if (index !== currentIndex) {
664        scrollToCard(index);
665      }
666    });
667  });
668  
669  // Touch/swipe support
670  let touchStartX = 0;
671  let touchStartY = 0;
672  let touchEndX = 0;
673  let touchEndY = 0;
674  let isHorizontalSwipe = false;
675  
676  scroll.addEventListener('touchstart', (e) => {
677    touchStartX = e.changedTouches[0].screenX;
678    touchStartY = e.changedTouches[0].screenY;
679    isHorizontalSwipe = false;
680  }, { passive: true });
681  
682  scroll.addEventListener('touchmove', (e) => {
683    if (!touchStartX || !touchStartY) return;
684    
685    const touchCurrentX = e.changedTouches[0].screenX;
686    const touchCurrentY = e.changedTouches[0].screenY;
687    const moveDistanceX = Math.abs(touchCurrentX - touchStartX);
688    const moveDistanceY = Math.abs(touchCurrentY - touchStartY);
689    
690    // If horizontal movement is greater than vertical, prevent scrolling
691    if (moveDistanceX > moveDistanceY && moveDistanceX > 10) {
692      isHorizontalSwipe = true;
693      e.preventDefault(); // Prevent scrolling during horizontal swipe
694    }
695  }, { passive: false });
696  
697  scroll.addEventListener('touchend', (e) => {
698    touchEndX = e.changedTouches[0].screenX;
699    touchEndY = e.changedTouches[0].screenY;
700    handleSwipe();
701  }, { passive: true });
702  
703  const handleSwipe = () => {
704    const swipeDistanceX = Math.abs(touchEndX - touchStartX);
705    const swipeDistanceY = Math.abs(touchEndY - touchStartY);
706    
707    // Only trigger if it's a horizontal swipe (horizontal movement > vertical movement)
708    if (swipeDistanceX <= swipeDistanceY || !isHorizontalSwipe) {
709      isHorizontalSwipe = false;
710      return;
711    }
712    
713    // Mark that this was a swipe gesture - disable auto-snap
714    wasSwipeGesture = true;
715    
716    // Directly jump to next/prev card without scrolling
717    if (touchEndX < touchStartX) {
718      // Swipe left - next card
719      nextCard();
720    } else {
721      // Swipe right - prev card
722      prevCard();
723    }
724    
725    // Reset flags after scroll animation completes
726    setTimeout(() => {
727      wasSwipeGesture = false;
728      isHorizontalSwipe = false;
729    }, 600);
730  };
731
732  // Button navigation (desktop controls)
733  if (leftBtn && rightBtn) {
734    leftBtn.addEventListener('click', () => {
735      prevCard();
736    });
737    rightBtn.addEventListener('click', () => {
738      nextCard();
739    });
740  }
741  
742  // Initial state for dots and arrows
743  updateDots();
744  updateButtons();
745  
746  // Update arrow visibility and widths on resize (e.g., when crossing desktop breakpoint)
747  window.addEventListener('resize', updateButtons);
748  
749  // Handle scroll events to update dots using Intersection Observer
750  const observer = new IntersectionObserver((entries) => {
751    entries.forEach(entry => {
752      if (entry.isIntersecting) {
753        const cardIndex = cards.indexOf(entry.target);
754        if (cardIndex !== -1 && cardIndex !== currentIndex) {
755          currentIndex = cardIndex;
756          updateDots();
757          updateButtons();
758        }
759      }
760    });
761  }, {
762    root: scroll,
763    threshold: 0.5
764  });
765  
766  // Observe all cards
767  cards.forEach(card => observer.observe(card));
768  
769  // Auto-snap to nearest card after manual scrolling
770  let scrollTimeout;
771  let isUserScrolling = false;
772  let wasSwipeGesture = false;
773  
774  const findNearestCard = () => {
775    if (!scroll || cards.length === 0) return -1;
776    
777    const scrollLeft = scroll.scrollLeft;
778    const scrollWidth = scroll.clientWidth;
779    const scrollCenter = scrollLeft + scrollWidth / 2;
780    
781    let nearestIndex = 0;
782    let minDistance = Infinity;
783    
784    cards.forEach((card, index) => {
785      // Get the card's position relative to the scroll container
786      const cardOffsetLeft = card.offsetLeft;
787      const cardWidth = card.offsetWidth;
788      const cardCenter = cardOffsetLeft + cardWidth / 2;
789      const distance = Math.abs(cardCenter - scrollCenter);
790      
791      if (distance < minDistance) {
792        minDistance = distance;
793        nearestIndex = index;
794      }
795    });
796    
797    return nearestIndex;
798  };
799  
800  const handleScrollEnd = () => {
801    if (isProgrammaticScroll || !isUserScrolling || wasSwipeGesture) return;
802    
803    isUserScrolling = false;
804    const nearestIndex = findNearestCard();
805    
806    // Always snap to nearest card, even if it's the same (to ensure perfect centering)
807    if (nearestIndex !== -1) {
808      scrollToCard(nearestIndex);
809    }
810  };
811  
812  scroll.addEventListener('scroll', () => {
813    if (isProgrammaticScroll) return;
814    
815    isUserScrolling = true;
816    clearTimeout(scrollTimeout);
817    
818    // Wait for scroll to end before snapping
819    scrollTimeout = setTimeout(() => {
820      handleScrollEnd();
821    }, 200);
822  }, { passive: true });
823})();
824
825// ===== Hero Image Slider =====
826(function(){
827  // Initialize all hero image sliders
828  const imageSliders = [
829    document.getElementById('heroImageSlider'),
830    document.getElementById('heroImageSliderOverlay')
831  ].filter(Boolean);
832  
833  imageSliders.forEach((imageSlider) => {
834    const slides = Array.from(imageSlider.querySelectorAll('.hero-image-slide'));
835    const controlButton = imageSlider.querySelector('.hero-image-control');
836    
837    if (!controlButton) return;
838    
839    let currentImageIndex = 0;
840    let imageSlideTimer = null;
841    let isPaused = false;
842    
843    // Sequential image preloading function
844    const preloadImagesSequentially = () => {
845      const imagesToPreload = slides.slice(1).map(slide => {
846        const img = slide.querySelector('img[data-src]');
847        if (!img) return null;
848        const isMobile = window.innerWidth <= 920;
849        if (isMobile && img.hasAttribute('data-mobile-src')) {
850          return img.getAttribute('data-mobile-src');
851        }
852        return img.getAttribute('data-src');
853      }).filter(Boolean);
854      
855      let preloadIndex = 0;
856      
857      const preloadNext = () => {
858        if (preloadIndex >= imagesToPreload.length) return;
859        
860        const img = new Image();
861        img.onload = () => {
862          // Find the corresponding slide and set src (desktop or mobile)
863          const slide = slides[preloadIndex + 1];
864          const slideImg = slide.querySelector('img[data-src]');
865          if (slideImg) {
866            // Store original sources for resize handling
867            if (!slideImg.dataset.originalDesktopSrc) {
868              slideImg.dataset.originalDesktopSrc = slideImg.getAttribute('data-src');
869            }
870            if (!slideImg.dataset.originalMobileSrc && slideImg.hasAttribute('data-mobile-src')) {
871              slideImg.dataset.originalMobileSrc = slideImg.getAttribute('data-mobile-src');
872            }
873            
874            const isMobile = window.innerWidth <= 920;
875            const srcAttr = (isMobile && slideImg.hasAttribute('data-mobile-src')) ? 'data-mobile-src' : 'data-src';
876            slideImg.src = slideImg.getAttribute(srcAttr);
877            slideImg.removeAttribute('data-src');
878          }
879          preloadIndex++;
880          // Preload next image after a short delay
881          setTimeout(preloadNext, 200);
882        };
883        img.onerror = () => {
884          // Continue even if one image fails
885          preloadIndex++;
886          setTimeout(preloadNext, 200);
887        };
888        img.src = imagesToPreload[preloadIndex];
889      };
890      
891      // Start preloading after first image is loaded
892      const firstImg = slides[0].querySelector('img');
893      if (firstImg && firstImg.complete) {
894        preloadNext();
895      } else if (firstImg) {
896        firstImg.addEventListener('load', preloadNext, { once: true });
897      } else {
898        preloadNext();
899      }
900    };
901    
902    // Function to ensure image is loaded before showing
903    const ensureImageLoaded = (img, callback) => {
904      if (!img) {
905        callback();
906        return;
907      }
908      
909      // If image has data-src, load it first (respecting mobile variant)
910      if (img.hasAttribute('data-src')) {
911        // Store original sources for resize handling
912        if (!img.dataset.originalDesktopSrc) {
913          img.dataset.originalDesktopSrc = img.getAttribute('data-src');
914        }
915        if (!img.dataset.originalMobileSrc && img.hasAttribute('data-mobile-src')) {
916          img.dataset.originalMobileSrc = img.getAttribute('data-mobile-src');
917        }
918        
919        const isMobile = window.innerWidth <= 920;
920        const srcAttr = (isMobile && img.hasAttribute('data-mobile-src')) ? 'data-mobile-src' : 'data-src';
921        img.src = img.getAttribute(srcAttr);
922        img.removeAttribute('data-src');
923      }
924      
925      // Check if image is already loaded
926      if (img.complete && img.naturalHeight !== 0) {
927        callback();
928        return;
929      }
930      
931      // Wait for image to load
932      img.onload = () => {
933        callback();
934      };
935      img.onerror = () => {
936        // Continue even if image fails to load
937        callback();
938      };
939    };
940    
941    // Ensure first slide image is loaded immediately (important for overlay hero with data-src only)
942    const firstSlide = slides[0];
943    if (firstSlide) {
944      const firstImg = firstSlide.querySelector('img');
945      if (firstImg) {
946        // Store original sources for resize handling (even if image already has src)
947        if (firstImg.hasAttribute('data-src') && !firstImg.dataset.originalDesktopSrc) {
948          firstImg.dataset.originalDesktopSrc = firstImg.getAttribute('data-src');
949        }
950        if (firstImg.hasAttribute('data-mobile-src') && !firstImg.dataset.originalMobileSrc) {
951          firstImg.dataset.originalMobileSrc = firstImg.getAttribute('data-mobile-src');
952        }
953        // If image already has src, also store it as desktop source
954        if (firstImg.src && !firstImg.dataset.originalDesktopSrc && !firstImg.hasAttribute('data-src')) {
955          firstImg.dataset.originalDesktopSrc = firstImg.src;
956        }
957        ensureImageLoaded(firstImg, () => {});
958      }
959    }
960
961    // Function to start image slideshow
962    const startImageSlideshow = () => {
963      imageSlideTimer = setInterval(() => {
964        const nextIndex = (currentImageIndex + 1) % slides.length;
965        const nextSlide = slides[nextIndex];
966        const nextImg = nextSlide.querySelector('img');
967        
968        // Ensure image is loaded before showing
969        ensureImageLoaded(nextImg, () => {
970          slides.forEach(slide => slide.classList.remove('is-active'));
971          currentImageIndex = nextIndex;
972          nextSlide.classList.add('is-active');
973        });
974      }, 4000); // Change image every 4 seconds
975    };
976    
977    // Function to stop image slideshow
978    const stopImageSlideshow = () => {
979      if (imageSlideTimer) {
980        clearInterval(imageSlideTimer);
981        imageSlideTimer = null;
982      }
983    };
984    
985    // Function to toggle pause/play
986    const toggleImageSlideshow = () => {
987      if (isPaused) {
988        startImageSlideshow();
989        isPaused = false;
990        updateControlButton();
991      } else {
992        stopImageSlideshow();
993        isPaused = true;
994        updateControlButton();
995      }
996    };
997    
998    // Function to update control button icon and label
999    const updateControlButton = () => {
1000      if (isPaused) {
1001        // Show play icon (filled triangle)
1002        controlButton.innerHTML = `
1003          <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
1004            <polygon points="5,3 19,12 5,21"></polygon>
1005          </svg>
1006        `;
1007        controlButton.setAttribute('aria-label', 'Play slideshow');
1008      } else {
1009        // Show pause icon (two filled rectangles)
1010        controlButton.innerHTML = `
1011          <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
1012            <rect x="6" y="4" width="4" height="16"></rect>
1013            <rect x="14" y="4" width="4" height="16"></rect>
1014          </svg>
1015        `;
1016        controlButton.setAttribute('aria-label', 'Pause slideshow');
1017      }
1018    };
1019    
1020    // Add event listener for control button
1021    controlButton.addEventListener('click', toggleImageSlideshow);
1022    
1023    // Start slideshow if multiple images
1024    if (slides.length > 1) {
1025      // Start preloading images sequentially
1026      preloadImagesSequentially();
1027      startImageSlideshow();
1028    } else {
1029      // Hide control button if only one image
1030      controlButton.style.display = 'none';
1031    }
1032    
1033    // Cleanup on page unload
1034    window.addEventListener('beforeunload', stopImageSlideshow);
1035    
1036    // Handle resize to switch between mobile/desktop images for overlay hero
1037    if (imageSlider.id === 'heroImageSliderOverlay') {
1038      let lastViewportWidth = window.innerWidth;
1039      let isCurrentlyMobile = lastViewportWidth <= 920;
1040      
1041      // Store original sources for all images
1042      slides.forEach(slide => {
1043        const img = slide.querySelector('img');
1044        if (img) {
1045          if (!img.dataset.originalDesktopSrc && img.hasAttribute('data-src')) {
1046            img.dataset.originalDesktopSrc = img.getAttribute('data-src');
1047          }
1048          if (!img.dataset.originalMobileSrc && img.hasAttribute('data-mobile-src')) {
1049            img.dataset.originalMobileSrc = img.getAttribute('data-mobile-src');
1050          }
1051        }
1052      });
1053      
1054      const reloadImagesForViewport = () => {
1055        const currentWidth = window.innerWidth;
1056        const nowMobile = currentWidth <= 920;
1057        
1058        // Only reload if we've crossed the breakpoint
1059        if (nowMobile !== isCurrentlyMobile) {
1060          isCurrentlyMobile = nowMobile;
1061          
1062          slides.forEach(slide => {
1063            const img = slide.querySelector('img');
1064            if (!img) return;
1065            
1066            // Get stored sources or fall back to attributes
1067            let desktopSrc = img.dataset.originalDesktopSrc;
1068            let mobileSrc = img.dataset.originalMobileSrc;
1069            
1070            // If not stored yet, try to get from attributes
1071            if (!desktopSrc) {
1072              desktopSrc = img.getAttribute('data-src') || (img.src && !nowMobile ? img.src : null);
1073            }
1074            if (!mobileSrc) {
1075              mobileSrc = img.getAttribute('data-mobile-src');
1076            }
1077            
1078            // If still no desktop source and image has src, use current src as desktop
1079            if (!desktopSrc && img.src && !img.src.includes('mobile')) {
1080              desktopSrc = img.src;
1081              img.dataset.originalDesktopSrc = desktopSrc;
1082            }
1083            
1084            if (nowMobile && mobileSrc) {
1085              // Switch to mobile image
1086              img.src = mobileSrc;
1087            } else if (!nowMobile && desktopSrc) {
1088              // Switch to desktop image
1089              img.src = desktopSrc;
1090            }
1091          });
1092        }
1093        
1094        lastViewportWidth = currentWidth;
1095      };
1096      
1097      window.addEventListener('resize', reloadImagesForViewport, { passive: true });
1098    }
1099  });
1100})();
1101
1102// ===== Hero Slider Height Adjustment =====
1103(function(){
1104const heroSlider = document.getElementById('heroSlider');
1105if (!heroSlider) return;
1106
1107const image = heroSlider.querySelector('.hero-slider__image');
1108const content = heroSlider.querySelector('.hero-slider__content');
1109
1110if (!image || !content) return;
1111
1112const adjustImageHeight = () => {
1113  const contentHeight = content.offsetHeight;
1114  const imageNaturalHeight = image.offsetWidth * (554 / 831); // Calculate natural height from aspect ratio
1115  
1116  if (contentHeight > imageNaturalHeight) {
1117    // Content is taller than image's natural height - stretch image to match content
1118    image.style.height = contentHeight + 'px';
1119    image.style.aspectRatio = 'unset'; // Override aspect ratio
1120  } else {
1121    // Image's natural height is fine - restore natural proportions
1122    image.style.height = '';
1123    image.style.aspectRatio = '831 / 554';
1124  }
1125};
1126
1127// Make function globally accessible
1128window.adjustHeroSliderHeight = adjustImageHeight;
1129
1130// Adjust on load and resize
1131adjustImageHeight();
1132window.addEventListener('resize', adjustImageHeight);
1133
1134// Adjust when content changes (e.g., slide transitions)
1135const observer = new MutationObserver(adjustImageHeight);
1136observer.observe(content, { childList: true, subtree: true });
1137})();
1138
1139// ===== Hero Slider =====
1140(function(){
1141  // Initialize all hero sliders (skip overlay variant as it's no longer a slider)
1142  const sliders = [
1143    document.getElementById('heroSlider')
1144  ].filter(Boolean);
1145  
1146  sliders.forEach((slider) => {
1147    const slides = Array.from(slider.querySelectorAll('.hero-slider__slide'));
1148    const allDots = Array.from(slider.querySelectorAll('.hero-dot'));
1149    let currentSlide = 0;
1150    let autoPlayTimer = null;
1151  
1152  let measureSlidesHeight = null; // Will be set for overlay variant
1153  
1154  const goToSlide = (index) => {
1155    slides.forEach((slide, i) => {
1156      slide.classList.toggle('is-active', i === index);
1157    });
1158    // Update all dots across all slides
1159    allDots.forEach((dot) => {
1160      const dotIndex = parseInt(dot.getAttribute('data-slide'));
1161      dot.classList.toggle('is-active', dotIndex === index);
1162    });
1163    currentSlide = index;
1164    
1165    // Trigger height adjustment after slide change
1166    setTimeout(() => {
1167      if (window.adjustHeroSliderHeight) {
1168        window.adjustHeroSliderHeight();
1169      }
1170      // Also measure slides height for overlay variant
1171      if (measureSlidesHeight) {
1172        measureSlidesHeight();
1173      }
1174    }, 100); // Small delay to ensure DOM has updated
1175  };
1176  
1177  const nextSlide = () => {
1178    const next = (currentSlide + 1) % slides.length;
1179    goToSlide(next);
1180  };
1181  
1182  const startAutoPlay = () => {
1183    stopAutoPlay();
1184    // Auto-advance after 5 seconds
1185    autoPlayTimer = setTimeout(() => {
1186      nextSlide();
1187      // Stop after reaching slide 2
1188      if (currentSlide === 1) {
1189        stopAutoPlay();
1190      }
1191    }, 5000);
1192  };
1193  
1194  const stopAutoPlay = () => {
1195    if (autoPlayTimer) {
1196      clearTimeout(autoPlayTimer);
1197      autoPlayTimer = null;
1198    }
1199  };
1200  
1201  // Dot click handlers - delegate from slider
1202  slider.addEventListener('click', (e) => {
1203    const dot = e.target.closest('.hero-dot');
1204    if (dot) {
1205      const index = parseInt(dot.getAttribute('data-slide'));
1206      stopAutoPlay();
1207      goToSlide(index);
1208    }
1209  });
1210  
1211  // Touch/swipe support
1212  let touchStartX = 0;
1213  let touchEndX = 0;
1214  
1215  slider.addEventListener('touchstart', (e) => {
1216    touchStartX = e.changedTouches[0].screenX;
1217  }, { passive: true });
1218  
1219  slider.addEventListener('touchend', (e) => {
1220    touchEndX = e.changedTouches[0].screenX;
1221    handleSwipe();
1222  }, { passive: true });
1223  
1224  const handleSwipe = () => {
1225    if (touchEndX < touchStartX - 50) {
1226      // Swipe left
1227      stopAutoPlay();
1228      nextSlide();
1229    }
1230    if (touchEndX > touchStartX + 50) {
1231      // Swipe right
1232      stopAutoPlay();
1233      goToSlide(Math.max(0, currentSlide - 1));
1234    }
1235  };
1236  
1237  // Start auto-play
1238  startAutoPlay();
1239  
1240  // Fix height jumping for overlay variant
1241  if (slider.classList.contains('hero-slider--overlay')) {
1242    const slidesContainer = slider.querySelector('.hero-slider__slides');
1243    if (slidesContainer && slides.length > 0) {
1244      measureSlidesHeight = () => {
1245        let maxHeight = 0;
1246        const isMobile = window.innerWidth <= 900;
1247        const containerPadding = isMobile ? 20 + 16 : 40 + 20; // top + bottom padding
1248        const containerWidth = isMobile ? slidesContainer.offsetWidth : slidesContainer.offsetWidth - 128; // minus left + right padding on desktop
1249        
1250        slides.forEach(slide => {
1251          // Temporarily show slide to measure
1252          const originalDisplay = slide.style.display;
1253          const originalPosition = slide.style.position;
1254          const originalOpacity = slide.style.opacity;
1255          
1256          slide.style.display = 'block';
1257          slide.style.position = 'static';
1258          slide.style.opacity = '1';
1259          slide.style.width = containerWidth + 'px';
1260          
1261          const height = slide.scrollHeight;
1262          if (height > maxHeight) {
1263            maxHeight = height;
1264          }
1265          
1266          // Restore
1267          slide.style.display = originalDisplay;
1268          slide.style.position = originalPosition;
1269          slide.style.opacity = originalOpacity;
1270          slide.style.width = '';
1271        });
1272        
1273        if (maxHeight > 0) {
1274          slidesContainer.style.height = (maxHeight + containerPadding) + 'px';
1275        }
1276      };
1277      
1278      // Measure and set height after a short delay to ensure DOM is ready
1279      setTimeout(measureSlidesHeight, 200);
1280      window.addEventListener('resize', measureSlidesHeight);
1281    }
1282  }
1283  });
1284})();
1285
1286// ===== Hero Video Play/Pause Toggle =====
1287(function(){
1288const heroImageContainer = document.querySelector('.hero-slider--overlay .hero-slider__image');
1289const heroVideo = document.querySelector('.hero-slider--overlay .hero-video');
1290
1291if (heroImageContainer && heroVideo) {
1292  heroImageContainer.addEventListener('click', function(e) {
1293    // Only toggle if not clicking on interactive elements (dots, links)
1294    const target = e.target;
1295    if (target.closest('.hero-dot') || target.closest('.hero-slider__cta')) {
1296      return;
1297    }
1298    // Toggle video play/pause
1299    if (heroVideo.paused) {
1300      heroVideo.play();
1301    } else {
1302      heroVideo.pause();
1303    }
1304  }, {passive: true});
1305}
1306})();
1307
1308// ===== Mobile Image Handler =====
1309(function(){
1310const handleMobileImages = () => {
1311  const images = document.querySelectorAll('img[data-mobile-src]');
1312  const isMobile = window.innerWidth <= 920;
1313  
1314  images.forEach(img => {
1315    // Hero overlay slider images are handled separately by the hero slider logic
1316    if (img.closest('.hero-slider--overlay')) {
1317      return;
1318    }
1319    const mobileSrc = img.getAttribute('data-mobile-src');
1320    const currentOriginal = img.getAttribute('data-original-src');
1321    
1322    if (isMobile && mobileSrc) {
1323      // On mobile, use mobile image
1324      if (currentOriginal === null) {
1325        const desktopSrc = img.getAttribute('src') || img.getAttribute('data-src') || '';
1326        img.setAttribute('data-original-src', desktopSrc);
1327      }
1328      img.src = mobileSrc;
1329    } else if (!isMobile && currentOriginal !== null) {
1330      // On desktop, restore original image
1331      const original = img.getAttribute('data-original-src');
1332      if (original) {
1333        img.src = original;
1334      }
1335    }
1336  });
1337};
1338
1339// Handle on load and resize
1340handleMobileImages();
1341window.addEventListener('resize', handleMobileImages);
1342})();
1343
1344
1345
1346/**
1347 * Minimal podcast controller
1348 * Expects feed.php → [{ title, pubDate, duration, audio }]
1349 * (Same shape as used on podcast.html.)
1350 */
1351(function(){
1352  const listEl = document.getElementById('pod-episodes');
1353  if(!listEl) return;
1354
1355  // Single shared audio element
1356  const audio = new Audio();
1357  audio.preload = 'none';
1358
1359  let activeRow = null;
1360  let activeSrc = null;
1361  let raf = null;
1362  let isResetting = false;
1363  let rows = [];
1364
1365  // Cache and responsive media query
1366  let allEpisodes = [];
1367  const mqMobile = window.matchMedia('(max-width: 980px)');
1368
1369  // helpers
1370  const setPlayIcon = (svg)=>{ if(!svg) return; svg.innerHTML=''; const p=document.createElementNS('http://www.w3.org/2000/svg','polygon'); p.setAttribute('points','6 1 6 23 20 12 6 1'); svg.appendChild(p); };
1371  const setPauseIcon = (svg)=>{ if(!svg) return; svg.innerHTML=''; const r1=document.createElementNS('http://www.w3.org/2000/svg','rect'); r1.setAttribute('x','6'); r1.setAttribute('y','1'); svg.appendChild(r1); const r2=document.createElementNS('http://www.w3.org/2000/svg','rect'); r2.setAttribute('x','14'); r2.setAttribute('y','1'); svg.appendChild(r2); };
1372  const fmtMinutes = (dur) => {
1373    if(!dur) return '—';
1374    const parts = dur.split(':').map(Number);
1375    let totalMin = 0;
1376    if(parts.length === 3){ totalMin = parts[0]*60 + parts[1] + parts[2]/60; }
1377    else if(parts.length === 2){ totalMin = parts[0] + parts[1]/60; }
1378    else { totalMin = (+parts[0] || 0) / 60; }
1379    return `${Math.round(totalMin)} min`;
1380  };
1381  const stopAnim = ()=>{ if(raf){ cancelAnimationFrame(raf); raf=null; } };
1382
1383  /** Matomo on-premise: trackEvent when an episode actually starts / resumes playback */
1384  const trackMatomoPodcastPlay = () => {
1385    if (typeof window._paq === 'undefined' || typeof window._paq.push !== 'function') return;
1386    if (!activeRow) return;
1387    const rowData = rows.find(function (r) { return r.row === activeRow; });
1388    if (!rowData || !rowData.ep) return;
1389    const ep = rowData.ep;
1390    const title = ep.title || '';
1391    window._paq.push(['trackEvent', 'Podcast', 'Episode Play', title]);
1392  };
1393
1394  const render = (episodes) => {
1395    listEl.innerHTML = '';
1396    listEl.setAttribute('aria-busy','false');
1397
1398    const maxItems = mqMobile.matches ? 4 : 7;
1399    rows = [];
1400    episodes.slice(0, maxItems).forEach((ep, idx)=>{
1401      const row = document.createElement('div');
1402      row.className = 'pod-ep';
1403      row.setAttribute('role','listitem');
1404      row.tabIndex = 0;
1405      row.dataset.src = ep.audio;
1406
1407      const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1408      icon.setAttribute('xmlns','http://www.w3.org/2000/svg');
1409      icon.setAttribute('viewBox','0 0 24 24');
1410      icon.setAttribute('fill','currentColor');
1411      icon.classList.add('pod-ep-icon');
1412      // default to play icon
1413      const playPath1 = document.createElementNS('http://www.w3.org/2000/svg','polygon');
1414      playPath1.setAttribute('points','6 1 6 23 20 12 6 1');
1415      icon.appendChild(playPath1);
1416
1417      const title = document.createElement('h4');
1418      title.className = 'pod-ep-title';
1419      title.textContent = ep.title;
1420
1421      const meta = document.createElement('div');
1422      meta.className = 'pod-ep-meta';
1423      meta.textContent = fmtMinutes(ep.duration);
1424
1425      const progress = document.createElement('div');
1426      progress.className = 'pod-ep-progress';
1427      const bar = document.createElement('div');
1428      bar.className = 'pod-ep-bar';
1429      progress.appendChild(bar);
1430
1431      const player = document.createElement('div');
1432      player.className = 'pod-ep-player';
1433      player.appendChild(icon.cloneNode(true));
1434      player.appendChild(progress.cloneNode(true));
1435
1436      row.appendChild(title);
1437      row.appendChild(icon);
1438      row.appendChild(meta);
1439      row.appendChild(player);
1440      listEl.appendChild(row);
1441      rows.push({ row, bar, icon, ep, player, meta });
1442
1443      const toggle = ()=>{
1444        // if clicking current → toggle play/pause
1445        if(activeRow === row){
1446          if(audio.paused){
1447            audio.play().catch(()=>{});
1448            row.classList.add('pod-ep--playing','pod-ep--active');
1449            const prog = row.querySelector('.pod-ep-progress'); if(prog){ prog.style.display=''; }
1450          } else {
1451            audio.pause();
1452          }
1453          return;
1454        }
1455        // open a new episode
1456        if(activeRow){
1457          activeRow.classList.remove('pod-ep--playing','pod-ep--active');
1458          const b = activeRow.querySelector('.pod-ep-bar');
1459          if(b) b.style.width = '0%';
1460          const playerProg = activeRow.querySelector('.pod-ep-player .pod-ep-progress');
1461          if(playerProg) playerProg.style.display = '';
1462          // reset old icon to play
1463          const oldIcon = activeRow.querySelector('.pod-ep-icon');
1464          if(oldIcon){ oldIcon.innerHTML=''; const p=document.createElementNS('http://www.w3.org/2000/svg','polygon'); p.setAttribute('points','8 5 19 12 8 19 8 5'); oldIcon.appendChild(p); }
1465          // Restore original duration display
1466          const metaEl = activeRow.querySelector('.pod-ep-meta');
1467          const oldRowData = rows.find(r => r.row === activeRow);
1468          if(metaEl && oldRowData){
1469            metaEl.textContent = fmtMinutes(oldRowData.ep.duration);
1470          }
1471        }
1472        activeRow = row;
1473        activeSrc = ep.audio;
1474        row.classList.add('pod-ep--playing','pod-ep--active');
1475        audio.src = ep.audio;
1476        audio.play().catch(()=>{ /* autoplay might be blocked; user can tap again */ });
1477        tick(); // start progress animation
1478        // set icon to pause
1479        setPauseIcon(icon);
1480        // ensure progress becomes visible immediately
1481        const playerProgress = row.querySelector('.pod-ep-player .pod-ep-progress');
1482        if(playerProgress) playerProgress.style.display = '';
1483      };
1484
1485      row.addEventListener('click', (e) => {
1486        // Don't toggle if clicking on the progress bar
1487        if (e.target.closest('.pod-ep-progress')) {
1488          e.stopPropagation();
1489          return;
1490        }
1491        toggle();
1492      }, {passive:true});
1493      
1494      // Add timeline click/drag functionality
1495      const playerProgress = player.querySelector('.pod-ep-progress');
1496      if (playerProgress) {
1497        playerProgress.addEventListener('click', (e) => {
1498        e.stopPropagation();
1499          if (!activeRow || activeRow !== row) return;
1500          
1501          const rect = playerProgress.getBoundingClientRect();
1502          const clickX = e.clientX - rect.left;
1503          const percentage = clickX / rect.width;
1504          const newTime = percentage * audio.duration;
1505          
1506          if (!isNaN(newTime) && isFinite(newTime)) {
1507            audio.currentTime = newTime;
1508          }
1509        });
1510        
1511        // Add drag functionality
1512        let isDragging = false;
1513        playerProgress.addEventListener('mousedown', (e) => {
1514        e.stopPropagation();
1515          if (!activeRow || activeRow !== row) return;
1516          isDragging = true;
1517          
1518          const rect = playerProgress.getBoundingClientRect();
1519          const clickX = e.clientX - rect.left;
1520          const percentage = Math.max(0, Math.min(1, clickX / rect.width));
1521          const newTime = percentage * audio.duration;
1522          
1523          if (!isNaN(newTime) && isFinite(newTime)) {
1524            audio.currentTime = newTime;
1525          }
1526        });
1527        
1528        document.addEventListener('mousemove', (e) => {
1529          if (!isDragging || !activeRow || activeRow !== row) return;
1530          
1531          const rect = playerProgress.getBoundingClientRect();
1532          const clickX = e.clientX - rect.left;
1533          const percentage = Math.max(0, Math.min(1, clickX / rect.width));
1534          const newTime = percentage * audio.duration;
1535          
1536          if (!isNaN(newTime) && isFinite(newTime)) {
1537            audio.currentTime = newTime;
1538          }
1539        });
1540        
1541        document.addEventListener('mouseup', () => {
1542          isDragging = false;
1543        });
1544      }
1545      
1546      row.addEventListener('keydown', (e)=>{ if(e.key==='Enter' || e.key===' '){ e.preventDefault(); toggle(); }});
1547    });
1548
1549    // Restore active UI after re-render (e.g., on responsive change)
1550    if (activeSrc) {
1551      const match = rows.find(r => r.ep.audio === activeSrc);
1552      if (match) {
1553        activeRow = match.row;
1554        match.row.classList.add('pod-ep--active');
1555        const isPlaying = !audio.paused && (audio.src && audio.src.includes(activeSrc));
1556        if (isPlaying) {
1557          match.row.classList.add('pod-ep--playing');
1558          const playerIcon = match.row.querySelector('.pod-ep-player .pod-ep-icon');
1559          if (playerIcon) setPauseIcon(playerIcon);
1560        } else {
1561          const playerIcon = match.row.querySelector('.pod-ep-player .pod-ep-icon');
1562          if (playerIcon) setPlayIcon(playerIcon);
1563        }
1564        const duration = audio.duration || 0;
1565        if (duration > 0) {
1566          match.bar.style.width = ((audio.currentTime / duration) * 100).toFixed(2) + '%';
1567        }
1568      } else {
1569        // If active episode got sliced out on mobile, clear UI ref
1570        activeRow = null;
1571      }
1572    }
1573  };
1574
1575  const formatTime = (seconds) => {
1576    if (!seconds || isNaN(seconds)) return '0:00';
1577    const mins = Math.floor(seconds / 60);
1578    const secs = Math.floor(seconds % 60);
1579    return `${mins}:${secs.toString().padStart(2, '0')}`;
1580  };
1581
1582  const tick = ()=>{
1583    stopAnim();
1584    const update = ()=>{
1585      if(!activeRow || isResetting) return;
1586      const bar = activeRow.querySelector('.pod-ep-bar');
1587      const metaDisplay = activeRow.querySelector('.pod-ep-meta');
1588      if(bar && audio.duration > 0){
1589        bar.style.width = ((audio.currentTime / audio.duration) * 100).toFixed(2) + '%';
1590      }
1591      if(metaDisplay && audio.duration > 0){
1592        metaDisplay.textContent = `${formatTime(audio.currentTime)} / ${formatTime(audio.duration)}`;
1593      }
1594      raf = requestAnimationFrame(update);
1595    };
1596    raf = requestAnimationFrame(update);
1597  };
1598
1599  // audio events
1600  audio.addEventListener('play', ()=>{
1601    if(activeRow){
1602      activeRow.classList.add('pod-ep--playing');
1603      const playerIcon = activeRow.querySelector('.pod-ep-player .pod-ep-icon');
1604      if(playerIcon){ playerIcon.innerHTML=''; const r1=document.createElementNS('http://www.w3.org/2000/svg','rect'); r1.setAttribute('x','6'); r1.setAttribute('y','1'); playerIcon.appendChild(r1); const r2=document.createElementNS('http://www.w3.org/2000/svg','rect'); r2.setAttribute('x','14'); r2.setAttribute('y','1'); playerIcon.appendChild(r2); }
1605      const playerProg = activeRow.querySelector('.pod-ep-player .pod-ep-progress');
1606      if(playerProg){ playerProg.style.display = ''; }
1607    }
1608    trackMatomoPodcastPlay();
1609    tick();
1610  });
1611  audio.addEventListener('pause', ()=> { 
1612    stopAnim();
1613    if(activeRow){
1614      activeRow.classList.remove('pod-ep--playing'); 
1615      activeRow.classList.add('pod-ep--active'); 
1616      const ic=activeRow.querySelector('.pod-ep-player .pod-ep-icon'); 
1617      setPlayIcon(ic);
1618      // Restore original duration display
1619      const metaEl = activeRow.querySelector('.pod-ep-meta');
1620      const rowData = rows.find(r => r.row === activeRow);
1621      if(metaEl && rowData){
1622        metaEl.textContent = fmtMinutes(rowData.ep.duration);
1623      }
1624    } 
1625  });
1626  audio.addEventListener('ended', ()=>{
1627    if(activeRow){
1628      isResetting = true;
1629      
1630      const rowToReset = activeRow;
1631    activeRow = null;
1632      
1633      stopAnim();
1634      if(raf) { 
1635        cancelAnimationFrame(raf); 
1636        raf = null; 
1637      }
1638      
1639      rowToReset.classList.remove('pod-ep--playing','pod-ep--active');
1640      
1641      const b = rowToReset.querySelector('.pod-ep-bar'); 
1642      if(b) b.style.width = '0%';
1643      
1644      const playerProg = rowToReset.querySelector('.pod-ep-player');
1645      if(playerProg) playerProg.style.display = '';
1646      
1647      const originalIcon = rowToReset.querySelector('.pod-ep-icon');
1648      if(originalIcon){ 
1649        originalIcon.innerHTML=''; 
1650        const p=document.createElementNS('http://www.w3.org/2000/svg','polygon'); 
1651        p.setAttribute('points','6 1 6 23 20 12 6 1');
1652        originalIcon.appendChild(p);
1653        originalIcon.style.opacity = '';
1654      }
1655      
1656      const metaEl = rowToReset.querySelector('.pod-ep-meta');
1657      const rowData = rows.find(r => r.row === rowToReset);
1658      if(metaEl && rowData){
1659        metaEl.textContent = fmtMinutes(rowData.ep.duration);
1660      }
1661      
1662      isResetting = false;
1663    }
1664  });
1665
1666  // fetch episodes from the same feed your podcast page uses
1667  // (podcast-feed.php lives in scripts/ at the site root, next to podcast-cache/)
1668  fetch('scripts/podcast-feed.php')
1669    .then(r => r.json())
1670    .then(data => {
1671      allEpisodes = Array.isArray(data) ? data : [];
1672      render(allEpisodes);
1673    })
1674    .catch(err => {
1675      console.error('Feed-Fehler', err);
1676      listEl.innerHTML = '<div class="feed-error">Fehler beim Laden des Feeds.</div>';
1677      listEl.setAttribute('aria-busy','false');
1678    });
1679
1680  // Re-render on viewport change to update the number of visible items
1681  mqMobile.addEventListener('change', () => {
1682    if (allEpisodes && allEpisodes.length) {
1683      render(allEpisodes);
1684    }
1685  });
1686})();
1687
1688// ===== Video logic moved to video.js =====
1689// videoOverlayController, the One Minute Homily Click-to-Play / slider IIFE,
1690// the Videos Click-to-Play (Basic Slider) IIFE, the YouTube IFrame API lazy
1691// loader, and the consent overlay all live in typo3temp/assets/compressed/video.js
1692// (loaded with `defer` from each page's <head>).
1693
1694// ===== Schwerpunkte Dot Navigation =====
1695(function(){
1696  const schwerpunkteDots = document.querySelectorAll('.features--schwerpunkte .dot');
1697  const schwerpunkteSlider = document.querySelector('.features--schwerpunkte .features-slider');
1698  const schwerpunkteTrack = document.querySelector('.features--schwerpunkte .features-track');
1699  let currentSchwerpunkteSlide = 0;
1700  
1701  const updateSchwerpunkteDots = () => {
1702    schwerpunkteDots.forEach((dot, index) => {
1703      dot.classList.toggle('is-active', index === currentSchwerpunkteSlide);
1704      dot.setAttribute('aria-selected', index === currentSchwerpunkteSlide);
1705    });
1706  };
1707  
1708  const scrollToSchwerpunkteSlide = (slideIndex) => {
1709    if (!schwerpunkteTrack) return;
1710    
1711    const cards = schwerpunkteTrack.querySelectorAll('.features-card');
1712    if (cards[slideIndex]) {
1713      cards[slideIndex].scrollIntoView({
1714        behavior: 'smooth',
1715        block: 'nearest',
1716        inline: 'center'
1717      });
1718      currentSchwerpunkteSlide = slideIndex;
1719      updateSchwerpunkteDots();
1720    }
1721  };
1722  
1723  // Dot click handlers
1724  schwerpunkteDots.forEach((dot, index) => {
1725    dot.addEventListener('click', () => {
1726      scrollToSchwerpunkteSlide(index);
1727    });
1728  });
1729  
1730  // Touch/swipe support for schwerpunkte slider
1731  let schwerpunkteTouchStartX = 0;
1732  let schwerpunkteTouchEndX = 0;
1733  let schwerpunkteTouchStartY = 0;
1734  let schwerpunkteTouchEndY = 0;
1735  
1736  if (schwerpunkteSlider) {
1737    schwerpunkteSlider.addEventListener('touchstart', (e) => {
1738      schwerpunkteTouchStartX = e.changedTouches[0].screenX;
1739      schwerpunkteTouchStartY = e.changedTouches[0].screenY;
1740    }, { passive: true });
1741    
1742    schwerpunkteSlider.addEventListener('touchend', (e) => {
1743      schwerpunkteTouchEndX = e.changedTouches[0].screenX;
1744      schwerpunkteTouchEndY = e.changedTouches[0].screenY;
1745      handleSchwerpunkteSwipe();
1746    }, { passive: true });
1747  }
1748  
1749  const handleSchwerpunkteSwipe = () => {
1750    const deltaX = schwerpunkteTouchEndX - schwerpunkteTouchStartX;
1751    const deltaY = schwerpunkteTouchEndY - schwerpunkteTouchStartY;
1752
1753    // Only treat as swipe if movement is mostly horizontal and exceeds threshold.
1754    if (Math.abs(deltaX) < 50 || Math.abs(deltaX) <= Math.abs(deltaY)) {
1755      return;
1756    }
1757
1758    if (deltaX < -50) {
1759      // Swipe left - next slide
1760      const nextSlide = (currentSchwerpunkteSlide + 1) % schwerpunkteDots.length;
1761      scrollToSchwerpunkteSlide(nextSlide);
1762    }
1763    if (deltaX > 50) {
1764      // Swipe right - previous slide
1765      const prevSlide = currentSchwerpunkteSlide === 0 ? schwerpunkteDots.length - 1 : currentSchwerpunkteSlide - 1;
1766      scrollToSchwerpunkteSlide(prevSlide);
1767    }
1768  };
1769  
1770  // Intersection Observer for schwerpunkte slides
1771  const schwerpunkteObserver = new IntersectionObserver((entries) => {
1772    entries.forEach(entry => {
1773      if (entry.isIntersecting) {
1774        const cardIndex = Array.from(schwerpunkteTrack.querySelectorAll('.features-card')).indexOf(entry.target);
1775        if (cardIndex !== -1 && cardIndex !== currentSchwerpunkteSlide) {
1776          currentSchwerpunkteSlide = cardIndex;
1777          updateSchwerpunkteDots();
1778        }
1779      }
1780    });
1781  }, {
1782    root: schwerpunkteSlider,
1783    threshold: 0.5
1784  });
1785  
1786  // Observe all schwerpunkte cards
1787  if (schwerpunkteTrack) {
1788    schwerpunkteTrack.querySelectorAll('.features-card').forEach(card => {
1789      schwerpunkteObserver.observe(card);
1790    });
1791  }
1792
1793  // Recompute active dot on resize for schwerpunkte
1794  const recomputeSchwerpunkteActive = () => {
1795    if (!schwerpunkteSlider || !schwerpunkteTrack) return;
1796    const cards = Array.from(schwerpunkteTrack.querySelectorAll('.features-card'));
1797    if (!cards.length) return;
1798    const sliderRect = schwerpunkteSlider.getBoundingClientRect();
1799    const sliderCenter = sliderRect.left + sliderRect.width / 2;
1800    let closestIdx = 0;
1801    let closestDist = Infinity;
1802    cards.forEach((card, idx) => {
1803      const rect = card.getBoundingClientRect();
1804      const cardCenter = rect.left + rect.width / 2;
1805      const dist = Math.abs(cardCenter - sliderCenter);
1806      if (dist < closestDist) { closestDist = dist; closestIdx = idx; }
1807    });
1808    if (closestIdx !== currentSchwerpunkteSlide) {
1809      currentSchwerpunkteSlide = closestIdx;
1810      updateSchwerpunkteDots();
1811    }
1812  };
1813  window.addEventListener('resize', recomputeSchwerpunkteActive, { passive: true });
1814})();
1815
1816// ===== Donation amount sync =====
1817  (function(){
1818    const donationSection = document.querySelector('.donation');
1819    if (!donationSection) return;
1820    const amountField = donationSection.querySelector('.donation__amount-field');
1821    const rangeField = donationSection.querySelector('.donation__range');
1822    const purposeSelect = donationSection.querySelector('#donationPurpose');
1823    const formEl = donationSection.querySelector('.donation__form');
1824    if (!amountField || !rangeField) return;
1825
1826    const clampAmount = (value) => {
1827      const min = parseInt(rangeField.min, 10);
1828      const max = parseInt(rangeField.max, 10);
1829      const parsed = parseInt(value, 10);
1830      if (Number.isNaN(parsed)) return min;
1831      return Math.min(max, Math.max(min, parsed));
1832    };
1833
1834    const syncFromRange = () => {
1835      amountField.value = clampAmount(rangeField.value);
1836      const percent = ((rangeField.value - rangeField.min) / (rangeField.max - rangeField.min)) * 100;
1837      rangeField.style.setProperty('--_percent', percent + '%');
1838    };
1839
1840    const syncFromInput = () => {
1841      const clamped = clampAmount(amountField.value);
1842      amountField.value = clamped;
1843      rangeField.value = clamped;
1844    };
1845
1846    rangeField.addEventListener('input', syncFromRange);
1847    amountField.addEventListener('change', syncFromInput);
1848    amountField.addEventListener('blur', syncFromInput);
1849    amountField.addEventListener('input', () => {
1850      const numeric = amountField.value.replace(/[^\d]/g, '');
1851      const limited = numeric.slice(0, 4); // hard-limit to 4 digits while typing
1852      amountField.value = limited;
1853      const value = clampAmount(limited || rangeField.min);
1854      rangeField.value = value;
1855      const percent = ((value - rangeField.min) / (rangeField.max - rangeField.min)) * 100;
1856      rangeField.style.setProperty('--_percent', percent + '%');
1857    });
1858    syncFromRange();
1859
1860    // Spendenzwecke aktuell von Twingle laden – serverseitig über scripts/twingle-targets.php,
1861    // der Browser kontaktiert twingle.de dabei nicht. Bei Fehlern bleiben die Optionen aus dem HTML.
1862    if (purposeSelect) {
1863      fetch('scripts/twingle-targets.php')
1864        .then(r => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)))
1865        .then(data => {
1866          const targets = data && Array.isArray(data.targets) ? data.targets : [];
1867          if (!targets.length) return;
1868          const previous = purposeSelect.value;
1869          const options = targets.map(t => new Option(t.label, t.label));
1870          if (data.freeInput) options.push(new Option('Freie Eingabe', 'other'));
1871          purposeSelect.replaceChildren(...options);
1872          if (options.some(o => o.value === previous)) purposeSelect.value = previous;
1873        })
1874        .catch(err => console.warn('Spendenzwecke: Fallback auf HTML-Optionen', err));
1875    }
1876
1877    // Open Twingle widget on submit
1878    if (formEl) {
1879      formEl.addEventListener('submit', function(e){
1880        e.preventDefault();
1881        const amount = clampAmount(amountField.value || rangeField.value);
1882        const purpose = purposeSelect ? (purposeSelect.value || purposeSelect.options[purposeSelect.selectedIndex].text) : '';
1883        // Eigene Spendenseite mit eingebettetem Twingle-Formular (/form); sie reicht die tw_*-Parameter ans iframe weiter.
1884        // Direkt zu Twingle: 'https://spenden.twingle.de/freunde-der-gesellschaft-jesu-e-v/jesuiten-im-dienst-fuer-gott-und-die-menschen/tw677e97f080c21/page'
1885        //const base = 'jetzt-spenden.html';
1886        const base = 'https://spenden.twingle.de/freunde-der-gesellschaft-jesu-e-v/jesuiten-im-dienst-fuer-gott-und-die-menschen/tw677e97f080c21/page';
1887        // "Freie Eingabe" nur vorwählen (tw_target), dann zeigt Twingle das leere Freitextfeld.
1888        // Mit tw_target_fix=other stünde sonst wörtlich "other" im Feld.
1889        const target = purpose === 'other'
1890          ? `tw_target=${encodeURIComponent('Freie Eingabe')}`
1891          : `tw_target_fix=${encodeURIComponent(purpose)}`;
1892        const url = `${base}?tw_amount=${encodeURIComponent(String(amount))}&${target}&tw_rhythm=one_time`;
1893        //window.location.href = url;
1894        window.open(url, '_blank', 'noopener');
1895      });
1896    }
1897  })();
1898
1899  // ===== Publication Region Modal =====
1900  (function () {
1901    const langBtn = document.getElementById('langDropdown');
1902    const modal = document.getElementById('modalPublicationRegion');
1903    if (!langBtn || !modal) return;
1904    
1905    // Open modal on button click
1906    langBtn.addEventListener('click', function() {
1907      modal.classList.add('active');
1908      modal.style.display = 'flex';
1909    });
1910    
1911    // Close modal on X button
1912    const closeBtn = modal.querySelector('.publication-modal-close');
1913    if (closeBtn) {
1914      closeBtn.addEventListener('click', function() {
1915        modal.classList.remove('active');
1916        modal.style.display = 'flex';
1917      });
1918    }
1919    
1920    // Close modal when clicking outside
1921    modal.addEventListener('click', function(e) {
1922      if (e.target === modal) {
1923        modal.classList.remove('active');
1924        modal.style.display = 'none';
1925      }
1926    });
1927    
1928    // Handle region selection
1929    const regionLinks = modal.querySelectorAll('.region-link');
1930    regionLinks.forEach(function(link) {
1931      link.addEventListener('click', function(e) {
1932        e.preventDefault();
1933        
1934        // Update button flag class using data attributes
1935        const btnFlag = langBtn.querySelector('.flag');
1936        if (btnFlag) {
1937          btnFlag.classList.remove('publication-flag-de', 'publication-flag-at', 'publication-flag-ch');
1938          if (link.dataset.flag.includes('Germany')) {
1939            btnFlag.classList.add('publication-flag-de');
1940          } else if (link.dataset.flag.includes('Austria')) {
1941            btnFlag.classList.add('publication-flag-at');
1942          } else if (link.dataset.flag.includes('Switzerland')) {
1943            btnFlag.classList.add('publication-flag-ch');
1944          }
1945        }
1946        const labelSpan = langBtn.querySelector('.magazin__lang-label');
1947        if (labelSpan && link.dataset.text) {
1948          labelSpan.textContent = link.dataset.text;
1949        }
1950        if (link.dataset.text) {
1951          langBtn.setAttribute('aria-label', link.dataset.text);
1952        }
1953        
1954        // Hide modal
1955        modal.classList.remove('active');
1956      });
1957    });
1958  })();
1959
1960  // ===== Expandable copy =====
1961  (function () {
1962    const copyWrapper = document.querySelector('.magazin__copy-wrapper');
1963    const moreLink = copyWrapper ? copyWrapper.querySelector('.magazin__more-link') : null;
1964    
1965    if (!copyWrapper || !moreLink) return;
1966    
1967    moreLink.addEventListener('click', function(e) {
1968      e.preventDefault();
1969      copyWrapper.classList.toggle('expanded');
1970      if (copyWrapper.classList.contains('expanded')) {
1971        moreLink.textContent = 'Weniger';
1972      } else {
1973        moreLink.textContent = 'Mehr';
1974      }
1975    });
1976  })();
1977
1978// --- Donation Image Slideshow
1979(function(){
1980  document.querySelectorAll('.donation__media[data-images]').forEach(function(container) {
1981    const imagesAttr = container.getAttribute('data-images');
1982    if (!imagesAttr) return;
1983    
1984    const imagePaths = imagesAttr.split(',').map(function(path) {
1985      return path.trim();
1986    }).filter(function(path) {
1987      return path.length > 0;
1988    });
1989    
1990    // If only one or no images, keep it static
1991    if (imagePaths.length <= 1) {
1992      if (imagePaths.length === 1) {
1993        const existingImg = container.querySelector('img');
1994        if (existingImg) {
1995          existingImg.src = imagePaths[0];
1996        } else {
1997          const img = document.createElement('img');
1998          img.src = imagePaths[0];
1999          img.alt = 'Donation image';
2000          container.appendChild(img);
2001        }
2002      }
2003      return;
2004    }
2005    
2006    const existingImg = container.querySelector('img');
2007    const altText = existingImg ? existingImg.getAttribute('alt') || '' : '';
2008    
2009    // Remove existing image
2010    if (existingImg) {
2011      existingImg.remove();
2012    }
2013    
2014    // Create all image elements
2015    const images = [];
2016    imagePaths.forEach(function(src, index) {
2017      const img = document.createElement('img');
2018      img.src = src;
2019      img.alt = altText;
2020      img.loading = index === 0 ? 'eager' : 'lazy';
2021      if (index === 0) {
2022        img.classList.add('is-active');
2023      }
2024      container.appendChild(img);
2025      images.push(img);
2026    });
2027    
2028    // Height calculation removed - container height is now determined by CSS
2029    
2030    // Simple fade slideshow
2031    let currentIndex = 0;
2032    function showNext() {
2033      images[currentIndex].classList.remove('is-active');
2034      currentIndex = (currentIndex + 1) % images.length;
2035      images[currentIndex].classList.add('is-active');
2036    }
2037    
2038    // Start after 3 seconds, then change every 4 seconds
2039    setTimeout(function() {
2040      setInterval(showNext, 4000);
2041    }, 3000);
2042  });
2043})();
2044
2045// --- Social media caption truncation
2046(function(){
2047  const maxLen = 700;
2048  document.querySelectorAll('.social-media__caption-text').forEach(function(el){
2049    const text = (el.textContent || '').trim();
2050    if (text.length > maxLen) {
2051      el.textContent = text.slice(0, maxLen).trimEnd() + '...';
2052    }
2053  });
2054})();
2055
2056// --- Matomo: FlippingBook Publisher (embed.js / .fbp-embed) – Öffnen & Download
2057(function () {
2058  var lastFbMs = {};
2059  function trackFlippingBookMatomo(action, url) {
2060    if (typeof window._paq === 'undefined' || typeof window._paq.push !== 'function') return;
2061    if (!url || typeof url !== 'string') return;
2062    var base = url.split('#')[0].split('?')[0];
2063    var key = action + '|' + base;
2064    var minGap = action === 'Download' ? 1500 : 900;
2065    var t = Date.now();
2066    if (lastFbMs[key] && t - lastFbMs[key] < minGap) return;
2067    lastFbMs[key] = t;
2068    window._paq.push(['trackEvent', 'Magazin', action, base]);
2069  }
2070
2071  function isFlippingBookIframeSrc(src) {
2072    if (!src || src.indexOf('embed=true') === -1) return false;
2073    return /kleininteractive\.com|\/?ausgaben\//i.test(src);
2074  }
2075
2076  document.addEventListener('click', function (e) {
2077    var a = e.target && e.target.closest && e.target.closest('a.fb-embed-link, a.fbp-embed');
2078    if (!a || !a.href) return;
2079    trackFlippingBookMatomo('Open', a.href);
2080  }, true);
2081
2082  window.addEventListener('message', function (ev) {
2083    var d = ev.data;
2084    if (!d || typeof d !== 'object' || typeof d.fbPublicationUrl !== 'string') return;
2085    var url = d.fbPublicationUrl;
2086    if (d.action === 'openMobileLightbox') {
2087      trackFlippingBookMatomo('Open', url);
2088      return;
2089    }
2090    if (d.action === 'closeLightbox') return;
2091    var act = String(d.action || '');
2092    if (/download/i.test(act) || d.type === 'download' || d.download === true) {
2093      trackFlippingBookMatomo('Download', url);
2094      return;
2095    }
2096    if (d.event && /download/i.test(String(d.event))) {
2097      trackFlippingBookMatomo('Download', url);
2098    }
2099  }, false);
2100
2101  if (typeof MutationObserver === 'undefined') return;
2102  var iframeSeen = {};
2103  function onIframe(el) {
2104    if (!el || el.tagName !== 'IFRAME') return;
2105    var src = el.src || el.getAttribute('src') || '';
2106    if (!isFlippingBookIframeSrc(src)) return;
2107    var id = src.split('#')[0];
2108    if (iframeSeen[id]) return;
2109    iframeSeen[id] = true;
2110    trackFlippingBookMatomo('Open', src);
2111  }
2112  function scanNode(n) {
2113    if (!n || n.nodeType !== 1) return;
2114    if (n.tagName === 'IFRAME') onIframe(n);
2115    var q = n.querySelectorAll && n.querySelectorAll('iframe');
2116    if (!q) return;
2117    for (var i = 0; i < q.length; i++) onIframe(q[i]);
2118  }
2119  var mo = new MutationObserver(function (muts) {
2120    for (var i = 0; i < muts.length; i++) {
2121      var nodes = muts[i].addedNodes;
2122      for (var j = 0; j < nodes.length; j++) scanNode(nodes[j]);
2123    }
2124  });
2125  var start = function () {
2126    if (document.body) mo.observe(document.body, { childList: true, subtree: true });
2127  };
2128  if (document.body) start();
2129  else document.addEventListener('DOMContentLoaded', start);
2130})();
2131// Personen-Profil: Foto-Slideshow im Hero
2132// Bullets, Autoplay, Swipe (Touch via Scroll-Snap), Klick blättert auf Geräten mit Maus weiter.
2133(function(){
2134  function init(){
2135    document.querySelectorAll('.person-profil-hero__slider').forEach(function(slider){
2136      var scroller = slider.querySelector('.person-profil-hero__slides');
2137      var bulletsWrap = slider.querySelector('.person-profil-hero__bullets');
2138      if(!scroller) return;
2139
2140      var slides = Array.prototype.slice.call(scroller.children);
2141      if(slides.length < 2) return;
2142
2143      slider.classList.add('has-multiple');
2144
2145      var current = 0;
2146      var bullets = [];
2147
2148      function goTo(i, behavior){
2149        var n = slides.length;
2150        i = ((i % n) + n) % n;
2151        scroller.scrollTo({ left: i * scroller.clientWidth, behavior: behavior || 'smooth' });
2152      }
2153
2154      if(bulletsWrap){
2155        slides.forEach(function(_, i){
2156          var b = document.createElement('button');
2157          b.type = 'button';
2158          b.className = 'person-profil-hero__bullet' + (i === 0 ? ' is-active' : '');
2159          b.setAttribute('aria-label', 'Foto ' + (i + 1) + ' von ' + slides.length);
2160          b.addEventListener('click', function(){ disableAutoplay(); goTo(i); });
2161          bulletsWrap.appendChild(b);
2162          bullets.push(b);
2163        });
2164      }
2165
2166      function setActive(i){
2167        if(i === current) return;
2168        current = i;
2169        bullets.forEach(function(b, k){ b.classList.toggle('is-active', k === i); });
2170      }
2171
2172      var scrollTimer = null;
2173      scroller.addEventListener('scroll', function(){
2174        if(scrollTimer) clearTimeout(scrollTimer);
2175        scrollTimer = setTimeout(function(){
2176          setActive(Math.round(scroller.scrollLeft / scroller.clientWidth));
2177        }, 80);
2178      }, { passive: true });
2179
2180      // Klick blättert weiter — nur auf Geräten ohne Touch (Maus/Trackpad)
2181      if(window.matchMedia('(hover: hover) and (pointer: fine)').matches){
2182        scroller.addEventListener('click', function(){ disableAutoplay(); goTo(current + 1); });
2183      }
2184
2185      // Autoplay: alle 5s weiter bis zum letzten Foto, dort endet es (kein Rücksprung
2186      // zum Anfang); pausiert außerhalb des Viewports / bei verstecktem Tab.
2187      // Sobald der Nutzer selbst blättert (Klick, Bullet, Swipe, Scrollrad), endet es dauerhaft.
2188      var AUTOPLAY_MS = 5000;
2189      var timer = null;
2190      var inView = true;
2191      var userTookOver = false;
2192      var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
2193
2194      function startAutoplay(){
2195        if(reduceMotion || userTookOver || timer || !inView || document.hidden) return;
2196        if(current >= slides.length - 1) return;
2197        timer = setInterval(function(){
2198          if(current >= slides.length - 1){ disableAutoplay(); return; }
2199          goTo(current + 1);
2200        }, AUTOPLAY_MS);
2201      }
2202      function stopAutoplay(){ if(timer){ clearInterval(timer); timer = null; } }
2203      function disableAutoplay(){ userTookOver = true; stopAutoplay(); }
2204
2205      if('IntersectionObserver' in window){
2206        new IntersectionObserver(function(entries){
2207          inView = entries[0].isIntersecting;
2208          if(inView){ startAutoplay(); } else { stopAutoplay(); }
2209        }, { threshold: 0.4 }).observe(slider);
2210      }
2211      document.addEventListener('visibilitychange', function(){
2212        if(document.hidden){ stopAutoplay(); } else { startAutoplay(); }
2213      });
2214      scroller.addEventListener('touchstart', disableAutoplay, { passive: true });
2215      scroller.addEventListener('wheel', disableAutoplay, { passive: true });
2216
2217      window.addEventListener('resize', function(){ goTo(current, 'auto'); }, { passive: true });
2218
2219      startAutoplay();
2220    });
2221  }
2222
2223  if(document.readyState === 'loading'){
2224    document.addEventListener('DOMContentLoaded', init);
2225  } else {
2226    init();
2227  }
2228})();

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.