/* ============================================================================
   CA Automator — landing page: "The Intelligence Layer for Modern CA Practice."

   Purely presentational: hero/ecosystem visualizations, scroll storytelling,
   the sign-in/sign-up overlay and the appointment-booking widget. Nothing
   here touches app state — the sign-in form still uses the same
   #g-email/#g-pass/#g-go/#g-offline IDs that app.js already wires, so this
   file only adds behaviour around it, never replaces it.
   ========================================================================= */
(function () {
  'use strict';

  const REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const esc = s => String(s == null ? '' : s)
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');

  // ── shared hub-and-spoke ecosystem SVG builder ──────────────────────────
  // Real product architecture, not decoration: one hub (the Client &
  // Engagement Hub) with a spoke to every module. Positions are computed on
  // a circle so any node count lays out evenly and stays responsive.
  function ringLayout(n, cx, cy, r) {
    const pts = [];
    for (let i = 0; i < n; i++) {
      const a = (i / n) * Math.PI * 2 - Math.PI / 2;
      pts.push({ x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) });
    }
    return pts;
  }

  function buildEcosystemSvg(nodes, opts) {
    const size = opts.size || 460;
    const cx = size / 2, cy = size / 2;
    const r = opts.radius || size * 0.38;
    const hubR = opts.hubR || size * 0.115;
    const nodeR = opts.nodeR || size * 0.052;
    const pts = ringLayout(nodes.length, cx, cy, r);
    let svg = '<svg viewBox="0 0 ' + size + ' ' + size + '" role="img" aria-label="CA Automator module ecosystem">';
    // spokes (static + animated flow overlay)
    pts.forEach(p => {
      svg += '<path class="eco-link" d="M' + cx + ' ' + cy + ' L' + p.x + ' ' + p.y + '"/>';
      svg += '<path class="eco-link-flow" d="M' + cx + ' ' + cy + ' L' + p.x + ' ' + p.y + '"/>';
    });
    // hub
    svg += '<circle class="eco-hub-ring" cx="' + cx + '" cy="' + cy + '" r="' + (hubR + 10) + '"/>';
    svg += '<circle class="eco-hub-circle" cx="' + cx + '" cy="' + cy + '" r="' + hubR + '"/>';
    svg += '<text class="eco-hub-label" x="' + cx + '" y="' + (cy - 3) + '" text-anchor="middle">HUB</text>';
    svg += '<text class="eco-hub-sub" x="' + cx + '" y="' + (cy + 10) + '" text-anchor="middle">Client Data</text>';
    // nodes
    nodes.forEach((n, i) => {
      const p = pts[i];
      const labelY = p.y + nodeR + 13;
      svg += '<g class="eco-node" data-key="' + esc(n.key) + '" data-idx="' + i + '">' +
        '<circle class="eco-node-circle" cx="' + p.x + '" cy="' + p.y + '" r="' + nodeR + '"/>' +
        '<text class="eco-node-label" x="' + p.x + '" y="' + (p.y + 3) + '" text-anchor="middle" style="font-size:' + (nodeR * 0.62) + 'px">' + esc(n.code) + '</text>' +
        '<text class="eco-node-label" x="' + p.x + '" y="' + labelY + '" text-anchor="middle">' + esc(n.short || n.name) + '</text>' +
        '</g>';
    });
    svg += '</svg>';
    return svg;
  }

  // ── hero ecosystem + demo dashboard cards ───────────────────────────────
  const HERO_MODULES = [
    { key: 'audit', code: 'AU', name: 'Audit', short: 'Audit' },
    { key: 'tax', code: 'TX', name: 'Tax', short: 'Tax' },
    { key: 'nfrs', code: 'NF', name: 'NFRS', short: 'NFRS' },
    { key: 'valuation', code: 'VL', name: 'Valuation', short: 'Valuation' },
    { key: 'fdi', code: 'FD', name: 'FDI', short: 'FDI' },
    { key: 'tp', code: 'TP', name: 'Transfer Pricing', short: 'Transfer Pricing' },
    { key: 'ipo', code: 'IP', name: 'IPO', short: 'IPO' },
    { key: 'dd', code: 'DD', name: 'Due Diligence', short: 'Due Diligence' },
    { key: 'forensic', code: 'FX', name: 'Forensic', short: 'Forensic' },
    { key: 'advisory', code: 'AD', name: 'Advisory', short: 'Advisory' },
    { key: 'compliance', code: 'CP', name: 'Compliance', short: 'Compliance' },
  ];

  const DASH_CARDS = [
    { label: 'Audit Risk', value: 'Moderate', tag: 'a', foot: 'Demo data', pos: { top: '4%', left: '2%' } },
    { label: 'Tax Exposure', value: 'NPR 2.1M', tag: 'a', foot: 'Demo data', pos: { top: '10%', right: '0%' } },
    { label: 'Compliance', value: '96%', tag: 'g', foot: 'Demo data', pos: { top: '58%', left: '0%' } },
    { label: 'Related Parties', value: '6 flagged', tag: 'a', foot: 'Demo data', pos: { top: '74%', left: '20%' } },
    { label: 'Valuation', value: 'रु 340.2M', tag: 'g', foot: 'Demo data', pos: { top: '2%', left: '32%' } },
    { label: 'Documents', value: '128 filed', tag: 'g', foot: 'Demo data', pos: { top: '84%', right: '4%' } },
    { label: 'Financial Health', value: 'Strong', tag: 'g', foot: 'Demo data', pos: { top: '40%', right: '-2%' } },
    { label: 'Review Status', value: '3 pending', tag: 'a', foot: 'Demo data', pos: { bottom: '2%', left: '46%' } },
  ];

  function initHeroEco() {
    const holder = document.getElementById('hero-eco');
    const dashHolder = document.getElementById('hero-dash');
    if (!holder) return;
    holder.innerHTML = buildEcosystemSvg(HERO_MODULES, { size: 440, radius: 168, hubR: 50, nodeR: 22 });

    if (dashHolder) {
      dashHolder.innerHTML = DASH_CARDS.map((d, i) => {
        const style = Object.keys(d.pos).map(k => k + ':' + d.pos[k]).join(';');
        return '<div class="dash-card" style="' + style + '" data-i="' + i + '">' +
          '<div class="dc-label"><span>' + esc(d.label) + '</span><span class="dc-tag ' + d.tag + '">' + (d.tag === 'g' ? 'OK' : 'WATCH') + '</span></div>' +
          '<div class="dc-value">' + esc(d.value) + '</div><div class="dc-foot">' + esc(d.foot) + '</div></div>';
      }).join('');
    }

    if (REDUCED) return;
    // gentle cursor-responsive parallax on the hero visual — transform/opacity
    // only, capped displacement, no effect on layout.
    const visual = document.querySelector('#gate .hero-visual');
    if (!visual) return;
    let raf = null;
    visual.addEventListener('mousemove', e => {
      if (raf) return;
      raf = requestAnimationFrame(() => {
        const rect = visual.getBoundingClientRect();
        const dx = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
        const dy = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
        holder.style.transform = 'translate(' + (dx * -6) + 'px,' + (dy * -6) + 'px)';
        Array.from(dashHolder.children).forEach((el, i) => {
          const depth = 4 + (i % 3) * 3;
          el.style.transform = 'translate(' + (dx * depth) + 'px,' + (dy * depth) + 'px)';
        });
        raf = null;
      });
    });
    visual.addEventListener('mouseleave', () => {
      holder.style.transform = '';
      Array.from(dashHolder.children).forEach(el => { el.style.transform = ''; });
    });
  }

  // ── "One Data Point. Multiple Professional Outputs." scroll story ──────
  function initDataFlowStory() {
    const section = document.getElementById('flow-diagram');
    if (!section) return;
    const outputs = Array.from(section.querySelectorAll('.flow-output'));
    const line = document.getElementById('flow-line');
    let fired = false;
    function play() {
      if (fired) return;
      fired = true;
      if (line) line.classList.add('lit');
      outputs.forEach((el, i) => {
        setTimeout(() => el.classList.add('lit'), REDUCED ? 0 : 150 + i * 130);
      });
    }
    if (REDUCED || !('IntersectionObserver' in window)) { play(); return; }
    const io = new IntersectionObserver(entries => {
      entries.forEach(e => { if (e.isIntersecting) play(); });
    }, { threshold: 0.4 });
    io.observe(section);
  }

  // ── "Independent by Design. Connected by Intelligence." ecosystem map ──
  const ECO_MODULES = [
    { key: 'audit', code: 'AU', name: 'Statutory & Internal Audit', short: 'Audit',
      blurb: 'Materiality, risk assessment, sampling and the completion checklist — usable on a client with no other module set up.',
      connects: ['Related parties → Transfer Pricing, Tax', 'Trial Balance → Accounting Engine'] },
    { key: 'accounting', code: 'AC', name: 'Accounting & Financial Statements', short: 'Accounting',
      blurb: 'Trial balance to signed financial statements — the most commonly shared source across the platform.',
      connects: ['Feeds → Audit, Tax, Valuation, Due Diligence'] },
    { key: 'tax', code: 'TX', name: 'Tax Advisory', short: 'Tax',
      blurb: 'Income tax, TDS, VAT reconciliation and cross-border tax exposure in one continuous position.',
      connects: ['Foreign shareholder → FDI, Transfer Pricing'] },
    { key: 'nfrs', code: 'NF', name: 'NFRS Compliance', short: 'NFRS',
      blurb: 'Standards-compliance checks embedded in Accounting and Audit rather than a separate silo.',
      connects: ['Disclosure checks → Financial Statements'] },
    { key: 'valuation', code: 'VL', name: 'Valuation', short: 'Valuation',
      blurb: 'NAV, DCF, P/E and EV/EBITDA — weighted to the entity, used at entry and exit alike.',
      connects: ['Debt/cash-flow anomaly → Due Diligence'] },
    { key: 'fdi', code: 'FD', name: 'FDI Structuring', short: 'FDI',
      blurb: 'Route eligibility, sector caps, tax pack and exit calculation for inbound investment.',
      connects: ['Foreign shareholder → Transfer Pricing, Tax, Corporate'] },
    { key: 'tp', code: 'TP', name: 'Transfer Pricing', short: 'Transfer Pricing',
      blurb: 'Related-party register, FAR analysis, method selection and benchmarking across jurisdictions.',
      connects: ['Related party found → Audit, Tax, Corporate'] },
    { key: 'ipo', code: 'IP', name: 'IPO Advisory', short: 'IPO',
      blurb: 'Readiness checklist, restatement analysis and peer comparison ahead of a public offer.',
      connects: ['Uses → Valuation, Due Diligence'] },
    { key: 'dd', code: 'DD', name: 'Due Diligence', short: 'Due Diligence',
      blurb: 'Quality-of-earnings bridge, net debt analysis and a rule-based risk register for a transaction.',
      connects: ['Deal bridge → Valuation, Tax'] },
    { key: 'forensic', code: 'FX', name: 'Forensic Audit', short: 'Forensic',
      blurb: "Benford's Law, duplicate and round-number detection, related-party screening.",
      connects: ['Findings → Audit, Corporate Compliance'] },
    { key: 'internal', code: 'IA', name: 'Internal Audit', short: 'Internal Audit',
      blurb: 'Risk-based planning, control testing and a tracked findings register.',
      connects: ['Findings → Statutory Audit'] },
    { key: 'corporate', code: 'CC', name: 'Corporate Compliance', short: 'Compliance',
      blurb: 'Shareholding, capital events, board resolutions and the company-law checklist.',
      connects: ['Shareholding → Transfer Pricing, FDI'] },
    { key: 'modelling', code: 'FM', name: 'Financial Modelling', short: 'Modelling',
      blurb: 'Projections, feasibility, DSCR/LLCR/PLCR appraisal and scenario stress-testing.',
      connects: ['Feeds → Valuation, Debt Advisory'] },
    { key: 'advisory', code: 'AD', name: 'Business Advisory', short: 'Advisory',
      blurb: 'Profitability, pricing, budgeting and a KPI dashboard for management.',
      connects: ['Draws on → Accounting, Tax'] },
  ];

  function initEcosystemMap() {
    const holder = document.getElementById('eco-map');
    const preview = document.getElementById('eco-preview');
    if (!holder || !preview) return;
    holder.innerHTML = buildEcosystemSvg(ECO_MODULES, { size: 560, radius: 220, hubR: 58, nodeR: 26 });

    function showPreview(mod) {
      preview.innerHTML =
        '<div class="eco-preview-tag">' + esc(mod.code) + ' — Module</div>' +
        '<h4>' + esc(mod.name) + '</h4>' +
        '<p>' + esc(mod.blurb) + '</p>' +
        '<div class="eco-preview-mini">' +
        mod.connects.map(c => '<div class="row"><span>Connects when shared</span><b>' + esc(c) + '</b></div>').join('') +
        '</div>';
    }
    function clearPreview() {
      preview.innerHTML = '<div class="eco-preview-empty">Hover a module in the map to see how it connects.</div>';
    }

    holder.querySelectorAll('.eco-node').forEach(el => {
      const key = el.getAttribute('data-key');
      const mod = ECO_MODULES.find(m => m.key === key);
      if (!mod) return;
      el.addEventListener('mouseenter', () => { el.classList.add('on'); showPreview(mod); });
      el.addEventListener('mouseleave', () => { el.classList.remove('on'); clearPreview(); });
      el.addEventListener('focus', () => { el.classList.add('on'); showPreview(mod); });
      el.addEventListener('blur', () => { el.classList.remove('on'); clearPreview(); });
      el.addEventListener('click', () => showPreview(mod));
      el.setAttribute('tabindex', '0');
    });
  }

  // ── "From Data to Decision" stage stepper ───────────────────────────────
  function initWorkflowStages() {
    const track = document.getElementById('stage-track');
    if (!track) return;
    const steps = Array.from(track.querySelectorAll('.stage-step'));
    let fired = false;
    function play() {
      if (fired) return;
      fired = true;
      steps.forEach((el, i) => setTimeout(() => el.classList.add('active'), REDUCED ? 0 : i * 130));
    }
    if (REDUCED || !('IntersectionObserver' in window)) { play(); return; }
    const io = new IntersectionObserver(entries => {
      entries.forEach(e => { if (e.isIntersecting) play(); });
    }, { threshold: 0.5 });
    io.observe(track);
  }

  // ── jurisdiction selector ────────────────────────────────────────────────
  const JURISDICTIONS = {
    np: { name: 'Nepal', rows: [
      ['Tax structure', 'Income Tax Act, 2058 — graduated + flat corporate rates'],
      ['Accounting standard', 'NFRS / NFRS for SMEs'],
      ['Fiscal year', 'Shrawan – Ashad (Bikram Sambat)'],
      ['Currency', 'NPR'],
      ['FDI framework', 'FITTA — automatic, DoI or IBN route by size and sector'],
      ['Transfer pricing', 'Notice-based related-party disclosure'],
    ] },
    in: { name: 'India', rows: [
      ['Tax structure', 'Income Tax Act, 1961'],
      ['Accounting standard', 'Ind AS / Indian GAAP'],
      ['Fiscal year', 'April – March'],
      ['Currency', 'INR'],
      ['FDI framework', 'Consolidated FDI Policy — automatic / government route'],
      ['Transfer pricing', 'Rule 10B, safe-harbour rules, Form 3CEB'],
    ] },
    ae: { name: 'UAE', rows: [
      ['Tax structure', 'Federal Corporate Tax — 9% standard rate'],
      ['Accounting standard', 'IFRS'],
      ['Fiscal year', 'Calendar year or firm-elected'],
      ['Currency', 'AED'],
      ['FDI framework', '100% foreign ownership — mainland &amp; free zone'],
      ['Transfer pricing', 'OECD-aligned — Ministerial Decision 97'],
    ] },
    gl: { name: 'Global', rows: [
      ['Tax structure', 'Configurable — added as data, not code'],
      ['Accounting standard', 'IFRS baseline, locally extensible'],
      ['Fiscal year', 'Custom, per entity'],
      ['Currency', 'Multi-currency'],
      ['FDI framework', 'Configurable ownership &amp; approval routes'],
      ['Transfer pricing', 'OECD Transfer Pricing Guidelines framework'],
    ] },
  };

  function initJurisdictionTabs() {
    const tabs = document.getElementById('juris-tabs');
    const panel = document.getElementById('juris-panel');
    if (!tabs || !panel) return;
    function render(key) {
      const j = JURISDICTIONS[key];
      panel.innerHTML = j.rows.map(r =>
        '<div class="juris-row"><div class="jr-label">' + esc(r[0]) + '</div><div class="jr-value">' + r[1] + '</div></div>').join('');
    }
    tabs.querySelectorAll('.juris-tab').forEach(btn => {
      btn.addEventListener('click', () => {
        tabs.querySelectorAll('.juris-tab').forEach(b => b.classList.toggle('on', b === btn));
        render(btn.dataset.juris);
      });
    });
    render('np');
  }

  // ── "Built for Every CA Engagement" grouped module cards ────────────────
  const MODULE_GROUPS = [
    { name: 'Audit & Assurance', note: '3 modules', modules: [
      { code: 'SA', name: 'Statutory Audit', blurb: 'Materiality, risk assessment, sample sizing and a full completion checklist.' },
      { code: 'IA', name: 'Internal Audit', blurb: 'Risk-based planning, control testing (RACM) and a tracked findings register.' },
      { code: 'AX', name: 'Audit Execution', blurb: 'Substantive testing, analytical review, going concern and disclosure checklists.' },
    ] },
    { name: 'Tax & Compliance', note: '3 modules', modules: [
      { code: 'TX', name: 'Income Tax & TDS', blurb: 'Business and personal income tax, TDS computation and withholding certificates.' },
      { code: 'VT', name: 'VAT Returns', blurb: 'Output/input VAT reconciliation and return preparation.' },
      { code: 'TA', name: 'Tax Advisory', blurb: 'VAT/TDS reconciliation, deferred tax, minimum tax check and dispute case management.' },
    ] },
    { name: 'Transactions & Valuation', note: '3 modules', modules: [
      { code: 'VL', name: 'Valuation', blurb: 'Nine valuation methods, DCF, and a WACC builder, weighted to entity and stage.' },
      { code: 'DD', name: 'Due Diligence', blurb: 'Quality-of-earnings bridge, net debt analyzer and a rule-based risk register.' },
      { code: 'CD', name: 'Capital & Debt Advisory', blurb: 'Debt capacity, loan restructuring and debt-vs-equity funding analysis.' },
    ] },
    { name: 'Corporate & FDI', note: '3 modules', modules: [
      { code: 'FD', name: 'FDI Structuring', blurb: 'Route eligibility, sector caps, repatriation tax pack and exit calculation.' },
      { code: 'CP', name: 'Corporate Advisory', blurb: 'Shareholding, capital events, board resolutions and company-law compliance.' },
      { code: 'IP', name: 'IPO Advisory', blurb: 'Readiness checklist, restatement analysis and peer/pricing comparison.' },
    ] },
    { name: 'Forensic & Risk', note: '2 modules', modules: [
      { code: 'FX', name: 'Forensic Audit', blurb: "Benford's Law, duplicate detection, related-party and payroll fraud screens." },
      { code: 'TP', name: 'Transfer Pricing', blurb: 'Related-party register, FAR analysis, method selection and benchmarking.' },
    ] },
    { name: 'Financial Intelligence', note: '3 modules', modules: [
      { code: 'FS', name: 'Financial Statements', blurb: 'Trial balance to signed statements, with auto-suggested TB-to-FS mapping.' },
      { code: 'AA', name: 'Accounting Automation', blurb: 'Cash flow, changes in equity, accruals, FX revaluation and closing checklist.' },
      { code: 'PJ', name: 'Financial Modelling', blurb: 'Projections, feasibility, bank appraisal and scenario stress-testing.' },
    ] },
    { name: 'Practice Management', note: '3 modules', modules: [
      { code: 'PM', name: 'Practice Management', blurb: 'Onboarding, KYC, proposals, engagement letters and staff time budgets.' },
      { code: 'BA', name: 'Business Advisory', blurb: 'Profitability, pricing, budgeting and a KPI dashboard for management.' },
      { code: 'RM', name: 'Rate Master', blurb: 'Every statutory rate and threshold, version-controlled and verification-tracked.' },
    ] },
  ];

  function initModuleGroups() {
    const holder = document.getElementById('module-groups');
    if (!holder) return;
    holder.innerHTML = MODULE_GROUPS.map(g =>
      '<div class="module-group"><div class="module-group-head"><h3>' + esc(g.name) + '</h3><span>' + esc(g.note) + '</span></div>' +
      '<div class="module-grid">' + g.modules.map(m =>
        '<div class="module-card"><div class="mc-code">' + esc(m.code) + '</div><div class="mc-name">' + esc(m.name) + '</div>' +
        '<div class="mc-blurb">' + esc(m.blurb) + '</div></div>').join('') + '</div></div>').join('');
    holder.querySelectorAll('.module-card').forEach(card => {
      card.addEventListener('click', () => card.classList.toggle('open'));
    });
  }

  // ── "Why CA Automator?" benefit grid ─────────────────────────────────────
  const BENEFITS = [
    ['No duplicate data entry', 'Enter a figure once — every module that needs it can reuse it, with your say-so.'],
    ['Connected workflows', 'Modules talk to each other on request, never behind your back.'],
    ['Transparent calculations', 'Every formula is inspectable — no black-box outputs.'],
    ['Reusable client data', 'One Client Data Hub, consumed wherever it is relevant.'],
    ['Professional judgment controls', 'Automation proposes. You accept, modify, reject or investigate.'],
    ['Regulatory traceability', 'Every figure carries its source, version and verification status.'],
    ['Automated working papers', 'Built as you work, referenced and filed, not assembled after the fact.'],
    ['Intelligent risk detection', 'Patterns are flagged with evidence, not just a bare warning.'],
    ['One-click report generation', 'Word and Excel output from every module, firm-branded.'],
    ['Scalable practice management', 'From a sole practice to a multi-partner firm, on the same desk.'],
  ];

  function initBenefitGrid() {
    const holder = document.getElementById('benefit-grid');
    if (!holder) return;
    holder.innerHTML = BENEFITS.map((b, i) =>
      '<div class="benefit-card"><div class="bc-n">' + String(i + 1).padStart(2, '0') + '</div><b>' + esc(b[0]) + '</b><span>' + esc(b[1]) + '</span></div>').join('');
  }

  // ── final CTA demo dashboard ──────────────────────────────────────────────
  function initFinalDash() {
    const holder = document.getElementById('final-dash');
    if (!holder) return;
    const items = [
      ['Client files synced', '142'], ['Working papers generated', '3,860'],
      ['Avg. review turnaround', '1.8 days'], ['Modules per engagement', '4.2'],
    ];
    holder.innerHTML = items.map(i =>
      '<div class="fd-item"><div class="fd-label">' + esc(i[0]) + '</div><div class="fd-value">' + esc(i[1]) + '</div></div>').join('') +
      '<div class="hero-demo-note" style="grid-column:1/-1;position:static;margin-top:4px">Illustrative figures, not live data</div>';
  }

  // ── hero stat counters ───────────────────────────────────────────────────
  function initCounters() {
    const modulesEl = document.getElementById('stat-modules');
    const jurisEl = document.getElementById('stat-juris');
    if (!modulesEl || !jurisEl) return;
    const totalModules = MODULE_GROUPS.reduce((s, g) => s + g.modules.length, 0);
    function count(el, target, suffix) {
      if (REDUCED) { el.textContent = target + (suffix || ''); return; }
      const start = performance.now(), dur = 900;
      function step(now) {
        const t = Math.min(1, (now - start) / dur);
        const eased = 1 - Math.pow(1 - t, 3);
        el.textContent = Math.round(eased * target) + (suffix || '');
        if (t < 1) requestAnimationFrame(step);
      }
      requestAnimationFrame(step);
    }
    let fired = false;
    function play() { if (fired) return; fired = true; count(modulesEl, totalModules, '+'); count(jurisEl, 4); }
    if (!('IntersectionObserver' in window)) { play(); return; }
    const io = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) play(); }); }, { threshold: 0.6 });
    io.observe(modulesEl.closest('.hero-stats'));
  }

  // ── generic scroll-reveal ────────────────────────────────────────────────
  function initReveal() {
    const els = Array.from(document.querySelectorAll('#gate .reveal'));
    if (REDUCED || !('IntersectionObserver' in window)) { els.forEach(el => el.classList.add('in')); return; }
    const io = new IntersectionObserver(entries => {
      entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } });
    }, { threshold: 0.12, rootMargin: '0px 0px -60px 0px' });
    els.forEach(el => io.observe(el));
  }

  // ── nav: transparent→solid on scroll, active-section indicator ──────────
  function initNav() {
    const nav = document.getElementById('lnav');
    if (!nav) return;
    function onScroll() { nav.classList.toggle('scrolled', window.scrollY > 40); }
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });

    const navLinks = Array.from(document.querySelectorAll('#gate .lnav-links a[data-nav]'));
    const sections = navLinks.map(a => document.getElementById(a.getAttribute('href').slice(1))).filter(Boolean);
    if (!sections.length || !('IntersectionObserver' in window)) return;
    const io = new IntersectionObserver(entries => {
      entries.forEach(e => {
        if (!e.isIntersecting) return;
        const id = e.target.id;
        navLinks.forEach(a => a.classList.toggle('on', a.getAttribute('href') === '#' + id));
      });
    }, { rootMargin: '-40% 0px -55% 0px' });
    sections.forEach(s => io.observe(s));
  }

  function initAuth() {
    const overlay = document.getElementById('auth-overlay');
    if (!overlay) return;
    const tabs = Array.from(document.querySelectorAll('.auth-tab'));
    const panes = { signin: document.getElementById('pane-signin'), signup: document.getElementById('pane-signup') };

    function showTab(name) {
      tabs.forEach(t => t.classList.toggle('on', t.dataset.tab === name));
      Object.keys(panes).forEach(k => { if (panes[k]) panes[k].hidden = k !== name; });
    }
    function open(tab) {
      showTab(tab || 'signin');
      overlay.hidden = false;
      document.body.style.overflow = 'hidden';
      const first = panes[tab || 'signin'].querySelector('input');
      if (first) setTimeout(() => first.focus(), 50);
    }
    function close() {
      overlay.hidden = true;
      document.body.style.overflow = '';
    }

    document.querySelectorAll('[data-auth-open]').forEach(el => el.addEventListener('click', () => open(el.dataset.authOpen)));
    tabs.forEach(t => t.addEventListener('click', () => showTab(t.dataset.tab)));
    const closeBtn = document.getElementById('auth-close');
    if (closeBtn) closeBtn.addEventListener('click', close);
    overlay.addEventListener('click', e => { if (e.target === overlay) close(); });
    document.addEventListener('keydown', e => { if (e.key === 'Escape' && !overlay.hidden) close(); });

    const suGo = document.getElementById('su-go');
    if (suGo) suGo.addEventListener('click', async () => {
      const name = document.getElementById('su-name').value.trim();
      const email = document.getElementById('su-email').value.trim();
      const phone = document.getElementById('su-phone').value.trim();
      const message = document.getElementById('su-message').value.trim();
      const msgEl = document.getElementById('su-msg');
      msgEl.textContent = '';
      if (!name || !email) { msgEl.textContent = "Name and email are required."; return; }
      suGo.disabled = true; suGo.textContent = 'Sending…';
      try {
        const res = await fetch('/api/leads', {
          method: 'POST', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ name, email, phone, message, source: 'landing_signup' })
        });
        let body = null;
        try { body = await res.json(); } catch (e) {}
        if (!res.ok) throw new Error((body && body.error) || 'Something went wrong — try again.');
        document.getElementById('pane-signup').innerHTML =
          '<div class="gate-mark">CA</div><h1>Thank you</h1>' +
          '<p class="gate-sub">We have your details and will be in touch shortly. If you already have an account, sign in from the other tab.</p>';
      } catch (e) {
        msgEl.textContent = e.message;
        suGo.disabled = false; suGo.textContent = 'Request access';
      }
    });
  }

  // ── smooth scroll nav — nav links, hero CTAs, footer links all resolve
  // to this. JS-computed offset (not bare CSS scroll-behavior) so the
  // sticky nav never overlaps the target heading.
  function initScrollNav() {
    const nav = document.querySelector('#gate .lnav');
    function scrollToId(id) {
      const el = document.getElementById(id);
      if (!el) return;
      const offset = (nav ? nav.getBoundingClientRect().height : 0) + 14;
      const top = el.getBoundingClientRect().top + window.pageYOffset - offset;
      window.scrollTo({ top: Math.max(top, 0), behavior: REDUCED ? 'auto' : 'smooth' });
    }
    document.querySelectorAll('[data-scroll]').forEach(btn => {
      btn.addEventListener('click', () => scrollToId(btn.getAttribute('data-scroll')));
    });
    document.querySelectorAll('#gate a[href^="#"]').forEach(a => {
      const id = a.getAttribute('href').slice(1);
      if (!id || !document.getElementById(id)) return;
      a.addEventListener('click', e => { e.preventDefault(); scrollToId(id); });
    });
  }

  // ── appointment booking widget ──────────────────────────────────────────
  // Slots are real rows a staff member opened in Admin — if none exist (or
  // none are left unbooked), this shows a plain "nothing available" state
  // rather than a form that would only fail at submit time. Booking races
  // (two visitors, one slot) are resolved server-side (409), not here —
  // this just re-fetches and tells the visitor plainly what happened.
  function initAppointments() {
    const body = document.getElementById('appt-body');
    if (!body) return;
    const WEEKDAY = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
    const MONTH = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    const fmtDay = iso => {
      const d = new Date(iso + 'T00:00:00');
      return WEEKDAY[d.getDay()] + ', ' + d.getDate() + ' ' + MONTH[d.getMonth()];
    };
    const fmtTime = hhmm => {
      const [h, m] = hhmm.split(':').map(Number);
      const ap = h >= 12 ? 'PM' : 'AM';
      const h12 = h % 12 === 0 ? 12 : h % 12;
      return h12 + (m ? ':' + String(m).padStart(2, '0') : '') + ' ' + ap;
    };

    let slots = [], methods = [], services = [], selected = null, selectedMethod = null, selectedService = null;

    function renderEmpty() {
      body.innerHTML = '<div class="appt-empty"><b style="display:block;color:var(--g-ink);margin-bottom:6px">No appointment times are currently available</b>' +
        'Check back soon, or use "Get started" above and we\'ll reach out directly.</div>';
    }

    // Rendered ONCE per load() — re-running this on every click was the bug:
    // it recreated the <input> elements from scratch, so whatever the
    // visitor had already typed (or picked) vanished the moment they chose
    // a different time or method. Selecting a slot/method/service now only
    // updates classes and small summary text (updateSelectionUI below),
    // never rebuilds the form fields, so typed text always survives.
    function renderShell() {
      const byDay = {};
      slots.forEach(s => { (byDay[s.date] = byDay[s.date] || []).push(s); });
      const days = Object.keys(byDay).sort();

      body.innerHTML = '<div class="appt-grid">' +
        '<div class="appt-days">' + days.map(d => '<div><div class="appt-day-label">' + esc(fmtDay(d)) + '</div>' +
          '<div class="appt-slots">' + byDay[d].map(s => '<button type="button" class="appt-slot" data-slot="' + s.id + '">' +
            esc(fmtTime(s.start_time)) + '<span class="p">NPR ' + s.price.toLocaleString('en-IN') + '</span></button>').join('') +
          '</div></div>').join('') + '</div>' +
        '<div class="appt-form">' +
        '<div id="appt-selected-summary"></div>' +
        '<label>Which service is this about?</label><div class="appt-methods" id="appt-service-picker">' +
        services.map(sv => '<button type="button" class="appt-method" data-service="' + esc(sv.value) + '">' + esc(sv.label) + '</button>').join('') + '</div>' +
        '<label>How would you like to meet?</label><div class="appt-methods" id="appt-method-picker">' +
        methods.map(m => '<button type="button" class="appt-method" data-method="' + esc(m.value) + '">' + esc(m.label) + '</button>').join('') + '</div>' +
        '<label>Name<input id="appt-name" type="text" autocomplete="name"></label>' +
        '<label>Email<input id="appt-email" type="email" autocomplete="email"></label>' +
        '<label>Phone<input id="appt-phone" type="text" autocomplete="tel"></label>' +
        '<label>Company (optional)<input id="appt-company" type="text"></label>' +
        '<label>What would you like to discuss?<textarea id="appt-message" rows="3"></textarea></label>' +
        '<button class="btn btn-primary" id="appt-go">Request this time</button>' +
        '<p class="appt-msg" id="appt-msg"></p>' +
        '</div></div>';

      body.querySelectorAll('[data-slot]').forEach(el => el.addEventListener('click', () => {
        selected = slots.find(s => s.id === Number(el.dataset.slot)) || null;
        updateSelectionUI();
      }));
      body.querySelectorAll('[data-method]').forEach(el => el.addEventListener('click', () => {
        selectedMethod = el.dataset.method;
        updateSelectionUI();
      }));
      body.querySelectorAll('[data-service]').forEach(el => el.addEventListener('click', () => {
        selectedService = el.dataset.service;
        updateSelectionUI();
      }));
      document.getElementById('appt-go').addEventListener('click', submit);
      updateSelectionUI();
    }

    // Cheap, targeted DOM updates only — no innerHTML rebuild of anything
    // that could hold text the visitor already typed.
    function updateSelectionUI() {
      body.querySelectorAll('[data-slot]').forEach(el => el.classList.toggle('on', selected && selected.id === Number(el.dataset.slot)));
      body.querySelectorAll('[data-method]').forEach(el => el.classList.toggle('on', selectedMethod === el.dataset.method));
      body.querySelectorAll('[data-service]').forEach(el => el.classList.toggle('on', selectedService === el.dataset.service));
      const sum = document.getElementById('appt-selected-summary');
      if (sum) sum.innerHTML = selected
        ? '<h3>' + esc(fmtDay(selected.date)) + '</h3><p class="sel">' + esc(fmtTime(selected.start_time)) + ' – ' + esc(fmtTime(selected.end_time)) +
          ' · NPR ' + selected.price.toLocaleString('en-IN') + '</p>'
        : '<h3>Choose a time</h3><p class="sel">Pick an open slot on the left to continue.</p>';
    }

    async function load() {
      try {
        const res = await fetch('/api/public/appointment-slots');
        const data = await res.json();
        slots = data.slots || []; methods = data.methods || []; services = data.services || [];
        if (!slots.length) { renderEmpty(); return; }
        renderShell();
      } catch (e) {
        body.innerHTML = '<div class="appt-empty">Could not load available times right now — please try again shortly.</div>';
      }
    }

    async function submit() {
      const msgEl = document.getElementById('appt-msg');
      msgEl.textContent = '';
      if (!selected) { msgEl.textContent = 'Choose a time first.'; return; }
      if (!selectedMethod) { msgEl.textContent = 'Choose how you\'d like to meet.'; return; }
      if (!selectedService) { msgEl.textContent = 'Choose which service this is about.'; return; }
      const name = document.getElementById('appt-name').value.trim();
      const email = document.getElementById('appt-email').value.trim();
      if (!name || !email) { msgEl.textContent = 'Name and email are required.'; return; }
      const goBtn = document.getElementById('appt-go');
      goBtn.disabled = true; goBtn.textContent = 'Booking…';
      try {
        const res = await fetch('/api/public/appointments', {
          method: 'POST', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            slot_id: selected.id, name, email,
            phone: document.getElementById('appt-phone').value.trim(),
            company: document.getElementById('appt-company').value.trim(),
            method: selectedMethod, service: selectedService,
            message: document.getElementById('appt-message').value.trim()
          })
        });
        const out = await res.json();
        if (!res.ok) {
          if (res.status === 409) { msgEl.textContent = out.error + ' Refreshing available times…'; selected = null; await load(); return; }
          throw new Error(out.error || 'Something went wrong — try again.');
        }
        body.innerHTML = '<div class="appt-confirm"><b>Request sent</b><br>' +
          esc(fmtDay(out.date)) + ' at ' + esc(fmtTime(out.start_time)) + ' — NPR ' + out.price.toLocaleString('en-IN') + '.<br>' +
          esc(out.note) + '</div>';
      } catch (e) {
        msgEl.textContent = e.message;
        goBtn.disabled = false; goBtn.textContent = 'Request this time';
      }
    }

    load();
  }

  function initFooterYear() {
    const el = document.getElementById('footer-year');
    if (el) el.textContent = new Date().getFullYear();
  }

  // ── public site content — services, testimonials, firm/contact/Maps info.
  // Everything here comes from GET /api/public/site (admin-managed, no
  // session required); nothing on this page's services/testimonials/contact
  // sections is hardcoded, so a change saved in Admin > Website appears here
  // on next load with no rebuild.
  function priceLabel(s) {
    if (s.price_type === 'enquire' || s.price == null) return 'Price on enquiry';
    const amount = (s.currency || 'NPR') + ' ' + Number(s.price).toLocaleString('en-IN');
    return s.price_type === 'starting_from' ? 'From ' + amount : amount;
  }

  // ── service checkout — Khalti/ConnectIPS. Only ever reachable for a
  // service marked buyable with a fixed price (server enforces this again;
  // this client-side gate is convenience, not the real check). Redirects
  // the whole page to the gateway — there's no in-page payment form here,
  // by design, since neither Khalti nor ConnectIPS wants card/wallet
  // details passing through this site at all.
  function openCheckout(slug, name) {
    const overlay = document.getElementById('checkout-overlay');
    if (!overlay) return;
    document.getElementById('checkout-title').textContent = 'Buy: ' + name;
    document.getElementById('checkout-price').textContent = '';
    document.getElementById('checkout-msg').textContent = '';
    document.getElementById('ck-name').value = '';
    document.getElementById('ck-email').value = '';
    document.getElementById('ck-phone').value = '';
    overlay.hidden = false;
    document.body.style.overflow = 'hidden';
    overlay.dataset.slug = slug;
  }
  function closeCheckout() {
    const overlay = document.getElementById('checkout-overlay');
    if (!overlay) return;
    overlay.hidden = true;
    document.body.style.overflow = '';
  }
  function initCheckout() {
    const overlay = document.getElementById('checkout-overlay');
    if (!overlay) return;
    const closeBtn = document.getElementById('checkout-close');
    if (closeBtn) closeBtn.addEventListener('click', closeCheckout);
    overlay.addEventListener('click', e => { if (e.target === overlay) closeCheckout(); });
    document.addEventListener('keydown', e => { if (e.key === 'Escape' && !overlay.hidden) closeCheckout(); });

    const goBtn = document.getElementById('checkout-go');
    goBtn.addEventListener('click', async () => {
      const msgEl = document.getElementById('checkout-msg');
      msgEl.textContent = '';
      const name = document.getElementById('ck-name').value.trim();
      const email = document.getElementById('ck-email').value.trim();
      if (!name || !email) { msgEl.textContent = 'Name and email are required.'; return; }
      const gateway = document.getElementById('ck-gateway').value;
      goBtn.disabled = true; goBtn.textContent = 'Redirecting…';
      try {
        const res = await fetch('/api/public/services/' + encodeURIComponent(overlay.dataset.slug) + '/checkout', {
          method: 'POST', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            name, email, gateway,
            phone: document.getElementById('ck-phone').value.trim(),
          })
        });
        const out = await res.json();
        if (!res.ok) throw new Error(out.error || 'Something went wrong — try again.');
        if (out.gateway === 'khalti') {
          location.href = out.payment_url;
        } else {
          const form = document.createElement('form');
          form.method = 'POST'; form.action = out.action_url;
          Object.keys(out.fields).forEach(k => {
            const input = document.createElement('input');
            input.type = 'hidden'; input.name = k; input.value = out.fields[k];
            form.appendChild(input);
          });
          document.body.appendChild(form);
          form.submit();
        }
      } catch (e) {
        msgEl.textContent = e.message;
        goBtn.disabled = false; goBtn.textContent = 'Continue to payment';
      }
    });
  }

  function renderServices(services) {
    const holder = document.getElementById('service-grid');
    if (!holder) return;
    if (!services.length) {
      holder.innerHTML = '<div class="service-empty">Services will appear here once the firm publishes them in Admin.</div>';
      return;
    }
    holder.innerHTML = services.map(s =>
      '<div class="service-card">' +
      (s.category_name ? '<div class="sc-cat">' + esc(s.category_name) + '</div>' : '') +
      '<h4><a href="/services/' + esc(s.slug) + '" class="sc-title-link">' + esc(s.name) + '</a></h4>' +
      '<p>' + esc(s.summary || '') + '</p>' +
      '<div class="sc-price">' + esc(priceLabel(s)) + '</div>' +
      '<div class="sc-actions">' +
      '<a href="/services/' + esc(s.slug) + '" class="btn btn-small btn-outline">Learn more</a> ' +
      (s.is_buyable && s.price
        ? '<button type="button" class="btn btn-small btn-primary" data-buy="' + esc(s.slug) + '" data-buy-name="' + esc(s.name) + '">Buy now</button>'
        : '<button type="button" class="btn btn-small btn-primary" data-scroll="book-appointment">Book a call</button>') +
      '</div></div>').join('');
    holder.querySelectorAll('[data-buy]').forEach(btn => {
      btn.addEventListener('click', () => openCheckout(btn.dataset.buy, btn.dataset.buyName));
    });
    holder.querySelectorAll('[data-scroll]').forEach(btn => {
      btn.addEventListener('click', () => {
        const el = document.getElementById(btn.getAttribute('data-scroll'));
        if (el) el.scrollIntoView({ behavior: REDUCED ? 'auto' : 'smooth' });
      });
    });
  }

  function renderTestimonials(testimonials) {
    const section = document.getElementById('testimonials-section');
    const holder = document.getElementById('testi-grid');
    if (!section || !holder) return;
    if (!testimonials.length) { section.hidden = true; return; }
    section.hidden = false;
    holder.innerHTML = testimonials.map(t =>
      '<div class="testi-card">' +
      (t.rating ? '<div class="tc-rating">' + '★'.repeat(Math.max(0, Math.min(5, t.rating))) + '</div>' : '') +
      '<p>&ldquo;' + esc(t.content) + '&rdquo;</p>' +
      '<div class="tc-name">' + esc(t.client_name) + '</div>' +
      (t.client_title || t.company ? '<div class="tc-role">' + esc([t.client_title, t.company].filter(Boolean).join(', ')) + '</div>' : '') +
      '</div>').join('');
  }

  // "Our Clients" — CA firms that use CA Automator, shown by name+logo only
  // (never contact/registration details — see the "Our Clients" comment on
  // /api/public/site's showcased_firms). Each firm opts in via Platform
  // Admin's per-firm "Show on landing page" toggle; nothing appears here
  // by default. The strip is duplicated once so the CSS marquee
  // (style.css .clients-track) can loop seamlessly at -50%.
  function renderOurClients(firms) {
    const section = document.getElementById('our-clients-section');
    const track = document.getElementById('clients-track');
    if (!section || !track) return;
    if (!firms.length) { section.hidden = true; return; }
    section.hidden = false;
    const tiles = firms.map(f =>
      '<div class="client-tile" title="' + esc(f.name) + '"><img src="' + esc(f.logo_path) + '" alt="' + esc(f.name) + '"></div>'
    ).join('');
    // Two identical groups back to back — the CSS animation scrolls exactly
    // one group's width so the loop is seamless. Under reduced motion the
    // second (purely decorative) group is hidden — see style.css.
    track.innerHTML = '<div class="clients-group">' + tiles + '</div><div class="clients-group" aria-hidden="true">' + tiles + '</div>';
  }

  function renderContactInfo(firm) {
    const holder = document.getElementById('contact-info');
    if (!holder) return;
    const rows = [];
    if (firm.address) rows.push(['Address', esc(firm.address).replace(/\n/g, '<br>')]);
    if (firm.phone) rows.push(['Phone', '<a href="tel:' + esc(firm.phone) + '">' + esc(firm.phone) + '</a>']);
    if (firm.email) rows.push(['Email', '<a href="mailto:' + esc(firm.email) + '">' + esc(firm.email) + '</a>']);
    if (firm.office_hours) rows.push(['Hours', esc(firm.office_hours)]);
    const social = ['facebook_url', 'linkedin_url', 'twitter_url', 'instagram_url']
      .map(k => ({ key: k.replace('_url', ''), url: firm[k] })).filter(s => s.url);

    holder.innerHTML =
      (rows.length ? rows.map(r => '<div class="ci-row"><div class="ci-label">' + esc(r[0]) + '</div><div class="ci-value">' + r[1] + '</div></div>').join('')
        : '<div class="contact-info-loading">Contact details will appear here once the firm sets them up in Admin.</div>') +
      (firm.google_maps_url ? '<div class="ci-row"><div class="ci-label">Maps</div><div class="ci-value"><a href="' +
        esc(firm.google_maps_url) + '" target="_blank" rel="noopener">Get directions →</a></div></div>' : '') +
      (social.length ? '<div class="ci-social">' + social.map(s =>
        '<a href="' + esc(s.url) + '" target="_blank" rel="noopener">' + esc(s.key.charAt(0).toUpperCase() + s.key.slice(1)) + '</a>').join('') + '</div>' : '') +
      (firm.google_maps_embed ? '<div class="map-embed"><iframe src="' + esc(firm.google_maps_embed) +
        '" loading="lazy" referrerpolicy="no-referrer-when-downgrade" title="Office location"></iframe></div>' : '');
  }

  function renderBlogPreview(posts) {
    const section = document.getElementById('blog-preview-section');
    const holder = document.getElementById('blog-preview-grid');
    if (!section || !holder) return;
    if (!posts.length) { section.hidden = true; return; }
    section.hidden = false;
    holder.innerHTML = posts.map(p =>
      '<a class="service-card" href="/blog/' + esc(p.slug) + '" style="text-decoration:none">' +
      (p.og_image_path ? '<img class="sc-img" src="' + esc(p.og_image_path) + '" alt="" loading="lazy">' : '') +
      (p.category_name ? '<div class="sc-cat">' + esc(p.category_name) + '</div>' : '') +
      '<h4>' + esc(p.h1 || p.title) + '</h4>' +
      '<p>' + esc(p.meta_description || '') + '</p>' +
      '<div class="sc-price">' + esc((p.published_at || '').slice(0, 10)) +
      (p.reading_minutes ? ' · ' + p.reading_minutes + ' min read' : '') + '</div>' +
      '</a>').join('');
  }

  function renderCareersPreview(jobs) {
    const holder = document.getElementById('careers-preview-grid');
    if (!holder) return;
    if (!jobs.length) { holder.innerHTML = ''; return; }
    holder.innerHTML = jobs.slice(0, 3).map(j =>
      '<a class="service-card" href="/careers/' + esc(j.slug) + '" style="text-decoration:none">' +
      '<h4>' + esc(j.title) + '</h4>' +
      '<p>' + esc([j.department, j.location].filter(Boolean).join(' · ')) + '</p>' +
      '<div class="sc-price">' + esc((j.employment_type || '').replace(/_/g, ' ')) + '</div>' +
      '</a>').join('');
  }

  // ── YouTube videos — the whole section stays hidden with none published.
  // Real <iframe> embeds (loading="lazy" so a page with several videos
  // doesn't initialize every player at once), never a fake "coming soon"
  // placeholder.
  function renderVideos(videos) {
    const section = document.getElementById('videos-section');
    const holder = document.getElementById('video-grid');
    if (!section || !holder) return;
    if (!videos.length) { section.hidden = true; return; }
    section.hidden = false;
    holder.innerHTML = videos.map(v =>
      '<div class="video-card">' +
      '<div class="video-embed"><iframe src="https://www.youtube-nocookie.com/embed/' + esc(v.video_id) +
      '" title="' + esc(v.title) + '" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>' +
      '<h4>' + esc(v.title) + '</h4>' +
      '</div>').join('');
  }

  function initPublicSite() {
    if (!document.getElementById('service-grid') && !document.getElementById('contact-info')) return;
    fetch('/api/public/site').then(r => r.json()).then(data => {
      renderServices(data.services || []);
      renderTestimonials(data.testimonials || []);
      renderOurClients(data.showcased_firms || []);
      renderContactInfo(data.firm || {});
      // A service detail page's "Buy" button links here as /?buy=<slug> —
      // that page has no checkout UI of its own (it isn't part of this
      // app's own script), so this reopens the same modal the services
      // grid itself uses, pre-selected to the service the visitor came from.
      const buySlug = new URLSearchParams(location.search).get('buy');
      if (buySlug) {
        const svc = (data.services || []).find(s => s.slug === buySlug);
        if (svc && svc.is_buyable && svc.price) openCheckout(svc.slug, svc.name);
      }
    }).catch(() => {
      renderServices([]);
      renderContactInfo({});
    });
    fetch('/api/public/blog?limit=3').then(r => r.json()).then(renderBlogPreview).catch(() => renderBlogPreview([]));
    fetch('/api/public/careers?limit=3').then(r => r.json()).then(renderCareersPreview).catch(() => renderCareersPreview([]));
    fetch('/api/public/youtube-videos').then(r => r.json()).then(renderVideos).catch(() => renderVideos([]));
  }

  // ── About: the signature scroll-driven timeline + team grid. Real data
  // only — both sections stay hidden entirely if the firm hasn't published
  // any milestones/team members yet, rather than showing an empty-looking
  // "About" section with nothing in it.
  function renderTimeline(milestones) {
    const wrap = document.getElementById('timeline-wrap');
    const holder = document.getElementById('timeline-items');
    if (!wrap || !holder) return;
    if (!milestones.length) { wrap.hidden = true; return; }
    wrap.hidden = false;
    holder.innerHTML = milestones.map((m, i) =>
      '<div class="timeline-item" data-ti="' + i + '">' +
      '<div class="ti-year">' + esc(m.year) + '</div>' +
      '<h4>' + esc(m.title) + '</h4>' +
      (m.description ? '<p>' + esc(m.description) + '</p>' : '') +
      (m.image_path ? '<img src="' + esc(m.image_path) + '" alt="' + esc(m.title) + '" loading="lazy">' : '') +
      '</div>').join('');

    const items = Array.from(holder.querySelectorAll('.timeline-item'));
    const fill = document.getElementById('timeline-fill');
    if (REDUCED || !window.gsap || !window.ScrollTrigger) {
      // No motion preference, or GSAP failed to load from the CDN (offline,
      // blocked, etc.) — the timeline still has to be fully readable, just
      // without the scroll-drawn line. Every item shown active immediately.
      items.forEach(el => el.classList.add('on'));
      if (fill) fill.style.height = '100%';
      return;
    }
    gsap.registerPlugin(ScrollTrigger);
    gsap.to(fill, {
      height: '100%', ease: 'none',
      scrollTrigger: { trigger: wrap, start: 'top 75%', end: 'bottom 60%', scrub: 0.6 },
    });
    items.forEach(el => {
      ScrollTrigger.create({
        trigger: el, start: 'top 80%', end: 'bottom 40%',
        onEnter: () => el.classList.add('on'),
        onEnterBack: () => el.classList.add('on'),
      });
    });
  }

  // Brief only — name, role, a short bio preview and top expertise tags.
  // The card links through to /team/<slug> (server/public_pages.py
  // team_detail_page) for the full bio and LinkedIn link, rather than
  // dumping everything into the front page's grid.
  function renderTeam(team) {
    const section = document.getElementById('team-section');
    const holder = document.getElementById('team-grid');
    if (!section || !holder) return;
    if (!team.length) { section.hidden = true; return; }
    section.hidden = false;
    holder.innerHTML = team.map(t => {
      const initials = (t.name || '').split(/\s+/).filter(Boolean).slice(0, 2).map(w => w[0].toUpperCase()).join('');
      const expertise = (t.expertise || '').split(',').map(s => s.trim()).filter(Boolean).slice(0, 3);
      const bioPreview = t.bio && t.bio.length > 110 ? t.bio.slice(0, 110).trim() + '…' : t.bio;
      const href = t.slug ? '/team/' + esc(t.slug) : null;
      const openTag = href ? '<a class="team-card" href="' + href + '">' : '<div class="team-card">';
      const closeTag = href ? '</a>' : '</div>';
      return openTag +
        (t.photo_path ? '<img class="team-photo" src="' + esc(t.photo_path) + '" alt="' + esc(t.name) + '">'
          : '<div class="team-photo-fallback">' + esc(initials) + '</div>') +
        '<h4>' + esc(t.name) + '</h4>' +
        (t.designation ? '<div class="tc-role">' + esc(t.designation) + '</div>' : '') +
        (bioPreview ? '<p>' + esc(bioPreview) + '</p>' : '') +
        (expertise.length ? '<div class="tc-expertise">' + expertise.map(x => '<span>' + esc(x) + '</span>').join('') + '</div>' : '') +
        (href ? '<span class="tc-more">View profile →</span>' : '') +
        closeTag;
    }).join('');
  }

  function initAbout() {
    if (!document.getElementById('timeline-wrap')) return;
    fetch('/api/public/about').then(r => r.json()).then(data => {
      renderTimeline(data.milestones || []);
      renderTeam(data.team || []);
    }).catch(() => { renderTimeline([]); renderTeam([]); });
  }

  // ── Cookie consent — a plain accept/decline choice, remembered in
  // localStorage so it only ever asks once per browser. Neither choice
  // blocks using the site; "Decline" just records the visitor's
  // preference (this app sets no third-party tracking cookies today —
  // the bar exists so that's a stated choice, not an assumption).
  const COOKIE_CONSENT_KEY = 'ca_cookie_consent';
  // ── Admin-managed site menu (Admin > Site Settings > Menu) — replaces
  // the top nav and footer "Resources" links with whatever's configured
  // there, falling back to the static links already in the HTML if the
  // fetch fails or nothing's configured yet (so the nav is never blank).
  // initNav()/initScrollNav() ran once already at boot against the static
  // fallback links; re-running them here re-collects the real ones so
  // scroll-spy highlighting and smooth-scroll clicks both still work.
  function initMenu() {
    fetch('/api/public/menu').then(r => r.json()).then(menu => {
      const navHolder = document.getElementById('lnav-links');
      if (navHolder && menu.primary && menu.primary.length) {
        navHolder.innerHTML = menu.primary.map(item =>
          '<a href="' + esc(item.url) + '"' + (item.url.charAt(0) === '#' ? ' data-nav' : '') + '>' + esc(item.label) + '</a>'
        ).join('');
      }
      const footerCol = document.getElementById('lfooter-resources-col');
      if (footerCol && menu.footer && menu.footer.length) {
        footerCol.querySelectorAll('a').forEach(a => a.remove());
        const h5 = footerCol.querySelector('h5');
        const linksHtml = menu.footer.map(item => '<a href="' + esc(item.url) + '">' + esc(item.label) + '</a>').join('');
        if (h5) h5.insertAdjacentHTML('afterend', linksHtml);
      }
      initNav();
      initScrollNav();
    }).catch(() => { /* static fallback links already in the HTML keep working */ });
  }

  function initCookieConsent() {
    const bar = document.getElementById('cookie-bar');
    if (!bar) return;
    if (localStorage.getItem(COOKIE_CONSENT_KEY)) return;
    bar.hidden = false;
    function decide(value) {
      try { localStorage.setItem(COOKIE_CONSENT_KEY, value); } catch (e) { /* private browsing etc. */ }
      bar.hidden = true;
    }
    document.getElementById('cookie-accept').addEventListener('click', () => decide('accepted'));
    document.getElementById('cookie-decline').addEventListener('click', () => decide('declined'));
  }

  // ── Popup announcement banner — stays hidden entirely with nothing
  // configured or the admin toggle off (GET /api/public/banner returns
  // null). Three ways to close, all equivalent: the × button, the Skip
  // button, or clicking the dimmed overlay outside the modal — plus an
  // optional auto-dismiss timer set in seconds from the admin panel.
  function initBanner() {
    const overlay = document.getElementById('banner-overlay');
    if (!overlay) return;
    fetch('/api/public/banner').then(r => r.json()).then(cfg => {
      if (!cfg || (!cfg.title && !cfg.message && !cfg.image_path)) return;
      const titleEl = document.getElementById('banner-title');
      const msgEl = document.getElementById('banner-message');
      const imgEl = document.getElementById('banner-image');
      const linkEl = document.getElementById('banner-link');
      titleEl.textContent = cfg.title || ''; titleEl.hidden = !cfg.title;
      msgEl.textContent = cfg.message || ''; msgEl.hidden = !cfg.message;
      if (cfg.image_path) { imgEl.src = cfg.image_path; imgEl.hidden = false; } else { imgEl.hidden = true; }
      if (cfg.link_url) { linkEl.href = cfg.link_url; linkEl.textContent = cfg.link_label || 'Learn more'; linkEl.hidden = false; }
      else { linkEl.hidden = true; }

      let timer = null;
      function close() {
        overlay.hidden = true;
        if (timer) clearTimeout(timer);
      }
      document.getElementById('banner-close').addEventListener('click', close);
      document.getElementById('banner-skip').addEventListener('click', close);
      overlay.addEventListener('click', e => { if (e.target === overlay) close(); });
      overlay.hidden = false;
      if (cfg.display_seconds > 0) timer = setTimeout(close, cfg.display_seconds * 1000);
    }).catch(() => {});
  }

  // ── Footer "Legal" links — whatever static pages (Privacy Policy, Terms,
  // ...) exist in Admin > Site Settings > Pages, with no hardcoded slug
  // list here: a page created in Admin shows up in the footer on its own,
  // and the footer shows nothing extra if none have been published yet.
  function initFooterPages() {
    const holder = document.getElementById('lfooter-pages');
    if (!holder) return;
    fetch('/api/public/pages').then(r => r.json()).then(pages => {
      holder.innerHTML = pages.map(p => '<a href="/page/' + esc(p.slug) + '">' + esc(p.title) + '</a>').join('');
    }).catch(() => {});
  }

  function initContactForm() {
    const form = document.getElementById('contact-form');
    if (!form) return;
    const msgEl = document.getElementById('ct-msg');
    const goBtn = document.getElementById('ct-go');
    form.addEventListener('submit', async e => {
      e.preventDefault();
      msgEl.textContent = '';
      const name = document.getElementById('ct-name').value.trim();
      const email = document.getElementById('ct-email').value.trim();
      const message = document.getElementById('ct-message').value.trim();
      if (!name || !email || !message) { msgEl.textContent = 'Name, email and message are required.'; return; }
      goBtn.disabled = true; goBtn.textContent = 'Sending…';
      try {
        const res = await fetch('/api/contact', {
          method: 'POST', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            name, email, message,
            phone: document.getElementById('ct-phone').value.trim(),
            subject: document.getElementById('ct-subject').value.trim(),
          })
        });
        const out = await res.json();
        if (!res.ok) throw new Error(out.error || 'Something went wrong — try again.');
        form.innerHTML = '<div class="appt-confirm"><b>Message sent</b><br>Thanks — we\'ll get back to you shortly.</div>';
      } catch (err) {
        msgEl.textContent = err.message;
        goBtn.disabled = false; goBtn.textContent = 'Send message';
      }
    });
  }

  document.addEventListener('DOMContentLoaded', () => {
    initNav();
    initHeroEco();
    initDataFlowStory();
    initEcosystemMap();
    initWorkflowStages();
    initJurisdictionTabs();
    initModuleGroups();
    initBenefitGrid();
    initFinalDash();
    initCounters();
    initReveal();
    initAuth();
    initScrollNav();
    initAppointments();
    initFooterYear();
    initPublicSite();
    initAbout();
    initFooterPages();
    initMenu();
    initCookieConsent();
    initBanner();
    initContactForm();
    initCheckout();
    const paymentResult = new URLSearchParams(location.search).get('payment');
    if (paymentResult) {
      // app.js owns the real toast() function, but it's scoped inside app.js's
      // own IIFE — this page never loads app.js, so this writes to the same
      // shared #toast element/CSS directly instead of calling a function that
      // doesn't exist in this file's scope.
      const t = document.getElementById('toast');
      if (t) {
        t.textContent = paymentResult === 'success' ? 'Payment received — thank you!' : 'Payment was not completed — nothing was charged.';
        t.className = 'toast' + (paymentResult === 'success' ? '' : ' bad');
        t.hidden = false;
        setTimeout(() => { t.hidden = true; }, 4000);
      }
    }
  });
})();
