1/* Escavox â site interactions (Motion-powered animations with graceful fallback) */ 2(function () { 3 "use strict"; 4 5 var M = window.Motion; // vendored motion.js (UMD global) 6 var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 7 var canAnimate = !!(M && M.animate && M.inView) && !reduce; 8 9 /* Mobile nav toggle */ 10 var toggle = document.querySelector(".nav-toggle"); 11 var menu = document.querySelector(".nav-menu"); 12 if (toggle && menu) { 13 toggle.addEventListener("click", function () { 14 var open = menu.classList.toggle("open"); 15 toggle.setAttribute("aria-expanded", open ? "true" : "false"); 16 }); 17 menu.querySelectorAll("a").forEach(function (a) { 18 a.addEventListener("click", function () { 19 menu.classList.remove("open"); 20 toggle.setAttribute("aria-expanded", "false"); 21 }); 22 }); 23 } 24 25 /* Sticky header shadow on scroll */ 26 var header = document.querySelector(".header"); 27 if (header) { 28 var onScroll = function () { header.classList.toggle("scrolled", window.scrollY > 8); }; 29 onScroll(); 30 window.addEventListener("scroll", onScroll, { passive: true }); 31 } 32 33 /* Reveal on scroll â Motion when available, instant reveal otherwise */ 34 var reveals = document.querySelectorAll(".reveal"); 35 var delayFor = function (el) { 36 if (el.classList.contains("d1")) return 0.08; 37 if (el.classList.contains("d2")) return 0.16; 38 if (el.classList.contains("d3")) return 0.24; 39 if (el.classList.contains("d4")) return 0.32; 40 return 0; 41 }; 42 if (reveals.length) { 43 if (canAnimate) { 44 reveals.forEach(function (el) { 45 M.inView(el, function () { 46 M.animate( 47 el, 48 { opacity: [0, 1], transform: ["translateY(24px)", "translateY(0px)"] }, 49 { duration: 0.7, delay: delayFor(el), ease: [0.22, 0.7, 0.3, 1] } 50 ); 51 }, { amount: 0.15, margin: "0px 0px -40px 0px" }); 52 }); 53 } else { 54 reveals.forEach(function (el) { el.classList.add("in"); }); // instant fallback 55 } 56 } 57 58 /* Count-up for elements with data-count */ 59 var counters = document.querySelectorAll("[data-count]"); 60 counters.forEach(function (el) { 61 var target = parseFloat(el.getAttribute("data-count")); 62 var start = el.getAttribute("data-count-start") ? parseFloat(el.getAttribute("data-count-start")) : 0; 63 var suffix = el.getAttribute("data-suffix") || ""; 64 var decimals = el.getAttribute("data-decimals") ? parseInt(el.getAttribute("data-decimals"), 10) : 0; 65 var format = el.getAttribute("data-format"); 66 var compact = function (v) { 67 if (v >= 1e6) { var m = v / 1e6; return (m % 1 === 0 ? m.toFixed(0) : m.toFixed(1)) + "M+"; } 68 return Math.round(v / 1e3) + "k"; 69 }; 70 var render = format === "compact" 71 ? function (v) { el.textContent = compact(v); } 72 : function (v) { el.textContent = v.toFixed(decimals) + suffix; }; 73 if (canAnimate) { 74 M.inView(el, function () { 75 M.animate(start, target, { 76 duration: 1.4, 77 ease: [0.22, 0.7, 0.3, 1], 78 onUpdate: render, 79 onComplete: function () { render(target); } 80 }); 81 }, { amount: 0.6 }); 82 } else { 83 render(target); // instant fallback 84 } 85 }); 86 87 /* Hero entrance â subtle spring lift on the live-tracker visual */ 88 if (canAnimate) { 89 var heroVisual = document.querySelector(".hero-visual .tracker-card"); 90 if (heroVisual) { 91 M.animate( 92 heroVisual, 93 { opacity: [0, 1], transform: ["translateY(28px) scale(0.98)", "translateY(0px) scale(1)"] }, 94 { duration: 0.9, delay: 0.15, type: "spring", stiffness: 90, damping: 16 } 95 ); 96 } 97 } 98 99 /* Contact form â submits to Web3Forms via AJAX, keeps inline success message */ 100 var form = document.querySelector("#contact-form"); 101 if (form) { 102 form.addEventListener("submit", function (ev) { 103 ev.preventDefault(); 104 if (form.checkValidity && !form.checkValidity()) { form.reportValidity(); return; } 105 106 var ok = document.querySelector("#form-success"); 107 var btn = form.querySelector('button[type="submit"]'); 108 var btnText = btn ? btn.textContent : ""; 109 if (btn) { btn.disabled = true; btn.textContent = "Sendingâ¦"; } 110 111 var payload = {}; 112 new FormData(form).forEach(function (value, key) { payload[key] = value; }); 113 114 fetch(form.getAttribute("action") || "https://api.web3forms.com/submit", { 115 method: "POST", 116 headers: { "Content-Type": "application/json", "Accept": "application/json" }, 117 body: JSON.stringify(payload) 118 }).then(function (res) { return res.json(); }).then(function (json) { 119 if (!json.success) throw new Error(json.message || "Submission failed"); 120 form.style.display = "none"; 121 if (ok) { ok.style.display = "block"; ok.scrollIntoView({ behavior: "smooth", block: "center" }); } 122 }).catch(function () { 123 if (btn) { btn.disabled = false; btn.textContent = btnText; } 124 alert("Sorry â your message couldn't be sent just now. Please email [email protected] and we'll get straight back to you."); 125 }); 126 }); 127 } 128 129 /* Footer year */ 130 var yr = document.querySelector("#year"); 131 if (yr) yr.textContent = new Date().getFullYear(); 132 133 /* Particle globe (hero orbiting visual) */ 134 document.querySelectorAll(".orbit-globe-canvas").forEach(function (canvas) { 135 var ctx = canvas.getContext("2d"); 136 if (!ctx) return; 137 138 var COUNT = 480; 139 var points = []; 140 var golden = Math.PI * (3 - Math.sqrt(5)); 141 for (var i = 0; i < COUNT; i++) { 142 var y = 1 - (i / (COUNT - 1)) * 2; 143 var r = Math.sqrt(1 - y * y); 144 var theta = golden * i; 145 points.push({ x: Math.cos(theta) * r, y: y, z: Math.sin(theta) * r }); 146 } 147 148 var width = 0, height = 0, colorStr = "rgb(3,126,176)"; 149 var readColor = function () { 150 var c = getComputedStyle(canvas).color; 151 if (c) colorStr = c; 152 }; 153 var resize = function () { 154 var rect = canvas.getBoundingClientRect(); 155 width = rect.width; height = rect.height; 156 var dpr = Math.min(window.devicePixelRatio || 1, 2); 157 canvas.width = Math.max(1, Math.floor(width * dpr)); 158 canvas.height = Math.max(1, Math.floor(height * dpr));
159 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 160 readColor(); 161 }; 162 resize(); 163 if ("ResizeObserver" in window) { new ResizeObserver(resize).observe(canvas); } 164 else { window.addEventListener("resize", resize, { passive: true }); } 165 166 var angle = 0; 167 var draw = function () { 168 var cx = width / 2, cy = height / 2; 169 var radius = (Math.min(width, height) / 2) * 0.94; 170 var cos = Math.cos(angle), sin = Math.sin(angle); 171 ctx.clearRect(0, 0, width, height); 172 ctx.fillStyle = colorStr; 173 for (var j = 0; j < points.length; j++) { 174 var p = points[j]; 175 var x = p.x * cos - p.z * sin; 176 var z = p.x * sin + p.z * cos; 177 var depth = (z + 1) / 2; 178 ctx.globalAlpha = 0.1 + depth * 0.7; 179 ctx.beginPath(); 180 ctx.arc(cx + x * radius, cy + p.y * radius, 0.6 + depth * 1.8, 0, Math.PI * 2); 181 ctx.fill(); 182 } 183 ctx.globalAlpha = 1; 184 angle += reduce ? 0.0012 : 0.005; 185 requestAnimationFrame(draw); 186 }; 187 requestAnimationFrame(draw); 188 }); 189})();
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.