document.documentElement.classList.add("js");
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ---------- Mobile nav toggle ---------- */
const navToggle = document.querySelector(".nav-toggle");
const siteNav = document.querySelector(".site-nav");
if (navToggle && siteNav) {
navToggle.addEventListener("click", () => {
const open = siteNav.classList.toggle("open");
navToggle.setAttribute("aria-expanded", String(open));
});
}
/* ---------- Staggered scroll reveal ---------- */
const revealEls = document.querySelectorAll(".reveal");
if ("IntersectionObserver" in window) {
revealEls.forEach((el) => {
if (el.dataset.delay) el.style.transitionDelay = el.dataset.delay + "ms";
});
const revealObserver = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
revealObserver.unobserve(entry.target);
}
}
},
{ threshold: 0.12, rootMargin: "0px 0px -4% 0px" }
);
revealEls.forEach((el) => revealObserver.observe(el));
} else {
revealEls.forEach((el) => el.classList.add("visible"));
}
/* ---------- Count-up stats ---------- */
const countEls = document.querySelectorAll("[data-count]");
function runCount(el) {
const target = parseInt(el.dataset.count, 10);
const suffix = el.dataset.suffix || "";
const duration = 1300;
const start = performance.now();
function frame(now) {
const t = Math.min((now - start) / duration, 1);
const eased = 1 - Math.pow(1 - t, 3);
el.textContent = Math.round(target * eased) + suffix;
if (t < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
if (countEls.length) {
if (reducedMotion || !("IntersectionObserver" in window)) {
countEls.forEach((el) => {
el.textContent = el.dataset.count + (el.dataset.suffix || "");
});
} else {
const countObserver = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
runCount(entry.target);
countObserver.unobserve(entry.target);
}
}
},
{ threshold: 0.6 }
);
countEls.forEach((el) => countObserver.observe(el));
}
}
/* ---------- Parallax drift (decorative cutouts) ---------- */
const parallaxItems = [];
if (!reducedMotion && "IntersectionObserver" in window) {
document.querySelectorAll("[data-parallax]").forEach((el) => {
parallaxItems.push({
el,
speed: parseFloat(el.dataset.parallax) || 0.15,
rotate: parseFloat(el.dataset.rotate) || 0,
top: 0,
height: 0,
y: 0,
visible: false,
});
});
}
if (parallaxItems.length) {
const gate = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
const item = parallaxItems.find((p) => p.el === entry.target);
if (item) item.visible = entry.isIntersecting;
}
},
{ rootMargin: "20% 0px 20% 0px" }
);
parallaxItems.forEach((p) => gate.observe(p.el));
function measure() {
for (const p of parallaxItems) {
const rect = p.el.getBoundingClientRect();
p.top = rect.top + window.scrollY - p.y;
p.height = rect.height;
}
}
let ticking = false;
function render() {
ticking = false;
const vh = window.innerHeight;
const sc = window.scrollY;
for (const p of parallaxItems) {
if (!p.visible) continue;
const fromCenter = p.top + p.height / 2 - sc - vh / 2;
p.y = fromCenter * -p.speed;
const deg = p.rotate * (fromCenter / vh);
p.el.style.transform =
"translate3d(0," + p.y.toFixed(1) + "px,0) rotate(" + deg.toFixed(2) + "deg)";
}
}
function requestRender() {
if (!ticking) {
ticking = true;
requestAnimationFrame(render);
}
}
measure();
render();
window.addEventListener("scroll", requestRender, { passive: true });
window.addEventListener("resize", () => {
measure();
requestRender();
});
}
/* ---------- Smart header (hide on scroll down, show on scroll up) ---------- */
const header = document.querySelector(".site-header");
if (header && !reducedMotion) {
let lastY = window.scrollY;
let headerTicking = false;
function updateHeader() {
headerTicking = false;
const y = window.scrollY;
const navOpen = siteNav && siteNav.classList.contains("open");
if (!navOpen && y > 180 && y > lastY + 6) {
header.classList.add("header-hidden");
} else if (y < lastY - 6 || y <= 180) {
header.classList.remove("header-hidden");
}
lastY = y;
}
window.addEventListener(
"scroll",
() => {
if (!headerTicking) {
headerTicking = true;
requestAnimationFrame(updateHeader);
}
},
{ passive: true }
);
}