/* ============================================================================
ANAX Capital — Motion Toolkit (no external libs, vanilla)
Patterns: smooth-scroll, scroll-reveal, magnetic CTA, counters, tilt,
sticky-shrinking nav, hero cursor-gradient, parallax,
page transitions, marquees (CSS only — JS just pauses).
Respects prefers-reduced-motion.
============================================================================ */
/* ----------------------------------------------------------------------------
UTM / click-id attribution capture — runs site-wide on every page load.
Persists last-touch UTM + the landing full_link for 90 days (localStorage)
so attribution SURVIVES internal navigation (which drops the query string).
form.js reads "anax_attribution_v1" at registration and forwards it to CRM.
---------------------------------------------------------------------------- */
(function () {
'use strict';
try {
var KEY = 'anax_attribution_v1';
var TTL = 90 * 24 * 60 * 60 * 1000; // 90 days
var qs = new URLSearchParams(location.search);
var utm = {};
qs.forEach(function (v, k) { if (k.indexOf('utm_') === 0 && v) utm[k] = v; });
var hasUtm = false; for (var _k in utm) { if (utm.hasOwnProperty(_k)) { hasUtm = true; break; } }
var hasClickId = qs.has('gclid') || qs.has('fbclid') || qs.has('bi_source_id') ||
qs.has('msclkid') || qs.has('ttclid');
var prev = null;
try { prev = JSON.parse(localStorage.getItem(KEY) || 'null'); } catch (e) {}
if (prev && prev.ts && (Date.now() - prev.ts) > TTL) { // expired (>90d)
prev = null;
try { localStorage.removeItem(KEY); } catch (e) {} // purge so stale UTM is never re-sent
}
// Last-touch: (re)write ONLY for a genuinely NEW campaign or a fresh click-id.
// Propagated internal links carry the SAME utm_* (no click-id) — detected as
// identical, so the original landing ts + full_link (with gclid) are preserved;
// plain navigation (no utm at all) never touches the stored value.
if (hasUtm || hasClickId) {
var sameUtm = false;
if (prev && prev.utm) {
var pk = Object.keys(prev.utm), nk = Object.keys(utm);
sameUtm = pk.length === nk.length && nk.every(function (k) { return prev.utm[k] === utm[k]; });
}
if (!prev || !sameUtm || hasClickId) {
localStorage.setItem(KEY, JSON.stringify({
utm: utm, // utm_* keys only (CRM accepts utm_* + full_link)
full_link: location.href, // landing URL — carries gclid / bi_source_id for per-ad attribution
ts: Date.now()
}));
}
}
} catch (e) { /* storage blocked (private mode) — fail silent; form falls back to URL */ }
})();
/* ----------------------------------------------------------------------------
UTM URL PROPAGATION — keep utm_* VISIBLE in the address bar on every page.
(1) self-heals the current URL (history.replaceState) when utm is missing but
was captured earlier, and (2) appends utm_* to every same-origin internal
link so a click carries it forward. Result: the UTM the visitor arrived
with stays in the URL across the whole site. utm_* only (clean URLs);
gclid/bi_source_id stay in localStorage full_link for CRM attribution.
---------------------------------------------------------------------------- */
(function () {
'use strict';
try {
var TTL = 90 * 24 * 60 * 60 * 1000;
var here = new URL(location.href);
var active = {};
// utm_* already in the URL win
here.searchParams.forEach(function (v, k) { if (k.indexOf('utm_') === 0 && v) active[k] = v; });
var hasActive = false; for (var a in active) { if (active.hasOwnProperty(a)) { hasActive = true; break; } }
// else fall back to the captured (non-expired) attribution
if (!hasActive) {
try {
var s = JSON.parse(localStorage.getItem('anax_attribution_v1') || 'null');
if (s && s.ts && (Date.now() - s.ts) <= TTL && s.utm) {
for (var k1 in s.utm) { if (s.utm.hasOwnProperty(k1)) active[k1] = s.utm[k1]; }
}
} catch (e) {}
}
var keys = []; for (var k2 in active) { if (active.hasOwnProperty(k2)) keys.push(k2); }
if (!keys.length) return; // visitor has no UTM — nothing to propagate
// (1) self-heal the address bar (no reload, replaces the current history entry)
var changed = false;
keys.forEach(function (k) { if (!here.searchParams.has(k)) { here.searchParams.set(k, active[k]); changed = true; } });
if (changed && window.history && history.replaceState) {
try { history.replaceState(history.state, '', here.toString()); } catch (e) {}
}
// (2) decorate same-origin internal links so navigation keeps the UTM
var decorate = function (el) {
try {
var href = el.getAttribute('href');
if (!href || href.charAt(0) === '#') return;
if (/^(mailto:|tel:|sms:|javascript:|whatsapp:|data:)/i.test(href)) return;
var u = new URL(href, location.href);
if (u.origin !== location.origin) return; // external link — leave untouched
var touched = false;
keys.forEach(function (k) { if (!u.searchParams.has(k)) { u.searchParams.set(k, active[k]); touched = true; } });
if (touched) el.setAttribute('href', u.toString());
} catch (e) {}
};
var run = function () { var as = document.getElementsByTagName('a'); for (var i = 0; i < as.length; i++) decorate(as[i]); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run); else run();
// fallback for links injected later (menus / dynamic): decorate on the way to navigation
document.addEventListener('click', function (ev) {
var t = ev.target;
var link = t && t.closest ? t.closest('a[href]') : null;
if (link) decorate(link);
}, true);
} catch (e) {}
})();
(() => {
'use strict';
const RM = matchMedia('(prefers-reduced-motion: reduce)').matches;
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
/* ----- Reveal-on-scroll opt-in via html.js-anim ----- */
// Above-the-fold items get .in BEFORE js-anim flips, so CSS gate never hides them.
const ABOVE = () => {
const vh = window.innerHeight || 800;
document.querySelectorAll('[data-reveal]:not(.in), [data-reveal-chunk]:not(.in)').forEach(el => {
const r = el.getBoundingClientRect();
if (r.top < vh * 1.05) el.classList.add('in');
});
};
ABOVE();
document.documentElement.classList.add('js-anim');
// Final safety: reveal anything still hidden after 2.5s
setTimeout(() => {
document.querySelectorAll('[data-reveal]:not(.in), [data-reveal-chunk]:not(.in)').forEach(el => el.classList.add('in'));
}, 2500);
/* ----- Smooth scroll for anchor links ----- */
document.addEventListener('click', (e) => {
const a = e.target.closest('a[href^="#"]');
if (!a) return;
const id = a.getAttribute('href');
if (!id || id === '#') return;
const t = document.querySelector(id);
if (!t) return;
e.preventDefault();
t.scrollIntoView({ behavior: RM ? 'auto' : 'smooth', block: 'start' });
});
/* ----- Scroll reveal ----- */
if (!RM) {
const io = new IntersectionObserver((entries) => {
entries.forEach(en => {
if (en.isIntersecting) {
en.target.classList.add('in');
io.unobserve(en.target);
}
});
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
$$('[data-reveal]').forEach(el => io.observe(el));
} else {
$$('[data-reveal]').forEach(el => el.classList.add('in'));
}
/* ----- Number counters ----- */
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
const animateCounter = (el) => {
const target = parseFloat(el.dataset.count);
const suffix = el.dataset.suffix || '';
const prefix = el.dataset.prefix || '';
const dec = parseInt(el.dataset.decimals || '0', 10);
const dur = 1400;
const t0 = performance.now();
const tick = (now) => {
const p = Math.min(1, (now - t0) / dur);
const v = target * easeOut(p);
el.textContent = prefix + v.toFixed(dec) + suffix;
if (p < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
};
const ioCount = new IntersectionObserver((entries) => {
entries.forEach(en => {
if (en.isIntersecting) {
if (!RM) animateCounter(en.target);
else {
const t = en.target;
t.textContent = (t.dataset.prefix||'') + t.dataset.count + (t.dataset.suffix||'');
}
ioCount.unobserve(en.target);
}
});
}, { threshold: 0.5 });
$$('[data-count]').forEach(el => ioCount.observe(el));
/* ----- Magnetic CTAs (subtle pull toward cursor) ----- */
if (!RM && matchMedia('(hover: hover)').matches) {
$$('.magnetic').forEach(btn => {
btn.addEventListener('mousemove', (e) => {
const r = btn.getBoundingClientRect();
const x = e.clientX - r.left - r.width / 2;
const y = e.clientY - r.top - r.height / 2;
btn.style.transform = `translate(${x * 0.18}px, ${y * 0.22}px)`;
});
btn.addEventListener('mouseleave', () => btn.style.transform = '');
});
}
/* ----- 3D tilt on .tilt cards ----- */
if (!RM && matchMedia('(hover: hover)').matches) {
$$('.tilt').forEach(card => {
const inner = card.querySelector('.tilt-inner') || card;
card.addEventListener('mousemove', (e) => {
const r = card.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width;
const y = (e.clientY - r.top) / r.height;
const rx = (0.5 - y) * 8;
const ry = (x - 0.5) * 10;
inner.style.transform = `rotateX(${rx}deg) rotateY(${ry}deg)`;
});
card.addEventListener('mouseleave', () => inner.style.transform = '');
});
}
/* ----- Hero cursor-follow gradient + arrow parallax ----- */
const hero = $('.hero');
if (hero && !RM) {
hero.addEventListener('mousemove', (e) => {
const r = hero.getBoundingClientRect();
const mx = ((e.clientX - r.left) / r.width) * 100;
const my = ((e.clientY - r.top) / r.height) * 100;
hero.style.setProperty('--mx', `${mx}%`);
hero.style.setProperty('--my', `${my}%`);
});
}
if (!RM) {
const arrow = $('.hero-art .arrow');
if (arrow) {
let raf = 0;
window.addEventListener('scroll', () => {
if (raf) return;
raf = requestAnimationFrame(() => {
const y = Math.min(120, window.scrollY * 0.12);
arrow.style.transform = `translateY(${y}px) rotate(${y * 0.08}deg)`;
raf = 0;
});
}, { passive: true });
}
}
/* ----- Sticky shrinking nav · ALWAYS visible -----
Header stays sticky at top, never hidden. Toggles .scrolled + .shrink only.
rAF-throttled + hysteresis to prevent flicker. */
const bar = $('.topbar');
if (bar) {
let ticking = false;
let scrolledOn = false;
let shrinkOn = false;
// Ensure header is always visible — remove any hide class
bar.classList.remove('hidden-top');
const update = () => {
const y = window.scrollY;
// Scrolled band: add at >12px, remove at <4px
if (!scrolledOn && y > 12) { bar.classList.add('scrolled'); scrolledOn = true; }
else if (scrolledOn && y < 4) { bar.classList.remove('scrolled'); scrolledOn = false; }
// Shrink band: add at >100px, remove at <70px
if (!shrinkOn && y > 100) { bar.classList.add('shrink'); shrinkOn = true; }
else if (shrinkOn && y < 70) { bar.classList.remove('shrink'); shrinkOn = false; }
ticking = false;
};
const onScroll = () => {
if (!ticking) { requestAnimationFrame(update); ticking = true; }
};
update();
window.addEventListener('scroll', onScroll, { passive: true });
}
/* ----- Burger + mobile nav drawer (v3 · 2026-05-18) -----
Drawer structure (after JS):
ul#primary-nav.nav (fixed container, .open class toggles slide-from-right)
├─ div.mnav-head (injected: logo + close)
├─ li ... (existing nav items — stay as direct children)
└─ div.mnav-foot (injected: Open Account + Login + Call CTA)
Sticky head (top) + sticky foot (bottom) + scroll on navUl itself.
*/
(function setupMobileNav() {
const burger = document.getElementById('nav-burger') || document.querySelector('.burger');
const navUl = document.getElementById('primary-nav');
if (!burger || !navUl) return;
// Idempotency guard — bail if drawer head already injected (prevents double close X)
if (navUl.querySelector(':scope > .mnav-head')) return;
// Resolve logo path — keep parity with topbar logo so relative paths work from any page
const topbarLogo = document.querySelector('.topbar .brand img');
const logoSrc = topbarLogo ? topbarLogo.getAttribute('src') : 'assets/logo/anax-logo-light.png';
// Defensive: remove any stale drawer fragments (e.g. from double-loaded script)
navUl.querySelectorAll(':scope > .mnav-head, :scope > .mnav-foot').forEach(n => n.remove());
// Build drawer header
const head = document.createElement('div');
head.className = 'mnav-head';
head.innerHTML = `
`;
// Keep
children as direct descendants of navUl so existing desktop CSS keeps working.
// Drawer uses sticky head/foot + middle scroll via overflow on navUl itself on mobile.
// Build footer with cloned CTAs
const foot = document.createElement('div');
foot.className = 'mnav-foot';
const loginHref = document.querySelector('.topbar .nav-cta a[href*="portal.anaxcapital"]')?.getAttribute('href') || 'https://portal.anaxcapital.ae';
const registerHref = document.querySelector('.topbar .nav-cta a[href*="register"]')?.getAttribute('href') || 'register.html';
foot.innerHTML = `
Open Live Account
Login to Portal
Call 800 MYANAX
`;
// Assemble: head pinned top, li's flow in middle, foot pinned bottom (via CSS sticky on mobile)
navUl.insertBefore(head, navUl.firstChild);
navUl.appendChild(foot);
// Pure class toggle — CSS display:none/flex handles visibility in mobile-menu.css
const closeMenu = () => {
navUl.classList.remove('open');
document.body.classList.remove('menu-open');
burger.setAttribute('aria-expanded', 'false');
navUl.querySelectorAll('li.open').forEach(li => {
li.classList.remove('open');
li.querySelector('button[aria-haspopup]')?.setAttribute('aria-expanded', 'false');
});
};
const openMenu = () => {
navUl.classList.add('open');
document.body.classList.add('menu-open');
burger.setAttribute('aria-expanded', 'true');
};
// Track last-open timestamp to ignore same-tick close events from doc listener
let lastOpenAt = 0;
burger.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
if (navUl.classList.contains('open')) {
closeMenu();
} else {
openMenu();
lastOpenAt = Date.now();
}
}, true); // capture phase so it fires before document listener
head.querySelector('.mnav-close').addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
closeMenu();
});
// Accordion submenu toggle — mobile drawer + desktop click toggle
navUl.querySelectorAll('li > button[aria-haspopup]').forEach(button => {
button.setAttribute('aria-expanded', 'false');
button.addEventListener('click', e => {
e.preventDefault();
const li = button.parentElement;
// Close any other open submenu
navUl.querySelectorAll('li.open').forEach(openLi => {
if (openLi !== li) {
openLi.classList.remove('open');
openLi.querySelector('button[aria-haspopup]')?.setAttribute('aria-expanded', 'false');
}
});
const isOpen = li.classList.toggle('open');
button.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
});
// Click outside closes open mega on desktop (mobile drawer has own close logic)
document.addEventListener('click', e => {
if (!matchMedia('(min-width: 1101px)').matches) return;
if (e.target.closest('.topbar .nav > li')) return;
navUl.querySelectorAll('li.open').forEach(openLi => {
openLi.classList.remove('open');
openLi.querySelector('button[aria-haspopup]')?.setAttribute('aria-expanded', 'false');
});
});
// Close on link click inside the drawer (any link, except the head brand link should still navigate then close)
navUl.addEventListener('click', e => {
const link = e.target.closest('a');
if (link) closeMenu();
});
// Click outside drawer closes it — but ignore clicks within 350ms of open
// (prevents same-tick close from burger tap bubbling on touch devices)
document.addEventListener('click', e => {
if (!document.body.classList.contains('menu-open')) return;
if (Date.now() - lastOpenAt < 350) return;
if (navUl.contains(e.target) || burger.contains(e.target)) return;
closeMenu();
});
// Escape key closes
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && document.body.classList.contains('menu-open')) closeMenu();
});
// Close on resize past breakpoint
window.addEventListener('resize', () => {
if (matchMedia('(min-width: 1101px)').matches && document.body.classList.contains('menu-open')) closeMenu();
});
})();
/* ----- FAQ ----- */
$$('.faq-item').forEach(it => {
const q = it.querySelector('.faq-q');
q.addEventListener('click', () => {
const open = it.classList.toggle('open');
q.setAttribute('aria-expanded', open ? 'true' : 'false');
});
});
/* ----- Cookie banner ----- */
const cookie = $('#cookie-banner');
if (cookie) {
const KEY = 'anax_cookie_v1';
if (!localStorage.getItem(KEY)) setTimeout(() => {
cookie.classList.add('show');
document.body.classList.add('cookie-visible');
}, 800);
cookie.querySelectorAll('[data-cookie]').forEach(b => {
b.addEventListener('click', () => {
localStorage.setItem(KEY, b.dataset.cookie);
cookie.classList.remove('show');
document.body.classList.remove('cookie-visible');
});
});
}
/* ----- Risk bar — DISABLED site-wide (Anax preference).
Completely remove the element from DOM so neither the strip nor its
dismiss X are rendered. Also kills the "2 close buttons on mobile menu"
issue (risk-bar's ✕ used to overlap with the drawer's close X). */
const risk = $('#risk-bar');
if (risk) risk.remove();
/* ----- TradingView main-product cards — auto-hide any card whose widget
fails to load (invalid symbol, blocked, or empty). Keeps only cards
actually showing live prices, per Anax preference. Used on every
/markets/* page (forex, metals, indices, energies, equity, uae-stocks,
all-instruments). Runs once after a generous load delay, then re-checks. */
(function tvCardCleanup(){
const tvCards = document.querySelectorAll('.tv-main-card');
if (!tvCards.length) return;
function cardLoaded(card){
const widget = card.querySelector('.tradingview-widget-container');
if (!widget) return false;
const inner = widget.querySelector('.tradingview-widget-container__widget');
if (!inner) return false;
// TradingView injects an iframe (mini-symbol-overview) once loaded
const iframe = inner.querySelector('iframe');
if (!iframe) return false;
const rect = iframe.getBoundingClientRect();
// Iframe must have a reasonable rendered size
if (rect.width < 120 || rect.height < 60) return false;
// Iframe must have a src pointing to TradingView
const src = iframe.getAttribute('src') || '';
if (!/tradingview/.test(src)) return false;
return true;
}
function hideCard(card){
card.style.display = 'none';
card.setAttribute('aria-hidden', 'true');
card.classList.add('tv-card-hidden');
}
function pruneOnce(){
let hidden = 0, total = tvCards.length;
tvCards.forEach(c => { if (!cardLoaded(c)) { hideCard(c); hidden++; } });
// If carousel has dots/nav, dispatch event so carousel JS (if any) re-counts
const carousel = document.querySelector('.tv-main-carousel');
if (carousel && hidden > 0) {
carousel.dispatchEvent(new CustomEvent('tv-cards-pruned', { detail: { hidden, total } }));
}
// If ALL cards failed, hide the entire section to avoid an empty block
if (hidden === total) {
const section = document.querySelector('section:has(.tv-main-carousel)');
if (section) section.style.display = 'none';
}
}
// First check at 8s (TradingView usually loads in 2-5s; this gives slow connections buffer)
setTimeout(pruneOnce, 8000);
// Safety re-check at 14s to catch late-loaders
setTimeout(pruneOnce, 14000);
})();
/* ----- Spreads ticker — refresh every 8s with synthetic micro-tick (UI only) ----- */
const ticker = $('.spreads-list');
if (ticker) {
const tick = () => {
$$('.spread-row', ticker).forEach(row => {
const bidEl = row.querySelector('.bid');
const askEl = row.querySelector('.ask');
if (!bidEl || !askEl) return;
const bid = parseFloat(bidEl.dataset.base);
const ask = parseFloat(askEl.dataset.base);
const dp = parseInt(row.dataset.dp || '5', 10);
const jitter = () => (Math.random() - 0.5) * (bid * 0.0004);
bidEl.textContent = (bid + jitter()).toFixed(dp);
askEl.textContent = (ask + jitter()).toFixed(dp);
});
};
tick();
if (!RM) setInterval(tick, 4000);
}
/* ----- Hero tagline rotator + image stage sync -----
Stage 0 (arrow / "Max Possibilities") plays ONLY ONCE at page load.
After first cycle reaches end, rotator skips index 0 forever — only
stages 1/2/3 (Get.Set.Grow., Compass, Signal) cycle continuously. */
const rotator = $('#tag-rotator');
const stage = $('#hero-stage');
const progress = $('#hero-progress');
if (rotator) {
const lines = $$('.tag-line', rotator);
const stages = stage ? $$('.stage-img', stage) : [];
const dots = progress ? $$('.dot-on', progress) : [];
let idx = 0;
let firstCycleDone = false;
dots.forEach((d, i) => d.classList.toggle('live', i === 0));
const swap = () => {
let next = (idx + 1) % lines.length;
// After first full cycle, stage 0 (arrow) retires — never returns
if (next === 0) firstCycleDone = true;
if (firstCycleDone && next === 0) {
next = 1 % lines.length;
}
lines[idx].classList.remove('active');
lines[next].classList.add('active');
if (stages.length) {
stages[idx]?.classList.remove('active');
stages[next]?.classList.add('active');
}
dots.forEach((d, i) => d.classList.toggle('live', i === next));
idx = next;
};
if (!RM) setInterval(swap, 3600);
}
/* ----- Interactive markets picker ----- */
const mp = $('#markets-pick');
if (mp) {
const tabs = $$('.mp-tab', mp);
const panels = $$('.mp-panel', mp);
const setActive = (key) => {
tabs.forEach(t => t.classList.toggle('active', t.dataset.mp === key));
panels.forEach(p => p.classList.toggle('active', p.dataset.mpPanel === key));
};
tabs.forEach(t => t.addEventListener('click', () => setActive(t.dataset.mp)));
// Auto-cycle every 5s if user hasn't clicked — desktop only.
// On mobile/tablet (≤1100), absolute-positioned inactive panels overlap
// visually mid-transition because mp-stage padding collapses; user swipes
// horizontally between tabs instead.
let userClicked = false;
tabs.forEach(t => t.addEventListener('click', () => { userClicked = true; }));
let i = 0;
if (!RM && matchMedia('(min-width: 1101px)').matches) {
setInterval(() => {
if (userClicked) return;
i = (i + 1) % tabs.length;
setActive(tabs[i].dataset.mp);
}, 5000);
}
}
/* ----- Scroll-pinned steps cinema (desktop + mobile spec) ----- */
const cinema = document.getElementById('steps-cinema');
if (cinema) {
const frames = cinema.querySelectorAll('.sc-frame');
const dots = cinema.querySelectorAll('.sc-dot');
const fill = cinema.querySelector('.sc-progress-fill');
const onCinemaScroll = () => {
const rect = cinema.getBoundingClientRect();
const vh = window.innerHeight;
const total = cinema.offsetHeight - vh;
const scrolled = Math.min(Math.max(-rect.top, 0), total);
const p = total > 0 ? scrolled / total : 0;
const idx = Math.min(3, Math.floor(p * 4));
frames.forEach((f, i) => f.classList.toggle('active', i === idx));
dots.forEach((d, i) => d.classList.toggle('lit', i <= idx));
if (fill) fill.style.height = Math.min(100, p * 100).toFixed(1) + '%';
};
onCinemaScroll();
window.addEventListener('scroll', onCinemaScroll, { passive: true });
window.addEventListener('resize', onCinemaScroll, { passive: true });
}
/* ----- Step-path SVG line draw + cascading reveal on scroll-in ----- */
$$('.steps-anim').forEach(stepsEl => {
const svgPath = stepsEl.querySelector('.step-line');
if (svgPath) {
const len = svgPath.getTotalLength ? svgPath.getTotalLength() : 1200;
svgPath.style.strokeDasharray = len;
svgPath.style.strokeDashoffset = len;
}
const ioStep = new IntersectionObserver((es) => {
es.forEach(e => {
if (e.isIntersecting) {
if (svgPath) {
svgPath.style.transition = 'stroke-dashoffset 1.8s var(--ease)';
svgPath.style.strokeDashoffset = '0';
}
stepsEl.querySelectorAll('.step').forEach((step, idx) => {
step.classList.add('in-view');
setTimeout(() => step.classList.add('lit'), 220 + idx * 320);
});
ioStep.unobserve(e.target);
}
});
}, { threshold: 0.25 });
ioStep.observe(stepsEl);
});
/* ----- Tab switcher (used on register page) ----- */
$$('.tab-bar').forEach(bar => {
const targetSel = bar.dataset.target;
bar.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => {
bar.querySelectorAll('button').forEach(b => b.classList.toggle('active', b === btn));
const go = btn.dataset.go;
if (targetSel) {
document.querySelectorAll(`${targetSel} .tab-panel`).forEach(p => {
p.classList.toggle('active', p.dataset.panel === go);
});
}
});
});
});
/* ----- Mobile sticky bottom CTA bar (on conversion-relevant pages) ----- */
if (matchMedia('(max-width: 640px)').matches) {
const bb = $('.bottom-bar-mobile');
if (bb) document.body.classList.add('has-bottom-bar');
}
/* ----- Timelapse text chunk reveal ----- */
if (!RM) {
const io2 = new IntersectionObserver((entries) => {
entries.forEach(en => {
if (en.isIntersecting) {
en.target.classList.add('in');
io2.unobserve(en.target);
}
});
}, { threshold: 0.18, rootMargin: '0px 0px -6% 0px' });
$$('[data-reveal-chunk]').forEach(el => io2.observe(el));
} else {
$$('[data-reveal-chunk]').forEach(el => el.classList.add('in'));
}
/* ----- Scroll progress bar (vertical, left edge) ----- */
let scrollProgress = document.getElementById('scroll-progress');
if (!scrollProgress) {
scrollProgress = document.createElement('div');
scrollProgress.id = 'scroll-progress';
scrollProgress.className = 'scroll-progress';
scrollProgress.setAttribute('aria-hidden', 'true');
document.body.appendChild(scrollProgress);
}
const updateProgress = () => {
const h = document.documentElement;
const max = h.scrollHeight - h.clientHeight;
const pct = max > 0 ? (window.scrollY / max) * 100 : 0;
scrollProgress.style.setProperty('--sp', pct.toFixed(2) + '%');
};
updateProgress();
window.addEventListener('scroll', updateProgress, { passive: true });
window.addEventListener('resize', updateProgress, { passive: true });
/* ----- KPI sparklines: append a mini SVG to each .kpi ----- */
$$('.kpi').forEach((kpi, i) => {
if (kpi.querySelector('.spark')) return;
const points = [];
const n = 14;
let v = 50 + Math.random() * 10;
for (let j = 0; j < n; j++) {
v += (Math.random() - 0.42) * 12;
v = Math.max(10, Math.min(90, v));
points.push(`${(j / (n - 1)) * 100},${100 - v}`);
}
const d = 'M ' + points.join(' L ');
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('class', 'spark');
svg.setAttribute('viewBox', '0 0 100 100');
svg.setAttribute('preserveAspectRatio', 'none');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', d);
svg.appendChild(path);
kpi.appendChild(svg);
});
const ioKpi = new IntersectionObserver((es) => {
es.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); ioKpi.unobserve(e.target); } });
}, { threshold: 0.4 });
$$('.kpi').forEach(k => ioKpi.observe(k));
/* ----- Live-markets strip: poll rates.php every 4s ----- */
const tilesWrap = $('#live-tiles');
if (tilesWrap) {
const fmtTime = (t) => {
const d = new Date((t || Date.now() / 1000) * 1000);
return d.toLocaleTimeString([], { hour12: false });
};
const sparkPath = (history, dp) => {
if (!history || history.length < 2) return '';
const min = Math.min(...history);
const max = Math.max(...history);
const range = (max - min) || 1;
return 'M ' + history.map((v, i) => {
const x = (i / (history.length - 1)) * 100;
const y = 28 - ((v - min) / range) * 24 - 2;
return `${x.toFixed(1)} ${y.toFixed(1)}`;
}).join(' L ');
};
const renderTiles = (items) => {
items.forEach(it => {
const tile = tilesWrap.querySelector(`[data-sym="${it.sym}"]`);
if (!tile) return;
const priceEl = tile.querySelector('.price');
const chgEl = tile.querySelector('.chg');
const symEl = tile.querySelector('.sym');
const pathEl = tile.querySelector('.spark2 path');
if (symEl) symEl.textContent = it.name;
if (priceEl) priceEl.textContent = it.last.toFixed(it.dp);
if (chgEl) {
const up = it.chg >= 0;
chgEl.textContent = (up ? '+' : '') + it.chg.toFixed(2) + '%';
chgEl.classList.toggle('up', up);
chgEl.classList.toggle('dn', !up);
chgEl.style.color = up ? 'var(--success)' : 'var(--danger)';
}
if (pathEl) pathEl.setAttribute('d', sparkPath(it.spark, it.dp));
if (pathEl) pathEl.setAttribute('stroke', it.chg >= 0 ? 'var(--success)' : 'var(--danger)');
});
};
const updatedEl = $('#rates-updated');
let lastPayload = null;
const fetchRates = async () => {
try {
const res = await fetch('rates.php', { credentials: 'same-origin', cache: 'no-store' });
if (!res.ok) throw new Error('rates ' + res.status);
const data = await res.json();
lastPayload = data;
renderTiles(data.items.slice(0, 6));
if (updatedEl) updatedEl.textContent = fmtTime(data.ts);
} catch (err) {
// Quiet fail — keep existing static numbers if proxy unavailable
if (updatedEl) updatedEl.textContent = 'sample data';
}
};
fetchRates();
if (!RM) setInterval(fetchRates, 4000);
}
/* Footer ticker (if present) */
const footerTicker = $('#footer-ticker');
if (footerTicker) {
setInterval(async () => {
try {
const r = await fetch('rates.php', { cache: 'no-store' });
const d = await r.json();
footerTicker.querySelectorAll('[data-sym]').forEach(el => {
const m = d.items.find(x => x.sym === el.dataset.sym);
if (m) el.querySelector('.p').textContent = m.last.toFixed(m.dp);
});
} catch (e) {}
}, 5000);
}
/* ----- Platforms tabbed showcase ----- */
const pxSection = document.getElementById('platforms-showcase');
if (pxSection) {
const pxTabs = pxSection.querySelectorAll('.px-tab');
const pxDevices = pxSection.querySelectorAll('.px-device');
const pxFeats = pxSection.querySelectorAll('.px-feat-panel');
const setPx = (key) => {
pxTabs.forEach(t => {
const active = t.dataset.px === key;
t.classList.toggle('active', active);
t.setAttribute('aria-selected', active ? 'true' : 'false');
});
pxDevices.forEach(d => d.classList.toggle('active', d.dataset.pxPanel === key));
pxFeats.forEach(f => f.classList.toggle('active', f.dataset.pxFeat === key));
};
pxTabs.forEach(t => t.addEventListener('click', () => setPx(t.dataset.px)));
// Auto-rotate every 6s if user hasn't clicked
let pxUserClicked = false;
pxTabs.forEach(t => t.addEventListener('click', () => { pxUserClicked = true; }));
if (!RM) {
let pxIdx = 0;
setInterval(() => {
if (pxUserClicked) return;
pxIdx = (pxIdx + 1) % pxTabs.length;
setPx(pxTabs[pxIdx].dataset.px);
}, 6000);
}
}
/* ----- Form: password show/hide ----- */
$$('[data-pwd-toggle]').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.pwdToggle;
const inp = document.getElementById(id);
if (!inp) return;
const show = inp.type === 'password';
inp.type = show ? 'text' : 'password';
btn.querySelector('use')?.setAttribute('href', show ? '/assets/icons.svg#i-eye-off' : '/assets/icons.svg#i-eye');
});
});
/* (Mobile nav submenu logic moved into setupMobileNav above — no duplicate.) */
/* ----- Trading session times indicator ----- */
const sessionsEl = document.getElementById('sessions-bar');
if (sessionsEl) {
const SESSIONS = [
{ city: 'Sydney', open: 22, close: 7, flag: '🇦🇺' },
{ city: 'Tokyo', open: 0, close: 9, flag: '🇯🇵' },
{ city: 'London', open: 8, close: 17, flag: '🇬🇧' },
{ city: 'New York',open: 13, close: 22, flag: '🇺🇸' },
];
const pad = n => String(n).padStart(2, '0');
const isOpen = (s, hUtc) => {
if (s.open < s.close) return hUtc >= s.open && hUtc < s.close;
return hUtc >= s.open || hUtc < s.close;
};
const minsTo = (target, nowMin) => {
let diff = (target * 60) - nowMin;
if (diff <= 0) diff += 24 * 60;
return diff;
};
const render = () => {
const now = new Date();
const hUtc = now.getUTCHours();
const mUtc = now.getUTCMinutes();
const nowMin = hUtc * 60 + mUtc;
sessionsEl.innerHTML = SESSIONS.map(s => {
const open = isOpen(s, hUtc);
const next = open ? minsTo(s.close, nowMin) : minsTo(s.open, nowMin);
const h = Math.floor(next / 60);
const m = next % 60;
const label = open ? `Closes in ${h}h ${pad(m)}m` : `Opens in ${h}h ${pad(m)}m`;
return `
${s.flag} ${s.city}
${pad(s.open)}:00–${pad(s.close)}:00 UTC
${label}
`;
}).join('');
};
render();
setInterval(render, 60000);
}
/* ----- IB commission calculator REMOVED (Anax preference).
Commercial rates discussed with the partner desk after KYC. ----- */
/* ----- Risk-suitability quiz ----- */
const quiz = document.getElementById('risk-quiz');
if (quiz) {
const steps = quiz.querySelectorAll('.quiz-step');
const fill = quiz.querySelector('.quiz-progress-fill');
const prevBtn = quiz.querySelector('[data-quiz="prev"]');
const nextBtn = quiz.querySelector('[data-quiz="next"]');
let idx = 0;
const scores = [];
const stepCount = steps.length - 1; // last step = result
const update = () => {
steps.forEach((s, i) => s.classList.toggle('active', i === idx));
if (fill) fill.style.width = (idx / stepCount * 100).toFixed(1) + '%';
if (prevBtn) prevBtn.style.visibility = idx === 0 ? 'hidden' : 'visible';
if (nextBtn) {
if (idx >= stepCount - 1) nextBtn.textContent = 'See result';
else if (idx === stepCount) nextBtn.textContent = 'Restart';
else nextBtn.textContent = 'Next →';
}
};
quiz.querySelectorAll('.quiz-opt').forEach(opt => {
opt.addEventListener('click', () => {
const stepEl = opt.closest('.quiz-step');
stepEl.querySelectorAll('.quiz-opt').forEach(o => o.classList.remove('selected'));
opt.classList.add('selected');
const stepIdx = parseInt(stepEl.dataset.step, 10);
scores[stepIdx] = parseInt(opt.dataset.score || '0', 10);
});
});
const showResult = () => {
const sumScore = scores.reduce((a, b) => a + (b || 0), 0);
let tier = 'Prime', desc = 'Start with a low-commitment account while you build confidence.', minDeposit = '$100';
if (sumScore >= 14) { tier = 'Elite'; desc = 'Institutional-grade access matches your experience and ambition.'; minDeposit = '$25,000'; }
else if (sumScore >= 8) { tier = 'Pro'; desc = 'You are ready for active trading with tighter spreads and a dealing desk.'; minDeposit = '$2,500'; }
const result = quiz.querySelector('.quiz-result');
if (result) {
result.querySelector('.tier-name').textContent = tier;
result.querySelector('.tier-min').textContent = `Min deposit ${minDeposit}`;
result.querySelector('.tier-desc').textContent = desc;
}
};
if (nextBtn) nextBtn.addEventListener('click', () => {
if (idx >= stepCount) { // restart
idx = 0; scores.length = 0;
quiz.querySelectorAll('.quiz-opt.selected').forEach(o => o.classList.remove('selected'));
update();
return;
}
const cur = steps[idx];
if (cur.querySelector('.quiz-opt') && !cur.querySelector('.quiz-opt.selected')) return;
idx++;
if (idx === stepCount) showResult();
update();
});
if (prevBtn) prevBtn.addEventListener('click', () => { if (idx > 0) { idx--; update(); } });
update();
}
/* ----- Academy lessons cookie progress ----- */
const academy = document.getElementById('academy-grid');
if (academy) {
const KEY = 'anax_academy_done';
const get = () => {
try { return JSON.parse(localStorage.getItem(KEY) || '[]'); } catch { return []; }
};
const set = (arr) => localStorage.setItem(KEY, JSON.stringify(arr));
const updateProgress = () => {
const done = get();
const cards = academy.querySelectorAll('.lesson-card');
cards.forEach(c => c.classList.toggle('done', done.includes(c.dataset.lesson)));
const totalLessons = cards.length;
const pct = totalLessons ? Math.round((done.length / totalLessons) * 100) : 0;
const apFill = document.querySelector('.academy-progress .ap-fill');
const apNum = document.querySelector('.academy-progress .ap-num');
const apTotal = document.querySelector('.academy-progress .ap-total');
if (apFill) apFill.style.width = pct + '%';
if (apNum) apNum.textContent = done.length;
if (apTotal) apTotal.textContent = totalLessons;
};
academy.querySelectorAll('.lesson-card').forEach(card => {
card.addEventListener('click', () => {
const id = card.dataset.lesson;
if (!id) return;
let done = get();
if (done.includes(id)) done = done.filter(x => x !== id);
else done.push(id);
set(done);
updateProgress();
});
});
updateProgress();
}
/* ----- Pull-to-refresh (mobile) ----- */
if (matchMedia('(max-width: 768px)').matches && !RM) {
const ptr = document.createElement('div');
ptr.className = 'ptr-indicator';
ptr.innerHTML = '';
document.body.appendChild(ptr);
let startY = 0, pulling = false;
const THRESHOLD = 80;
window.addEventListener('touchstart', (e) => {
if (window.scrollY > 0) { pulling = false; return; }
startY = e.touches[0].clientY;
pulling = true;
}, { passive: true });
window.addEventListener('touchmove', (e) => {
if (!pulling) return;
const d = e.touches[0].clientY - startY;
if (d < 0) return;
const drag = Math.min(THRESHOLD * 1.4, d * 0.5);
ptr.style.setProperty('--ptr', drag + 'px');
ptr.classList.add('pulling');
ptr.querySelector('svg').style.transform = `rotate(${drag * 4}deg)`;
}, { passive: true });
window.addEventListener('touchend', () => {
if (!pulling) return;
const drag = parseFloat(getComputedStyle(ptr).getPropertyValue('--ptr')) || 0;
ptr.classList.remove('pulling');
if (drag >= THRESHOLD * 0.7) {
ptr.classList.add('refreshing');
setTimeout(() => location.reload(), 500);
} else {
ptr.style.setProperty('--ptr', '0px');
}
pulling = false;
});
}
/* ----- Mobile zoom lock (iOS Safari ignores user-scalable=no in some versions) ----- */
(function lockZoom() {
if (!('ontouchstart' in window)) return;
// Block pinch-zoom (multi-touch gesture)
document.addEventListener('gesturestart', e => e.preventDefault(), { passive: false });
document.addEventListener('gesturechange', e => e.preventDefault(), { passive: false });
document.addEventListener('gestureend', e => e.preventDefault(), { passive: false });
// Block double-tap zoom
let lastTouch = 0;
document.addEventListener('touchend', e => {
const now = Date.now();
if (now - lastTouch <= 300) e.preventDefault();
lastTouch = now;
}, { passive: false });
// Block ctrl+wheel zoom on hybrid touch devices
document.addEventListener('wheel', e => {
if (e.ctrlKey) e.preventDefault();
}, { passive: false });
})();
})();