(() => { const body = document.body; const header = document.querySelector(".site-header"); const menuButton = document.querySelector(".menu-button"); const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches; addEventListener("load", () => setTimeout( () => document.querySelector(".boot")?.classList.add("done"), 380, ), ); addEventListener( "scroll", () => header?.classList.toggle("scrolled", scrollY > 24), { passive: true }, ); const closeMenu = () => { body.classList.remove("menu-open"); menuButton?.setAttribute("aria-expanded", "false"); menuButton?.setAttribute("aria-label", "Open menu"); }; menuButton?.addEventListener("click", () => { const open = body.classList.toggle("menu-open"); menuButton.setAttribute("aria-expanded", String(open)); menuButton.setAttribute("aria-label", open ? "Close menu" : "Open menu"); }); document .querySelectorAll(".site-header nav a") .forEach((link) => link.addEventListener("click", closeMenu)); const observer = new IntersectionObserver( (entries) => entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add("visible"); observer.unobserve(entry.target); }), { threshold: 0.08, rootMargin: "0px 0px -4% 0px" }, ); document .querySelectorAll(".reveal") .forEach((element) => observer.observe(element)); if (!reducedMotion) { document.querySelectorAll(".magnetic").forEach((card) => { card.addEventListener("pointermove", (event) => { const rect = card.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; card.style.setProperty("--mx", `${x - 95}px`); card.style.setProperty("--my", `${y - 95}px`); card.style.transform = `perspective(850px) rotateX(${((rect.height / 2 - y) / rect.height) * 5}deg) rotateY(${((x - rect.width / 2) / rect.width) * 5}deg)`; }); card.addEventListener("pointerleave", () => (card.style.transform = "")); }); } const form = document.querySelector("#contact-form"); form?.addEventListener("submit", (event) => { event.preventDefault(); const data = new FormData(form); const summary = `reFORGE group enquiry\n\nName: ${data.get("name")}\nEmail: ${data.get("email")}\nAlignment: ${data.get("alignment")}\n\nOpportunity:\n${data.get("problem")}`; const status = form.querySelector(".form-status"); status.textContent = "Your enquiry is prepared. Secure delivery will be connected before launch."; navigator.clipboard ?.writeText(summary) .then(() => { status.textContent = "Your enquiry is prepared and copied. Secure delivery will be connected before launch."; }) .catch(() => {}); }); document.querySelector("#year").textContent = new Date().getFullYear(); if (reducedMotion) return; const forge = document.querySelector(".forge-scroll"); const stage = forge?.querySelector(".forge-stage"); const symbol = forge?.querySelector(".forge-symbol"); const slices = forge ? [...forge.querySelectorAll(".forge-slice")] : []; const phases = forge ? [...forge.querySelectorAll(".forge-phase")] : []; const percent = forge?.querySelector(".forge-percent"); const heroCopy = document.querySelector(".hero-copy"); const heroMark = document.querySelector(".hero-monogram"); let ticking = false; const clamp = (value, min = 0, max = 1) => Math.min(max, Math.max(min, value)); const ease = (value) => 1 - Math.pow(1 - clamp(value), 3); function updateScrollStory() { ticking = false; const heroProgress = clamp(scrollY / Math.max(innerHeight, 1)); if (heroCopy) { heroCopy.style.transform = `translate3d(0, ${heroProgress * -70}px, 0)`; heroCopy.style.opacity = String(1 - heroProgress * 0.72); } if (heroMark) { heroMark.style.transform = `translate3d(0, ${heroProgress * 120}px, 0) translateY(-45%) rotate(${2 + heroProgress * 7}deg)`; } if (!forge || !stage || !symbol || slices.length !== 3) return; const rect = forge.getBoundingClientRect(); const distance = Math.max(forge.offsetHeight - innerHeight, 1); const progress = clamp(-rect.top / distance); const assemble = ease(progress / 0.25); const loose = 1 - assemble; const mobile = innerWidth <= 620; const offsets = mobile ? [ [-0.58 * innerWidth, -0.24 * innerHeight, -18], [0.61 * innerWidth, -0.02 * innerHeight, 13], [-0.46 * innerWidth, 0.25 * innerHeight, 16], ] : [ [-0.34 * innerWidth, -0.3 * innerHeight, -18], [0.36 * innerWidth, -0.04 * innerHeight, 13], [-0.24 * innerWidth, 0.31 * innerHeight, 16], ]; slices.forEach((slice, index) => { const [x, y, rotation] = offsets[index]; slice.style.transform = `translate3d(${x * loose}px, ${y * loose}px, 0) rotate(${rotation * loose}deg)`; slice.style.opacity = String(0.4 + assemble * 0.6); }); const move = ease((progress - 0.27) / 0.25); const final = ease((progress - 0.7) / 0.25); const symbolX = mobile ? 0 : -innerWidth * 0.235 * move; const symbolY = mobile ? -innerHeight * 0.17 * move : 0; const symbolScale = 0.78 + assemble * 0.22 - move * 0.14 + final * 0.12; symbol.style.transform = `translate3d(calc(-50% + ${symbolX}px), calc(-50% + ${symbolY}px), 0) scale(${symbolScale}) rotate(${final * -4}deg)`; const disciplineIn = ease((progress - 0.25) / 0.12); const disciplineOut = 1 - ease((progress - 0.63) / 0.12); const disciplineOpacity = Math.min(disciplineIn, disciplineOut); const wash = ease((progress - 0.76) / 0.18); stage.style.setProperty( "--forge-progress", `${Math.round(progress * 100)}%`, ); stage.style.setProperty( "--discipline-opacity", disciplineOpacity.toFixed(3), ); stage.style.setProperty("--wash-opacity", (wash * 0.96).toFixed(3)); stage.style.setProperty("--orbit-scale", String(0.7 + progress * 0.65)); stage.style.setProperty("--orbit-opacity", String(0.42 - wash * 0.25)); stage.style.setProperty( "--scroll-note-opacity", String(1 - ease(progress / 0.12)), ); stage.classList.toggle("is-final", progress > 0.74); if (percent) percent.textContent = String(Math.round(progress * 100)).padStart(2, "0"); const phase = progress < 0.22 ? 0 : progress < 0.48 ? 1 : progress < 0.73 ? 2 : 3; phases.forEach((item, index) => item.classList.toggle("is-active", index === phase), ); } function requestScrollStory() { if (ticking) return; ticking = true; requestAnimationFrame(updateScrollStory); } addEventListener("scroll", requestScrollStory, { passive: true }); addEventListener("resize", requestScrollStory); updateScrollStory(); })();