`;
}
function renderFooter() {
const mount = document.getElementById("site-footer");
if (!mount) return;
mount.innerHTML = `
`;
}
function renderChromeExtras() {
if (document.getElementById("floatingCta")) return;
const frag = document.createElement("div");
frag.innerHTML = `
Let's TalkTalk→
`;
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 = `
We use optional analytics cookies (Google Analytics) to understand how the site is used. Your theme preference is stored on this device. Read the Privacy Policy.