/* =============================================================================
Utils (ready, debounce, waitFor)
============================================================================= */
(() => {
const U = {
debounce(fn, delay = 150) {
let t;
return function debounced(...args) {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), delay);
};
},
onReady(cb) {
if (document.readyState !== "loading") cb();
else document.addEventListener("DOMContentLoaded", cb, { once: true });
},
waitFor(checkFn, { timeout = 8000, interval = 50 } = {}) {
return new Promise((res, rej) => {
if (checkFn()) return res(true);
const start = Date.now();
const timer = setInterval(() => {
if (checkFn()) {
clearInterval(timer);
res(true);
} else if (Date.now() - start > timeout) {
clearInterval(timer);
rej(new Error("waitFor timeout"));
}
}, interval);
});
},
has(el) {
return !!(el && (el.length === undefined ? el : el.length));
},
};
window.__DF_UTILS__ = U;
})();
/* =============================================================================
0. Dev “edit mode” off
============================================================================= */
(() => {
const { onReady } = window.__DF_UTILS__;
onReady(() => {
document.querySelectorAll(".dev-edite-mode.is-on").forEach((el) => {
el.classList.remove("is-on");
});
});
})();
/* =============================================================================
1. Clickable CMS cards (safe)
============================================================================= */
(() => {
const { onReady } = window.__DF_UTILS__;
onReady(() => {
const blocks = document.querySelectorAll(
".card-white-blog, .grid-blog-content"
);
if (!blocks.length) return;
blocks.forEach((block) => {
block.addEventListener(
"click",
(ev) => {
if (ev.target.closest("a, button, [role='button']")) return;
const link = block.querySelector(".is-cms-link");
if (link?.href) window.location.href = link.href;
},
{ passive: true }
);
});
});
})();
/* =============================================================================
2. Interactive Grid Tabs (GSAP + ScrollTrigger)
============================================================================= */
(() => {
const { onReady, debounce, waitFor } = window.__DF_UTILS__;
function initInteractiveGrids(root = document) {
const wrappers = root.querySelectorAll(
".interactive-grid_wrapper:not(.__inited)"
);
if (!wrappers.length) return;
if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined")
return;
gsap.registerPlugin(ScrollTrigger);
wrappers.forEach((wrapper) => {
wrapper.classList.add("__inited");
setupInteractiveGrid(wrapper);
});
function setupInteractiveGrid(wrapper) {
const duration = parseFloat(wrapper.dataset.stepDuration) || 6;
const autoMediaHeight = wrapper.dataset.mediaWrapperAuto === "true";
const fadeDuration = parseFloat(wrapper.dataset.fadeDuration) || 0.5;
const animateInterContent = wrapper.dataset.animeInterContent === "true";
const tabEls = Array.from(wrapper.querySelectorAll(".interactive-tab"));
if (!tabEls.length) return;
const tabs = tabEls.map((el) => ({
container: el,
hidden: el.querySelector(".interactive-tab_content_hidden"),
visible: el.querySelector(".interactive-tab_content_visible"),
content: el.querySelector(".interactive-tab_content"),
bullet: el.querySelector(".bullets_active"),
mediaWrapper: el.querySelector(".interactive-tab_media_wrap"),
progressBar: el.querySelector(".interactive-progress"),
progressWrapper: el.querySelector(".interactive-progress_wrap"),
lottieEl: el.querySelector(".lottie-element"),
contentInteractiveMedia: el.querySelector(
".content-in-interactive-media"
),
}));
let heightCache = [];
let activeIndex = 0;
let playTimer = null;
let progressTween = null;
let isAutoPlay = true;
let isInViewport = false;
function measureHeights() {
heightCache = tabs.map((tab) => {
if (!tab.hidden) return 0;
const w = tab.hidden.getBoundingClientRect().width;
gsap.set(tab.hidden, {
height: "auto",
width: `${w}px`,
opacity: 1,
position: "absolute",
visibility: "hidden",
});
const h = tab.hidden.scrollHeight;
gsap.set(tab.hidden, {
clearProps: "height,width,opacity,position,visibility",
});
return h;
});
}
measureHeights();
const reMeasure = debounce(() => {
measureHeights();
const a = tabs[activeIndex];
if (!a) return;
if (a.hidden) gsap.set(a.hidden, { height: "auto", opacity: 1 });
if (a.mediaWrapper) {
if (window.innerWidth < 991) {
if (autoMediaHeight) {
gsap.set(a.mediaWrapper, { height: "auto", opacity: 1 });
} else {
gsap.set(a.mediaWrapper, {
height: "80vw",
overflow: "hidden",
opacity: 1,
});
}
} else {
gsap.set(a.mediaWrapper, { clearProps: "height", opacity: 1 });
}
}
}, 100);
window.addEventListener("resize", reMeasure, { passive: true });
function clearPlay() {
if (playTimer) clearTimeout(playTimer);
playTimer = null;
if (progressTween) progressTween.kill();
progressTween = null;
}
function startProgress() {
if (!isAutoPlay || !isInViewport) return;
const t = tabs[activeIndex];
if (!t?.progressBar || !t?.progressWrapper) return;
clearPlay();
gsap.set(t.progressWrapper, { opacity: 1 });
gsap.set(t.progressBar, { opacity: 1, width: 0 });
progressTween = gsap.fromTo(
t.progressBar,
{ width: 0 },
{ width: "100%", ease: "none", duration }
);
playTimer = setTimeout(
() => activateTab((activeIndex + 1) % tabs.length, false),
duration * 1000
);
}
const scrollStart = wrapper.dataset.scrollStart || "top 100%";
const scrollEnd = wrapper.dataset.scrollEnd || "bottom 0%";
ScrollTrigger.create({
trigger: wrapper,
start: scrollStart,
end: scrollEnd,
onEnter: () => ((isInViewport = true), startProgress()),
onEnterBack: () => ((isInViewport = true), startProgress()),
onLeave: () => ((isInViewport = false), clearPlay()),
onLeaveBack: () => ((isInViewport = false), clearPlay()),
});
function resetTabs() {
tabs.forEach((tab) => {
tab.container?.classList.remove("is-interactive-active");
if (tab.hidden)
gsap.set(tab.hidden, { clearProps: "height,opacity,width" });
if (tab.mediaWrapper)
gsap.set(tab.mediaWrapper, { clearProps: "height,opacity" });
if (tab.progressWrapper)
gsap.set(tab.progressWrapper, { clearProps: "opacity" });
if (tab.progressBar)
gsap.set(tab.progressBar, { clearProps: "width,opacity" });
if (tab.content) gsap.set(tab.content, { clearProps: "opacity" });
if (tab.bullet) gsap.set(tab.bullet, { clearProps: "opacity" });
if (animateInterContent && tab.contentInteractiveMedia) {
gsap.set(tab.contentInteractiveMedia, {
clearProps: "opacity,y,zIndex,position",
});
}
});
}
function activateTab(index, userClicked) {
const mobileUpStop = wrapper.dataset.mobileUpStop === "true";
activeIndex = index;
resetTabs();
clearPlay();
const tab = tabs[index];
if (!tab) return;
tab.container?.classList.add("is-interactive-active");
tab.content &&
gsap.to(tab.content, {
opacity: 1,
duration: fadeDuration,
ease: "power2.out",
});
tab.bullet &&
gsap.to(tab.bullet, {
opacity: 1,
duration: fadeDuration,
ease: "power2.out",
});
tab.visible &&
gsap.to(tab.visible, {
opacity: 1,
duration: fadeDuration,
ease: "power2.out",
});
if (tab.hidden) {
gsap.fromTo(
tab.hidden,
{ height: 0, opacity: 0 },
{
height: `${heightCache[index]}px`,
opacity: 1,
duration: 0.5,
ease: "power2.out",
onComplete: () => gsap.set(tab.hidden, { height: "auto" }),
}
);
}
if (tab.mediaWrapper) {
if (window.innerWidth < 991) {
if (autoMediaHeight) {
const mw = tab.mediaWrapper;
const w = mw.getBoundingClientRect().width;
gsap.set(mw, {
height: "auto",
width: `${w}px`,
position: "absolute",
visibility: "hidden",
});
const targetH = mw.scrollHeight;
gsap.set(mw, {
clearProps: "width,position,visibility",
height: 0,
overflow: "hidden",
opacity: 1,
});
gsap.to(mw, {
height: `${targetH}px`,
duration: 0.5,
ease: "power2.out",
onComplete: () => gsap.set(mw, { height: "auto" }),
});
} else {
gsap.to(tab.mediaWrapper, {
height: "68vw",
overflow: "hidden",
opacity: 1,
duration: 0.5,
ease: "power2.out",
});
}
} else {
gsap.to(tab.mediaWrapper, {
opacity: 1,
duration: 0.5,
ease: "power2.out",
});
}
}
if (
animateInterContent &&
window.innerWidth >= 991 &&
tab.contentInteractiveMedia
) {
tab.contentInteractiveMedia.style.position = "relative";
tab.contentInteractiveMedia.style.zIndex = "10";
gsap.fromTo(
tab.contentInteractiveMedia,
{ y: 40, opacity: 0 },
{ y: 0, opacity: 1, duration: 0.5, ease: "power2.out" }
);
}
if (userClicked) {
isAutoPlay = false;
if (tab.progressWrapper && tab.progressBar) {
gsap.set(tab.progressWrapper, { opacity: 1 });
gsap.set(tab.progressBar, { opacity: 1, width: "100%" });
}
} else {
startProgress();
}
if (userClicked && window.innerWidth < 991 && !mobileUpStop) {
const header = document.querySelector("header, .navbar_component");
const headerH = header ? header.getBoundingClientRect().height : 0;
const extraOff = 30;
const topY =
tab.container.getBoundingClientRect().top + window.pageYOffset;
window.scrollTo({
top: topY - headerH - extraOff,
behavior: "smooth",
});
}
if (tab.lottieEl?.__lottieAnim)
tab.lottieEl.__lottieAnim.goToAndPlay(0, true);
}
tabs.forEach((tab, i) => {
const clickArea = tab.container?.querySelector(
".interactive-tab_content_wrap"
);
if (clickArea) {
clickArea.addEventListener("click", () => {
if (activeIndex !== i) activateTab(i, true);
});
}
});
activateTab(0, false);
}
}
onReady(() => {
waitFor(
() => typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined"
)
.then(() => initInteractiveGrids())
.catch(() => {
/* silently skip if no GSAP */
});
// Re-init on .menu_tab click after dynamic content changes
document.addEventListener("click", (e) => {
if (e.target.closest(".menu_tab")) {
setTimeout(() => {
initInteractiveGrids();
if (typeof ScrollTrigger !== "undefined") ScrollTrigger.refresh();
window.dispatchEvent(new Event("resize"));
}, 50);
}
});
});
})();
/* =============================================================================
3. Progress lines in scrolling components (GSAP)
============================================================================= */
(() => {
const { onReady, waitFor } = window.__DF_UTILS__;
onReady(() => {
waitFor(
() => typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined"
)
.then(() => {
const boxes = document.querySelectorAll(
".box-wrapper-scroll, .scrolling_component"
);
if (!boxes.length) return;
boxes.forEach((box) => {
const line = box.querySelector(".progress-line-scroll");
if (!line) return;
gsap.to(line, {
height: "100%",
ease: "none",
scrollTrigger: {
trigger: box,
start: "top 50%",
end: "bottom 50%",
scrub: 0.7,
},
});
});
})
.catch(() => {
/* skip if no GSAP */
});
});
})();
/* =============================================================================
4. Sticky cards transform (desktop only) + wiring
============================================================================= */
(() => {
const { onReady, debounce } = window.__DF_UTILS__;
function applyCardTransforms() {
if (window.innerWidth < 768) return;
const cards = document.querySelectorAll(".layout_card");
if (!cards.length) return;
const vh = window.innerHeight;
cards.forEach((card, index) => {
const rect = card.getBoundingClientRect();
const mid = rect.top + rect.height / 2;
const ratio = Math.min(
Math.max(1 - Math.abs(mid - vh / 2) / (vh / 2), 0),
1
);
const scale = 0.9 + ratio * 0.1;
const offsetY = index * 12;
card.style.transform = `translateY(${offsetY}px) scale(${scale})`;
});
}
window.applyCardTransforms = applyCardTransforms; // used by slider block
onReady(() => {
const deb = debounce(applyCardTransforms, 150);
window.addEventListener("scroll", applyCardTransforms, { passive: true });
window.addEventListener("resize", deb);
window.addEventListener("orientationchange", deb);
window.addEventListener("load", applyCardTransforms, { passive: true });
});
})();
/* =============================================================================
5. Sticky slider (sticky-swiper-investor) with deferred Swiper
============================================================================= */
(() => {
const { onReady, debounce, waitFor } = window.__DF_UTILS__;
const SELECTOR = ".sticky-swiper-investor";
const BP = 768;
const instances = new Map(); // el -> Swiper
function listStickyEls() {
return Array.from(document.querySelectorAll(SELECTOR));
}
function getControls(sliderEl) {
const block = sliderEl.closest(".block-wrapper") || document;
return {
prevArrow: block.querySelector("#blog-arrow-slider-prev") || null,
nextArrow: block.querySelector("#blog-arrow-slider-next") || null,
scrollbarEl: block.querySelector(".swiper-scrollbar") || null,
paginationEl: block.querySelector(".swiper-pagination") || null,
};
}
function createSwiper(el) {
const { prevArrow, nextArrow, scrollbarEl, paginationEl } = getControls(el);
const opts = {
slidesPerView: 1,
spaceBetween: 12,
grabCursor: true,
a11y: true,
loop: false,
initialSlide: 0,
breakpoints: {
0: { slidesPerView: 1 },
480: { slidesPerView: 1 },
640: { slidesPerView: 1 },
},
};
if (prevArrow && nextArrow) {
opts.navigation = { prevEl: prevArrow, nextEl: nextArrow };
}
if (scrollbarEl) {
opts.scrollbar = { el: scrollbarEl, hide: false, draggable: true };
}
if (paginationEl) {
opts.pagination = { el: paginationEl, type: "progressbar" };
}
const inst = new Swiper(el, opts);
instances.set(el, inst);
return inst;
}
function destroySwiper(el) {
const inst = instances.get(el);
if (inst) {
inst.destroy(true, true);
instances.delete(el);
}
}
function gcInstances() {
for (const el of Array.from(instances.keys())) {
if (!document.documentElement.contains(el)) destroySwiper(el);
}
}
onReady(() => {
let running = false;
const maybeInitAll = () => {
if (running) return;
running = true;
try {
if (!window.Swiper) return;
const w = window.innerWidth;
const els = listStickyEls();
gcInstances();
els.forEach((el) => {
const has = instances.has(el);
if (w < BP && !has) {
createSwiper(el);
} else if (w >= BP && has) {
destroySwiper(el);
}
});
} finally {
running = false;
}
};
const deb = debounce(maybeInitAll, 200);
waitFor(() => !!window.Swiper, { timeout: 7000 }).finally(() => {
maybeInitAll();
window.addEventListener("resize", deb, { passive: true });
window.addEventListener("orientationchange", deb, { passive: true });
window.addEventListener("pageshow", maybeInitAll, { passive: true });
});
const mo = new MutationObserver(deb);
mo.observe(document.documentElement, { childList: true, subtree: true });
window.addEventListener("load", maybeInitAll, { passive: true });
});
})();
/* =============================================================================
6. Marquee (no jank safeguards)
============================================================================= */
(() => {
const { onReady } = window.__DF_UTILS__;
function initMarquees(selector, speed) {
const marquees = document.querySelectorAll(selector);
if (!marquees.length) return;
marquees.forEach((parent) => {
const original = parent.innerHTML;
parent.insertAdjacentHTML("beforeend", original);
parent.insertAdjacentHTML("beforeend", original);
let offset = 0;
let paused = false;
// Example hover pause (optional):
// parent.addEventListener("mouseenter", () => (paused = true));
// parent.addEventListener("mouseleave", () => (paused = false));
setInterval(() => {
if (paused) return;
const first = parent.firstElementChild;
if (!first) return;
first.style.marginLeft = `-${offset}px`;
if (offset > first.clientWidth) offset = 0;
else offset += speed;
}, 16);
});
}
onReady(() => initMarquees(".marquee", 0.9));
})();
/* =============================================================================
7. Slider for Related Resources (.blog-swiper-wrap) – wait for Swiper
============================================================================= */
(() => {
const { onReady, waitFor } = window.__DF_UTILS__;
onReady(() => {
waitFor(() => !!window.Swiper, { timeout: 7000 })
.then(() => {
document.querySelectorAll(".blog-swiper-wrap").forEach((sliderEl) => {
const block = sliderEl.closest(".block-wrapper") || document;
const prevArrow = block.querySelector("#blog-arrow-slider-prev");
const nextArrow = block.querySelector("#blog-arrow-slider-next");
const swiper = new Swiper(sliderEl, {
slidesPerView: 1,
spaceBetween: 20,
effect: "fade",
fadeEffect: { crossFade: true },
speed: 600,
navigation:
prevArrow && nextArrow
? { prevEl: prevArrow, nextEl: nextArrow }
: undefined,
breakpoints: {
992: { slidesPerView: 1, spaceBetween: 20 },
768: { slidesPerView: 1, spaceBetween: 8 },
0: { slidesPerView: 1, spaceBetween: 8 },
},
on: {
afterInit(sw) {
fixA11y(sw.el);
},
slidesLengthChange(sw) {
fixA11y(sw.el);
},
},
});
function fixA11y(root) {
const wrapper = root.querySelector(".swiper-wrapper");
if (wrapper) wrapper.setAttribute("role", "list");
root.querySelectorAll(".swiper-slide").forEach((slide) => {
slide.setAttribute("role", "listitem");
slide.removeAttribute("aria-roledescription");
});
}
function updateArrowState() {
if (!prevArrow || !nextArrow) return;
prevArrow.classList.toggle("is-on", !swiper.isBeginning);
nextArrow.classList.toggle("is-on", !swiper.isEnd);
}
updateArrowState();
swiper.on("slideChange", updateArrowState);
swiper.on("breakpoint", updateArrowState);
});
})
.catch(() => {
/* no Swiper → skip */
});
});
})();
/* =============================================================================
8. Simple Custom Tabs (.tab-wrapper)
============================================================================= */
(() => {
const { onReady } = window.__DF_UTILS__;
onReady(() => {
document.querySelectorAll(".tab-wrapper").forEach((wrapper) => {
const tabs = wrapper.querySelectorAll(
".menu_tab, .switch_tab, .tab-img_switch"
);
const panels = wrapper.querySelectorAll(".content_tab");
if (!tabs.length || !panels.length) return;
tabs.forEach((tab, idx) => {
tab.addEventListener("click", () => {
tabs.forEach((t) => t.classList.remove("is-active"));
tab.classList.add("is-active");
panels.forEach((p) =>
p.classList.remove("is-active", "visible-anime")
);
const target = panels[idx];
if (!target) return;
target.classList.add("is-active");
// force reflow for CSS animation
void target.offsetWidth;
target.classList.add("visible-anime");
});
});
// optional: activate the first tab
tabs[0]?.click();
});
});
})();
/* =============================================================================
9. Mobile-only sliders init/destroy (menu-tabs-slider, winter, brand)
============================================================================= */
(() => {
const { onReady, waitFor } = window.__DF_UTILS__;
onReady(() => {
waitFor(() => !!window.Swiper, { timeout: 7000 })
.then(() => {
const BREAKPOINT = 768;
const instances = new Map();
function init() {
document.querySelectorAll(".menu-tabs-slider").forEach((el) => {
if (!instances.has(el)) {
let space = parseInt(el.dataset.sliderSpace, 10);
if (isNaN(space)) space = 8;
const sw = new Swiper(el, {
slidesPerView: 2,
spaceBetween: space,
});
instances.set(el, sw);
}
});
document.querySelectorAll(".winter-slider").forEach((el) => {
if (!instances.has(el)) {
const sw = new Swiper(el, {
slidesPerView: 2.1,
spaceBetween: 8,
loop: true,
pagination: {
el: ".swiper-bullet-wrapper.is-slider-winter",
clickable: true,
bulletClass: "swiper-bullet-winter",
bulletActiveClass: "is_active_winter",
},
});
instances.set(el, sw);
}
});
document.querySelectorAll(".brand-slider").forEach((el) => {
if (!instances.has(el)) {
const sw = new Swiper(el, {
slidesPerView: 1.2,
spaceBetween: 8,
loop: true,
pagination: {
el: ".swiper-bullet-wrapper.is-slider-brand",
clickable: true,
bulletClass: "swiper-bullet-brand",
bulletActiveClass: "is_active_brand",
},
});
instances.set(el, sw);
}
});
}
function destroyAll() {
instances.forEach((sw, el) => {
sw.destroy(true, true);
instances.delete(el);
});
}
function check() {
window.innerWidth <= BREAKPOINT ? init() : destroyAll();
}
check();
window.addEventListener("resize", check, { passive: true });
window.addEventListener("orientationchange", check, { passive: true });
})
.catch(() => {
/* skip if no Swiper */
});
});
})();
/* =============================================================================
10. Webflow Lightbox: dynamic video src (safe)
============================================================================= */
(() => {
const { onReady, waitFor } = window.__DF_UTILS__;
function isUsableHttpsUrl(s) {
if (!/^https:\/\//i.test(s)) return false;
const placeholderRe =
/(put\s+your\s+link\s+here|your\s+link|paste\s+link|insert\s+link|встав(те|ити)?.*посиланн|сюди\s*лінк|сюди\s*посилання)/i;
if (placeholderRe.test(s)) return false;
try {
const u = new URL(s);
if (!u.hostname || u.protocol !== "https:") return false;
if (/\s/.test(s)) return false;
} catch {
return false;
}
return true;
}
function parseYouTube(url) {
const m = url.match(
/(?:youtube\.com\/.*[?&]v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{6,})/i
);
return m ? { id: m[1] } : null;
}
function parseVimeo(url) {
const m = url.match(/vimeo\.com\/(?:video\/)?(\d+)/i);
return m ? { id: m[1] } : null;
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(//g, ">");
}
function iframeHtml(src, w, h) {
const s = escapeHtml(src);
return ``;
}
function buildLightboxItem(url) {
const yt = parseYouTube(url);
if (yt) {
const embed = `https://www.youtube.com/embed/${yt.id}?autoplay=1&rel=0&showinfo=0`;
return {
type: "video",
originalUrl: url,
url,
html: iframeHtml(embed, 940, 528),
thumbnailUrl: `https://i.ytimg.com/vi/${yt.id}/hqdefault.jpg`,
width: 940,
height: 528,
};
}
const vm = parseVimeo(url);
if (vm) {
const embed = `https://player.vimeo.com/video/${vm.id}?autoplay=1&title=0&byline=0&portrait=0`;
return {
type: "video",
originalUrl: url,
url,
html: iframeHtml(embed, 940, 528),
width: 940,
height: 528,
};
}
if (/\.(mp4|webm|ogg)(\?.*)?$/i.test(url)) {
const html = ``;
return {
type: "video",
originalUrl: url,
url,
html,
width: 940,
height: 528,
};
}
return {
type: "video",
originalUrl: url,
url,
html: iframeHtml(url, 940, 528),
width: 940,
height: 528,
};
}
function initDynamicLightboxes(root) {
const lightboxes = root.querySelectorAll(".dynamic-src");
if (!lightboxes.length) return;
lightboxes.forEach((lb) => {
const urlEl = lb.querySelector(".video-data-url");
const rawUrl = (urlEl?.textContent || "").trim();
if (!isUsableHttpsUrl(rawUrl)) return;
const item = buildLightboxItem(rawUrl);
if (!item) return;
lb.setAttribute("href", rawUrl);
let jsonScript = lb.querySelector("script.w-json");
if (!jsonScript) {
jsonScript = document.createElement("script");
jsonScript.type = "application/json";
jsonScript.className = "w-json";
lb.appendChild(jsonScript);
}
jsonScript.textContent = JSON.stringify({ items: [item], group: "" });
lb.removeAttribute("data-wf-lightbox");
});
try {
if (window.Webflow?.require) {
const mod = Webflow.require("lightbox");
if (mod && typeof mod.ready === "function") mod.ready();
}
} catch {}
}
onReady(() => {
// Webflow ready
waitFor(() => !!window.Webflow?.push, { timeout: 4000 })
.then(() => {
window.Webflow = window.Webflow || [];
window.Webflow.push(() => {
initDynamicLightboxes(document);
document.addEventListener("fs-cmsload", () =>
initDynamicLightboxes(document)
);
});
})
.catch(() => {
// Fallback: just run on DOM ready if Webflow API not accessible
initDynamicLightboxes(document);
document.addEventListener("fs-cmsload", () =>
initDynamicLightboxes(document)
);
});
});
})();
/* =============================================================================
11. Lottie: load on visibility + hover control (with .lottie-data-url)
============================================================================= */
(() => {
const { onReady } = window.__DF_UTILS__;
// patch loadAnimation to stash ref on container
onReady(() => {
if (!window.bodymovin) {
console.warn("[Lottie] bodymovin not found.");
return;
}
const orig = bodymovin.loadAnimation;
bodymovin.loadAnimation = function (config) {
const anim = orig(config);
if (config.container) config.container.__lottieAnim = anim;
return anim;
};
});
function getLottiePath(el) {
const inlineEl = el.querySelector(".lottie-data-url");
const inlineUrl = inlineEl?.textContent?.trim() || "";
if (inlineUrl) {
inlineEl.style.display = "none";
return inlineUrl;
}
return (el.getAttribute("data-lottie-src") || "").trim();
}
function initLottie(el) {
if (!window.bodymovin) return;
const path = getLottiePath(el);
if (!path) return;
const playOnHover = el.hasAttribute("data-play-hover");
const loopLottie = el.hasAttribute("data-lottie-loop");
const rendererType = el.getAttribute("data-lottie-renderer") || "svg";
const anim = bodymovin.loadAnimation({
container: el,
renderer: rendererType,
path,
rendererSettings: { preserveAspectRatio: "xMidYMid slice" },
loop: !playOnHover && loopLottie,
autoplay: !playOnHover,
});
if (playOnHover) {
const parent = el.closest(".lottie-wrapper-hover") || el;
anim.setDirection(1);
parent.addEventListener(
"mouseenter",
() => {
anim.setDirection(1);
anim.play();
},
{ passive: true }
);
parent.addEventListener(
"mouseleave",
() => {
anim.setDirection(-1);
anim.play();
},
{ passive: true }
);
}
}
onReady(() => {
if (!window.bodymovin) return;
const observer = new IntersectionObserver(
(entries, obs) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
if (!entry.target.__lottieAnim) initLottie(entry.target);
obs.unobserve(entry.target);
});
},
{ rootMargin: "0px 0px 200px 0px", threshold: 0.1 }
);
const setCandidates = new Set();
document
.querySelectorAll(".lottie-element, [data-lottie-src]")
.forEach((el) => setCandidates.add(el));
const els = Array.from(setCandidates);
els.forEach((el) => {
el.style.position = "relative";
el.style.width = "100%";
el.style.height = "100%";
el.style.overflow = "hidden";
if (el.hasAttribute("data-no-wait")) {
if (!el.__lottieAnim) initLottie(el);
} else {
observer.observe(el);
}
});
});
})();
/* =============================================================================
12. Filters accordion (jQuery if present)
============================================================================= */
(() => {
const { waitFor } = window.__DF_UTILS__;
waitFor(() => !!window.jQuery, { timeout: 4000 })
.then(() => {
const $ = window.jQuery;
function initFiltersAccordion() {
const $groups = $(".filters_filter-group");
if (!$groups.length) return;
const $headings = $groups.find(".filters_filter-group-heading");
$headings.off(".accordion");
if ($(window).width() < 991) {
$groups.removeClass("is-active").find(".flex-filtres-left").hide();
$headings.on("click.accordion", function () {
const $group = $(this).closest(".filters_filter-group");
const $content = $group.find(".flex-filtres-left");
if ($group.hasClass("is-active")) {
$content.slideUp(200);
$group.removeClass("is-active");
} else {
$groups
.filter(".is-active")
.removeClass("is-active")
.find(".flex-filtres-left")
.slideUp(200);
$group.addClass("is-active");
$content.slideDown(200);
}
});
} else {
$groups.each(function () {
const $g = $(this);
const $c = $g.find(".flex-filtres-left");
$g.hasClass("is-active") ? $c.show() : $c.hide();
});
$headings.on("click.accordion", function () {
const $group = $(this).closest(".filters_filter-group");
const $content = $group.find(".flex-filtres-left");
if ($group.hasClass("is-active")) {
$group.removeClass("is-active");
$content.slideUp(200);
} else {
$group.addClass("is-active");
$content.slideDown(200);
}
});
}
}
initFiltersAccordion();
let rt;
$(window).on("resize", function () {
clearTimeout(rt);
rt = setTimeout(initFiltersAccordion, 120);
});
})
.catch(() => {
/* no jQuery → skip */
});
})();
/* =============================================================================
13. Filters open/close on tablet (jQuery if present)
============================================================================= */
(() => {
const { waitFor } = window.__DF_UTILS__;
waitFor(() => !!window.jQuery, { timeout: 4000 })
.then(() => {
const $ = window.jQuery;
function initFilterToggle() {
const $wrapper = $(".filters_lists-wrapper");
if (!$wrapper.length) return;
const $openBtn = $("[data-filters-open]");
const $closeBtn = $("[data-filters-close]");
$openBtn.off("click.filterToggle");
$closeBtn.off("click.filterToggle");
if ($(window).width() < 991) {
$wrapper.hide().removeClass("is-active");
$openBtn.on("click.filterToggle", function () {
$wrapper.hasClass("is-active")
? $wrapper.removeClass("is-active").slideUp(200)
: $wrapper.addClass("is-active").slideDown(200);
});
$closeBtn.on("click.filterToggle", function () {
if ($wrapper.hasClass("is-active"))
$wrapper.removeClass("is-active").slideUp(200);
});
} else {
$wrapper.show().removeClass("is-active");
}
}
initFilterToggle();
let rt;
$(window).on("resize", function () {
clearTimeout(rt);
rt = setTimeout(initFilterToggle, 120);
});
})
.catch(() => {
/* no jQuery → skip */
});
})();
/* =============================================================================
14. Pagination hide state (robust observers)
============================================================================= */
(() => {
const { onReady } = window.__DF_UTILS__;
const PAGINATION_SELECTOR = ".pagination";
const COUNT_SELECTOR = ".w-page-count";
function normalize(text) {
return (text || "")
.replace(/\u00A0/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function readTotalPages(paginationEl) {
const countEl = paginationEl.querySelector(COUNT_SELECTOR);
if (!countEl) return null;
const text = normalize(countEl.textContent);
const m = text.match(/^(\d+)\s*\/\s*(\d+)$/);
if (m) {
const total = parseInt(m[2], 10);
if (!Number.isNaN(total)) return total;
}
const aria = countEl.getAttribute("aria-label") || "";
const ariaMatch = aria.match(/of\s+(\d+)/i);
if (ariaMatch) {
const total = parseInt(ariaMatch[1], 10);
if (!Number.isNaN(total)) return total;
}
return null;
}
function updateVisibility(paginationEl) {
const total = readTotalPages(paginationEl);
if (total === null) {
paginationEl.removeAttribute("data-hidden");
return;
}
total <= 1
? paginationEl.setAttribute("data-hidden", "true")
: paginationEl.removeAttribute("data-hidden");
}
function observeCount(paginationEl) {
const countEl = paginationEl.querySelector(COUNT_SELECTOR);
if (!countEl) return;
updateVisibility(paginationEl);
const mo = new MutationObserver(() =>
setTimeout(() => updateVisibility(paginationEl), 0)
);
mo.observe(countEl, {
subtree: true,
childList: true,
characterData: true,
});
}
function attach() {
const paginationEl = document.querySelector(PAGINATION_SELECTOR);
if (!paginationEl) return;
observeCount(paginationEl);
}
onReady(attach);
const outer = new MutationObserver(() => {
const paginationEl = document.querySelector(PAGINATION_SELECTOR);
if (!paginationEl) return;
observeCount(paginationEl);
});
outer.observe(document.body, { childList: true, subtree: true });
[
"fs-cmsfilter-update",
"fs-cmsfilter-reset",
"fs-cmsfilter-change",
"fs-cmsload",
].forEach((evt) => {
window.addEventListener(evt, () => setTimeout(attach, 0), {
passive: true,
});
});
})();
/* =============================================================================
15. Scrolling Component (desktop) – sticky media & Lottie reset
============================================================================= */
(() => {
const { onReady, waitFor } = window.__DF_UTILS__;
onReady(() => {
waitFor(
() => typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined"
)
.then(() => {
const mm = gsap.matchMedia();
mm.add("(min-width: 992px)", () => {
document.querySelectorAll(".scrolling_component").forEach((comp) => {
comp
.querySelectorAll(".scrolling_content-and-media")
.forEach((sec) => {
ScrollTrigger.create({
trigger: sec,
start: "top 50%",
end: "bottom 50%",
onEnter: () => activate(sec),
onEnterBack: () => activate(sec),
});
});
});
function activate(sec) {
const parent = sec.closest(".scrolling_component");
parent
?.querySelectorAll(".scrolling_content-and-media")
.forEach((s) => s.classList.remove("is-active-scrolling"));
sec.classList.add("is-active-scrolling");
const lottie = sec.querySelector(".lottie-element");
lottie?.__lottieAnim?.goToAndPlay(0, true);
}
return () => ScrollTrigger.getAll().forEach((t) => t.kill());
});
})
.catch(() => {
/* no GSAP */
});
});
})();
/* =============================================================================
16. New scroll block component (pairs): desktop scrub & mobile reveal
============================================================================= */
(() => {
const { onReady, waitFor } = window.__DF_UTILS__;
onReady(() => {
waitFor(
() => typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined"
)
.then(() => {
const pairs = [];
gsap.utils
.toArray(".scrolling_content-and-media-new")
.forEach((wrapper) => {
const content = wrapper.querySelector(".scrolling_content-box-new");
const media = wrapper.querySelector(".scrolling_media_wrap-new");
if (content && media) pairs.push({ wrapper, content, media });
});
if (!pairs.length) {
gsap.utils.toArray(".scrolling_content-box-new").forEach((box) => {
const next = box.nextElementSibling;
if (next?.matches(".scrolling_media_wrap-new")) {
const wrapper =
box.closest(".scrolling_content-and-media-new") ||
box.parentElement;
pairs.push({ wrapper, content: box, media: next });
}
});
}
if (!pairs.length) return;
const getOffset = (c, m) =>
m.getAttribute("data-media-offset") ||
c.getAttribute("data-media-offset") ||
"3rem";
pairs.forEach(({ content, media }) => {
const off = getOffset(content, media);
gsap.set(content, { opacity: 0 });
gsap.set(media, { opacity: 0, y: off });
});
function setupDesktop({ content, media }) {
const D1 = 20,
D2 = 40,
D3 = 30;
const offset = getOffset(content, media);
const tl = gsap.timeline({
defaults: { ease: "none", overwrite: "auto" },
scrollTrigger: {
trigger: content,
start: "top 50%",
end: "top 0%",
scrub: true,
invalidateOnRefresh: true,
},
});
tl.fromTo(content, { opacity: 0 }, { opacity: 1, duration: D1 })
.to(content, { opacity: 1, duration: D2 })
.to(content, { opacity: 0, duration: D3 });
ScrollTrigger.create({
trigger: content,
start: "top 50%",
end: "top -11%",
invalidateOnRefresh: true,
onEnter: () =>
gsap.fromTo(
media,
{ opacity: 0, y: offset },
{
opacity: 1,
y: 0,
duration: 0.5,
ease: "power2.out",
overwrite: "auto",
}
),
onEnterBack: () =>
gsap.fromTo(
media,
{ opacity: 0, y: offset },
{
opacity: 1,
y: 0,
duration: 0.5,
ease: "power2.out",
overwrite: "auto",
}
),
onLeave: () =>
gsap.to(media, {
opacity: 0,
y: offset,
duration: 0.3,
ease: "power2.out",
overwrite: "auto",
}),
onLeaveBack: () =>
gsap.to(media, {
opacity: 0,
y: offset,
duration: 0.3,
ease: "power2.out",
overwrite: "auto",
}),
});
}
function setupMobile({ wrapper, content, media }) {
const offset = getOffset(content, media);
gsap.set([content, media], { opacity: 0 });
gsap.set(media, { y: offset });
gsap
.timeline({
defaults: { ease: "power2.out", overwrite: "auto" },
scrollTrigger: {
trigger: wrapper || content,
start: "top 50%",
toggleActions: "play none none none",
once: true,
invalidateOnRefresh: true,
},
})
.fromTo(
[content, media],
{ opacity: 0, y: (i, el) => (el === media ? offset : "3rem") },
{ opacity: 1, y: 0, duration: 0.8, stagger: 0 }
);
}
const mm = gsap.matchMedia();
mm.add("(min-width: 992px)", () => pairs.forEach(setupDesktop));
mm.add("(max-width: 991px)", () => pairs.forEach(setupMobile));
window.addEventListener("load", () => ScrollTrigger.refresh(), {
passive: true,
});
})
.catch(() => {
/* no GSAP */
});
});
})();
/* =============================================================================
17. Progress lines for .scrolling_component-new
============================================================================= */
(() => {
const { onReady, waitFor } = window.__DF_UTILS__;
onReady(() => {
waitFor(
() => typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined"
)
.then(() => {
const boxes = document.querySelectorAll(".scrolling_component-new");
if (!boxes.length) return;
boxes.forEach((box) => {
const line = box.querySelector(".progress-line-scroll");
if (!line) return;
gsap.to(line, {
height: "100%",
ease: "none",
scrollTrigger: {
trigger: box,
start: "top 50%",
end: "bottom 50%",
scrub: true,
},
});
});
})
.catch(() => {
/* no GSAP */
});
});
})();