<script>
/* ============================================================
SCROLLYCORE JAVASCRIPT
This is the shared behavior engine.
Keep this once near the top of any Squarespace page that uses ScrollyBlocks.
============================================================ */
(function () {
function initScrollyCore() {
if (!document.getElementById("scrollProgress")) {
const progressBar = document.createElement("div");
progressBar.className = "scroll-progress";
progressBar.id = "scrollProgress";
document.body.prepend(progressBar);
}
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) {
document.querySelectorAll(".video-hero video").forEach(video => {
video.pause();
video.removeAttribute("autoplay");
});
}
const fadeObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
}
});
}, { threshold: 0.15 });
document.querySelectorAll(".fade-in").forEach(el => fadeObserver.observe(el));
const stepObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const group = entry.target.closest("[data-step-group]") || document;
group.querySelectorAll(".step").forEach(step => step.classList.remove("active"));
entry.target.classList.add("active");
}
});
}, { threshold: 0.55 });
document.querySelectorAll(".step").forEach(step => stepObserver.observe(step));
document.querySelectorAll(".switcher-button").forEach(button => {
button.addEventListener("click", () => {
const parent = button.closest("[data-switcher]");
if (!parent) return;
const itemIndex = button.dataset.switcherItem;
parent.querySelectorAll(".switcher-button").forEach(btn => {
btn.classList.remove("active");
btn.setAttribute("aria-pressed", "false");
});
button.classList.add("active");
button.setAttribute("aria-pressed", "true");
parent.querySelectorAll(".switcher-panel").forEach(panel => panel.classList.remove("active"));
parent.querySelectorAll(`[data-switcher-panel="${itemIndex}"], [data-switcher-media="${itemIndex}"]`).forEach(panel => {
panel.classList.add("active");
});
});
});
document.querySelectorAll("[data-rabbit-open]").forEach(button => {
button.addEventListener("click", () => {
const modalId = button.dataset.rabbitOpen;
const modal = document.getElementById(modalId);
if (!modal) return;
modal.classList.add("open");
document.body.classList.add("rabbit-hole-open");
const closeButton = modal.querySelector("[data-rabbit-close]");
if (closeButton) closeButton.focus();
});
});
document.querySelectorAll("[data-rabbit-close]").forEach(button => {
button.addEventListener("click", () => {
const modalId = button.dataset.rabbitClose;
const modal = document.getElementById(modalId);
if (!modal) return;
modal.classList.remove("open");
document.body.classList.remove("rabbit-hole-open");
const opener = document.querySelector(`[data-rabbit-open="${modalId}"]`);
if (opener) opener.focus();
});
});
document.querySelectorAll(".rabbit-hole-overlay").forEach(overlay => {
overlay.addEventListener("click", event => {
if (event.target !== overlay) return;
overlay.classList.remove("open");
document.body.classList.remove("rabbit-hole-open");
});
});
document.addEventListener("keydown", event => {
if (event.key !== "Escape") return;
document.querySelectorAll(".rabbit-hole-overlay.open").forEach(modal => {
modal.classList.remove("open");
});
document.body.classList.remove("rabbit-hole-open");
});
/* ============================================================
LAUREN-PROOF AREA #6: SCROLLSWAP IMAGE-SWITCH TIMING
More negative rootMargin switches later. Less negative switches earlier.
============================================================ */
const swapStepObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const step = entry.target;
const parent = step.closest("[data-scroll-swap]");
if (!parent) return;
const image = parent.querySelector("[data-scroll-swap-image]");
const caption = parent.querySelector("[data-scroll-swap-caption]");
const newImage = step.dataset.image;
const newAlt = step.dataset.alt || "";
const newCaption = step.dataset.caption || "";
if (!image || !newImage) return;
parent.querySelectorAll(".swap-step").forEach(s => s.classList.remove("active"));
step.classList.add("active");
image.classList.add("is-changing");
window.setTimeout(() => {
image.src = newImage;
image.alt = newAlt;
if (caption) caption.textContent = newCaption;
image.addEventListener("load", () => {
image.classList.remove("is-changing");
}, { once: true });
}, 420);
});
}, { threshold: 0.3, rootMargin: window.matchMedia("(max-width: 780px)").matches ? "0px 0px -20% 0px" : "0px 0px -50% 0px" });
document.querySelectorAll(".swap-step").forEach(step => swapStepObserver.observe(step));
const diagramStepObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const step = entry.target;
const target = step.dataset.diagramTarget;
const parent = step.closest("[data-diagram]");
if (!parent) return;
parent.querySelectorAll(".diagram-step").forEach(s => s.classList.remove("active"));
step.classList.add("active");
parent.querySelectorAll(".diagram-node, .diagram-link").forEach(part => {
part.classList.toggle("active", part.dataset.diagramPart === target || target === "capacity");
});
});
}, { threshold: 0.55 });
document.querySelectorAll(".diagram-step").forEach(step => diagramStepObserver.observe(step));
document.querySelectorAll(".diagram-node, .diagram-link").forEach(part => {
part.addEventListener("mouseenter", () => {
const diagram = part.closest("[data-diagram]");
if (!diagram) return;
const key = part.dataset.diagramPart;
diagram.querySelectorAll(".diagram-node, .diagram-link").forEach(piece => {
piece.classList.toggle("hover-match", piece.dataset.diagramPart === key);
});
});
part.addEventListener("mouseleave", () => {
const diagram = part.closest("[data-diagram]");
if (!diagram) return;
diagram.querySelectorAll(".diagram-node, .diagram-link").forEach(piece => {
piece.classList.remove("hover-match");
});
});
});
function updateParallax() {
if (prefersReducedMotion) return;
document.querySelectorAll("[data-parallax-section]").forEach(section => {
const bg = section.querySelector(".parallax-bg");
if (!bg) return;
const rect = section.getBoundingClientRect();
const windowHeight = window.innerHeight;
const progress = (windowHeight - rect.top) / (windowHeight + rect.height);
const clamped = Math.max(0, Math.min(1, progress));
const movement = (clamped - 0.5) * 800;
bg.style.transform = `translateY(${movement}px)`;
});
}
function updateProgress() {
const progressBar = document.getElementById("scrollProgress");
if (!progressBar) return;
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
progressBar.style.width = `${progress}%`;
}
window.addEventListener("scroll", () => {
updateProgress();
updateParallax();
});
window.addEventListener("resize", updateParallax);
updateProgress();
updateParallax();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initScrollyCore);
} else {
initScrollyCore();
}
})();
</script>