1/* ========================================================================== 2 Gupt landing â premium interactions 3 - Theme toggle (with smooth transition + system follow-up) 4 - Smooth scroll for in-page anchors 5 - FAQ exclusive accordion 6 - Reveal-on-scroll with stagger via data-delay 7 - Scroll-spy nav underline 8 - Magnetic buttons (cursor pull) 9 - Bento cards radial spotlight on hover 10 - Counter animations on stats 11 - Back-to-top button visibility 12 - Mobile burger toggle 13 ========================================================================== */ 14(function () { 15 "use strict"; 16 17 const root = document.documentElement; 18 const prefersReduced = matchMedia("(prefers-reduced-motion: reduce)").matches; 19 const isPointerFine = matchMedia("(pointer: fine)").matches; 20 const STORAGE_KEY = "gupt-site-theme"; 21 22 // ---------- 1. Theme toggle ---------- 23 function applyTheme(theme, persist) { 24 root.classList.add("theme-transition"); 25 root.setAttribute("data-theme", theme); 26 if (persist) { 27 try { 28 localStorage.setItem(STORAGE_KEY, theme); 29 } catch (e) {} 30 } 31 window.setTimeout(() => root.classList.remove("theme-transition"), 320); 32 } 33 34 document.querySelectorAll("[data-theme-toggle]").forEach((btn) => { 35 btn.addEventListener("click", () => { 36 const current = root.getAttribute("data-theme") || "light"; 37 applyTheme(current === "dark" ? "light" : "dark", true); 38 }); 39 }); 40 41 // Follow OS theme if user hasn't explicitly chosen 42 try { 43 if (!localStorage.getItem(STORAGE_KEY) && window.matchMedia) { 44 const mq = window.matchMedia("(prefers-color-scheme: dark)"); 45 const onChange = (e) => { 46 if (!localStorage.getItem(STORAGE_KEY)) { 47 applyTheme(e.matches ? "dark" : "light", false); 48 } 49 }; 50 if (mq.addEventListener) mq.addEventListener("change", onChange); 51 else if (mq.addListener) mq.addListener(onChange); 52 } 53 } catch (e) {} 54 55 // ---------- 2. Smooth scroll ---------- 56 document.querySelectorAll('a[href^="#"]').forEach((link) => { 57 link.addEventListener("click", (event) => { 58 const href = link.getAttribute("href"); 59 if (!href || href === "#") return; 60 const target = document.querySelector(href); 61 if (!target) return; 62 event.preventDefault(); 63 const top = target.getBoundingClientRect().top + window.scrollY - 80; 64 window.scrollTo({ top, behavior: prefersReduced ? "auto" : "smooth" }); 65 history.replaceState(null, "", href); 66 }); 67 }); 68 69 // ---------- 3. FAQ accordion (exclusive) ---------- 70 const faqItems = document.querySelectorAll(".faq details"); 71 faqItems.forEach((item) => { 72 item.addEventListener("toggle", () => { 73 if (!item.open) return; 74 faqItems.forEach((other) => { 75 if (other !== item) other.open = false; 76 }); 77 }); 78 }); 79 80 // ---------- 4. Footer year ---------- 81 document.querySelectorAll("[data-year]").forEach((el) => { 82 el.textContent = String(new Date().getFullYear()); 83 }); 84 85 // ---------- 5. Nav: shadow + active section spy ---------- 86 const nav = document.querySelector(".nav"); 87 const navLinks = Array.from(document.querySelectorAll("[data-nav-link]")); 88 const sectionTargets = navLinks 89 .map((link) => { 90 const id = link.getAttribute("href"); 91 return id ? document.querySelector(id) : null; 92 }) 93 .filter(Boolean); 94 95 const onScroll = () => { 96 if (nav) nav.classList.toggle("is-scrolled", window.scrollY > 6); 97 98 // back-to-top visibility 99 const top = document.querySelector("[data-to-top]"); 100 if (top) top.classList.toggle("is-visible", window.scrollY > 480); 101 }; 102 onScroll(); 103 window.addEventListener("scroll", onScroll, { passive: true }); 104 105 // Scroll spy via IntersectionObserver â pick the first section "near the top" 106 if (sectionTargets.length && "IntersectionObserver" in window) { 107 const spy = new IntersectionObserver( 108 (entries) => { 109 entries.forEach((entry) => { 110 if (!entry.isIntersecting) return; 111 const id = "#" + entry.target.id; 112 navLinks.forEach((l) => 113 l.classList.toggle("is-active", l.getAttribute("href") === id) 114 ); 115 }); 116 }, 117 { rootMargin: "-30% 0px -55% 0px", threshold: 0 } 118 );
119 sectionTargets.forEach((s) => spy.observe(s)); 120 } 121 122 // ---------- 6. Reveal-on-scroll with stagger ---------- 123 if ("IntersectionObserver" in window && !prefersReduced) { 124 const reveals = document.querySelectorAll(".reveal"); 125 reveals.forEach((el) => { 126 const delay = parseInt(el.getAttribute("data-delay") || "0", 10); 127 el.style.transitionDelay = delay + "ms"; 128 }); 129 130 const io = new IntersectionObserver( 131 (entries) => { 132 entries.forEach((entry) => { 133 if (entry.isIntersecting) { 134 entry.target.classList.add("is-in-view"); 135 io.unobserve(entry.target); 136 } 137 }); 138 }, 139 { threshold: 0.14, rootMargin: "0px 0px -40px 0px" } 140 ); 141 reveals.forEach((el) => io.observe(el)); 142 } else { 143 document.querySelectorAll(".reveal").forEach((el) => el.classList.add("is-in-view")); 144 } 145 146 // ---------- 7. Magnetic buttons ---------- 147 if (isPointerFine && !prefersReduced) { 148 document.querySelectorAll("[data-magnetic]").forEach((el) => { 149 const STRENGTH = 0.22; 150 const MAX = 8; 151 152 const onMove = (e) => { 153 const rect = el.getBoundingClientRect(); 154 const x = e.clientX - rect.left - rect.width / 2; 155 const y = e.clientY - rect.top - rect.height / 2; 156 const dx = Math.max(-MAX, Math.min(MAX, x * STRENGTH)); 157 const dy = Math.max(-MAX, Math.min(MAX, y * STRENGTH)); 158 el.style.transform = `translate3d(${dx}px, ${dy}px, 0)`; 159 }; 160 161 const onLeave = () => { 162 el.style.transform = ""; 163 }; 164 165 el.addEventListener("mousemove", onMove); 166 el.addEventListener("mouseleave", onLeave); 167 }); 168 } 169 170 // ---------- 8. Bento cards: cursor spotlight ---------- 171 if (isPointerFine && !prefersReduced) { 172 document.querySelectorAll(".bento-card").forEach((card) => { 173 card.addEventListener("mousemove", (e) => { 174 const rect = card.getBoundingClientRect(); 175 const mx = ((e.clientX - rect.left) / rect.width) * 100; 176 const my = ((e.clientY - rect.top) / rect.height) * 100; 177 card.style.setProperty("--mx", mx + "%"); 178 card.style.setProperty("--my", my + "%"); 179 }); 180 }); 181 } 182 183 // ---------- 9. Counter animations ---------- 184 if (!prefersReduced && "IntersectionObserver" in window) { 185 const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3); 186 187 const startCounter = (el) => { 188 const target = parseFloat(el.getAttribute("data-target") || "0"); 189 const suffix = el.getAttribute("data-suffix") || ""; 190 const duration = 1400; 191 const start = performance.now(); 192 193 const tick = (now) => { 194 const t = Math.min(1, (now - start) / duration); 195 const value = Math.round(target * easeOutCubic(t)); 196 el.textContent = String(value) + suffix; 197 if (t < 1) requestAnimationFrame(tick); 198 }; 199 requestAnimationFrame(tick); 200 }; 201 202 const counterIo = new IntersectionObserver( 203 (entries) => { 204 entries.forEach((entry) => { 205 if (entry.isIntersecting) { 206 startCounter(entry.target); 207 counterIo.unobserve(entry.target); 208 } 209 }); 210 }, 211 { threshold: 0.4 } 212 ); 213 214 document.querySelectorAll("[data-counter]").forEach((el) => counterIo.observe(el)); 215 } 216 217 // ---------- 10. Back to top ---------- 218 document.querySelectorAll("[data-to-top]").forEach((btn) => { 219 btn.addEventListener("click", () => { 220 window.scrollTo({ top: 0, behavior: prefersReduced ? "auto" : "smooth" }); 221 }); 222 }); 223 224 // ---------- 11. Mobile burger (mounts a slide-down drawer) ---------- 225 const burger = document.querySelector("[data-burger]"); 226 if (burger) { 227 const drawer = document.createElement("nav"); 228 drawer.className = "nav-drawer"; 229 drawer.setAttribute("aria-label", "Mobile menu"); 230 drawer.innerHTML = ` 231 <a href="#features">Features</a> 232 <a href="#how">How it works</a> 233 <a href="#privacy">Privacy</a> 234 <a href="#pricing">Pricing</a> 235 <a href="#faq">FAQ</a> 236 `; 237 document.body.appendChild(drawer); 238 239 const closeDrawer = () => { 240 drawer.classList.remove("is-open"); 241 burger.setAttribute("aria-expanded", "false"); 242 }; 243 244 burger.addEventListener("click", () => { 245 const isOpen = drawer.classList.toggle("is-open"); 246 burger.setAttribute("aria-expanded", isOpen ? "true" : "false"); 247 }); 248 249 drawer.querySelectorAll("a").forEach((a) => { 250 a.addEventListener("click", closeDrawer); 251 }); 252 253 document.addEventListener("keydown", (e) => { 254 if (e.key === "Escape") closeDrawer(); 255 }); 256 } 257 258 // ---------- 12. Mesh blob parallax (very subtle, follows cursor) ---------- 259 if (isPointerFine && !prefersReduced) { 260 const blobs = document.querySelectorAll(".mesh-blob"); 261 if (blobs.length) { 262 let raf = null; 263 window.addEventListener( 264 "mousemove", 265 (e) => { 266 if (raf) return; 267 raf = requestAnimationFrame(() => { 268 const x = (e.clientX / window.innerWidth - 0.5) * 30; 269 const y = (e.clientY / window.innerHeight - 0.5) * 30;
270 blobs.forEach((b, i) => { 271 const factor = i === 0 ? 1 : i === 1 ? -1 : 0.6; 272 b.style.translate = `${x * factor}px ${y * factor}px`; 273 }); 274 raf = null; 275 }); 276 }, 277 { passive: true } 278 ); 279 } 280 } 281})();
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.