PageSourceSearch

https://theicelandtrail.com/wp-content/plugins/tbx-plugin/blox/frontend-scripts.js?ver=1791448353

js theicelandtrail.com collected 2026-10-08 10:24:40 UTC 79,731 bytes, 1,999 lines download raw bytes

1// requires tb-base.js to be loaded first
2
3// True in the admin document and in the iframed editor canvas (whose body has no
4// `wp-admin` class). Lazy: in the canvas this script runs before <body> exists.
5function tbIsAdmin() {
6    return !!document.body?.matches('.wp-admin, .editor-styles-wrapper');
7}
8// The element that carries the --tb-* variables: .tb-root (the body, or the
9// connector's own wrapper when our markup is embedded elsewhere). css/style.scss
10// declares its static fallbacks there too, and a declaration on that element
11// shadows anything set on html — so publishing a measurement on :root would never
12// reach the page. Lazy for the same reason as tbIsAdmin(): in the iframed editor
13// canvas this script runs before <body> exists.
14function tbCssRoot() {
15    return document.querySelector('.tb-root') || document.documentElement;
16}
17
18tbPopup = {
19    bodyElement: null,
20    popupElement: null,
21    popupContentElement: null,
22    popupTriggers: null,
23    initialized: false,
24    startingIndex: 0,
25    clickedImageInGallery: 0,
26
27    init() {
28        if (!this.initialized) {
29            this.bodyElement = document.querySelector(tb.BODY_SELECTOR);
30            this.popupElement = document.querySelector('#tb-popup');
31            this.popupTriggers = document.querySelectorAll('.tb-popup-trigger');
32            // The editor canvas iframe has no #tb-popup — that dialog lives in the admin document.
33            this.popupContentElement = this.popupElement?.querySelector('.tb-popup');
34            this.initialized = true;
35        }
36    },
37
38    showPopup(parent) {
39        this.popupContentElement.innerHTML = '';
40        this.popupContentElement.append(...parent.childNodes);
41        if (!this.popupElement.open) {
42            this.popupElement.showModal();
43        }
44        this.popupElement.originalParent = parent;
45
46        // copy classes from parent to popup, except for tb-popup
47        this.popupElement.classList.add(...parent.classList);
48        this.popupElement.classList.remove('tb-popup');
49
50        // add class to body to prevent scrolling
51        this.bodyElement.classList.add('popup-is-open');
52
53        // register click events for previous and next buttons
54        const prevButton = this.popupElement.querySelector('.tb-popup-previous');
55        const nextButton = this.popupElement.querySelector('.tb-popup-next');
56
57        // check if there is a slider in the popup. If so, make sure the scroll nav is up to date
58        const sliderEntries = this.popupElement.querySelector('.tb-carousel-slider .entries');
59        if (sliderEntries) {
60            if (this.popupElement.classList.contains('tb-popup--open-clicked-image')) {
61                sliderEntries.scrollTo({
62                    left:
63                        (sliderEntries.children[0].offsetWidth + sliderEntries.gap) *
64                        (clickedImageInGallery - startingIndex) *
65                        sliderEntries.numEntriesPerView,
66                });
67                setTimeout(() => {
68                    tbCarousel.updateScrollNav(sliderEntries);
69                }, '100');
70            }
71        }
72
73        if (nextButton) {
74            nextButton.addEventListener('click', tbPopup.handleNextPopupClick);
75            // if it's the last element, hide the next button
76            if (!parent.closest('.tb-popup-trigger').nextElementSibling) {
77                nextButton.style.visibility = 'hidden';
78            }
79        }
80
81        if (prevButton) {
82            prevButton.addEventListener('click', tbPopup.handlePreviousPopupCl
82ick);
83            // if it's the first element, hide the previous button
84            if (!parent.closest('.tb-popup-trigger').previousElementSibling) {
85                prevButton.style.visibility = 'hidden';
86            }
87        }
88
89        // Emit event to allow other scripts to hook into the popup opening
90        const event = new CustomEvent('tbPopupOpened', { bubbles: true });
91        parent.dispatchEvent(event);
92    },
93
94    closePopup(e = null) {
95        // remove click events for previous and next buttons
96        const prevButton = this.popupElement.querySelector('.tb-popup-previous');
97        const nextButton = this.popupElement.querySelector('.tb-popup-next');
98        if (prevButton) {
99            prevButton.removeEventListener('click', this.handlePreviousPopupClick);
100        }
101        if (nextButton) {
102            nextButton.removeEventListener('click', this.handleNextPopupClick);
103        }
104
105        // restore content to original parent
106        if (this.popupElement.originalParent) {
107            this.popupElement.originalParent.append(...this.popupContentElement.childNodes);
108        }
109
110        if (
111            e &&
112            e.target &&
113            (e.target.classList.contains('tb-popup-next') || e.target.classList.contains('tb-popup-previous'))
114        ) {
115            return;
116        }
117
118        // If not next or previous button. tb-root has to survive: it is what scopes the
119        // --tb-* variables onto this dialog, which lives outside the .tb-root wrapper
120        // whenever our markup is embedded through the Connector.
121        this.popupElement.className = 'tb-root';
122        // Remove class from body to allow scrolling
123        this.bodyElement.classList.remove('popup-is-open');
124
125        this.popupElement.classList.add('hide');
126
127        let closed = false;
128        const closeDialog = () => {
129            if (closed) return;
130            closed = true;
131            this.popupElement.classList.remove('hide');
132            this.popupElement.close();
133        };
134
135        this.popupElement.addEventListener('animationend', closeDialog, { once: true });
136        setTimeout(closeDialog, 300);
137
138        // Emit event to allow other scripts to hook into the popup closing
139        const event = new CustomEvent('tbPopupClosed', { bubbles: true });
140        this.popupElement.originalParent.dispatchEvent(event);
141    },
142
143    // Swap the open dialog's content for another block's, keeping it open.
144    // The tb-popup-previous/next nav only works for triggers that are siblings
145    // and wrap their own dialog; blocks laid out otherwise step through here.
146    swapPopup(parent) {
147        if (!this.popupElement || !this.popupElement.open || !this.popupElement.originalParent) {
148            return;
149        }
150        this.popupElement.originalParent.append(...this.popupContentElement.childNodes);
151        this.showPopup(parent);
152        this.popupContentElement.scrollTop = 0;
153    },
154
155    handlePreviousPopupClick(e) {
156        tbPopup.handlePrevNextClick(e, 'previous');
157    },
158
159    handleNextPopupClick(e) {
160        tbPopup.handlePrevNextClick(e, 'next');
161    },
162
163    handlePrevNextClick(e, direction) {
164        e.preventDefault();
165        const sibling = this.popupElement.originalParent.closest('.tb-popup-trigger')[direction + 'ElementSibling'];
166        if (sibling) {
167            this.closePopup(e);
168            this.showPopup(sibling.querySelector('.tb-popup'));
169        }
170    },
171
172    enableTriggers(element) {
173        this.init();
174
175        if (!this.popupElement) {
176            return;
177        }
178
179        const popupTriggers = element.querySelectorAll('.tb-popup-trigger');
180
181        const triggerEventHandler = (e) => {
182            this.popupTriggerEventHandler(element, e.currentTarget);
183        };
184
185        for (const trigger of popupTriggers) {
186            // Remove any existing event listeners to prevent duplicates
187            trigger.removeEventListener('click', trigger._triggerEventHandler);
188            trigger._triggerEventHandler = triggerEventHandler;
189            trigger.addEventListener('click', triggerEventHandler);
190        }
191
192        const popupClickHandler = (e) => {
193            if (e.target.matches('.tb-popup-close,.tb-popup-close *,#tb-popup')) {
194                tbPopup.closePopup();
195            }
196        };
197
198        const popupCancelHandler = (e) => {
199            e.preventDefault();
200            tbPopup.closePopup();
201        };
202
203        // Remove any existing event listeners to prevent duplicates
204        this.popupElement.removeEventListener('click', this.popupElement._popupClickHandler);
205        this.popupElement._popupClickHandler = popupClickHandler;
206        this.popupElement.addEventListener('click', popupClickHandler);
207
208        this.popupElement.removeEventListener('cancel', this.popupElement._popupCancelHandler);
209        this.popupElement._popupCancelHandler = popupCancelHandler;
210        this.popupElement.addEventListener('cancel', popupCancelHandler);
211    },
212
213    popupTriggerEventHandler(element, trigger) {
214        const popupTriggers = element.querySelectorAll('.tb-popup-trigger');
215
216        const thesePopupTriggers = trigger.parentElement.parentElement.querySelectorAll('.tb-popup-trigger');
217        startingIndex = Array.from(popupTriggers).indexOf(thesePopupTriggers[0]);
218        clickedImageInGallery = Array.from(popupTriggers).indexOf(trigger);
219        if (trigger.closest('.popup-on-readmore.is-text-complete, .popup-on-readmore.is-readmore-inline')) {
220            // the card shows the whole text already, or unfolds it in place
221            return;
222        }
223        if (trigger.closest('.popup-on-readmore') && trigger.closest('.is-mobile-width')) {
224            // At mobile width the card holds its own gallery slider, so a tap on the card
225            // must not open the popup — the read more link does that (see below).
226            return;
227        }
228
229        // find popup content element
230        let popupContentElement = trigger.querySelector('.tb-popup');
231
232        if (!popupContentElement) {
233            // if not found within the triggering element, look for closest parent with class tb-has-popup-content
234            if ((popupContentElement = trigger.closest('.tb-has-popup-content'))) {
235                popupContentElement = trigger.closest('.tb-has-popup-content').querySelector('.tb-popup');
236            }
237        }
238
239        tbPopup.showPopup(popupContentElement);
240    },
241
242    initLangSwitchSuggestion(element) {
243        // only proceed if TRP is activated
244        if (!tb.TRP_IS_ACTIVATED) {
245            return;
246        }
247
248        // only proceed if is frontend
249        if (tbIsAdmin()) {
250            return;
251        }
252
253        // only proceed if it's not a TB Connector request
254        if (tb.IS_TB_CONNECTOR_REQUEST) {
255            return;
256        }
257
258        // if the cookie doesn't exist yet, create it (so CS scanner picks it up)
259        if (!tb.cookieExists('tb_language_switcher_dismissed')) {
260            tb.setCookie(
261                'tb_language_switcher_dismissed',
262                '0',
263                '/',
264                7, // expire in days
265            );
266        }
267
268        // only proceed if cookie actually exists (CookieScript can block the creation of it) or if it's not "1"
269        if (
270            !tb.cookieExists('tb_language_switcher_dismissed') ||
271            tb.getCookie('tb_language_switcher_dismissed') === '1'
272        ) {
273            return;
274        }
275
276        const bodyElement = element.querySelector(tb.BODY_SELECTOR);
277        const urlData = {
278            request_uri: window.location.origin + window.location.pathname,
279            querystring: window.location.search,
280        };
281
282        // cookie set and not "1" so get the dialog
283        fetch(tb.AJAX_URL, {
284            method: 'POST',
285            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
286            body: new URLSearchParams({
287                action: 'tb_load_lang_switcher_dialog',
288                url: JSON.stringify(urlData),
289                tb_current_language: tb.CURRENT_LANGUAGE,
290            }),
291        })
292            .then((response) => response.json())
293            .then((data) => {
294                if (data.success && data.data.html) {
295                    // Insert the template HTML directly into the page
296                    let lang_switch_container = element.createElement('div');
297                    lang_switch_container.innerHTML = data.data.html; // Convert HTML string to DOM nodes
298                    document.body.appendChild(lang_switch_container.firstElementChild);
299                    const langSwitchModal = element.querySelector('.tb-popup-lang-switcher');
300
301                    if (langSwitchModal) {
302                        bodyElement.classList.add('popup-is-open');
303                        langSwitchModal.showModal();
304
305                        const cookieName = langSwitchModal.getAttribute('data-dialog-dismiss-cookie');
306                        const popupClickHandler = (e) => {
307                            if (
308                                e.target.matches(
309                                    '.tb-popup-close,.tb-popup-close *,.tb-popup-lang-switcher,#dialog-dismiss',
310                                )
311                            ) {
312                                bodyElement.classList.remove('popup-is-open');
313                                langSwitchModal.close();
314                                tb.setCookie(
315                                    'tb_language_switcher_dismissed',
316                                    1,
317                                    '/',
318                                    7, // expire in days
319                                );
320                            }
321
322                            if (e.target.matches('#dialog-switch-language-button')) {
323                                e.preventDefault();
324                                const selectedRadio = langSwitchModal.querySelector(
325                                    '.dialog-switch-language-options .radio-option input:checked',
326                                );
327                                if (selectedRadio) {
328                                    const url = selectedRadio.getAttribute('data-url');
329                                    if (url) {
330                                        const isCurrentLang = selectedRadio.getAttribute('data-current-lang') !== null;
331                                        //if (cookieName && isCurrentLang) {
332                                        // only save cookie if selection is also current lang
333                                        tb.setCookie(
334                                            'tb_language_switcher_dismissed',
335                                            1,
336                                            '/',
337                                            7, // expire in days
338                                        );
339                                        //}
340                                        window.location.href = url; // Redirect to the URL
341                                    }
342                                }
343                            }
344                        };
345
346                        const popupCancelHandler = (e) => {
347                            e.preventDefault();
348                            bodyElement.classList.remove('popup-is-open');
349                            element.close();
350                        };
351
352                        // Remove any existing event listeners to prevent duplicates
353                        langSwitchModal.removeEventListener('click', langSwitchModal._popupClickHandler);
354                        langSwitchModal._popupClickHandler = popupClickHandler;
355                        langSwitchModal.addEventListener('click', popupClickHandler);
356
357                        langSwitchModal.removeEventListener('cancel', langSwitchModal._popupCancelHandler);
358                        langSwitchModal._popupCancelHandler = popupCancelHandler;
359                        langSwitchModal.addEventListener('cancel', popupCancelHandler);
360                    }
361                }
362            })
363            .catch(() => {
364                // Fetch can fail when the user navigates away mid-request (iOS "Load failed").
365                // Non-critical: the language suggestion simply won't show.
366            });
367    },
368};
369
370tbCarousel = {
371    init(carouselContainer) {
372        // Guard against double init: observers/handlers would stack on the same element.
373        if (carouselContainer.dataset.tbCarouselInitialized) {
374            return;
375        }
376        carouselContainer.dataset.tbCarouselInitialized = '1';
377
378        // make navigation buttons visible when content is wider than container
379        const navElement = carouselContainer.querySelector(':scope > .tb-carousel-nav');
380        if (!navElement) {
381            return;
382        }
383
384        //slide to the right image
385        const firstImageInSlider = carouselContainer.querySelector('.entries').dataset.sliderFirst;
386        const entriesElement = carouselContainer.querySelector(':scope > .entries');
387
388        entriesElement.navElement = navElement;
389        entriesElement.carouselContainer = carouselContainer;
390
391        entriesElement.prevButton = navElement.querySelector('.prev');
392        entriesElement.nextButton = navElement.querySelector('.next');
393
394        this.updateChildList(entriesElement);
395
396        tb.onElementResized(entriesElement, (entry) => {
397            this.updateSize(entriesElement);
398        });
399
400        // recalculate when child elements get removed or added
401        const mo = new MutationObserver((mutationList, observer) => {
402            const mutation = mutationList[0];
403            const entriesElement = mutation.target.closest('.entries');
404            this.updateChildList(entriesElement);
405        });
406        mo.observe(entriesElement, {
407            childList: true,
408            attributes: false,
409            subtree: true,
410        });
411
412        entriesElement.addEventListener('scroll', (e) => this.handleScroll(e));
413
414        navElement.addEventListener('click', (e) => {
415            if (e.target.closest('div').matches('.prev')) {
416                entriesElement.scrollTo({
417                    left:
418                        entriesElement.scrollLeft -
419                        (entriesElement.entryWidth - entriesElement.gap) * entriesElement.numEntriesPerView,
420                    behavior: 'smooth',
421                });
422            }
423            if (e.target.closest('div').matches('.next')) {
424                entriesElement.scrollTo({
425                    left:
426                        entriesElement.scrollLeft +
427                        (entriesElement.entryWidth + entriesElement.gap) * entriesElement.numEntriesPerView,
428                    behavior: 'smooth',
429                });
430            }
431            if (e.target.matches('.dot')) {
432                const index = parseInt(e.target.dataset.index);
433                entriesElement.scrollTo({
434                    left: (entriesElement.entryWidth + entriesElement.gap) * index * entriesElement.numEntriesPerView,
435                    behavior: 'smooth',
436                });
437            }
438        });
439    },
440
441    handleScroll(e) {
442        this.updateScrollNav(e.currentTarget);
443    },
444
445    updateScrollNav(entriesElement) {
446        // get scroll x position
447        const scrollX = entriesElement.scrollLeft;
448        const activeIndex = Math.ceil(
449            scrollX /
450                ((entriesElement.entryWidth + entriesElement.gap) * entriesElement.numEntriesPerView +
451                    entriesElement.gap),
452        );
453        // set active dot
454        if (entriesElement.dotsElement) {
455            const activeDot = entriesElement.dotsElement.querySelector('.dot.active');
456            if (activeDot) {
457                activeDot.classList.remove('active');
458            }
459            const newActiveDot = entriesElement.dotsElement.querySelector(`.dot[data-index="${activeIndex}"]`);
460            if (newActiveDot) {
461                newActiveDot.classList.add('active');
462            }
463
464            // disable prev button if first dot is active.
465            const firstDot = entriesElement.dotsElement.querySelector('.dot');
466            if (firstDot) {
467                if (entriesElement.prevButton) {
468                    if (firstDot.classList.contains('active')) {
469                        entriesElement.prevButton.classList.add('disabled');
470                    } else {
471                        entriesElement.prevButton.classList.remove('disabled');
472                    }
473                }
474            }
475
476            // disable next button if last dot is active.
477            const lastDot = entriesElement.dotsElement.querySelector('.dot:last-child');
478            if (lastDot) {
479                if (entriesElement.nextButton) {
480                    if (lastDot.classList.contains('active')) {
481                        entriesElement.nextButton.classList.add('disabled');
482                    } else {
483                        entriesElement.nextButton.classList.remove('disabled');
484                    }
485                }
486            }
487        }
488    },
489
490    updateSize(entriesElement) {
491        const entriesWidth = entriesElement.scrollWidth;
492        if (entriesElement.firstEntryElement) {
493            entriesElement.entryWidth = entriesElement.firstEntryElement.offsetWidth;
494
495            const carouselContainerWidth = entriesElement.carouselContainer.clientWidth;
496            if (entriesWidth > carouselContainerWidth + 40) {
497                
498                entriesElement.style.paddingBottom = '';
499                entriesElement.navElement.style.opacity = 1;
500                entriesElement.navElement.style.display = 'flex';
501
502                // calculate how many entries fit in the container
503                entriesElement.numEntriesPerView = this.calculateNumEntriesPerView(entriesElement);
504
505                //remove snap-here class from all entries
506                entriesElement.querySelectorAll(':scope > .entry').forEach((el) => {
507                    el.classList.remove('snap-here');
508                });
509
510                entriesElement.entryElements.forEach((el, index) => {
511                    // give every first element in a "view" the class snap-here
512                    if ((index + entriesElement.numEntriesPerView) % entriesElement.numEntriesPerView === 0) {
513                        el.classList.add('snap-here');
514                    }
515                });
516
517                const numViews = Math.ceil(entriesElement.numEntries / entriesElement.numEntriesPerView);
518                const dotsHtml = [...Array(numViews).keys()]
519                    .map((i) => `<div class="dot ${i == 0 ? ' active' : ''}" data-index="${i}"></div>`)
520                    .join('');
521                if (entriesElement.dotsElement) {
522                    entriesElement.dotsElement.innerHTML = dotsHtml;
523                }
524                entriesElement.classList.remove('no-overflow');
525            } else {
526                entriesElement.style.paddingBottom = '20px';
527                entriesElement.navElement.style.opacity = 0;
528                entriesElement.navElement.style.display = 'none';
529                entriesElement.classList.add('no-overflow');
530            }
531        }
532    },
533
534    calculateContentWidth(entriesElement) {
535        const navEl = entriesElement.navElement;
536        if (!(navEl instanceof Element)) {
537            return 0;
538        }
539        const computedStyle = getComputedStyle(navEl);
540        let elementWidth = navEl.clientWidth; // width with padding
541        elementWidth -= parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
542        return elementWidth;
543    },
544
545    calculateNumEntriesPerView(entriesElement) {
546        const contentWidth = this.calculateContentWidth(entriesElement);
547        return Math.max(1, Math.floor(contentWidth / (entriesElement.entryWidth + entriesElement.gap)));
548    },
549
550    updateChildList(entriesElement) {
551        if (!(entriesElement instanceof Element)) {
552            return;
553        }
554        entriesElement.entryElements = entriesElement.querySelectorAll(':scope > .entry:not(.hidden)');
555        entriesElement.firstEntryElement = entriesElement.entryElements[0];
556        entriesElement.numEntries = entriesElement.entryElements.length;
557        entriesElement.entryWidth = entriesElement.entryWidth || 100;
558        entriesElement.dotsElement = entriesElement.navElement.querySelector('.dots');
559        entriesElement.gap = parseInt(window.getComputedStyle(entriesElement).gap) || 0;
560        entriesElement.numEntriesPerView = this.calculateNumEntriesPerView(entriesElement);
561
562        this.updateSize(entriesElement);
563    },
564};
565
566tb.onRootElementReady((tbRootElement) => {
567    // -----------------------------------
568    // Add mobile / desktop classes to body and set current body width as CSS variable
569    // -----------------------------------
570
571    // The observer also fires when only the height changes — an accordion o
571pening,
572    // a mobile browser hiding its address bar. Listeners relayout on this event, so
573    // it goes out on a real width change only.
574    let lastMeasuredWidth = null;
575
576    tb.onElementResized(tbRootElement, (entry) => {
577        // this is a bit funky, but necessary to keep it in sync with media queries while also keeping support for admin editor
578        const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
579        const width = entry.contentRect.width + scrollBarWidth;
580        const widthChanged = width !== lastMeasuredWidth;
581        lastMeasuredWidth = width;
582
583        if (width <= tb.MOBILE_BREAKPOINT) {
584            tbRootElement.classList.add('is-mobile-width');
585            tbRootElement.classList.remove('is-desktop-width');
586            if (widthChanged) {
587                tbRootElement.dispatchEvent(new CustomEvent('tb-width-changed', { detail: { width: 'mobile' } }));
588            }
589        } else {
590            tbRootElement.classList.add('is-desktop-width');
591            tbRootElement.classList.remove('is-mobile-width');
592            if (widthChanged) {
593                tbRootElement.dispatchEvent(new CustomEvent('tb-width-changed', { detail: { width: 'desktop' } }));
594            }
595        }
596        tbCssRoot().style.setProperty('--tb-dynamic-body-width', width + 'px');
597    });
598
599    // -----------------------------------
600    // Add platform classes to body
601    // -----------------------------------
602    var iOS =
603        (/iPad|iPhone|iPod/.test(navigator.platform) ||
604            (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
605        !window.MSStream;
606    if (iOS) {
607        tbRootElement.classList.add('platform-ios');
608    }
609
610    // -----------------------------------
611    // Add classes to body with current scroll state (for sticky header)
612    // -----------------------------------
613    const bodyElement = document.querySelector(tb.BODY_SELECTOR);
614
615    let scrollPosition = 0;
616    let needsScrollUpdate = true;
617    let scrollStarted = false;
618    let lastScrollDirection = '';
619
620    // Observe the elements
621    // The hero the scroll threshold is measured against: a tb/header block, or
622    // the theme-rendered header on single posts. Templates like the RTA guide
623    // article are `single` but render a real tb/header — falling back keeps the
624    // nav transparent there instead of flagging the page as heroless.
625    // `.tb-nav-hero` is the opt-in hook for theme templates that paint their own
626    // hero band (404) and want the transparent nav on top of it.
627    let headerEl = null;
628    if (document.querySelector('body').classList.contains('single')) {
629        headerEl = document.querySelector('.tb-page-blog-post__header');
630    }
631    if (!headerEl) {
632        headerEl = document.querySelector('.tb-header') || document.querySelector('.tb-nav-hero');
633    }
634
635    let subNavEl = document.querySelector('.tb-header__subnav');
636    // The fixed top nav. Its full (unscrolled) height feeds --tb-header-height-full;
637    // it shrinks to --tb-header-height once the user scrolls (scrolled-1-3-header).
638    let siteHeaderEl = document.querySelector('#header');
639    let headerHeight,
640        subNavHeight = 0;
641
642    // Measure the fixed nav in its full state and publish it as a CSS variable so
643    // nothing has to hardcode the height. Only refresh while at the top of the page,
644    // because once scrolled the header is collapsed and would report the small height.
645    // When a re-measure is requested while scrolled (header collapsed), we can't
646    // measure the full height yet. Remember it and do it once we're back at top.
647    let pendingFullHeaderMeasure = false;
648    let fullHeaderMeasureTimeout;
649    function updateFullHeaderHeight() {
650        if (!siteHeaderEl) return;
651        if (window.scrollY !== 0) {
652            pendingFullHeaderMeasure = true;
653            return;
654        }
655        pendingFullHeaderMeasure = false;
656        // .site-header transitions for 0.2s (its padding changes across the desktop
657        // breakpoint); measuring mid-transition would publish a stale height.
658        clearTimeout(fullHeaderMeasureTimeout);
659        fullHeaderMeasureTimeout = setTimeout(() => {
660            tbCssRoot().style.setProperty('--tb-header-height-full', siteHeaderEl.offsetHeight + 'px');
661        }, 250);
662    }
663
664    // Function to update heights
665    function updateHeightsForNav() {
666        headerHeight = (headerEl && headerEl.offsetHeight) || 0;
667        subNavHeight = (subNavEl && subNavEl.offsetHeight) || 0;
668        updateFullHeaderHeight();
669    }
670
671    let lastObservedWidth = document.documentElement.clientWidth;
672    const resizeObserver = new ResizeObserver(() => {
673        // ResizeObserver also fires on height changes (scroll collapse, subnav,
674        // late media). We only react to WIDTH changes; bail otherwise.
675        const width = document.documentElement.clientWidth;
676        if (width === lastObservedWidth) return;
677        lastObservedWidth = width;
678        updateHeightsForNav();
679    });
680
681    if (headerEl) {
682        resizeObserver.observe(headerEl);
683
684        // Initial call to set the heights on page load.
685        updateHeightsForNav();
686
687        if (subNavEl) {
688            resizeObserver.observe(subNavEl);
689        }
690    } else {
691        // No hero/header element to measure against, so the scroll threshold
692        // (headerHeight) never becomes defined and the nav would otherwise stay
693        // transparent forever. Give it the solid background from the start.
694        tbRootElement.classList.add('nav-no-header');
695    }
696
697    // Keep --tb-header-height-full correct even on pages without a hero header, and
698    // when the viewport changes while scrolled (re-measured next time we hit the top).
699    if (siteHeaderEl) {
700        resizeObserver.observe(siteHeaderEl);
701        updateFullHeaderHeight();
702    }
703
704    const scrollClasses = ['scrolled-to-top', 'scrolled-down', 'scrolled-up'];
705    tbRootElement.classList.add('scrolled-to-top');
706    document.addEventListener(
707        'scroll',
708        function () {
709            // call handleScroll function only once every 100ms
710            if (needsScrollUpdate) {
711                needsScrollUpdate = false;
712                window.handleScrollTimeout = setTimeout(function () {
713                    handleScroll(scrollStarted ? 'scrolling' : 'scrollStart');
714                    needsScrollUpdate = true;
715                    scrollStarted = true;
716                }, 100);
717            }
718
719            // call handleScroll function a final time when scrolling ends
720            clearTimeout(window.handleScrollEndTimeout);
721            window.handleScrollEndTimeout = setTimeout(function () {
722                handleScroll('scrollEnd');
723                scrollStarted = false;
724            }, 100);
725        },
726        { passive: true, capture: false },
727    );
728
729    function handleScroll(type = 'scrolling') {
730        let scrollDirection = lastScrollDirection;
731        if (window.scrollY > scrollPosition) {
732            scrollDirection = 'down';
733        } else if (window.scrollY < scrollPosition) {
734            scrollDirection = 'up';
735        }
736
737        if (window.scrollY == 0 && type == 'scrollEnd') {
738            tbRootElement.classList.remove(...scrollClasses);
739            tbRootElement.classList.add('scrolled-to-top');
740            // Back at the top: only re-measure if a width change was requested
741            // while we were scrolled (header collapsed). Otherwise nothing changed
742            // and the value is still correct — don't update on every scroll-to-top.
743            if (pendingFullHeaderMeasure) {
744                updateFullHeaderHeight();
745            }
746        } else if (scrollDirection !== lastScrollDirection) {
747            tbRootElement.classList.remove(...scrollClasses);
748            tbRootElement.classList.add('scrolled-' + scrollDirection);
749        }
750
751        lastScrollDirection = scrollDirection;
752        scrollPosition = window.scrollY;
753
754        //topnav should disappear when 1/3 the header
755        if (window.scrollY > headerHeight / 3) {
756            tbRootElement.classList.add('scrolled-1-3-header');
757        } else {
758            tbRootElement.classList.remove('scrolled-1-3-header');
759        }
760
761        //topnav should disappear when halfway the header
762        if (window.scrollY > headerHeight / 2) {
763            tbRootElement.classList.add('scrolled-halfway-header');
764        } else {
765            tbRootElement.classList.remove('scrolled-halfway-header');
766        }
767
768        //subnav
769        if (window.scrollY > headerHeight) {
770            tbRootElement.classList.add('scrolled-past-header');
771        } else {
772            tbRootElement.classList.remove('scrolled-past-header');
773        }
774
775        if (window.scrollY > headerHeight + subNavHeight) {
776            tbRootElement.classList.add('scrolled-past-subnav');
777        } else {
778            tbRootElement.classList.remove('scrolled-past-subnav');
779        }
780    }
781
782    // -----------------------------------
783    // Popups with triggers
784    // -----------------------------------
785    tbPopup.enableTriggers(document);
786
787    // -----------------------------------
788    // Language Switcher Popup
789    // -----------------------------------
790    tbPopup.initLangSwitchSuggestion(document);
791
792    // -----------------------------------
793    // Section toggler
794    // -----------------------------------
795
796    initSectionTogglers();
797
798    // -----------------------------------
799    // Scroll down button
800    // -----------------------------------
801
802    const scrollDownButton = document.querySelector('.scroll-down-button');
803    if (scrollDownButton) {
804        scrollDownButton.addEventListener('click', (e) => {
805            // get second block ID
806            const secondBlockId = document.querySelector('.tb-block:nth-child(2)').id;
807            // add block to URL as hash and scroll to it
808            window.location.hash = '';
809            window.location.hash = secondBlockId;
810            e.preventDefault();
811        });
812    }
813
814    // -----------------------------------
815    // Read more
816    // -----------------------------------
817
818    const readmoreGroups = collectReadmoreGroups(document);
819    const refreshReadmoreLinks = () => refreshReadmoreIn(document);
820
821    tbRootElement.addEventListener('tb-width-changed', refreshReadmoreLinks);
822    // a webfont swapping in changes how many lines the text takes
823    document.fonts?.ready.then(refreshReadmoreLinks);
824    // so does an image in the text arriving late
825    window.addEventListener('load', refreshReadmoreLinks);
826
827    let readmoreResizeTimer;
828    window.addEventListener('resize', () => {
829        clearTimeout(readmoreResizeTimer);
830        readmoreResizeTimer = setTimeout(refreshReadmoreLinks, 200);
831    });
832
833    setTimeout(() => {
834        refreshReadmoreLinks();
835
836        for (const entry of [...readmoreGroups.values()].flat()) {
837            const { link, content, card } = entry;
838
839            // The whole text part reacts, the read more link included. The image
840            // part stays out of it: it holds the card's own gallery slider.
841            const textPart = link.closest('.tb-card-content') || link;
842
843            textPart.addEventListener('click', (e) => {
844                // At desktop width the card itself is the popup trigger and handles the click.
845                if (!textPart.closest('.is-mobile-width')) {
846                    return;
847                }
848                // let real links and buttons in the card do their own thing
849                if (e.target.closest('a, button')) {
850                    return;
851                }
852                if (card.classList.contains('is-readmore-inline')) {
853                    e.stopPropagation();
854                    toggleReadmoreExpanded(entry);
855                    return;
856                }
857                if (!card.classList.contains('is-readmore-popup')) {
858                    // nothing to unfold: the card already shows the whole text
859                    return;
860                }
861                tbPopup.init();
862                if (!tbPopup.popupContentElement) {
863                    return;
864                }
865                e.stopPropagation();
866                tbPopup.showPopup(card.querySelector('.tb-popup'));
867            });
868        }
869    }, 500);
870
871    // -----------------------------------
872    // Readmore accordion
873    // -----------------------------------
874
875    const readmoreAccordions = document.querySelectorAll('.readmore-accordion');
876
877    for (const readmoreAccordion of readmoreAccordions) {
878        const readmoreLink = readmoreAccordion.querySelector('.readmore-accordion-link');
879        const readmoreContent = readmoreAccordion.querySelector('.readmore-accordion-content');
880        const readmoreContentItems = readmoreAccordion.querySelectorAll('.readmore-accordion-content-item');
881
882        readmoreLink.addEventListener('click', () => {
883            if (readmoreContent) {
884                var originalHeight = readmoreContent.scrollHeight;
885                readmoreContent.style.maxHeight = `${Math.ceil(originalHeight) + 10}px`;
886            }
887            if (readmoreContentItems) {
888                readmoreContentItems.forEach((item) => {
889                    item.style.maxHeight = `${Math.ceil(item.scrollHeight) + 10}px`;
890                });
891            }
892
893            if (readmoreAccordion.classList.contains('readmore-accordion--expanded')) {
894                if (readmoreContent) {
895                    readmoreContent.style.maxHeight = '0';
896                }
897                if (readmoreContentItems) {
898                    readmoreContentItems.forEach((item) => {
899                        item.style.maxHeight = '0';
900                    });
901                }
902
903                readmoreAccordion.classList.remove('readmore-accordion--expanded');
904            } else {
905                readmoreAccordion.classList.add('readmore-accordion--expanded');
906            }
907        });
908    }
909
910    // if screensize changes, reset the readmore accordion
911    tbRootElement.addEventListener('tb-width-changed', (e) => {
912        if (e.detail.width === 'desktop') {
913            for (const readmoreAccordion of readmoreAccordions) {
914                const readmoreContent = readmoreAccordion.querySelector('.readmore-accordion-content');
915                if (readmoreContent) {
916                    readmoreContent.style.maxHeight = '';
917                }
918                const readmoreContentItems = readmoreAccordion.querySelectorAll('.readmore-accordion-content-item');
919                if (readmoreContentItems) {
920                    readmoreContentItems.forEach((item) => {
921                        item.style.maxHeight = '';
922                    });
923                }
924            }
925        }
926    });
927
928    // -----------------------------------
929    // Carousel
930    // -----------------------------------
931    const carouselContainers = document.querySelectorAll('.tb-carousel-container');
932    for (const carouselContainer of carouselContainers) {
933        tbCarousel.init(carouselContainer);
934    }
935
936    // -----------------------------------
937    // Accordion
938    // -----------------------------------
939
940    class TB_AccordionItem {
941        constructor(el, parent) {
942            this.parent = parent;
943
944            // Store the <details> element
945            this.el = el;
946            // Store the <summary> element
947            this.summary = el.querySelector('.summary');
948            // Store the <div class="content"> element
949            this.content = el.querySelector('.content');
950
951            this.borderWidth = 1;
952
953            // Store the animation object (so we can cancel it if needed)
954            this.animation = null;
955            // Store if the element is closing
956            this.isClosing = false;
957            // Store if the element is expanding
958            this.isExpanding = false;
959            // Detect user clicks on the summary element
960            this.summary.addEventListener('click', (e) => this.onClick(e));
961        }
962
963        onClick(e) {
964            // Stop default behaviour from the browser
965            e.preventDefault();
966
967            // Add an overflow on the <details> to avoid content overflowing
968            this.el.style.overflow = 'hidden';
969
970            if (this.el.tagName !== 'DETAILS') {
971                // Check if the element is being closed or is already closed
972                if (this.isClosing || !this.el.classList.contains('open')) {
973                    this.open();
974                    // Check if the element is being opened or is already open
975                } else if (this.isExpanding || this.el.classList.contains('open')) {
976                    this.close();
977                }
978            } else {
979                if (this.isClosing || !this.el.open) {
980                    this.open();
981                    // Check if the element is being openned or is already open
982                } else if (this.isExpanding || this.el.open) {
983                    this.close();
984                }
985            }
986
987            // close other accordions
988            this.parent.closeOthers(this);
989        }
990
991        close() {
992            // Set the element as "being closed"
993            this.isClosing = true;
994
995            // set overflow to hidden
996            this.el.style.overflow = 'hidden';
997
998            // Store the current height of the element
999            const startHeight = `${this.el.offsetHeight}px`;
1000            // Calculate the height of the summary
1001            const endHeight = `${this.summary.offsetHeight + this.borderWidth * 2}px`;
1002
1003            // If there is already an animation running
1004            if (this.animation) {
1005                // Cancel the current animation
1006                this.animation.cancel();
1007            }
1008
1009            // Start a WAAPI animation
1010            this.animation = this.el.animate(
1011                {
1012                    // Set the keyframes from the startHeight to endHeight
1013                    height: [startHeight, endHeight],
1014                },
1015                {
1016                    duration: 200,
1017                    easing: 'ease-out',
1018                },
1019            );
1020
1021            // When the animation is complete, call onAnimationFinish()
1022            this.animation.onfinish = () => this.onAnimationFinish(false);
1023            // If the animation is cancelled, isClosing variable is set to false
1024            this.animation.oncancel = () => (this.isClosing = false);
1025        }
1026
1027        open() {
1028            // Apply a fixed height on the element
1029            this.el.style.height = `${this.el.offsetHeight}px`;
1030
1031            if (this.el.tagName == 'DETAILS') {
1032                // Force the [open] attribute on the details element
1033                this.el.open = true;
1034            } else {
1035                //set overflow to hidden
1036                this.el.style.overflow = 'hidden';
1037                //add class open to the element
1038                this.el.classList.add('open');
1039            }
1040
1041            // Wait for the next frame to call the expand function
1042            window.requestAnimationFrame(() => this.expand());
1043        }
1044
1045        expand() {
1046            // Set the element as "being expanding"
1047            this.isExpanding = true;
1048            // Get the current fixed height of the element
1049            const startHeight = `${this.el.offsetHeight}px`;
1050            // Calculate the open height of the element (summary height + content height)
1051
1052            const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight + this.borderWidth * 2}px`;
1053
1054            // If there is already an animation running
1055            if (this.animation) {
1056                // Cancel the current animation
1057                this.animation.cancel();
1058            }
1059
1060            // Start a WAAPI animation
1061            this.animation = this.el.animate(
1062                {
1063                    // Set the keyframes from the startHeight to endHeight
1064                    height: [startHeight, endHeight],
1065                },
1066                {
1067                    duration: 200,
1068                    easing: 'ease-out',
1069                },
1070            );
1071            // When the animation is complete, call onAnimationFinish()
1072            this.animation.onfinish = () => this.onAnimationFinish(true);
1073            // If the animation is cancelled, isExpanding variable is set to false
1074            this.animation.oncancel = () => (this.isExpanding = false);
1075        }
1076
1077        onAnimationFinish(open) {
1078            if (this.el.tagName == 'DETAILS') {
1079                this.el.open = open;
1080            } else {
1081                // Set the open class based on the parameter
1082                if (!open) {
1083                    this.el.classList.remove('open');
1084                }
1085            }
1086
1087            // Clear the stored animation
1088            this.animation = null;
1089            // Reset isClosing & isExpanding
1090            this.isClosing = false;
1091            this.isExpanding = false;
1092            // Remove the overflow hidden and the fixed height
1093            // this.el.style.height = "";
1094            // this.el.style.overflow = "";
1095            this.el.style.height = this.el.style.overflow = '';
1096        }
1097    }
1098
1099    class TB_Accordion {
1100        constructor(el) {
1101            this.el = el;
1102            this.items = [];
1103            this.el.querySelectorAll('.details').forEach((el) => {
1104                // Skip items broken by bad editor HTML (missing .summary/.content)
1105                if (!el.querySelector('.summary') || !el.querySelector('.content')) {
1106                    return;
1107                }
1108                this.items.push(new TB_AccordionItem(el, this));
1109            });
1110        }
1111
1112        closeOthers(item) {
1113            this.items.forEach((otherItem) => {
1114                if (otherItem.el.tagName == 'DETAILS') {
1115                    if (otherItem !== item && otherItem.el.open == true) {
1116                        otherItem.close();
1117                    }
1118                } else {
1119                    if (otherItem !== item && otherItem.el.classList.contains('open')) {
1120                        otherItem.close();
1121                    }
1122                }
1123            });
1124        }
1125    }
1126
1127    document.querySelectorAll('.tb-accordion--animation').forEach((el) => {
1128        new TB_Accordion(el);
1129    });
1130
1131    // -----------------------------------
1132    // highlight navigation items depending on scroll position
1133    // -----------------------------------
1134
1135    initIntersectionNavigation(document.querySelectorAll('.tb-block'), document.querySelector('.main-nav'), '200px');
1136    initIntersectionNavigation(
1137        document.querySelectorAll('.tb-block'),
1138        document.querySelector('.tb-header__subnav'),
1139        '50px',
1140    );
1141
1142    // -----------------------------------
1143    // Custom selects
1144    // -----------------------------------
1145
1146    initCustomSelects();
1147
1148    // -----------------------------------
1149    // Show label tooltip on hover
1150    // -----------------------------------
1151
1152    const showToolTipOnHoverItems = document.querySelectorAll('.tb-show-label-tooltip-on-hover');
1153    if (showToolTipOnHoverItems.length > 0) {
1154        showToolTipOnHoverItems.forEach((item) => {
1155            if (item.querySelector('.tb-tooltip-label')) {
1156                const itemLabel = item.querySelector('.tb-tooltip-label').innerText;
1157
1158                let tooltipElement = null;
1159
1160                const showTooltip = () => {
1161                    tooltipElement = document.createElement('div');
1162                    tooltipElement.classList.add('tb-tooltip');
1163                    tooltipElement.innerHTML = itemLabel;
1164                    item.appendChild(tooltipElement);
1165
1166                    const tooltipRect = tooltipElement.getBoundingClientRect();
1167                    const screenWidth = window.innerWidth;
1168                    if (tooltipRect.right > screenWidth) {
1169                        tooltipElement.style.right = 0;
1170                        tooltipElement.style.left = 'auto';
1171                        tooltipElement.style.transform = 'none';
1172                    }
1173
1174                    setTimeout(() => {
1175                        tooltipElement.classList.add('tb-tooltip--active');
1176                    }, 0);
1177                };
1178
1179                const hideTooltip = () => {
1180                    if (tooltipElement) {
1181                        tooltipElement.classList.remove('tb-tooltip--active');
1182                        setTimeout(() => {
1183                            tooltipElement?.remove();
1184                            tooltipElement = null;
1185                        }, 200);
1186                    }
1187                };
1188
1189                item.addEventListener('mouseenter', showTooltip);
1190
1191                item.addEventListener('mouseleave', (e) => {
1192                    // Check if the mouse is leaving the trigger or the tooltip itself
1193                    if (!item.contains(e.relatedTarget)) {
1194                        hideTooltip();
1195                    }
1196                });
1197
1198                // Ensure tooltip is removed if the mouse leaves the document or parent container
1199                document.addEventListener('mousemove', (e) => {
1200                    if (!item.contains(e.target)) {
1201                        hideTooltip();
1202                    }
1203                });
1204            }
1205        });
1206    }
1207
1208    // -----------------------------------
1209    // Phone tooltip functionality
1210    // -----------------------------------
1211
1212    const phoneTooltipTriggers = document.querySelectorAll('.tb-phone-tooltip-trigger');
1213    phoneTooltipTriggers.forEach((trigger) => {
1214        const tooltipElement = trigger.querySelector('.tb-phone-tooltip');
1215        let isTooltipVisible = false;
1216
1217        const showPhoneTooltip = () => {
1218            if (isTooltipVisible || !tooltipElement) return;
1219
1220            // Position the tooltip
1221            const tooltipRect = tooltipElement.getBoundingClientRect();
1222            const screenWidth = window.innerWidth;
1223            if (tooltipRect.right > screenWidth) {
1224                tooltipElement.style.right = '0';
1225                tooltipElement.style.left = 'auto';
1226                tooltipElement.style.transform = 'none';
1227            }
1228
1229            setTimeout(() => {
1230                tooltipElement.classList.add('tb-phone-tooltip--active');
1231                isTooltipVisible = true;
1232            }, 0);
1233        };
1234
1235        const hidePhoneTooltip = () => {
1236            if (tooltipElement && isTooltipVisible) {
1237                tooltipElement.classList.remove('tb-phone-tooltip--active');
1238                isTooltipVisible = false;
1239            }
1240        };
1241
1242        trigger.addEventListener('click', (e) => {
1243            // Don't toggle if clicking on the tooltip itself
1244            if (e.target.closest('.tb-tooltip')) {
1245                return;
1246            }
1247            e.preventDefault();
1248            if (isTooltipVisible) {
1249                hidePhoneTooltip();
1250            } else {
1251                showPhoneTooltip();
1252            }
1253        });
1254
1255        // Close tooltip when clicking outside
1256        document.addEventListener('click', (e) => {
1257            if (!trigger.contains(e.target) && isTooltipVisible) {
1258                hidePhoneTooltip();
1259            }
1260        });
1261    });
1262
1263    initActionInputs();
1264});
1265function initCustomSelects() {
1266    const customSelects = document.querySelectorAll('.tb-custom-select');
1267
1268    if (customSelects.length > 0) {
1269        customSelects.forEach((customSelect) => {
1270            if (!customSelect.classList.contains('tb-custom-select--initialized')) {
1271                initCustomSelect(customSelect);
1272            }
1273        });
1274    }
1275}
1276
1277function initCustomSelect(customSelect) {
1278    const options = customSelect.querySelectorAll('option');
1279    const select = customSelect.querySelector('select');
1280    const optionsContainer = document.createElement('div');
1281    const selectedOptionElement = document.createElement('div');
1282    optionsContainer.classList.add('tb-select');
1283    const optionsInnerContainer = document.createElement('div');
1284    optionsInnerContainer.classList.add('tb-select-inner');
1285    // optionsContainer.classList.add('tb-input');
1286    selectedOptionElement.classList.add('tb-input');
1287    optionsContainer.value = select.value;
1288
1289    // Create and style the selected option element
1290    selectedOptionElement.classList.add('selected-option');
1291    selectedOptionElement.textContent = options[select.selectedIndex].textContent;
1292    // customSelect.insertBefore(selectedOptionElement, select);
1293    customSelect.appendChild(selectedOptionElement);
1294
1295    options.forEach((option) => {
1296        const optionElement = document.createElement('div');
1297        optionElement.classList.add('tb-option');
1298        if (option.selected) {
1299            optionElement.classList.add('selected');
1300        }
1301        if (option.disabled) {
1302            optionElement.classList.add('tb-option--disabled');
1303            optionElement.setAttribute('aria-disabled', 'true');
1304        }
1305        if (select.multiple) {
1306            optionElement.innerHTML = "<div class='tb-select-box'></div><span>" + option.innerHTML + '</span>';
1307        } else {
1308            optionElement.innerHTML = "<div class='tb-radio-box'></div><span>" + option.innerHTML + '</span>';
1309        }
1310        optionElement.addEventListener('click', (e) => {
1311            e.stopPropagation(); // Prevent the dropdown from closing
1312            if (option.disabled) {
1313                return;
1314            }
1315            // Remove selected class from all options
1316            optionsContainer.querySelectorAll('.tb-option').forEach((el) => el.classList.remove('selected'));
1317            // Add selected class to the clicked option
1318            optionElement.classList.add('selected');
1319            select.value = option.value;
1320            select.dispatchEvent(new Event('change'));
1321            // Update the selected option element text
1322            selectedOptionElement.textContent = option.textContent;
1323        });
1324        optionsInnerContainer.appendChild(optionElement);
1325    });
1326
1327    optionsContainer.appendChild(optionsInnerContainer);
1328    customSelect.appendChild(optionsContainer);
1329    customSelect.addEventListener('click', (e) => {
1330        e.stopPropagation(); // Prevent the dropdown from closing
1331
1332        // Close all other open custom selects before toggling this one
1333        document.querySelectorAll('.tb-custom-select.tb-open').forEach((openSelect) => {
1334            if (openSelect !== customSelect) {
1335                openSelect.classList.remove('tb-open');
1336                const openOptionsContainer = openSelect.querySelector('div.tb-select');
1337                if (openOptionsContainer) {
1338                    openOptionsContainer.style.height = '0px';
1339                }
1340                openSelect.classList.remove('tb-custom-select--has-overflow');
1341            }
1342        });
1343
1344        customSelect.classList.toggle('tb-open');
1345        if (customSelect.classList.contains('tb-open')) {
1346            optionsContainer.style.height = 0;
1347            requestAnimationFrame(() => {
1348                optionsContainer.style.height = optionsContainer.scrollHeight + 'px';
1349
1350                // Listen for the transition end event
1351                const onTransitionEnd = () => {
1352                    checkSelectOverflow(optionsInnerContainer);
1353                    optionsContainer.removeEventListener('transitionend', onTransitionEnd);
1354                };
1355                optionsContainer.addEventListener('transitionend', onTransitionEnd);
1356            });
1357        } else {
1358            optionsContainer.style.height = 0;
1359            customSelect.classList.remove('tb-custom-select--has-overflow');
1360        }
1361    });
1362
1363    // Close the dropdown when clicking outside of it
1364    document.addEventListener('click', (e) => {
1365        if (!customSelect.contains(e.target)) {
1366            customSelect.classList.remove('tb-open');
1367            optionsContainer.style.height = 0;
1368        }
1369    });
1370
1371    // Listen for changes on the original select element
1372    select.addEventListener('change', () => {
1373        // Remove selected class from all options
1374        optionsContainer.querySelectorAll('.tb-option').forEach((el) => el.classList.remove('selected'));
1375        // Add selected class to the newly selected option
1376        const selectedOption = optionsContainer.querySelector(`.tb-option:nth-child(${select.selectedIndex + 1})`);
1377        if (selectedOption) {
1378            selectedOption.classList.add('selected');
1379        }
1380        // Update the selected option element text
1381        selectedOptionElement.textContent = options[select.selectedIndex].textContent;
1382    });
1383    customSelect.classList.add('tb-custom-select--initialized');
1384
1385    function checkSelectOverflow(optionsInnerContainer) {
1386        if (optionsInnerContainer.parentElement.clientHeight > 0) {
1387            if (checkIfElementHasOverflow(optionsInnerContainer, optionsInnerContainer.parentElement)) {
1388                customSelect.classList.add('tb-custom-select--has-overflow');
1389            } else {
1390                customSelect.classList.remove('tb-custom-select--has-overflow');
1391            }
1392        }
1393    }
1394
1395    checkSelectOverflow(optionsInnerContainer);
1396    window.removeEventListener('resize', checkSelectOverflow(optionsInnerContainer));
1397    window.addEventListener('resize', checkSelectOverflow(optionsInnerContainer));
1398}
1399
1400// -----------------------------------
1401// highlight navigation items depending on scroll position functions
1402// -----------------------------------
1403function initIntersectionNavigation(sections, navElement, intersectionRootMargin = '50px') {
1404    if (!navElement) {
1405        return;
1406    }
1407
1408    const changeNav = (entries, observer) => {
1409        let updated = false;
1410
1411        entries.forEach((entry) => {
1412            if (updated) {
1413                return;
1414            }
1415
1416            let activeBlock;
1417
1418            const nextSibling = entry.target.nextElementSibling;
1419            const previousSibling = entry.target.previousElementSibling;
1420
1421            if (entry.isIntersecting) {
1422                activeBlock = entry.target;
1423            } else {
1424                if (isElementInViewport(nextSibling)) {
1425                    activeBlock = nextSibling;
1426                } else if (isElementInViewport(previousSibling)) {
1427                    activeBlock = previousSibling;
1428                    while (isElementInViewport(activeBlock.previousElementSibling)) {
1429                        activeBlock = activeBlock.previousElementSibling;
1430                    }
1431                }
1432            }
1433
1434            if (activeBlock) {
1435                const id = activeBlock.getAttribute('id');
1436                const a = navElement.querySelector(`[href="#${id}"]`);
1437                if (a) {
1438                    const navItemElement = a.closest('li');
1439                    if (navItemElement && !navItemElement.classList.contains('active')) {
1440                        navElement.querySelectorAll('li.active').forEach((el) => {
1441                            el.classList.remove('active');
1442                        });
1443                        navItemElement.classList.add('active');
1444
1445                        if (
1446                            navElement.querySelector('ul') &&
1447                            navElement.querySelector('ul').scrollWidth > navElement.querySelector('ul').clientWidth
1448                        ) {
1449                            // Only scroll on click, not on intersection change
1450                            // scrollHorizontalMenu(navItemElement);
1451                        }
1452                        updated = true;
1453                    }
1454                }
1455            }
1456        });
1457    };
1458
1459    function isElementInViewport(el) {
1460        if (!el) {
1461            return false;
1462        }
1463        const rect = el.getBoundingClientRect();
1464        return rect.top >= 0 && rect.bottom <= window.innerHeight;
1465    }
1466
1467    function scrollHorizontalMenu(activeLink) {
1468        activeLink.scrollIntoView({ behavior: 'smooth', inline: 'center' });
1469    }
1470
1471    const changeNavObserver = new IntersectionObserver(changeNav, {
1472        rootMargin: `-${intersectionRootMargin} 0px 0px 0px`, // Apply margin only to the top
1473        root: null, // Use null for the root; document is not a valid root
1474        threshold: [],
1475    });
1476
1477    sections.forEach((sectionNavTrigger) => {
1478        changeNavObserver.observe(sectionNavTrigger);
1479    });
1480
1481    // Add click event listeners to navigation items
1482    const navItems = navElement.querySelectorAll('li');
1483    navItems.forEach((navItem) => {
1484        navItem.removeEventListener('click', () => {
1485            scrollHorizontalMenu(navItem);
1486        });
1487        navItem.addEventListener('click', () => {
1488            scrollHorizontalMenu(navItem);
1489        });
1490    });
1491
1492    let intersectingEntries = [];
1493
1494    // const observer = new IntersectionObserver((entries, observer) => {
1495    //     if (navElement) {
1496    //         entries.forEach((entry) => {
1497    //             const id = entry.target.getAttribute('id');
1498    //             const navLink = navElement.querySelector(`[href="#${id}"]`);
1499    //             if (navLink) {
1500    //                 const navItemElement = navLink.closest('li');
1501
1502    //                 if (entry.isIntersecting) {
1503    //                     if (!intersectingEntries.includes(navLink)) {
1504    //                         intersectingEntries.push({ link: navLink, entry: entry });
1505    //                     }
1506    //                 } else {
1507    //                     intersectingEntries = intersectingEntries.filter((item) => item.link !== navLink);
1508    //                 }
1509    //             }
1510    //         });
1511
1512    //         intersectingEntries.sort((a, b) => {
1513    //             return a.entry.boundingClientRect.top - b.entry.boundingClientRect.top;
1514    //         });
1515
1516    //         navElement.querySelectorAll('li.active').forEach((el) => {
1517    //             el.classList.remove('active');
1518    //         });
1519
1520    //         if (intersectingEntries.length > 0) {
1521    //             intersectingEntries[0].link.closest('li').classList.add('active');
1522    //         }
1523    //     }
1524    // }, options);
1525
1526    // sections.forEach((section) => {
1527    //     observer.observe(section);
1528    // });
1529
1530    // -----------------------------------
1531    // Autoplay video when in viewport
1532    // -----------------------------------
1533
1534    const html5Videos = document.querySelectorAll('.tb-video-part video');
1535
1536    const loadAndPlayVideo = (video) => {
1537        video.setAttribute('preload', 'auto');
1538        //video.setAttribute('autoplay', true);
1539    };
1540
1541    const handleIntersection = (entries, observer) => {
1542        entries.forEach((entry) => {
1543            if (entry.isIntersecting) {
1544                loadAndPlayVideo(entry.target);
1545                observer.unobserve(entry.target); // Stop observing the video after loading and playing
1546            }
1547        });
1548    };
1549
1550    const optionsHtml5Videos = {
1551        root: null, // Use the viewport as the root
1552        rootMargin: '100px', // Trigger 100px before the video comes into the viewport
1553        threshold: 0, // Trigger as soon as any part of the video is visible
1554    };
1555
1556    const observerHtml5Videos = new IntersectionObserver(handleIntersection, optionsHtml5Videos);
1557
1558    // html5Videos.forEach((video) => {
1559    //     observerHtml5Videos.observe(video);
1560    // });
1561}
1562
1563function initSectionTogglers() {
1564    const sectionTogglers = document.querySelectorAll('.section-toggler');
1565    for (const toggler of sectionTogglers) {
1566        if (toggler) {
1567            const innerToggler = toggler.querySelector('.section-toggler__toggles');
1568            const togglerWrapper = toggler.querySelector('.section-toggler__toggles-wrapper');
1569
1570            const handleClick = (e) => {
1571                if (e.target.tagName !== 'LABEL' && e.detail.type !== 'toggleClick') {
1572                    return;
1573                }
1574                const targetId = e.detail.type === 'toggleClick' ? e.detail.targetId : e.target.dataset.targetId;
1575
1576                const tbBlock = e.currentTarget.closest('.tb-block');
1577                const allSections = tbBlock.querySelectorAll('.tb-section');
1578                const firstLayerSections = Array.from(allSections).filter(
1579                    (section) => section.closest('.tb-block') === tbBlock,
1580                );
1581                const sections = firstLayerSections;
1582
1583                const targetElement = toggler.closest('.tb-block');
1584                const offset = -100;
1585                const bodyRect = document.body.getBoundingClientRect().top;
1586                const elementRect = targetElement.getBoundingClientRect().top;
1587                const elementPosition = elementRect - bodyRect;
1588                const offsetPosition = elementPosition + offset;
1589
1590                // window.scrollTo({
1591                //     top: offsetPosition,
1592                //     behavior: 'smooth',
1593                // });
1594
1595                for (const section of sections) {
1596                    if (section.id === targetId) {
1597                        section.style.display = 'flex';
1598                        // Only now that it is on screen: a hidden section measures as nothing.
1599                        refreshReadmoreIn(section);
1600                    } else {
1601                        section.style.display = 'none';
1602                    }
1603                }
1604            };
1605
1606            toggler.removeEventListener('click', handleClick);
1607            toggler.addEventListener('click', handleClick);
1608
1609            document.addEventListener('DOMContentLoaded', function () {
1610                if (togglerWrapper) {
1611                    setTogglerOverflowClasses(togglerWrapper, toggler);
1612                    togglerWrapper.addEventListener('scroll', () => {
1613                        setTogglerOverflowClasses(togglerWrapper, toggler);
1614                    });
1615                }
1616            });
1617        }
1618    }
1619
1620    const handleResize = () => {
1621        for (const toggler of sectionTogglers) {
1622            if (toggler) {
1623                const togglerWrapper = toggler.querySelector('.section-toggler__toggles-wrapper');
1624                if (togglerWrapper) {
1625                    setTogglerOverflowClasses(togglerWrapper, toggler);
1626                }
1627            }
1628        }
1629    };
1630
1631    window.removeEventListener('resize', handleResize);
1632    window.addEventListener('resize', handleResize);
1633
1634    // Handle hash navigation for section toggles
1635    // (eg: using #options-2 in the url will scroll to the #options block and open the second section)
1636    const handleHashNavigation = () => {
1637        const hash = window.location.hash.substring(1); // Remove the #
1638
1639        if (hash) {
1640            // Convert hash format from "example-1" to "example-section-0"
1641            // Match pattern: any text followed by dash and number(s)
1642            const match = hash.match(/^(.+)-(\d+)$/);
1643
1644            if (match) {
1645                const blockId = match[1];
1646                const sectionNumber = parseInt(match[2]) - 1; // Convert to 0-based index
1647                const fullSectionId = `${blockId}-section-${sectionNumber}`;
1648
1649                // Find the label element
1650                const radioId = fullSectionId + '-label';
1651                const radioInput = document.getElementById(radioId);
1652                const label = radioInput ? document.querySelector(`label[for="${radioId}"]`) : null;
1653
1654                if (label) {
1655                    // Trigger a click on the label to activate the toggle logic
1656                    label.click();
1657
1658                    // Scroll to the main container
1659                    const mainContainer = document.getElementById(blockId);
1660                    if (mainContainer) {
1661                        setTimeout(() => {
1662                            mainContainer.scrollIntoView({ behavior: 'smooth' });
1663                        }, 100);
1664                    }
1665                }
1666            }
1667        }
1668    };
1669
1670    // Run on page load
1671    handleHashNavigation();
1672
1673    // Also handle hash changes (when clicking links on the same page)
1674    window.removeEventListener('hashchange', handleHashNavigation);
1675    window.addEventListener('hashchange', handleHashNavigation);
1676}
1677
1678function setTogglerOverflowClasses(innerToggler, toggler) {
1679    if (innerToggler.scrollWidth > 0) {
1680        if (checkIfElementHasOverflow(innerToggler, toggler) && innerToggler.scrollLeft < toggler.scrollWidth - 100) {
1681            toggler.classList.add('section-toggler--has-overflow');
1682        } else {
1683            toggler.classList.remove('section-toggler--has-overflow');
1684        }
1685    }
1686}
1687
1688// Cards in one slider behave the same way, so a visitor swiping through them meets
1689// one pattern: the group is the carousel track, or the block when a card stands on
1690// its own.
1691function collectReadmoreGroups(root) {
1692    const groups = new Map();
1693
1694    for (const link of root.querySelectorAll('.js-readmore')) {
1695        const content = link.previousElementSibling;
1696        const card = link.closest('.popup-on-readmore');
1697        const group = link.closest('.entries') || link.closest('.tb-block');
1698
1699        if (!card || !group || !content || !content.classList.contains('readmore-content')) {
1700            continue;
1701        }
1702
1703        if (!groups.has(group)) {
1704            groups.set(group, []);
1705        }
1706
1707        groups.get(group).push({ link, content, card });
1708    }
1709
1710    for (const entries of groups.values()) {
1711        for (const entry of entries) {
1712            entry.group = entries;
1713        }
1714    }
1715
1716    return groups;
1717}
1718
1719function refreshReadmoreIn(root) {
1720    for (const entries of collectReadmoreGroups(root).values()) {
1721        applyReadmoreMode(entries);
1722    }
1723}
1724
1725// Up to this many lines a card simply shows its text: no link, no fade.
1726const TB_READMORE_FIT_LINES = 7;
1727// One text this long in a slider sends every card in it to the popup — unfolding
1728// that much inside a card would push the ones next to it far off screen.
1729const TB_READMORE_POPUP_LINES = 12;
1730
1731// Decides, for one slider, whether its cards unfold in place or open a popup, and
1732// hides the link on the cards that show their whole text already.
1733function applyReadmoreMode(entries) {
1734    // The block's design setting switched the read more off at this width: every
1735    // card shows its whole text, and a tap on it does nothing.
1736    if (!!entries[0] && readmoreIsOff(entries[0].card)) {
1737        for (const entry of entries) {
1738            if (entry.card.classList.contains('is-expanded')) {
1739                collapseReadmore(entry, false);
1740            }
1741
1742            entry.link.hidden = true;
1743            entry.card.classList.add('is-text-complete');
1744            entry.card.classList.remove('is-readmore-inline', 'is-readmore-popup', 'is-text-clamped');
1745        }
1746
1747        return;
1748    }
1749
1750    // A card only unfolds in place where its own stylesheet says it can grow —
1751    // the fixed-height desktop card cannot. The flag comes from CSS so it follows
1752    // the block's container query, which the root's width class does not.
1753    const inlinePossible = !!entries[0] && readmoreAllowsInline(entries[0].card);
1754    const lineCounts = entries.map((entry) => readmoreLineCount(entry.content));
1755    const longest = Math.max(0, ...lineCounts);
1756
1757    // Nothing rendered — a section the toggler keeps hidden. Leave the cards as
1758    // they are; they are measured again when the section is shown.
1759    if (longest === 0) {
1760        return;
1761    }
1762
1763    const mode = inlinePossible && longest < TB_READMORE_POPUP_LINES ? 'inline' : 'popup';
1764
1765    entries.forEach((entry, i) => {
1766        const { link, card } = entry;
1767        const expanded = card.classList.contains('is-expanded');
1768        const fits = !expanded && lineCounts[i] <= TB_READMORE_FIT_LINES && !readmoreContentIsCut(entry.content);
1769
1770        if (expanded && mode !== 'inline') {
1771            collapseReadmore(entry, false);
1772        }
1773
1774        link.toggleAttribute('hidden', fits);
1775        card.classList.toggle('is-text-complete', fits);
1776        card.classList.toggle('is-readmore-inline', !fits && mode === 'inline');
1777        card.classList.toggle('is-readmore-popup', !fits && mode === 'popup');
1778        // drives the fade over the last line: only the script knows the text was cut
1779        card.classList.toggle('is-text-clamped', !fits && !card.classList.contains('is-expanded'));
1780    });
1781}
1782
1783const TB_READMORE_SLIDE_MS = 300;
1784
1785function readmoreAllowsInline(card) {
1786    return getComputedStyle(card).getPropertyValue('--tb-readmore-inline').trim() === '1';
1787}
1788
1789// Set by the block's stylesheet under its `readmore-mobile-off` class, so it
1790// follows the same width as the clamp it lifts.
1791function readmoreIsOff(card) {
1792    return getComputedStyle(card).getPropertyValue('--tb-readmore-off').trim() === '1';
1793}
1794
1795// One link speaks for the whole slider: unfolding a card unfolds the ones next to
1796// it, so the row is read in one go instead of card by card.
1797function toggleReadmoreExpanded(entry) {
1798    const expand = !entry.card.classList.contains('is-expanded');
1799    const targets = (entry.group || [entry]).filter((other) => {
1800        if (other === entry) {
1801            return true;
1802        }
1803        // a card showing its whole text has nothing to unfold
1804        if (!other.card.classList.contains('is-readmore-inline')) {
1805            return false;
1806        }
1807
1808        return other.card.classList.contains('is-expanded') !== expand;
1809    });
1810
1811    // measured before any card moves: cards in a row share their height
1812    const heights = targets.map((other) => other.content.getBoundingClientRect().height);
1813
1814    targets.forEach((other, i) => {
1815        if (expand) {
1816            expandReadmore(other, true, heights[i]);
1817        } else {
1818            collapseReadmore(other, true, heights[i]);
1819        }
1820    });
1821}
1822
1823function expandReadmore(entry, animate, from = entry.content.getBoundingClientRect().height) {
1824    entry.card.classList.add('is-expanded');
1825    entry.card.classList.remove('is-text-clamped');
1826    // Lifts the clamp without fighting the block's own max-height rule.
1827    entry.content.style.maxHeight = 'none';
1828
1829    slideReadmore(entry.content, from, animate, () => {
1830        entry.content.style.maxHeight = 'none';
1831    });
1832}
1833
1834function collapseReadmore(entry, animate, from = entry.content.getBoundingClientRect().height) {
1835    entry.card.classList.remove('is-expanded');
1836    entry.card.classList.add('is-text-clamped');
1837    entry.content.style.maxHeight = '';
1838
1839    slideReadmore(entry.content, from, animate, () => {
1840        entry.content.style.maxHeight = '';
1841    });
1842}
1843
1844// Slides between the height the text had and the one it has now. The end state
1845// is already applied when this runs, so the animation is a replay of the jump
1846// that just happened — and skipping it leaves the card correct either way.
1847function slideReadmore(content, from, animate, settle) {
1848    if (!animate || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
1849        return;
1850    }
1851
1852    const to = content.getBoundingClientRect().height;
1853
1854    content.style.transition = 'none';
1855    content.style.maxHeight = `${from}px`;
1856    content.style.overflow = 'hidden';
1857
1858    requestAnimationFrame(() => {
1859        content.style.transition = `max-height ${TB_READMORE_SLIDE_MS}ms ease-out`;
1860        content.style.maxHeight = `${to}px`;
1861    });
1862
1863    window.setTimeout(() => {
1864        content.style.transition = '';
1865        content.style.overflow = '';
1866        settle();
1867    }, TB_READMORE_SLIDE_MS);
1868}
1869
1870// How many lines the text takes when nothing clips it. Counted from the tops of
1871// the text's own boxes, so paragraph margins do not pass for lines and a bold run
1872// in the middle of a line does not count twice.
1873function readmoreLineCount(content) {
1874    const range = document.createRange();
1875    range.selectNodeContents(content);
1876
1877    const tops = new Set();
1878
1879    for (const rect of range.getClientRects()) {
1880        if (rect.width > 0 && rect.height > 0) {
1881            tops.add(Math.round(rect.top));
1882        }
1883    }
1884
1885    return tops.size;
1886}
1887
1888function readmoreContentIsCut(content) {
1889    const textBottom = readmoreTextBottom(content);
1890
1891    if (textBottom === null) {
1892        return false;
1893    }
1894
1895    // Where the text stops being visible: the clamp, or the card around it when
1896    // that is the shorter of the two. The card content box keeps its height
1897    // while it slides on hover, so both rects move together.
1898    let clipBottom = content.getBoundingClientRect().bottom;
1899    const box = content.closest('.tb-card-content');
1900
1901    if (box) {
1902        const paddingBottom = parseFloat(getComputedStyle(box).paddingBottom) || 0;
1903        clipBottom = Math.min(clipBottom, box.getBoundingClientRect().bottom - paddingBottom);
1904    }
1905
1906    // The slack absorbs sub-pixel line heights: a rounded-up bottom is not a cut line.
1907    return textBottom > clipBottom + 2;
1908}
1909
1910// The bottom of the last line a visitor can actually read. Measured from the
1911// text's own boxes, which sit where they would without the clamp — scrollHeight
1912// would also count a trailing empty paragraph and report a cut that isn't one.
1913function readmoreTextBottom(content) {
1914    const range = document.createRange();
1915    range.selectNodeContents(content);
1916
1917    let bottom = null;
1918
1919    for (const rect of range.getClientRects()) {
1920        if (rect.width > 0 && rect.height > 0) {
1921            bottom = bottom === null ? rect.bottom : Math.max(bottom, rect.bottom);
1922        }
1923    }
1924
1925    return bottom;
1926}
1927
1928function checkIfElementHasOverflow(el, parentel) {
1929    if (el) {
1930        return el.scrollHeight > parentel.clientHeight || el.scrollWidth > parentel.clientWidth;
1931    }
1932}
1933
1934function initActionInputs() {
1935    const chatFormControls = document.querySelectorAll('.input-wrapper--chat');
1936    const mayaChatForms = document.querySelectorAll('.maya-chat-form');
1937
1938    chatFormControls.forEach((formControl) => {
1939        const chatInput = formControl.querySelector('input');
1940        const customPlaceholder = formControl.querySelector('.custom-placeholder');
1941
1942        if (!chatInput || !customPlaceholder) return;
1943
1944        const togglePlaceholder = () => {
1945            const hasValue = chatInput.value.trim() !== '';
1946
1947            if (hasValue) {
1948                chatInput.classList.add('has-value');
1949                customPlaceholder.style.display = 'none';
1950            } else {
1951                chatInput.classList.remove('has-value');
1952                customPlaceholder.style.display = '';
1953            }
1954        };
1955
1956        chatInput.addEventListener('focus', function () {
1957            customPlaceholder.style.display = 'none';
1958        });
1959
1960        chatInput.addEventListener('blur', function () {
1961            togglePlaceholder();
1962        });
1963
1964        chatInput.addEventListener('input', function () {
1965            togglePlaceholder();
1966        });
1967
1968        chatInput.addEventListener('keydown', function (e) {
1969            if (e.key === 'Enter') {
1970                setTimeout(() => {
1971                    chatInput.value = '';
1972                    togglePlaceholder();
1973                }, 100);
1974            }
1975        });
1976
1977        togglePlaceholder();
1978    });
1979
1980    mayaChatForms.forEach((form) => {
1981        initMayaChatForm(form);
1982    });
1983}
1984
1985function initMayaChatForm(form) {
1986    if (!form) return;
1987    form.addEventListener('submit', function (e) {
1988        e.preventDefault();
1989
1990        const mayaWidget = document.querySelector('#maya-b2b-full-page');
1991        if (!mayaWidget || mayaWidget.getAttribute('data-widget-mode') !== 'popover') return;
1992
1993        const input = form.elements.namedItem('chat')?.value;
1994        if (input && window.sendMayaUserMessage) {
1995            window.sendMayaUserMessage(input);
1996        }
1997    });
1998
1999}

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.