678 lines
25 KiB
JavaScript
678 lines
25 KiB
JavaScript
/* Zettai Systems — main site scripts */
|
||
(function () {
|
||
"use strict";
|
||
|
||
const ROOT = typeof window.SITE_ROOT === "string" ? window.SITE_ROOT : "";
|
||
const PAGE_RAW = document.body?.dataset?.page || "";
|
||
const PAGE = PAGE_RAW === "service" ? "services" : PAGE_RAW;
|
||
|
||
const NAV_ITEMS = [
|
||
{ href: "", label: "Home", id: "home" },
|
||
{ href: "about", label: "About Us", id: "about" },
|
||
{ href: "services", label: "Services", id: "services" },
|
||
{ href: "solutions", label: "Solutions", id: "solutions" },
|
||
{ href: "industries", label: "Industries", id: "industries" },
|
||
{ href: "careers", label: "Careers", id: "careers" },
|
||
{ href: "contact", label: "Contact", id: "contact" },
|
||
];
|
||
|
||
function asset(path) {
|
||
const clean = String(path || "").replace(/^\//, "");
|
||
if (!ROOT) return "/" + clean;
|
||
return ROOT + clean;
|
||
}
|
||
|
||
function pageHref(path) {
|
||
if (!path) return "/";
|
||
const clean = String(path).replace(/^\//, "");
|
||
if (!ROOT) return "/" + clean;
|
||
return ROOT + clean;
|
||
}
|
||
|
||
function navLink(item) {
|
||
const active = PAGE === item.id ? " active" : "";
|
||
return `<li><a class="${active.trim()}" href="${pageHref(item.href)}" ${PAGE === item.id ? 'aria-current="page"' : ""}>${item.label}</a></li>`;
|
||
}
|
||
|
||
function renderHeader() {
|
||
const mount = document.getElementById("site-header");
|
||
if (!mount) return;
|
||
|
||
mount.innerHTML = `
|
||
<a class="skip-link" href="#main">Skip to content</a>
|
||
<div class="site-header" id="headerBar">
|
||
<div class="container-zs navbar-zs">
|
||
<a class="brand-logo" href="${pageHref("")}" aria-label="Zettai Systems home">
|
||
<img class="logo-light" src="${asset("logos/logo-light-transparent.png")}" alt="Zettai Systems" height="44" />
|
||
<img class="logo-dark" src="${asset("logos/logo-dark-transparent.png")}" alt="Zettai Systems" height="44" />
|
||
</a>
|
||
<ul class="nav-links" role="list">
|
||
${NAV_ITEMS.map(navLink).join("")}
|
||
</ul>
|
||
<div class="nav-actions">
|
||
<button type="button" class="theme-toggle" id="themeToggle" aria-label="Toggle color theme" title="Toggle theme">
|
||
<i class="bi bi-sun-fill icon-sun" aria-hidden="true"></i>
|
||
<i class="bi bi-moon-stars-fill icon-moon" aria-hidden="true"></i>
|
||
</button>
|
||
<a class="btn-zs btn-zs-primary btn-zs-sm nav-cta nav-cta-full" href="${pageHref("contact")}">Consult with an Architect <span class="arrow" aria-hidden="true">→</span></a>
|
||
<a class="btn-zs btn-zs-primary btn-zs-sm nav-cta-compact" href="${pageHref("contact")}">Consult <span class="arrow" aria-hidden="true">→</span></a>
|
||
<button type="button" class="menu-toggle" id="menuToggle" aria-label="Open menu" aria-expanded="false" aria-controls="mobileDrawer">
|
||
<i class="bi bi-list" style="font-size:1.4rem" aria-hidden="true"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<nav class="mobile-drawer" id="mobileDrawer" aria-label="Mobile" hidden>
|
||
<ul class="mobile-links">
|
||
${NAV_ITEMS.map((item) => {
|
||
const active = PAGE === item.id ? " active" : "";
|
||
return `<li><a class="${active.trim()}" href="${pageHref(item.href)}">${item.label}</a></li>`;
|
||
}).join("")}
|
||
</ul>
|
||
<a class="btn-zs btn-zs-primary w-100" href="${pageHref("contact")}">Consult with an Architect <span class="arrow">→</span></a>
|
||
</nav>
|
||
`;
|
||
}
|
||
|
||
function renderFooter() {
|
||
const mount = document.getElementById("site-footer");
|
||
if (!mount) return;
|
||
|
||
mount.innerHTML = `
|
||
<footer class="site-footer">
|
||
<div class="container-zs">
|
||
<div class="row g-4">
|
||
<div class="col-lg-4">
|
||
<div class="footer-brand">
|
||
<img src="${asset("logos/logo-dark-transparent.png")}" alt="Zettai Systems" height="44" />
|
||
<p class="footer-formerly">(Formerly Venba Information Technology)</p>
|
||
<p>Modern IT and digital solutions for businesses that need reliable software, scalable architecture, and long-term technology partnership.</p>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-4 col-lg-2 footer-col">
|
||
<h4>Company</h4>
|
||
<ul>
|
||
<li><a href="${pageHref("about")}">About Us</a></li>
|
||
<li><a href="${pageHref("careers")}">Careers</a></li>
|
||
<li><a href="${pageHref("contact")}">Contact Us</a></li>
|
||
</ul>
|
||
</div>
|
||
<div class="col-6 col-md-4 col-lg-3 footer-col">
|
||
<h4>Services</h4>
|
||
<ul>
|
||
<li><a href="${pageHref("services/agentic-ai")}">Agentic Workflows</a></li>
|
||
<li><a href="${pageHref("services/enterprise-rag")}">Domain RAG</a></li>
|
||
<li><a href="${pageHref("services/ai-microservices")}">AI Microservices</a></li>
|
||
<li><a href="${pageHref("services/intelligent-process-automation")}">Process Automation</a></li>
|
||
<li><a href="${pageHref("services/ai-governance")}">AI Governance</a></li>
|
||
<li><a href="${pageHref("services/web-development")}">Web Platforms</a></li>
|
||
<li><a href="${pageHref("services/mobile-development")}">Flutter Mobile</a></li>
|
||
<li><a href="${pageHref("services/cloud-integration")}">Cloud & MLOps</a></li>
|
||
<li><a href="${pageHref("services/custom-software")}">Custom Software</a></li>
|
||
<li><a href="${pageHref("services/data-analytics")}">Data Analytics</a></li>
|
||
<li><a href="${pageHref("services/it-consulting")}">IT Consulting</a></li>
|
||
<li><a href="${pageHref("services/cybersecurity")}">Cybersecurity</a></li>
|
||
<li><a href="${pageHref("services/erp")}">ERP</a></li>
|
||
</ul>
|
||
</div>
|
||
<div class="col-md-4 col-lg-3 footer-col">
|
||
<h4>Solutions</h4>
|
||
<ul>
|
||
<li><a href="${pageHref("solutions")}#digital-transformation">Digital Transformation</a></li>
|
||
<li><a href="${pageHref("solutions")}#business-automation">Business Automation</a></li>
|
||
<li><a href="${pageHref("solutions")}#enterprise">Enterprise Solutions</a></li>
|
||
<li><a href="${pageHref("services/ecommerce")}">E-Commerce</a></li>
|
||
</ul>
|
||
<h4 class="mt-4">Contact</h4>
|
||
<ul class="footer-contact">
|
||
<li>
|
||
<i class="bi bi-telephone-fill" aria-hidden="true"></i>
|
||
<a href="tel:+918072998024" aria-label="Call Zettai Systems at +91 807 299 8024">+91 807 299 8024</a>
|
||
</li>
|
||
<li>
|
||
<i class="bi bi-geo-alt-fill" aria-hidden="true"></i>
|
||
<span>No.10/21, 27th Street, Thillai Ganga Nagar, Nanganallur, Chennai – 600061, Tamil Nadu, India</span>
|
||
</li>
|
||
<li>
|
||
<i class="bi bi-envelope-fill" aria-hidden="true"></i>
|
||
<a href="mailto:solutions@zettaisystems.com">solutions@zettaisystems.com</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="footer-bottom">
|
||
<div>© ${new Date().getFullYear()} Zettai Systems Pvt Ltd. All Rights Reserved.</div>
|
||
<div class="footer-legal">
|
||
<a href="${pageHref("privacy-policy")}">Privacy Policy</a>
|
||
<a href="${pageHref("terms")}">Terms & Conditions</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
`;
|
||
}
|
||
|
||
function renderChromeExtras() {
|
||
if (document.getElementById("floatingCta")) return;
|
||
const frag = document.createElement("div");
|
||
frag.innerHTML = `
|
||
<a class="floating-cta" id="floatingCta" href="${pageHref("contact")}">
|
||
<i class="bi bi-chat-dots-fill" aria-hidden="true"></i>
|
||
<span class="label-full">Let's Talk</span>
|
||
<span class="d-sm-none">Talk</span>
|
||
<span class="arrow" aria-hidden="true">→</span>
|
||
</a>
|
||
<button type="button" class="back-to-top" id="backToTop" aria-label="Back to top">
|
||
<i class="bi bi-arrow-up" aria-hidden="true"></i>
|
||
</button>
|
||
`;
|
||
document.body.append(...frag.children);
|
||
}
|
||
|
||
/* Theme */
|
||
function getPreferredTheme() {
|
||
const saved = localStorage.getItem("zs-theme");
|
||
if (saved === "light" || saved === "dark") return saved;
|
||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||
}
|
||
|
||
function applyFavicon(theme) {
|
||
const link = document.querySelector('link[rel="icon"]');
|
||
if (!link) return;
|
||
const file = theme === "dark" ? "favicon-dark.png" : "favicon.png";
|
||
link.setAttribute("type", "image/png");
|
||
link.setAttribute("href", asset("assets/logos/" + file));
|
||
}
|
||
|
||
function applyTheme(theme) {
|
||
document.documentElement.setAttribute("data-theme", theme);
|
||
localStorage.setItem("zs-theme", theme);
|
||
applyFavicon(theme);
|
||
const toggle = document.getElementById("themeToggle");
|
||
if (toggle) {
|
||
toggle.setAttribute("aria-label", theme === "dark" ? "Switch to light theme" : "Switch to dark theme");
|
||
}
|
||
}
|
||
|
||
function initTheme() {
|
||
applyTheme(getPreferredTheme());
|
||
document.getElementById("themeToggle")?.addEventListener("click", () => {
|
||
const next = document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark";
|
||
applyTheme(next);
|
||
});
|
||
}
|
||
|
||
/* Mobile nav */
|
||
function initMobileNav() {
|
||
const toggle = document.getElementById("menuToggle");
|
||
const drawer = document.getElementById("mobileDrawer");
|
||
if (!toggle || !drawer) return;
|
||
|
||
const focusableSelector = "a[href], button:not([disabled])";
|
||
|
||
const close = () => {
|
||
const wasOpen = drawer.classList.contains("is-open");
|
||
drawer.classList.remove("is-open");
|
||
drawer.hidden = true;
|
||
toggle.setAttribute("aria-expanded", "false");
|
||
toggle.setAttribute("aria-label", "Open menu");
|
||
document.body.classList.remove("nav-open");
|
||
const icon = toggle.querySelector("i");
|
||
if (icon) icon.className = "bi bi-list";
|
||
icon && (icon.style.fontSize = "1.4rem");
|
||
if (wasOpen) toggle.focus();
|
||
};
|
||
|
||
const open = () => {
|
||
drawer.hidden = false;
|
||
requestAnimationFrame(() => drawer.classList.add("is-open"));
|
||
toggle.setAttribute("aria-expanded", "true");
|
||
toggle.setAttribute("aria-label", "Close menu");
|
||
document.body.classList.add("nav-open");
|
||
const icon = toggle.querySelector("i");
|
||
if (icon) icon.className = "bi bi-x-lg";
|
||
icon && (icon.style.fontSize = "1.25rem");
|
||
const first = drawer.querySelector(focusableSelector);
|
||
first?.focus();
|
||
};
|
||
|
||
const focusables = () => {
|
||
const items = [toggle, ...drawer.querySelectorAll(focusableSelector)];
|
||
return items.filter((el) => el && !el.hasAttribute("hidden"));
|
||
};
|
||
|
||
toggle.addEventListener("click", () => {
|
||
if (drawer.classList.contains("is-open")) close();
|
||
else open();
|
||
});
|
||
|
||
drawer.querySelectorAll("a").forEach((a) => a.addEventListener("click", close));
|
||
window.addEventListener("resize", () => {
|
||
if (window.innerWidth >= 992) close();
|
||
});
|
||
|
||
document.addEventListener("keydown", (e) => {
|
||
if (!drawer.classList.contains("is-open")) return;
|
||
if (e.key === "Escape") {
|
||
e.preventDefault();
|
||
close();
|
||
return;
|
||
}
|
||
if (e.key !== "Tab") return;
|
||
const items = focusables();
|
||
if (!items.length) return;
|
||
const first = items[0];
|
||
const last = items[items.length - 1];
|
||
if (e.shiftKey && document.activeElement === first) {
|
||
e.preventDefault();
|
||
last.focus();
|
||
} else if (!e.shiftKey && document.activeElement === last) {
|
||
e.preventDefault();
|
||
first.focus();
|
||
}
|
||
});
|
||
}
|
||
|
||
/* Scroll effects */
|
||
function initScrollUI() {
|
||
const header = document.getElementById("headerBar");
|
||
const back = document.getElementById("backToTop");
|
||
|
||
const onScroll = () => {
|
||
const y = window.scrollY || 0;
|
||
header?.classList.toggle("is-scrolled", y > 12);
|
||
back?.classList.toggle("is-visible", y > 480);
|
||
};
|
||
|
||
onScroll();
|
||
window.addEventListener("scroll", onScroll, { passive: true });
|
||
back?.addEventListener("click", () => window.scrollTo({ top: 0, behavior: "smooth" }));
|
||
}
|
||
|
||
/* Forms */
|
||
function validateEmail(value) {
|
||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||
}
|
||
|
||
function validatePhone(value) {
|
||
const digits = value.replace(/\D/g, "");
|
||
return digits.length >= 10;
|
||
}
|
||
|
||
function formEndpoint(kind) {
|
||
if (kind === "careers") return "/api/careers.php";
|
||
return "/api/contact.php";
|
||
}
|
||
|
||
function successMessage(kind) {
|
||
if (kind === "careers") {
|
||
return "Thank you. Your application has been sent. Our team will review it and follow up if there is a fit.";
|
||
}
|
||
return "Thank you. Your message has been sent. We will get back to you soon.";
|
||
}
|
||
|
||
function resetSubmitButton(submitBtn) {
|
||
submitBtn?.classList.remove("is-loading");
|
||
if (submitBtn) {
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerHTML = submitBtn.dataset.label || "Send";
|
||
}
|
||
}
|
||
|
||
/** Hosting bot-gate (humans_XXXX cookie) returns 409 HTML that fetch cannot execute. */
|
||
function applyHostBotCookie(html) {
|
||
if (!html || typeof html !== "string") return false;
|
||
const match = html.match(/document\.cookie\s*=\s*["']([^"';]+)["']/i);
|
||
if (!match) return false;
|
||
let cookie = match[1].trim();
|
||
if (!/;\s*path=/i.test(cookie)) cookie += "; path=/";
|
||
document.cookie = cookie;
|
||
return true;
|
||
}
|
||
|
||
async function parseFormResponse(response) {
|
||
const raw = await response.text();
|
||
let data = null;
|
||
try {
|
||
data = JSON.parse(raw);
|
||
} catch (_) {
|
||
data = null;
|
||
}
|
||
return { raw, data };
|
||
}
|
||
|
||
async function postForm(endpoint, formData, retried) {
|
||
const response = await fetch(endpoint, {
|
||
method: "POST",
|
||
body: formData,
|
||
headers: { Accept: "application/json" },
|
||
credentials: "same-origin",
|
||
});
|
||
|
||
const { raw, data } = await parseFormResponse(response);
|
||
|
||
// Bot challenge: set cookie and retry once
|
||
if (response.status === 409 && !retried && applyHostBotCookie(raw)) {
|
||
return postForm(endpoint, formData, true);
|
||
}
|
||
|
||
return { response, data, raw };
|
||
}
|
||
|
||
function friendlyFormError(response, data, raw) {
|
||
if (data?.error) return data.error;
|
||
if (response.status === 403) {
|
||
return "Security check failed. Please refresh the page and try again.";
|
||
}
|
||
if (response.status === 429) {
|
||
return "Too many submissions. Please wait a few minutes and try again.";
|
||
}
|
||
if (response.status === 409 || (raw && /humans_\d+/i.test(raw))) {
|
||
return "Security check blocked this send. Please refresh the page and try again, or email solutions@zettaisystems.com.";
|
||
}
|
||
if (response.status >= 500) {
|
||
return "Server error while sending. Please try again later or email us directly.";
|
||
}
|
||
if (response.status === 404) {
|
||
return "Mail service is not available on the server yet. Please email us directly.";
|
||
}
|
||
return "Something went wrong while sending. Please try again or email us directly.";
|
||
}
|
||
|
||
function csrfToken() {
|
||
const match = document.cookie.match(/(?:^|; )zs_csrf=([^;]*)/);
|
||
if (match && match[1]) return decodeURIComponent(match[1]);
|
||
const bytes = new Uint8Array(16);
|
||
crypto.getRandomValues(bytes);
|
||
const token = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
||
document.cookie = "zs_csrf=" + token + "; path=/; SameSite=Lax";
|
||
return token;
|
||
}
|
||
|
||
function initForms() {
|
||
csrfToken();
|
||
document.querySelectorAll("[data-form]").forEach((form) => {
|
||
form.addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
let valid = true;
|
||
|
||
form.querySelectorAll("[data-required]").forEach((field) => {
|
||
const wrap = field.closest(".form-field") || field.parentElement;
|
||
const value = (field.value || "").trim();
|
||
let ok = value.length > 0;
|
||
if (field.type === "email") ok = validateEmail(value);
|
||
if (field.dataset.validate === "phone" && value) ok = validatePhone(value);
|
||
if (field.type === "checkbox") ok = field.checked;
|
||
if (field.type === "file") ok = field.files && field.files.length > 0;
|
||
|
||
wrap.classList.toggle("field-invalid", !ok);
|
||
if (!ok) valid = false;
|
||
});
|
||
|
||
const status = form.querySelector(".form-status");
|
||
const submitBtn = form.querySelector('[type="submit"]');
|
||
const kind = form.getAttribute("data-form") || "contact";
|
||
|
||
if (!valid) {
|
||
if (status) {
|
||
status.className = "form-status is-error";
|
||
status.textContent = "Please check the highlighted fields and try again.";
|
||
}
|
||
return;
|
||
}
|
||
|
||
submitBtn?.classList.add("is-loading");
|
||
if (submitBtn) {
|
||
submitBtn.disabled = true;
|
||
submitBtn.dataset.label = submitBtn.innerHTML;
|
||
submitBtn.innerHTML = "Sending…";
|
||
}
|
||
if (status) {
|
||
status.className = "form-status";
|
||
status.textContent = "";
|
||
}
|
||
|
||
try {
|
||
const formData = new FormData(form);
|
||
formData.set("csrf_token", csrfToken());
|
||
const { response, data, raw } = await postForm(
|
||
formEndpoint(kind),
|
||
formData,
|
||
false
|
||
);
|
||
|
||
resetSubmitButton(submitBtn);
|
||
|
||
if (!response.ok || !data?.ok) {
|
||
if (status) {
|
||
status.className = "form-status is-error";
|
||
status.textContent = friendlyFormError(response, data, raw);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (status) {
|
||
status.className = "form-status is-success";
|
||
status.textContent = data.message || successMessage(kind);
|
||
}
|
||
if (typeof window.gtag === "function") {
|
||
window.gtag("event", "generate_lead", {
|
||
event_category: "form",
|
||
event_label: kind,
|
||
});
|
||
}
|
||
form.reset();
|
||
form.querySelectorAll(".field-invalid").forEach((el) => el.classList.remove("field-invalid"));
|
||
} catch (_) {
|
||
resetSubmitButton(submitBtn);
|
||
if (status) {
|
||
status.className = "form-status is-error";
|
||
status.textContent =
|
||
"Network error. Please check your connection or email us directly.";
|
||
}
|
||
}
|
||
});
|
||
|
||
form.querySelectorAll("input, textarea, select").forEach((field) => {
|
||
field.addEventListener("input", () => {
|
||
const wrap = field.closest(".form-field") || field.parentElement;
|
||
wrap.classList.remove("field-invalid");
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/* AOS */
|
||
function initAOS() {
|
||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||
if (typeof AOS !== "undefined") {
|
||
const isMobile = window.matchMedia("(max-width: 991.98px)").matches;
|
||
if (isMobile) {
|
||
document.querySelectorAll('[data-aos="fade-left"], [data-aos="fade-right"]').forEach((el) => {
|
||
el.setAttribute("data-aos", "fade-up");
|
||
});
|
||
}
|
||
AOS.init({
|
||
duration: 650,
|
||
easing: "ease-out-cubic",
|
||
once: true,
|
||
offset: 60,
|
||
disable: false,
|
||
});
|
||
}
|
||
}
|
||
|
||
/* Year helpers already in footer */
|
||
|
||
function initPageNetworks() {
|
||
const canvases = document.querySelectorAll(".page-net-canvas");
|
||
if (!canvases.length) return;
|
||
|
||
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||
const BLUE = "#4ba3dc";
|
||
const ORANGE = "#f88820";
|
||
|
||
canvases.forEach((canvas) => {
|
||
const section = canvas.closest(".page-hero");
|
||
const ctx = canvas.getContext("2d");
|
||
if (!ctx || !section) return;
|
||
|
||
let nodes = [];
|
||
let raf = 0;
|
||
let width = 0;
|
||
let height = 0;
|
||
|
||
function resize() {
|
||
const rect = section.getBoundingClientRect();
|
||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||
width = Math.max(1, Math.floor(rect.width));
|
||
height = Math.max(1, Math.floor(rect.height));
|
||
canvas.width = Math.floor(width * dpr);
|
||
canvas.height = Math.floor(height * dpr);
|
||
canvas.style.width = `${width}px`;
|
||
canvas.style.height = `${height}px`;
|
||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||
seedNodes();
|
||
}
|
||
|
||
function seedNodes() {
|
||
const count = Math.max(28, Math.min(56, Math.floor((width * height) / 18000)));
|
||
nodes = Array.from({ length: count }, (_, i) => {
|
||
const accent = i % 9 === 0;
|
||
return {
|
||
x: Math.random() * width,
|
||
y: Math.random() * height,
|
||
vx: (Math.random() - 0.5) * (reduceMotion ? 0 : 0.35),
|
||
vy: (Math.random() - 0.5) * (reduceMotion ? 0 : 0.35),
|
||
r: accent ? 2.8 : 1.6 + Math.random() * 1.4,
|
||
color: accent ? ORANGE : BLUE,
|
||
};
|
||
});
|
||
}
|
||
|
||
function draw() {
|
||
ctx.clearRect(0, 0, width, height);
|
||
const linkDist = Math.min(160, Math.max(110, width * 0.12));
|
||
|
||
for (let i = 0; i < nodes.length; i++) {
|
||
const a = nodes[i];
|
||
if (!reduceMotion) {
|
||
a.x += a.vx;
|
||
a.y += a.vy;
|
||
if (a.x < 0 || a.x > width) a.vx *= -1;
|
||
if (a.y < 0 || a.y > height) a.vy *= -1;
|
||
a.x = Math.max(0, Math.min(width, a.x));
|
||
a.y = Math.max(0, Math.min(height, a.y));
|
||
}
|
||
|
||
for (let j = i + 1; j < nodes.length; j++) {
|
||
const b = nodes[j];
|
||
const dx = a.x - b.x;
|
||
const dy = a.y - b.y;
|
||
const dist = Math.hypot(dx, dy);
|
||
if (dist < linkDist) {
|
||
const alpha = (1 - dist / linkDist) * 0.45;
|
||
ctx.beginPath();
|
||
ctx.moveTo(a.x, a.y);
|
||
ctx.lineTo(b.x, b.y);
|
||
ctx.strokeStyle = `rgba(160, 200, 230, ${alpha})`;
|
||
ctx.lineWidth = 1;
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
ctx.beginPath();
|
||
ctx.arc(a.x, a.y, a.r, 0, Math.PI * 2);
|
||
ctx.fillStyle = a.color;
|
||
ctx.shadowColor = a.color;
|
||
ctx.shadowBlur = a.color === ORANGE ? 10 : 6;
|
||
ctx.fill();
|
||
ctx.shadowBlur = 0;
|
||
}
|
||
|
||
if (!reduceMotion) raf = requestAnimationFrame(draw);
|
||
}
|
||
|
||
resize();
|
||
draw();
|
||
window.addEventListener("resize", () => {
|
||
cancelAnimationFrame(raf);
|
||
resize();
|
||
draw();
|
||
});
|
||
});
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
renderHeader();
|
||
renderFooter();
|
||
renderChromeExtras();
|
||
initTheme();
|
||
initMobileNav();
|
||
initScrollUI();
|
||
initForms();
|
||
initAOS();
|
||
initPageNetworks();
|
||
initCareerApply();
|
||
initCookieBanner();
|
||
});
|
||
|
||
function initCookieBanner() {
|
||
if (document.getElementById("cookieBanner")) return;
|
||
let choice = "";
|
||
try {
|
||
choice = localStorage.getItem("zs-analytics") || "";
|
||
} catch (_) {
|
||
choice = "";
|
||
}
|
||
if (choice === "granted" || choice === "denied") return;
|
||
|
||
const banner = document.createElement("div");
|
||
banner.className = "cookie-banner";
|
||
banner.id = "cookieBanner";
|
||
banner.setAttribute("role", "dialog");
|
||
banner.setAttribute("aria-label", "Cookie consent");
|
||
banner.innerHTML = `
|
||
<p>We use optional analytics cookies (Google Analytics) to understand how the site is used. Your theme preference is stored on this device. Read the <a href="${pageHref("privacy-policy")}">Privacy Policy</a>.</p>
|
||
<div class="cookie-banner-actions">
|
||
<button type="button" class="btn-zs btn-zs-outline btn-zs-sm" id="cookieDecline">Decline</button>
|
||
<button type="button" class="btn-zs btn-zs-primary btn-zs-sm" id="cookieAccept">Accept</button>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(banner);
|
||
document.body.classList.add("has-cookie-banner");
|
||
|
||
const setChoice = (value) => {
|
||
try {
|
||
localStorage.setItem("zs-analytics", value);
|
||
} catch (_) {}
|
||
banner.remove();
|
||
document.body.classList.remove("has-cookie-banner");
|
||
if (value === "granted" && typeof window.zsLoadAnalytics === "function") {
|
||
window.zsLoadAnalytics();
|
||
}
|
||
};
|
||
|
||
document.getElementById("cookieAccept")?.addEventListener("click", () => setChoice("granted"));
|
||
document.getElementById("cookieDecline")?.addEventListener("click", () => setChoice("denied"));
|
||
}
|
||
|
||
function initCareerApply() {
|
||
const select = document.getElementById("c-position");
|
||
if (!select) return;
|
||
|
||
document.querySelectorAll(".job-card a[href='#apply']").forEach((link) => {
|
||
link.addEventListener("click", () => {
|
||
const title = link.closest(".job-card")?.querySelector("h3")?.textContent.trim();
|
||
if (!title) return;
|
||
const match = [...select.options].some((opt) => opt.value === title);
|
||
if (match) {
|
||
select.value = title;
|
||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||
select.focus({ preventScroll: true });
|
||
}
|
||
});
|
||
});
|
||
}
|
||
})();
|