/* ============================================================================
   CA Automator — application layer.

   Offline model
     Every keystroke lands in IndexedDB within a second. Nothing waits on the
     network. Every five minutes a heartbeat pushes whatever is dirty to the
     server; if there's no connection the beat is queued and the dial shows it
     hollow instead of filled. On reconnect the whole queue goes up in one batch,
     in sequence order, and the server parks anything that conflicts rather than
     overwriting it.
   ========================================================================= */
(function () {
  'use strict';

  const AUTOSAVE_MS = 300000;      // the five-minute beat
  const LOCAL_DEBOUNCE_MS = 800;   // local write after typing stops
  const DB_NAME = 'ca-automator';
  const DB_VERSION = 1;

  // Fiscal-year order, not calendar order — Nepal's income year starts on
  // Shrawan 1, so bs_month 1 here must be Shrawan to line up with
  // engines.TRANCHE_BY_MONTH (and its Python mirror), both keyed the same way.
  const NEPALI_MONTHS = ['Shrawan','Bhadra','Ashwin','Kartik','Mangsir','Poush',
                         'Magh','Falgun','Chaitra','Baishakh','Jestha','Ashadh'];

  const state = {
    token: localStorage.getItem('ca.token') || null,
    user: JSON.parse(localStorage.getItem('ca.user') || 'null'),
    deviceId: localStorage.getItem('ca.device') || null,
    boot: null,
    rates: [],
    // No hardcoded fiscal year here — a fresh browser (nothing in localStorage
    // yet) gets today's actual FY via the same July-16 heuristic the server
    // falls back on, not a string that goes stale every Shrawan. Once
    // state.boot loads, checkFyCurrency() compares this against the server's
    // authoritative current_fy and offers a switch rather than silently
    // overriding — a user reviewing a prior year's data mid-session shouldn't
    // get yanked out of it.
    fy: localStorage.getItem('ca.fy') || CAEngines.fyLabelForAd(new Date().toISOString().slice(0, 10)),
    files: {},            // localKey -> file record
    dirty: new Set(),
    beats: [],            // {t: Date, synced: bool}
    online: navigator.onLine,
    route: location.hash.replace('#/', '') || 'desk',
    lastLocalSave: null,
    offlineMode: false
  };

  if (!state.deviceId) {
    state.deviceId = 'dev-' + Math.random().toString(36).slice(2, 10);
    localStorage.setItem('ca.device', state.deviceId);
  }

  // ─────────────────────────────────────────────────────────── IndexedDB
  let dbp = null;
  function db() {
    if (dbp) return dbp;
    dbp = new Promise((res, rej) => {
      const req = indexedDB.open(DB_NAME, DB_VERSION);
      req.onupgradeneeded = () => {
        const d = req.result;
        if (!d.objectStoreNames.contains('files')) d.createObjectStore('files', { keyPath: 'localKey' });
        if (!d.objectStoreNames.contains('kv')) d.createObjectStore('kv');
        if (!d.objectStoreNames.contains('beats')) d.createObjectStore('beats', { keyPath: 'id', autoIncrement: true });
      };
      req.onsuccess = () => res(req.result);
      req.onerror = () => rej(req.error);
    });
    return dbp;
  }
  async function idbPut(store, value, key) {
    const d = await db();
    return new Promise((res, rej) => {
      const tx = d.transaction(store, 'readwrite');
      const r = key !== undefined ? tx.objectStore(store).put(value, key) : tx.objectStore(store).put(value);
      r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error);
    });
  }
  async function idbGet(store, key) {
    const d = await db();
    return new Promise((res, rej) => {
      const r = d.transaction(store, 'readonly').objectStore(store).get(key);
      r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error);
    });
  }
  async function idbAll(store) {
    const d = await db();
    return new Promise((res, rej) => {
      const r = d.transaction(store, 'readonly').objectStore(store).getAll();
      r.onsuccess = () => res(r.result || []); r.onerror = () => rej(r.error);
    });
  }

  // ─────────────────────────────────────────────────────────── networking
  async function api(path, opts) {
    opts = opts || {};
    const headers = Object.assign({ 'Content-Type': 'application/json', 'X-Device-Id': state.deviceId }, opts.headers || {});
    if (state.token) headers.Authorization = 'Bearer ' + state.token;
    const res = await fetch(path, {
      method: opts.method || 'GET', headers,
      body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined
    });
    if (res.status === 401) { signOut(true); throw new Error('Session expired'); }
    const text = await res.text();
    let json; try { json = text ? JSON.parse(text) : null; } catch (e) { json = null; }
    if (!res.ok) throw new Error((json && json.error) || ('Request failed: ' + res.status));
    return json;
  }

  // ─────────────────────────────────────────────────────────── formatting
  const money = n => (n === null || n === undefined || n === '' || isNaN(n)) ? '–'
    : Number(n).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  const pct = n => (Number(n) * 100).toFixed(2) + '%';
  const esc = s => String(s === null || s === undefined ? '' : s)
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  const hhmm = d => String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');

  // Shared across every module that takes a currency (FDI investors, TP
  // transactions, etc.) — a picked value can't be mistyped the way a free
  // text field can. NPR first since this is a Nepal CA practice; the rest
  // cover the jurisdictions this app already deals in (India, UAE) plus the
  // other currencies most likely to show up in a cross-border investment.
  const WORLD_CURRENCIES = ['NPR', 'USD', 'INR', 'EUR', 'GBP', 'AED', 'CNY', 'JPY', 'SGD', 'HKD', 'AUD', 'CAD',
    'CHF', 'SAR', 'QAR', 'KWD', 'BHD', 'OMR', 'THB', 'MYR', 'IDR', 'PHP', 'VND', 'KRW', 'BDT', 'PKR', 'LKR', 'ZAR'];
  function currencyOptions(selected) {
    const list = (selected && WORLD_CURRENCIES.indexOf(selected) === -1) ? [selected].concat(WORLD_CURRENCIES) : WORLD_CURRENCIES;
    return list.map(c => '<option value="' + esc(c) + '"' + (c === selected ? ' selected' : '') + '>' + esc(c) + '</option>').join('');
  }

  function toast(msg, bad) {
    const t = document.getElementById('toast');
    t.textContent = msg; t.className = 'toast' + (bad ? ' bad' : ''); t.hidden = false;
    clearTimeout(toast._t); toast._t = setTimeout(() => { t.hidden = true; }, 3200);
  }

  // ──────────────────────────────────────────────────────── alerts (bell)
  // Raised by notify.sweep_deadlines() server-side (deadline reminder points
  // and overdue items) — this is just the read/display side. "Sweep now"
  // lets a firm without cron or the night worker running raise today's
  // reminders on demand.
  const notifState = { items: [], unread: 0, panelOpen: false };

  async function refreshNotifications() {
    if (!state.online || !state.token || state.offlineMode) return;
    try {
      notifState.items = await api('/api/notifications');
      notifState.unread = notifState.items.filter(r => !r.read_at).length;
      renderNotifBadge();
      if (notifState.panelOpen) renderNotifPanel();
    } catch (e) { /* the bell isn't critical path — fail quietly */ }
  }

  function renderNotifBadge() {
    const badge = document.getElementById('notif-badge');
    if (!badge) return;
    if (notifState.unread) { badge.hidden = false; badge.textContent = notifState.unread; }
    else badge.hidden = true;
  }

  function renderNotifPanel() {
    const panel = document.getElementById('notif-panel');
    if (!panel) return;
    let h = '<div class="notif-head"><b>Alerts</b><div>' +
      '<button class="btn btn-small" id="notif-sweep">Sweep now</button> ' +
      '<button class="btn btn-small" id="notif-mark-all">Mark all read</button></div></div>';
    h += notifState.items.length
      ? notifState.items.map(n => '<div class="notif-item' + (n.read_at ? '' : ' unread') + '" data-notif="' + n.id + '">' +
          '<b>' + esc(n.title) + '</b><span>' + esc(n.body || '') + '</span></div>').join('')
      : '<div class="notif-empty">No alerts.</div>';
    panel.innerHTML = h;

    panel.querySelectorAll('[data-notif]').forEach(el => el.addEventListener('click', async e => {
      const id = e.currentTarget.dataset.notif;
      const item = notifState.items.find(x => x.id == id);
      if (!item) return;
      if (item.link) location.hash = item.link.includes('#') ? item.link.slice(item.link.indexOf('#')) : item.link;
      if (!item.read_at) {
        try {
          await api('/api/notifications/' + id, { method: 'PUT' });
          item.read_at = new Date().toISOString();
          notifState.unread = notifState.items.filter(x => !x.read_at).length;
          renderNotifBadge(); renderNotifPanel();
        } catch (err) { /* ignore */ }
      }
    }));
    const sweepBtn = panel.querySelector('#notif-sweep');
    if (sweepBtn) sweepBtn.addEventListener('click', async () => {
      sweepBtn.textContent = 'Sweeping…'; sweepBtn.disabled = true;
      try {
        const r = await api('/api/notifications/sweep', { method: 'POST' });
        toast(r.raised ? ('Raised ' + r.raised + ' alert' + (r.raised > 1 ? 's' : ''))
                       + (r.email_configured ? '' : ' (email not configured — in-app only)') : 'Nothing new due');
        await refreshNotifications();
      } catch (err) { toast(err.message, true); }
      sweepBtn.textContent = 'Sweep now'; sweepBtn.disabled = false;
    });
    const markAllBtn = panel.querySelector('#notif-mark-all');
    if (markAllBtn) markAllBtn.addEventListener('click', async () => {
      try {
        await api('/api/notifications/mark-all-read', { method: 'POST' });
        notifState.items.forEach(x => { x.read_at = x.read_at || new Date().toISOString(); });
        notifState.unread = 0;
        renderNotifBadge(); renderNotifPanel();
      } catch (err) { toast(err.message, true); }
    });
  }

  // ─────────────────────────────────────────────────────── the shift dial
  function renderDial() {
    const now = new Date();
    const frac = (now.getHours() * 60 + now.getMinutes()) / 1440;

    const arc = document.getElementById('dial-day');
    const len = arc.getTotalLength();
    arc.style.strokeDasharray = (len * frac) + ' ' + len;

    document.getElementById('dial-hand').style.transform = 'rotate(' + (-90 + frac * 180) + 'deg)';

    const today = now.toDateString();
    const beats = state.beats.filter(b => b.t.toDateString() === today);
    const pips = beats.slice(-40).map(b => {
      const f = (b.t.getHours() * 60 + b.t.getMinutes()) / 1440;
      const ang = Math.PI * (1 - f);
      const x = 66 + 58 * Math.cos(ang), y = 70 - 58 * Math.sin(ang);
      return '<circle class="' + (b.synced ? 'pip' : 'pip-queued') + '" cx="' + x.toFixed(1) +
             '" cy="' + y.toFixed(1) + '" r="2.4"/>';
    }).join('');
    document.getElementById('dial-pips').innerHTML = pips;

    const queued = state.dirty.size;
    document.getElementById('dial-count').textContent = queued ? queued : beats.length;
    document.getElementById('dial-label').textContent = queued
      ? (queued === 1 ? 'file waiting' : 'files waiting')
      : (beats.length === 1 ? 'beat today' : 'beats today');

    const netChip = document.getElementById('chip-net');
    netChip.className = 'chip' + (state.online ? '' : ' off');
    netChip.querySelector('span').textContent = state.online ? (state.offlineMode ? 'Local only' : 'Online') : 'Offline';

    const saveChip = document.getElementById('chip-save');
    saveChip.textContent = state.lastLocalSave ? 'Saved ' + hhmm(state.lastLocalSave) : 'Not saved yet';
  }

  // ────────────────────────────────────────────────── workfile store & save
  function fileKey(moduleKey, clientId, fy) { return moduleKey + ':' + (clientId || 0) + ':' + fy; }

  async function loadFiles() {
    const rows = await idbAll('files');
    rows.forEach(r => { state.files[r.localKey] = r; if (r.dirty) state.dirty.add(r.localKey); });
  }

  function getFile(moduleKey, clientId, fy, seed) {
    const key = fileKey(moduleKey, clientId, fy);
    if (!state.files[key]) {
      const payload = seed || {};
      // Every module's own "entity"/"client" sub-object starts blank even
      // though the workfile is already scoped to a picked client — pre-fill
      // it here once, centrally, at creation time, instead of asking the
      // preparer to retype the name they just selected. It stays a plain
      // editable field afterwards (a trading name can legitimately differ
      // from the CRM record), and this never overwrites an existing value.
      const nameHolder = payload.entity || payload.client;
      if (nameHolder && typeof nameHolder === 'object' && !nameHolder.name && clientId) {
        nameHolder.name = clientNameOf(clientId);
      }
      state.files[key] = {
        localKey: key, serverId: null, module: moduleKey, clientId: clientId || null,
        clientName: clientNameOf(clientId), fy, payload, version: 0,
        dirty: false, seq: 0, updatedAt: new Date().toISOString()
      };
    }
    return state.files[key];
  }

  let localTimer = null;
  function touch(file) {
    file.dirty = true; file.seq = (file.seq || 0) + 1; file.updatedAt = new Date().toISOString();
    state.dirty.add(file.localKey);
    clearTimeout(localTimer);
    localTimer = setTimeout(async () => {
      await idbPut('files', JSON.parse(JSON.stringify(file)));
      state.lastLocalSave = new Date();
      renderDial();
    }, LOCAL_DEBOUNCE_MS);
  }

  async function saveAllLocal() {
    for (const key of Object.keys(state.files)) {
      await idbPut('files', JSON.parse(JSON.stringify(state.files[key])));
    }
    state.lastLocalSave = new Date();
    renderDial();
  }

  /** The five-minute heartbeat. */
  async function beat(manual) {
    await saveAllLocal();
    const dirtyKeys = Array.from(state.dirty);
    if (!dirtyKeys.length) {
      if (manual) toast('Everything on this device is already on the server');
      state.beats.push({ t: new Date(), synced: true });
      renderDial();
      return;
    }
    if (!state.online || state.offlineMode || !state.token) {
      state.beats.push({ t: new Date(), synced: false });
      renderDial();
      if (manual) toast('No connection — your work is saved on this device and queued', true);
      return;
    }

    try {
      // Files created offline need a server workfile before their state can go up.
      for (const key of dirtyKeys) {
        const f = state.files[key];
        if (!f.serverId && f.engagementId) {
          const wf = await api('/api/workfiles', { method: 'POST', body: {
            engagement_id: f.engagementId, module_key: f.module,
            title: MODULES[f.module].name + ' — ' + f.fy, payload: f.payload } });
          f.serverId = wf.id; f.version = wf.version || 1;
        }
      }
      const beats = dirtyKeys.map(k => state.files[k]).filter(f => f.serverId).map(f => ({
        workfile_id: f.serverId, payload: f.payload, base_version: f.version, client_seq: f.seq
      }));
      if (beats.length) {
        const res = await api('/api/sync', { method: 'POST', body: { beats } });
        let conflicts = 0;
        res.results.forEach(r => {
          const f = Object.values(state.files).find(x => x.serverId === r.workfile_id);
          if (!f) return;
          if (r.conflict) { conflicts++; f.conflict = true; }
          else { f.version = r.version || f.version; f.dirty = false; state.dirty.delete(f.localKey); }
        });
        if (conflicts) {
          toast(conflicts + ' file(s) were changed elsewhere — open Sync to compare', true);
        }
      }
      // Files with no engagement yet stay local until one is chosen.
      Array.from(state.dirty).forEach(k => { if (!state.files[k].engagementId) state.dirty.delete(k); });
      await saveAllLocal();
      state.beats.push({ t: new Date(), synced: true });
      renderDial();
      if (manual) toast('Synced');
    } catch (e) {
      state.beats.push({ t: new Date(), synced: false });
      renderDial();
      if (manual) toast(e.message, true);
    }
  }

  // ─────────────────────────────────────────────────────── offline export
  function csvEscape(v) {
    const s = String(v === null || v === undefined ? '' : v);
    return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
  }
  function downloadBlob(name, blob) {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = name; document.body.appendChild(a); a.click();
    setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 500);
  }
  function exportCsv(name, headers, rows) {
    const body = [headers.map(csvEscape).join(',')]
      .concat(rows.map(r => r.map(csvEscape).join(','))).join('\r\n');
    downloadBlob(name + '.csv', new Blob(['\ufeff' + body], { type: 'text/csv;charset=utf-8' }));
  }
  /** tables: [{title, headers, rows}, ...] \u2014 every table in one CSV, each as
      its own titled section (a blank line, then the title, then that
      table's own header row and data), since a CSV can't hold multiple
      sheets the way the real workbook does. */
  function exportCsvMulti(name, tables) {
    const sections = (tables || []).map(t =>
      (t.title ? csvEscape(t.title) + '\r\n' : '') +
      [t.headers.map(csvEscape).join(',')].concat((t.rows || []).map(r => r.map(csvEscape).join(','))).join('\r\n'));
    const body = sections.join('\r\n\r\n');
    downloadBlob(name + '.csv', new Blob(['\ufeff' + body], { type: 'text/csv;charset=utf-8' }));
  }
  function exportWordHtml(name, title, meta, tables) {
    const style = 'body{font-family:Arial,sans-serif;font-size:10pt}h1{font-size:14pt}h2{font-size:11pt}' +
      'table{border-collapse:collapse;width:100%}td,th{border:1px solid #999;padding:4px 6px;font-size:9pt}' +
      'th{background:#eee;text-align:left}.num{text-align:right}.warn{color:#DC3A3A;font-weight:bold}';
    const t = tables.map(tb => '<h2>' + esc(tb.title || '') + '</h2><table><tr>' +
      tb.headers.map(h => '<th>' + esc(h) + '</th>').join('') + '</tr>' +
      tb.rows.map(r => '<tr>' + r.map(c => '<td class="' + (typeof c === 'number' ? 'num' : '') + '">' +
        (typeof c === 'number' ? money(c) : esc(c)) + '</td>').join('') + '</tr>').join('') + '</table>').join('');
    const html = '<html xmlns:w="urn:schemas-microsoft-com:office:word"><head><meta charset="utf-8">' +
      '<title>' + esc(title) + '</title><style>' + style + '</style></head><body>' +
      '<h1>' + esc(meta.firm || '') + '</h1><p><b>' + esc(title) + '</b><br>' + esc(meta.line || '') + '</p>' +
      (meta.warn ? '<p class="warn">' + esc(meta.warn) + '</p>' : '') + t +
      '<p style="margin-top:28px">Prepared by ______________ &nbsp;&nbsp; Reviewed by ______________</p>' +
      '</body></html>';
    downloadBlob(name + '.doc', new Blob(['\ufeff' + html], { type: 'application/msword' }));
  }

  /** Excel and Word from the server when online; CSV and Word-HTML when not. */
  async function generateReport(moduleKey, fmt, file, result) {
    const client = clientNameOf(file.clientId) || 'Client';
    const unverified = CAEngines.unverifiedOf(moduleKey, result);
    const stamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, '');
    const base = client + ' - ' + moduleKey + ' - ' + stamp;

    if (state.online && state.token && !state.offlineMode) {
      try {
        toast('Building the ' + (fmt === 'xlsx' ? 'Excel' : 'Word') + ' file…');
        const res = await api('/api/reports', { method: 'POST', body: {
          module_key: moduleKey, format: fmt, fy: file.fy, data: file.payload,
          client_name: client, workfile_id: file.serverId, engagement_id: file.engagementId } });
        const a = document.createElement('a');
        a.href = res.download_url; a.download = res.filename;
        document.body.appendChild(a); a.click(); a.remove();
        toast('Downloaded ' + res.filename);
        return;
      } catch (e) {
        toast('Server export failed — using the offline format instead', true);
      }
    }

    const view = MODULES[moduleKey].tabulate(result);
    const meta = {
      firm: (state.boot && state.boot.firm && state.boot.firm.name) || 'Your Firm',
      line: 'Client: ' + client + '   |   FY ' + file.fy + '   |   ' + new Date().toLocaleDateString(),
      warn: unverified.length ? 'Unverified rates used: ' + unverified.join(', ') +
        '. Confirm each against the Act before this working paper is issued.' : ''
    };
    if (fmt === 'xlsx') {
      // Every table tabulate() returns, not just the first — a CSV can't
      // hold multiple sheets, so each table is written as its own titled
      // section in one file rather than silently dropping everything after
      // the first (this used to lose the journal-entries table entirely for
      // any module whose report has more than one sheet).
      exportCsvMulti(base, view);
      toast('Saved as CSV — reconnect and export again for the formatted workbook');
    } else {
      exportWordHtml(base, MODULES[moduleKey].name, meta, view);
      toast('Saved as a Word-compatible file — reconnect for the full .docx');
    }
  }

  // ────────────────────────────────────────────────────────────── helpers
  const clients = () => (state.boot && state.boot.clients) || [];
  const clientNameOf = id => { const c = clients().find(c => c.id == id); return c ? c.name : ''; };

  /** Whatever depreciation.pool_<x>_rate rows exist in Rate Master, not a fixed A-E — add
      a pool by adding a rate row and it shows up here with no code change. */
  function depreciationPoolKeys() {
    const found = (state.rates || [])
      .filter(r => r.domain === 'depreciation' && /^pool_.+_rate$/.test(r.code))
      .map(r => r.code.slice(5, -5));
    return found.length ? Array.from(new Set(found)).sort() : ['a', 'b', 'c', 'd', 'e'];
  }

  function field(label, key, obj, type, opts) {
    opts = opts || {};
    const id = 'f_' + Math.random().toString(36).slice(2, 8);
    // The hint text (already authored for most fields across every module)
    // is shown below the field AND, via title=, on hover over the label or
    // the control itself — one piece of writing, two ways to see it.
    const titleAttr = opts.hint ? ' title="' + esc(opts.hint) + '"' : '';
    let control;
    if (type === 'select') {
      control = '<select id="' + id + '" data-key="' + key + '"' + titleAttr + '>' + opts.options.map(o =>
        '<option value="' + esc(o.value) + '"' + (String(obj[key]) === String(o.value) ? ' selected' : '') + '>' +
        esc(o.label) + '</option>').join('') + '</select>';
    } else if (type === 'check') {
      return '<label class="check"' + titleAttr + '><input type="checkbox" id="' + id + '" data-bind="' + key + '"' +
        (obj[key] ? ' checked' : '') + '> ' + esc(label) + '</label>';
    } else {
      control = '<input id="' + id + '" data-key="' + key + '" type="' + (type || 'text') + '" value="' + esc(obj[key] === undefined || obj[key] === null ? '' : obj[key]) + '"' +
        (opts.placeholder ? ' placeholder="' + esc(opts.placeholder) + '"' : '') + titleAttr + '>';
    }
    return '<div class="field"><label for="' + id + '"' + titleAttr + '>' + esc(label) + '</label>' + control +
      (opts.hint ? '<span class="hint">' + esc(opts.hint) + '</span>' : '') + '</div>';
  }

  // ── Shared image upload — a labelled preview + a URL field an admin can
  // either paste into directly (reusing a link they already have, or a URL
  // copied from the Media Gallery — see drawGallery()) or fill by uploading
  // a file through /api/admin/media (JPEG/PNG/WebP only, 5MB cap, checked
  // against both the browser-reported type and the file's own magic bytes
  // server-side). Either path lands the resulting URL in the same visible
  // text field with the given dataKey, so a plain
  // `box.querySelectorAll('[data-key]')` sweep on save picks it up exactly
  // like any other field. Reused by Services, Blog (cover image), About
  // (milestone image, team photo) and the Popup Banner.
  const IMAGE_ACCEPT = 'image/jpeg,image/png,image/webp';
  function imageUploadHtml(dataKey, label, imagePath, hint) {
    return '<div class="field" style="margin-top:8px"><label>' + esc(label) + '</label>' +
      '<p class="hint">' + esc(hint || 'JPEG, PNG or WebP only, up to 5MB.') + ' Paste an image URL below, or upload a file — either publishes it immediately.</p>' +
      '<div data-cover-preview>' + (imagePath ? '<img src="' + esc(imagePath) + '" style="max-width:220px;max-height:130px;border-radius:6px;display:block;margin-bottom:6px">' : '') + '</div>' +
      '<input type="text" data-key="' + esc(dataKey) + '" data-cover-url value="' + esc(imagePath || '') + '"' +
      ' placeholder="Paste an image URL (e.g. from the Media Gallery)…" style="width:100%">' +
      '<input type="file" accept="' + IMAGE_ACCEPT + '" data-cover-file style="margin-top:6px">' +
      '</div>';
  }
  function wireImageUpload(container) {
    const urlInput = container.querySelector('[data-cover-url]');
    const fileInput = container.querySelector('[data-cover-file]');
    const preview = container.querySelector('[data-cover-preview]');
    function updatePreview(url) {
      if (!preview) return;
      preview.innerHTML = url ? '<img src="' + esc(url) + '" style="max-width:220px;max-height:130px;border-radius:6px;display:block;margin-bottom:6px">' : '';
    }
    if (urlInput) {
      // Typed or pasted directly — no upload round trip needed, the URL is
      // just used as-is (this is the whole point: reuse a Gallery image, or
      // any other image URL, with no re-upload).
      urlInput.addEventListener('input', () => updatePreview(urlInput.value.trim()));
    }
    if (!fileInput) return;
    fileInput.addEventListener('change', () => {
      const file = fileInput.files[0];
      if (!file) return;
      if (['image/jpeg', 'image/png', 'image/webp'].indexOf(file.type) === -1) {
        toast('Only JPEG, PNG or WebP images are allowed', true); fileInput.value = ''; return;
      }
      if (file.size > 5 * 1024 * 1024) { toast('Images must be 5MB or smaller', true); fileInput.value = ''; return; }
      const reader = new FileReader();
      reader.onload = async () => {
        const base64 = reader.result.split(',')[1];
        try {
          const res = await api('/api/admin/media', { method: 'POST', body: { content_base64: base64, content_type: file.type, filename: file.name } });
          if (urlInput) urlInput.value = res.url;
          updatePreview(res.url);
          toast('Image uploaded');
        } catch (e) { toast(e.message, true); }
        fileInput.value = '';
      };
      reader.readAsDataURL(file);
    });
  }

  // Financial Statements' "Pull from other modules" panel needs one extra
  // network round trip that isn't part of the module's own file.payload —
  // cached per client so switching tabs and back doesn't re-fetch, cleared
  // by an explicit "Refresh" action. Keyed by clientId; each entry is either
  // undefined (never fetched), 'loading', an array of source objects, or
  // {error}.
  const fsLinkedCache = {};
  // Which schedule period the preparer has picked as "nearest FY-end" for an
  // emi/lease source, keyed by workfile_id — persists across re-renders so
  // switching another field doesn't reset the choice.
  const fsPeriodPick = {};
  function fsLoadLinked(clientId, rerender) {
    if (!clientId) return;
    fsLinkedCache[clientId] = 'loading';
    api('/api/clients/' + clientId + '/linked-workfiles').then(res => {
      fsLinkedCache[clientId] = res.sources || [];
      rerender();
    }).catch(e => { fsLinkedCache[clientId] = { error: e.message }; rerender(); });
  }

  // 'YYYY-YY' -> the following BS fiscal year label, e.g. '2083-84' -> '2084-85'.
  // Just a placeholder suggestion in the add-fiscal-year form, not authoritative.
  function nextFyLabel(label) {
    const year = parseInt((label || '').split('-')[0], 10);
    if (!year) return '';
    return (year + 1) + '-' + String((year + 2) % 100).padStart(2, '0');
  }

  // Expands a "compound X% every N years" rule into the flat from_period/
  // payment list the lease engine actually consumes (it has no concept of a
  // repeating pattern — see leaseSchedule/lease_schedule, which only ever
  // look up the latest step at-or-before the current period). Periods here
  // are already in whatever unit the lease's own frequency uses.
  const LEASE_PERIODS_PER_YEAR = { monthly: 12, quarterly: 4, half_yearly: 2, annual: 1 };
  // Two genuinely different escalation clauses show up in real leases —
  // "+10% every 2 years" (compounding, each step a % above the previous
  // step) and "+NPR 2,000 every year" (a flat amount added each step,
  // never compounded — a % of a rupee amount makes no sense as a fixed
  // increment). escalation.type picks which arithmetic applies; everything
  // else about the generator (frequency, every_years) is shared.
  //
  // first_increase_after_years lets the FIRST step land later than one
  // interval — e.g. "+10% after year 4, then every 2 years" is
  // every_years=2, first_increase_after_years=4: escalations at year 5
  // (the increase takes effect the year AFTER the 4-year base period),
  // year 7, year 9... Left blank, the first step lands after exactly one
  // interval, same as before this field existed.
  function leaseGenerateSteps(p) {
    const ppy = LEASE_PERIODS_PER_YEAR[p.frequency] || 12;
    const everyPeriods = Math.max(1, Math.round((p.escalation.every_years || 0) * ppy) || 1);
    const delayYears = Number(p.escalation.first_increase_after_years) || 0;
    const firstStepPeriod = delayYears > 0 ? Math.round(delayYears * ppy) + 1 : 1 + everyPeriods;
    const isAmount = p.escalation.type === 'amount';
    const rate = (p.escalation.pct || 0) / 100;
    const amount = p.escalation.amount || 0;
    const steps = [];
    let k = 0;
    while (firstStepPeriod + k * everyPeriods < (p.periods || 0)) {
      const stepNumber = k + 1;
      const fromPeriod = firstStepPeriod + k * everyPeriods;
      const payment = isAmount ? (p.payment || 0) + stepNumber * amount : (p.payment || 0) * Math.pow(1 + rate, stepNumber);
      steps.push({ from_period: fromPeriod, payment: CAEngines.R2(payment) });
      k++;
    }
    return steps;
  }

  // ──────────────────────────────────────────────────────── module registry
  const MODULES = {

    depreciation: {
      code: 'DP', name: 'Depreciation', group: 'Tax computation',
      blurb: 'Pool-wise depreciation under Schedule 2, with the absorption tranche taken from the Nepali month of purchase and the section 16 repair ceiling applied automatically.',
      seed: () => ({ pools: [{ pool: 'a', opening_base: 0, additions: [], disposals: 0, repairs: 0, business_use_pct: 100 }], opts: {} }),
      bulkImport: {
        // Each imported row carries its own pool letter; route it in, creating
        // the pool if this workfile doesn't have one yet. Additions never start
        // with a default placeholder row, so this is a plain append.
        apply(file, sheets) {
          const p = file.payload;
          (sheets.additions.rows || []).forEach(r => {
            const key = String(r.pool || 'd').toLowerCase();
            let pool = p.pools.find(x => x.pool === key);
            if (!pool) {
              pool = { pool: key, opening_base: 0, additions: [], disposals: 0, repairs: 0, business_use_pct: 100 };
              p.pools.push(pool);
            }
            pool.additions = pool.additions || [];
            pool.additions.push({ desc: r.desc || '', amount: r.amount || 0, bs_month: r.bs_month || 1 });
          });
        }
      },
      view(file, res) {
        const p = file.payload;
        let h = '<div class="card"><h3>Pools</h3>' +
          '<p class="card-note">Enter one row per pool. Additions are listed separately because the month of purchase decides how much of the cost enters this year\'s depreciation base.</p>' +
          '<div class="table-wrap"><table><thead><tr><th>Pool</th><th class="num">Opening base</th>' +
          '<th class="num">Disposals</th><th class="num">Repairs</th><th class="num">Business use %</th><th></th></tr></thead><tbody>';
        p.pools.forEach((pool, i) => {
          h += '<tr><td><select data-pool="' + i + '" data-k="pool">' +
            depreciationPoolKeys().map(x => '<option value="' + x + '"' + (pool.pool === x ? ' selected' : '') + '>Pool ' + x.toUpperCase() + '</option>').join('') +
            '</select></td>' +
            ['opening_base', 'disposals', 'repairs', 'business_use_pct'].map(k =>
              '<td class="num"><input type="number" step="0.01" data-pool="' + i + '" data-k="' + k + '" value="' + (pool[k] || 0) + '"></td>').join('') +
            '<td><button class="btn btn-small btn-danger" data-del-pool="' + i + '" title="Remove this row">Remove</button></td></tr>';
        });
        h += '</tbody></table></div><button class="btn btn-small" id="add-pool">Add a pool</button></div>';

        h += '<div class="card"><h3>Additions during the year</h3>' +
          '<div class="table-wrap"><table><thead><tr><th>Pool</th><th>Description</th><th>Nepali month of purchase</th>' +
          '<th class="num">Cost</th><th class="num">Absorbed</th><th></th></tr></thead><tbody>';
        p.pools.forEach((pool, pi) => (pool.additions || []).forEach((a, ai) => {
          const computed = res.pools[pi] && res.pools[pi].additions[ai];
          h += '<tr><td>' + pool.pool.toUpperCase() + '</td>' +
            '<td><input data-add="' + pi + ',' + ai + '" data-k="desc" value="' + esc(a.desc || '') + '"></td>' +
            '<td><select data-add="' + pi + ',' + ai + '" data-k="bs_month">' +
            NEPALI_MONTHS.map((m, j) => '<option value="' + (j + 1) + '"' + (Number(a.bs_month) === j + 1 ? ' selected' : '') + '>' + m + '</option>').join('') +
            '</select></td>' +
            '<td class="num"><input type="number" step="0.01" data-add="' + pi + ',' + ai + '" data-k="amount" value="' + (a.amount || 0) + '"></td>' +
            '<td class="num">' + (computed ? money(computed.absorbed) + ' <span class="tag">' + computed.tranche.toUpperCase() + '</span>' : '–') + '</td>' +
            '<td><button class="btn btn-small btn-danger" data-del-add="' + pi + ',' + ai + '" title="Remove this row">Remove</button></td></tr>';
        }));
        h += '</tbody></table></div>' +
          '<div class="grid c2" style="margin-top:12px"><div class="field"><label>Add an addition to</label><select id="add-to-pool">' +
          p.pools.map((x, i) => '<option value="' + i + '">Pool ' + x.pool.toUpperCase() + '</option>').join('') +
          '</select></div><div class="field"><label>&nbsp;</label><button class="btn" id="add-addition">Add addition</button></div></div>' +
          '<label class="check" style="margin-top:10px"><input type="checkbox" id="special-ind"' +
          (p.opts && p.opts.special_industry ? ' checked' : '') + '> Special industry — claim the additional depreciation</label></div>';

        h += resultCard('Computation', ['Pool', 'Rate', 'Opening base', 'Additions absorbed', 'Repairs capitalised',
          'Disposals', 'Depreciation base', 'Depreciation', 'Closing base'],
          res.pools.map(x => [x.pool, pct(x.rate), x.opening_base, x.absorbed_additions, x.repairs_capitalised,
            x.disposals, x.depreciation_base, x.depreciation, x.closing_base]),
          ['Total', '', res.totals.opening, res.totals.absorbed, '', res.totals.disposals, '', res.totals.depreciation, res.totals.closing]);

        const notes = res.pools.filter(x => x.note);
        if (notes.length) h += '<div class="card"><h3>Points arising</h3><ul>' +
          notes.map(x => '<li><b>Pool ' + x.pool + ':</b> ' + esc(x.note) + '</li>').join('') + '</ul></div>';
        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        root.querySelectorAll('[data-pool]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.pool, k = e.target.dataset.k;
          p.pools[i][k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-add]').forEach(el => el.addEventListener('change', e => {
          const [pi, ai] = e.target.dataset.add.split(',').map(Number);
          const k = e.target.dataset.k;
          p.pools[pi].additions[ai][k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-del-pool]').forEach(el => el.addEventListener('click', e => {
          p.pools.splice(+e.target.dataset.delPool, 1); rerender();
        }));
        root.querySelectorAll('[data-del-add]').forEach(el => el.addEventListener('click', e => {
          const [pi, ai] = e.target.dataset.delAdd.split(',').map(Number);
          p.pools[pi].additions.splice(ai, 1); rerender();
        }));
        const addPool = root.querySelector('#add-pool');
        if (addPool) addPool.addEventListener('click', () => {
          p.pools.push({ pool: 'd', opening_base: 0, additions: [], disposals: 0, repairs: 0, business_use_pct: 100 });
          rerender();
        });
        const addAdd = root.querySelector('#add-addition');
        if (addAdd) addAdd.addEventListener('click', () => {
          const i = +root.querySelector('#add-to-pool').value;
          p.pools[i].additions = p.pools[i].additions || [];
          p.pools[i].additions.push({ desc: '', amount: 0, bs_month: 1 });
          rerender();
        });
        const si = root.querySelector('#special-ind');
        if (si) si.addEventListener('change', e => {
          p.opts = p.opts || {}; p.opts.special_industry = e.target.checked; rerender();
        });
      },
      tabulate: res => [{
        title: 'Pool computation',
        headers: ['Pool', 'Rate', 'Opening base', 'Additions absorbed', 'Repairs capitalised', 'Disposals', 'Depreciation base', 'Depreciation', 'Closing base'],
        rows: res.pools.map(x => [x.pool, pct(x.rate), x.opening_base, x.absorbed_additions, x.repairs_capitalised,
          x.disposals, x.depreciation_base, x.depreciation, x.closing_base])
          .concat([['Total', '', res.totals.opening, res.totals.absorbed, '', res.totals.disposals, '', res.totals.depreciation, res.totals.closing]])
      }, {
        title: 'Additions',
        headers: ['Pool', 'Description', 'BS month', 'Tranche', 'Factor', 'Cost', 'Absorbed'],
        rows: res.pools.reduce((acc, p) => acc.concat((p.additions || []).map(a =>
          [p.pool, a.desc, a.bs_month, (a.tranche || '').toUpperCase(), a.factor, a.amount, a.absorbed])), [])
      }]
    },

    salary_tds: {
      code: 'TD', name: 'Salary & TDS', group: 'Tax computation',
      blurb: 'Income from employment, permutation by permutation — perquisites, foreign postings, retirement lump sums, a second employer and the medical credit, not just basic and allowance.',
      seed: () => ({ employees: [{ name: '', designation: '', status: 'individual', basic_monthly: 0, allowance_monthly: 0, months: 12, ssf_contributor: true }] }),
      bulkImport: {
        // The seed file starts with one untouched placeholder employee; drop it
        // before appending imported rows so a bulk import doesn't leave a blank
        // employee sitting in the register.
        apply(file, sheets) {
          const isBlank = e => !e.name && !e.basic_monthly;
          file.payload.employees = file.payload.employees.filter(e => !isBlank(e)).concat(sheets.employees.rows || []);
        }
      },
      view(file, res) {
        const p = file.payload;
        let h = '';
        p.employees.forEach((e, i) => {
          const r = res.employees[i] || {};
          const perq = r.perquisites || {};
          h += '<details class="card" open><summary><b>' + esc(e.name || 'New employee') + '</b>' +
            '<span class="hint"> — taxable ' + money(r.taxable_income) + ' · total tax ' + money(r.total_tax_payable) +
            ' · monthly TDS ' + money(r.monthly_tds) + '</span></summary>';

          h += '<div class="grid c3" style="margin-top:12px"><h4 class="span-all">Who, and for how long</h4>' +
            '<div class="field"><label>Name</label><input data-emp="' + i + '" data-k="name" value="' + esc(e.name || '') + '"></div>' +
            '<div class="field"><label>Designation</label><input data-emp="' + i + '" data-k="designation" value="' + esc(e.designation || '') + '"></div>' +
            '<div class="field"><label>Filing status</label><select data-emp="' + i + '" data-k="status">' +
            '<option value="individual"' + (e.status !== 'couple' ? ' selected' : '') + '>Individual</option>' +
            '<option value="couple"' + (e.status === 'couple' ? ' selected' : '') + '>Couple</option></select></div>' +
            '<div class="field"><label>Months this year</label><input type="number" data-emp="' + i + '" data-k="months" value="' + (e.months ?? 12) + '"></div>' +
            '<div class="field"><label>Months of TDS remaining</label><input type="number" data-emp="' + i + '" data-k="remaining_months" value="' + (e.remaining_months || '') + '" placeholder="defaults to months"></div>' +
            '<div class="field"><label>TDS already deducted</label><input type="number" data-emp="' + i + '" data-k="tds_already_deducted" value="' + (e.tds_already_deducted || 0) + '"></div>' +
            '<label class="check"><input type="checkbox" data-emp="' + i + '" data-k="is_resident_woman"' + (e.is_resident_woman ? ' checked' : '') + '> Resident woman (single filing rebate)</label>' +
            '<label class="check"><input type="checkbox" data-emp="' + i + '" data-k="is_senior_citizen"' + (e.is_senior_citizen ? ' checked' : '') + '> Senior citizen (wider slab 1)</label>' +
            '<label class="check"><input type="checkbox" data-emp="' + i + '" data-k="ssf_contributor"' + (e.ssf_contributor !== false ? ' checked' : '') + '> SSF contributor (else provident fund)</label>' +
            '</div>';

          h += '<div class="grid c3" style="margin-top:12px"><h4 class="span-all">Cash pay</h4>' +
            [['basic_monthly', 'Basic / month'], ['allowance_monthly', 'Allowance / month'], ['other_annual', 'Other allowances (annual)'],
             ['bonus_annual', 'Bonus (annual)'], ['overtime_annual', 'Overtime (annual)'], ['commission_annual', 'Commission (annual)'],
             ['leave_encashment_annual', 'Leave encashment (annual)']].map(([k, label]) =>
              '<div class="field"><label>' + label + '</label><input type="number" data-emp="' + i + '" data-k="' + k + '" value="' + (e[k] || 0) + '"></div>').join('') +
            '<div class="field"><label>Employee\'s own fund contribution</label><input type="number" data-emp="' + i + '" data-k="employee_contrib_override" value="' + (e.employee_contrib_override || '') + '" placeholder="defaults to % of basic"></div>' +
            '</div>';

          h += '<div class="grid c3" style="margin-top:12px"><h4 class="span-all">Employer-provided perquisites</h4>' +
            '<label class="check"><input type="checkbox" data-emp="' + i + '" data-k="vehicle_facility"' + (e.vehicle_facility ? ' checked' : '') + '> Vehicle facility (dual use)</label>' +
            '<div class="field"><label>Vehicle prescribed cost</label><input type="number" data-emp="' + i + '" data-k="vehicle_cost" value="' + (e.vehicle_cost || 0) + '"></div>' +
            '<div></div>' +
            '<label class="check"><input type="checkbox" data-emp="' + i + '" data-k="accommodation_facility"' + (e.accommodation_facility ? ' checked' : '') + '> Accommodation facility</label>' +
            '<div class="field"><label>Rent actually borne by employer</label><input type="number" data-emp="' + i + '" data-k="accommodation_rent_paid" value="' + (e.accommodation_rent_paid || 0) + '" placeholder="0 uses the % rule"></div>' +
            '<div></div>' +
            '<div class="field"><label>Employer loan — amount</label><input type="number" data-emp="' + i + '" data-k="employer_loan_amount" value="' + (e.employer_loan_amount || 0) + '"></div>' +
            '<div class="field"><label>Employer loan — rate actually charged</label><input type="number" step="0.001" data-emp="' + i + '" data-k="employer_loan_rate_charged" value="' + (e.employer_loan_rate_charged || 0) + '" placeholder="e.g. 0.03 for 3%"></div>' +
            '<div class="field"><label>Other perquisites (annual)</label><input type="number" data-emp="' + i + '" data-k="other_perquisites_annual" value="' + (e.other_perquisites_annual || 0) + '"></div>' +
            (perq.total ? '<div class="hint span-all">Perquisites this year: ' + money(perq.total) + (perq.vehicle ? ' · vehicle ' + money(perq.vehicle) : '') +
              (perq.accommodation ? ' · accommodation ' + money(perq.accommodation) : '') + (perq.employer_loan ? ' · loan ' + money(perq.employer_loan) : '') + '</div>' : '') +
            '</div>';

          h += '<div class="grid c3" style="margin-top:12px"><h4 class="span-all">Foreign posting, retirement, second employer</h4>' +
            '<label class="check"><input type="checkbox" data-emp="' + i + '" data-k="is_foreign_posting"' + (e.is_foreign_posting ? ' checked' : '') + '> Posted abroad</label>' +
            '<div class="field"><label>Foreign allowance (annual)</label><input type="number" data-emp="' + i + '" data-k="foreign_allowance_annual" value="' + (e.foreign_allowance_annual || 0) + '"></div>' +
            '<div></div>' +
            '<label class="check"><input type="checkbox" data-emp="' + i + '" data-k="is_retirement_year"' + (e.is_retirement_year ? ' checked' : '') + '> Separating this year</label>' +
            '<div class="field"><label>Retirement lump sum</label><input type="number" data-emp="' + i + '" data-k="retirement_lumpsum_amount" value="' + (e.retirement_lumpsum_amount || 0) + '" placeholder="gratuity + fund corpus paid out"></div>' +
            '<div></div>' +
            '<div class="field"><label>Other employer\'s taxable income (this FY)</label><input type="number" data-emp="' + i + '" data-k="other_employer_income_annual" value="' + (e.other_employer_income_annual || 0) + '" placeholder="for correct marginal-slab stacking"></div>' +
            '</div>';

          h += '<div class="grid c3" style="margin-top:12px"><h4 class="span-all">Deductions &amp; credits</h4>' +
            '<div class="field"><label>Life insurance premium</label><input type="number" data-emp="' + i + '" data-k="insurance_premium" value="' + (e.insurance_premium || 0) + '"></div>' +
            '<div class="field"><label>Health insurance premium</label><input type="number" data-emp="' + i + '" data-k="health_premium" value="' + (e.health_premium || 0) + '"></div>' +
            '<div class="field"><label>Approved medical expense (annual)</label><input type="number" data-emp="' + i + '" data-k="approved_medical_expense_annual" value="' + (e.approved_medical_expense_annual || 0) + '"></div>' +
            '<div class="field"><label>Remote area category</label><select data-emp="' + i + '" data-k="remote_area_category"><option value=""' + (!e.remote_area_category ? ' selected' : '') + '>— none —</option>' +
            ['A', 'B', 'C', 'D', 'E'].map(c => '<option value="' + c + '"' + (e.remote_area_category === c ? ' selected' : '') + '>Category ' + c + '</option>').join('') + '</select></div>' +
            '<div class="field"><label>Remote area allowance received</label><input type="number" data-emp="' + i + '" data-k="remote_area_allowance" value="' + (e.remote_area_allowance || 0) + '"></div>' +
            '</div>';

          h += '<div class="card-note" style="margin-top:10px">Perquisites total ' + money(perq.total) + ' · gross assessable ' + money(r.gross_assessable) +
            ' · deductions ' + money(r.deductions && r.deductions.total) + ' · combined taxable (with other employer) ' + money(r.combined_taxable_income) +
            ' · medical credit ' + money(r.medical_credit) + (r.retirement && r.retirement.applicable ? ' · retirement tax (Sec 65, flat ' + (r.retirement.rate * 100).toFixed(0) + '%) ' + money(r.retirement.tax) : '') + '</div>';

          h += '<button class="btn btn-small btn-danger" style="margin-top:10px" data-del-emp="' + i + '">Remove this employee</button></details>';
        });
        h += '<button class="btn" id="add-emp">Add an employee</button>';

        h += resultCard('TDS register', ['Employee', 'Taxable income', 'Annual tax', 'Retirement tax', 'Total tax payable', 'TDS to date', 'Monthly TDS'],
          res.employees.map(e => [e.name || '(unnamed)', e.taxable_income, e.annual_tax,
            (e.retirement && e.retirement.applicable) ? e.retirement.tax : 0, e.total_tax_payable, e.tds_already_deducted, e.monthly_tds]),
          ['Total', sum(res.employees, 'taxable_income'), sum(res.employees, 'annual_tax'),
            res.employees.reduce((s, e) => s + ((e.retirement && e.retirement.applicable) ? e.retirement.tax : 0), 0),
            sum(res.employees, 'total_tax_payable'), '', sum(res.employees, 'monthly_tds')]);
        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        root.querySelectorAll('[data-emp]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.emp, k = e.target.dataset.k;
          const raw = e.target.value;
          p.employees[i][k] = e.target.type === 'checkbox' ? e.target.checked
            : (e.target.type === 'number' ? (raw === '' ? undefined : Number(raw)) : raw);
          rerender();
        }));
        root.querySelectorAll('[data-del-emp]').forEach(el => el.addEventListener('click', e => {
          p.employees.splice(+e.target.dataset.delEmp, 1); rerender();
        }));
        const add = root.querySelector('#add-emp');
        if (add) add.addEventListener('click', () => {
          p.employees.push({ name: '', designation: '', status: 'individual', basic_monthly: 0, allowance_monthly: 0, months: 12, ssf_contributor: true });
          rerender();
        });
      },
      tabulate: res => [{
        title: 'TDS register',
        headers: ['Employee', 'Gross assessable', 'Deductions', 'Taxable income', 'Annual tax', 'Retirement tax', 'Total tax payable', 'TDS to date', 'Monthly TDS'],
        rows: res.employees.map(e => [e.name, e.gross_assessable, e.deductions.total, e.taxable_income, e.annual_tax,
          (e.retirement && e.retirement.applicable) ? e.retirement.tax : 0, e.total_tax_payable, e.tds_already_deducted, e.monthly_tds])
      }]
    },

    personal_tax: {
      code: 'PT', name: 'Personal Income Tax', group: 'Tax computation',
      blurb: 'A whole-person computation for an individual (or filing couple) with income from more than one head — employment, business, rental, other — not tied to a single employer\'s payroll. Bring the assessable figures from Salary & TDS or Business Income Tax if this person has either; this module combines them, applies the deduction caps, and walks the slabs.',
      seed: () => ({
        person: { status: 'individual', is_resident_woman: false, ssf_contributor: false, is_senior_citizen: false },
        incomes: { employment_assessable: 0, business_net_profit: 0, rental_gross: 0, rental_expenses: 0, other_income: 0 },
        deductions: { retirement_contribution: 0, life_insurance_premium: 0, health_insurance_premium: 0, donations: 0 },
      }),
      view(file, res) {
        const p = file.payload;
        p.person = p.person || {}; p.incomes = p.incomes || {}; p.deductions = p.deductions || {};

        let h = '<div class="card"><h3>Person</h3><div class="grid c3" id="pit-person">' +
          field('Filing status', 'status', p.person, 'select', { options: [
            { value: 'individual', label: 'Individual' }, { value: 'couple', label: 'Couple (joint filing)' }] }) +
          '<label class="check"><input type="checkbox" data-flag="is_resident_woman"' + (p.person.is_resident_woman ? ' checked' : '') + '> Resident woman (single filing) — rebate</label>' +
          '<label class="check"><input type="checkbox" data-flag="ssf_contributor"' + (p.person.ssf_contributor ? ' checked' : '') + '> Contributes to the Social Security Fund</label>' +
          '<label class="check"><input type="checkbox" data-flag="is_senior_citizen"' + (p.person.is_senior_citizen ? ' checked' : '') + '> Senior citizen</label>' +
          '</div></div>';

        h += '<div class="card"><h3>Income sources</h3><p class="card-note">For employment or business income, use the Salary &amp; TDS or Business Income Tax module for the detailed computation and bring the resulting assessable/net-profit figure here — this module combines heads, it doesn\'t re-derive them.</p><div class="grid c3" id="pit-incomes">' +
          field('Employment income (assessable)', 'employment_assessable', p.incomes, 'number') +
          field('Business net profit', 'business_net_profit', p.incomes, 'number') +
          '<div></div>' +
          field('Rental — gross receipts', 'rental_gross', p.incomes, 'number') +
          field('Rental — deductible repair/maintenance expense actually incurred', 'rental_expenses', p.incomes, 'number', { hint: 'Actual, not a flat percentage — enter what\'s genuinely deductible for this property.' }) +
          '<div></div>' +
          field('Other income (capital gains not finally withheld, misc.)', 'other_income', p.incomes, 'number') +
          '</div></div>';

        h += '<div class="card"><h3>Deductions</h3><div class="grid c3" id="pit-deductions">' +
          field('Retirement fund contribution', 'retirement_contribution', p.deductions, 'number', { hint: 'Sec 63 — capped at the lower of the actual amount, the fixed ceiling and % of income.' }) +
          field('Life insurance premium', 'life_insurance_premium', p.deductions, 'number') +
          field('Health/medical insurance premium', 'health_insurance_premium', p.deductions, 'number') +
          field('Donations', 'donations', p.deductions, 'number', { hint: 'Capped at the lower of the actual amount, % of adjusted income and the fixed ceiling.' }) +
          '</div></div>';

        if (res.incomes) {
          h += resultCard('Income summary', ['Particulars', 'Amount'], [
            ['Employment income', res.incomes.employment], ['Business net profit', res.incomes.business],
            ['Rental — net', res.incomes.rental_net], ['Other income', res.incomes.other],
          ], ['Gross assessable income', res.incomes.gross_income]);

          h += resultCard('Deductions applied', ['Particulars', 'Amount'], [
            ['Retirement contribution', res.deductions.retirement], ['Life insurance premium', res.deductions.life_insurance],
            ['Health insurance premium', res.deductions.health_insurance], ['Donation', res.deductions.donation],
          ], ['Total deductions', res.deductions.total]);
          if (res.deductions.donation_disallowed) {
            h += '<div class="card-note">Donation disallowed above the deductible cap: ' + money(res.deductions.donation_disallowed) + '.</div>';
          }

          h += resultCard('Slab-wise tax', ['Band', 'Rate', 'Income in band', 'Tax'],
            res.slab_lines.map(l => [l.band, pct(l.rate), l.income, l.tax]));

          h += '<div class="grid c4">' +
            kpi('Taxable income', money(res.taxable_income)) +
            kpi('Tax before rebate', money(res.tax_before_rebate)) +
            kpi('Rebate', money(res.rebate)) +
            kpi('Tax payable', money(res.tax_payable), 'accent') +
            '</div>';
        }

        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        wireInto(root, 'pit-person', p.person, rerender);
        wireInto(root, 'pit-incomes', p.incomes, rerender);
        wireInto(root, 'pit-deductions', p.deductions, rerender);
        root.querySelectorAll('[data-flag]').forEach(el => el.addEventListener('change', e => {
          p.person[e.target.dataset.flag] = e.target.checked; rerender();
        }));
      },
      tabulate: res => [{
        title: 'Computation', headers: ['Particulars', 'Amount'],
        rows: [
          ['Employment income (assessable)', res.incomes.employment], ['Business net profit', res.incomes.business],
          ['Rental — gross receipts', res.incomes.rental_gross], ['Rental — deductible expenses', res.incomes.rental_expenses],
          ['Rental — net', res.incomes.rental_net], ['Other income', res.incomes.other],
          ['Gross assessable income', res.incomes.gross_income],
          ['Less: retirement contribution deduction', res.deductions.retirement],
          ['Less: life insurance premium deduction', res.deductions.life_insurance],
          ['Less: health insurance premium deduction', res.deductions.health_insurance],
          ['Adjusted income (before donation)', res.adjusted_income],
          ['Less: donation deduction', res.deductions.donation],
          ['Taxable income', res.taxable_income], ['Tax before rebate', res.tax_before_rebate],
          ['Less: rebate', res.rebate], ['Tax payable', res.tax_payable],
        ]
      }, {
        title: 'Slab detail', headers: ['Band', 'Rate', 'Income in band', 'Tax'],
        rows: (res.slab_lines || []).map(l => [l.band, pct(l.rate), l.income, l.tax])
      }]
    },

    biz_tax: {
      code: 'BT', name: 'Business income tax', group: 'Tax computation',
      blurb: 'Book profit to taxable income line by line — a dynamic company rate list, the donation cap, loss carry-forward with expiry, a tax-holiday relief and an optional Sec 4 presumptive-tax regime, not just a flat rate on the adjusted figure.',
      seed: () => ({ profit_before_tax: 0, entity: 'company', rate_category: 'general', tax_holiday_pct: 0,
        additions: [], deductions: [], brought_forward_losses: [],
        presumptive: { elected: false, turnover: 0, area_category: 'metro' } }),
      bulkImport: {
        // One sheet, routed by a Type column into the two separate lists the
        // engine expects — the same routing-by-column idea depreciation uses
        // for pool letters.
        apply(file, sheets) {
          const p = file.payload;
          p.additions = p.additions || []; p.deductions = p.deductions || [];
          (sheets.adjustments.rows || []).forEach(r => {
            const target = String(r.kind || '').toLowerCase().startsWith('d') ? p.deductions : p.additions;
            target.push({ desc: r.desc || '', amount: r.amount || 0 });
          });
        }
      },
      view(file, res) {
        const p = file.payload;
        p.presumptive = p.presumptive || { elected: false, turnover: 0, area_category: 'metro' };
        p.brought_forward_losses = p.brought_forward_losses || [];

        let h = '<div class="card"><h3>Sec 4 — presumptive tax (optional)</h3>' +
          '<p class="card-note">A turnover-based flat tax that replaces the whole computation below when elected — for an eligible small taxpayer, not the default path.</p>' +
          '<div class="grid c3" id="biz-presumptive">' +
          '<label class="check"><input type="checkbox" data-pflag="elected"' + (p.presumptive.elected ? ' checked' : '') + '> Elect presumptive tax</label>' +
          field('Turnover', 'turnover', p.presumptive, 'number') +
          field('Area category', 'area_category', p.presumptive, 'select', { options: [
            { value: 'metro', label: 'Metropolitan' }, { value: 'submetro', label: 'Sub-metropolitan' },
            { value: 'municipality', label: 'Municipality' }, { value: 'rural', label: 'Rural municipality' }],
            hint: 'Only "metro" brackets are seeded — add the rest in Rate Master before using the others' }) +
          '</div></div>';

        if (p.presumptive.elected) {
          h += resultCard('Presumptive tax', ['Particulars', 'Amount'], [
            ['Turnover', res.presumptive && res.presumptive.turnover],
            ['Bracket', res.presumptive && res.presumptive.bracket_label],
            ['Flat tax', res.presumptive && res.presumptive.flat_tax],
            ['Less: TDS credit', -(res.tds_credit || 0)],
            ['Less: advance tax', -(res.advance_tax_paid || 0)]
          ], ['Net tax payable', res.net_payable]);
          if (res.presumptive && res.presumptive.note) h += '<div class="card"><b>' + esc(res.presumptive.note) + '</b></div>';
          if (res.refund_due) h += '<div class="card"><b>Refund of ' + money(res.refund_due) + '</b> arises and should be claimed in the return.</div>';
          return h;
        }

        const catOptions = (res.rate_categories && res.rate_categories.length)
          ? res.rate_categories.map(c => ({ value: c.slug, label: c.label + ' (' + (c.rate * 100).toFixed(0) + '%)' }))
          : [{ value: p.rate_category || 'general', label: 'No categories found in Rate Master' }];

        h += '<div class="card"><h3>Starting point</h3><div class="grid c3">' +
          field('Profit before tax', 'profit_before_tax', p, 'number') +
          field('Entity', 'entity', p, 'select', { options: [
            { value: 'company', label: 'Company' }, { value: 'individual', label: 'Individual' }, { value: 'couple', label: 'Couple' }] }) +
          (p.entity === 'company' ? field('Rate category', 'rate_category', p, 'select', { options: catOptions }) : '<div></div>') +
          field('Depreciation in books', 'book_depreciation', p, 'number') +
          field('Depreciation per Schedule 2', 'tax_depreciation', p, 'number', { hint: 'Pull this from the Depreciation module' }) +
          field('Donations (actual)', 'donations', p, 'number', { hint: 'Capped automatically to the lower of the % and fixed-ceiling limbs' }) +
          field('Tax holiday relief this year', 'tax_holiday_pct', p, 'number', { hint: 'Fraction, e.g. 0.5 for a 50% holiday — your own reading of the approval letter' }) +
          field('TDS credit', 'tds_credit', p, 'number') +
          field('Advance tax paid', 'advance_tax_paid', p, 'number') +
          '</div></div>';

        h += adjustTable('Disallowances and add-backs', 'additions', p.additions);
        h += adjustTable('Deductions and allowances', 'deductions', p.deductions);

        h += '<div class="card"><h3>Brought-forward losses</h3><p class="card-note">Aged against the carry-forward window for the chosen rate category (' +
          (res.loss_carry_forward_years != null ? res.loss_carry_forward_years : '—') + ' years) — an expired loss is excluded automatically.</p>' +
          '<div class="table-wrap"><table><thead><tr><th>FY incurred</th><th class="num">Amount</th><th></th></tr></thead><tbody>';
        p.brought_forward_losses.forEach((l, i) => {
          const computed = res.losses && res.losses[i];
          h += '<tr><td><input data-loss="' + i + '" data-k="fy_label" value="' + esc(l.fy_label || '') + '" placeholder="2081-82"></td>' +
            '<td class="num"><input type="number" data-loss="' + i + '" data-k="amount" value="' + (l.amount || 0) + '"></td>' +
            '<td>' + (computed ? (computed.expired ? '<span class="tag tag-red">Expired (' + computed.age_years + 'y)</span>'
                                                     : '<span class="tag tag-green">Usable — ' + money(computed.utilised) + '</span>') : '') +
            ' <button class="btn btn-small btn-danger" data-del-loss="' + i + '" title="Remove this row">Remove</button></td></tr>';
        });
        h += '</tbody></table></div><button class="btn btn-small" id="add-loss">Add a loss year</button></div>';

        h += resultCard('Computation', ['Particulars', 'Amount'], [
          ['Profit before tax', res.profit_before_tax],
          ['Add: disallowances', res.add_total],
          ['Add: depreciation in books', res.book_depreciation],
          ['Less: deductions', -res.deduction_total],
          ['Less: depreciation per Schedule 2', -res.tax_depreciation],
          ['Adjusted income', res.adjusted_income],
          ['Less: donations allowed', res.donations && -res.donations.allowed],
          ['Less: brought-forward loss utilised', -res.loss_utilised],
          ['Taxable income', res.taxable_income],
          ['Tax — ' + res.rate_label, res.tax_before_holiday],
          ['Less: tax holiday relief', -(res.tax_holiday_relief || 0)],
          ['Tax liability', res.tax_liability],
          ['Less: TDS credit', -res.tds_credit],
          ['Less: advance tax', -res.advance_tax_paid]
        ], ['Net tax payable', res.net_payable]);
        if (res.donations && res.donations.disallowed) h += '<div class="card"><b>' + money(res.donations.disallowed) + '</b> of the donation exceeds the deductible cap and stays in taxable income.</div>';
        if (res.loss_expired) h += '<div class="card"><b>' + money(res.loss_expired) + '</b> of brought-forward loss has passed the carry-forward window and can no longer be used.</div>';
        if (res.refund_due) h += '<div class="card"><b>Refund of ' + money(res.refund_due) + '</b> arises and should be claimed in the return.</div>';

        h += '<div class="card"><h3>Common disallowances — a checklist, not an auto-rule</h3><p class="card-note">Facts decide disallowability; this is a reminder of what to look for while building the additions table above, not something the engine applies for you.</p><ul>' +
          ['Domestic or personal expenses charged to the business',
           'Expenses not incurred wholly for business purposes',
           'Income tax itself, and fines or penalties for breaking any law',
           'General or unrealised provisions (bad debts, warranty) not yet actually incurred',
           'Capital expenditure charged as a revenue expense',
           'Entertainment and gifts beyond what the client can evidence as business-related',
           'Interest or payments to a related party not at arm\'s length'
          ].map(x => '<li>' + esc(x) + '</li>').join('') + '</ul></div>';
        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        p.presumptive = p.presumptive || { elected: false, turnover: 0, area_category: 'metro' };
        // wireInto MUST run before wireSimple: wireSimple scans the whole
        // root for '.field' controls with no container scoping, so if it ran
        // first it would claim the presumptive-tax fields too (marking them
        // wired) and write their values onto the top-level payload instead
        // of p.presumptive — silently discarding the turnover the preparer
        // typed. Both helpers use the same data-wired marker to avoid
        // double-binding, so whichever runs first wins the field.
        wireInto(root, 'biz-presumptive', p.presumptive, rerender);
        wireSimple(root, file, rerender);
        wireAdjust(root, file, rerender);
        root.querySelectorAll('[data-pflag]').forEach(el => el.addEventListener('change', e => {
          p.presumptive[e.target.dataset.pflag] = e.target.checked; rerender();
        }));
        root.querySelectorAll('[data-loss]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.loss;
          p.brought_forward_losses[i][e.target.dataset.k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-del-loss]').forEach(el => el.addEventListener('click', e => {
          p.brought_forward_losses.splice(+e.target.dataset.delLoss, 1); rerender();
        }));
        const a = root.querySelector('#add-loss');
        if (a) a.addEventListener('click', () => {
          p.brought_forward_losses.push({ fy_label: '', amount: 0 }); rerender();
        });
      },
      tabulate: res => res.regime === 'presumptive' ? [{
        title: 'Tax computation',
        headers: ['Particulars', 'Amount'],
        rows: [['Turnover', res.presumptive.turnover], ['Area category', res.presumptive.area_category],
          ['Bracket', res.presumptive.bracket_label || ''], ['Flat tax (Sec 4)', res.presumptive.flat_tax],
          ['Less: TDS credit', -res.tds_credit], ['Less: advance tax paid', -res.advance_tax_paid],
          ['Net tax payable', res.net_payable], ['Refund due', res.refund_due]]
      }] : [{
        title: 'Tax computation',
        headers: ['Particulars', 'Amount'],
        rows: [['Profit before tax as per financial statements', res.profit_before_tax]]
          .concat((res.additions || []).map(a => ['  Add: ' + a.desc, a.amount]))
          .concat([['  Add: book depreciation', res.book_depreciation]])
          .concat((res.deductions || []).map(d => ['  Less: ' + d.desc, -d.amount]))
          .concat([
            ['  Less: depreciation per Schedule 2', -res.tax_depreciation],
            ['Adjusted taxable income', res.adjusted_income],
            ['  Less: donations allowed (of ' + money(res.donations.actual) + ' claimed)', -res.donations.allowed],
            ['  Less: brought-forward loss utilised', -res.loss_utilised],
            ['Taxable income', res.taxable_income],
            ['Tax at ' + res.rate_label, res.tax_before_holiday],
            ['  Less: tax holiday relief (' + Math.round(res.tax_holiday_pct * 100) + '%)', -res.tax_holiday_relief],
            ['Tax liability', res.tax_liability],
            ['  Less: TDS credit', -res.tds_credit],
            ['  Less: advance tax paid', -res.advance_tax_paid],
            ['Net tax payable', res.net_payable],
            ['Refund due', res.refund_due],
          ])
      }].concat((res.losses || []).length ? [{
        title: 'Losses',
        headers: ['Loss year', 'Amount', 'Age (years)', 'Status', 'Utilised'],
        rows: res.losses.map(l => [l.fy_label, l.amount, l.age_years, l.expired ? 'Expired' : 'Usable', l.utilised])
      }] : [])
    },

    penalty: {
      code: 'PN', name: 'Fees & interest', group: 'Tax computation',
      blurb: 'A scenario matrix, not one formula — filing fees, instalment and late-payment interest, a false-statement penalty, TDS exposure and VAT, each raised only when its own facts are present, with the basis spelled out.',
      seed: () => ({ estimate_return_filed: true, books_maintained: true, instalment_shortfalls: [] }),
      bulkImport: {
        apply(file, sheets) {
          file.payload.instalment_shortfalls = (file.payload.instalment_shortfalls || [])
            .concat((sheets.shortfalls.rows || []).map(r =>
              ({ label: r.label || '', shortfall: r.shortfall || 0, months: r.months || 0 })));
        }
      },
      view(file, res) {
        const p = file.payload;
        let h = '<div class="card"><h3>Sec 117 — filing and records</h3><div class="grid c3">' +
          field('Assessable income', 'assessable_income', p, 'number') +
          field('Months the income return (D-03) is late', 'months_late_return', p, 'number') +
          '<div></div>' +
          '<label class="check"><input type="checkbox" data-flag="estimate_return_filed"' +
          (p.estimate_return_filed !== false ? ' checked' : '') + '> Estimated return (D-01) was filed</label>' +
          '<label class="check"><input type="checkbox" data-flag="books_maintained"' +
          (p.books_maintained !== false ? ' checked' : '') + '> Books of account were maintained</label>' +
          '</div></div>';

        h += '<div class="card"><h3>Sec 118 — instalment (advance tax) interest</h3>' +
          '<p class="card-note">Auto-computed from the estimate and what was actually paid by each date, against the statutory 40% / 70% cumulative thresholds. Leave the estimate blank to skip this and use manual rows only.</p>' +
          '<div class="grid c3">' +
          field('Estimated tax liability for the year', 'advance_tax_estimate', p, 'number') +
          field('Paid by Poush end (1st instalment)', 'paid_by_poush', p, 'number') +
          field('Paid by Chaitra end (2nd instalment)', 'paid_by_chaitra', p, 'number') +
          field('Paid by Ashad end (3rd instalment)', 'paid_by_ashad', p, 'number', { hint: 'A shortfall still open here is a Sec 119 late-payment case below, not Sec 118' }) +
          '</div></div>';

        h += '<div class="card"><h3>Manual instalment-interest lines</h3><p class="card-note">For anything the simple three-date model above doesn\'t fit — a revised estimate mid-year, part-year incorporation, and so on. Additive to the auto-computed lines, not a replacement.</p><div class="table-wrap"><table><thead><tr>' +
          '<th>Label</th><th class="num">Shortfall</th><th class="num">Months</th><th></th></tr></thead><tbody>';
        (p.instalment_shortfalls || []).forEach((s, i) => {
          h += '<tr><td><input data-inst="' + i + '" data-k="label" value="' + esc(s.label || '') + '" placeholder="Revised estimate"></td>' +
            '<td class="num"><input type="number" data-inst="' + i + '" data-k="shortfall" value="' + (s.shortfall || 0) + '"></td>' +
            '<td class="num"><input type="number" data-inst="' + i + '" data-k="months" value="' + (s.months || 0) + '"></td>' +
            '<td><button class="btn btn-small btn-danger" data-del-inst="' + i + '" title="Remove this row">Remove</button></td></tr>';
        });
        h += '</tbody></table></div><button class="btn btn-small" id="add-inst">Add a line</button></div>';

        h += '<div class="card"><h3>Sec 119 — interest on tax paid late</h3><div class="grid c3">' +
          field('Tax unpaid past the due date', 'tax_unpaid', p, 'number') +
          field('Months unpaid', 'months_unpaid', p, 'number') +
          '</div></div>';

        h += '<div class="card"><h3>Sec 120 — false or misleading statement</h3><p class="card-note">Additional tax found on reassessment because a statement made was false or misleading.</p><div class="grid c3">' +
          field('Understatement found on reassessment', 'understatement_amount', p, 'number') +
          field('Intent', 'understatement_intent', p, 'select', { options: [
            { value: 'careless', label: 'Careless' }, { value: 'knowing', label: 'Knowing / reckless' }] }) +
          '</div></div>';

        h += '<div class="card"><h3>TDS — withholding agent\'s own exposure</h3><div class="grid c3">' +
          field('TDS not deducted / not deposited', 'tds_shortfall', p, 'number') +
          field('Months outstanding', 'tds_months', p, 'number') +
          field('TDS returns filed late (count)', 'tds_returns_late', p, 'number') +
          '</div></div>';

        h += '<div class="card"><h3>VAT</h3><div class="grid c3">' +
          field('VAT returns filed late (count)', 'vat_returns_late', p, 'number') +
          field('VAT unpaid', 'vat_unpaid', p, 'number') +
          field('Months VAT unpaid', 'vat_months', p, 'number') +
          field('Turnover earned while unregistered', 'vat_unregistered_turnover', p, 'number') +
          field('Months trading unregistered', 'vat_unregistered_months', p, 'number') +
          '<div></div></div></div>';

        const CAT_LABEL = { income_tax: 'Income tax', tds: 'TDS', vat: 'VAT' };
        h += resultCard('Exposure', ['Category', 'Head of charge', 'Basis', 'Amount'],
          res.lines.map(l => [CAT_LABEL[l.category] || l.category, l.head, l.basis, l.amount]),
          ['Total exposure', '', '', res.total]);
        return h;
      },
      wire(root, file, rerender) {
        wireSimple(root, file, rerender);
        const p = file.payload;
        root.querySelectorAll('[data-inst]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.inst;
          p.instalment_shortfalls[i][e.target.dataset.k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-del-inst]').forEach(el => el.addEventListener('click', e => {
          p.instalment_shortfalls.splice(+e.target.dataset.delInst, 1); rerender();
        }));
        const a = root.querySelector('#add-inst');
        if (a) a.addEventListener('click', () => {
          p.instalment_shortfalls = p.instalment_shortfalls || [];
          p.instalment_shortfalls.push({ label: '', shortfall: 0, months: 0 }); rerender();
        });
      },
      tabulate: res => [{
        title: 'Exposure', headers: ['Category', 'Head of charge', 'Basis', 'Amount'],
        rows: res.lines.map(l => [l.category, l.head, l.basis, l.amount]).concat([['', 'Total', '', res.total]])
      }]
    },

    uae_corporate_tax: {
      code: 'CT', name: 'UAE Corporate Tax', group: 'Tax computation',
      blurb: 'Federal Decree-Law No. 47 of 2022 — 0% on taxable income up to AED 375,000, 9% above it. Bring the already-adjusted taxable income figure here; unlike Business Income Tax (Nepal), this module does not itself walk book-profit-to-taxable-income adjustments.',
      seed: () => ({ taxable_income: 0 }),
      view(file, res) {
        const p = file.payload;
        let h = '<div class="card"><h3>Taxable income</h3><div class="grid c3">' +
          field('Taxable income (AED)', 'taxable_income', p, 'number', { hint: 'After any Small Business Relief or free-zone Qualifying Income election — those elections are not computed by this module.' }) +
          '</div></div>';

        if (res.slab_lines) {
          h += resultCard('Slab-wise tax', ['Band', 'Rate', 'Income in band', 'Tax'],
            res.slab_lines.map(l => [l.band, pct(l.rate), l.income, l.tax]));
          h += '<div class="grid c3">' +
            kpi('Tax before rebate', money(res.tax_before_rebate)) +
            kpi('Rebate', money(res.rebate)) +
            kpi('Tax payable', money(res.tax_payable), 'accent') +
            '</div>';
        }
        return h;
      },
      wire(root, file, rerender) { wireSimple(root, file, rerender); },
      tabulate: res => [{
        title: 'Computation', headers: ['Band', 'Rate', 'Income in band', 'Tax'],
        rows: (res.slab_lines || []).map(l => [l.band, pct(l.rate), l.income, l.tax])
          .concat([['', '', 'Tax payable', res.tax_payable]])
      }]
    },

    uae_penalties: {
      code: 'UP', name: 'UAE Penalties', group: 'Tax computation',
      blurb: 'Federal Tax Authority administrative penalties (Cabinet Decision 49/2021, late-payment mechanics as revised by 108/2021) — late registration/deregistration, late return filing, record-keeping failures and the capped late-payment charge, each raised only when its own facts are present.',
      seed: () => ({ late_registration: false, late_deregistration: false, late_return_filed: false,
        late_return_is_repeat: false, records_not_kept: false, records_repeat_violation: false,
        unpaid_tax: 0, months_since_due_date: 0 }),
      view(file, res) {
        const p = file.payload;
        let h = '<div class="card"><h3>Registration &amp; filing</h3><div class="grid c3">' +
          '<label class="check"><input type="checkbox" data-flag="late_registration"' + (p.late_registration ? ' checked' : '') + '> Late VAT/Corporate Tax registration</label>' +
          '<label class="check"><input type="checkbox" data-flag="late_deregistration"' + (p.late_deregistration ? ' checked' : '') + '> Late deregistration application</label>' +
          '<div></div>' +
          '<label class="check"><input type="checkbox" data-flag="late_return_filed"' + (p.late_return_filed ? ' checked' : '') + '> Return filed late</label>' +
          '<label class="check"><input type="checkbox" data-flag="late_return_is_repeat"' + (p.late_return_is_repeat ? ' checked' : '') + '> ...a repeat violation within 24 months</label>' +
          '<div></div></div></div>';

        h += '<div class="card"><h3>Record-keeping</h3><div class="grid c3">' +
          '<label class="check"><input type="checkbox" data-flag="records_not_kept"' + (p.records_not_kept ? ' checked' : '') + '> Required records not kept</label>' +
          '<label class="check"><input type="checkbox" data-flag="records_repeat_violation"' + (p.records_repeat_violation ? ' checked' : '') + '> ...a repeat violation</label>' +
          '<div></div></div></div>';

        h += '<div class="card"><h3>Late payment</h3><div class="grid c3">' +
          field('Tax unpaid past the due date', 'unpaid_tax', p, 'number') +
          field('Whole months elapsed since the due date', 'months_since_due_date', p, 'number',
            { hint: '0 = still within the due date — no late-payment penalty yet' }) +
          '</div></div>';

        h += resultCard('Exposure', ['Head', 'Basis', 'Amount'],
          res.lines.map(l => [l.head, l.basis, l.amount]), ['Total exposure', '', res.total]);
        return h;
      },
      wire(root, file, rerender) {
        wireSimple(root, file, rerender);
        root.querySelectorAll('[data-flag]').forEach(el => el.addEventListener('change', e => {
          file.payload[e.target.dataset.flag] = e.target.checked; rerender();
        }));
      },
      tabulate: res => [{
        title: 'Exposure', headers: ['Head', 'Basis', 'Amount'],
        rows: res.lines.map(l => [l.head, l.basis, l.amount]).concat([['', 'Total', res.total]])
      }]
    },

    tds: {
      code: 'TD', name: 'TDS / Withholding', group: 'Tax computation',
      blurb: 'Withholding on payments the client makes as a deducting agent — rent, contract, professional fees, commission, interest, dividend, non-resident, meeting fee, freight, vehicle rent, royalty — rate looked up per category from Rate Master, grouped into a per-payee certificate ready to issue.',
      seed: () => ({ entries: [], signatory: { name: '', designation: 'ca', membership_no: '', firm_name: '', place: '', date: '', udin: '' } }),
      bulkImport: {
        apply(file, sheets) {
          const p = file.payload;
          p.entries = (p.entries || []).concat((sheets.entries.rows || []).map(r => ({
            payee_name: r.payee_name || '', payee_pan: r.payee_pan || '', category: r.category || '',
            description: r.description || '', gross_amount: r.gross_amount || 0, rate_override: r.rate_override,
            payment_date: r.payment_date || '', deposited: !!r.deposited, deposit_date: r.deposit_date || '',
            deposit_ref: r.deposit_ref || '',
          })));
        }
      },
      view(file, res) {
        const p = file.payload;
        p.entries = p.entries || []; p.signatory = p.signatory || {};
        const catOptions = (res.categories || []).map(c => ({ value: c.slug, label: c.label + ' (' + c.rate.toFixed(2) + '%)' }));

        let h = '<div class="card"><h3>Payments</h3><p class="card-note">Leave the rate override blank to use the category\'s Rate Master rate — only set it when this specific payment genuinely differs (a treaty rate, a lower-deduction certificate).</p>' +
          '<div class="table-wrap"><table><thead><tr><th>Date</th><th>Payee</th><th>PAN</th><th>Category</th>' +
          '<th>Description</th><th class="num">Gross</th><th class="num">Rate override %</th><th class="num">TDS</th>' +
          '<th class="num">Net</th><th>Deposited</th><th>Deposit date</th><th>Deposit ref</th><th></th></tr></thead><tbody>';
        p.entries.forEach((e, i) => {
          const computed = (res.entries || [])[i];
          h += '<tr>' +
            '<td><input type="date" data-tds="' + i + '" data-k="payment_date" value="' + esc(e.payment_date || '') + '"></td>' +
            '<td><input data-tds="' + i + '" data-k="payee_name" value="' + esc(e.payee_name || '') + '"></td>' +
            '<td><input data-tds="' + i + '" data-k="payee_pan" value="' + esc(e.payee_pan || '') + '"></td>' +
            '<td><select data-tds="' + i + '" data-k="category">' + '<option value="">— choose —</option>' +
            catOptions.map(o => '<option value="' + o.value + '"' + (e.category === o.value ? ' selected' : '') + '>' + esc(o.label) + '</option>').join('') + '</select></td>' +
            '<td><input data-tds="' + i + '" data-k="description" value="' + esc(e.description || '') + '"></td>' +
            '<td class="num"><input type="number" data-tds="' + i + '" data-k="gross_amount" value="' + (e.gross_amount || 0) + '"></td>' +
            '<td class="num"><input type="number" step="0.01" data-tds="' + i + '" data-k="rate_override" value="' + (e.rate_override ?? '') + '"></td>' +
            '<td class="num">' + (computed ? money(computed.tds_amount) : '—') + '</td>' +
            '<td class="num">' + (computed ? money(computed.net_amount) : '—') + '</td>' +
            '<td><input type="checkbox" data-tds-flag="' + i + '"' + (e.deposited ? ' checked' : '') + '></td>' +
            '<td><input type="date" data-tds="' + i + '" data-k="deposit_date" value="' + esc(e.deposit_date || '') + '"></td>' +
            '<td><input data-tds="' + i + '" data-k="deposit_ref" value="' + esc(e.deposit_ref || '') + '"></td>' +
            '<td><button class="btn btn-small btn-danger" data-del-tds="' + i + '" title="Remove this row">Remove</button></td></tr>';
        });
        h += '</tbody></table></div><button class="btn btn-small" id="add-tds">Add a payment</button></div>';

        if (res.summary) {
          const s = res.summary;
          h += '<div class="grid c4">' +
            kpi('Total TDS', money(s.total_tds), 'accent') +
            kpi('Deposited', money(s.deposited_tds)) +
            kpi('Pending deposit', money(s.pending_tds), s.pending_tds ? 'alert' : '') +
            kpi('Net paid out', money(s.total_net)) +
            '</div>';
          h += resultCard('By category', ['Category', 'Gross', 'TDS'], s.by_category.map(c => [c.label, c.gross, c.tds]));
        }
        if (res.by_payee && res.by_payee.length) {
          h += resultCard('By payee (this becomes one certificate each)', ['Payee', 'PAN', 'Gross', 'TDS'],
            res.by_payee.map(pp => [pp.payee_name, pp.payee_pan, pp.gross, pp.tds]));
        }

        h += '<div class="card"><h3>Certificate signatory</h3><div class="grid c3" id="tds-signatory">' +
          field('Signatory name', 'name', p.signatory, 'text') +
          field('Designation', 'designation', p.signatory, 'select', { options: [
            { value: 'ca', label: 'Chartered Accountant (CA)' }, { value: 'ra', label: 'Registered Auditor (RA)' }] }) +
          field('Membership no.', 'membership_no', p.signatory, 'text') +
          field('Firm name', 'firm_name', p.signatory, 'text') +
          field('Place', 'place', p.signatory, 'text') +
          field('Date', 'date', p.signatory, 'date') +
          field('UDIN (if applicable)', 'udin', p.signatory, 'text') +
          '</div></div>';

        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        wireInto(root, 'tds-signatory', p.signatory, rerender);

        root.querySelectorAll('[data-tds]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.tds, k = e.target.dataset.k;
          p.entries[i][k] = (e.target.type === 'number' && e.target.value !== '') ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-tds-flag]').forEach(el => el.addEventListener('change', e => {
          p.entries[+e.target.dataset.tdsFlag].deposited = e.target.checked; rerender();
        }));
        root.querySelectorAll('[data-del-tds]').forEach(el => el.addEventListener('click', e => {
          p.entries.splice(+e.target.dataset.delTds, 1); rerender();
        }));
        const addTds = root.querySelector('#add-tds');
        if (addTds) addTds.addEventListener('click', () => {
          p.entries.push({ payee_name: '', payee_pan: '', category: '', description: '', gross_amount: 0,
            rate_override: null, payment_date: '', deposited: false, deposit_date: '', deposit_ref: '' });
          rerender();
        });
      },
      tabulate: res => [{
        title: 'Register',
        headers: ['Date', 'Payee', 'PAN', 'Category', 'Description', 'Gross', 'Rate %', 'TDS', 'Net paid', 'Deposited', 'Deposit date', 'Deposit ref'],
        rows: (res.entries || []).map(e => [e.payment_date, e.payee_name, e.payee_pan, e.category_label, e.description || '',
          e.gross_amount, e.rate, e.tds_amount, e.net_amount, e.deposited ? 'Yes' : 'No', e.deposit_date || '', e.deposit_ref || ''])
      }, {
        title: 'Summary by category', headers: ['Category', 'Gross', 'TDS'],
        rows: ((res.summary && res.summary.by_category) || []).map(c => [c.label, c.gross, c.tds])
      }, {
        title: 'Summary by payee', headers: ['Payee', 'PAN', 'Gross', 'TDS'],
        rows: (res.by_payee || []).map(p => [p.payee_name, p.payee_pan, p.gross, p.tds])
      }]
    },

    vat: {
      code: 'VT', name: 'VAT Return', group: 'Tax computation',
      blurb: 'Output VAT on sales and input VAT on purchases, netted against the credit brought forward — blocked-input categories (Rule 41) excluded from the credit, and the credit apportioned automatically when exempt sales are mixed in with taxable ones — ready for the monthly or trimester return.',
      seed: () => ({
        entity: { name: '', pan: '', period_label: '' },
        sales: [], purchases: [],
        credit_notes_issued: 0, debit_notes_issued: 0,
        credit_notes_received: 0, debit_notes_received: 0,
        opening_credit: 0, mixed_supply: false,
        signatory: { name: '', designation: 'ca', membership_no: '', firm_name: '', place: '', date: '', udin: '' },
      }),
      view(file, res) {
        const p = file.payload;
        p.entity = p.entity || {}; p.sales = p.sales || []; p.purchases = p.purchases || []; p.signatory = p.signatory || {};
        const salesRes = res.sales || [], purchRes = res.purchases || [];

        let h = '<div class="card"><h3>Entity</h3><div class="grid c3" id="vat-entity">' +
          field('Name', 'name', p.entity, 'text') +
          field('PAN', 'pan', p.entity, 'text') +
          field('Period', 'period_label', p.entity, 'text', { placeholder: 'FY ' + state.fy }) +
          '</div></div>';

        h += '<div class="card"><h3>Sales</h3><p class="card-note">Taxable, zero-rated (exports) and exempt sales, split per line — output VAT is computed on the taxable amount only, at the Rate Master standard rate.</p>' +
          '<div class="table-wrap"><table><thead><tr><th>Date</th><th>Party</th><th>Description</th>' +
          '<th class="num">Taxable</th><th class="num">Zero-rated</th><th class="num">Exempt</th><th class="num">Output VAT</th><th></th></tr></thead><tbody>';
        p.sales.forEach((s, i) => {
          const c = salesRes[i];
          h += '<tr>' +
            '<td><input type="date" data-vs="' + i + '" data-k="date" value="' + esc(s.date || '') + '"></td>' +
            '<td><input data-vs="' + i + '" data-k="party" value="' + esc(s.party || '') + '"></td>' +
            '<td><input data-vs="' + i + '" data-k="desc" value="' + esc(s.desc || '') + '"></td>' +
            '<td class="num"><input type="number" data-vs="' + i + '" data-k="taxable_amount" value="' + (s.taxable_amount || 0) + '"></td>' +
            '<td class="num"><input type="number" data-vs="' + i + '" data-k="zero_rated_amount" value="' + (s.zero_rated_amount || 0) + '"></td>' +
            '<td class="num"><input type="number" data-vs="' + i + '" data-k="exempt_amount" value="' + (s.exempt_amount || 0) + '"></td>' +
            '<td class="num">' + (c ? money(c.vat_amount) : '—') + '</td>' +
            '<td><button class="btn btn-small btn-danger" data-del-vs="' + i + '" title="Remove this row">Remove</button></td></tr>';
        });
        h += '</tbody></table></div><button class="btn btn-small" id="add-vs">Add a sale</button></div>';

        h += '<div class="card"><h3>Purchases</h3><p class="card-note">Mark a line "blocked" when it falls under a Rule 41 category (reference list below) — it stays on the register but earns no input credit. "Capital goods" is for reporting only.</p>' +
          '<div class="table-wrap"><table><thead><tr><th>Date</th><th>Party</th><th>Description</th>' +
          '<th class="num">Taxable</th><th>Blocked</th><th>Capital goods</th><th class="num">Input VAT</th><th></th></tr></thead><tbody>';
        p.purchases.forEach((pu, i) => {
          const c = purchRes[i];
          h += '<tr>' +
            '<td><input type="date" data-vp="' + i + '" data-k="date" value="' + esc(pu.date || '') + '"></td>' +
            '<td><input data-vp="' + i + '" data-k="party" value="' + esc(pu.party || '') + '"></td>' +
            '<td><input data-vp="' + i + '" data-k="desc" value="' + esc(pu.desc || '') + '"></td>' +
            '<td class="num"><input type="number" data-vp="' + i + '" data-k="taxable_amount" value="' + (pu.taxable_amount || 0) + '"></td>' +
            '<td><input type="checkbox" data-vp-flag="' + i + ',blocked"' + (pu.blocked ? ' checked' : '') + '></td>' +
            '<td><input type="checkbox" data-vp-flag="' + i + ',capital_goods"' + (pu.capital_goods ? ' checked' : '') + '></td>' +
            '<td class="num">' + (c ? money(c.vat_amount) : '—') + '</td>' +
            '<td><button class="btn btn-small btn-danger" data-del-vp="' + i + '" title="Remove this row">Remove</button></td></tr>';
        });
        h += '</tbody></table></div><button class="btn btn-small" id="add-vp">Add a purchase</button></div>';

        h += '<div class="card"><h3>Adjustments and carry-forward</h3><div class="grid c3" id="vat-adjustments">' +
          field('Credit notes issued (reduces output)', 'credit_notes_issued', p, 'number') +
          field('Debit notes issued (increases output)', 'debit_notes_issued', p, 'number') +
          field('Credit notes received (reduces input)', 'credit_notes_received', p, 'number') +
          field('Debit notes received (increases input)', 'debit_notes_received', p, 'number') +
          field('Opening credit brought forward', 'opening_credit', p, 'number') +
          '</div><label class="check" style="margin-top:8px"><input type="checkbox" data-flag="mixed_supply"' + (p.mixed_supply ? ' checked' : '') +
          '> Mixed taxable and exempt supply — restrict the input credit proportionately (input VAT can\'t be traced separately to each)</label></div>';

        if (res.totals) {
          const t = res.totals;
          h += '<div class="grid c4">' +
            kpi('Output VAT', money(t.output_vat), 'accent') +
            kpi('Eligible input VAT', money(t.eligible_input_vat)) +
            kpi(t.payable ? 'VAT payable' : 'Credit carried forward', money(t.payable || t.credit_carried_forward), t.payable ? 'alert' : '') +
            kpi('Blocked purchases (no credit)', money(t.blocked_purchases)) +
            '</div>';
          h += resultCard('Return summary', ['Particulars', 'Amount'], [
            ['Taxable sales', t.taxable_sales], ['Zero-rated sales', t.zero_rated_sales], ['Exempt sales', t.exempt_sales],
            ['Net taxable sales', t.net_taxable_sales], ['Output VAT', t.output_vat],
            ['Creditable purchases', t.creditable_purchases], ['Net creditable purchases', t.net_creditable_purchases],
            ['Input VAT (gross)', t.input_vat_gross],
            ['Apportionment ratio', t.mixed_supply ? t.apportionment_ratio.toFixed(2) + '%' : 'n/a'],
            ['Eligible input VAT', t.eligible_input_vat], ['Restricted input VAT', t.restricted_input_vat],
            ['Opening credit b/f', t.opening_credit], ['Total credit available', t.total_credit_available],
            ['VAT payable', t.payable], ['Credit carried forward', t.credit_carried_forward],
          ]);
        }

        if (res.blocked_categories && res.blocked_categories.length) {
          h += '<div class="card"><h3>Reference — blocked input categories (Rule 41(1))</h3><ul>' +
            res.blocked_categories.map(c => '<li>' + esc(c) + '</li>').join('') + '</ul></div>';
        }

        h += '<div class="card"><h3>Signatory</h3><div class="grid c3" id="vat-signatory">' +
          field('Signatory name', 'name', p.signatory, 'text') +
          field('Designation', 'designation', p.signatory, 'select', { options: [
            { value: 'ca', label: 'Chartered Accountant (CA)' }, { value: 'ra', label: 'Registered Auditor (RA)' }] }) +
          field('Membership no.', 'membership_no', p.signatory, 'text') +
          field('Firm name', 'firm_name', p.signatory, 'text') +
          field('Place', 'place', p.signatory, 'text') +
          field('Date', 'date', p.signatory, 'date') +
          field('UDIN (if applicable)', 'udin', p.signatory, 'text') +
          '</div></div>';

        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        wireInto(root, 'vat-entity', p.entity, rerender);
        wireInto(root, 'vat-adjustments', p, rerender);
        wireInto(root, 'vat-signatory', p.signatory, rerender);

        root.querySelectorAll('[data-flag]').forEach(el => el.addEventListener('change', e => {
          p[e.target.dataset.flag] = e.target.checked; rerender();
        }));

        root.querySelectorAll('[data-vs]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.vs, k = e.target.dataset.k;
          p.sales[i][k] = (e.target.type === 'number' && e.target.value !== '') ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-del-vs]').forEach(el => el.addEventListener('click', e => {
          p.sales.splice(+e.target.dataset.delVs, 1); rerender();
        }));
        const addVs = root.querySelector('#add-vs');
        if (addVs) addVs.addEventListener('click', () => {
          p.sales.push({ date: '', party: '', desc: '', taxable_amount: 0, zero_rated_amount: 0, exempt_amount: 0 });
          rerender();
        });

        root.querySelectorAll('[data-vp]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.vp, k = e.target.dataset.k;
          p.purchases[i][k] = (e.target.type === 'number' && e.target.value !== '') ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-vp-flag]').forEach(el => el.addEventListener('change', e => {
          const [i, k] = e.target.dataset.vpFlag.split(',');
          p.purchases[+i][k] = e.target.checked; rerender();
        }));
        root.querySelectorAll('[data-del-vp]').forEach(el => el.addEventListener('click', e => {
          p.purchases.splice(+e.target.dataset.delVp, 1); rerender();
        }));
        const addVp = root.querySelector('#add-vp');
        if (addVp) addVp.addEventListener('click', () => {
          p.purchases.push({ date: '', party: '', desc: '', taxable_amount: 0, blocked: false, capital_goods: false });
          rerender();
        });
      },
      tabulate: res => [{
        title: 'Sales', headers: ['Date', 'Party', 'Description', 'Taxable', 'Zero-rated', 'Exempt', 'Output VAT'],
        rows: (res.sales || []).map(s => [s.date, s.party, s.desc, s.taxable_amount, s.zero_rated_amount, s.exempt_amount, s.vat_amount])
      }, {
        title: 'Purchases', headers: ['Date', 'Party', 'Description', 'Taxable', 'Blocked', 'Capital goods', 'Input VAT'],
        rows: (res.purchases || []).map(p => [p.date, p.party, p.desc, p.taxable_amount, p.blocked ? 'Yes' : 'No', p.capital_goods ? 'Yes' : 'No', p.vat_amount])
      }, {
        title: 'Return summary', headers: ['Particulars', 'Amount'],
        rows: res.totals ? (() => { const t = res.totals; return [
          ['Taxable sales', t.taxable_sales], ['Zero-rated sales', t.zero_rated_sales],
          ['Exempt sales', t.exempt_sales], ['Credit notes issued', t.credit_notes_issued],
          ['Debit notes issued', t.debit_notes_issued], ['Net taxable sales', t.net_taxable_sales],
          ['Output VAT @ ' + Math.round(res.rate) + '%', t.output_vat],
          ['Creditable purchases', t.creditable_purchases], ['Blocked purchases (no credit)', t.blocked_purchases],
          ['— of which capital goods', t.capital_purchases],
          ['Credit notes received', t.credit_notes_received], ['Debit notes received', t.debit_notes_received],
          ['Net creditable purchases', t.net_creditable_purchases], ['Input VAT (gross)', t.input_vat_gross],
          [t.mixed_supply ? 'Apportionment ratio applied' : 'Apportionment (not applicable — no exempt mix)', t.apportionment_ratio.toFixed(2) + '%'],
          ['Eligible input VAT', t.eligible_input_vat], ['Input VAT restricted (exempt-supply mix)', t.restricted_input_vat],
          ['Opening credit brought forward', t.opening_credit], ['Total credit available', t.total_credit_available],
          ['VAT payable', t.payable], ['Credit carried forward', t.credit_carried_forward],
        ]; })() : []
      }]
    },

    inventory: {
      code: 'IV', name: 'Inventory (NAS 2)', group: 'Accounting',
      blurb: 'Raw materials, work in progress and finished goods carried at the lower of cost and net realisable value — FIFO or weighted average, any number of materials, equivalent-unit process costing for WIP with normal-capacity overhead absorption, normal and abnormal loss, and the full journal register.',
      seed: () => ({
        entity: { name: '', period_label: '' },
        raw_materials: [],
        wip: { closing_material_pct: 100, closing_conversion_pct: 100 },
        fg: { costing_method: 'weighted_avg', opening: {}, sales: [] }
      }),
      bulkImport: {
        apply(file, sheets) {
          const p = file.payload;
          p.raw_materials = p.raw_materials || [];
          (sheets.raw_materials.rows || []).forEach(r => {
            if (!r.name) return;
            let m = p.raw_materials.find(x => x.name === r.name);
            if (!m) {
              m = { name: r.name, unit: r.unit || '', costing_method: r.costing_method || 'weighted_avg',
                opening: { date: '', qty: 0, rate: 0 }, purchases: [], issues: [],
                replacement_cost_per_unit: 0, fg_expected_below_cost: false };
              p.raw_materials.push(m);
            }
            m.opening.qty = r.opening_qty || 0;
            m.opening.rate = r.opening_rate || 0;
            m.replacement_cost_per_unit = r.replacement_cost_per_unit || 0;
            m.fg_expected_below_cost = !!r.fg_expected_below_cost;
          });
          (sheets.rm_purchases.rows || []).forEach(r => {
            const m = p.raw_materials.find(x => x.name === r.material);
            if (!m) return;
            m.purchases = m.purchases || [];
            m.purchases.push({ date: r.date || '', qty: r.qty || 0, rate: r.rate || 0, freight: r.freight || 0,
              duty: r.duty || 0, other_cost: r.other_cost || 0, discount: r.discount || 0, desc: r.desc || '' });
          });
          (sheets.rm_issues.rows || []).forEach(r => {
            const m = p.raw_materials.find(x => x.name === r.material);
            if (!m) return;
            m.issues = m.issues || [];
            m.issues.push({ date: r.date || '', qty: r.qty || 0, desc: r.desc || '' });
          });
          p.fg = p.fg || { costing_method: 'weighted_avg', opening: {}, sales: [] };
          p.fg.sales = p.fg.sales || [];
          (sheets.fg_sales.rows || []).forEach(r => {
            p.fg.sales.push({ date: r.date || '', qty: r.qty || 0, desc: r.desc || '' });
          });
        }
      },
      view(file, res) {
        const p = file.payload;
        p.entity = p.entity || {}; p.raw_materials = p.raw_materials || [];
        p.wip = p.wip || {}; p.fg = p.fg || { opening: {}, sales: [] };
        p.fg.opening = p.fg.opening || {}; p.fg.sales = p.fg.sales || [];
        const COSTING = [{ value: 'fifo', label: 'FIFO' }, { value: 'weighted_avg', label: 'Weighted average' }];
        const rmRes = {}; (res.raw_materials || []).forEach(m => { rmRes[m.name] = m; });

        let h = '<div class="card"><h3>Entity</h3><div class="grid c3" id="inv-entity">' +
          field('Name', 'name', p.entity, 'text') +
          field('Period', 'period_label', p.entity, 'text', { placeholder: 'FY ' + state.fy }) +
          '</div></div>';

        h += '<div class="card"><h3>Raw materials</h3><p class="card-note">Add as many materials as needed. Each carries its own costing method, opening balance, purchases (landed cost — freight, duty, other cost, discount all roll into the unit rate) and issues to production. A material is written down to replacement cost only when the finished goods it feeds are expected to sell below cost — the NAS 2 rule, not a blanket lower-of-cost-NRV on raw materials.</p>';
        p.raw_materials.forEach((m, i) => {
          const computed = rmRes[m.name];
          h += '<details class="card" open style="margin-top:10px"><summary><b>' + esc(m.name || 'New material') + '</b>' +
            (computed ? '<span class="hint"> — closing ' + money(computed.closing_qty) + ' ' + esc(m.unit || '') + ' @ ' + money(computed.closing_unit_cost) + ' = ' + money(computed.closing_value) + '</span>' : '') +
            '</summary><div class="grid c3" style="margin-top:12px">' +
            '<div class="field"><label>Name</label><input data-rm="' + i + '" data-k="name" value="' + esc(m.name || '') + '"></div>' +
            '<div class="field"><label>Unit</label><input data-rm="' + i + '" data-k="unit" value="' + esc(m.unit || '') + '"></div>' +
            '<div class="field"><label>Costing method</label><select data-rm="' + i + '" data-k="costing_method">' +
            COSTING.map(o => '<option value="' + o.value + '"' + (m.costing_method === o.value ? ' selected' : '') + '>' + o.label + '</option>').join('') + '</select></div>' +
            '<div class="field"><label>Opening date</label><input type="date" data-rmopen="' + i + '" data-k="date" value="' + esc((m.opening && m.opening.date) || '') + '"></div>' +
            '<div class="field"><label>Opening qty</label><input type="number" data-rmopen="' + i + '" data-k="qty" value="' + ((m.opening && m.opening.qty) || 0) + '"></div>' +
            '<div class="field"><label>Opening rate</label><input type="number" data-rmopen="' + i + '" data-k="rate" value="' + ((m.opening && m.opening.rate) || 0) + '"></div>' +
            '<div class="field"><label>Replacement cost per unit</label><input type="number" data-rm="' + i + '" data-k="replacement_cost_per_unit" value="' + (m.replacement_cost_per_unit || 0) + '"></div>' +
            '<label class="check"><input type="checkbox" data-rmflag="' + i + '"' + (m.fg_expected_below_cost ? ' checked' : '') + '> The finished goods this feeds are expected to sell below cost</label>' +
            '</div>';

          h += '<div class="table-wrap" style="margin-top:12px"><b style="font-size:12px">Purchases</b><table><thead><tr>' +
            '<th>Date</th><th class="num">Qty</th><th class="num">Rate</th><th class="num">Freight</th><th class="num">Duty</th>' +
            '<th class="num">Other cost</th><th class="num">Discount</th><th>Description</th><th></th></tr></thead><tbody>';
          (m.purchases || []).forEach((pu, pi) => {
            h += '<tr>' +
              '<td><input type="date" data-rmp="' + i + ',' + pi + '" data-k="date" value="' + esc(pu.date || '') + '"></td>' +
              ['qty', 'rate', 'freight', 'duty', 'other_cost', 'discount'].map(k =>
                '<td class="num"><input type="number" data-rmp="' + i + ',' + pi + '" data-k="' + k + '" value="' + (pu[k] || 0) + '"></td>').join('') +
              '<td><input data-rmp="' + i + ',' + pi + '" data-k="desc" value="' + esc(pu.desc || '') + '"></td>' +
              '<td><button class="btn btn-small btn-danger" data-del-rmp="' + i + ',' + pi + '" title="Remove this row">Remove</button></td></tr>';
          });
          h += '</tbody></table></div><button class="btn btn-small" data-add-rmp="' + i + '">Add a purchase</button>';

          h += '<div class="table-wrap" style="margin-top:12px"><b style="font-size:12px">Issues to production</b><table><thead><tr>' +
            '<th>Date</th><th class="num">Qty</th><th>Description</th><th></th></tr></thead><tbody>';
          (m.issues || []).forEach((is, ii) => {
            h += '<tr><td><input type="date" data-rmi="' + i + ',' + ii + '" data-k="date" value="' + esc(is.date || '') + '"></td>' +
              '<td class="num"><input type="number" data-rmi="' + i + ',' + ii + '" data-k="qty" value="' + (is.qty || 0) + '"></td>' +
              '<td><input data-rmi="' + i + ',' + ii + '" data-k="desc" value="' + esc(is.desc || '') + '"></td>' +
              '<td><button class="btn btn-small btn-danger" data-del-rmi="' + i + ',' + ii + '" title="Remove this row">Remove</button></td></tr>';
          });
          h += '</tbody></table></div><button class="btn btn-small" data-add-rmi="' + i + '">Add an issue</button>';

          if (computed && computed.write_down) h += '<div class="card-note" style="margin-top:10px">Written down to replacement cost: ' + money(computed.write_down) + '</div>';
          if (computed && computed.shortfall_qty) h += '<div class="card-note" style="margin-top:10px;color:var(--crimson)">Issues exceed what was available by ' + money(computed.shortfall_qty) + ' ' + esc(m.unit || '') + ' — check the quantities.</div>';
          h += '<button class="btn btn-small btn-danger" style="margin-top:10px" data-del-rm="' + i + '">Remove this material</button></details>';
        });
        h += '<button class="btn" style="margin-top:10px" id="add-rm">Add a raw material</button></div>';

        h += '<div class="card"><h3>Work in progress</h3><p class="card-note">Equivalent-unit process costing (weighted average). Fixed overhead is absorbed on normal capacity — if actual production falls short, the shortfall is expensed as a period cost, not carried into inventory. Normal loss reduces the material cost pool by its scrap value; abnormal loss is costed at the same rate as good production and expensed immediately, net of its own scrap recovery.</p><div class="grid c3" id="inv-wip">' +
          field('Opening WIP — units', 'opening_qty', p.wip, 'number') +
          field('Opening WIP — cost', 'opening_cost', p.wip, 'number') +
          field('Units introduced this period', 'units_introduced', p.wip, 'number') +
          field('Material cost override (blank = use raw materials consumed above)', 'material_cost_override', p.wip, 'number') +
          field('Direct labour cost', 'direct_labor_cost', p.wip, 'number') +
          field('Variable overhead', 'variable_overhead', p.wip, 'number') +
          field('Budgeted fixed overhead', 'budgeted_fixed_overhead', p.wip, 'number') +
          field('Normal capacity — units', 'normal_capacity_units', p.wip, 'number') +
          field('Actual production — units', 'actual_production_units', p.wip, 'number') +
          field('Normal loss — units', 'normal_loss_units', p.wip, 'number') +
          field('Abnormal loss — units', 'abnormal_loss_units', p.wip, 'number') +
          field('Scrap value per unit of loss', 'scrap_rate_per_unit', p.wip, 'number') +
          field('Units completed and transferred to FG', 'units_completed_transferred', p.wip, 'number') +
          field('Closing WIP — units', 'closing_wip_units', p.wip, 'number') +
          field('Closing WIP — % complete for material', 'closing_material_pct', p.wip, 'number') +
          field('Closing WIP — % complete for conversion', 'closing_conversion_pct', p.wip, 'number') +
          field('NRV — expected selling price per unit', 'nrv_selling_price_per_unit', p.wip, 'number') +
          field('NRV — cost to complete per unit', 'nrv_cost_to_complete_per_unit', p.wip, 'number') +
          field('NRV — selling cost per unit', 'nrv_selling_cost_per_unit', p.wip, 'number') +
          '</div></div>';

        if (res.wip) {
          const w = res.wip;
          h += resultCard('Work in progress — computed', ['Particulars', 'Amount'], [
            ['Fixed overhead rate per unit', w.fixed_overhead_rate], ['Fixed overhead absorbed', w.fixed_overhead_absorbed],
            ['Unabsorbed fixed overhead (expensed)', w.unabsorbed_fixed_overhead],
            ['Equivalent units — material', w.equivalent_units_material], ['Equivalent units — conversion', w.equivalent_units_conversion],
            ['Rate per equivalent unit — material', w.material_rate_per_eu], ['Rate per equivalent unit — conversion', w.conversion_rate_per_eu],
            ['Abnormal loss cost (expensed)', w.abnormal_loss_cost],
            ['Transferred to finished goods', w.transferred_cost], ['Closing WIP before NRV', w.closing_wip_value_before_nrv],
            ['NRV write-down', w.nrv_write_down]
          ], ['Closing WIP value', w.closing_wip_value]);
          if (!w.reconciles) h += '<div class="card-note" style="color:var(--crimson)">WIP units do not reconcile — opening + introduced should equal transferred + closing + normal loss + abnormal loss (' + w.units_to_account + ' vs ' + w.units_accounted + '). Check the inputs above.</div>';
        }

        h += '<div class="card"><h3>Finished goods</h3><div class="grid c3" id="inv-fg">' +
          '<div class="field"><label>Costing method</label><select data-fgk="costing_method">' +
          COSTING.map(o => '<option value="' + o.value + '"' + (p.fg.costing_method === o.value ? ' selected' : '') + '>' + o.label + '</option>').join('') + '</select></div>' +
          '<div class="field"><label>Opening date</label><input type="date" data-fgopen="date" value="' + esc(p.fg.opening.date || '') + '"></div>' +
          '<div class="field"><label>Opening qty</label><input type="number" data-fgopen="qty" value="' + (p.fg.opening.qty || 0) + '"></div>' +
          '<div class="field"><label>Opening rate</label><input type="number" data-fgopen="rate" value="' + (p.fg.opening.rate || 0) + '"></div>' +
          '<div class="field"><label>Transfer-in date (from WIP)</label><input type="date" data-fgk="transfer_date" value="' + esc(p.fg.transfer_date || '') + '"></div>' +
          '<div></div>' +
          field('Selling price per unit', 'selling_price_per_unit', p.fg, 'number') +
          field('Selling cost per unit', 'selling_cost_per_unit', p.fg, 'number') +
          '</div>';
        h += '<div class="table-wrap" style="margin-top:12px"><b style="font-size:12px">Sales</b><table><thead><tr>' +
          '<th>Date</th><th class="num">Qty</th><th>Description</th><th></th></tr></thead><tbody>';
        p.fg.sales.forEach((s, si) => {
          h += '<tr><td><input type="date" data-fgs="' + si + '" data-k="date" value="' + esc(s.date || '') + '"></td>' +
            '<td class="num"><input type="number" data-fgs="' + si + '" data-k="qty" value="' + (s.qty || 0) + '"></td>' +
            '<td><input data-fgs="' + si + '" data-k="desc" value="' + esc(s.desc || '') + '"></td>' +
            '<td><button class="btn btn-small btn-danger" data-del-fgs="' + si + '" title="Remove this row">Remove</button></td></tr>';
        });
        h += '</tbody></table></div><button class="btn btn-small" id="add-fgs">Add a sale</button></div>';

        if (res.fg) {
          const f = res.fg;
          h += resultCard('Finished goods — computed', ['Particulars', 'Amount'], [
            ['Closing qty', f.closing_qty], ['Closing rate', f.closing_unit_cost], ['Write-down to NRV', f.write_down],
            ['Cost of goods sold — qty', f.cogs_qty], ['Cost of goods sold — value', f.cogs_value]
          ], ['Closing finished goods value', f.closing_value]);
          if (f.shortfall_qty) h += '<div class="card-note" style="color:var(--crimson)">Sales exceed what was available by ' + money(f.shortfall_qty) + ' — check quantities/transfers.</div>';
        }

        if (res.summary) {
          const s = res.summary;
          h += resultCard('Summary', ['Particulars', 'Amount'], [
            ['Closing raw materials', s.closing_raw_materials], ['Closing WIP', s.closing_wip],
            ['Closing finished goods', s.closing_finished_goods], ['Total NRV write-down', s.total_nrv_write_down],
            ['Cost of goods sold', s.cogs], ['Abnormal loss expense', s.abnormal_loss_expense],
            ['Unabsorbed overhead expense', s.unabsorbed_overhead_expense]
          ], ['Total closing inventory', s.closing_inventory_value]);
        }

        h += journalTable('Journal entries', res.journals);
        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        wireInto(root, 'inv-entity', p.entity, rerender);
        wireInto(root, 'inv-wip', p.wip, rerender);
        wireInto(root, 'inv-fg', p.fg, rerender);

        root.querySelectorAll('[data-rm]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.rm, k = e.target.dataset.k;
          p.raw_materials[i][k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-rmflag]').forEach(el => el.addEventListener('change', e => {
          p.raw_materials[+e.target.dataset.rmflag].fg_expected_below_cost = e.target.checked; rerender();
        }));
        root.querySelectorAll('[data-rmopen]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.rmopen, k = e.target.dataset.k;
          p.raw_materials[i].opening = p.raw_materials[i].opening || {};
          p.raw_materials[i].opening[k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-del-rm]').forEach(el => el.addEventListener('click', e => {
          p.raw_materials.splice(+e.target.dataset.delRm, 1); rerender();
        }));
        const addRm = root.querySelector('#add-rm');
        if (addRm) addRm.addEventListener('click', () => {
          p.raw_materials.push({ name: '', unit: '', costing_method: 'weighted_avg', opening: { date: '', qty: 0, rate: 0 },
            purchases: [], issues: [], replacement_cost_per_unit: 0, fg_expected_below_cost: false });
          rerender();
        });

        root.querySelectorAll('[data-rmp]').forEach(el => el.addEventListener('change', e => {
          const [i, pi] = e.target.dataset.rmp.split(',').map(Number);
          const k = e.target.dataset.k;
          p.raw_materials[i].purchases[pi][k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-del-rmp]').forEach(el => el.addEventListener('click', e => {
          const [i, pi] = e.target.dataset.delRmp.split(',').map(Number);
          p.raw_materials[i].purchases.splice(pi, 1); rerender();
        }));
        root.querySelectorAll('[data-add-rmp]').forEach(el => el.addEventListener('click', e => {
          const i = +e.target.dataset.addRmp;
          p.raw_materials[i].purchases = p.raw_materials[i].purchases || [];
          p.raw_materials[i].purchases.push({ date: '', qty: 0, rate: 0, freight: 0, duty: 0, other_cost: 0, discount: 0, desc: '' });
          rerender();
        }));

        root.querySelectorAll('[data-rmi]').forEach(el => el.addEventListener('change', e => {
          const [i, ii] = e.target.dataset.rmi.split(',').map(Number);
          const k = e.target.dataset.k;
          p.raw_materials[i].issues[ii][k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-del-rmi]').forEach(el => el.addEventListener('click', e => {
          const [i, ii] = e.target.dataset.delRmi.split(',').map(Number);
          p.raw_materials[i].issues.splice(ii, 1); rerender();
        }));
        root.querySelectorAll('[data-add-rmi]').forEach(el => el.addEventListener('click', e => {
          const i = +e.target.dataset.addRmi;
          p.raw_materials[i].issues = p.raw_materials[i].issues || [];
          p.raw_materials[i].issues.push({ date: '', qty: 0, desc: '' });
          rerender();
        }));

        root.querySelectorAll('[data-fgk]').forEach(el => el.addEventListener('change', e => {
          p.fg[e.target.dataset.fgk] = e.target.value; rerender();
        }));
        root.querySelectorAll('[data-fgopen]').forEach(el => el.addEventListener('change', e => {
          p.fg.opening = p.fg.opening || {};
          const k = e.target.dataset.fgopen;
          p.fg.opening[k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-fgs]').forEach(el => el.addEventListener('change', e => {
          const si = +e.target.dataset.fgs, k = e.target.dataset.k;
          p.fg.sales[si][k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-del-fgs]').forEach(el => el.addEventListener('click', e => {
          p.fg.sales.splice(+e.target.dataset.delFgs, 1); rerender();
        }));
        const addFgs = root.querySelector('#add-fgs');
        if (addFgs) addFgs.addEventListener('click', () => { p.fg.sales.push({ date: '', qty: 0, desc: '' }); rerender(); });

        wireSimple(root, file, rerender);
      },
      tabulate: res => [{
        title: 'Summary', headers: ['Particulars', 'Amount'],
        rows: res.summary ? [
          ['Entity', (res.entity && res.entity.name) || ''], ['Period', (res.entity && res.entity.period_label) || ''],
          ['Closing raw materials', res.summary.closing_raw_materials], ['Closing WIP', res.summary.closing_wip],
          ['Closing finished goods', res.summary.closing_finished_goods],
          ['Total closing inventory', res.summary.closing_inventory_value],
          ['Total NRV write-down', res.summary.total_nrv_write_down], ['Cost of goods sold', res.summary.cogs],
          ['Abnormal loss expense', res.summary.abnormal_loss_expense],
          ['Unabsorbed overhead expense', res.summary.unabsorbed_overhead_expense],
        ] : []
      }, {
        // Per-material ledgers (like salary_tds's per-employee sheets) are left
        // to the full online workbook — this covers the closing position of
        // every material, not the day-by-day movement of each one.
        title: 'Raw materials', headers: ['Material', 'Unit', 'Method', 'Closing qty', 'Closing rate', 'Closing value', 'Write-down', 'Valuation basis'],
        rows: (res.raw_materials || []).map(m => [m.name, m.unit, m.costing_method, m.closing_qty, m.closing_unit_cost, m.closing_value, m.write_down, m.valuation_basis])
      }, {
        title: 'WIP', headers: ['Particulars', 'Units', 'Amount'],
        rows: res.wip ? (() => { const w = res.wip; return [
          ['Opening WIP — units / cost', w.opening_qty, w.opening_cost], ['Units introduced', w.units_introduced, ''],
          ['Material cost', w.material_cost, ''], ['Direct labour', w.direct_labor_cost, ''],
          ['Variable overhead', w.variable_overhead, ''],
          ['Fixed overhead — rate per unit (normal capacity basis)', w.fixed_overhead_rate, ''],
          ['Fixed overhead absorbed', w.fixed_overhead_absorbed, ''],
          ['Unabsorbed fixed overhead (expensed, not inventoried)', w.unabsorbed_fixed_overhead, ''],
          ['Total conversion cost', w.total_conversion_cost, ''],
          ['Normal loss units / scrap value', w.normal_loss_units, w.normal_scrap_value],
          ['Abnormal loss units / net cost expensed', w.abnormal_loss_units, w.abnormal_loss_cost],
          ['Equivalent units — material', w.equivalent_units_material, ''],
          ['Equivalent units — conversion', w.equivalent_units_conversion, ''],
          ['Rate per equivalent unit — material', w.material_rate_per_eu, ''],
          ['Rate per equivalent unit — conversion', w.conversion_rate_per_eu, ''],
          ['Transferred to FG — units / cost', w.transferred_units, w.transferred_cost],
          ['Closing WIP — units / value before NRV', w.closing_wip_units, w.closing_wip_value_before_nrv],
          ['NRV write-down', '', w.nrv_write_down], ['Closing WIP value', '', w.closing_wip_value],
          ['Units to account for / accounted for', w.units_to_account, w.units_accounted],
        ]; })() : []
      }, {
        title: 'Finished goods', headers: ['Particulars', 'Amount'],
        rows: res.fg ? [
          ['Costing method', res.fg.costing_method], ['Closing quantity', res.fg.closing_qty],
          ['Closing rate', res.fg.closing_unit_cost], ['Write-down to NRV', res.fg.write_down],
          ['Closing value', res.fg.closing_value], ['COGS quantity', res.fg.cogs_qty], ['COGS value', res.fg.cogs_value],
        ] : []
      }].concat(journalTabulateTables(res.journals))
    },

    fixed_assets: {
      code: 'FA', name: 'Depreciation (Accounts)', group: 'Accounting',
      needsFiscalYears: true,
      blurb: 'Book depreciation under NAS 16, kept separate from the Schedule 2 tax pools — asset "types" you define yourself, SLM or WDV by class or overridden per asset, additions and disposals pro-rated to the day from a real calendar date, an asset master and depreciation summary, and the full journal register.',
      seed: () => ({ entity: { name: '', period_label: '' }, classes: [] }),
      bulkImport: {
        apply(file, sheets) {
          const p = file.payload;
          p.classes = p.classes || [];
          (sheets.classes.rows || []).forEach(r => {
            if (!r.key) return;
            let c = p.classes.find(x => x.key === r.key);
            if (!c) { c = { key: r.key, label: r.label || r.key, method: r.method || 'wdv', assets: [] }; p.classes.push(c); }
            c.label = r.label || c.label; c.method = r.method || c.method || 'wdv';
            c.rate_pct = r.rate_pct || c.rate_pct; c.useful_life_years = r.useful_life_years || c.useful_life_years;
            c.residual_pct = r.residual_pct || c.residual_pct;
            c.opening_block_nbv = r.opening_block_nbv || c.opening_block_nbv;
            c.opening_block_remaining_life_years = r.opening_block_remaining_life_years || c.opening_block_remaining_life_years;
            c.opening_block_residual_value = r.opening_block_residual_value || c.opening_block_residual_value;
          });
          (sheets.assets.rows || []).forEach(r => {
            const c = p.classes.find(x => x.key === r.class_key);
            if (!c) return;
            c.assets = c.assets || [];
            c.assets.push({
              code: r.code || '', name: r.name || '', status: r.status || (r.acquisition_date ? 'addition' : 'existing'),
              acquisition_date: r.acquisition_date || '', cost: r.cost || 0, additional_cost: r.additional_cost || 0,
              opening_nbv: r.opening_nbv || 0,
              opening_nbv_remaining_life_years: r.opening_nbv_remaining_life_years || 0,
              opening_nbv_residual_value: r.opening_nbv_residual_value || 0,
              method: r.method || '', rate_pct: r.rate_pct || null, useful_life_years: r.useful_life_years || null,
              residual_pct: r.residual_pct || null, business_use_pct: r.business_use_pct || 100,
              disposal_date: r.disposal_date || '', disposal_proceeds: r.disposal_proceeds || 0
            });
          });
        }
      },
      view(file, res) {
        const p = file.payload;
        p.entity = p.entity || {}; p.classes = p.classes || [];
        const METHODS = [{ value: 'wdv', label: 'WDV (written-down value)' }, { value: 'slm', label: 'SLM (straight line)' }];
        const METHOD_OVERRIDE = [{ value: '', label: '— use class default —' }].concat(METHODS);
        const STATUS = [{ value: 'existing', label: 'Existing (carried forward)' }, { value: 'addition', label: 'Addition this year' }];
        const resClasses = {}; (res.classes || []).forEach(c => { resClasses[c.key] = c; });

        let h = '<div class="card"><h3>Entity</h3><div class="grid c3" id="fa-entity">' +
          field('Name', 'name', p.entity, 'text') +
          field('Period', 'period_label', p.entity, 'text', { placeholder: 'FY ' + state.fy }) +
          '</div></div>';

        h += '<div class="card"><h3>Asset classes</h3><p class="card-note">Add as many asset "types" as you need — Furniture, Vehicles, Computer &amp; IT equipment, and so on. Each carries its own default method and rate/life; an individual asset can override any of these. The opening-balance fields are only for a legacy amount not yet broken down into individual assets — leave them at zero once every asset is tracked one by one.</p>';

        p.classes.forEach((cls, ci) => {
          cls.assets = cls.assets || [];
          const rc = resClasses[cls.key];
          const rawAssets = (rc && rc.assets) || [];
          const userAssets = (Number(cls.opening_block_nbv) ? rawAssets.slice(1) : rawAssets);
          const blockComputed = Number(cls.opening_block_nbv) ? rawAssets[0] : null;

          h += '<details class="card" open style="margin-top:10px"><summary><b>' + esc(cls.label || 'New class') + '</b>' +
            (rc ? '<span class="hint"> — closing NBV ' + money(rc.totals.closing_nbv) + ', depreciation ' + money(rc.totals.depreciation) + '</span>' : '') +
            '</summary><div class="grid c3" style="margin-top:12px" id="fa-cls-' + ci + '">' +
            '<div class="field"><label>Class key</label><input data-cls="' + ci + '" data-k="key" value="' + esc(cls.key || '') + '"></div>' +
            '<div class="field"><label>Class label</label><input data-cls="' + ci + '" data-k="label" value="' + esc(cls.label || '') + '"></div>' +
            '<div class="field"><label>Method</label><select data-cls="' + ci + '" data-k="method">' +
            METHODS.map(o => '<option value="' + o.value + '"' + (cls.method === o.value ? ' selected' : '') + '>' + o.label + '</option>').join('') + '</select></div>' +
            '<div class="field"><label>WDV rate % p.a.</label><input type="number" data-cls="' + ci + '" data-k="rate_pct" value="' + (cls.rate_pct || 0) + '"></div>' +
            '<div class="field"><label>SLM useful life (years)</label><input type="number" data-cls="' + ci + '" data-k="useful_life_years" value="' + (cls.useful_life_years || 0) + '"></div>' +
            '<div class="field"><label>SLM residual value %</label><input type="number" data-cls="' + ci + '" data-k="residual_pct" value="' + (cls.residual_pct || 0) + '"></div>' +
            '</div>';

          h += '<details style="margin-top:10px"><summary>Unallocated opening balance (legacy, optional)' +
            (blockComputed ? ' — <span class="hint">depreciation ' + money(blockComputed.depreciation) + ', closing ' + money(blockComputed.closing_nbv) + '</span>' : '') +
            '</summary><div class="grid c3" style="margin-top:12px">' +
            '<div class="field"><label>Opening NBV not broken into assets</label><input type="number" data-cls="' + ci + '" data-k="opening_block_nbv" value="' + (cls.opening_block_nbv || 0) + '"></div>' +
            '<div class="field"><label>Remaining SLM life (years)</label><input type="number" data-cls="' + ci + '" data-k="opening_block_remaining_life_years" value="' + (cls.opening_block_remaining_life_years || 0) + '"></div>' +
            '<div class="field"><label>SLM residual value (amount)</label><input type="number" data-cls="' + ci + '" data-k="opening_block_residual_value" value="' + (cls.opening_block_residual_value || 0) + '"></div>' +
            '</div></details>';

          h += '<div class="table-wrap" style="margin-top:12px"><b style="font-size:12px">Assets</b><table><thead><tr>' +
            '<th>Code</th><th>Name</th><th>Status</th><th>Acquisition date</th><th class="num">Cost</th><th class="num">Addl. cost</th>' +
            '<th class="num">Opening NBV</th><th class="num">Remaining life (existing SLM)</th><th class="num">Residual value (existing SLM)</th>' +
            '<th>Method override</th><th class="num">Rate % override</th><th class="num">Life yrs override</th><th class="num">Residual % override</th>' +
            '<th class="num">Business use %</th><th>Disposal date</th><th class="num">Disposal proceeds</th>' +
            '<th class="num">Depreciation</th><th class="num">Closing NBV</th><th></th></tr></thead><tbody>';
          cls.assets.forEach((a, ai) => {
            const computed = userAssets[ai];
            h += '<tr>' +
              '<td><input data-asset="' + ci + ',' + ai + '" data-k="code" value="' + esc(a.code || '') + '"></td>' +
              '<td><input data-asset="' + ci + ',' + ai + '" data-k="name" value="' + esc(a.name || '') + '"></td>' +
              '<td><select data-asset="' + ci + ',' + ai + '" data-k="status">' +
              STATUS.map(o => '<option value="' + o.value + '"' + (a.status === o.value ? ' selected' : '') + '>' + o.label + '</option>').join('') + '</select></td>' +
              '<td><input type="date" data-asset="' + ci + ',' + ai + '" data-k="acquisition_date" value="' + esc(a.acquisition_date || '') + '"></td>' +
              '<td class="num"><input type="number" data-asset="' + ci + ',' + ai + '" data-k="cost" value="' + (a.cost || 0) + '"></td>' +
              '<td class="num"><input type="number" data-asset="' + ci + ',' + ai + '" data-k="additional_cost" value="' + (a.additional_cost || 0) + '"></td>' +
              '<td class="num"><input type="number" data-asset="' + ci + ',' + ai + '" data-k="opening_nbv" value="' + (a.opening_nbv || 0) + '"></td>' +
              '<td class="num"><input type="number" data-asset="' + ci + ',' + ai + '" data-k="opening_nbv_remaining_life_years" value="' + (a.opening_nbv_remaining_life_years || 0) + '"></td>' +
              '<td class="num"><input type="number" data-asset="' + ci + ',' + ai + '" data-k="opening_nbv_residual_value" value="' + (a.opening_nbv_residual_value || 0) + '"></td>' +
              '<td><select data-asset="' + ci + ',' + ai + '" data-k="method">' +
              METHOD_OVERRIDE.map(o => '<option value="' + o.value + '"' + ((a.method || '') === o.value ? ' selected' : '') + '>' + o.label + '</option>').join('') + '</select></td>' +
              '<td class="num"><input type="number" data-asset="' + ci + ',' + ai + '" data-k="rate_pct" value="' + (a.rate_pct ?? '') + '"></td>' +
              '<td class="num"><input type="number" data-asset="' + ci + ',' + ai + '" data-k="useful_life_years" value="' + (a.useful_life_years ?? '') + '"></td>' +
              '<td class="num"><input type="number" data-asset="' + ci + ',' + ai + '" data-k="residual_pct" value="' + (a.residual_pct ?? '') + '"></td>' +
              '<td class="num"><input type="number" data-asset="' + ci + ',' + ai + '" data-k="business_use_pct" value="' + (a.business_use_pct || 100) + '"></td>' +
              '<td><input type="date" data-asset="' + ci + ',' + ai + '" data-k="disposal_date" value="' + esc(a.disposal_date || '') + '"></td>' +
              '<td class="num"><input type="number" data-asset="' + ci + ',' + ai + '" data-k="disposal_proceeds" value="' + (a.disposal_proceeds || 0) + '"></td>' +
              '<td class="num">' + (computed ? money(computed.depreciation) : '–') + '</td>' +
              '<td class="num">' + (computed ? money(computed.closing_nbv) : '–') + '</td>' +
              '<td><button class="btn btn-small btn-danger" data-del-asset="' + ci + ',' + ai + '" title="Remove this row">Remove</button></td></tr>';
            if (computed && computed.gain_loss_on_disposal != null) {
              h += '<tr><td colspan="19" class="card-note">Gain/(loss) on disposal: ' + money(computed.gain_loss_on_disposal) + '</td></tr>';
            }
          });
          h += '</tbody></table></div><button class="btn btn-small" data-add-asset="' + ci + '">Add an asset</button>' +
            '<button class="btn btn-small btn-danger" style="margin-left:8px" data-del-cls="' + ci + '">Remove this class</button></details>';
        });
        h += '<div class="card"><button class="btn" id="add-cls">Add an asset class</button></div>';

        if (res.classes && res.classes.length) {
          h += resultCard('Depreciation summary — by class', ['Class', 'Method', 'Opening NBV', 'Additions', 'Depreciation', 'Closing NBV', 'Disposal proceeds', 'Gain/(loss)'],
            res.classes.map(c => [c.label, c.method.toUpperCase(), c.totals.opening_nbv, c.totals.additions_cost, c.totals.depreciation, c.totals.closing_nbv, c.totals.disposal_proceeds, c.totals.gain_loss]),
            ['Total', '', res.summary.opening_nbv, res.summary.additions_cost, res.summary.depreciation, res.summary.closing_nbv, res.summary.disposal_proceeds, res.summary.gain_loss_on_disposal]);
        }

        h += journalTable('Journal entries', res.journals);
        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        wireInto(root, 'fa-entity', p.entity, rerender);

        root.querySelectorAll('[data-cls]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.cls, k = e.target.dataset.k;
          p.classes[i][k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-del-cls]').forEach(el => el.addEventListener('click', e => {
          p.classes.splice(+e.target.dataset.delCls, 1); rerender();
        }));
        const addCls = root.querySelector('#add-cls');
        if (addCls) addCls.addEventListener('click', () => {
          p.classes.push({ key: '', label: '', method: 'wdv', rate_pct: 0, useful_life_years: 0, residual_pct: 0, assets: [] });
          rerender();
        });

        root.querySelectorAll('[data-asset]').forEach(el => el.addEventListener('change', e => {
          const [ci, ai] = e.target.dataset.asset.split(',').map(Number);
          const k = e.target.dataset.k;
          const numeric = ['cost', 'additional_cost', 'opening_nbv', 'opening_nbv_remaining_life_years',
            'opening_nbv_residual_value', 'rate_pct', 'useful_life_years', 'residual_pct', 'business_use_pct', 'disposal_proceeds'];
          let v = e.target.value;
          if (numeric.includes(k)) v = v === '' ? null : Number(v);
          p.classes[ci].assets[ai][k] = v;
          rerender();
        }));
        root.querySelectorAll('[data-del-asset]').forEach(el => el.addEventListener('click', e => {
          const [ci, ai] = e.target.dataset.delAsset.split(',').map(Number);
          p.classes[ci].assets.splice(ai, 1); rerender();
        }));
        root.querySelectorAll('[data-add-asset]').forEach(el => el.addEventListener('click', e => {
          const ci = +e.target.dataset.addAsset;
          p.classes[ci].assets = p.classes[ci].assets || [];
          p.classes[ci].assets.push({ code: '', name: '', status: 'addition', acquisition_date: '', cost: 0,
            additional_cost: 0, opening_nbv: 0, opening_nbv_remaining_life_years: 0, opening_nbv_residual_value: 0,
            method: '', rate_pct: null, useful_life_years: null, residual_pct: null, business_use_pct: 100,
            disposal_date: '', disposal_proceeds: 0 });
          rerender();
        }));
      },
      tabulate: res => [{
        title: 'Summary', headers: ['Particulars', 'Amount'],
        rows: [
          ['Entity', (res.entity && res.entity.name) || ''], ['Period', (res.entity && res.entity.period_label) || ''],
          ['Fiscal year — from', res.fy_start || ''], ['Fiscal year — to', res.fy_end || ''],
          ['Opening net book value', res.summary.opening_nbv], ['Additions during the year (at cost)', res.summary.additions_cost],
          ['Depreciation charge for the year', res.summary.depreciation], ['Closing net book value', res.summary.closing_nbv],
          ['Disposal proceeds', res.summary.disposal_proceeds], ['Gain / (loss) on disposal', res.summary.gain_loss_on_disposal],
        ]
      }, {
        title: 'Depreciation summary', headers: ['Class', 'Method', 'Rate / life', 'Opening NBV', 'Additions', 'Depreciation', 'Closing NBV', 'Disposal proceeds', 'Gain/(loss)'],
        rows: (res.classes || []).map(c => {
          const basis = c.method === 'wdv' && c.rate_pct != null ? c.rate_pct + '% WDV'
            : (c.method === 'slm' && c.useful_life_years != null ? c.useful_life_years + ' yrs SLM' : c.method.toUpperCase());
          const t = c.totals;
          return [c.label, c.method.toUpperCase(), basis, t.opening_nbv, t.additions_cost, t.depreciation, t.closing_nbv, t.disposal_proceeds, t.gain_loss];
        }).concat([['Total', '', '', res.summary.opening_nbv, res.summary.additions_cost, res.summary.depreciation,
                    res.summary.closing_nbv, res.summary.disposal_proceeds, res.summary.gain_loss_on_disposal]])
      }, {
        title: 'Asset master', headers: ['Class', 'Code', 'Name', 'Status', 'Method', 'Acquisition date', 'Cost', 'Opening NBV', 'Depreciation', 'Closing NBV', 'Disposal date', 'Disposal proceeds', 'Gain/(loss)'],
        rows: (res.classes || []).reduce((acc, c) => acc.concat((c.assets || []).map(a =>
          [c.label, a.code, a.name, a.status, a.method, a.acquisition_date || '', a.cost, a.opening_nbv, a.depreciation,
           a.closing_nbv, a.disposal_date || '', a.disposal_proceeds, a.gain_loss_on_disposal])), [])
      }].concat(journalTabulateTables(res.journals))
    },

    emi: {
      code: 'LN', name: 'Loan & EMI', group: 'Accounting',
      blurb: 'Any repayment frequency, a real start date (so a facility taken years ago has actual dates, not a bare period count), mid-term modifications, and — once a processing fee applies — the NFRS 9 amortised-cost view, the journal register grouped by fiscal year, and the NAS 1 current/non-current split.',
      needsFiscalYears: true,
      seed: () => ({ principal: 0, annual_rate: 0, tenure_periods: 12, frequency: 'monthly', repayment: 'emi', modifications: [] }),
      view(file, res) {
        const p = file.payload;
        p.modifications = p.modifications || [];
        let h = '<div class="card"><h3>Loan terms</h3><div class="grid c3">' +
          field('Principal', 'principal', p, 'number') +
          field('Interest rate (% per year)', 'annual_rate', p, 'number') +
          field('Frequency', 'frequency', p, 'select', { options: [
            { value: 'monthly', label: 'Monthly' }, { value: 'quarterly', label: 'Quarterly' },
            { value: 'half_yearly', label: 'Half-yearly (6-month)' }, { value: 'annual', label: 'Annual' }] }) +
          field('Tenure (periods)', 'tenure_periods', p, 'number', { hint: 'Number of instalments at the frequency above' }) +
          field('Moratorium (periods)', 'moratorium_periods', p, 'number', { hint: 'Interest accrues and is capitalised' }) +
          field('Processing fee', 'processing_fee', p, 'number', { hint: 'Netted against the carrying amount and amortised at the effective rate (NFRS 9), not expensed upfront' }) +
          field('Repayment', 'repayment', p, 'select', { options: [
            { value: 'emi', label: 'Equal instalment (EMI)' }, { value: 'equal_principal', label: 'Equal principal' }] }) +
          field('Start date (AD)', 'start_date_ad', p, 'date', { hint: 'Leave blank for an abstract period-numbered schedule. A date already years in the past works — the schedule and journals fall where they actually did.' }) +
          '</div></div>';

        h += '<div class="card"><h3>Modifications</h3><p class="card-note">Rate change, tenure change and/or an extra payment applied after a given period — each becomes its own segment with a fresh instalment and a Sec 118/NFRS 9 modification gain or loss booked in the journal register.</p>' +
          '<div class="table-wrap"><table><thead><tr><th>After period</th><th class="num">New rate % (optional)</th>' +
          '<th class="num">New total tenure from here (optional)</th><th class="num">Extra payment (optional)</th><th></th></tr></thead><tbody>';
        p.modifications.forEach((m, i) => {
          h += '<tr><td class="num"><input type="number" data-mod="' + i + '" data-k="at_period" value="' + (m.at_period || 0) + '"></td>' +
            '<td class="num"><input type="number" step="0.01" data-mod="' + i + '" data-k="new_annual_rate" value="' + (m.new_annual_rate ?? '') + '"></td>' +
            '<td class="num"><input type="number" data-mod="' + i + '" data-k="new_tenure_periods" value="' + (m.new_tenure_periods ?? '') + '"></td>' +
            '<td class="num"><input type="number" data-mod="' + i + '" data-k="extra_payment" value="' + (m.extra_payment || 0) + '"></td>' +
            '<td><button class="btn btn-small btn-danger" data-del-mod="' + i + '" title="Remove this row">Remove</button></td></tr>';
        });
        h += '</tbody></table></div><button class="btn btn-small" id="add-mod">Add a modification</button></div>';

        if (res.rows && res.rows.length) {
          h += '<div class="card"><h3>Summary</h3><div class="grid c4">' +
            kpi('Instalment (first segment)', money(res.summary.emi), 'accent') +
            kpi('Total interest (contractual)', money(res.summary.total_interest)) +
            kpi('Total paid', money(res.summary.total_paid)) +
            kpi('Cost of borrowing', money(res.summary.total_cost), 'alert') +
            (res.summary.effective_annual_rate != null ? kpi('Effective annual rate', pct(res.summary.effective_annual_rate), 'accent') : '') +
            (res.summary.net_proceeds != null ? kpi('Net proceeds (carrying amount)', money(res.summary.net_proceeds)) : '') +
            '</div></div>';
          if (res.summary.eir_skipped_due_to_modification) {
            h += '<div class="card"><b>A processing fee and a modification are both present.</b> The amortised-cost/EIR view below reflects the original, unmodified contractual cash flows only — review the fee treatment manually for the post-modification segment.</div>';
          }

          h += resultCard('Contractual schedule (cash paid)', ['Period', 'Date', 'Opening', 'Instalment', 'Interest', 'Principal', 'Closing', 'Current', 'Non-current', 'Note'],
            res.rows.map(r => [r.period, r.date_ad ? (r.date_ad + ' (' + r.date_bs_approx + ' BS)') : '', r.opening, r.instalment, r.interest, r.principal, r.closing, r.current_portion, r.non_current_portion, r.note]));

          if (res.eir_rows && res.eir_rows.length) {
            h += resultCard('Amortised cost — effective interest rate method (NFRS 9)',
              ['Period', 'Opening', 'Effective interest', 'Cash paid', 'Principal reduction', 'Closing', 'Current', 'Non-current'],
              res.eir_rows.map(r => [r.period, r.opening, r.effective_interest, r.cash_flow, r.principal_reduction, r.closing, r.current_portion, r.non_current_portion]));
          }
          if (res.journals_by_fy && Object.keys(res.journals_by_fy).length) {
            Object.keys(res.journals_by_fy).sort().forEach(fy => {
              h += journalTable('Journal register — FY ' + fy, res.journals_by_fy[fy]);
            });
          } else {
            h += journalTable('Journal register', res.journals);
          }
        }
        return h;
      },
      wire(root, file, rerender) {
        wireSimple(root, file, rerender);
        const p = file.payload;
        p.modifications = p.modifications || [];
        root.querySelectorAll('[data-mod]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.mod, k = e.target.dataset.k;
          p.modifications[i][k] = e.target.value === '' ? null : Number(e.target.value);
          rerender();
        }));
        root.querySelectorAll('[data-del-mod]').forEach(el => el.addEventListener('click', e => {
          p.modifications.splice(+e.target.dataset.delMod, 1); rerender();
        }));
        const a = root.querySelector('#add-mod');
        if (a) a.addEventListener('click', () => { p.modifications.push({ at_period: 0 }); rerender(); });
      },
      tabulate: res => [{
        title: 'Amortisation schedule',
        headers: ['Period', 'Opening', 'Instalment', 'Interest', 'Principal', 'Closing'],
        rows: (res.rows || []).map(r => [r.period, r.opening, r.instalment, r.interest, r.principal, r.closing])
      }].concat((res.eir_rows || []).length ? [{
        title: 'Amortised cost (EIR)',
        headers: ['Period', 'Opening', 'Effective interest', 'Cash paid', 'Principal reduction', 'Closing'],
        rows: res.eir_rows.map(r => [r.period, r.opening, r.effective_interest, r.cash_flow, r.principal_reduction, r.closing])
      }] : []).concat((res.modifications_applied || []).length ? [{
        title: 'Modifications',
        headers: ['After period', 'Carrying amount', 'Revised PV at original rate', 'Gain / (loss)'],
        rows: res.modifications_applied.map(m => [m.at_period, m.carrying_amount, m.revised_pv_at_original_rate, m.gain_or_loss])
      }] : []).concat(journalTabulateTables(res.journals, res.journals_by_fy))
    },

    lease: {
      code: 'LS', name: 'Lease (NFRS 16)', group: 'Accounting',
      blurb: 'Any repayment frequency, a real start date, rent escalation on any pattern (one-off or repeating, e.g. +10% every 2 years), who bears the rental TDS, mid-term remeasurement, the short-term/low-value exemption, the journal register grouped by fiscal year and the current/non-current split.',
      needsFiscalYears: true,
      seed: () => ({ payment: 0, periods: 12, frequency: 'monthly', annual_rate: 0, timing: 'arrear',
        tds_borne_by: 'none', rent_steps: [], modifications: [], escalation: { type: 'pct', pct: 0, amount: 0, every_years: 1, first_increase_after_years: '' } }),
      view(file, res) {
        const p = file.payload;
        p.rent_steps = p.rent_steps || []; p.modifications = p.modifications || [];
        p.escalation = p.escalation || { type: 'pct', pct: 0, amount: 0, every_years: 1, first_increase_after_years: '' };
        if (!p.escalation.type) p.escalation.type = 'pct';
        let h = '<div class="card"><h3>Lease terms</h3><div class="grid c3">' +
          '<label class="check"><input type="checkbox" data-flag="short_term_or_low_value"' + (p.short_term_or_low_value ? ' checked' : '') +
          '> Short-term (≤12 months) or low-value asset — elect the recognition exemption</label>' +
          field('Base payment per period', 'payment', p, 'number', { hint: 'Used until the first escalation step, if any, below' }) +
          field('Frequency', 'frequency', p, 'select', { options: [
            { value: 'monthly', label: 'Monthly' }, { value: 'quarterly', label: 'Quarterly' },
            { value: 'half_yearly', label: 'Half-yearly (6-month)' }, { value: 'annual', label: 'Annual' }] }) +
          field('Number of periods', 'periods', p, 'number') +
          field('Start date (AD)', 'start_date_ad', p, 'date', { hint: 'Leave blank for an abstract period-numbered schedule' }) +
          (!p.short_term_or_low_value ? (
            field('Discount rate (% per year)', 'annual_rate', p, 'number', { hint: 'Incremental borrowing rate unless the rate implicit in the lease is known' }) +
            field('Payment timing', 'timing', p, 'select', { options: [
              { value: 'arrear', label: 'End of period' }, { value: 'advance', label: 'Start of period' }] }) +
            field('Initial direct costs', 'initial_direct_costs', p, 'number') +
            field('Lease incentives received', 'incentives', p, 'number') +
            field('Restoration provision', 'restoration_provision', p, 'number')
          ) : '') +
          '</div></div>';

        h += '<div class="card" id="lease-escalation-gen"><h3>Repeating escalation</h3>' +
          '<p class="card-note">For a regular pattern — e.g. "+10% every year", "+5% every 2 years", or "+10% after year 4, then every 2 years" (set the delay below), or a lease that instead steps up by a flat amount like "+NPR 2,000 every year" — set it here and generate the steps below automatically. For anything irregular that isn\'t just a delayed start, skip this and add one-off steps by hand below instead.</p>' +
          '<div class="grid c3">' +
          field('Increment type', 'type', p.escalation, 'select', { options: [
            { value: 'pct', label: 'Percentage (%) — compounding' }, { value: 'amount', label: 'Fixed amount — added each step' }],
            hint: 'Percentage compounds on the previous step; a fixed amount is simply added each time, never compounded.' }) +
          (p.escalation.type === 'amount'
            ? field('Increase amount', 'amount', p.escalation, 'number', { hint: 'Added to the previous step\'s payment each time (or the base payment, for the first one) — never compounded' })
            : field('Increase %', 'pct', p.escalation, 'number', { hint: 'Compounding — each step is this % above the previous step (or the base payment, for the first one)' })) +
          field('Every N years', 'every_years', p.escalation, 'number', { hint: 'Use a fraction for a sub-year interval, e.g. 0.5 for every 6 months' }) +
          field('First increase after N years (optional)', 'first_increase_after_years', p.escalation, 'number', { hint: 'Leave blank for the first increase to land after one full interval, same as "every N years" above. Set this to delay only the FIRST increase — e.g. every_years=2 with this set to 4 gives +X% at year 4, then again at year 6, 8...' }) +
          '<div class="field"><label>&nbsp;</label>' +
          '<button class="btn btn-small" id="gen-steps" title="Replaces the escalation steps below with a generated series based on the settings here">Generate steps</button> ' +
          '<button class="btn btn-small btn-ghost" id="clear-steps" title="Empty the escalation steps below — rent stays flat at the base payment">Clear steps</button>' +
          '</div></div></div>';

        h += '<div class="card"><h3>Rent escalation</h3><p class="card-note">Any pattern — a flat annual step, one increase then held for two years, anything — just list each period the payment changes and its new amount. Leave empty for a flat rent throughout. Generated above, or add your own rows here.</p>' +
          '<div class="table-wrap"><table><thead><tr><th>From period</th><th class="num">New payment</th><th></th></tr></thead><tbody>';
        p.rent_steps.forEach((s, i) => {
          h += '<tr><td class="num"><input type="number" data-step="' + i + '" data-k="from_period" value="' + (s.from_period || 1) + '"></td>' +
            '<td class="num"><input type="number" data-step="' + i + '" data-k="payment" value="' + (s.payment || 0) + '"></td>' +
            '<td><button class="btn btn-small btn-danger" data-del-step="' + i + '" title="Remove this row">Remove</button></td></tr>';
        });
        h += '</tbody></table></div><button class="btn btn-small" id="add-step">Add an escalation step</button></div>';

        if (!p.short_term_or_low_value) {
          h += '<div class="card"><h3>Rental TDS</h3><div class="grid c3">' +
            field('Borne by', 'tds_borne_by', p, 'select', { options: [
              { value: 'none', label: 'Not applicable / not withheld' },
              { value: 'lessor', label: 'Lessor — withheld from the payment (standard)' },
              { value: 'lessee', label: 'Lessee — pays rent gross, bears TDS separately' }] }) +
            (res.summary && res.summary.tds_rate != null ? '<div class="field"><label>Rate used</label><div style="padding:9px 0">' + pct(res.summary.tds_rate) + ' (Sec 88(1), Rate Master)</div></div>' : '<div></div>') +
            '<div></div></div></div>';

          h += '<div class="card"><h3>Modifications</h3><p class="card-note">A remeasurement after a given period — new payment, new discount rate and/or a new remaining term. The liability is remeasured at the new rate against the revised payments; the difference adjusts the right-of-use asset (NFRS 16), not profit or loss, unless the asset would go negative.</p>' +
            '<div class="table-wrap"><table><thead><tr><th>After period</th><th class="num">New payment (optional)</th>' +
            '<th class="num">New rate % (optional)</th><th class="num">New remaining periods (optional)</th><th></th></tr></thead><tbody>';
          p.modifications.forEach((m, i) => {
            h += '<tr><td class="num"><input type="number" data-mod="' + i + '" data-k="at_period" value="' + (m.at_period || 0) + '"></td>' +
              '<td class="num"><input type="number" data-mod="' + i + '" data-k="new_payment" value="' + (m.new_payment ?? '') + '"></td>' +
              '<td class="num"><input type="number" step="0.01" data-mod="' + i + '" data-k="new_annual_rate" value="' + (m.new_annual_rate ?? '') + '"></td>' +
              '<td class="num"><input type="number" data-mod="' + i + '" data-k="new_remaining_periods" value="' + (m.new_remaining_periods ?? '') + '"></td>' +
              '<td><button class="btn btn-small btn-danger" data-del-mod="' + i + '" title="Remove this row">Remove</button></td></tr>';
          });
          h += '</tbody></table></div><button class="btn btn-small" id="add-mod">Add a modification</button></div>';
        }

        if (res.regime === 'exempt' && res.rows && res.rows.length) {
          h += '<div class="card"><b>' + esc(res.summary.note) + '</b></div>';
          h += resultCard('Lease expense', ['Period', 'Date', 'Expense'],
            res.rows.map(r => [r.period, r.date_ad ? (r.date_ad + ' (' + r.date_bs_approx + ' BS)') : '', r.expense]),
            ['Total', '', res.summary.total_payments]);
          h += journalTable('Journal register', res.journals);
          return h;
        }

        if (res.rows && res.rows.length) {
          h += '<div class="card"><h3>Initial recognition</h3><div class="grid c4">' +
            kpi('Lease liability', money(res.summary.initial_liability), 'accent') +
            kpi('Right-of-use asset', money(res.summary.rou_asset), 'accent') +
            kpi('Total payments', money(res.summary.total_payments)) +
            kpi('Finance cost over term', money(res.summary.total_interest)) + '</div></div>';
          h += resultCard('Schedule', ['Period', 'Date', 'Opening liability', 'Payment', 'Interest', 'Principal', 'Closing liability', 'Current', 'Non-current', 'ROU amortisation', 'ROU carrying'],
            res.rows.map(r => [r.period, r.date_ad ? (r.date_ad + ' (' + r.date_bs_approx + ' BS)') : '', r.opening_liability, r.payment, r.interest, r.principal, r.closing_liability, r.current_portion, r.non_current_portion, r.rou_amortisation, r.rou_carrying]));
          if (res.journals_by_fy && Object.keys(res.journals_by_fy).length) {
            Object.keys(res.journals_by_fy).sort().forEach(fy => {
              h += journalTable('Journal register — FY ' + fy, res.journals_by_fy[fy]);
            });
          } else {
            h += journalTable('Journal register', res.journals);
          }
        }
        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        p.rent_steps = p.rent_steps || []; p.modifications = p.modifications || [];
        p.escalation = p.escalation || { type: 'pct', pct: 0, amount: 0, every_years: 1, first_increase_after_years: '' };
        // wireInto MUST run before wireSimple — see the identical note in the
        // biz_tax module's wire(). Here it means the escalation generator's
        // type/pct/amount/every_years fields would otherwise get silently
        // written onto the top-level payload instead of p.escalation, so
        // "Generate steps" would always read the untouched pct:0 default and
        // produce a flat (non-escalating) schedule no matter what was typed.
        wireInto(root, 'lease-escalation-gen', p.escalation, rerender);
        wireSimple(root, file, rerender);
        const genSteps = root.querySelector('#gen-steps');
        if (genSteps) genSteps.addEventListener('click', () => {
          p.rent_steps = leaseGenerateSteps(p);
          rerender();
        });
        const clearSteps = root.querySelector('#clear-steps');
        if (clearSteps) clearSteps.addEventListener('click', () => { p.rent_steps = []; rerender(); });
        root.querySelectorAll('[data-step]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.step;
          p.rent_steps[i][e.target.dataset.k] = Number(e.target.value);
          rerender();
        }));
        root.querySelectorAll('[data-del-step]').forEach(el => el.addEventListener('click', e => {
          p.rent_steps.splice(+e.target.dataset.delStep, 1); rerender();
        }));
        const addStep = root.querySelector('#add-step');
        if (addStep) addStep.addEventListener('click', () => { p.rent_steps.push({ from_period: 1, payment: 0 }); rerender(); });

        root.querySelectorAll('[data-mod]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.mod, k = e.target.dataset.k;
          p.modifications[i][k] = e.target.value === '' ? null : Number(e.target.value);
          rerender();
        }));
        root.querySelectorAll('[data-del-mod]').forEach(el => el.addEventListener('click', e => {
          p.modifications.splice(+e.target.dataset.delMod, 1); rerender();
        }));
        const addMod = root.querySelector('#add-mod');
        if (addMod) addMod.addEventListener('click', () => { p.modifications.push({ at_period: 0 }); rerender(); });
      },
      tabulate: res => (res.regime === 'exempt' ? [{
        title: 'Lease expense', headers: ['Period', 'Expense'],
        rows: (res.rows || []).map(r => [r.period, r.expense])
      }] : [{
        title: 'Lease schedule',
        headers: ['Period', 'Opening liability', 'Payment', 'Interest', 'Principal', 'Closing liability', 'ROU amortisation'],
        rows: (res.rows || []).map(r => [r.period, r.opening_liability, r.payment, r.interest, r.principal, r.closing_liability, r.rou_amortisation])
      }].concat((res.remeasurements || []).length ? [{
        title: 'Remeasurements',
        headers: ['After period', 'Old liability', 'New liability', 'ROU adjustment', 'ROU carrying after', 'P&L gain'],
        rows: res.remeasurements.map(m => [m.at_period, m.old_liability, m.new_liability, m.rou_adjustment, m.rou_carrying_after, m.pl_gain])
      }] : [])).concat(journalTabulateTables(res.journals, res.journals_by_fy))
    },

    fin_statements: {
      code: 'FS', name: 'Financial statements', group: 'Accounting',
      blurb: 'The one-stop financial statements module: pull figures already computed in Fixed Assets, Inventory, Loan/Lease, Business Tax, TDS and VAT straight in as trial-balance rows, or upload/type a trial balance yourself (opening balances included) — every ledger mapped to a financial-statement head, tied, and turned into a balance sheet, P&L and real notes to accounts with accounting policies and disclosures.',
      seed: () => ({
        entity: { name: '', address: '', pan: '', fy_label: '', prev_label: '' },
        rows: [], closing_stock: { current: 0, previous: 0 },
        accounting_policy: 'These financial statements have been prepared in accordance with Nepal Financial ' +
          'Reporting Standards (NFRS) for Small and Medium-sized Entities, on the historical cost basis, and are ' +
          'presented in Nepalese Rupees.\n\nBasis of accounting: the accrual basis is used throughout.\n\n' +
          'Property, plant and equipment: stated at cost less accumulated depreciation. Depreciation is charged ' +
          'on a [straight-line / reducing-balance] basis over each asset’s estimated useful life.\n\n' +
          'Inventories: valued at the lower of cost and net realisable value.\n\n' +
          'Revenue recognition: revenue is recognised when the significant risks and rewards of ownership have ' +
          'transferred to the customer.\n\nTaxation: current tax is provided at the rates and on the basis ' +
          'enacted at the reporting date.\n\n[This is a starting draft — edit every bracketed choice and ' +
          'anything else that doesn’t match this entity before it is relied on.]',
        disclosures: { related_party: '', contingent_liabilities: '', capital_commitments: '', subsequent_events: '' },
      }),
      bulkImport: {
        apply(file, sheets) {
          file.payload.rows = file.payload.rows.concat((sheets.tb.rows || []).map(r =>
            ({ name: r.name || '', group: r.group || '', opening_dr: r.opening_dr || 0, opening_cr: r.opening_cr || 0,
               dr: r.dr || 0, cr: r.cr || 0, prev: r.prev, head: '' })));
        }
      },
      // -------------------------------------------------------- pull-in helpers
      // Shared between rendering (what figure/button to show) and the actual
      // pull action, so the two can never drift apart.
      _pullRows(src) {
        const f = src.figures || {};
        const common = { source_module: src.module_key, source_workfile_id: src.workfile_id, source_version: src.version,
                          source_unverified: !!(src.unverified && src.unverified.length) };
        const tag = (t, extra) => Object.assign({}, common, { source_tag: t }, extra);
        if (src.module_key === 'fixed_assets') {
          return [
            tag('ppe', { name: 'Fixed assets — net book value (' + (src.title || '') + ')', group: '',
              dr: f.closing_nbv || 0, cr: 0, head: 'ppe',
              source_movement: { opening_nbv: f.opening_nbv, additions_cost: f.additions_cost, depreciation: f.depreciation,
                disposal_proceeds: f.disposal_proceeds, closing_nbv: f.closing_nbv } }),
            tag('depreciation', { name: 'Depreciation for the year (' + (src.title || '') + ')', group: '',
              dr: f.depreciation || 0, cr: 0, head: 'depreciation' }),
          ];
        }
        if (src.module_key === 'inventory') {
          return [tag('inventories', { name: 'Closing inventory (' + (src.title || '') + ')', group: '',
            dr: f.closing_inventory_value || 0, cr: 0, head: 'inventories',
            source_movement: { closing_raw_materials: f.closing_raw_materials, closing_wip: f.closing_wip,
              closing_finished_goods: f.closing_finished_goods, closing_inventory_value: f.closing_inventory_value, cogs: f.cogs } })];
        }
        if (src.module_key === 'biz_tax') {
          if (!(f.net_payable > 0)) return [];
          return [tag('tax_prov', { name: 'Provision for taxation (' + (src.title || '') + ')', group: '',
            dr: 0, cr: f.net_payable, head: 'tax_prov' })];
        }
        if (src.module_key === 'tds') {
          return [tag('tds_payable', { name: 'TDS payable (' + (src.title || '') + ')', group: '',
            dr: 0, cr: f.pending_tds || 0, head: 'tds_payable' })];
        }
        if (src.module_key === 'vat') {
          if ((f.payable || 0) > 0) return [tag('vat_payable', { name: 'VAT payable (' + (src.title || '') + ')', group: '',
            dr: 0, cr: f.payable, head: 'vat_payable' })];
          return [tag('st_adv', { name: 'VAT credit carried forward (' + (src.title || '') + ')', group: '',
            dr: f.credit_carried_forward || 0, cr: 0, head: 'st_adv' })];
        }
        if (src.module_key === 'emi' || src.module_key === 'lease') {
          const rows = src.rows || [];
          const idx = fsPeriodPick[src.workfile_id] != null ? fsPeriodPick[src.workfile_id] : rows.length - 1;
          const picked = rows[idx] || {};
          const label = src.module_key === 'emi' ? 'Loan' : 'Lease liability';
          return [
            tag('st_borrow', { name: label + ' — current portion (' + (src.title || '') + ')', group: '',
              dr: 0, cr: picked.current_portion || 0, head: 'st_borrow' }),
            tag('lt_borrow', { name: label + ' — non-current portion (' + (src.title || '') + ')', group: '',
              dr: 0, cr: picked.non_current_portion || 0, head: 'lt_borrow' }),
          ];
        }
        return [];
      },
      view(file, res) {
        const p = file.payload;
        p.disclosures = p.disclosures || { related_party: '', contingent_liabilities: '', capital_commitments: '', subsequent_events: '' };
        const heads = CAEngines.FS_HEADS();
        const linked = fsLinkedCache[file.clientId];

        let h = '<div class="card" id="fs-entity"><h3>Entity</h3><div class="grid c3">' +
          field('Entity name', 'name', p.entity, 'text') +
          field('Address', 'address', p.entity, 'text') +
          field('PAN', 'pan', p.entity, 'text') +
          field('This year label', 'fy_label', p.entity, 'text', { placeholder: 'FY 2082/83' }) +
          field('Previous year label', 'prev_label', p.entity, 'text', { placeholder: 'FY 2081/82' }) +
          '</div></div>';

        // ---------------------------------------------------- pull from other modules
        h += '<div class="card" id="fs-pull"><h3>Pull from other modules' +
          '<button class="btn btn-small btn-ghost" id="fs-pull-refresh" style="float:right" title="Re-check every source module for updated figures">Refresh</button></h3>' +
          '<p class="card-note">Figures already computed elsewhere for this client. Adding one creates an editable, removable trial-balance row — nothing here changes a statement silently.</p>';
        if (!file.clientId) {
          h += '<div class="empty">Select a client on this workfile first.</div>';
        } else if (linked === undefined) {
          h += '<div class="empty">Loading…</div>';
        } else if (linked === 'loading') {
          h += '<div class="empty">Loading…</div>';
        } else if (linked && linked.error) {
          h += '<div class="empty">' + esc(linked.error) + '</div>';
        } else if (!linked || !linked.length) {
          h += '<div class="empty">No Fixed Assets, Inventory, Loan/EMI, Lease, Business Tax, TDS or VAT workfiles found for this client yet.</div>';
        } else {
          linked.forEach(src => {
            const already = p.rows.some(r => r.source_module === src.module_key && r.source_workfile_id === src.workfile_id);
            const btnLabel = already ? 'Update pulled row(s)' : 'Add as TB row(s)';
            const modName = (MODULES[src.module_key] && MODULES[src.module_key].name) || src.module_key;
            const fyTag = src.fy_label ? '<span class="tag">FY ' + esc(src.fy_label) + '</span>'
              : '<span class="hint">fiscal year not tagged on this workfile — check before relying on it</span>';
            const statusTag = src.status !== 'signed_off' ? ' <span class="tag tag-amber">Not yet reviewed</span>' : '';
            const unverTag = (src.unverified && src.unverified.length) ? ' <span class="tag tag-red">Unverified rates</span>' : '';
            let figuresHtml = '', canAdd = true;
            const f = src.figures || {};
            if (src.module_key === 'fixed_assets') {
              figuresHtml = 'Closing NBV ' + money(f.closing_nbv) + ' · Depreciation ' + money(f.depreciation);
            } else if (src.module_key === 'inventory') {
              figuresHtml = 'Closing inventory ' + money(f.closing_inventory_value);
            } else if (src.module_key === 'biz_tax') {
              if (f.net_payable > 0) { figuresHtml = 'Tax payable ' + money(f.net_payable); }
              else { figuresHtml = 'No provision to add (refund due ' + money(f.refund_due || 0) + ' — enter manually if relevant).'; canAdd = false; }
            } else if (src.module_key === 'tds') {
              figuresHtml = 'Pending (undeposited) TDS ' + money(f.pending_tds);
            } else if (src.module_key === 'vat') {
              figuresHtml = (f.payable > 0) ? 'VAT payable ' + money(f.payable) : 'Credit carried forward ' + money(f.credit_carried_forward || 0);
            } else if (src.module_key === 'emi' || src.module_key === 'lease') {
              const rows = src.rows || [];
              if (fsPeriodPick[src.workfile_id] === undefined) fsPeriodPick[src.workfile_id] = rows.length ? rows.length - 1 : 0;
              const pickIdx = fsPeriodPick[src.workfile_id];
              const picked = rows[pickIdx] || {};
              figuresHtml = '<select data-pull-period="' + src.workfile_id + '">' +
                rows.map((r, i) => '<option value="' + i + '"' + (i === pickIdx ? ' selected' : '') + '>Period ' + r.period +
                  (r.date_ad ? ' — ' + esc(r.date_ad) : (r.date_bs_approx ? ' — BS ' + esc(r.date_bs_approx) : ' — no date on this schedule')) +
                  '</option>').join('') + '</select><br>Current portion ' + money(picked.current_portion) + ' · Non-current ' + money(picked.non_current_portion);
              canAdd = rows.length > 0;
            }
            h += '<div class="subcard"><div><b>' + esc(modName) + '</b> — ' + esc(src.title || '') + ' ' + fyTag + statusTag + unverTag + '</div>' +
              '<div class="hint" style="margin:6px 0">' + figuresHtml + '</div>' +
              (canAdd ? '<button class="btn btn-small" data-pull="' + src.module_key + ':' + src.workfile_id + '">' + btnLabel + '</button>' : '') +
              '</div>';
          });
        }
        h += '</div>';

        h += '<div class="card"><h3>Trial balance</h3>' +
          '<p class="card-note">One row per ledger. Opening balance is optional. Pick the financial-statement head — a suggestion is offered from the ledger name, but you choose.</p>' +
          '<div class="table-wrap"><table><thead><tr><th>Ledger</th><th class="num">Opening Dr</th><th class="num">Opening Cr</th>' +
          '<th class="num">Debit</th><th class="num">Credit</th><th class="num" title="Enter last year\'s balance as a plain positive number, same as you would say it out loud — whether the account is a debit or credit balance is worked out automatically from its financial-statement head, same as this year\'s figures">Previous year</th><th>Financial statement head</th>' +
          '<th>Source</th><th></th></tr></thead><tbody>';
        p.rows.forEach((row, i) => {
          let badges = '';
          if (row.source_module) {
            const cur = Array.isArray(linked) ? linked.find(s => s.module_key === row.source_module && s.workfile_id === row.source_workfile_id) : null;
            if (cur) {
              if (cur.version !== row.source_version) badges += '<span class="tag tag-amber" title="The source workfile has changed since this figure was pulled in">Stale</span> ';
              if (cur.status !== 'signed_off') badges += '<span class="tag" title="Source workfile is not yet signed off">Not reviewed</span> ';
            } else if (Array.isArray(linked)) {
              badges += '<span class="tag tag-red" title="This workfile no longer appears among the client\'s linked modules">Source missing</span> ';
            }
            if (row.source_unverified) badges += '<span class="tag tag-red" title="The source computation relied on an unconfirmed rate">Unverified rate</span> ';
            if (!badges) badges = '<span class="hint">pulled in</span>';
          }
          h += '<tr><td><input data-tb="' + i + '" data-k="name" value="' + esc(row.name || '') + '"></td>' +
            '<td class="num"><input type="number" step="0.01" data-tb="' + i + '" data-k="opening_dr" value="' + (row.opening_dr || 0) + '"></td>' +
            '<td class="num"><input type="number" step="0.01" data-tb="' + i + '" data-k="opening_cr" value="' + (row.opening_cr || 0) + '"></td>' +
            '<td class="num"><input type="number" step="0.01" data-tb="' + i + '" data-k="dr" value="' + (row.dr || 0) + '"></td>' +
            '<td class="num"><input type="number" step="0.01" data-tb="' + i + '" data-k="cr" value="' + (row.cr || 0) + '"></td>' +
            '<td class="num"><input type="number" step="0.01" data-tb="' + i + '" data-k="prev" value="' + (row.prev || '') + '"></td>' +
            '<td><select data-tb="' + i + '" data-k="head"><option value="">— suggest —</option>' +
            heads.map(hd => '<option value="' + hd.id + '"' + (row.head === hd.id ? ' selected' : '') + '>' + esc(hd.label) + '</option>').join('') +
            '</select></td>' +
            '<td>' + badges + '</td>' +
            '<td><button class="btn btn-small btn-danger" data-del-tb="' + i + '" title="Remove this row">Remove</button></td></tr>';
        });
        h += '</tbody></table></div><button class="btn btn-small" id="add-tb">Add a ledger</button> ' +
          '<button class="btn btn-small" id="suggest-tb">Suggest heads for blanks</button></div>';

        h += '<div class="card" id="fs-cs"><h3>Closing stock adjustment</h3><p class="card-note">Only needed if the closing stock is not already a ledger in the trial balance.</p><div class="grid c2">' +
          field('Closing stock — this year', 'current', p.closing_stock, 'number') +
          field('Closing stock — previous year', 'previous', p.closing_stock, 'number') + '</div></div>';

        const ctl = res.control;
        h += '<div class="card"><h3>Debit / credit check</h3><div class="grid c3">' +
          kpi('Total debit', money(ctl.total_dr)) + kpi('Total credit', money(ctl.total_cr)) +
          kpi('Difference', money(ctl.diff), ctl.tied ? '' : 'alert') + '</div>' +
          (ctl.tied ? '<p class="msg good" style="margin-top:10px">Debits equal credits.</p>'
                    : '<p class="msg bad" style="margin-top:10px">Out of balance — statements below will not tie until this is fixed.</p>') + '</div>';

        const oc = res.opening_control;
        if (oc && (oc.total_opening_dr || oc.total_opening_cr)) {
          h += '<div class="card"><h3>Opening balance check</h3><div class="grid c3">' +
            kpi('Total opening debit', money(oc.total_opening_dr)) + kpi('Total opening credit', money(oc.total_opening_cr)) +
            kpi('Difference', money(oc.diff), oc.tied ? '' : 'alert') + '</div>' +
            (oc.tied ? '<p class="msg good" style="margin-top:10px">Opening balances are internally consistent.</p>'
                      : '<p class="msg bad" style="margin-top:10px">Opening balances don\'t tie — check the figures. This checks internal consistency only, not agreement with last year\'s filed statements.</p>') + '</div>';
        }

        if (res.unmapped && res.unmapped.length) {
          h += '<div class="card"><h3>Unclassified ledgers</h3><ul>' +
            res.unmapped.map(u => '<li>' + esc(u.name) + ' — ' + money(Math.abs(u.net)) + (u.net > 0 ? ' Dr' : ' Cr') + '</li>').join('') +
            '</ul></div>';
        }

        const cy = p.entity.fy_label || 'This year', py = p.entity.prev_label || 'Previous year';
        function stmtCard(title, rows, anyOpening) {
          const headers = ['Particulars', 'Note'].concat(anyOpening ? ['Opening'] : []).concat([cy]).concat(res.any_prev ? [py] : []);
          const out = [];
          rows.forEach(r => {
            // Column order must mirror headers exactly: Note, then Opening
            // (conditional), then the current-year value, then Prev (conditional).
            const blank = [''].concat(anyOpening ? [''] : []).concat(['']).concat(res.any_prev ? [''] : []);
            if (r.t === 'sec') out.push([r.l].concat(blank));
            else if (r.t === 'sub') out.push(['  ' + r.l].concat(blank));
            else out.push([(r.sub ? '  ' : '') + r.l, r.n || '']
              .concat(anyOpening ? [r.o === null || r.o === undefined ? '' : r.o] : [])
              .concat([r.v]).concat(res.any_prev ? [r.p === null || r.p === undefined ? '' : r.p] : []));
          });
          return resultCard(title, headers, out);
        }
        h += stmtCard('Balance sheet as at ' + cy, res.balance_sheet.rows, res.balance_sheet.any_opening);
        h += res.balance_sheet.tied
          ? '<div class="msg good">Balance sheet ties.</div>'
          : '<div class="msg bad">Does not tie by ' + money(Math.abs(res.balance_sheet.tie)) + ' — a head is likely mapped to the wrong side.</div>';
        h += stmtCard('Statement of profit and loss for ' + cy, res.profit_and_loss.rows, false);

        h += '<div class="card" id="fs-policy"><h3>Significant accounting policies</h3>' +
          '<p class="card-note">A starting draft, not filed text — edit every bracketed choice and anything else that doesn\'t match this entity.</p>' +
          '<textarea data-key="accounting_policy" rows="8" style="width:100%">' + esc(p.accounting_policy || '') + '</textarea></div>';

        if (res.notes.length) {
          h += '<div class="card"><h3>Notes to accounts</h3>';
          res.notes.forEach(n => {
            h += '<p><b>Note ' + n.n + ' — ' + esc(n.label) + '</b></p>';
            if (n.movement) {
              const labels = n.id === 'ppe'
                ? [['opening_nbv', 'Opening net book value'], ['additions_cost', 'Additions'], ['depreciation', 'Depreciation for the year'],
                   ['disposal_proceeds', 'Disposals (proceeds)'], ['closing_nbv', 'Closing net book value']]
                : [['closing_raw_materials', 'Raw materials'], ['closing_wip', 'Work in progress'], ['closing_finished_goods', 'Finished goods'],
                   ['closing_inventory_value', 'Total closing inventory'], ['cogs', 'Cost of goods sold for the year']];
              const moveRows = labels.filter(([k]) => n.movement[k] !== null && n.movement[k] !== undefined).map(([k, lbl]) => [lbl, n.movement[k]]);
              if (moveRows.length) h += resultCard('Movement (from source module)', ['Particulars', 'Amount'], moveRows);
            }
            h += resultCard('', ['Particulars', 'Amount'], n.items.map(it => [it.name, it.net]));
          });
          h += '</div>';
        }

        h += '<div class="card" id="fs-disclosures"><h3>Other disclosures</h3>' +
          '<p class="card-note">Optional — only appears in the exported statements when filled in.</p><div class="grid c2">' +
          [['related_party', 'Related party transactions'], ['contingent_liabilities', 'Contingent liabilities'],
           ['capital_commitments', 'Capital commitments'], ['subsequent_events', 'Subsequent events']].map(([k, lbl]) =>
            '<div class="field"><label>' + esc(lbl) + '</label><textarea data-disc="' + k + '" rows="3">' + esc(p.disclosures[k] || '') + '</textarea></div>').join('') +
          '</div></div>';
        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        if (file.clientId && fsLinkedCache[file.clientId] === undefined) fsLoadLinked(file.clientId, rerender);
        wireInto(root, 'fs-entity', p.entity, rerender);
        wireInto(root, 'fs-cs', p.closing_stock, rerender);
        root.querySelectorAll('[data-tb]').forEach(el => el.addEventListener('change', e => {
          const i = +e.target.dataset.tb, k = e.target.dataset.k;
          p.rows[i][k] = (e.target.type === 'number') ? Number(e.target.value) : e.target.value;
          rerender();
        }));
        root.querySelectorAll('[data-del-tb]').forEach(el => el.addEventListener('click', e => {
          p.rows.splice(+e.target.dataset.delTb, 1); rerender();
        }));
        const add = root.querySelector('#add-tb');
        if (add) add.addEventListener('click', () => { p.rows.push({ name: '', dr: 0, cr: 0, prev: null, opening_dr: 0, opening_cr: 0, head: '' }); rerender(); });
        const suggest = root.querySelector('#suggest-tb');
        if (suggest) suggest.addEventListener('click', () => {
          p.rows.forEach(r => { if (!r.head) r.head = CAEngines.suggestFsHead(r.name, r.group); });
          rerender();
        });

        const refreshLinked = root.querySelector('#fs-pull-refresh');
        if (refreshLinked) refreshLinked.addEventListener('click', () => fsLoadLinked(file.clientId, rerender));
        root.querySelectorAll('[data-pull-period]').forEach(sel => sel.addEventListener('change', e => {
          fsPeriodPick[e.target.dataset.pullPeriod] = +e.target.value; rerender();
        }));
        root.querySelectorAll('[data-pull]').forEach(btn => btn.addEventListener('click', e => {
          const [moduleKey, wid] = e.target.dataset.pull.split(':');
          const linked = fsLinkedCache[file.clientId];
          if (!Array.isArray(linked)) return;
          const src = linked.find(s => s.module_key === moduleKey && String(s.workfile_id) === wid);
          if (!src) return;
          MODULES.fin_statements._pullRows(src).forEach(nr => {
            const idx = p.rows.findIndex(r => r.source_module === nr.source_module && r.source_workfile_id === nr.source_workfile_id && r.source_tag === nr.source_tag);
            if (idx >= 0) p.rows[idx] = Object.assign({}, nr, { head: p.rows[idx].head || nr.head });
            else p.rows.push(nr);
          });
          toast('Pulled in from ' + ((MODULES[moduleKey] && MODULES[moduleKey].name) || moduleKey));
          // Master Data Hub write-through: record that this pull happened, so
          // it's visible to hubLinkActions()/hubStatusBadge() wherever a
          // future screen wants it — purely additive, never blocks or alters
          // the pull above if it fails (e.g. offline, or the Hub route
          // isn't reachable for some other reason).
          if (src.hub_snapshot_id && file.clientId) {
            api('/api/hub/links', { method: 'POST', body: {
              client_id: file.clientId, consuming_module: 'fin_statements',
              consuming_ref: String(file.serverId || file.localKey), snapshot_id: src.hub_snapshot_id
            } }).catch(() => {});
          }
          rerender();
        }));

        const policyTa = root.querySelector('[data-key="accounting_policy"]');
        if (policyTa) policyTa.addEventListener('change', e => { p.accounting_policy = e.target.value; rerender(); });
        root.querySelectorAll('[data-disc]').forEach(ta => ta.addEventListener('change', e => {
          p.disclosures[e.target.dataset.disc] = e.target.value; rerender();
        }));
      },
      tabulate: res => [
        { title: 'Balance sheet', headers: ['Particulars', 'Note', 'Amount'],
          rows: res.balance_sheet.rows.filter(r => r.t === 'row' || r.t === 'tot').map(r => [r.l, r.n || '', r.v]) },
        { title: 'Profit and loss', headers: ['Particulars', 'Note', 'Amount'],
          rows: res.profit_and_loss.rows.filter(r => r.t === 'row' || r.t === 'tot').map(r => [r.l, r.n || '', r.v]) },
      ].concat((res.accounting_policy || '').trim() ? [{
        title: 'Accounting policies', headers: ['Policy'], rows: [[res.accounting_policy]]
      }] : []).concat((res.notes || []).map(note => ({
        title: 'Note ' + note.n + ' — ' + note.label,
        headers: ['Particulars', 'Amount'],
        rows: (note.movement ? Object.keys(note.movement).filter(k => note.movement[k] != null)
          .map(k => [k, note.movement[k]]) : []).concat((note.items || []).map(it => [it.name, it.net]))
      }))).concat((res.unmapped || []).length ? [{
        title: 'Unclassified — mapping pending', headers: ['Ledger', 'Net'],
        rows: res.unmapped.map(u => [u.name, u.net])
      }] : []).concat((() => {
        const d = res.disclosures || {};
        const labels = [['related_party', 'Related party transactions'], ['contingent_liabilities', 'Contingent liabilities'],
          ['capital_commitments', 'Capital commitments'], ['subsequent_events', 'Subsequent events']];
        const active = labels.filter(([k]) => (d[k] || '').trim());
        return active.length ? [{ title: 'Other disclosures', headers: ['Disclosure', 'Text'],
          rows: active.map(([k, lbl]) => [lbl, d[k]]) }] : [];
      })())
    },

    grant: {
      code: 'GR', name: 'Government grants', group: 'Accounting',
      blurb: 'Recognition, measurement and the deferred-income schedule for government grants under NAS 20 / IAS 20, with concessional loans measured at fair value under NFRS 9 / IFRS 9.',
      seed: () => ({ meta: { fy_start: '07-16', fy_label: 'bs' }, grants: [], events: [] }),
      view(file, res) {
        const p = file.payload;
        let h = '<div class="card" id="gr-meta"><h3>Fiscal year</h3><div class="grid c2">' +
          field('FY start (MM-DD)', 'fy_start', p.meta, 'text', { hint: 'Nepal Shrawan 1 ≈ 07-16' }) +
          field('FY label', 'fy_label', p.meta, 'select', { options: [{ value: 'bs', label: 'Bikram Sambat' }, { value: 'ad', label: 'Gregorian' }] }) +
          '</div></div>';

        h += '<div class="card"><h3>Grants</h3>';
        p.grants.forEach((g, i) => {
          const gres = (res.grants || []).find(x => x.id === g.id);
          h += '<div class="subcard"><div class="grid c3">' +
            field('Name', 'name', g, 'text') +
            field('Grantor', 'grantor', g, 'text') +
            field('Type', 'type', g, 'select', { options: [
              { value: 'loan', label: 'Subsidised / concessional loan' }, { value: 'asset', label: 'Asset-related' },
              { value: 'income', label: 'Income-related' }, { value: 'pastcost', label: 'Compensation for past costs' },
              { value: 'forgivable', label: 'Forgivable loan' }, { value: 'nonmonetary', label: 'Non-monetary' }] }) +
            '</div><div class="grid c4">';
          if (g.type === 'loan' || g.type === 'forgivable') {
            h += field('Principal', 'principal', g, 'number') + field('Disbursement date', 'disb_date', g, 'date') +
              field('Concessional rate %', 'rate_c', g, 'number') + field('Market rate %', 'rate_m', g, 'number') +
              field('Tenor (periods)', 'tenor', g, 'number') + field('Frequency/year', 'freq', g, 'number') +
              field('Grace periods', 'grace', g, 'number') +
              field('Repayment mode', 'mode', g, 'select', { options: [
                { value: 'emi', label: 'Equal instalment' }, { value: 'epi', label: 'Equal principal' },
                { value: 'bullet', label: 'Bullet' }, { value: 'zero', label: 'Zero-servicing' }] }) +
              field('Release basis', 'release', g, 'select', { options: [
                { value: 'interest', label: 'Against interest cost' }, { value: 'sl', label: 'Straight-line over term' },
                { value: 'asset', label: 'Over asset life' }, { value: 'immediate', label: 'Immediately' }] }) +
              field('Reasonable assurance date', 'assurance', g, 'date');
            if (g.type === 'forgivable') h += field('Forgiveness date', 'forgive_date', g, 'date') + field('Amount forgiven (blank = balance)', 'forgive_amt', g, 'number');
          } else if (g.type === 'asset' || g.type === 'nonmonetary') {
            h += field('Grant amount', 'amount', g, 'number') + field('Asset cost', 'asset_cost', g, 'number') +
              field('Asset in-service date', 'asset_date', g, 'date') + field('Receipt date', 'receipt_date', g, 'date') +
              field('Life', 'life', g, 'number') +
              field('Life unit', 'life_unit', g, 'select', { options: [{ value: 'years', label: 'Years' }, { value: 'months', label: 'Months' }] }) +
              field('Residual value', 'residual', g, 'number') +
              field('Depreciation method', 'dep_method', g, 'select', { options: [{ value: 'slm', label: 'Straight-line' }, { value: 'wdv', label: 'Reducing balance' }] }) +
              field('WDV rate % (if reducing balance)', 'wdv_rate', g, 'number') +
              field('Presentation', 'present', g, 'select', { options: [{ value: 'deferred', label: 'Deferred income' }, { value: 'netcost', label: 'Deduct from asset' }] });
            if (g.type === 'nonmonetary') h += field('Basis', 'nm_basis', g, 'select', { options: [{ value: 'fv', label: 'Fair value' }, { value: 'nominal', label: 'Nominal amount' }] });
          } else {
            h += field('Grant amount', 'amount', g, 'number') + field('Receipt date', 'receipt_date', g, 'date') +
              field('Period start', 'inc_start', g, 'date') + field('Period end', 'inc_end', g, 'date') +
              field('Pattern', 'inc_pattern', g, 'select', { options: [{ value: 'sl', label: 'Even over period' }, { value: 'immediate', label: 'Immediately' }] }) +
              field('Presentation', 'inc_present', g, 'select', { options: [{ value: 'other', label: 'Other income' }, { value: 'netexp', label: 'Deduct from related expense' }] });
          }
          h += '</div>';
          if (gres) {
            h += '<div class="grid c3" style="margin-top:8px">' +
              (gres.grant !== undefined ? kpi('Grant benefit', money(gres.grant), 'accent') : '') +
              (gres.fv !== undefined ? kpi('Loan at fair value', money(gres.fv)) : '') +
              (gres.total_dep !== undefined ? kpi('Total depreciation', money(gres.total_dep)) : '') + '</div>';
            (gres.warn || []).forEach(w => { h += '<p class="msg bad">' + esc(w) + '</p>'; });
          }
          h += '<button class="btn btn-small btn-danger" data-del-grant="' + i + '">Remove this grant</button></div>';
        });
        h += '<button class="btn btn-small" id="add-grant">Add a grant</button></div>';

        if (res.consolidated && res.consolidated.length) {
          h += resultCard('Consolidated — all grants', ['FY', 'Grant income', 'Depreciation', 'Interest', 'Deferred income closing', 'Cash movement'],
            res.consolidated.map(c => [c.fy_label, c.income, c.dep, c.interest, c.deferred_income_closing, c.cash]));
        }
        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        wireInto(root, 'gr-meta', p.meta, rerender);
        Array.from(root.querySelectorAll('.subcard')).forEach((card, i) => {
          card.querySelectorAll('.field').forEach(f => {
            const control = f.querySelector('input, select, textarea');
            if (!control || control.dataset.wiredGrant) return;
            control.dataset.wiredGrant = '1';
            const key = control.dataset.key;
            if (!key) return;
            control.addEventListener('change', () => {
              p.grants[i][key] = control.type === 'number' ? Number(control.value) : control.value;
              rerender();
            });
          });
          const del = card.querySelector('[data-del-grant]');
          if (del) del.addEventListener('click', () => { p.grants.splice(i, 1); rerender(); });
        });
        const add = root.querySelector('#add-grant');
        if (add) add.addEventListener('click', () => {
          p.grants.push({ id: 'g' + Date.now() + Math.random().toString(36).slice(2, 6), name: '', type: 'loan',
            freq: 12, tenor: 60, mode: 'emi', release: 'interest', dep_method: 'slm', present: 'deferred' });
          rerender();
        });
      },
      tabulate: res => [{
        title: 'Consolidated grant schedule',
        headers: ['FY', 'Grant income', 'Depreciation', 'Interest', 'Deferred income closing', 'Cash movement'],
        rows: (res.consolidated || []).map(c => [c.fy_label, c.income, c.dep, c.interest, c.deferred_income_closing, c.cash])
      }]
    },

    projection: {
      code: 'PJ', name: 'Projection', group: 'Advisory', needsIndustries: true,
      blurb: 'Financial projection, feasibility and bank appraisal — statements, loan schedule, net trading assets and the working-capital limit, ratios (including DSCR, LLCR and PLCR), DCF appraisal, validation and a verdict, all resizing to the projection period.',
      seed: () => ({ company: '', client: 'greenfield', years: 5, industry: 'Plastic Products — PVC & HDPE Pipe',
        loan_type: 'epi', loan: 60000000, equity: 40000000, rate: .115, wc_rate: .12, wc_margin: .75,
        od_spread: .01, disc_rate: .13, tv_basis: 'going', tgrowth: .03, proc_fee: .0075, payout: 0 }),
      view(file, res) {
        const p = file.payload;
        const industries = (state.boot && state.boot.industry_defaults) || [];
        const yopts = []; for (let y = 2; y <= 10; y++) yopts.push({ value: y, label: y + ' years' });
        const money0 = v => v === null || v === undefined ? '—' : money(v);

        let h = '<div class="card"><h3>Entity and engagement</h3><div class="grid c3">' +
          field('Company / project name', 'company', p, 'text') +
          field('Basis of preparation', 'client', p, 'select', { options: [
            { value: 'greenfield', label: 'Proposed company (greenfield)' },
            { value: 'firstyear', label: 'First year of operation' },
            { value: 'audited', label: 'Audited client (expansion)' }] }) +
          field('Projection period', 'years', p, 'select', { options: yopts }) +
          field('Industry', 'industry', p, 'select', { wide: true, options: industries.map(i => ({ value: i.name, label: i.name + ' — ' + (i.category || '') })) }) +
          '</div></div>';

        const ind = industries.find(i => i.name === p.industry);
        if (ind) h += '<div class="msg good">' + esc(ind.note || '') + '</div>';

        h += '<div class="card"><h3>Revenue</h3><p class="card-note">Leave a field blank to use the industry default shown as the placeholder.</p><div class="grid c3">' +
          field('Revenue basis', 'revenue_mode', p, 'select', { options: [
            { value: 'turnover', label: 'Asset turnover — sales as a multiple of investment' },
            { value: 'capacity', label: 'Capacity × price' }] }) +
          field('Asset turnover', 'at', p, 'number', { placeholder: ind ? ind.asset_turnover : '' }) +
          field('Installed capacity per year', 'capacity', p, 'number') +
          field('Net selling price per unit (year 1)', 'price', p, 'number') +
          field('Other income per year', 'other_income', p, 'number') +
          field('Price escalation per year', 'price_esc', p, 'number', { step: '0.01' }) +
          field('Cost escalation per year', 'cost_esc', p, 'number', { step: '0.01' }) +
          '</div></div>';

        h += '<div class="card"><h3>Capacity utilisation, year by year</h3><div class="grid" style="grid-template-columns:repeat(' +
          res.N + ',1fr)">' + res.util.map((u, i) =>
            '<div class="field"><label>Year ' + (i + 1) + '</label><input type="number" step="1" data-util="' + i +
            '" value="' + Math.round(u * 100) + '"></div>').join('') +
          '</div></div>';

        h += '<div class="card"><h3>Operating cost — % of sales</h3><div class="grid c4">' +
          field('Raw material', 'rm', p, 'number', { placeholder: ind ? pct(ind.cost_pct[0]) : '', step: '0.001' }) +
          field('Labour', 'labour', p, 'number', { placeholder: ind ? pct(ind.cost_pct[1]) : '', step: '0.001' }) +
          field('Power', 'power', p, 'number', { placeholder: ind ? pct(ind.cost_pct[2]) : '', step: '0.001' }) +
          field('Factory overhead', 'mfgoh', p, 'number', { placeholder: ind ? pct(ind.cost_pct[3]) : '', step: '0.001' }) +
          field('Admin', 'admin', p, 'number', { placeholder: ind ? pct(ind.cost_pct[4]) : '', step: '0.001' }) +
          field('Selling', 'selling', p, 'number', { placeholder: ind ? pct(ind.cost_pct[5]) : '', step: '0.001' }) +
          field('Repairs (% of gross block)', 'repairs', p, 'number', { placeholder: ind ? pct(ind.repairs_pct) : '', step: '0.001' }) +
          '</div></div>';

        h += '<div class="card"><h3>Working capital cycle (days)</h3><div class="grid c4">' +
          field('Debtors', 'd_deb', p, 'number', { placeholder: ind ? ind.wc_days[0] : '' }) +
          field('Raw material stock', 'd_rm', p, 'number', { placeholder: ind ? ind.wc_days[1] : '' }) +
          field('Finished goods stock', 'd_fg', p, 'number', { placeholder: ind ? ind.wc_days[2] : '' }) +
          field('Creditors', 'd_cred', p, 'number', { placeholder: ind ? ind.wc_days[3] : '' }) +
          '</div></div>';

        h += '<div class="card"><h3>Depreciation and tax</h3><div class="grid c4">' +
          field('Corporate tax rate', 'tax_rate', p, 'number', { placeholder: ind ? pct(ind.tax_rate) : '', step: '0.001' }) +
          field('Concession period (years)', 'hol_years', p, 'number', { placeholder: ind ? ind.hol_years : '' }) +
          field('Tax rate during concession', 'hol_rate', p, 'number', { placeholder: ind ? pct(ind.hol_rate) : '', step: '0.001' }) +
          field('Economic life (years)', 'life_years', p, 'number', { placeholder: ind ? ind.life_years : '' }) +
          field('Maintenance capex (% of gross block)', 'maint_capex', p, 'number', { step: '0.001' }) +
          '</div><label class="check" style="margin-top:10px"><input type="checkbox" data-flag="special_dep"' +
          (p.special_dep ? ' checked' : '') + '> Claim the additional one-third depreciation as a special industry</label></div>';

        h += '<div class="card"><h3>Loan and finance</h3><div class="grid c3">' +
          field('Loan type', 'loan_type', p, 'select', { wide: true, options: [
            { value: 'epi', label: 'Term loan — equal principal' }, { value: 'emi', label: 'Term loan — EMI' },
            { value: 'bullet', label: 'Bullet / balloon — interest only' }, { value: 'balloon', label: 'Part amortised with balloon' },
            { value: 'stepup', label: 'Step-up repayment' }, { value: 'moratint', label: 'Moratorium with interest capitalised' },
            { value: 'hp', label: 'Hire purchase / vehicle loan' }, { value: 'subsidised', label: 'Concessional / subsidised loan' },
            { value: 'wconly', label: 'Working capital limit only' }, { value: 'none', label: 'No borrowing — fully equity funded' }] }) +
          field('Loan amount', 'loan', p, 'number') + field('Promoter\'s equity', 'equity', p, 'number') +
          field('Grant / subsidy received as capital', 'grant_amt', p, 'number') +
          field('Unsecured promoter loan', 'promoter_loan', p, 'number') +
          field('Interest rate per annum', 'rate', p, 'number', { step: '0.001' }) +
          (p.loan_type === 'subsidised' ? field('Interest subsidy', 'subsidy', p, 'number', { step: '0.001' }) : '') +
          field('Tenure including moratorium (years)', 'tenure', p, 'number', { placeholder: ind ? ind.tenure_years : '' }) +
          field('Moratorium (years)', 'mor', p, 'number', { placeholder: ind ? ind.moratorium_years : '' }) +
          (p.loan_type === 'balloon' ? field('Balloon due at maturity', 'balloon_pct', p, 'number', { step: '0.01' }) : '') +
          field('Processing fee', 'proc_fee', p, 'number', { step: '0.001' }) +
          '</div></div>';

        h += '<div class="card"><h3>Working capital finance</h3><div class="grid c3">' +
          field('Interest on working capital limit', 'wc_rate', p, 'number', { step: '0.001' }) +
          field('Bank funding of the working capital gap', 'wc_margin', p, 'number', { step: '0.01' }) +
          field('Spread on unplanned overdraft', 'od_spread', p, 'number', { step: '0.001' }) +
          '</div></div>';

        h += '<div class="card"><h3>Appraisal and distribution</h3><div class="grid c3">' +
          field('Discount rate / WACC', 'disc_rate', p, 'number', { step: '0.001' }) +
          field('Dividend payout (% of PAT)', 'payout', p, 'number', { step: '0.01' }) +
          field('Terminal value basis', 'tv_basis', p, 'select', { options: [
            { value: 'going', label: 'Going concern' }, { value: 'salvage', label: 'Salvage value' }, { value: 'none', label: 'None' }] }) +
          (p.tv_basis === 'going' ? field('Terminal growth rate', 'tgrowth', p, 'number', { step: '0.001' }) : '') +
          (p.tv_basis === 'salvage' ? field('Salvage value (% of gross block)', 'salvage', p, 'number', { step: '0.01' }) : '') +
          '</div></div>';

        h += '<div class="card"><h3>Net trading assets — detailed assessment</h3>' +
          '<label class="check"><input type="checkbox" data-flag="nta_on"' + (p.nta_on ? ' checked' : '') +
          '> Prepare a detailed net trading assets assessment</label>';
        if (p.nta_on) {
          h += '<div class="grid c4" style="margin-top:12px">' +
            field('Method of lending', 'wc_method', p, 'select', { wide: true, options: [
              { value: 'margin', label: 'Margin on net trading assets' }, { value: 'tandon1', label: 'Tandon I' },
              { value: 'tandon2', label: 'Tandon II' }, { value: 'turnover', label: 'Turnover method (Nayak)' },
              { value: 'dp', label: 'Drawing power' }] }) +
            field('Sanctioned limit, if fixed', 'sanction_limit', p, 'number') +
            field('Peak to year-end factor', 'peak_factor', p, 'number', { step: '0.05' }) +
            field('Stores / spares (days)', 'd_spares', p, 'number') +
            field('Other current assets (days of sales)', 'd_other_ca', p, 'number') +
            field('Advances from customers (days of sales)', 'd_adv', p, 'number') +
            field('Other current liabilities (days of cost)', 'd_other_cl', p, 'number') +
            field('Margin — raw material', 'm_rm', p, 'number', { step: '0.01' }) +
            field('Margin — WIP', 'm_wip', p, 'number', { step: '0.01' }) +
            field('Margin — finished goods', 'm_fg', p, 'number', { step: '0.01' }) +
            field('Margin — receivables', 'm_deb', p, 'number', { step: '0.01' }) +
            field('Margin — other current assets', 'm_other', p, 'number', { step: '0.01' }) +
            '</div>';
        }
        h += '</div>';

        if ((p.client || 'greenfield') !== 'greenfield') {
          h += '<div class="card"><h3>Existing business</h3><div class="grid c4">' +
            field('Revenue', 'h_rev', p, 'number') + field('EBITDA', 'h_ebitda', p, 'number') +
            field('Expected growth', 'h_growth', p, 'number', { step: '0.01' }) +
            field('Net block', 'h_nb', p, 'number') + field('Current assets', 'h_ca', p, 'number') +
            field('Current liabilities', 'h_cl', p, 'number') + field('Term loan outstanding', 'h_loan', p, 'number') +
            field('Net worth', 'h_nw', p, 'number') + '</div></div>';
        }

        const v = res.verdict, vcls = v.k === 'ok' ? 'accent' : (v.k === 'mid' ? '' : 'alert');
        h += '<div class="card"><h3>Assessment</h3><div class="grid c4">' +
          kpi('Project cost', money0(res.T)) + kpi('Gearing', pct(res.gearing)) +
          kpi('NPV', money0(res.npv), res.npv >= 0 ? 'accent' : 'alert') +
          kpi('Project IRR', res.irr === null ? '—' : pct(res.irr)) +
          kpi('DSCR min · avg', (res.min_dscr === null ? '—' : res.min_dscr) + ' · ' + (res.avg_dscr === null ? '—' : res.avg_dscr)) +
          kpi('Payback (years)', res.payback >= 90 ? '—' : res.payback) +
          (res.llcr !== null ? kpi('LLCR · PLCR', res.llcr + ' · ' + res.plcr,
            null, 'Loan Life / Project Life Coverage Ratio — PV of cash available for debt service over the loan tenure (LLCR) or the full projection period (PLCR), against the initial drawn balance. Lenders typically look for at least 1.2–1.4x.') : '') +
          '</div><div class="msg ' + (v.k === 'ok' ? 'good' : 'bad') + '" style="margin-top:12px"><b>' + esc(v.t) + '</b><br>' + esc(v.s) + '</div></div>';

        const years = Array.from({ length: res.N }, (_, i) => 'Year ' + (i + 1));
        function yt(title, rows) { return resultCard(title, ['Particulars', ...years], rows.map(([l, arr]) => [l, ...arr.map(x => x === null ? '—' : x)])); }

        h += yt('Profit and loss', [
          ['Sales', res.sales], ['EBITDA', res.ebitda], ['Book depreciation', res.book_dep],
          ['EBIT', res.ebit], ['Finance cost', res.fin], ['Profit before tax', res.pbt],
          ['Tax', res.tax], ['Profit after tax', res.pat]]);
        h += yt('Balance sheet', [
          ['Net block', res.nb], ['Net trading assets', res.gap], ['Cash', res.cash],
          ['Total assets', res.ta], ['Net worth', res.nw], ['Term loan', res.tl_close],
          ['WC limit drawn', res.wcl], ['Overdraft', res.od], ['Tie check', res.chk]]);
        h += yt('Cash flow', [['Operating', res.ocf], ['Investing', res.icf], ['Financing', res.fcf], ['Closing cash', res.cash]]);
        h += yt('Loan schedule', [['Opening', res.tl_open], ['Interest', res.tl_int], ['Principal', res.tl_prin], ['Closing', res.tl_close]]);
        h += yt('Net trading assets and bank finance', [
          ['Net trading assets (gap)', res.gap], ['Margin method', res.mpbf.margin], ['Tandon I', res.mpbf.tandon1],
          ['Tandon II', res.mpbf.tandon2], ['Turnover method', res.mpbf.turnover], ['Drawing power', res.dp],
          ['Limit assessed', res.limit], ['Amount drawn', res.wcl]]);
        h += yt('Ratios', [['DSCR', res.dscr], ['Current ratio', res.cr], ['Interest cover', res.icr],
          ['ROCE', res.roce], ['ROE', res.roe], ['Debt / equity', res.de]]);

        h += resultCard('Appraisal', ['Particulars', 'Value'], [
          ['NPV', res.npv], ['Project IRR', res.irr === null ? '—' : pct(res.irr)],
          ['Equity IRR', res.eirr === null ? '—' : pct(res.eirr)], ['Profitability index', res.pi],
          ['Payback (years)', res.payback], ['Terminal value', res.tv]]);

        h += resultCard('Sensitivity', ['Scenario', 'Avg DSCR', 'Min DSCR', 'Project IRR', 'Cumulative PAT', 'NPV'],
          res.sensitivity.map(sc => [sc.scenario, sc.avg_dscr === null ? '—' : sc.avg_dscr, sc.min_dscr === null ? '—' : sc.min_dscr,
            sc.irr === null ? '—' : pct(sc.irr), sc.cum_pat, sc.npv]));

        const errs = res.checks.filter(c => c.s === 'err'), warns = res.checks.filter(c => c.s === 'warn');
        h += '<div class="card"><h3>Validation — ' + errs.length + ' error(s), ' + warns.length + ' warning(s)</h3>' +
          errs.concat(warns).map(c => '<p class="msg ' + (c.s === 'err' ? 'bad' : 'good') + '"><b>' + esc(c.t) + '</b>' +
            (c.d ? '<br>' + esc(c.d) : '') + '</p>').join('') +
          (errs.length + warns.length === 0 ? '<p class="msg good">All checks passed.</p>' : '') + '</div>';
        return h;
      },
      wire(root, file, rerender) {
        const p = file.payload;
        wireSimple(root, file, rerender);
        root.querySelectorAll('[data-util]').forEach(el => el.addEventListener('change', () => {
          const i = +el.dataset.util;
          const u = (p.util && p.util.length) ? p.util.slice() : [];
          while (u.length <= i) u.push(0);
          u[i] = Number(el.value) / 100;
          p.util = u;
          rerender();
        }));
      },
      tabulate: res => {
        const years = Array.from({ length: res.N }, (_, i) => 'Year ' + (i + 1));
        const yearTable = (title, lines) => ({ title, headers: ['Particulars', ...years], rows: lines.map(([l, arr]) => [l, ...arr]) });
        const err = res.checks.filter(c => c.s === 'err'), warn = res.checks.filter(c => c.s === 'warn');
        return [{
          title: 'Summary', headers: ['Particulars', 'Amount'],
          rows: [
            ['Verdict', res.verdict.t], ['Industry', res.industry], ['Projection period (years)', res.N],
            ['Total project cost', res.T], ['Gearing', res.gearing], ['NPV', res.npv], ['Project IRR', res.irr],
            ['Equity IRR', res.eirr], ['Minimum DSCR', res.min_dscr], ['Average DSCR', res.avg_dscr],
            ['Loan Life Coverage Ratio', res.llcr], ['Project Life Coverage Ratio', res.plcr],
            ['Payback (years)', res.payback], ['Terminal value', res.tv],
          ]
        }, {
          title: 'Validation', headers: ['Status', 'Point', 'Detail'],
          rows: res.checks.filter(c => c.s !== 'ok').map(c => [c.s.toUpperCase(), c.t, c.d || ''])
        },
        yearTable('Profit and loss', [
          ['Sales', res.sales], ['Other income', res.income.map((x, i) => x - res.sales[i])], ['Total income', res.income],
          ['Raw material', res.cost_lines.rm], ['Labour', res.cost_lines.labour], ['Power', res.cost_lines.power],
          ['Factory overhead', res.cost_lines.mfgoh], ['Repairs', res.cost_lines.repairs], ['Admin', res.cost_lines.admin],
          ['Selling', res.cost_lines.selling], ['Total operating cost', res.opex], ['EBITDA', res.ebitda],
          ['Book depreciation', res.book_dep], ['Amortisation', res.amort], ['EBIT', res.ebit], ['Finance cost', res.fin],
          ['Profit before tax', res.pbt], ['Tax', res.tax], ['Profit after tax', res.pat], ['Dividend', res.div],
        ]),
        yearTable('Balance sheet', [
          ['Net block', res.nb], ['Preoperative expenses', res.preop_close], ['Net trading assets', res.gap],
          ['Cash / bank', res.cash], ['Total assets', res.ta], ['Net worth', res.nw], ['Term loan', res.tl_close],
          ['Working capital limit drawn', res.wcl], ['Overdraft', res.od], ['Trade creditors and other CL', res.cred],
          ['Total equity and liabilities', res.tel], ['Tie check (should be ~0)', res.chk],
        ]),
        yearTable('Cash flow', [
          ['Operating cash flow', res.ocf], ['Investing cash flow', res.icf], ['Financing cash flow', res.fcf],
          ['Closing cash', res.cash], ['Closing overdraft', res.od],
        ]),
        yearTable('Loan schedule', [
          ['Opening balance', res.tl_open], ['Interest', res.tl_int], ['Principal repaid', res.tl_prin], ['Closing balance', res.tl_close],
        ]),
        yearTable('Net trading assets', [
          ['Current assets (operating)', res.ca_ops], ['Current liabilities', res.cred], ['Net trading assets (gap)', res.gap],
          ['Margin method', res.mpbf.margin], ['Tandon I', res.mpbf.tandon1], ['Tandon II', res.mpbf.tandon2],
          ['Turnover method', res.mpbf.turnover], ['Drawing power', res.dp], ['Limit assessed', res.limit],
          ['Amount drawn', res.wcl], ['Cash conversion cycle (days)', res.ccc],
        ]),
        yearTable('Ratios and appraisal', [
          ['DSCR', res.dscr], ['Current ratio', res.cr], ['Interest cover', res.icr],
          ['Return on capital employed', res.roce], ['Return on equity', res.roe], ['Debt / equity', res.de],
          ['Break-even (fraction of sales)', res.bep],
        ]), {
          title: 'Appraisal', headers: ['Particulars', 'Value'],
          rows: [['NPV', res.npv], ['Project IRR', res.irr], ['Equity IRR', res.eirr], ['Profitability index', res.pi],
            ['Payback (years)', res.payback], ['Discounted payback (years)', res.disc_payback], ['Terminal value', res.tv]]
        }, {
          title: 'Sensitivity', headers: ['Scenario', 'Average DSCR', 'Minimum DSCR', 'Project IRR', 'Cumulative PAT', 'NPV'],
          rows: (res.sensitivity || []).map(sc => [sc.scenario, sc.avg_dscr, sc.min_dscr, sc.irr, sc.cum_pat, sc.npv])
        }];
      }
    }
  };

  // ------------------------------------------------- generic register editor
  // Every DD register (and the per-year financials) is a list of typed rows.
  // One renderer + one wiring function handles all of them via data-reg/data-i/
  // data-k attributes, instead of writing a bespoke table per register.
  const DD_SEL = {
    yesno: ['Yes', 'No'],
    drCat: ['Financial', 'Tax', 'Legal', 'Other'],
    drStatus: ['Pending', 'Partial', 'Received', 'Not applicable'],
    conf: ['High', 'Medium', 'Low'],
    covStatus: ['Not tested', 'Compliant', 'Breached', 'No covenant'],
    coc: ['Unknown', 'No', 'Yes-consent needed'],
    pg: ['No', 'Yes', 'Unknown'],
    dlikeCat: ['Debt-like', 'Cash-like', 'Off balance sheet', 'Neither'],
    rpRel: ['Director', 'Shareholder', 'KMP', 'Relative of KMP', 'Group company', 'Associate-JV', 'Other'],
    rpNat: ['Sales', 'Purchases', 'Rent paid', 'Rent received', 'Loan given', 'Loan taken', 'Guarantee', 'Remuneration', 'Commission', 'Asset transfer', 'Expense sharing', 'Other'],
    arm: ['Not assessed', 'Yes', 'No', 'Partly'],
    disc: ['Not disclosed', 'Disclosed', 'Partial'],
    oneoffNat: ['Non-recurring expense', 'Non-recurring income', 'Non-operating', 'Owner-related', 'Discretionary', 'Prior period', 'Provision reversal'],
    prob: ['Probable', 'Possible', 'Remote'],
    filed: ['Filed on time', 'Filed late', 'Not filed', 'Unknown'],
    itStat: ['Self-assessed', 'Under audit', 'Amended u/s 101', 'Administrative review u/s 115', 'Revenue Tribunal', 'Court', 'Closed-time barred'],
    vatFiled: ['On time', 'Late', 'Not filed', 'Partly'],
    tdsDep: ['Yes', 'No', 'Partial'],
    acctStatus: ['Not assessed', 'Compliant', 'Deviation', 'Not applicable'],
    rag: ['G', 'A', 'R', 'N'],
    findWs: ['Data room', 'Financial DD', 'Tax DD', 'Accounting DD'],
    protect: ['Price adjustment', 'Indemnity', 'Escrow / holdback', 'Warranty', 'Condition precedent', 'Post-deal action', 'Accept', 'Deal breaker'],
    findStatus: ['Open', 'Info awaited', 'Closed'],
    prio: ['High', 'Medium', 'Low'],
    stage: ['Pre-signing', 'Condition precedent', 'Completion', 'Post-completion']
  };
  const DD_GRID = {
    dr: [['doc', 'Document', 'text'], ['cat', 'Set', 'sel', DD_SEL.drCat], ['per', 'Period', 'text'],
         ['crit', 'Critical', 'sel', DD_SEL.yesno], ['status', 'Status', 'sel', DD_SEL.drStatus],
         ['recd', 'Received', 'text'], ['src', 'Source', 'text'], ['rem', 'Remarks', 'text']],
    conc: [['cust', 'Customer', 'text'], ['rel', 'Relationship', 'text'], ['amt', 'Amount', 'num'],
           ['recv', 'Receivable', 'num'], ['yrs', 'Years as customer', 'num'], ['rem', 'Remarks', 'text']],
    stream: [['name', 'Revenue stream', 'text'], ['amt', 'Amount', 'num'], ['gm', 'Gross margin %', 'num'], ['rem', 'Remarks', 'text']],
    norm: [['fy', 'FY', 'text'], ['desc', 'Description', 'text'], ['amt', 'Amount', 'num'],
           ['conf', 'Confidence', 'sel', DD_SEL.conf], ['ev', 'Evidence', 'text'], ['rem', 'Remarks', 'text']],
    wcm: [['m', 'Month', 'text'], ['inv', 'Inventory', 'num'], ['tr', 'Receivables', 'num'],
          ['oca', 'Other CA', 'num'], ['tp', 'Payables', 'num'], ['ocl', 'Other CL', 'num']],
    debt: [['lender', 'Lender', 'text'], ['ty', 'Type', 'text'], ['sanc', 'Sanctioned', 'num'], ['out', 'Outstanding', 'num'],
           ['rate', 'Rate %', 'num'], ['mat', 'Maturity', 'text'], ['cov', 'Covenant', 'sel', DD_SEL.covStatus],
           ['coc', 'Change of control', 'sel', DD_SEL.coc], ['pg', 'Promoter guarantee', 'sel', DD_SEL.pg]],
    dlike: [['item', 'Item', 'text'], ['cat', 'Category', 'sel', DD_SEL.dlikeCat], ['amt', 'Amount', 'num'], ['basis', 'Basis', 'text'], ['rem', 'Remarks', 'text']],
    rp: [['party', 'Party', 'text'], ['rel', 'Relationship', 'sel', DD_SEL.rpRel], ['nat', 'Nature', 'sel', DD_SEL.rpNat],
         ['amt', 'Amount', 'num'], ['bal', 'Balance', 'num'], ['arm', "Arm's length", 'sel', DD_SEL.arm],
         ['disc', 'NAS 24 disclosure', 'sel', DD_SEL.disc], ['act', 'Action', 'text']],
    oneoff: [['fy', 'FY', 'text'], ['desc', 'Description', 'text'], ['nat', 'Nature', 'sel', DD_SEL.oneoffNat],
             ['amt', 'Amount', 'num'], ['inc', 'In normalisation', 'sel', DD_SEL.yesno], ['ev', 'Evidence', 'text'], ['rem', 'Remarks', 'text']],
    cont: [['item', 'Item', 'text'], ['ty', 'Type', 'text'], ['amt', 'Gross exposure', 'num'], ['prov', 'Provided', 'num'],
           ['prob', 'Probability', 'sel', DD_SEL.prob], ['stat', 'Status / forum', 'text'], ['rem', 'Remarks', 'text']],
    it: [['fy', 'FY', 'text'], ['filed', 'Filed', 'sel', DD_SEL.filed], ['inc', 'Assessable income', 'num'],
         ['tself', 'Tax self-assessed', 'num'], ['tpaid', 'Tax paid', 'num'], ['stat', 'Status', 'sel', DD_SEL.itStat],
         ['dis', 'Estimated disallowance', 'num'], ['mo', 'Months', 'num'], ['rem', 'Remarks', 'text']],
    tds: [['head', 'Payment head', 'text'], ['sec', 'Section', 'text'], ['fy', 'FY', 'text'], ['base', 'Base', 'num'],
          ['applied', 'Applied %', 'num'], ['req', 'Required %', 'num'], ['dep', 'Deposited', 'sel', DD_SEL.tdsDep],
          ['mo', 'Months', 'num'], ['rem', 'Remarks', 'text']],
    vat: [['per', 'Period', 'text'], ['filed', 'Filed', 'sel', DD_SEL.vatFiled], ['late', 'Late returns (count)', 'num'],
          ['sales', 'Sales', 'num'], ['out', 'Output VAT', 'num'], ['inp', 'Input claimed', 'num'],
          ['dis', 'Input at risk', 'num'], ['short', 'Output under-declared', 'num'], ['mo', 'Months', 'num']],
    notice: [['ref', 'Reference', 'text'], ['auth', 'Authority', 'text'], ['fy', 'FY', 'text'], ['subj', 'Subject', 'text'],
             ['dem', 'Demand', 'num'], ['dep', 'Deposit made', 'num'], ['prob', 'Loss probability %', 'num'], ['rem', 'Remarks', 'text']],
    acct: [['std', 'Standard', 'text'], ['area', 'Area', 'text'], ['q', 'Test', 'text'], ['status', 'Status', 'sel', DD_SEL.acctStatus],
           ['find', 'Finding', 'text'], ['pl', 'P&L effect', 'num'], ['bs', 'BS effect', 'num'], ['rag', 'RAG', 'sel', DD_SEL.rag]],
    find: [['ws', 'Workstream', 'sel', DD_SEL.findWs], ['title', 'Title', 'text'], ['detail', 'Detail', 'text'],
           ['qty', 'Quantum', 'num'], ['lik', 'Likelihood (1-5)', 'num'], ['imp', 'Impact (1-5)', 'num'],
           ['prot', 'Protection', 'sel', DD_SEL.protect], ['status', 'Status', 'sel', DD_SEL.findStatus]],
    rec: [['area', 'Area', 'text'], ['rec', 'Recommendation', 'text'], ['prio', 'Priority', 'sel', DD_SEL.prio],
          ['stage', 'Stage', 'sel', DD_SEL.stage], ['own', 'Owner', 'text']]
  };
  function ddRegisterCard(title, note, key, rows, cols) {
    let h = '<div class="card"><h3>' + esc(title) + '</h3>' + (note ? '<p class="card-note">' + esc(note) + '</p>' : '') +
      '<div class="table-wrap"><table><thead><tr>' + cols.map(c => '<th>' + esc(c[1]) + '</th>').join('') + '<th></th></tr></thead><tbody>';
    rows.forEach((row, i) => {
      h += '<tr>' + cols.map(c => {
        const [k, , ty, opts] = c;
        const v = row[k] === undefined || row[k] === null ? '' : row[k];
        if (ty === 'sel') return '<td><select data-reg="' + key + '" data-i="' + i + '" data-k="' + k + '">' +
          opts.map(o => '<option value="' + esc(o) + '"' + (String(v) === o ? ' selected' : '') + '>' + esc(o) + '</option>').join('') + '</select></td>';
        if (ty === 'num') return '<td class="num"><input type="number" step="any" data-reg="' + key + '" data-i="' + i + '" data-k="' + k + '" value="' + esc(v) + '"></td>';
        return '<td><input data-reg="' + key + '" data-i="' + i + '" data-k="' + k + '" value="' + esc(v) + '"></td>';
      }).join('') + '<td><button class="btn btn-small btn-danger" data-del-reg="' + key + '" data-i="' + i + '" title="Remove this row">Remove</button></td></tr>';
    });
    h += '</tbody></table></div><button class="btn btn-small" data-add-reg="' + key + '">Add a row</button></div>';
    return h;
  }
  function wireDdRegisters(root, payload, rerender) {
    payload.g = payload.g || {};
    root.querySelectorAll('[data-reg]').forEach(el => el.addEventListener('change', e => {
      const key = e.target.dataset.reg, i = +e.target.dataset.i, k = e.target.dataset.k;
      payload.g[key] = payload.g[key] || [];
      payload.g[key][i] = payload.g[key][i] || {};
      payload.g[key][i][k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
      rerender();
    }));
    root.querySelectorAll('[data-del-reg]').forEach(el => el.addEventListener('click', e => {
      const key = e.target.dataset.delReg, i = +e.target.dataset.i;
      payload.g[key].splice(i, 1); rerender();
    }));
    root.querySelectorAll('[data-add-reg]').forEach(el => el.addEventListener('click', e => {
      const key = e.target.dataset.addReg;
      payload.g[key] = payload.g[key] || [];
      payload.g[key].push({}); rerender();
    }));
  }

  const DD_DR_SEED = [
    ["Audited financial statements + audit report", "Financial", "5 years", "Yes"],
    ["Trial balance (year-wise, full)", "Financial", "5 years", "Yes"],
    ["General ledger / detailed ledger dumps", "Financial", "3 years", "Yes"],
    ["Income tax returns (D-03) + tax computation", "Tax", "5 years", "Yes"],
    ["Income tax assessment / amended assessment orders", "Tax", "5 years", "Yes"],
    ["VAT returns + purchase/sales registers", "Tax", "3 years", "Yes"],
    ["TDS returns (e-TDS) + deposit vouchers", "Tax", "3 years", "Yes"],
    ["Tax clearance certificates", "Tax", "5 years", "Yes"],
    ["Bank statements — all accounts", "Financial", "24 months", "Yes"],
    ["Loan agreements, sanction letters, security documents", "Financial", "Current", "Yes"],
    ["Legal: MOA / AOA / incorporation & PAN-VAT certificates", "Legal", "Current", "Yes"],
    ["Legal: licences, permits, industry registration", "Legal", "Current", "Yes"],
    ["Legal: litigation status, notices, legal counsel letter", "Legal", "Current", "Yes"],
    ["Fixed asset register + depreciation schedule (tax & book)", "Financial", "5 years", "No"],
    ["Inventory listing with ageing & valuation basis", "Financial", "3 years", "No"],
    ["Debtors & creditors ageing", "Financial", "3 years", "No"],
    ["Payroll register, PF / SSF / gratuity workings", "Financial", "2 years", "No"],
    ["Related-party listing, agreements and balances", "Financial", "5 years", "Yes"],
    ["Board minutes / AGM minutes / shareholder register", "Legal", "5 years", "No"],
    ["Major customer & supplier contracts", "Legal", "Current", "No"],
    ["Insurance policies and claim history", "Legal", "Current", "No"],
    ["Audit management letters / internal audit reports", "Financial", "3 years", "No"],
    ["Statutory dues status (PF, SSF, VAT, TDS, local tax)", "Tax", "Current", "Yes"],
    ["Capital structure history / share transfer records", "Legal", "Since inception", "No"]
  ].map(d => ({ doc: d[0], cat: d[1], per: d[2], crit: d[3], status: 'Pending', recd: '', src: '', rem: '' }));
  const DD_ACCT_SEED = [
    ["NFRS 15", "Revenue recognition", "Is revenue recognised on transfer of control, with the 5-step model documented?"],
    ["NFRS 15", "Cut-off", "Are period-end sales / returns / credit notes recorded in the correct period?"],
    ["NAS 2", "Inventory valuation", "Lower of cost and NRV; costing method consistent; slow-moving provision."],
    ["NAS 16", "PPE recognition & measurement", "Capitalisation policy, componentisation, useful lives, residual values."],
    ["NAS 16", "Book vs tax depreciation", "Book depreciation aligned to useful life, not Schedule 2 pool rates."],
    ["NFRS 9", "ECL on receivables", "Expected credit loss model applied; provision matrix supportable."],
    ["NAS 37", "Provisions", "Present obligation, reliable estimate, no profit-smoothing reserves."],
    ["NAS 37", "Contingent liabilities", "Disclosure completeness — guarantees, LCs, disputes, tax cases."],
    ["NAS 19", "Employee benefits", "Gratuity / leave encashment measured actuarially, not ad-hoc."],
    ["NFRS 16", "Leases", "Right-of-use assets and lease liabilities recognised for leases."],
    ["NAS 12", "Deferred tax", "Full temporary-difference approach; recoverability of DTA."],
    ["NAS 24", "Related parties", "Identification and disclosure of all related-party transactions."],
    ["NAS 1", "Presentation & going concern", "Classification, offsetting, going-concern assessment."]
  ].map(a => ({ std: a[0], area: a[1], q: a[2], status: 'Not assessed', find: '', pl: 0, bs: 0, rag: 'N' }));
  const DD_TDS_SEED = [
    ["Salary & wages", "Sec 87", 0], ["House rent — to person", "Sec 88(1)", 10], ["Service fee / contract payment", "Sec 89", 1.5],
    ["Consultancy / professional (VAT registered)", "Sec 88", 1.5], ["Consultancy / professional (non-registered)", "Sec 88", 15],
    ["Commission", "Sec 88", 15], ["Interest to non-bank", "Sec 88", 15], ["Dividend distribution", "Sec 88", 5],
    ["Payment to non-resident", "Sec 88", 15], ["Meeting / sitting fee", "Sec 88", 15], ["Freight / carriage", "Sec 89", 1.5],
    ["Rent of vehicle (registered)", "Sec 88", 1.5]
  ].map(t => ({ head: t[0], sec: t[1], fy: '', base: 0, applied: 0, req: t[2], dep: 'Yes', mo: 0, rem: '' }));

  MODULES.dd = {
    code: 'DD', name: 'Due diligence', group: 'Advisory',
    blurb: 'Financial, tax, accounting and commercial due diligence for buy-side or vendor engagements — five-year financials, a quality-of-earnings bridge to normalised EBITDA, a standalone net debt analyzer, a scored commercial DD assessment, seventeen working registers, a rule-based risk register with RAG scoring, quantified tax exposure and an EV-to-equity deal-impact bridge.',
    bulkImport: {
      // Seventeen sheets, one per register, matched by key straight into
      // payload.g — the server-side spec's columns are kept in lockstep with
      // DD_GRID above, so the imported row shape is already what wireDdRegisters expects.
      apply(file, sheets) {
        const p = file.payload;
        p.g = p.g || {};
        Object.keys(sheets).forEach(key => {
          p.g[key] = (p.g[key] || []).concat(sheets[key].rows || []);
        });
      }
    },
    seed: () => ({
      eng: { target: '', pan: '', sector: 'Manufacturing', txn: 'Acquisition', stake: 100, dealval: 0,
             unit: 'Thousands', cur: 'NPR', latest_fy: '2081/82', nyears: 5, matmode: 'auto', mat: 0, multiple: 0,
             perspective: 'buy_side' },
      fin: {}, ov: {},
      comm: { market_size: '', market_growth_pct: '', competitive_position: '', key_customers_note: '',
              growth_drivers: '', commercial_risks: '', notes: '' },
      g: { dr: DD_DR_SEED.map(r => Object.assign({}, r)), acct: DD_ACCT_SEED.map(r => Object.assign({}, r)),
           tds: DD_TDS_SEED.map(r => Object.assign({}, r)), conc: [], stream: [], norm: [], wcm: [], debt: [],
           dlike: [], rp: [], oneoff: [], cont: [], it: [], vat: [], notice: [], find: [], rec: [] }
    }),
    view(file, res) {
      const p = file.payload;
      p.g = p.g || {}; p.fin = p.fin || {}; p.ov = p.ov || {}; p.comm = p.comm || {};
      let h = '<div class="card" id="dd-eng"><h3>Engagement</h3><div class="grid c3">' +
        field('Target', 'target', p.eng, 'text') +
        field('Sector', 'sector', p.eng, 'text') +
        field('Transaction type', 'txn', p.eng, 'select', { options: [
          { value: 'Acquisition', label: 'Acquisition' }, { value: 'Investment', label: 'Investment' },
          { value: 'Merger', label: 'Merger' }, { value: 'IPO readiness', label: 'IPO readiness' },
          { value: 'Slump sale', label: 'Slump sale' }, { value: 'Joint venture', label: 'Joint venture' }] }) +
        field('Perspective', 'perspective', p.eng, 'select', { hint: 'Who commissioned the review — changes only the report framing, not the analysis', options: [
          { value: 'buy_side', label: 'Buy-side (acquirer-commissioned)' },
          { value: 'sell_side', label: 'Vendor / sell-side (target-commissioned, pre-emptive)' }] }) +
        field('Stake acquired %', 'stake', p.eng, 'number') +
        field('Latest fiscal year', 'latest_fy', p.eng, 'text', { hint: 'e.g. 2081/82' }) +
        field('Years in review', 'nyears', p.eng, 'select', { options: [3, 4, 5, 6].map(n => ({ value: n, label: n + ' years' })) }) +
        field('Presentation unit', 'unit', p.eng, 'select', { options: [
          { value: 'Actual', label: 'Actual' }, { value: 'Thousands', label: 'Thousands' },
          { value: 'Lakh', label: 'Lakh' }, { value: 'Million', label: 'Million' }, { value: 'Crore', label: 'Crore' }] }) +
        field('Currency', 'cur', p.eng, 'text') +
        field('Deal value (if no multiple)', 'dealval', p.eng, 'number') +
        field('EV / EBITDA multiple (0 = use deal value)', 'multiple', p.eng, 'number') +
        field('Materiality basis', 'matmode', p.eng, 'select', { options: [
          { value: 'auto', label: 'Auto — higher of 0.5% revenue or 5% EBITDA' }, { value: 'manual', label: 'Manual' }] }) +
        (p.eng.matmode === 'manual' ? field('Materiality', 'mat', p.eng, 'number') : '') +
        '</div></div>';

      const v = res.overall, vcls = v === 'R' ? 'alert' : (v === 'A' ? '' : 'accent');
      h += '<div class="card"><h3>Assessment</h3><div class="grid c4">' +
        kpi('Overall rating', ({ R: 'Red', A: 'Amber', G: 'Green' })[v], vcls) +
        kpi('Findings — R · A · G', res.rag.R + ' · ' + res.rag.A + ' · ' + res.rag.G) +
        kpi('Quantified exposure', money(res.exposure), 'alert') +
        kpi('Materiality', money(res.mat)) +
        '</div></div>';

      h += resultCard('Deal impact bridge', ['Particulars', 'Amount'], [
        ['Enterprise value', res.deal.ev], ['Less: net debt (adjusted)', -res.deal.netdebt],
        ['Working capital adjustment', res.deal.wc_adj], ['Less: tax exposure', res.deal.tax],
        ['Less: contingent liabilities (weighted)', res.deal.cont], ['Accounting adjustments', res.deal.acct],
        ['Indicative equity value', res.deal.equity], ['Suggested escrow / holdback', res.deal.escrow]]);

      h += resultCard('Net debt analyzer', ['Particulars', 'Amount'], [
        ['Gross financial debt (per the debt register)', res.debt_gross],
        ['Add: debt-like items (unfunded benefits, statutory dues, declared dividends, capex creditors...)', res.dlike_d],
        ['Less: cash and cash equivalents', -res.L.cash],
        ['Less: cash-like items', -res.dlike_c],
        ['Adjusted net debt', res.net_debt_adj],
        ['Memo: off-balance-sheet items (not in the total above — disclose separately)', res.off_bs]]);

      h += '<div class="card"><h3>Commercial due diligence</h3><p class="card-note">Market and competitive position — mostly qualitative, alongside the financial workstreams above. The score below is a starting-point assessment (market growth + customer concentration, already computed from the DD financials above + your position rating) — a judgement call for you to review, not a substitute for it.</p><div class="grid c2" id="dd-comm">' +
        field('Market size', 'market_size', p.comm, 'text') +
        field('Market growth % per year', 'market_growth_pct', p.comm, 'number') +
        field('Competitive position', 'competitive_position', p.comm, 'text', { placeholder: 'Market leader / challenger / niche...' }) +
        field('Position rating (1 weak — 5 strong)', 'position_rating', p.comm, 'select', { options: [
          { value: '', label: '— not rated —' }, { value: '1', label: '1 — weak / distressed' }, { value: '2', label: '2 — below average' },
          { value: '3', label: '3 — average' }, { value: '4', label: '4 — strong' }, { value: '5', label: '5 — market leader' }] }) +
        field('Key customer / supplier dependency', 'key_customers_note', p.comm, 'text') +
        field('Growth drivers', 'growth_drivers', p.comm, 'text') +
        field('Commercial risks', 'commercial_risks', p.comm, 'text') +
        '<div class="field" style="grid-column:1/-1"><label>Notes</label><textarea data-key="notes" rows="2">' + esc(p.comm.notes || '') + '</textarea></div>' +
        '</div>' +
        (res.comm_score && res.comm_score.overall_score !== null ?
          '<div class="grid c4" style="margin-top:14px">' +
          kpi('Growth score', res.comm_score.growth_score === null ? '—' : res.comm_score.growth_score) +
          kpi('Concentration score', res.comm_score.concentration_score === null ? '—' : res.comm_score.concentration_score) +
          kpi('Position score', res.comm_score.position_score === null ? '—' : res.comm_score.position_score) +
          kpi('Overall commercial score', res.comm_score.overall_score, res.comm_score.rag === 'R' ? 'alert' : (res.comm_score.rag === 'G' ? 'accent' : '')) +
          '</div>' : '') +
        '</div>';

      const ys = res.ys;
      h += resultCard('Quality of earnings bridge — reported to normalised EBITDA', ['Particulars', ...ys], [
        ['Reported EBITDA', ...ys.map(y => res.y[y].ebitda)],
        ['+/- normalisation adjustments', ...ys.map(y => res.y[y].norm_adj)],
        ['+/- one-off / non-recurring items', ...ys.map(y => res.y[y].oneoff_adj)],
        ['= Normalised (quality-of-earnings) EBITDA', ...ys.map(y => res.y[y].nebitda)]]);
      h += '<div class="card"><h3>Financials — profit and loss</h3><div class="table-wrap"><table><thead><tr><th>FY</th>' +
        ['rev', 'otherinc', 'cogs', 'staff', 'rentx', 'admin', 'selling', 'otheropex', 'dep', 'amort', 'fin', 'excep', 'tax']
          .map(k => '<th>' + k + '</th>').join('') + '</tr></thead><tbody>';
      ys.forEach(yr => {
        const row = p.fin[yr] || {};
        h += '<tr><td>' + esc(yr) + '</td>' + ['rev', 'otherinc', 'cogs', 'staff', 'rentx', 'admin', 'selling', 'otheropex', 'dep', 'amort', 'fin', 'excep', 'tax']
          .map(k => '<td class="num"><input type="number" step="any" data-fin="' + yr + '" data-k="' + k + '" value="' + (row[k] || 0) + '"></td>').join('') + '</tr>';
      });
      h += '</tbody></table></div></div>';

      h += '<div class="card"><h3>Financials — balance sheet</h3><div class="table-wrap"><table><thead><tr><th>FY</th>' +
        ['ppe', 'cwip', 'intang', 'invst', 'othernca', 'inv', 'tr', 'otherca', 'cash', 'eqcap', 'resv', 'ltd', 'ltprov', 'dtl', 'std', 'curmat', 'tp', 'othercl', 'provtax']
          .map(k => '<th>' + k + '</th>').join('') + '</tr></thead><tbody>';
      ys.forEach(yr => {
        const row = p.fin[yr] || {};
        h += '<tr><td>' + esc(yr) + '</td>' + ['ppe', 'cwip', 'intang', 'invst', 'othernca', 'inv', 'tr', 'otherca', 'cash', 'eqcap', 'resv', 'ltd', 'ltprov', 'dtl', 'std', 'curmat', 'tp', 'othercl', 'provtax']
          .map(k => '<td class="num"><input type="number" step="any" data-fin="' + yr + '" data-k="' + k + '" value="' + (row[k] || 0) + '"></td>').join('') + '</tr>';
      });
      h += '</tbody></table></div></div>';

      h += '<div class="card"><h3>Financials — cash flow and other</h3><div class="table-wrap"><table><thead><tr><th>FY</th>' +
        ['cfo', 'capex', 'top5', 'rppct', 'cashsales'].map(k => '<th>' + k + '</th>').join('') + '<th>Audit opinion</th></tr></thead><tbody>';
      ys.forEach(yr => {
        const row = p.fin[yr] || {};
        h += '<tr><td>' + esc(yr) + '</td>' + ['cfo', 'capex', 'top5', 'rppct', 'cashsales']
          .map(k => '<td class="num"><input type="number" step="any" data-fin="' + yr + '" data-k="' + k + '" value="' + (row[k] || 0) + '"></td>').join('') +
          '<td><select data-fin="' + yr + '" data-k="opinion">' +
          ['Unmodified', 'Emphasis of matter', 'Qualified', 'Adverse', 'Disclaimer', 'Not audited'].map(o =>
            '<option value="' + o + '"' + ((row.opinion || 'Unmodified') === o ? ' selected' : '') + '>' + o + '</option>').join('') + '</select></td></tr>';
      });
      h += '</tbody></table></div></div>';

      h += resultCard('Financial ratios by year', ['Metric', ...ys], [
        ['Revenue', ...ys.map(y => res.y[y].rev)], ['EBITDA', ...ys.map(y => res.y[y].ebitda)],
        ['Normalised EBITDA', ...ys.map(y => res.y[y].nebitda)], ['PAT', ...ys.map(y => res.y[y].pat)],
        ['EBITDA margin %', ...ys.map(y => res.y[y].em)], ['Current ratio', ...ys.map(y => res.y[y].cr)],
        ['Net debt / EBITDA', ...ys.map(y => res.y[y].lev)], ['Interest cover', ...ys.map(y => res.y[y].icr)],
        ['Cash conversion %', ...ys.map(y => res.y[y].conv)], ['DSO (days)', ...ys.map(y => res.y[y].dso)],
        ['Balance check (should be ~0)', ...ys.map(y => res.y[y].chk)]]);

      const regTitles = {
        conc: ['Customer concentration', ''], stream: ['Revenue streams', ''],
        norm: ['EBITDA normalisation adjustments', ''], wcm: ['Monthly working capital (12 months for the seasonality check)', ''],
        debt: ['Loan and facility register', ''], dlike: ['Debt-like / cash-like items', ''],
        rp: ['Related-party register', ''], oneoff: ['One-off / non-recurring items', ''],
        cont: ['Contingent liabilities', ''], it: ['Income tax by year', ''], tds: ['TDS register', ''],
        vat: ['VAT register', ''], notice: ['Tax notices / assessments', ''],
        acct: ['NFRS / NAS accounting assessment', ''], dr: ['Data room / document register', ''],
        find: ['Manual findings', 'Auto-raised findings appear in the risk register below and do not need a row here.'],
        rec: ['Recommendations', '']
      };
      Object.keys(regTitles).forEach(key => {
        h += ddRegisterCard(regTitles[key][0], regTitles[key][1], key, p.g[key] || [], DD_GRID[key]);
      });

      h += '<div class="card"><h3>Risk and findings register</h3><div class="table-wrap"><table><thead><tr>' +
        '<th>#</th><th>Rating</th><th>Workstream</th><th>Finding</th><th class="num">Quantum</th><th>Protection</th><th>Status</th></tr></thead><tbody>';
      res.find.forEach(f => {
        const ragCls = f.rag === 'R' ? 'tag-red' : (f.rag === 'A' ? 'tag-amber' : 'tag-green');
        h += '<tr><td>' + f.n + '</td><td><span class="tag ' + ragCls + '">' + ({ R: 'Red', A: 'Amber', G: 'Green' })[f.rag] + '</span></td>' +
          '<td>' + esc(f.ws || '') + '</td><td><b>' + esc(f.title) + '</b><br><small>' + esc(f.detail || '') + '</small></td>' +
          '<td class="num">' + money(f.qty) + '</td><td>' + esc(f.prot || '') + '</td><td>' + esc(f.status || '') + '</td></tr>';
      });
      h += '</tbody></table></div></div>';
      return h;
    },
    wire(root, file, rerender) {
      const p = file.payload;
      wireInto(root, 'dd-eng', p.eng, rerender);
      wireInto(root, 'dd-comm', p.comm, rerender);
      wireDdRegisters(root, p, rerender);
      root.querySelectorAll('[data-fin]').forEach(el => el.addEventListener('change', e => {
        const yr = e.target.dataset.fin, k = e.target.dataset.k;
        p.fin[yr] = p.fin[yr] || {};
        p.fin[yr][k] = e.target.tagName === 'SELECT' ? e.target.value : Number(e.target.value);
        rerender();
      }));
    },
    tabulate: res => {
      const ragLabel = { R: 'Red', A: 'Amber', G: 'Green', N: '—' };
      const d = res.deal, comm = res.comm || {};
      const commRows = [['Market size', comm.market_size], ['Market growth', comm.market_growth_pct],
        ['Competitive position', comm.competitive_position], ['Key customer / supplier dependency', comm.key_customers_note],
        ['Growth drivers', comm.growth_drivers], ['Commercial risks', comm.commercial_risks], ['Notes', comm.notes]]
        .filter(([, v]) => v);
      const ys = res.ys;
      const finMetrics = [['Revenue', 'rev'], ['EBITDA', 'ebitda'], ['+/- normalisation adjustments', 'norm_adj'],
        ['+/- one-off / non-recurring items', 'oneoff_adj'], ['Normalised (quality-of-earnings) EBITDA', 'nebitda'],
        ['PAT', 'pat'], ['Net working capital', 'nwc'], ['Net debt', 'netdebt'], ['EBITDA margin %', 'em'],
        ['Current ratio', 'cr'], ['Net debt / EBITDA', 'lev'], ['Interest cover', 'icr'], ['Cash conversion %', 'conv'],
        ['DSO (days)', 'dso'], ['DIO (days)', 'dio'], ['DPO (days)', 'dpo']];
      return [{
        title: 'Summary', headers: ['Particulars', 'Amount'],
        rows: [['Overall rating', ragLabel[res.overall] || res.overall],
          ['Red · Amber · Green', res.rag.R + ' · ' + res.rag.A + ' · ' + res.rag.G],
          ['Quantified exposure', res.exposure], ['Materiality', res.mat],
          ['Enterprise value', d.ev], ['Less: net debt (adjusted)', -d.netdebt],
          ['Working capital adjustment', d.wc_adj], ['Less: tax exposure', d.tax],
          ['Less: contingent liabilities (weighted)', d.cont], ['Accounting adjustments', d.acct],
          ['Indicative equity value', d.equity], ['Suggested escrow / holdback', d.escrow]]
      }].concat(commRows.length ? [{ title: 'Commercial due diligence', headers: ['Particulars', 'Value'], rows: commRows }] : []).concat([{
        title: 'Financial summary', headers: ['Metric', ...ys],
        rows: finMetrics.map(([label, key]) => [label, ...ys.map(yr => res.y[yr] && res.y[yr][key])])
      }, {
        title: 'Risk register',
        headers: ['#', 'Rating', 'Workstream', 'Finding', 'Detail', 'Quantum', 'Likelihood', 'Impact', 'Protection', 'Status'],
        rows: res.find.map(f => [f.n, ragLabel[f.rag] || f.rag, f.ws, f.title, f.detail, f.qty, f.lik, f.imp, f.prot, f.status])
      }, {
        title: 'Data room', headers: ['Document', 'Set', 'Period', 'Critical', 'Status', 'Received', 'Source'],
        rows: ((res.registers && res.registers.dr) || []).map(r_ => [r_.doc, r_.cat || '', r_.per || '', r_.crit || '', r_.status || '', r_.recd || '', r_.src || ''])
      }, {
        title: 'Tax exposure', headers: ['Head', 'Amount'],
        rows: [['Income tax', res.tax_it], ['TDS', res.tax_tds], ['VAT', res.tax_vat],
          ['Open notices (probability-weighted)', res.tax_not], ['Total', res.tax_total]]
      }]);
    }
  };

  MODULES.capital_advisory = {
    code: 'CS', name: 'Capital & Debt Advisory', group: 'Advisory',
    blurb: 'Debt capacity (DSCR and leverage based, with a headroom check), a side-by-side loan restructuring comparison against the current facility, a debt-vs-equity funding mix / WACC analyzer, and break-even analysis — each with a built-in what-if / sensitivity table. For the full amortisation schedule of whichever restructuring option is adopted, use the Loan & EMI module\'s own modification feature; for a full multi-year project cash-flow model with its own 7-scenario sensitivity, use Projection.',
    seed: () => ({
      entity: { name: '', currency: 'NPR', period_label: '', purpose: '' },
      capacity: { ebitda: 0, cash_accruals: '', existing_debt: 0, existing_debt_service: 0, target_dscr: 1.25,
                 interest_rate: 12, tenure_years: 5, max_leverage_multiple: 3, proposed_new_debt: '' },
      restructure: { current: { principal: 0, rate: 12, tenure_years: 5 }, discount_rate: '', options: [] },
      funding: { project_cost: 0, cost_of_debt: 12, cost_of_equity: 18, tax_rate: 25,
                scenarios: [{ label: '70:30', debt_pct: 70 }, { label: '60:40', debt_pct: 60 }, { label: '50:50', debt_pct: 50 }] },
      breakeven: { mode: 'unit', fixed_costs: 0, selling_price_per_unit: 0, variable_cost_per_unit: 0, current_volume: 0,
                  variable_cost_pct_of_revenue: 0, current_revenue: 0 },
      signatory: { name: '', designation: 'ca', membership_no: '', firm_name: '', place: '', date: '' },
    }),
    view(file, res) {
      const p = file.payload;
      p.entity = p.entity || {}; p.capacity = p.capacity || {}; p.restructure = p.restructure || {};
      p.restructure.current = p.restructure.current || {}; p.restructure.options = p.restructure.options || [];
      p.funding = p.funding || {}; p.funding.scenarios = p.funding.scenarios || [];
      p.breakeven = p.breakeven || {}; p.signatory = p.signatory || {};

      let h = '<div class="card"><h3>Engagement</h3><div class="grid c3" id="ca-entity">' +
        field('Client / entity name', 'name', p.entity, 'text') +
        field('Currency', 'currency', p.entity, 'text') +
        field('Period', 'period_label', p.entity, 'text', { placeholder: 'FY ' + state.fy }) +
        field('Purpose', 'purpose', p.entity, 'text', { placeholder: 'e.g. Term loan enhancement, refinancing, M&A financing' }) +
        '</div></div>';

      // ---- Debt capacity ----
      h += '<div class="card"><h3>Debt capacity analyzer</h3><p class="card-note">Two independent ceilings — the smaller one binds. DSCR-based caps the new annual debt service so total debt service (existing + new) stays within the target cover; leverage-based caps total debt as a multiple of EBITDA.</p><div class="grid c3" id="ca-capacity">' +
        field('EBITDA (or normalised EBITDA)', 'ebitda', p.capacity, 'number') +
        field('Cash accruals available for debt service (blank = use EBITDA)', 'cash_accruals', p.capacity, 'number') +
        field('Existing debt outstanding', 'existing_debt', p.capacity, 'number') +
        field('Existing annual debt service', 'existing_debt_service', p.capacity, 'number') +
        field('Target DSCR', 'target_dscr', p.capacity, 'number', { step: '0.01' }) +
        field('Interest rate on new debt (% p.a.)', 'interest_rate', p.capacity, 'number', { step: '0.01' }) +
        field('Tenure of new debt (years)', 'tenure_years', p.capacity, 'number') +
        field('Maximum Debt / EBITDA (leverage covenant)', 'max_leverage_multiple', p.capacity, 'number', { step: '0.1' }) +
        field('Proposed new debt (optional, for a headroom check)', 'proposed_new_debt', p.capacity, 'number') +
        '</div></div>';

      const c = res.capacity;
      h += '<div class="grid c4">' +
        kpi('DSCR-based capacity', money(c.dscr_based_capacity)) +
        kpi('Leverage-based capacity', money(c.leverage_based_capacity)) +
        kpi('Recommended max new debt', money(c.recommended_max_new_debt), 'accent') +
        kpi('Binding constraint', c.binding_constraint) +
        '</div>';
      if (c.headroom !== null && c.headroom !== undefined) {
        h += '<div class="msg ' + (c.headroom < 0 ? 'bad' : 'good') + '">' +
          (c.headroom < 0 ? 'The proposed new debt exceeds capacity by ' + money(-c.headroom) + '.' : 'Headroom of ' + money(c.headroom) + ' above the proposed new debt.') + '</div>';
      }
      h += resultCard('Debt capacity — sensitivity to EBITDA', ['Scenario', 'DSCR-based', 'Leverage-based', 'Recommended'],
        c.sensitivity.map(s => [s.scenario, s.dscr_based_capacity, s.leverage_based_capacity, s.recommended_max_new_debt]));

      // ---- Loan restructuring comparator ----
      h += '<div class="card"><h3>Loan restructuring analyzer</h3><p class="card-note">Compares proposed restructuring terms against the current facility on a like-for-like present-value basis (discounted at the rate below) — a longer tenure at a lower rate can cost MORE in total nominal interest yet still be cheaper in present-value terms, and both figures are shown.</p><div class="grid c3" id="ca-restructure-current">' +
        field('Current principal outstanding', 'principal', p.restructure.current, 'number') +
        field('Current rate (% p.a.)', 'rate', p.restructure.current, 'number', { step: '0.01' }) +
        field('Current remaining tenure (years)', 'tenure_years', p.restructure.current, 'number') +
        '</div><div class="grid c3" id="ca-restructure-pv">' +
        field('Discount rate for PV comparison (blank = current rate)', 'discount_rate', p.restructure, 'number', { step: '0.01' }) +
        '</div></div>';

      h += '<div class="card"><h3>Restructuring options</h3><div class="table-wrap"><table><thead><tr><th>Label</th>' +
        '<th class="num">New rate %</th><th class="num">New tenure (yrs)</th><th class="num">Moratorium (yrs)</th><th></th></tr></thead><tbody>';
      p.restructure.options.forEach((o, i) => {
        h += '<tr><td><input data-rst="' + i + '" data-k="label" value="' + esc(o.label || '') + '"></td>' +
          '<td class="num"><input type="number" step="0.01" data-rst="' + i + '" data-k="new_rate" value="' + (o.new_rate ?? '') + '"></td>' +
          '<td class="num"><input type="number" data-rst="' + i + '" data-k="new_tenure_years" value="' + (o.new_tenure_years ?? '') + '"></td>' +
          '<td class="num"><input type="number" data-rst="' + i + '" data-k="moratorium_years" value="' + (o.moratorium_years || 0) + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-rst="' + i + '" title="Remove">Remove</button></td></tr>';
      });
      h += '</tbody></table></div><button class="btn btn-small" id="ca-add-rst">Add an option</button></div>';

      const rs = res.restructuring;
      if (rs.options && rs.options.length) {
        h += resultCard('Restructuring comparison', ['Option', 'Rate %', 'Tenure', 'Annual payment', 'Total interest', 'PV of payments', 'Interest saving', 'PV saving'],
          [[rs.current.label, rs.current.rate_pct, rs.current.tenure_years, rs.current.annual_payment, rs.current.total_interest, rs.current.pv_of_payments, 0, 0]]
            .concat(rs.options.map(o => [o.label, o.rate_pct, o.tenure_years, o.annual_payment, o.total_interest, o.pv_of_payments, o.interest_saving_vs_current, o.pv_saving_vs_current])));
        h += '<div class="msg good">Lowest present-value option: <b>' + esc(rs.lowest_pv_option || '') + '</b></div>';
      }

      // ---- Debt vs equity funding ----
      h += '<div class="card"><h3>Debt vs equity funding analyzer</h3><div class="grid c4" id="ca-funding">' +
        field('Project cost', 'project_cost', p.funding, 'number') +
        field('Cost of debt (% p.a., pre-tax)', 'cost_of_debt', p.funding, 'number', { step: '0.01' }) +
        field('Cost of equity (% p.a.)', 'cost_of_equity', p.funding, 'number', { step: '0.01' }) +
        field('Tax rate (%)', 'tax_rate', p.funding, 'number', { step: '0.01' }) +
        '</div></div>';

      h += '<div class="card"><h3>Funding mix scenarios</h3><div class="table-wrap"><table><thead><tr><th>Label</th><th class="num">Debt %</th><th></th></tr></thead><tbody>';
      p.funding.scenarios.forEach((s, i) => {
        h += '<tr><td><input data-fnd="' + i + '" data-k="label" value="' + esc(s.label || '') + '"></td>' +
          '<td class="num"><input type="number" step="1" data-fnd="' + i + '" data-k="debt_pct" value="' + (s.debt_pct || 0) + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-fnd="' + i + '" title="Remove">Remove</button></td></tr>';
      });
      h += '</tbody></table></div><button class="btn btn-small" id="ca-add-fnd">Add a scenario</button></div>';

      const fu = res.funding;
      if (fu.scenarios && fu.scenarios.length) {
        h += resultCard('Funding mix comparison', ['Scenario', 'Debt %', 'Equity %', 'Debt amount', 'Equity amount', 'Annual interest', 'Annual tax shield', 'WACC %'],
          fu.scenarios.map(s => [s.label, s.debt_pct, s.equity_pct, s.debt_amount, s.equity_amount, s.annual_interest, s.annual_tax_shield, s.wacc_pct]));
        h += '<div class="msg good">Lowest WACC: <b>' + esc(fu.lowest_wacc_scenario || '') + '</b> — WACC alone does not capture financial-distress risk at high leverage; use alongside the debt capacity check above.</div>';
      }

      // ---- Break-even ----
      h += '<div class="card"><h3>Break-even analysis</h3><div class="grid c3" id="ca-breakeven">' +
        field('Mode', 'mode', p.breakeven, 'select', { options: [
          { value: 'unit', label: 'Unit economics (price and variable cost per unit)' },
          { value: 'revenue', label: 'Revenue-based (service business — % of revenue)' }] }) +
        field('Fixed costs (per year)', 'fixed_costs', p.breakeven, 'number') +
        (p.breakeven.mode === 'revenue' ?
          field('Variable cost — % of revenue', 'variable_cost_pct_of_revenue', p.breakeven, 'number', { step: '0.01' }) +
          field('Current revenue', 'current_revenue', p.breakeven, 'number')
          :
          field('Selling price per unit', 'selling_price_per_unit', p.breakeven, 'number') +
          field('Variable cost per unit', 'variable_cost_per_unit', p.breakeven, 'number') +
          field('Current volume (units)', 'current_volume', p.breakeven, 'number')) +
        '</div></div>';

      const be = res.breakeven;
      const pctv = v => (v === null || v === undefined) ? '—' : v.toFixed(2) + '%';
      if (be.mode === 'revenue') {
        h += '<div class="grid c4">' +
          kpi('Contribution margin ratio', pctv(be.contribution_margin_ratio_pct)) +
          kpi('Break-even revenue', money(be.bep_revenue), 'accent') +
          kpi('Margin of safety', be.margin_of_safety != null ? money(be.margin_of_safety) : '—') +
          kpi('Margin of safety %', pctv(be.margin_of_safety_pct)) +
          '</div>';
        h += resultCard('Break-even — sensitivity', ['Scenario', 'Break-even revenue'], be.sensitivity.map(s => [s.scenario, s.bep_revenue]));
      } else {
        h += '<div class="grid c4">' +
          kpi('Contribution margin / unit', money(be.contribution_margin_per_unit)) +
          kpi('Break-even units', be.bep_units != null ? be.bep_units : '—', 'accent') +
          kpi('Break-even revenue', be.bep_revenue != null ? money(be.bep_revenue) : '—') +
          kpi('Margin of safety %', pctv(be.margin_of_safety_pct)) +
          '</div>';
        if (be.operating_leverage != null) h += '<div class="msg good">Degree of operating leverage: ' + be.operating_leverage + 'x — a 1% change in volume moves operating profit by roughly ' + be.operating_leverage + '%.</div>';
        h += resultCard('Break-even — sensitivity', ['Scenario', 'Break-even units', 'Break-even revenue'], be.sensitivity.map(s => [s.scenario, s.bep_units, s.bep_revenue]));
      }

      h += '<div class="card"><h3>Report signatory</h3><div class="grid c3" id="ca-signatory">' +
        field('Signatory name', 'name', p.signatory, 'text') +
        field('Designation', 'designation', p.signatory, 'select', { options: [
          { value: 'ca', label: 'Chartered Accountant (CA)' }, { value: 'ra', label: 'Registered Auditor (RA)' }] }) +
        field('Membership no.', 'membership_no', p.signatory, 'text') +
        field('Firm name', 'firm_name', p.signatory, 'text') +
        field('Place', 'place', p.signatory, 'text') +
        field('Date', 'date', p.signatory, 'date') +
        '</div></div>';

      return h;
    },
    wire(root, file, rerender) {
      const p = file.payload;
      wireInto(root, 'ca-entity', p.entity, rerender);
      wireInto(root, 'ca-capacity', p.capacity, rerender);
      wireInto(root, 'ca-restructure-current', p.restructure.current, rerender);
      wireInto(root, 'ca-restructure-pv', p.restructure, rerender);
      wireInto(root, 'ca-funding', p.funding, rerender);
      wireInto(root, 'ca-breakeven', p.breakeven, rerender);
      wireInto(root, 'ca-signatory', p.signatory, rerender);

      root.querySelectorAll('[data-rst]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.rst, k = e.target.dataset.k;
        p.restructure.options[i][k] = (k === 'label') ? e.target.value : (e.target.value === '' ? null : Number(e.target.value));
        rerender();
      }));
      root.querySelectorAll('[data-del-rst]').forEach(el => el.addEventListener('click', e => {
        p.restructure.options.splice(+e.target.dataset.delRst, 1); rerender();
      }));
      const addRst = root.querySelector('#ca-add-rst');
      if (addRst) addRst.addEventListener('click', () => { p.restructure.options.push({ label: 'Option', moratorium_years: 0 }); rerender(); });

      root.querySelectorAll('[data-fnd]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.fnd, k = e.target.dataset.k;
        p.funding.scenarios[i][k] = (k === 'label') ? e.target.value : Number(e.target.value);
        rerender();
      }));
      root.querySelectorAll('[data-del-fnd]').forEach(el => el.addEventListener('click', e => {
        p.funding.scenarios.splice(+e.target.dataset.delFnd, 1); rerender();
      }));
      const addFnd = root.querySelector('#ca-add-fnd');
      if (addFnd) addFnd.addEventListener('click', () => { p.funding.scenarios.push({ label: 'New scenario', debt_pct: 50 }); rerender(); });
    },
    tabulate: res => {
      const c = res.capacity, rs = res.restructuring, fu = res.funding, be = res.breakeven;
      const tables = [];
      const capRows = [
        ['EBITDA / cash accruals base', c.available_for_debt_service],
        ['Target DSCR', c.target_dscr],
        ['DSCR-based capacity', c.dscr_based_capacity],
        ['Max Debt/EBITDA leverage', c.max_leverage_multiple],
        ['Leverage-based capacity', c.leverage_based_capacity],
        ['Recommended max new debt', c.recommended_max_new_debt],
        ['Binding constraint', c.binding_constraint],
      ];
      if (c.headroom != null) capRows.push(['Headroom over proposed new debt', c.headroom]);
      tables.push({ title: 'Debt capacity', headers: ['Particulars', 'Value'], rows: capRows });
      tables.push({
        title: 'Debt capacity sensitivity', headers: ['Scenario', 'DSCR-based', 'Leverage-based', 'Recommended'],
        rows: (c.sensitivity || []).map(s => [s.scenario, s.dscr_based_capacity, s.leverage_based_capacity, s.recommended_max_new_debt])
      });

      const rsRows = [[rs.current.label, rs.current.rate_pct, rs.current.tenure_years, rs.current.annual_payment,
        rs.current.total_interest, rs.current.pv_of_payments, 0, 0]]
        .concat((rs.options || []).map(o => [o.label, o.rate_pct, o.tenure_years, o.annual_payment, o.total_interest,
          o.pv_of_payments, o.interest_saving_vs_current, o.pv_saving_vs_current]));
      tables.push({
        title: 'Loan restructuring',
        headers: ['Option', 'Rate %', 'Tenure', 'Annual payment', 'Total interest', 'PV of payments', 'Interest saving', 'PV saving'],
        rows: rsRows
      });

      tables.push({
        title: 'Debt vs equity',
        headers: ['Scenario', 'Debt %', 'Equity %', 'Debt amount', 'Equity amount', 'Annual interest', 'Annual tax shield', 'WACC %'],
        rows: (fu.scenarios || []).map(s => [s.label, s.debt_pct, s.equity_pct, s.debt_amount, s.equity_amount,
          s.annual_interest, s.annual_tax_shield, s.wacc_pct])
      });

      if (be.mode === 'revenue') {
        tables.push({
          title: 'Break-even', headers: ['Particulars', 'Value'],
          rows: [
            ['Contribution margin ratio %', be.contribution_margin_ratio_pct],
            ['Break-even revenue', be.bep_revenue],
            ['Current revenue', be.current_revenue],
            ['Margin of safety', be.margin_of_safety],
            ['Margin of safety %', be.margin_of_safety_pct],
          ]
        });
        tables.push({
          title: 'Break-even sensitivity', headers: ['Scenario', 'Break-even revenue'],
          rows: (be.sensitivity || []).map(s => [s.scenario, s.bep_revenue])
        });
      } else {
        tables.push({
          title: 'Break-even', headers: ['Particulars', 'Value'],
          rows: [
            ['Contribution margin / unit', be.contribution_margin_per_unit],
            ['Contribution margin ratio %', be.contribution_margin_ratio_pct],
            ['Break-even units', be.bep_units],
            ['Break-even revenue', be.bep_revenue],
            ['Current volume', be.current_volume],
            ['Margin of safety %', be.margin_of_safety_pct],
            ['Operating leverage (x)', be.operating_leverage],
          ]
        });
        tables.push({
          title: 'Break-even sensitivity', headers: ['Scenario', 'Break-even units', 'Break-even revenue'],
          rows: (be.sensitivity || []).map(s => [s.scenario, s.bep_units, s.bep_revenue])
        });
      }
      return tables;
    }
  };

  const FDI_SECTOR_FALLBACK = [
    'Manufacturing — general', 'Manufacturing — agro / food processing', 'Manufacturing — cement / clinker',
    'Manufacturing — pharmaceuticals', 'Hydropower and renewable energy', 'Information technology / software / BPO',
    'Tourism — star hotel / resort', 'Health — hospital / diagnostics', 'Education', 'Construction and infrastructure',
    'Trading — wholesale / import-export', 'Banking and financial institutions', 'Insurance', 'Telecommunications / ISP',
    'Airlines — domestic', 'Airlines — international', 'Mining and quarrying', 'Logistics and warehousing',
    'Agriculture — commercial / processing (non-primary)', 'Consultancy and professional services', 'Other / to be specified'
  ];
  const FDI_NEG_LIST_UI = [
    'Primary agriculture — poultry, fisheries, bee-keeping, fruits, vegetables, oilseed, pulses, dairy and other primary agro-production',
    'Cottage and small industries (as classified under the industrial law)',
    'Personal service business — hair cutting, beauty parlour, tailoring, driving training and the like',
    'Arms and ammunition, gunpowder and explosives, and industries related to radioactive material',
    'Real estate business (construction industry is not covered by this restriction)',
    'Motion pictures produced in the national language or in a language of the nation / local level',
    'Security printing', 'Bank notes and coin business',
    'Retail business — except a retail chain already operating in two or more countries',
    'Internal courier service', 'Atomic energy',
    'Travel agency, trekking agency, tourist guiding, rural tourism including home stay',
    'Business of mass communication media — newspaper, radio, television and online news',
    'Management, accountancy, engineering and legal consultancy services beyond the permitted foreign equity ceiling',
    'Language training, music and computer training', 'Consultancy services beyond 51% foreign equity'
  ];
  const FDI_DD_ITEM_LIST = [
    ['Investor identity', 'Certificate of incorporation / passport verified against the original or an authenticated copy'],
    ['Investor identity', 'Ownership chain traced to the ultimate beneficial owner, with percentages'],
    ['Investor identity', 'Directors and authorised signatory identified, bio-data obtained'],
    ['Sanctions and integrity', 'Screening of investor, UBOs and directors against sanctions and watch lists'],
    ['Sanctions and integrity', 'Adverse media search on investor and group'],
    ['Sanctions and integrity', 'Country risk: is the source jurisdiction subject to enhanced scrutiny'],
    ['Source of funds', 'Audited financial statements for the last three years obtained'],
    ['Source of funds', 'Bank solvency or credit standing letter obtained'],
    ['Source of funds', 'Written declaration of source of funds and source of wealth'],
    ['Source of funds', 'Funds routed only through the banking channel, no third-party remitter'],
    ['Legal capacity', 'Board resolution authorising the investment and the signatory'],
    ['Legal capacity', 'Power of attorney executed, notarised and authenticated or apostilled'],
    ['Target / project', 'Sector confirmed outside the negative list; foreign equity ceiling confirmed'],
    ['Target / project', 'Land, lease and location rights verified; ownership documents seen'],
    ['Target / project', 'Environmental approval requirement assessed (brief / IEE / EIA)'],
    ['Target / project', 'Sector licence requirement mapped and feasibility of obtaining it confirmed'],
    ['Target — if acquiring', 'Audited financial statements for five years reviewed and normalised'],
    ['Target — if acquiring', 'Tax position reviewed: returns filed, assessments open, TDS and VAT exposure'],
    ['Target — if acquiring', 'Contingent liabilities, guarantees, litigation and related-party dealings listed'],
    ['Target — if acquiring', 'Title to assets, encumbrances and bank charges verified'],
    ['Target — if acquiring', 'Employee liabilities, gratuity, SSF and labour compliance checked'],
    ['Commercial', 'Business plan and financial projections stress-tested'],
    ['Commercial', 'Market and demand evidence obtained, not assumed'],
    ['Commercial', 'Valuation basis agreed and supportable to DoI and NRB'],
    ['Exit', 'Exit route agreed in writing: put or call option, drag and tag, IPO, buy-back'],
    ['Exit', 'Repatriation path modelled with tax and documentary requirements']
  ];
  const FDI_DD_ITEMS = FDI_DD_ITEM_LIST.length;
  const FDI_DOI_STEPS = [
    ['Pre-screening and structuring', 'Confirm the sector is outside the negative list, check the foreign equity ceiling, fix the investment mode and the holding structure, and size the investment against the minimum threshold and the route thresholds.', 'Advisor', '3-7 days'],
    ['Investor due diligence and KYC', 'Collect and verify investor identity, ownership chain up to the ultimate beneficial owner, financial standing and source of funds. Screen against sanctions and adverse media.', 'Advisor', '5-10 days'],
    ['Company name reservation at OCR', 'Reserve the proposed company name with the Office of the Company Registrar (only where a new company is being formed).', 'Advisor', '1-3 days'],
    ['Prepare the FDI application package', 'Draft the application, project report, JV agreement, power of attorney, board resolutions and the investor profile.', 'Advisor', '5-10 days'],
    ['File with DoI or IBN', 'Submit through the Department of Industry (or the Investment Board where the size or the project type requires it).', 'Advisor', '1 day'],
    ['Approval and conditions', 'Respond to queries, attend the approval committee if called, and obtain the foreign investment approval letter.', 'DoI / IBN', '7-35 days'],
    ['Company incorporation or share transfer at OCR', 'Incorporate the company with the approved capital structure, or record the share transfer / capital increase.', 'Advisor', '3-7 days'],
    ['Industry registration at DoI', 'Register the industry and obtain the industry registration certificate.', 'Advisor', '3-7 days'],
    ['PAN / VAT registration at IRD', 'Obtain the permanent account number and register for VAT where required.', 'Advisor', '1-3 days'],
    ['NRB approval and bank account', 'Open the account, obtain Nepal Rastra Bank approval for the inflow and ensure the remittance narrative matches the approval.', 'Advisor / Bank', '7-21 days'],
    ['Remit capital through banking channel', 'Bring the funds in through the banking channel in convertible foreign currency.', 'Investor', '1-5 days'],
    ['Allot shares and record with OCR', 'Allot the shares, issue share certificates, update the shareholder register and file the return of allotment.', 'Advisor', '3-7 days'],
    ['Record the investment with NRB', 'Obtain the foreign investment recording from NRB — without this, repatriation will be obstructed.', 'Advisor', '7-21 days'],
    ['Sector licences, visa and local registration', 'Obtain any sector licence, register at the local level, complete labour and social security registration, and obtain visas.', 'Advisor', '7-30 days'],
    ['Commence operations and start the compliance clock', 'Move to the post-investment reporting calendar: tax, VAT, TDS, labour, OCR, DoI and NRB reporting.', 'Client', 'Ongoing']
  ];
  MODULES.fdi = {
    code: 'FDI', name: 'FDI structuring', group: 'Advisory',
    blurb: 'Foreign direct investment structuring — investors, means of finance, the project financial model, route and eligibility against the negative list and sector cap, tax and repatriation, exit and valuation, a full validation pass, foreign-loan compliance, an FX-movement analysis and a post-approval compliance calendar.',
    needsFdiSectors: true,
    extraFormats: [{ fmt: 'docx_jva', label: 'JVA' }, { fmt: 'docx_poa', label: 'POA' },
                   { fmt: 'docx_tta', label: 'Tech transfer' }, { fmt: 'docx_profiles', label: 'Investor profiles' },
                   { fmt: 'docx_advisory', label: 'Advisory report' }],
    bulkImport: {
      apply(file, sheets) {
        file.payload.investors = (file.payload.investors || []).concat((sheets.investors.rows || []).map(r => ({
          kind: (r.kind || 'FC').toUpperCase(), name: r.name || '', country: r.country || '',
          amount: r.amount || 0, currency: r.currency || 'USD', source_of_funds: r.source_of_funds || ''
        })));
      }
    },
    seed: () => ({
      client: { name: '', status: 'Proposed — to be incorporated', sector: 'Manufacturing — general', pan: '' },
      fx: { base: 'USD', rate: 135, rates: { USD: 135, EUR: 146, INR: 1.6 } },
      investors: [],
      capital: { authorised: 0, issued: 0, face_value: 100, premium: 0 },
      cost: [['Land and land development', 0], ['Building and civil works', 0], ['Plant and machinery', 0],
             ['Utilities and installation', 0], ['Vehicles', 0], ['Office equipment and furniture', 0],
             ['Preliminary and pre-operative expenses', 0], ['Contingency', 0], ['Margin money for working capital', 0]],
      loan: { f_amt: 0, f_rate: 9, f_tenure: 7, f_morat: 1, f_cur: 'USD', l_amt: 0, l_rate: 11, l_tenure: 7, l_morat: 1 },
      proj: { years: 5, rev_y1: 0, growth: 10, util: [60, 75, 85, 95, 100],
              cogs_pct: 55, admin_pct: 8, sell_pct: 5, other_pct: 2, salary_pct: 0,
              dep_life: 15, dep_bldg: 20, preop_amort: 5, tax_rate: 20, tax_holiday: 0, concession_pct: 0,
              ar_days: 45, inv_days: 60, ap_days: 30, payout: 40, royalty_pct: 0, tech_fee_pct: 0, mgmt_fee_pct: 0 },
      params: { min_fdi: 20000000, min_fdi_exempt_it: true, auto_cap: 500000000, ibn_threshold: 6000000000,
                dividend_wht: 5, interest_wht: 15, royalty_wht: 15, service_wht: 15,
                cgt_non_resident: 25, cgt_res_entity: 15, cgt_res_indiv: 10, vat: 13,
                de_ratio: 70, royalty_domestic_sales_cap: 5, royalty_domestic_profit_cap: 15,
                royalty_export_sales_cap: 10, royalty_export_profit_cap: 20, foreign_loan_rate_ceiling: '', last_verified: '' },
      route: { neg_checks: {}, cap_override: '', authority: 'auto', auto_eligible: 'auto' },
      fx_exit: {}, loan_compliance: {}, compliance: {},
      val: { wacc: 16, tg: 4, pe: 12, ev_ebitda: 8, net_debt: 0, net_assets: 0, surplus: 0,
             exit_year: 5, exit_multiple: 8, exit_cost_base: 0, nav_weight: 30, dcf_weight: 40, pe_weight: 15, ebitda_weight: 15 },
      dd: {}, tt: { has: false, licensor: '', type: 'Technical know-how and trade mark licence', term: 10,
                    royalty_base: 'Net sales', royalty_pct: 3, lump_sum: 0, territory: 'Nepal',
                    exclusive: true, sublicence: false, notes: '' },
      jva: { place: 'Kathmandu', law_state: 'Nepal', arb: 'Nepal Council of Arbitration (NEPCA), Kathmandu, in English',
             board_size: 5, quorum: 3, reserved: true, rofr: true, tag: true, drag: true, drag_pct: 75,
             lock_in: 3, non_compete: 3, put_option: true, call_option: true, ipo_exit: true, exit_year: 7,
             deadlock: 'Escalation to chairpersons, then a swing-vote expert, then a buy-sell mechanism',
             effective_date: '', business_desc: '' },
      poa: { grantor_type: 'auto', attorney_name: '', attorney_desig: '', attorney_citizenship: '', attorney_addr: '',
             scope: { doi: true, ocr: true, nrb: true, ird: true, bank: true, land: false, litigation: false, shares: true, visa: true },
             place: 'Kathmandu, Nepal', validity: 2, sub_delegate: false, notary_country: '', apostille: true },
      doi: {},
      roadmap: [
        { t: 'Understanding FITTA and types of investment structures', s: 'Not scheduled', d: '', o: '', n: '' },
        { t: 'Automatic vs. approval routes and minimum investment thresholds', s: 'Not scheduled', d: '', o: '', n: '' },
        { t: 'Step-by-step Department of Industry (DoI) procedures', s: 'Not scheduled', d: '', o: '', n: '' },
        { t: 'Due diligence requirements and setting up a roadmap', s: 'Not scheduled', d: '', o: '', n: '' },
        { t: 'NRB approval, currency regulations, and foreign loan vs. equity', s: 'Not scheduled', d: '', o: '', n: '' },
        { t: 'Tax efficiency and repatriation of profits, capital and dividends', s: 'Not scheduled', d: '', o: '', n: '' },
        { t: 'Joint ventures, licensing models and technology transfer agreements', s: 'Not scheduled', d: '', o: '', n: '' },
        { t: 'Post-investment reporting and share valuation for entry / exit', s: 'Not scheduled', d: '', o: '', n: '' },
        { t: 'Handling repatriation challenges and regulatory scrutiny in high-value deals', s: 'Not scheduled', d: '', o: '', n: '' }
      ]
    }),
    view(file, res) {
      const p = file.payload;
      p.route = p.route || {}; p.route.neg_checks = p.route.neg_checks || {};
      p.fx_exit = p.fx_exit || {}; p.loan_compliance = p.loan_compliance || {}; p.compliance = p.compliance || {};
      const sectors = ((state.boot && state.boot.fdi_sectors) || []).map(s => s.name);
      const sectorOpts = (sectors.length ? sectors : FDI_SECTOR_FALLBACK).map(n => ({ value: n, label: n }));

      let h = '<div class="card" id="fdi-client"><h3>Client and sector</h3><div class="grid c3">' +
        field('Project company', 'name', p.client, 'text') +
        field('Status', 'status', p.client, 'select', { options: [
          { value: 'Proposed — to be incorporated', label: 'Proposed — to be incorporated' },
          { value: 'Existing company', label: 'Existing company' }] }) +
        field('Sector', 'sector', p.client, 'select', { options: sectorOpts }) +
        field('PAN', 'pan', p.client, 'text') +
        '</div></div>';

      h += '<div class="card" id="fdi-fx"><h3>Currency</h3><div class="grid c2">' +
        field('Base foreign currency', 'base', p.fx, 'select', { options: Object.keys(p.fx.rates).map(c => ({ value: c, label: c })) }) +
        field('Headline rate (NPR per unit)', 'rate', p.fx, 'number') +
        '</div></div>' +
        '<div class="card" id="fdi-fx-exit"><h3>Expected exit rate</h3><p class="card-note">NPR per unit at the assumed exit date — leave blank to assume no movement from the rate above. Isolates the FX-movement component of a foreign investor\'s return.</p><div class="grid c4">' +
        Object.keys(p.fx.rates).map(c => field(c + ' at exit', c, p.fx_exit, 'number', { placeholder: String(p.fx.rates[c]) })).join('') +
        '</div></div>';

      h += '<div class="card"><h3>Investors</h3><div class="table-wrap"><table><thead><tr>' +
        '<th>Kind</th><th>Name</th><th>Country</th><th class="num">Amount</th><th>Currency</th><th>Source of funds</th><th></th></tr></thead><tbody>';
      (p.investors || []).forEach((inv, i) => {
        h += '<tr>' +
          '<td><select data-inv="' + i + '" data-k="kind">' + ['FC', 'FI', 'NC', 'NI'].map(k =>
            '<option value="' + k + '"' + (inv.kind === k ? ' selected' : '') + '>' + k + '</option>').join('') + '</select></td>' +
          '<td><input data-inv="' + i + '" data-k="name" value="' + esc(inv.name || '') + '"></td>' +
          '<td><input data-inv="' + i + '" data-k="country" value="' + esc(inv.country || '') + '"></td>' +
          '<td class="num"><input type="number" data-inv="' + i + '" data-k="amount" value="' + (inv.amount || 0) + '"></td>' +
          '<td><select data-inv="' + i + '" data-k="currency">' + currencyOptions(inv.currency || 'NPR') + '</select></td>' +
          '<td><input data-inv="' + i + '" data-k="source_of_funds" value="' + esc(inv.source_of_funds || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-inv="' + i + '" title="Remove this row">Remove</button></td></tr>';
      });
      h += '</tbody></table></div><button class="btn btn-small" id="add-inv">Add an investor</button></div>';

      h += '<div class="card" id="fdi-capital"><h3>Capital</h3><div class="grid c4">' +
        field('Authorised capital', 'authorised', p.capital, 'number') +
        field('Issued capital', 'issued', p.capital, 'number') +
        field('Face value per share', 'face_value', p.capital, 'number') +
        field('Share premium', 'premium', p.capital, 'number') +
        '</div></div>';

      h += '<div class="card"><h3>Cost of project</h3><div class="table-wrap"><table><thead><tr><th>Head</th><th class="num">Amount</th></tr></thead><tbody>';
      (p.cost || []).forEach((c, i) => {
        h += '<tr><td>' + esc(c[0]) + '</td><td class="num"><input type="number" data-cost="' + i + '" value="' + (c[1] || 0) + '"></td></tr>';
      });
      h += '</tbody></table></div></div>';

      h += '<div class="card" id="fdi-loan"><h3>Loan and finance</h3><div class="grid c4">' +
        field('Foreign loan amount', 'f_amt', p.loan, 'number') +
        field('Foreign loan currency', 'f_cur', p.loan, 'select', { options: Object.keys(p.fx.rates).map(c => ({ value: c, label: c })) }) +
        field('Foreign loan rate %', 'f_rate', p.loan, 'number') +
        field('Foreign loan tenure (years)', 'f_tenure', p.loan, 'number') +
        field('Foreign loan moratorium (years)', 'f_morat', p.loan, 'number') +
        field('Local loan amount (NPR)', 'l_amt', p.loan, 'number') +
        field('Local loan rate %', 'l_rate', p.loan, 'number') +
        field('Local loan tenure (years)', 'l_tenure', p.loan, 'number') +
        field('Local loan moratorium (years)', 'l_morat', p.loan, 'number') +
        '</div></div>';

      if (res.loan_compliance && res.loan_compliance.has_loan) {
        const lcDone = res.loan_compliance.items.filter(i => i.done).length;
        h += '<div class="card"><h3>Foreign loan compliance — ' + lcDone + ' of ' + res.loan_compliance.items.length + ' point(s) confirmed</h3>' +
          res.loan_compliance.items.map((it, i) => '<label class="check" style="display:block;margin:4px 0"><input type="checkbox" data-loan-comp="' + i + '"' +
            (p.loan_compliance[i] ? ' checked' : '') + '> ' + esc(it.item) + '</label>').join('') +
          res.loan_compliance.flags.map(f => '<p class="msg ' + (f.t === 'bad' ? 'bad' : 'good') + '">' + esc(f.m) + '</p>').join('') +
          '</div>';
      }

      h += '<div class="card" id="fdi-proj"><h3>Projections</h3><div class="grid c4">' +
        field('Projection years', 'years', p.proj, 'select', { options: [2, 3, 4, 5, 6, 7, 8, 9, 10].map(n => ({ value: n, label: n + ' years' })) }) +
        field('Revenue at 100% (year 1)', 'rev_y1', p.proj, 'number') +
        field('Annual growth %', 'growth', p.proj, 'number') +
        field('COGS % of revenue', 'cogs_pct', p.proj, 'number') +
        field('Admin % of revenue', 'admin_pct', p.proj, 'number') +
        field('Selling % of revenue', 'sell_pct', p.proj, 'number') +
        field('Other opex % of revenue', 'other_pct', p.proj, 'number') +
        field('Royalty % of revenue', 'royalty_pct', p.proj, 'number') +
        field('Tech fee % of revenue', 'tech_fee_pct', p.proj, 'number') +
        field('Mgmt fee % of revenue', 'mgmt_fee_pct', p.proj, 'number') +
        field('Depreciation life — other assets (years)', 'dep_life', p.proj, 'number') +
        field('Depreciation life — building (years)', 'dep_bldg', p.proj, 'number') +
        field('Pre-operative amortisation (years)', 'preop_amort', p.proj, 'number') +
        field('Tax rate %', 'tax_rate', p.proj, 'number') +
        field('Tax holiday (years)', 'tax_holiday', p.proj, 'number') +
        field('Debtor days', 'ar_days', p.proj, 'number') +
        field('Inventory days', 'inv_days', p.proj, 'number') +
        field('Creditor days', 'ap_days', p.proj, 'number') +
        field('Dividend payout %', 'payout', p.proj, 'number') +
        '</div>';
      const n = Number(p.proj.years) || 5;
      const util = (p.proj.util && p.proj.util.length ? p.proj.util : [60, 75, 85, 95, 100]);
      h += '<h4 style="margin:12px 0 6px">Capacity utilisation by year (%)</h4><div class="grid" style="grid-template-columns:repeat(' + n + ',1fr)">' +
        Array.from({ length: n }, (_, i) => '<div class="field"><label>Year ' + (i + 1) + '</label><input type="number" data-util="' + i +
          '" value="' + (util[i] != null ? util[i] : 100) + '"></div>').join('') + '</div></div>';

      h += '<div class="card" id="fdi-params"><h3>Statutory parameters</h3><p class="card-note">Planning defaults. Verify against the current Act, Regulation and gazette notice, then set the verified date.</p><div class="grid c4">' +
        field('Minimum FDI per investor', 'min_fdi', p.params, 'number') +
        field('Automatic-route ceiling', 'auto_cap', p.params, 'number') +
        field('Investment Board threshold', 'ibn_threshold', p.params, 'number') +
        field('Dividend WHT %', 'dividend_wht', p.params, 'number') +
        field('Interest WHT %', 'interest_wht', p.params, 'number') +
        field('Royalty / fee WHT %', 'royalty_wht', p.params, 'number') +
        field('CGT — non-resident %', 'cgt_non_resident', p.params, 'number') +
        field('Gearing norm %', 'de_ratio', p.params, 'number') +
        field('Royalty ceiling — domestic sales %', 'royalty_domestic_sales_cap', p.params, 'number') +
        field('Foreign loan interest ceiling %', 'foreign_loan_rate_ceiling', p.params, 'number', { placeholder: 'Blank until confirmed against the current NRB position' }) +
        field('Last verified (date)', 'last_verified', p.params, 'text', { placeholder: 'YYYY-MM-DD' }) +
        '</div></div>';

      h += '<div class="card" id="fdi-route"><h3>Route and eligibility</h3><div class="grid c3">' +
        field('Authority', 'authority', p.route, 'select', { options: [
          { value: 'auto', label: 'Auto — decide from investment size' },
          { value: 'Department of Industry (DoI)', label: 'Department of Industry (DoI)' },
          { value: 'Investment Board Nepal (IBN)', label: 'Investment Board Nepal (IBN)' }] }) +
        field('Automatic-route eligible', 'auto_eligible', p.route, 'select', { options: [
          { value: 'auto', label: 'Auto — decide from the rules' }, { value: 'yes', label: 'Yes — override' }, { value: 'no', label: 'No — override' }] }) +
        field('Sector cap override %', 'cap_override', p.route, 'number') +
        '</div><h4 style="margin:12px 0 6px">Negative list — tick anything the activity touches</h4>' +
        (res.negative_list || FDI_NEG_LIST_UI).map((label, i) =>
          '<label class="check" style="display:block;margin:4px 0"><input type="checkbox" data-neg="' + i + '"' +
          (p.route.neg_checks[i] ? ' checked' : '') + '> ' + esc(label) + '</label>').join('') +
        '</div>';

      h += '<div class="card" id="fdi-jva"><h3>Joint venture terms</h3><p class="card-note">Drives the JVA export.</p><div class="grid c4">' +
        field('Place of execution', 'place', p.jva, 'text') +
        field('Governing law', 'law_state', p.jva, 'text') +
        field('Board size', 'board_size', p.jva, 'number') +
        field('Quorum', 'quorum', p.jva, 'number') +
        field('Lock-in (years)', 'lock_in', p.jva, 'number') +
        field('Non-compete (years)', 'non_compete', p.jva, 'number') +
        field('Drag-along threshold %', 'drag_pct', p.jva, 'number') +
        field('Exit year', 'exit_year', p.jva, 'number') +
        field('Effective date', 'effective_date', p.jva, 'text', { placeholder: 'YYYY-MM-DD' }) +
        field('Business description', 'business_desc', p.jva, 'text') +
        '</div><div class="grid c4" style="margin-top:8px">' +
        ['reserved', 'rofr', 'tag', 'drag', 'put_option', 'call_option', 'ipo_exit'].map(k =>
          '<label class="check"><input type="checkbox" data-flag-jva="' + k + '"' + (p.jva[k] ? ' checked' : '') + '> ' +
          k.replace(/_/g, ' ') + '</label>').join('') + '</div></div>';

      h += '<div class="card" id="fdi-poa"><h3>Power of attorney</h3><p class="card-note">Drives the POA export.</p><div class="grid c4">' +
        field('Attorney name', 'attorney_name', p.poa, 'text') +
        field('Attorney designation', 'attorney_desig', p.poa, 'text') +
        field('Attorney address', 'attorney_addr', p.poa, 'text') +
        field('Validity (years)', 'validity', p.poa, 'number') +
        field('Place', 'place', p.poa, 'text') +
        field('Notary country', 'notary_country', p.poa, 'text') +
        '</div><h4 style="margin:12px 0 6px">Scope of powers</h4><div class="grid c4">' +
        Object.keys(p.poa.scope).map(k => '<label class="check"><input type="checkbox" data-poa-scope="' + k + '"' +
          (p.poa.scope[k] ? ' checked' : '') + '> ' + k.toUpperCase() + '</label>').join('') + '</div></div>';

      h += '<div class="card" id="fdi-tt"><h3>Technology transfer</h3><label class="check"><input type="checkbox" data-flag-tt="has"' +
        (p.tt.has ? ' checked' : '') + '> This deal includes a technology transfer / licence agreement</label>';
      if (p.tt.has) {
        h += '<div class="grid c4" style="margin-top:10px">' +
          field('Licensor', 'licensor', p.tt, 'text') +
          field('Term (years)', 'term', p.tt, 'number') +
          field('Royalty base', 'royalty_base', p.tt, 'select', { options: [{ value: 'Net sales', label: 'Net sales' }, { value: 'Gross profit', label: 'Gross profit' }] }) +
          field('Royalty %', 'royalty_pct', p.tt, 'number') +
          field('Lump sum fee', 'lump_sum', p.tt, 'number') +
          field('Territory', 'territory', p.tt, 'text') +
          '</div>';
      }
      h += '</div>';

      const doiDone = FDI_DOI_STEPS.filter((_, i) => (p.doi[i] || {}).done).length;
      h += '<div class="card"><h3>Department of Industry procedure — ' + doiDone + ' of ' + FDI_DOI_STEPS.length + ' steps done</h3>' +
        '<div class="table-wrap"><table><thead><tr><th></th><th>Step</th><th>Authority</th><th>Typical timing</th><th>Date</th><th>Reference</th></tr></thead><tbody>';
      FDI_DOI_STEPS.forEach((step, i) => {
        const row = p.doi[i] || {};
        h += '<tr><td><input type="checkbox" data-doi-done="' + i + '"' + (row.done ? ' checked' : '') + '></td>' +
          '<td><b>' + esc(step[0]) + '</b><br><small>' + esc(step[1]) + '</small></td>' +
          '<td>' + esc(step[2]) + '</td><td>' + esc(step[3]) + '</td>' +
          '<td><input data-doi-date="' + i + '" value="' + esc(row.date || '') + '" placeholder="BS date" style="width:100px"></td>' +
          '<td><input data-doi-ref="' + i + '" value="' + esc(row.ref || '') + '" style="width:110px"></td></tr>';
      });
      h += '</tbody></table></div></div>';

      const ddDone = Object.keys(p.dd).filter(k => p.dd[k]).length;
      h += '<div class="card"><h3>Due diligence checklist — ' + ddDone + ' of ' + FDI_DD_ITEMS + ' points done</h3>';
      let lastCat = '';
      FDI_DD_ITEM_LIST.forEach((item, i) => {
        if (item[0] !== lastCat) { h += '<h4 style="margin:12px 0 4px">' + esc(item[0]) + '</h4>'; lastCat = item[0]; }
        h += '<label class="check" style="display:block;margin:3px 0"><input type="checkbox" data-dd-item="' + i + '"' +
          (p.dd[i] ? ' checked' : '') + '> ' + esc(item[1]) + '</label>';
      });
      h += '</div>';

      h += '<div class="card"><h3>Advisory roadmap</h3><div class="table-wrap"><table><thead><tr>' +
        '<th>Topic</th><th>Status</th><th>Date</th><th>Owner</th><th>Note</th></tr></thead><tbody>';
      (p.roadmap || []).forEach((item, i) => {
        h += '<tr><td>' + esc(item.t) + '</td>' +
          '<td><select data-rm-s="' + i + '">' + ['Not scheduled', 'Scheduled', 'In progress', 'Completed'].map(s =>
            '<option value="' + s + '"' + (item.s === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>' +
          '<td><input data-rm-d="' + i + '" value="' + esc(item.d || '') + '" style="width:100px"></td>' +
          '<td><input data-rm-o="' + i + '" value="' + esc(item.o || '') + '" style="width:100px"></td>' +
          '<td><input data-rm-n="' + i + '" value="' + esc(item.n || '') + '"></td></tr>';
      });
      h += '</tbody></table></div></div>';

      const compDone = res.compliance_calendar.filter((_, i) => (p.compliance[i] || {}).done).length;
      h += '<div class="card"><h3>Post-approval compliance calendar — ' + compDone + ' of ' + res.compliance_calendar.length + ' current</h3>' +
        '<p class="card-note">Recurring obligations after the FDI approval itself — separate from the one-off DoI procedure above.</p>' +
        '<div class="table-wrap"><table><thead><tr><th></th><th>Obligation</th><th>Frequency</th><th>Due / last done</th><th>Note</th></tr></thead><tbody>';
      res.compliance_calendar.forEach((item, i) => {
        const row = p.compliance[i] || {};
        h += '<tr><td><input type="checkbox" data-comp-done="' + i + '"' + (row.done ? ' checked' : '') + '></td>' +
          '<td>' + esc(item[0]) + '</td><td>' + esc(item[1]) + '</td>' +
          '<td><input data-comp-date="' + i + '" value="' + esc(row.date || '') + '" style="width:100px"></td>' +
          '<td><input data-comp-note="' + i + '" value="' + esc(row.note || '') + '"></td></tr>';
      });
      h += '</tbody></table></div></div>';

      h += '<div class="card" id="fdi-val"><h3>Valuation assumptions</h3><div class="grid c4">' +
        field('WACC %', 'wacc', p.val, 'number') +
        field('Terminal growth %', 'tg', p.val, 'number') +
        field('P/E multiple', 'pe', p.val, 'number') +
        field('EV/EBITDA multiple', 'ev_ebitda', p.val, 'number') +
        field('Exit year', 'exit_year', p.val, 'number') +
        field('Exit EV/EBITDA multiple', 'exit_multiple', p.val, 'number') +
        field('NAV weight %', 'nav_weight', p.val, 'number') +
        field('DCF weight %', 'dcf_weight', p.val, 'number') +
        field('P/E weight %', 'pe_weight', p.val, 'number') +
        field('EV/EBITDA weight %', 'ebitda_weight', p.val, 'number') +
        '</div></div>';

      const rt = res.route, vcls = rt.flags.some(f => f.t === 'bad') ? 'alert' : 'accent';
      h += '<div class="card"><h3>Route and eligibility — result</h3><div class="grid c4">' +
        kpi('Route', rt.route, vcls) + kpi('Authority', rt.authority) +
        kpi('Foreign holding', pct(rt.foreign_pct / 100)) + kpi('Sector cap', pct(rt.cap / 100, 0)) + '</div>' +
        rt.flags.map(f => '<p class="msg ' + (f.t === 'bad' ? 'bad' : 'good') + '">' + esc(f.m) + '</p>').join('') + '</div>';

      const m = res.model;
      h += '<div class="card"><h3>Appraisal</h3><div class="grid c4">' +
        kpi('Total project cost', money(res.finance.cost)) +
        kpi('NPV', money(m.npv), m.npv >= 0 ? 'accent' : 'alert') +
        kpi('Project IRR', m.irr === null ? '—' : pct(m.irr / 100)) +
        kpi('Min · avg DSCR', (m.min_dscr === null ? '—' : m.min_dscr.toFixed(2)) + ' · ' + (m.avg_dscr === null ? '—' : m.avg_dscr.toFixed(2))) +
        '</div></div>';

      const years = m.Y.map(y => 'Year ' + y.y);
      h += resultCard('Financial model', ['Particulars', ...years], [
        ['Revenue', ...m.Y.map(y => y.rev)], ['EBITDA', ...m.Y.map(y => y.ebitda)],
        ['EBIT', ...m.Y.map(y => y.ebit)], ['PBT', ...m.Y.map(y => y.pbt)], ['PAT', ...m.Y.map(y => y.pat)],
        ['DSCR', ...m.Y.map(y => y.dscr === null ? '—' : y.dscr)],
        ['Tie check', ...m.Y.map(y => y.diff)]]);

      h += resultCard('Tax and repatriation', ['Year', 'Corporate tax', 'Net dividend', 'Net interest', 'Net fees', 'Total tax'],
        res.tax_pack.rows.map(r => [r.y, r.corp_tax, r.net_div, r.net_int, r.net_fee, r.total_tax]));

      if (res.exit) {
        h += resultCard('Exit', ['Particulars', 'Value'], [
          ['Exit year', res.exit.ey], ['Equity value', res.exit.eqv], ['Foreign proceeds', res.exit.foreign_proceeds],
          ['Capital gains tax', res.exit.cgt], ['Net proceeds', res.exit.net], ['MOIC', res.exit.moic]]);
      }
      if (res.fx_analysis && res.fx_analysis.rows.length) {
        h += '<div class="card"><h3>Foreign currency analysis — return in the investor\'s own currency</h3>' +
          '<p class="card-note">Isolates the exchange-rate movement from the business return by re-expressing exit proceeds at the exit rate entered above, instead of the entry rate the rest of the model uses.</p>' +
          '<div class="table-wrap"><table><thead><tr><th>Investor</th><th>Currency</th><th class="num">Entry rate</th>' +
          '<th class="num">Exit rate</th><th class="num">FX move %</th><th class="num">Invested</th><th class="num">Proceeds</th><th class="num">MOIC</th></tr></thead><tbody>' +
          res.fx_analysis.rows.map(r => '<tr><td>' + esc(r.investor) + '</td><td>' + esc(r.currency) + '</td>' +
            '<td class="num">' + r.entry_rate + '</td><td class="num">' + r.exit_rate + '</td>' +
            '<td class="num">' + r.fx_move_pct + '%</td><td class="num">' + r.invested_fc.toLocaleString() + '</td>' +
            '<td class="num">' + r.proceeds_fc.toLocaleString() + '</td><td class="num">' + r.moic_fc + 'x</td></tr>').join('') +
          '</tbody></table></div></div>';
      }
      h += resultCard('Valuation', ['Method', 'Equity value'], [
        ['Net asset value', res.valuation.nav], ['DCF', res.valuation.dcf_eq],
        ['P/E multiple', res.valuation.pe_eq], ['EV/EBITDA multiple', res.valuation.ev_eq],
        ['Weighted', res.valuation.weighted]]);

      const errs = res.checks.filter(c => c.sev === 'err'), warns = res.checks.filter(c => c.sev === 'warn');
      h += '<div class="card"><h3>Validation — ' + errs.length + ' error(s), ' + warns.length + ' warning(s)</h3>' +
        errs.concat(warns).map(c => '<p class="msg ' + (c.sev === 'err' ? 'bad' : 'good') + '"><b>' + esc(c.area) + '.</b> ' + esc(c.msg) + '</p>').join('') +
        (errs.length + warns.length === 0 ? '<p class="msg good">All checks passed.</p>' : '') + '</div>';
      return h;
    },
    wire(root, file, rerender) {
      const p = file.payload;
      wireInto(root, 'fdi-client', p.client, rerender);
      wireInto(root, 'fdi-fx', p.fx, rerender);
      wireInto(root, 'fdi-fx-exit', p.fx_exit, rerender);
      wireInto(root, 'fdi-capital', p.capital, rerender);
      wireInto(root, 'fdi-loan', p.loan, rerender);
      wireInto(root, 'fdi-proj', p.proj, rerender);
      wireInto(root, 'fdi-params', p.params, rerender);
      wireInto(root, 'fdi-route', p.route, rerender);
      wireInto(root, 'fdi-val', p.val, rerender);
      wireInto(root, 'fdi-jva', p.jva, rerender);
      wireInto(root, 'fdi-poa', p.poa, rerender);
      wireInto(root, 'fdi-tt', p.tt, rerender);
      root.querySelectorAll('[data-flag-jva]').forEach(el => el.addEventListener('change', e => {
        p.jva[e.target.dataset.flagJva] = e.target.checked; rerender();
      }));
      root.querySelectorAll('[data-poa-scope]').forEach(el => el.addEventListener('change', e => {
        p.poa.scope[e.target.dataset.poaScope] = e.target.checked; rerender();
      }));
      root.querySelectorAll('[data-flag-tt]').forEach(el => el.addEventListener('change', e => {
        p.tt[e.target.dataset.flagTt] = e.target.checked; rerender();
      }));
      root.querySelectorAll('[data-doi-done]').forEach(el => el.addEventListener('change', e => {
        const i = e.target.dataset.doiDone;
        p.doi[i] = p.doi[i] || {}; p.doi[i].done = e.target.checked; rerender();
      }));
      root.querySelectorAll('[data-doi-date]').forEach(el => el.addEventListener('change', e => {
        const i = e.target.dataset.doiDate;
        p.doi[i] = p.doi[i] || {}; p.doi[i].date = e.target.value; rerender();
      }));
      root.querySelectorAll('[data-doi-ref]').forEach(el => el.addEventListener('change', e => {
        const i = e.target.dataset.doiRef;
        p.doi[i] = p.doi[i] || {}; p.doi[i].ref = e.target.value; rerender();
      }));
      root.querySelectorAll('[data-dd-item]').forEach(el => el.addEventListener('change', e => {
        p.dd[e.target.dataset.ddItem] = e.target.checked; rerender();
      }));
      root.querySelectorAll('[data-loan-comp]').forEach(el => el.addEventListener('change', e => {
        p.loan_compliance[e.target.dataset.loanComp] = e.target.checked; rerender();
      }));
      root.querySelectorAll('[data-comp-done]').forEach(el => el.addEventListener('change', e => {
        const i = e.target.dataset.compDone;
        p.compliance[i] = p.compliance[i] || {}; p.compliance[i].done = e.target.checked; rerender();
      }));
      root.querySelectorAll('[data-comp-date]').forEach(el => el.addEventListener('change', e => {
        const i = e.target.dataset.compDate;
        p.compliance[i] = p.compliance[i] || {}; p.compliance[i].date = e.target.value; rerender();
      }));
      root.querySelectorAll('[data-comp-note]').forEach(el => el.addEventListener('change', e => {
        const i = e.target.dataset.compNote;
        p.compliance[i] = p.compliance[i] || {}; p.compliance[i].note = e.target.value; rerender();
      }));
      root.querySelectorAll('[data-rm-s]').forEach(el => el.addEventListener('change', e => {
        p.roadmap[+e.target.dataset.rmS].s = e.target.value; rerender();
      }));
      root.querySelectorAll('[data-rm-d]').forEach(el => el.addEventListener('change', e => {
        p.roadmap[+e.target.dataset.rmD].d = e.target.value; rerender();
      }));
      root.querySelectorAll('[data-rm-o]').forEach(el => el.addEventListener('change', e => {
        p.roadmap[+e.target.dataset.rmO].o = e.target.value; rerender();
      }));
      root.querySelectorAll('[data-rm-n]').forEach(el => el.addEventListener('change', e => {
        p.roadmap[+e.target.dataset.rmN].n = e.target.value; rerender();
      }));
      root.querySelectorAll('[data-inv]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.inv, k = e.target.dataset.k;
        p.investors[i][k] = (k === 'amount') ? Number(e.target.value) : e.target.value;
        rerender();
      }));
      root.querySelectorAll('[data-del-inv]').forEach(el => el.addEventListener('click', e => {
        p.investors.splice(+e.target.dataset.delInv, 1); rerender();
      }));
      const addInv = root.querySelector('#add-inv');
      if (addInv) addInv.addEventListener('click', () => {
        p.investors = p.investors || [];
        p.investors.push({ kind: 'FC', name: '', country: '', amount: 0, currency: 'USD', source_of_funds: '' });
        rerender();
      });
      root.querySelectorAll('[data-cost]').forEach(el => el.addEventListener('change', e => {
        p.cost[+e.target.dataset.cost][1] = Number(e.target.value); rerender();
      }));
      root.querySelectorAll('[data-util]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.util;
        const u = (p.proj.util && p.proj.util.length) ? p.proj.util.slice() : [];
        while (u.length <= i) u.push(100);
        u[i] = Number(e.target.value);
        p.proj.util = u;
        rerender();
      }));
      root.querySelectorAll('[data-neg]').forEach(el => el.addEventListener('change', e => {
        p.route.neg_checks[e.target.dataset.neg] = e.target.checked; rerender();
      }));
    },
    tabulate: res => {
      const rt = res.route, fin = res.finance, m = res.model, tp = res.tax_pack, ex = res.exit, val = res.valuation;
      const tables = [];
      const summaryRows = [
        ['Route', rt.route + ' (' + rt.authority + ')'],
        ['Sector', res.sector.name + ' — cap ' + rt.cap.toFixed(0) + '% — foreign holding ' + rt.foreign_pct.toFixed(2) + '%'],
      ];
      tables.push({ title: 'Summary', headers: ['Particulars', 'Value'], rows: summaryRows });
      if (rt.flags && rt.flags.length) {
        tables.push({ title: 'Route flags', headers: ['Severity', 'Message'], rows: rt.flags.map(f => [f.t.toUpperCase(), f.m]) });
      }
      tables.push({
        title: 'Means of finance', headers: ['Particulars', 'Amount'],
        rows: [
          ['Equity (all investors)', fin.equity], ['Foreign loan (NPR)', fin.f_loan], ['Local loan', fin.l_loan],
          ['Total sources', fin.sources], ['Total project cost', fin.cost], ['Gap (cost − sources)', fin.gap],
          ['Debt : equity', fin.de_pct.toFixed(1) + '% : ' + fin.eq_pct.toFixed(1) + '%'],
        ]
      });
      tables.push({
        title: 'Appraisal', headers: ['Particulars', 'Value'],
        rows: [
          ['NPV', m.npv], ['Project IRR %', m.irr], ['Equity IRR %', m.eirr],
          ['Minimum DSCR', m.min_dscr], ['Average DSCR', m.avg_dscr], ['Payback (years)', m.payback],
        ]
      });

      const years = m.Y.map(y => 'Year ' + y.y);
      const lineItems = [
        ['Revenue', m.Y.map(y => y.rev)], ['EBITDA', m.Y.map(y => y.ebitda)], ['Depreciation', m.Y.map(y => y.dep)],
        ['EBIT', m.Y.map(y => y.ebit)], ['Interest', m.Y.map(y => y.interest)], ['PBT', m.Y.map(y => y.pbt)],
        ['Tax', m.Y.map(y => y.tax)], ['PAT', m.Y.map(y => y.pat)], ['Dividend', m.Y.map(y => y.div)],
        ['Net block', m.Y.map(y => y.net_block)], ['Cash', m.Y.map(y => y.cash)], ['Overdraft', m.Y.map(y => y.od)],
        ['Assets', m.Y.map(y => y.assets)], ['Liabilities', m.Y.map(y => y.liab)], ['Tie check', m.Y.map(y => y.diff)],
        ['DSCR', m.Y.map(y => y.dscr)],
      ];
      tables.push({ title: 'Financial model', headers: ['Particulars', ...years], rows: lineItems.map(([label, vals]) => [label, ...vals]) });

      tables.push({
        title: 'Tax and repatriation',
        headers: ['Year', 'Corporate tax', 'Dividend (foreign)', 'Dividend WHT', 'Net dividend', 'Interest (foreign)',
          'Interest WHT', 'Net interest', 'Fees (foreign)', 'Fee WHT', 'Net fees', 'Total tax'],
        rows: tp.rows.map(x => [x.y, x.corp_tax, x.div_f, x.wht, x.net_div, x.int_f, x.int_wht, x.net_int, x.fees, x.fee_wht, x.net_fee, x.total_tax])
      });

      if (ex) {
        tables.push({
          title: 'Exit and valuation', headers: ['Exit particulars', 'Value'],
          rows: [
            ['Exit year', ex.ey], ['EBITDA at exit', ex.ebitda], ['Enterprise value', ex.ev], ['Net debt', ex.net_debt],
            ['Equity value', ex.eqv], ['Foreign proceeds', ex.foreign_proceeds], ['Capital gains tax', ex.cgt],
            ['Net proceeds to foreign investor', ex.net], ['MOIC', ex.moic],
          ]
        });
      }
      tables.push({
        title: ex ? 'Valuation methods' : 'Exit and valuation', headers: ['Valuation method', 'Equity value'],
        rows: [
          ['Net asset value', val.nav], ['DCF', val.dcf_eq], ['P/E multiple', val.pe_eq],
          ['EV/EBITDA multiple', val.ev_eq], ['Weighted', val.weighted],
        ]
      });

      tables.push({
        title: 'Validation', headers: ['Severity', 'Area', 'Message'],
        rows: (res.checks || []).map(c => [c.sev.toUpperCase(), c.area, c.msg])
      });
      return tables;
    }
  };

  // ---------------------------------------------------------- valuation
  // One module, any subject (entity/share/brand/land/machinery) and any
  // number of method "workings" run side by side and blended by weight into
  // a concluded value — see engines.js valuation() for why there's one
  // calculator per approach rather than one per subject.
  const VAL_SUBJECTS = [
    { value: 'entity', label: 'Entity / business' }, { value: 'share', label: 'Share' },
    { value: 'brand', label: 'Brand / intangible' }, { value: 'land', label: 'Land' },
    { value: 'machinery', label: 'Machinery / plant' }
  ];
  const VAL_ARRAY_SPECS = {
    cashflows: [['year', 'Year', 'num'], ['fcf', 'FCF', 'num']],
    revenue_projections: [['year', 'Year', 'num'], ['revenue', 'Revenue', 'num']],
    projections: [['year', 'Year', 'num'], ['earnings', 'Earnings', 'num']],
    comparables: [['name', 'Name', 'text'], ['value', 'Value / multiple', 'num'], ['_adj_pct', 'Net adjustment %', 'num']],
    assets: [['desc', 'Description', 'text'], ['book_value', 'Book value', 'num'], ['fair_value', 'Fair value (optional)', 'num']],
    liabilities: [['desc', 'Description', 'text'], ['book_value', 'Book value', 'num'], ['fair_value', 'Fair value (optional)', 'num']],
    contributory_asset_charges: [['desc', 'Description', 'text'], ['annual_charge', 'Annual charge', 'num']],
    deductions: [['desc', 'Description', 'text'], ['amount', 'Amount', 'num']],
    premiums: [['desc', 'Description', 'text'], ['amount', 'Amount', 'num']]
  };
  const VAL_METHOD_SCALARS = {
    dcf: [['discount_rate_pct', 'Discount rate (WACC) %'], ['terminal_method', 'Terminal method', 'select',
            [['gordon', 'Gordon growth'], ['exit_multiple', 'Exit multiple']]],
          ['terminal_growth_pct', 'Terminal growth %'], ['exit_multiple', 'Exit multiple (if exit-multiple method)'],
          ['exit_metric_value', 'Exit metric value (e.g. terminal-year EBITDA)'], ['net_debt', 'Net debt'],
          ['non_operating_assets', 'Non-operating assets'], ['minority_interest', 'Minority interest'],
          ['shares_outstanding', 'Shares outstanding (for value per share)']],
    fcfe: [['discount_rate_pct', 'Discount rate (cost of equity) %'], ['terminal_method', 'Terminal method', 'select',
            [['gordon', 'Gordon growth'], ['exit_multiple', 'Exit multiple']]],
          ['terminal_growth_pct', 'Terminal growth %'], ['exit_multiple', 'Exit multiple (if exit-multiple method)'],
          ['exit_metric_value', 'Exit metric value (e.g. terminal-year earnings)'],
          ['non_operating_assets', 'Non-operating assets'], ['minority_interest', 'Minority interest'],
          ['shares_outstanding', 'Shares outstanding (for value per share)']],
    income_capitalization: [['normalized_income', 'Normalised income'], ['cap_rate_pct', 'Capitalisation rate %'],
          ['growth_pct', 'Growth % (0 for a flat perpetuity)'], ['net_debt', 'Net debt (0 for land/machinery)'],
          ['shares_outstanding', 'Shares outstanding (for value per share)']],
    market_comparable: [['selected_stat', 'Statistic', 'select', [['median', 'Median'], ['mean', 'Mean'], ['custom', 'Custom']]],
          ['custom_value', 'Custom multiple/rate (if Custom selected)'],
          ['subject_metric', "Subject metric (EBITDA, area, etc — leave 1 if comparables are already direct values)"],
          ['shares_outstanding', 'Shares outstanding (for value per share)']],
    precedent_transaction: [['selected_stat', 'Statistic', 'select', [['median', 'Median'], ['mean', 'Mean'], ['custom', 'Custom']]],
          ['custom_value', 'Custom multiple/rate (if Custom selected)'],
          ['subject_metric', "Subject metric (EBITDA, revenue, etc — leave 1 if deal values are already direct values)"],
          ['shares_outstanding', 'Shares outstanding (for value per share)']],
    nav: [['shares_outstanding', 'Shares outstanding (for value per share)']],
    liquidation: [['nav_value', 'Net asset value (carry over from a NAV working, or enter directly)'],
          ['forced_sale_discount_pct', 'Forced-sale discount %'], ['winding_up_costs', 'Winding-up costs'],
          ['priority_claims', 'Priority claims ahead of equity'], ['shares_outstanding', 'Shares outstanding']],
    relief_from_royalty: [['royalty_rate_pct', 'Royalty rate %'], ['tax_rate_pct', 'Tax rate %'],
          ['discount_rate_pct', 'Discount rate %'], ['terminal_growth_pct', 'Terminal growth %'],
          ['tax_amortization_benefit', 'Tax amortisation benefit', 'check'], ['tab_years', 'TAB amortisation years']],
    excess_earnings: [['tax_rate_pct', 'Tax rate %'], ['discount_rate_pct', 'Discount rate %'],
          ['terminal_growth_pct', 'Terminal growth %']],
    rcnld: [['replacement_cost_new', 'Replacement cost new'], ['effective_age_years', 'Effective age (years)'],
          ['remaining_useful_life_years', 'Remaining useful life (years)'],
          ['physical_deterioration_pct_override', 'Physical deterioration % (override — default is age / (age + remaining life))'],
          ['functional_obsolescence_pct', 'Functional obsolescence %'], ['economic_obsolescence_pct', 'Economic obsolescence %']]
  };
  const VAL_METHOD_ARRAYS = {
    dcf: [['cashflows', 'Cash flows (free cash flow to firm, by year)']],
    fcfe: [['cashflows', 'Cash flows (free cash flow to equity, by year)']],
    market_comparable: [['comparables', 'Comparables'], ['deductions', 'Deductions'], ['premiums', 'Premiums']],
    precedent_transaction: [['comparables', 'Precedent transactions'], ['deductions', 'Deductions'], ['premiums', 'Premiums (e.g. control premium)']],
    nav: [['assets', 'Assets'], ['liabilities', 'Liabilities']],
    relief_from_royalty: [['revenue_projections', 'Revenue projections']],
    excess_earnings: [['projections', 'Earnings projections'], ['contributory_asset_charges', 'Contributory asset charges']]
  };
  const VAL_METHOD_RESULT_KEYS = {
    dcf: [['PV of cash flows', 'pv_of_cashflows'], ['Terminal value', 'terminal_value'],
          ['PV of terminal value', 'pv_of_terminal_value'], ['Enterprise value', 'enterprise_value'],
          ['Less: net debt', 'net_debt'], ['Add: non-operating assets', 'non_operating_assets'],
          ['Less: minority interest', 'minority_interest'], ['Equity value', 'value'], ['Value per share', 'value_per_share']],
    fcfe: [['PV of cash flows', 'pv_of_cashflows'], ['Terminal value', 'terminal_value'],
          ['PV of terminal value', 'pv_of_terminal_value'], ['Add: non-operating assets', 'non_operating_assets'],
          ['Less: minority interest', 'minority_interest'], ['Equity value', 'value'], ['Value per share', 'value_per_share']],
    income_capitalization: [['Gross value', 'gross_value'], ['Less: net debt', 'net_debt'], ['Value', 'value'], ['Value per share', 'value_per_share']],
    market_comparable: [['Chosen multiple/rate', 'chosen_multiple_or_rate'], ['Gross value', 'gross_value'],
          ['Less: deductions', 'deduction_total'], ['Add: premiums', 'premium_total'], ['Value', 'value'], ['Value per share', 'value_per_share']],
    precedent_transaction: [['Chosen multiple/rate', 'chosen_multiple_or_rate'], ['Gross value', 'gross_value'],
          ['Less: deductions', 'deduction_total'], ['Add: premiums', 'premium_total'], ['Value', 'value'], ['Value per share', 'value_per_share']],
    nav: [['Total assets (fair value)', 'assets_total'], ['Total liabilities (fair value)', 'liabilities_total'], ['Value', 'value'], ['Value per share', 'value_per_share']],
    liquidation: [['Value after discount', 'value_after_discount'], ['Value', 'value']],
    relief_from_royalty: [['PV of royalty savings', 'pv_of_royalty_savings'], ['Terminal value', 'terminal_value'], ['Value', 'value']],
    excess_earnings: [['PV of excess earnings', 'pv_of_excess_earnings'], ['Terminal value', 'terminal_value'], ['Value', 'value']],
    rcnld: [['Value after physical', 'value_after_physical'], ['Value after functional', 'value_after_functional'], ['Value', 'value']]
  };

  function valArrayTable(wi, arrayKey, title, rows) {
    const spec = VAL_ARRAY_SPECS[arrayKey];
    let h = '<div style="margin-top:10px"><b style="font-size:12px">' + esc(title) + '</b><div class="table-wrap"><table><thead><tr>' +
      spec.map(c => '<th' + (c[2] === 'num' ? ' class="num"' : '') + '>' + esc(c[1]) + '</th>').join('') + '<th></th></tr></thead><tbody>';
    (rows || []).forEach((row, i) => {
      const display = k => k === '_adj_pct'
        ? ((row.adjustments && row.adjustments[0] && row.adjustments[0].pct) || 0)
        : (row[k] != null ? row[k] : '');
      h += '<tr>' + spec.map(c => '<td' + (c[2] === 'num' ? ' class="num"' : '') + '><input' + (c[2] === 'num' ? ' type="number"' : '') +
        ' data-warr="' + wi + ',' + arrayKey + ',' + i + '" data-k="' + c[0] + '" value="' + esc(display(c[0])) + '"></td>').join('') +
        '<td><button class="btn btn-small btn-danger" data-del-warr="' + wi + ',' + arrayKey + ',' + i + '" title="Remove this row">Remove</button></td></tr>';
    });
    h += '</tbody></table></div><button class="btn btn-small" data-add-warr="' + wi + ',' + arrayKey + '">Add a row</button></div>';
    return h;
  }

  MODULES.valuation = {
    code: 'VL', name: 'Valuation', group: 'Advisory',
    blurb: 'Entity, share, brand, land or machinery — nine income/market/asset/cost methods (including FCFF and FCFE DCF, comparable-company and precedent-transaction analysis) run side by side and blended into one concluded value, each with a built-in discount-rate x growth sensitivity grid. A CAPM/WACC discount-rate builder, and a rule-based valuation review checklist, sit alongside the workings.',
    seed: () => ({ subject: { name: '', subject_type: 'entity', valuation_date: '', purpose: '' }, workings: [],
      discount_rate_builder: { risk_free_rate_pct: 0, beta: 1, equity_risk_premium_pct: 0, size_premium_pct: 0,
        specific_risk_premium_pct: 0, cost_of_debt_pct: 0, tax_rate_pct: 0, market_value_of_equity: 0, market_value_of_debt: 0 },
      conclusion: { weights: {}, notes: '' },
      review: { reviewed_by: '', review_date: '', key_assumptions_challenged: '', independence_confirmed: false, review_conclusion: '', review_status: 'pending' } }),
    view(file, res) {
      const p = file.payload;
      p.subject = p.subject || {}; p.workings = p.workings || []; p.conclusion = p.conclusion || { weights: {}, notes: '' };
      p.discount_rate_builder = p.discount_rate_builder || {};
      p.review = p.review || {};
      const byId = {}; (res.workings || []).forEach(w => { byId[w.id] = w; });

      let h = '<div class="card"><h3>Subject</h3><div class="grid c3" id="val-subject">' +
        field('Name', 'name', p.subject, 'text') +
        field('Subject type', 'subject_type', p.subject, 'select', { options: VAL_SUBJECTS }) +
        field('Valuation date', 'valuation_date', p.subject, 'date') +
        field('Purpose', 'purpose', p.subject, 'text', { placeholder: 'Transfer pricing, ESOP, buy-back, financial reporting...' }) +
        '</div></div>';

      h += '<details class="card"><summary><b>Discount rate builder — cost of equity (CAPM) &amp; WACC</b></summary>' +
        '<p class="card-note" style="margin-top:8px">Computes a rate for you to read off and type into a DCF/FCFE/income-capitalisation working\'s own discount rate field — it is not itself a working and does not affect the concluded value.</p>' +
        '<div class="grid c3" id="val-drb" style="margin-top:12px">' +
        field('Risk-free rate %', 'risk_free_rate_pct', p.discount_rate_builder, 'number', { step: '0.01' }) +
        field('Beta', 'beta', p.discount_rate_builder, 'number', { step: '0.01' }) +
        field('Equity risk premium %', 'equity_risk_premium_pct', p.discount_rate_builder, 'number', { step: '0.01' }) +
        field('Size premium %', 'size_premium_pct', p.discount_rate_builder, 'number', { step: '0.01' }) +
        field('Specific/company risk premium %', 'specific_risk_premium_pct', p.discount_rate_builder, 'number', { step: '0.01' }) +
        field('Cost of debt (pre-tax) %', 'cost_of_debt_pct', p.discount_rate_builder, 'number', { step: '0.01' }) +
        field('Tax rate %', 'tax_rate_pct', p.discount_rate_builder, 'number', { step: '0.01' }) +
        field('Market value of equity', 'market_value_of_equity', p.discount_rate_builder, 'number') +
        field('Market value of debt', 'market_value_of_debt', p.discount_rate_builder, 'number') +
        '</div>';
      if (res.discount_rate_builder) {
        const drb = res.discount_rate_builder;
        h += '<div class="grid c4" style="margin-top:12px">' +
          kpi('Cost of equity (CAPM)', drb.cost_of_equity_pct.toFixed(2) + '%', 'accent') +
          kpi('After-tax cost of debt', drb.after_tax_cost_of_debt_pct.toFixed(2) + '%') +
          kpi('Weights — equity · debt', drb.weight_of_equity_pct.toFixed(0) + '% · ' + drb.weight_of_debt_pct.toFixed(0) + '%') +
          kpi('WACC', drb.wacc_pct.toFixed(2) + '%', 'accent') +
          '</div>';
      }
      h += '</details>';

      p.workings.forEach((w, wi) => {
        w.inputs = w.inputs || {};
        const applicable = Object.keys(CAEngines.VALUATION_METHOD_META).filter(m =>
          CAEngines.VALUATION_METHOD_META[m].subjects.includes(p.subject.subject_type));
        const computed = byId[w.id];
        h += '<details class="card" open><summary><b>' + esc(w.label || 'New working') + '</b>' +
          (computed ? '<span class="hint"> — ' + money(computed.value) + (computed.value_per_share != null ? ' (' + money(computed.value_per_share) + '/share)' : '') + '</span>' : '') +
          '</summary>';
        h += '<div class="grid c3" style="margin-top:12px">' +
          '<div class="field"><label>Label</label><input data-w="' + wi + '" data-k="label" value="' + esc(w.label || '') + '"></div>' +
          '<div class="field"><label>Method</label><select data-w="' + wi + '" data-k="method">' +
          applicable.map(m => '<option value="' + m + '"' + (w.method === m ? ' selected' : '') + '>' + esc(CAEngines.VALUATION_METHOD_META[m].label) + '</option>').join('') +
          '</select></div>' +
          '<div class="field"><label>Fair value level (NFRS 13)</label><select data-w="' + wi + '" data-k="fair_value_level">' +
          '<option value="">— not applicable —</option>' +
          [1, 2, 3].map(l => '<option value="' + l + '"' + (String(w.fair_value_level) === String(l) ? ' selected' : '') + '>Level ' + l + '</option>').join('') +
          '</select></div></div>';

        const scalars = VAL_METHOD_SCALARS[w.method] || [];
        if (scalars.length) {
          h += '<div class="grid c3" style="margin-top:12px">' + scalars.map(s => {
            const [key, label, type, opts] = s;
            if (type === 'select') return '<div class="field"><label>' + esc(label) + '</label><select data-winput="' + wi + '" data-k="' + key + '">' +
              opts.map(o => '<option value="' + o[0] + '"' + (w.inputs[key] === o[0] ? ' selected' : '') + '>' + esc(o[1]) + '</option>').join('') + '</select></div>';
            if (type === 'check') return '<label class="check"><input type="checkbox" data-winput="' + wi + '" data-k="' + key + '"' + (w.inputs[key] ? ' checked' : '') + '> ' + esc(label) + '</label>';
            return '<div class="field"><label>' + esc(label) + '</label><input type="number" data-winput="' + wi + '" data-k="' + key + '" value="' + (w.inputs[key] ?? '') + '"></div>';
          }).join('') + '</div>';
        }
        (VAL_METHOD_ARRAYS[w.method] || []).forEach(([arrayKey, title]) => {
          h += valArrayTable(wi, arrayKey, title, w.inputs[arrayKey]);
        });

        if (computed) {
          const keys = VAL_METHOD_RESULT_KEYS[w.method] || [];
          h += '<div class="card-note" style="margin-top:10px">' +
            keys.filter(([, k]) => computed[k] != null).map(([label, k]) => label + ': ' + (typeof computed[k] === 'number' ? money(computed[k]) : computed[k])).join(' · ') +
            '</div>';
          if (computed.sensitivity_grid) {
            const rateKey = Object.keys(computed.sensitivity_grid[0]).find(k => k !== 'cells');
            const growthKey = Object.keys(computed.sensitivity_grid[0].cells[0]).find(k => k !== 'value');
            h += '<div style="margin-top:10px"><b style="font-size:12px">Sensitivity — value at each combination</b>' +
              '<div class="table-wrap"><table><thead><tr><th>' + esc(rateKey) + '</th>' +
              computed.sensitivity_grid[0].cells.map(c => '<th class="num">' + esc(growthKey) + ' ' + c[growthKey] + '</th>').join('') +
              '</tr></thead><tbody>' +
              computed.sensitivity_grid.map(row => '<tr><td>' + row[rateKey] + '</td>' +
                row.cells.map(c => '<td class="num">' + (c.value != null ? money(c.value) : '—') + '</td>').join('') + '</tr>').join('') +
              '</tbody></table></div></div>';
          }
        }
        h += '<button class="btn btn-small btn-danger" style="margin-top:10px" data-del-w="' + wi + '">Remove this working</button></details>';
      });
      h += '<div class="card"><button class="btn" id="add-working">Add a working</button></div>';

      if (res.workings && res.workings.length) {
        h += '<div class="card"><h3>Conclusion</h3><p class="card-note">Weight the workings you want blended into a concluded value — weights don\'t need to add to 100, they\'re normalised. Leave a working\'s weight at 0 to exclude it.</p>' +
          '<div class="table-wrap"><table><thead><tr><th>Working</th><th>Method</th><th class="num">Value</th><th class="num">Weight</th></tr></thead><tbody>' +
          res.workings.map(w => '<tr><td>' + esc(w.label) + '</td><td>' + esc(CAEngines.VALUATION_METHOD_META[w.method].label) + '</td>' +
            '<td class="num">' + money(w.value) + '</td>' +
            '<td class="num"><input type="number" data-weight="' + w.id + '" value="' + (p.conclusion.weights[w.id] || 0) + '" style="width:70px"></td></tr>').join('') +
          '</tbody></table></div>' +
          '<div class="field" style="margin-top:12px"><label>Notes</label><textarea data-key="notes" rows="3">' + esc(p.conclusion.notes || '') + '</textarea></div>' +
          '</div>';
        h += resultCard('Concluded value', ['Working', 'Value', 'Weight'],
          res.conclusion.blend.map(b => [b.label, b.value, b.weight]),
          ['Concluded value', res.conclusion.concluded_value, '']);
        if (res.conclusion.range_low != null) {
          h += '<div class="card"><b>Range across all workings:</b> ' + money(res.conclusion.range_low) + ' — ' + money(res.conclusion.range_high) + '</div>';
        }
      }

      const review = res.review || { flags: [] };
      h += '<div class="card"><h3>Valuation review / challenge</h3><p class="card-note">Rule-based prompts, computed automatically — not a pass/fail verdict, just what a reviewer would ask about.</p>';
      if (review.flags && review.flags.length) {
        h += '<div class="table-wrap"><table><thead><tr><th>Severity</th><th>Prompt</th></tr></thead><tbody>' +
          review.flags.map(f => '<tr><td><span class="tag' + (f.severity === 'High' ? ' alert' : '') + '">' + esc(f.severity) + '</span></td><td>' + esc(f.message) + '</td></tr>').join('') +
          '</tbody></table></div>';
      } else {
        h += '<div class="msg good">No automated review prompts on the current workings.</div>';
      }
      h += '<div class="grid c3" style="margin-top:12px" id="val-review">' +
        field('Reviewed by', 'reviewed_by', p.review, 'text') +
        field('Review date', 'review_date', p.review, 'date') +
        field('Review status', 'review_status', p.review, 'select', { options: [
          { value: 'pending', label: 'Pending' }, { value: 'approved', label: 'Approved' },
          { value: 'sent_back', label: 'Sent back for revision' }] }) +
        '<div class="field" style="grid-column:1/-1"><label>Key assumptions challenged</label><textarea data-key="key_assumptions_challenged" rows="2">' + esc(p.review.key_assumptions_challenged || '') + '</textarea></div>' +
        '<div class="field" style="grid-column:1/-1"><label>Review conclusion</label><textarea data-key="review_conclusion" rows="2">' + esc(p.review.review_conclusion || '') + '</textarea></div>' +
        '<label class="check"><input type="checkbox" data-review-flag="independence_confirmed"' + (p.review.independence_confirmed ? ' checked' : '') + '> Reviewer independent of preparer</label>' +
        '</div></div>';
      return h;
    },
    wire(root, file, rerender) {
      const p = file.payload;
      wireInto(root, 'val-subject', p.subject, rerender);
      wireInto(root, 'val-drb', p.discount_rate_builder, rerender);
      wireInto(root, 'val-review', p.review, rerender);
      root.querySelectorAll('[data-review-flag]').forEach(el => el.addEventListener('change', e => {
        p.review[e.target.dataset.reviewFlag] = e.target.checked; rerender();
      }));

      root.querySelectorAll('[data-w]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.w, k = e.target.dataset.k;
        p.workings[i][k] = e.target.value === '' ? null : e.target.value;
        if (k === 'method') p.workings[i].inputs = {};
        rerender();
      }));
      root.querySelectorAll('[data-del-w]').forEach(el => el.addEventListener('click', e => {
        p.workings.splice(+e.target.dataset.delW, 1); rerender();
      }));
      const addW = root.querySelector('#add-working');
      if (addW) addW.addEventListener('click', () => {
        const applicable = Object.keys(CAEngines.VALUATION_METHOD_META).filter(m =>
          CAEngines.VALUATION_METHOD_META[m].subjects.includes(p.subject.subject_type));
        const method = applicable[0] || 'dcf';
        p.workings.push({ id: 'w' + Date.now() + Math.floor(Math.random() * 1000), method,
          label: CAEngines.VALUATION_METHOD_META[method].label, fair_value_level: '', inputs: {} });
        rerender();
      });

      root.querySelectorAll('[data-winput]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.winput, k = e.target.dataset.k;
        const inputs = p.workings[i].inputs;
        inputs[k] = e.target.type === 'checkbox' ? e.target.checked
          : (e.target.type === 'number' ? (e.target.value === '' ? null : Number(e.target.value)) : e.target.value);
        rerender();
      }));

      root.querySelectorAll('[data-warr]').forEach(el => el.addEventListener('change', e => {
        const [wi, arrayKey, i] = e.target.dataset.warr.split(',');
        const inputs = p.workings[+wi].inputs;
        inputs[arrayKey] = inputs[arrayKey] || [];
        const row = inputs[arrayKey][+i] = inputs[arrayKey][+i] || {};
        const k = e.target.dataset.k;
        const val = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        if (k === '_adj_pct') row.adjustments = [{ factor: 'Net adjustment', pct: val }];
        else row[k] = val;
        rerender();
      }));
      root.querySelectorAll('[data-del-warr]').forEach(el => el.addEventListener('click', e => {
        const [wi, arrayKey, i] = e.target.dataset.delWarr.split(',');
        p.workings[+wi].inputs[arrayKey].splice(+i, 1); rerender();
      }));
      root.querySelectorAll('[data-add-warr]').forEach(el => el.addEventListener('click', e => {
        const [wi, arrayKey] = e.target.dataset.addWarr.split(',');
        const inputs = p.workings[+wi].inputs;
        inputs[arrayKey] = inputs[arrayKey] || [];
        inputs[arrayKey].push({}); rerender();
      }));

      root.querySelectorAll('[data-weight]').forEach(el => el.addEventListener('change', e => {
        p.conclusion.weights[e.target.dataset.weight] = Number(e.target.value) || 0; rerender();
      }));
      const notes = root.querySelector('textarea[data-key="notes"]');
      if (notes) notes.addEventListener('change', e => { p.conclusion.notes = e.target.value; rerender(); });
    },
    tabulate: res => {
      const methodLabel = m => (CAEngines.VALUATION_METHOD_META[m] && CAEngines.VALUATION_METHOD_META[m].label) || m;
      const subj = res.subject || {};
      const cons = res.conclusion || {};
      const drb = res.discount_rate_builder;
      const review = res.review || {};
      const tables = [];

      tables.push({
        title: 'Subject', headers: ['Particulars', 'Detail'],
        rows: [['Subject', subj.name || ''], ['Subject type', subj.subject_type || ''],
          ['Valuation date', subj.valuation_date || ''], ['Purpose', subj.purpose || '']]
      });

      tables.push({
        title: 'Workings', headers: ['Working', 'Method', 'Approach', 'Fair value level (NFRS 13)', 'Value'],
        rows: (res.workings || []).map(w => [w.label, methodLabel(w.method), w.approach || '', w.fair_value_level || '', w.value])
      });

      if (cons.blend && cons.blend.length) {
        const rows = cons.blend.map(b => [b.label, b.value, b.weight]);
        rows.push(['Concluded value', cons.concluded_value, '']);
        tables.push({ title: 'Conclusion — blend', headers: ['Working', 'Value', 'Weight'], rows });
      }
      if (cons.range_low != null) {
        tables.push({ title: 'Conclusion — range', headers: ['Particulars', 'Value'],
          rows: [['Range low', cons.range_low], ['Range high', cons.range_high], ['Notes', cons.notes || '']] });
      } else if (cons.notes) {
        tables.push({ title: 'Conclusion — notes', headers: ['Notes'], rows: [[cons.notes]] });
      }

      if (drb) {
        tables.push({
          title: 'Discount rate builder (CAPM / WACC)', headers: ['Particulars', 'Value'],
          rows: [
            ['Risk-free rate %', drb.risk_free_rate_pct], ['Beta', drb.beta],
            ['Equity risk premium %', drb.equity_risk_premium_pct], ['Size premium %', drb.size_premium_pct],
            ['Specific risk premium %', drb.specific_risk_premium_pct], ['Cost of equity % (CAPM)', drb.cost_of_equity_pct],
            ['Cost of debt %', drb.cost_of_debt_pct], ['Tax rate %', drb.tax_rate_pct],
            ['After-tax cost of debt %', drb.after_tax_cost_of_debt_pct], ['Weight of equity %', drb.weight_of_equity_pct],
            ['Weight of debt %', drb.weight_of_debt_pct], ['WACC %', drb.wacc_pct],
          ]
        });
      }

      if ((review.flags && review.flags.length) || review.reviewed_by) {
        if (review.flags && review.flags.length) {
          tables.push({ title: 'Valuation review — flags', headers: ['Severity', 'Message'],
            rows: review.flags.map(f => [f.severity, f.message]) });
        }
        if (review.reviewed_by) {
          tables.push({
            title: 'Valuation review', headers: ['Particulars', 'Detail'],
            rows: [
              ['Reviewed by', review.reviewed_by], ['Review date', review.review_date],
              ['Key assumptions challenged', review.key_assumptions_challenged],
              ['Independence confirmed', review.independence_confirmed ? 'Yes' : 'No'],
              ['Review conclusion', review.review_conclusion], ['Review status', review.review_status],
            ]
          });
        }
      }

      const VAL_SUMMARY_KEYS = {
        dcf: [['PV of cash flows', 'pv_of_cashflows'], ['Discount rate %', 'discount_rate_pct'], ['Terminal value', 'terminal_value'],
          ['PV of terminal value', 'pv_of_terminal_value'], ['Enterprise value', 'enterprise_value'], ['Less: net debt', 'net_debt'],
          ['Add: non-operating assets', 'non_operating_assets'], ['Less: minority interest', 'minority_interest'],
          ['Equity value', 'value'], ['Value per share', 'value_per_share']],
        fcfe: [['PV of cash flows', 'pv_of_cashflows'], ['Discount rate (cost of equity) %', 'discount_rate_pct'],
          ['Terminal value', 'terminal_value'], ['PV of terminal value', 'pv_of_terminal_value'],
          ['Add: non-operating assets', 'non_operating_assets'], ['Less: minority interest', 'minority_interest'],
          ['Equity value', 'value'], ['Value per share', 'value_per_share']],
        income_capitalization: [['Normalised income', 'normalized_income'], ['Cap rate %', 'cap_rate_pct'], ['Growth %', 'growth_pct'],
          ['Gross value', 'gross_value'], ['Less: net debt', 'net_debt'], ['Value', 'value'], ['Value per share', 'value_per_share']],
        market_comparable: [['Chosen multiple/rate', 'chosen_multiple_or_rate'], ['Subject metric', 'subject_metric'],
          ['Gross value', 'gross_value'], ['Less: deductions', 'deduction_total'], ['Add: premiums', 'premium_total'],
          ['Value', 'value'], ['Value per share', 'value_per_share']],
        precedent_transaction: [['Chosen multiple/rate', 'chosen_multiple_or_rate'], ['Subject metric', 'subject_metric'],
          ['Gross value', 'gross_value'], ['Less: deductions', 'deduction_total'], ['Add: premiums', 'premium_total'],
          ['Value', 'value'], ['Value per share', 'value_per_share']],
        nav: [['Total assets (fair value)', 'assets_total'], ['Total liabilities (fair value)', 'liabilities_total'],
          ['Value', 'value'], ['Value per share', 'value_per_share']],
        liquidation: [['NAV', 'nav_value'], ['Forced-sale discount %', 'forced_sale_discount_pct'],
          ['Value after discount', 'value_after_discount'], ['Less: winding-up costs', 'winding_up_costs'],
          ['Less: priority claims', 'priority_claims'], ['Value', 'value']],
        relief_from_royalty: [['PV of royalty savings', 'pv_of_royalty_savings'], ['Terminal value', 'terminal_value'],
          ['PV of terminal value', 'pv_of_terminal_value'], ['Tax amortisation benefit applied', 'tax_amortization_benefit_applied'],
          ['Value', 'value']],
        excess_earnings: [['PV of excess earnings', 'pv_of_excess_earnings'], ['Terminal value', 'terminal_value'],
          ['PV of terminal value', 'pv_of_terminal_value'], ['Value', 'value']],
        rcnld: [['Replacement cost new', 'replacement_cost_new'], ['Physical deterioration %', 'physical_deterioration_pct'],
          ['Value after physical deterioration', 'value_after_physical'], ['Functional obsolescence %', 'functional_obsolescence_pct'],
          ['Value after functional obsolescence', 'value_after_functional'], ['Economic obsolescence %', 'economic_obsolescence_pct'],
          ['Value', 'value']],
      };
      const VAL_LINE_SPECS = {
        dcf: [['Year', 'FCF', 'Discount factor', 'PV'], ['year', 'fcf', 'discount_factor', 'pv']],
        fcfe: [['Year', 'FCFE', 'Discount factor', 'PV'], ['year', 'fcf', 'discount_factor', 'pv']],
        relief_from_royalty: [['Year', 'Revenue', 'Royalty saving', 'After-tax saving', 'PV'],
          ['year', 'revenue', 'royalty_saving', 'after_tax_saving', 'pv']],
        excess_earnings: [['Year', 'Earnings', 'After-tax earnings', 'Contributory charges', 'Excess earnings', 'PV'],
          ['year', 'earnings', 'after_tax_earnings', 'contributory_charges', 'excess_earnings', 'pv']],
      };
      const VAL_TABLE_SPECS = {
        nav: [['assets', 'Assets', ['Description', 'Book value', 'Fair value', 'Adjustment'], ['desc', 'book_value', 'fair_value', 'adjustment']],
          ['liabilities', 'Liabilities', ['Description', 'Book value', 'Fair value', 'Adjustment'], ['desc', 'book_value', 'fair_value', 'adjustment']]],
        market_comparable: [['comparables', 'Comparables', ['Name', 'Base value', 'Adjustment %', 'Adjusted value'],
          ['name', 'base_value', 'adjustment_pct', 'adjusted_value']]],
        precedent_transaction: [['comparables', 'Precedent transactions', ['Name', 'Base value', 'Adjustment %', 'Adjusted value'],
          ['name', 'base_value', 'adjustment_pct', 'adjusted_value']]],
      };

      (res.workings || []).forEach(w => {
        const prefix = w.label || w.method;
        const summary = VAL_SUMMARY_KEYS[w.method] || [];
        const srows = summary.filter(([, key]) => w[key] != null).map(([label, key]) => [label, w[key]]);
        tables.push({ title: prefix + ' — summary', headers: ['Particulars', 'Amount'], rows: srows });

        if (VAL_LINE_SPECS[w.method]) {
          const [headers, keys] = VAL_LINE_SPECS[w.method];
          const lrows = (w.lines || []).map(l => keys.map(k => l[k]));
          if (lrows.length) tables.push({ title: prefix + ' — lines', headers, rows: lrows });
        }

        (VAL_TABLE_SPECS[w.method] || []).forEach(([key, title, headers, keys2]) => {
          const rows2 = (w[key] || []).map(x => keys2.map(k => x[k]));
          if (rows2.length) tables.push({ title: prefix + ' — ' + title, headers, rows: rows2 });
        });

        if (w.sensitivity_grid && w.sensitivity_grid.length) {
          const grid = w.sensitivity_grid;
          const rateKey = Object.keys(grid[0]).find(k => k !== 'cells');
          const growthKey = Object.keys(grid[0].cells[0]).find(k => k !== 'value');
          const growthVals = grid[0].cells.map(c => c[growthKey]);
          const headers = [rateKey].concat(growthVals.map(g => growthKey + ' ' + g));
          const rows = grid.map(row => [row[rateKey]].concat(row.cells.map(c => c.value)));
          tables.push({ title: prefix + ' — sensitivity', headers, rows });
        }
      });

      return tables;
    }
  };

  // shared view helpers -------------------------------------------------------
  const sum = (arr, k) => CAEngines.R2(arr.reduce((s, x) => s + (Number(x[k]) || 0), 0));
  const kpi = (label, value, cls, tip) => '<div class="kpi ' + (cls || '') + '"' + (tip ? ' title="' + esc(tip) + '"' : '') +
    '><span>' + esc(label) + '</span><b>' + value + '</b></div>';

  // ── Master Data Hub — generic sync-status badge + action bar. Reusable by
  // any module that pulls a hub_snapshot in via a hub_link (GET
  // /api/hub/links?client_id=&consuming_module=). Not wired into any screen
  // yet — this is the shared building block a module reaches for the first
  // time it publishes/consumes a Hub snapshot, so every module that does
  // ends up with the same badge and the same three actions rather than
  // reinventing its own.
  const HUB_STATUS_META = {
    synced: { cls: 'tag-green', label: 'Synced' },
    outdated: { cls: 'tag-amber', label: 'Outdated' },
    requires_review: { cls: 'tag-amber', label: 'Requires review' },
    overridden: { cls: 'tag', label: 'Overridden' },
    finalized: { cls: 'tag-green', label: 'Finalized' },
  };
  function hubStatusBadge(status) {
    const meta = HUB_STATUS_META[status] || { cls: 'tag', label: status || 'Unknown' };
    return '<span class="tag ' + meta.cls + '">' + esc(meta.label) + '</span>';
  }
  /** link: a row from GET /api/hub/links (carries effective_status + snapshot).
      Renders Refresh / Recalculate / Compare Changes — wire the three
      data-hub-* buttons to: re-fetch+re-render (Refresh), re-run this
      module's own compute() (Recalculate), and a diff view of
      link.snapshot.payload vs. what was pulled in (Compare Changes). */
  function hubLinkActions(link) {
    return '<span class="hub-link-actions" data-hub-link="' + link.id + '">' + hubStatusBadge(link.effective_status || link.status) +
      ' <button class="btn btn-small btn-ghost" data-hub-refresh="' + link.id + '" title="Re-check this link against the source">Refresh</button>' +
      ' <button class="btn btn-small btn-ghost" data-hub-recalc="' + link.id + '" title="Re-run this module\'s own computation">Recalculate</button>' +
      ' <button class="btn btn-small btn-ghost" data-hub-compare="' + link.id + '" title="See what changed in the source since this was pulled in">Compare Changes</button></span>';
  }

  /** Renders a journal register: [{period, narration, debit:[{account,amount}], credit:[{account,amount}]}].
      One row per debit/credit line, narration and period shown once per entry via rowspan. */
  function journalTable(title, journals) {
    if (!journals || !journals.length) return '';
    let h = '<div class="card"><h3>' + esc(title) + '</h3><div class="table-wrap"><table><thead><tr>' +
      '<th>Period</th><th>Narration</th><th>Account</th><th class="num">Debit</th><th class="num">Credit</th></tr></thead><tbody>';
    journals.forEach(entry => {
      const lines = entry.debit.map(x => ({ account: x.account, amount: x.amount, credit: false }))
        .concat(entry.credit.map(x => ({ account: x.account, amount: x.amount, credit: true })));
      lines.forEach((line, i) => {
        h += '<tr>' +
          (i === 0 ? '<td rowspan="' + lines.length + '">' + esc(entry.period) + '</td><td rowspan="' + lines.length + '">' + esc(entry.narration) + '</td>' : '') +
          '<td>' + (line.credit ? '&nbsp;&nbsp;&nbsp;&nbsp;' : '') + esc(line.account) + '</td>' +
          '<td class="num">' + (line.credit ? '' : money(line.amount)) + '</td>' +
          '<td class="num">' + (line.credit ? money(line.amount) : '') + '</td></tr>';
      });
    });
    return h + '</tbody></table></div></div>';
  }

  /** Same journal-entry shape as journalTable(), flattened into tabulate()-
      style {title, headers, rows} tables — one per fiscal year when
      journalsByFy is grouped (mirrors reports.py's one-sheet-per-FY), or one
      flat table otherwise. Used so the offline/CSV export includes the same
      journal entries the full online workbook does, instead of silently
      dropping them (this table shape is exactly what the CSV export in
      generateReport() already knows how to write). */
  function journalTabulateTables(journals, journalsByFy, baseTitle) {
    baseTitle = baseTitle || 'Journal entries';
    const headers = ['Period', 'Narration', 'Account', 'Debit', 'Credit'];
    function rowsFor(list) {
      const rows = [];
      (list || []).forEach(entry => {
        const lines = entry.debit.map(x => ({ account: x.account, amount: x.amount, credit: false }))
          .concat(entry.credit.map(x => ({ account: x.account, amount: x.amount, credit: true })));
        lines.forEach((line, i) => {
          rows.push([i === 0 ? entry.period : '', i === 0 ? entry.narration : '',
            (line.credit ? '    ' : '') + line.account, line.credit ? '' : line.amount, line.credit ? line.amount : '']);
        });
      });
      return rows;
    }
    const groups = journalsByFy && Object.keys(journalsByFy).length ? journalsByFy : null;
    if (groups) {
      return Object.keys(groups).sort().map(fy => ({ title: baseTitle + ' — FY ' + fy, headers, rows: rowsFor(groups[fy]) }));
    }
    return (journals && journals.length) ? [{ title: baseTitle, headers, rows: rowsFor(journals) }] : [];
  }

  function resultCard(title, headers, rows, totalRow) {
    return '<div class="card"><h3>' + esc(title) + '</h3><div class="table-wrap"><table><thead><tr>' +
      headers.map((h, i) => '<th class="' + (i ? 'num' : '') + '">' + esc(h) + '</th>').join('') +
      '</tr></thead><tbody>' +
      rows.map(r => '<tr>' + r.map((c, i) => '<td class="' + (typeof c === 'number' ? 'num' : '') + '">' +
        (typeof c === 'number' ? money(c) : esc(c)) + '</td>').join('') + '</tr>').join('') +
      (totalRow ? '<tr class="total">' + totalRow.map(c => '<td class="' + (typeof c === 'number' ? 'num' : '') + '">' +
        (typeof c === 'number' ? money(c) : esc(c)) + '</td>').join('') + '</tr>' : '') +
      '</tbody></table></div></div>';
  }

  function adjustTable(title, key, rows) {
    let h = '<div class="card"><h3>' + esc(title) + '</h3><div class="table-wrap"><table><thead><tr>' +
      '<th>Particulars</th><th class="num">Amount</th><th></th></tr></thead><tbody>';
    (rows || []).forEach((r, i) => {
      h += '<tr><td><input data-adj="' + key + ',' + i + '" data-k="desc" value="' + esc(r.desc || '') + '"></td>' +
        '<td class="num"><input type="number" data-adj="' + key + ',' + i + '" data-k="amount" value="' + (r.amount || 0) + '"></td>' +
        '<td><button class="btn btn-small btn-danger" data-del-adj="' + key + ',' + i + '" title="Remove this row">Remove</button></td></tr>';
    });
    return h + '</tbody></table></div><button class="btn btn-small" data-add-adj="' + key + '">Add a line</button></div>';
  }

  function wireAdjust(root, file, rerender) {
    const p = file.payload;
    root.querySelectorAll('[data-adj]').forEach(el => el.addEventListener('change', e => {
      const [key, i] = e.target.dataset.adj.split(',');
      p[key][+i][e.target.dataset.k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
      rerender();
    }));
    root.querySelectorAll('[data-del-adj]').forEach(el => el.addEventListener('click', e => {
      const [key, i] = e.target.dataset.delAdj.split(',');
      p[key].splice(+i, 1); rerender();
    }));
    root.querySelectorAll('[data-add-adj]').forEach(el => el.addEventListener('click', e => {
      const key = e.target.dataset.addAdj;
      p[key] = p[key] || []; p[key].push({ desc: '', amount: 0 }); rerender();
    }));
  }

  /** Binds field() controls found within one container to a nested object, not file.payload directly. */
  function wireInto(root, containerId, target, rerender) {
    const container = root.querySelector('#' + containerId);
    if (!container) return;
    container.querySelectorAll('.field').forEach(f => {
      const control = f.querySelector('input, select, textarea');
      if (!control || control.dataset.wired) return;
      control.dataset.wired = '1';
      const key = control.dataset.key;
      if (!key) return;
      control.addEventListener('change', () => {
        target[key] = control.type === 'number' ? Number(control.value) : control.value;
        rerender();
      });
    });
  }

  /** Binds the plain field() controls, which carry their label text as the key. */
  function wireSimple(root, file, rerender) {
    const p = file.payload;
    root.querySelectorAll('.field').forEach(f => {
      const control = f.querySelector('input, select, textarea');
      if (!control || control.dataset.wired) return;
      control.dataset.wired = '1';
      const key = control.dataset.key;
      if (!key) return;
      control.addEventListener('change', () => {
        p[key] = control.type === 'number' ? Number(control.value) : control.value;
        rerender();
      });
    });
    root.querySelectorAll('[data-flag]').forEach(el => el.addEventListener('change', e => {
      p[e.target.dataset.flag] = e.target.checked; rerender();
    }));
  }

  MODULES.internal_audit = {
    code: 'IA', name: 'Internal Audit', group: 'Audit', needsAuditCatalog: true,
    blurb: 'Risk-based planning, a Risk & Control Matrix with automatic gap analysis, process walkthroughs, an entity-type-driven checklist, a fraud risk assessment linked to the Forensic Audit module, a classified findings register that can push corrective actions straight into Practice Management, follow-up audit tracking, and a printable Internal Audit Report. The criterion bank and entity-type list are extensible right from this screen — a check or type not yet covered is a data addition, not a rebuild.',
    seed: () => ({
      entity: { name: '', entity_type: '', other_entity_type_label: '', period_label: '', scope: '', objectives: '' },
      planning: { previous_findings_status: '', key_risks_noted: '', materiality: '', team: '', start_date: '', end_date: '' },
      criteria_responses: {},
      risk_universe: [],
      racm: [],
      fraud_risk: { incentives_pressures: '', opportunities: '', attitudes_rationalization: '', override_notes: '', conclusion: '' },
      walkthroughs: [],
      follow_ups: [],
      findings: [],
      conclusion: { overall_rating: '', summary: '' },
      signatory: { name: '', designation: 'ca', membership_no: '', firm_name: '', place: '', date: '', udin: '' },
    }),
    view(file, res) {
      const p = file.payload;
      p.entity = p.entity || {}; p.planning = p.planning || {}; p.criteria_responses = p.criteria_responses || {};
      p.risk_universe = p.risk_universe || []; p.racm = p.racm || []; p.fraud_risk = p.fraud_risk || {};
      p.walkthroughs = p.walkthroughs || []; p.follow_ups = p.follow_ups || [];
      p.findings = p.findings || []; p.conclusion = p.conclusion || {}; p.signatory = p.signatory || {};
      const names = ((state.boot && state.boot.audit_entity_types) || []).map(t => t.name);
      if (!names.includes('Other')) names.push('Other');
      const entityTypeOptions = [{ value: '', label: '— select —' }].concat(names.map(n => ({ value: n, label: n })));
      const STATUS_OPTS = [
        { value: 'not_tested', label: 'Not tested' }, { value: 'satisfactory', label: 'Satisfactory' },
        { value: 'exception', label: 'Exception' }, { value: 'not_applicable', label: 'Not applicable' },
      ];
      const EFFECTIVENESS_OPTS = ['Not Assessed', 'Effective', 'Partially Effective', 'Ineffective'];
      const FINDING_CATEGORIES = ['Control Deficiency', 'Compliance', 'Fraud Risk', 'Process Inefficiency', 'Financial Reporting', 'IT / Systems', 'Other'];
      const FOLLOWUP_STATUS = ['Not started', 'In progress', 'Completed'];

      let h = '<div class="card"><h3>Engagement setup</h3><div class="grid c3" id="ia-entity">' +
        field('Client / entity name', 'name', p.entity, 'text') +
        field('Entity type', 'entity_type', p.entity, 'select', { options: entityTypeOptions }) +
        (p.entity.entity_type === 'Other' ? field('Specify entity type', 'other_entity_type_label', p.entity, 'text') : '<div></div>') +
        field('Period', 'period_label', p.entity, 'text', { placeholder: 'FY ' + state.fy }) +
        field('Scope', 'scope', p.entity, 'text', { placeholder: 'e.g. All departments, head office only...' }) +
        field('Objectives', 'objectives', p.entity, 'text') +
        '</div><div style="margin-top:10px"><button class="btn btn-small" id="ia-add-entity-type">Add a new entity type to the list</button></div></div>';

      h += '<div class="card"><h3>Planning</h3><div class="grid c3" id="ia-planning">' +
        field('Prior period findings — closure status', 'previous_findings_status', p.planning, 'text') +
        field('Key risks noted for this period', 'key_risks_noted', p.planning, 'text') +
        field('Materiality', 'materiality', p.planning, 'number') +
        field('Audit team', 'team', p.planning, 'text', { placeholder: 'Names / roles' }) +
        field('Fieldwork start date', 'start_date', p.planning, 'date') +
        field('Fieldwork end date', 'end_date', p.planning, 'date') +
        '</div></div>';

      if (res.summary) {
        const s = res.summary;
        h += '<div class="grid c4">' +
          kpi('Checklist complete', s.completion_pct.toFixed(1) + '%', s.completion_pct >= 100 ? 'accent' : '') +
          kpi('Exceptions', s.exceptions_count, s.exceptions_count ? 'alert' : '') +
          kpi('Findings open', s.findings_open, s.findings_open ? 'alert' : '') +
          kpi('Control gaps', s.control_gaps_count, s.control_gaps_count ? 'alert' : '') +
          '</div>';
      }

      h += '<div class="card"><h3>Risk-based audit planning</h3><p class="card-note">Score each auditable process on likelihood x impact (1-5 each) to build the risk universe that drives the audit plan — highest score leads.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Process / area</th><th>Likelihood (1-5)</th><th>Impact (1-5)</th><th>Score</th><th>Priority</th><th>Notes</th><th></th></tr></thead><tbody>';
      p.risk_universe.forEach((r, i) => {
        const score = (Number(r.likelihood) || 0) * (Number(r.impact) || 0);
        const priority = score >= 15 ? 'High' : (score >= 8 ? 'Medium' : 'Low');
        h += '<tr><td><input data-ru="' + i + '" data-k="process" value="' + esc(r.process || '') + '"></td>' +
          '<td><input type="number" min="1" max="5" data-ru="' + i + '" data-k="likelihood" value="' + esc(r.likelihood || '') + '"></td>' +
          '<td><input type="number" min="1" max="5" data-ru="' + i + '" data-k="impact" value="' + esc(r.impact || '') + '"></td>' +
          '<td>' + score + '</td><td><span class="tag' + (priority === 'High' ? ' alert' : '') + '">' + priority + '</span></td>' +
          '<td><input data-ru="' + i + '" data-k="notes" value="' + esc(r.notes || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-ru="' + i + '" title="Remove">Remove</button></td></tr>';
      });
      h += '</tbody></table></div><button class="btn btn-small" id="ia-add-ru">Add a process to the risk universe</button></div>';

      h += '<div class="card"><h3>Process risk &amp; control matrix (RACM)</h3><p class="card-note">One row per risk-control pair. Control Gap Analysis below is derived automatically from any row marked Partially Effective or Ineffective — nothing to enter twice.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Process</th><th>Risk description</th><th>Control description</th><th>Control type</th><th>Frequency</th><th>Owner</th><th>Design</th><th>Operating</th><th>Testing result</th><th></th></tr></thead><tbody>';
      p.racm.forEach((r, i) => {
        h += '<tr><td><input data-racm="' + i + '" data-k="process" value="' + esc(r.process || '') + '"></td>' +
          '<td><input data-racm="' + i + '" data-k="risk_description" value="' + esc(r.risk_description || '') + '"></td>' +
          '<td><input data-racm="' + i + '" data-k="control_description" value="' + esc(r.control_description || '') + '"></td>' +
          '<td><input data-racm="' + i + '" data-k="control_type" value="' + esc(r.control_type || '') + '" placeholder="Preventive / Detective"></td>' +
          '<td><input data-racm="' + i + '" data-k="control_frequency" value="' + esc(r.control_frequency || '') + '"></td>' +
          '<td><input data-racm="' + i + '" data-k="control_owner" value="' + esc(r.control_owner || '') + '"></td>' +
          '<td><select data-racm="' + i + '" data-k="design_effectiveness">' + EFFECTIVENESS_OPTS.map(o => '<option value="' + o + '"' + ((r.design_effectiveness || 'Not Assessed') === o ? ' selected' : '') + '>' + o + '</option>').join('') + '</select></td>' +
          '<td><select data-racm="' + i + '" data-k="operating_effectiveness">' + EFFECTIVENESS_OPTS.map(o => '<option value="' + o + '"' + ((r.operating_effectiveness || 'Not Assessed') === o ? ' selected' : '') + '>' + o + '</option>').join('') + '</select></td>' +
          '<td><input data-racm="' + i + '" data-k="testing_result" value="' + esc(r.testing_result || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-racm="' + i + '" title="Remove">Remove</button></td></tr>';
      });
      h += '</tbody></table></div><button class="btn btn-small" id="ia-add-racm">Add a risk-control row</button></div>';

      if ((res.control_gaps || []).length) {
        h += '<div class="card"><h3>Control gap analysis</h3><p class="card-note">Rows from the RACM above where design or operating effectiveness is not Effective.</p>' +
          '<div class="table-wrap"><table><thead><tr><th>Process</th><th>Risk</th><th>Control</th><th>Design</th><th>Operating</th></tr></thead><tbody>' +
          res.control_gaps.map(g => '<tr><td>' + esc(g.process || '') + '</td><td>' + esc(g.risk_description || '') + '</td>' +
            '<td>' + esc(g.control_description || '') + '</td><td><span class="tag alert">' + esc(g.design_effectiveness) + '</span></td>' +
            '<td><span class="tag alert">' + esc(g.operating_effectiveness) + '</span></td></tr>').join('') + '</tbody></table></div></div>';
      }

      h += '<div class="card"><h3>Checklist</h3><p class="card-note">Filtered to criteria applicable to this entity type, plus everything generic. Add a criterion the bank is missing directly from an area below — it becomes available for every future audit, not just this one.</p></div>';
      (res.areas || []).forEach((a, ai) => {
        const testedN = a.criteria.filter(c => c.status !== 'not_tested').length;
        h += '<details class="card" style="margin-top:10px"><summary><b>' + esc(a.area) + '</b> <span class="hint">' +
          testedN + ' / ' + a.criteria.length + ' tested</span></summary>' +
          '<div class="table-wrap" style="margin-top:12px"><table><thead><tr><th>Criterion</th><th>Status</th><th>Evidence</th><th>Remark</th></tr></thead><tbody>';
        a.criteria.forEach(c => {
          h += '<tr><td>' + esc(c.criterion) + '</td>' +
            '<td><select data-crit="' + c.id + '" data-k="status">' +
            STATUS_OPTS.map(o => '<option value="' + o.value + '"' + (c.status === o.value ? ' selected' : '') + '>' + o.label + '</option>').join('') + '</select></td>' +
            '<td><input data-crit="' + c.id + '" data-k="evidence" value="' + esc(c.evidence || '') + '"></td>' +
            '<td><input data-crit="' + c.id + '" data-k="remark" value="' + esc(c.remark || '') + '"></td></tr>';
        });
        h += '</tbody></table></div>' +
          '<div class="grid c3" style="margin-top:10px"><div class="field" style="grid-column:1/-1"><label>Add a criterion to ' + esc(a.area) + '</label>' +
          '<input id="ia-new-crit-' + ai + '" placeholder="New checklist item for this area"></div>' +
          '<div class="field"><label>&nbsp;</label><button class="btn btn-small" data-add-crit-area="' + esc(a.area) + '" data-input="ia-new-crit-' + ai + '">Add</button></div></div>' +
          '</details>';
      });

      h += '<div class="card"><h3>Fraud risk assessment</h3><p class="card-note">Qualitative fraud-triangle factors (ISA/NSA 240 style). For a data-driven check across a client\'s actual transactions — Benford\'s Law, duplicate payments, journal-entry scoring, and more — use the Forensic Audit module.</p><div class="grid c2" id="ia-fraud-risk">' +
        field('Incentives / pressures observed', 'incentives_pressures', p.fraud_risk, 'text') +
        field('Opportunities for fraud (control weaknesses)', 'opportunities', p.fraud_risk, 'text') +
        field('Attitudes / rationalization risk', 'attitudes_rationalization', p.fraud_risk, 'text') +
        field('Management override of controls — notes', 'override_notes', p.fraud_risk, 'text') +
        '<div class="field" style="grid-column:1/-1"><label>Assessment conclusion</label><textarea data-key="conclusion" rows="2">' + esc(p.fraud_risk.conclusion || '') + '</textarea></div>' +
        '</div><div style="margin-top:10px"><button class="btn btn-small" id="ia-goto-fa">Run data-driven fraud detectors in Forensic Audit</button></div></div>';

      h += '<div class="card"><h3>Process walkthroughs</h3><div class="table-wrap"><table><thead><tr><th>Process</th><th>Narrative</th><th>Date</th><th>Performed by</th><th>Conclusion</th><th></th></tr></thead><tbody>';
      p.walkthroughs.forEach((w, i) => {
        h += '<tr><td><input data-wt="' + i + '" data-k="process" value="' + esc(w.process || '') + '"></td>' +
          '<td><input data-wt="' + i + '" data-k="narrative" value="' + esc(w.narrative || '') + '"></td>' +
          '<td><input type="date" data-wt="' + i + '" data-k="walkthrough_date" value="' + esc(w.walkthrough_date || '') + '"></td>' +
          '<td><input data-wt="' + i + '" data-k="performed_by" value="' + esc(w.performed_by || '') + '"></td>' +
          '<td><input data-wt="' + i + '" data-k="conclusion" value="' + esc(w.conclusion || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-wt="' + i + '" title="Remove">Remove</button></td></tr>';
      });
      h += '</tbody></table></div><button class="btn btn-small" id="ia-add-wt">Add a walkthrough</button></div>';

      h += '<div class="card"><h3>Findings register</h3><p class="card-note">Each finding can be classified and, once raised, pushed straight into Practice Management as a tracked corrective-action assignment.</p><div class="table-wrap"><table><thead><tr>' +
        '<th>Area</th><th>Category</th><th>Observation</th><th>Risk</th><th>Recommendation</th><th>Management response</th>' +
        '<th>Responsible</th><th>Target date</th><th>Status</th><th></th></tr></thead><tbody>';
      p.findings.forEach((f, i) => {
        h += '<tr>' +
          '<td><input data-find="' + i + '" data-k="area" value="' + esc(f.area || '') + '"></td>' +
          '<td><select data-find="' + i + '" data-k="category">' +
          [''].concat(FINDING_CATEGORIES).map(c => '<option value="' + esc(c) + '"' + ((f.category || '') === c ? ' selected' : '') + '>' + (c || '— select —') + '</option>').join('') + '</select></td>' +
          '<td><input data-find="' + i + '" data-k="observation" value="' + esc(f.observation || '') + '"></td>' +
          '<td><select data-find="' + i + '" data-k="risk_rating">' +
          ['high', 'medium', 'low'].map(r => '<option value="' + r + '"' + (f.risk_rating === r ? ' selected' : '') + '>' + r.charAt(0).toUpperCase() + r.slice(1) + '</option>').join('') + '</select></td>' +
          '<td><input data-find="' + i + '" data-k="recommendation" value="' + esc(f.recommendation || '') + '"></td>' +
          '<td><input data-find="' + i + '" data-k="management_response" value="' + esc(f.management_response || '') + '"></td>' +
          '<td><input data-find="' + i + '" data-k="responsible_person" value="' + esc(f.responsible_person || '') + '"></td>' +
          '<td><input type="date" data-find="' + i + '" data-k="target_date" value="' + esc(f.target_date || '') + '"></td>' +
          '<td><select data-find="' + i + '" data-k="status">' +
          ['open', 'closed'].map(s => '<option value="' + s + '"' + (f.status === s ? ' selected' : '') + '>' + (s === 'open' ? 'Open' : 'Closed') + '</option>').join('') + '</select></td>' +
          '<td><button class="btn btn-small" data-send-pm="' + i + '" title="Create a tracked corrective-action assignment in Practice Management">To PM</button> ' +
          '<button class="btn btn-small btn-danger" data-del-find="' + i + '" title="Remove this row">Remove</button></td></tr>';
      });
      h += '</tbody></table></div><button class="btn btn-small" id="ia-add-find">Add a finding</button></div>';

      h += '<div class="card"><h3>Follow-up audit</h3><p class="card-note">Track whether raised findings\' corrective actions were actually implemented on a later visit.</p><div class="table-wrap"><table><thead><tr><th>Finding reference</th><th>Follow-up date</th><th>Action taken</th><th>Status</th><th>Remarks</th><th></th></tr></thead><tbody>';
      p.follow_ups.forEach((fu, i) => {
        h += '<tr><td><input data-fu="' + i + '" data-k="finding_ref" value="' + esc(fu.finding_ref || '') + '" placeholder="e.g. area / observation"></td>' +
          '<td><input type="date" data-fu="' + i + '" data-k="follow_up_date" value="' + esc(fu.follow_up_date || '') + '"></td>' +
          '<td><input data-fu="' + i + '" data-k="action_taken" value="' + esc(fu.action_taken || '') + '"></td>' +
          '<td><select data-fu="' + i + '" data-k="status">' + FOLLOWUP_STATUS.map(s => '<option value="' + s + '"' + ((fu.status || FOLLOWUP_STATUS[0]) === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>' +
          '<td><input data-fu="' + i + '" data-k="remarks" value="' + esc(fu.remarks || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-fu="' + i + '" title="Remove">Remove</button></td></tr>';
      });
      h += '</tbody></table></div><button class="btn btn-small" id="ia-add-fu">Add a follow-up</button></div>';

      h += '<div class="card"><h3>Conclusion</h3><div class="grid c3" id="ia-conclusion">' +
        field('Overall rating', 'overall_rating', p.conclusion, 'select', { options: [
          { value: '', label: '— select —' }, { value: 'Satisfactory', label: 'Satisfactory' },
          { value: 'Needs improvement', label: 'Needs improvement' }, { value: 'Unsatisfactory', label: 'Unsatisfactory' }] }) +
        '<div class="field" style="grid-column:2/-1"><label>Summary</label><textarea data-key="summary" rows="2">' + esc(p.conclusion.summary || '') + '</textarea></div>' +
        '</div></div>';

      h += '<div class="card"><h3>Report signatory</h3><p class="card-note">Printed on the Internal Audit Report — space is left for the physical signature and firm stamp.</p><div class="grid c3" id="ia-signatory">' +
        field('Signatory name', 'name', p.signatory, 'text') +
        field('Designation', 'designation', p.signatory, 'select', { options: [
          { value: 'ca', label: 'Chartered Accountant (CA)' }, { value: 'ra', label: 'Registered Auditor (RA)' }] }) +
        field('Membership no.', 'membership_no', p.signatory, 'text') +
        field('Firm name', 'firm_name', p.signatory, 'text') +
        field('Place', 'place', p.signatory, 'text') +
        field('Date', 'date', p.signatory, 'date') +
        field('UDIN (if applicable)', 'udin', p.signatory, 'text') +
        '</div></div>';

      return h;
    },
    wire(root, file, rerender) {
      const p = file.payload;
      wireInto(root, 'ia-entity', p.entity, rerender);
      wireInto(root, 'ia-planning', p.planning, rerender);
      wireInto(root, 'ia-fraud-risk', p.fraud_risk, rerender);
      wireInto(root, 'ia-conclusion', p.conclusion, rerender);
      wireInto(root, 'ia-signatory', p.signatory, rerender);

      const gotoFaBtn = root.querySelector('#ia-goto-fa');
      if (gotoFaBtn) gotoFaBtn.addEventListener('click', () => {
        if (file.clientId) localStorage.setItem('ca.client.fa', file.clientId);
        location.hash = '#/fa';
      });

      root.querySelectorAll('[data-ru]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.ru, k = e.target.dataset.k;
        p.risk_universe[i][k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        rerender();
      }));
      root.querySelectorAll('[data-del-ru]').forEach(el => el.addEventListener('click', e => {
        p.risk_universe.splice(+e.target.dataset.delRu, 1); rerender();
      }));
      const addRu = root.querySelector('#ia-add-ru');
      if (addRu) addRu.addEventListener('click', () => {
        p.risk_universe.push({ process: '', likelihood: 3, impact: 3, notes: '' });
        rerender();
      });

      root.querySelectorAll('[data-racm]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.racm, k = e.target.dataset.k;
        p.racm[i][k] = e.target.value;
        rerender();
      }));
      root.querySelectorAll('[data-del-racm]').forEach(el => el.addEventListener('click', e => {
        p.racm.splice(+e.target.dataset.delRacm, 1); rerender();
      }));
      const addRacm = root.querySelector('#ia-add-racm');
      if (addRacm) addRacm.addEventListener('click', () => {
        p.racm.push({ process: '', risk_description: '', control_description: '', control_type: '', control_frequency: '',
          control_owner: '', design_effectiveness: 'Not Assessed', operating_effectiveness: 'Not Assessed', testing_result: '' });
        rerender();
      });

      root.querySelectorAll('[data-wt]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.wt, k = e.target.dataset.k;
        p.walkthroughs[i][k] = e.target.value;
        rerender();
      }));
      root.querySelectorAll('[data-del-wt]').forEach(el => el.addEventListener('click', e => {
        p.walkthroughs.splice(+e.target.dataset.delWt, 1); rerender();
      }));
      const addWt = root.querySelector('#ia-add-wt');
      if (addWt) addWt.addEventListener('click', () => {
        p.walkthroughs.push({ process: '', narrative: '', walkthrough_date: '', performed_by: '', conclusion: '' });
        rerender();
      });

      root.querySelectorAll('[data-fu]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.fu, k = e.target.dataset.k;
        p.follow_ups[i][k] = e.target.value;
        rerender();
      }));
      root.querySelectorAll('[data-del-fu]').forEach(el => el.addEventListener('click', e => {
        p.follow_ups.splice(+e.target.dataset.delFu, 1); rerender();
      }));
      const addFu = root.querySelector('#ia-add-fu');
      if (addFu) addFu.addEventListener('click', () => {
        p.follow_ups.push({ finding_ref: '', follow_up_date: '', action_taken: '', status: 'Not started', remarks: '' });
        rerender();
      });

      const addTypeBtn = root.querySelector('#ia-add-entity-type');
      if (addTypeBtn) addTypeBtn.addEventListener('click', async () => {
        const name = prompt('New entity type name');
        if (!name) return;
        if (!state.online || state.offlineMode) { toast('Adding an entity type needs a connection', true); return; }
        try {
          const t = await api('/api/audit/entity-types', { method: 'POST', body: { name } });
          state.boot.audit_entity_types = state.boot.audit_entity_types || [];
          state.boot.audit_entity_types.push(t);
          await idbPut('kv', state.boot, 'boot');
          p.entity.entity_type = t.name;
          toast('Added ' + t.name);
          rerender();
        } catch (e) { toast(e.message, true); }
      });

      root.querySelectorAll('[data-crit]').forEach(el => el.addEventListener('change', e => {
        const id = e.target.dataset.crit, k = e.target.dataset.k;
        p.criteria_responses[id] = p.criteria_responses[id] || {};
        p.criteria_responses[id][k] = e.target.value;
        rerender();
      }));

      root.querySelectorAll('[data-add-crit-area]').forEach(el => el.addEventListener('click', async e => {
        const area = e.target.dataset.addCritArea;
        const inputEl = root.querySelector('#' + e.target.dataset.input);
        const text = inputEl ? inputEl.value.trim() : '';
        if (!text) { toast('Type the new criterion first', true); return; }
        if (!state.online || state.offlineMode) { toast('Adding a criterion needs a connection', true); return; }
        try {
          await api('/api/audit/criteria', { method: 'POST', body: { area, criterion: text, applies_to: 'all' } });
          const boot = await api('/api/bootstrap');
          state.boot.audit_criteria = boot.audit_criteria;
          await idbPut('kv', state.boot, 'boot');
          toast('Added to ' + area);
          rerender();
        } catch (err) { toast(err.message, true); }
      }));

      root.querySelectorAll('[data-find]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.find, k = e.target.dataset.k;
        p.findings[i][k] = e.target.value;
        rerender();
      }));
      root.querySelectorAll('[data-del-find]').forEach(el => el.addEventListener('click', e => {
        p.findings.splice(+e.target.dataset.delFind, 1); rerender();
      }));
      const addFind = root.querySelector('#ia-add-find');
      if (addFind) addFind.addEventListener('click', () => {
        p.findings.push({ area: '', category: '', observation: '', risk_rating: 'medium', recommendation: '',
          management_response: '', responsible_person: '', target_date: '', status: 'open' });
        rerender();
      });
      root.querySelectorAll('[data-send-pm]').forEach(el => el.addEventListener('click', async e => {
        if (!file.clientId) { toast('This workfile has no client attached', true); return; }
        if (!state.online || state.offlineMode) { toast('Sending to Practice Management needs a connection', true); return; }
        const f = p.findings[+e.target.dataset.sendPm];
        const priority = f.risk_rating === 'high' ? 'high' : (f.risk_rating === 'low' ? 'low' : 'normal');
        try {
          await api('/api/pm/clients/' + file.clientId + '/assignments', { method: 'POST', body: {
            task: 'Corrective action — ' + (f.area ? f.area + ': ' : '') + (f.observation || '(untitled finding)'),
            due_date: f.target_date || '', priority, engagement_id: null } });
          toast('Sent to Practice Management assignments');
        } catch (err) { toast(err.message, true); }
      }));
    },
    tabulate: res => {
      const tables = [];
      if (res.summary) {
        tables.push({
          title: 'Internal audit summary', headers: ['Particulars', 'Value'],
          rows: [
            ['Criteria tested', res.summary.tested_criteria + ' of ' + res.summary.total_criteria],
            ['Completion %', res.summary.completion_pct],
            ['Exceptions', res.summary.exceptions_count],
            ['Findings total', res.summary.findings_total],
            ['Findings open', res.summary.findings_open],
            ['Control gaps', res.summary.control_gaps_count],
          ]
        });
      }

      const checklistRows = [];
      (res.areas || []).forEach(a => (a.criteria || []).forEach(c =>
        checklistRows.push([a.area, c.criterion, c.status, c.evidence || '', c.remark || ''])));
      tables.push({ title: 'Checklist', headers: ['Area', 'Criterion', 'Status', 'Evidence', 'Remark'], rows: checklistRows });

      tables.push({
        title: 'Findings',
        headers: ['Area', 'Category', 'Observation', 'Risk', 'Recommendation', 'Management response', 'Responsible', 'Target date', 'Status'],
        rows: (res.findings || []).map(f => [f.area || '', f.category || '', f.observation || '', (f.risk_rating || '').toUpperCase(),
          f.recommendation || '', f.management_response || '', f.responsible_person || '', f.target_date || '', (f.status || 'open').toUpperCase()])
      });

      if (res.risk_universe && res.risk_universe.length) {
        tables.push({
          title: 'Risk universe', headers: ['Process / area', 'Likelihood', 'Impact', 'Score', 'Priority', 'Notes'],
          rows: res.risk_universe.map(ru => [ru.process, ru.likelihood, ru.impact, ru.score, ru.priority, ru.notes || ''])
        });
      }
      if (res.racm && res.racm.length) {
        tables.push({
          title: 'RACM',
          headers: ['Process', 'Risk description', 'Control description', 'Control type', 'Frequency', 'Owner', 'Design', 'Operating', 'Testing result'],
          rows: res.racm.map(row => [row.process || '', row.risk_description || '', row.control_description || '', row.control_type || '',
            row.control_frequency || '', row.control_owner || '', row.design_effectiveness || '', row.operating_effectiveness || '', row.testing_result || ''])
        });
      }
      if (res.walkthroughs && res.walkthroughs.length) {
        tables.push({
          title: 'Walkthroughs', headers: ['Process', 'Narrative', 'Date', 'Performed by', 'Conclusion'],
          rows: res.walkthroughs.map(w => [w.process || '', w.narrative || '', w.walkthrough_date || '', w.performed_by || '', w.conclusion || ''])
        });
      }
      if (res.follow_ups && res.follow_ups.length) {
        tables.push({
          title: 'Follow-up', headers: ['Finding reference', 'Date', 'Action taken', 'Status', 'Remarks'],
          rows: res.follow_ups.map(fu => [fu.finding_ref || '', fu.follow_up_date || '', fu.action_taken || '', fu.status || '', fu.remarks || ''])
        });
      }
      return tables;
    }
  };

  MODULES.audit_report = {
    code: 'AR', name: "Auditor's Report", group: 'Audit',
    blurb: "Independent Auditor's Report generator — any combination of opinion type, Key Audit Matters, Emphasis of Matter and Other Matter, assembled in the correct order, ready to print on letterhead with the signatory block and UDIN.",
    seed: () => ({
      entity: { name: '' },
      opinion_type: 'unqualified',
      opinion_paragraph: 'In our opinion, the accompanying financial statements present fairly, in all material respects, the financial position of [Entity] as at [date], and its financial performance and cash flows for the year then ended in accordance with Nepal Financial Reporting Standards (NFRS).',
      basis_paragraph: "We conducted our audit in accordance with Nepal Standards on Auditing (NSA). Our responsibilities under those standards are further described in the Auditor's Responsibilities section below. We are independent of the entity in accordance with the ICAN Handbook of Code of Ethics, and we have fulfilled our other ethical responsibilities in accordance with these requirements. We believe that the audit evidence we have obtained is sufficient and appropriate to provide a basis for our opinion.",
      eom_paragraph: '', other_matter_paragraph: '', kam_paragraphs: [],
      responsibilities_management: '', responsibilities_auditor: '',
      signatory: { name: '', designation: 'ca', membership_no: '', firm_name: '', place: '', date: '', udin: '' },
    }),
    view(file, res) {
      const p = file.payload;
      p.entity = p.entity || {}; p.signatory = p.signatory || {}; p.kam_paragraphs = p.kam_paragraphs || [];
      const opinionOpts = Object.keys(CAEngines.OPINION_TYPES).map(k => ({ value: k, label: CAEngines.OPINION_TYPES[k] }));

      let h = '<div class="card"><h3>Entity</h3><div class="grid c2" id="ar-entity">' +
        field('Client / entity name', 'name', p.entity, 'text') +
        '</div></div>';

      h += '<div class="card"><h3>Opinion</h3><div class="grid c3" id="ar-opinion-type">' +
        field('Opinion type', 'opinion_type', p, 'select', { options: opinionOpts }) +
        '</div>' +
        '<div class="field" style="margin-top:10px"><label>Opinion paragraph</label><textarea id="ar-opinion-p" rows="4">' + esc(p.opinion_paragraph || '') + '</textarea></div>' +
        (p.opinion_type !== 'unqualified' ? '<div class="card-note">A qualified/adverse/disclaimer opinion needs this paragraph rewritten for the specific reason — the default text above is an unqualified starting point only, not this engagement\'s actual wording.</div>' : '') +
        '<div class="field" style="margin-top:10px"><label>Basis for opinion</label><textarea id="ar-basis-p" rows="4">' + esc(p.basis_paragraph || '') + '</textarea>' +
        (p.opinion_type !== 'unqualified' ? '<span class="hint">Required — state the specific matter giving rise to the ' + esc((CAEngines.OPINION_TYPES[p.opinion_type] || '').toLowerCase()) + '.</span>' : '') +
        '</div></div>';

      h += '<div class="card"><h3>Key Audit Matters (optional)</h3><div class="table-wrap"><table><thead><tr><th>Heading</th><th>Description</th><th></th></tr></thead><tbody>';
      p.kam_paragraphs.forEach((k, i) => {
        h += '<tr><td><input data-kam="' + i + '" data-k="heading" value="' + esc(k.heading || '') + '"></td>' +
          '<td><input data-kam="' + i + '" data-k="text" value="' + esc(k.text || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-kam="' + i + '" title="Remove this row">Remove</button></td></tr>';
      });
      h += '</tbody></table></div><button class="btn btn-small" id="ar-add-kam">Add a key audit matter</button></div>';

      h += '<div class="card"><h3>Emphasis of Matter (optional)</h3><div class="field"><label>&nbsp;</label><textarea id="ar-eom" rows="3">' + esc(p.eom_paragraph || '') + '</textarea></div></div>';
      h += '<div class="card"><h3>Other Matter (optional)</h3><div class="field"><label>&nbsp;</label><textarea id="ar-other" rows="3">' + esc(p.other_matter_paragraph || '') + '</textarea></div></div>';

      h += '<div class="card"><h3>Responsibilities paragraphs</h3><p class="card-note">Pre-filled with standard NSA 700 wording — edit if this engagement needs different language.</p>' +
        '<div class="field"><label>Management\'s / those-charged-with-governance\'s responsibilities</label><textarea id="ar-resp-mgmt" rows="4">' + esc(p.responsibilities_management || '') + '</textarea></div>' +
        '<div class="field" style="margin-top:10px"><label>Auditor\'s responsibilities</label><textarea id="ar-resp-audit" rows="4">' + esc(p.responsibilities_auditor || '') + '</textarea></div></div>';

      h += '<div class="card"><h3>Signatory</h3><div class="grid c3" id="ar-signatory">' +
        field('Signatory name', 'name', p.signatory, 'text') +
        field('Designation', 'designation', p.signatory, 'select', { options: [
          { value: 'ca', label: 'Chartered Accountant (CA)' }, { value: 'ra', label: 'Registered Auditor (RA)' }] }) +
        field('Membership no.', 'membership_no', p.signatory, 'text') +
        field('Firm name', 'firm_name', p.signatory, 'text') +
        field('Place', 'place', p.signatory, 'text') +
        field('Date', 'date', p.signatory, 'date') +
        field('UDIN (if applicable)', 'udin', p.signatory, 'text') +
        '</div></div>';

      if (res.opinion_type_label) {
        h += '<div class="card"><h3>Report structure preview</h3><p class="card-note">' +
          esc(res.opinion_type_label) +
          (res.basis_paragraph ? ' → Basis for ' + esc(res.opinion_type_label) : '') +
          (res.has_kam ? ' → Key Audit Matters' : '') +
          (res.has_eom ? ' → Emphasis of Matter' : '') +
          (res.has_other_matter ? ' → Other Matter' : '') +
          ' → Responsibilities → Signatory.</p></div>';
      }

      return h;
    },
    wire(root, file, rerender) {
      const p = file.payload;
      wireInto(root, 'ar-entity', p.entity, rerender);
      wireInto(root, 'ar-opinion-type', p, rerender);
      wireInto(root, 'ar-signatory', p.signatory, rerender);

      const bindTextarea = (id, key) => {
        const el = root.querySelector('#' + id);
        if (el) el.addEventListener('change', e => { p[key] = e.target.value; rerender(); });
      };
      bindTextarea('ar-opinion-p', 'opinion_paragraph');
      bindTextarea('ar-basis-p', 'basis_paragraph');
      bindTextarea('ar-eom', 'eom_paragraph');
      bindTextarea('ar-other', 'other_matter_paragraph');
      bindTextarea('ar-resp-mgmt', 'responsibilities_management');
      bindTextarea('ar-resp-audit', 'responsibilities_auditor');

      root.querySelectorAll('[data-kam]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.kam, k = e.target.dataset.k;
        p.kam_paragraphs[i][k] = e.target.value; rerender();
      }));
      root.querySelectorAll('[data-del-kam]').forEach(el => el.addEventListener('click', e => {
        p.kam_paragraphs.splice(+e.target.dataset.delKam, 1); rerender();
      }));
      const addKam = root.querySelector('#ar-add-kam');
      if (addKam) addKam.addEventListener('click', () => { p.kam_paragraphs.push({ heading: '', text: '' }); rerender(); });
    },
    tabulate: res => {
      const sig = res.signatory || {};
      return [{
        title: "Independent auditor's report — summary", headers: ['Particulars', 'Value'],
        rows: [
          ['Opinion type', res.opinion_type_label || ''],
          ['Opinion paragraph', res.opinion_paragraph || ''],
          ['Basis paragraph', res.basis_paragraph || ''],
          ['Emphasis of Matter', res.has_eom ? (res.eom_paragraph || '') : ''],
          ['Other Matter', res.has_other_matter ? (res.other_matter_paragraph || '') : ''],
          ['Signatory', sig.name || ''], ['Designation', sig.designation || ''],
          ['Firm', sig.firm_name || ''], ['Place', sig.place || ''],
          ['Date', sig.date || ''], ['UDIN', sig.udin || ''],
        ]
      }];
    }
  };

  MODULES.stat_audit = {
    code: 'SA', name: 'Statutory Audit', group: 'Audit', needsAuditCatalog: true,
    blurb: 'Phase 2 of the audit module — materiality, a judgmental sample-size estimate, and an inherent/control risk assessment by area, layered on the same criterion bank and findings register as Internal Audit. Once fieldwork concludes, use the Auditor\'s Report module to issue the opinion.',
    seed: () => ({
      entity: { name: '', entity_type: '', other_entity_type_label: '', period_label: '' },
      materiality: { benchmark: '', benchmark_value: 0, materiality_pct: 1, pm_pct: 75, ct_pct: 5 },
      sample_size: { population_value: 0, population_count: 0, tolerable_misstatement: '', expected_misstatement: 0, risk_level: 'medium' },
      risk_assessment: {},
      criteria_responses: {},
      findings: [],
      conclusion: { overall_rating: '', summary: '' },
      signatory: { name: '', designation: 'ca', membership_no: '', firm_name: '', place: '', date: '', udin: '' },
    }),
    view(file, res) {
      const p = file.payload;
      p.entity = p.entity || {}; p.materiality = p.materiality || {}; p.sample_size = p.sample_size || {};
      p.risk_assessment = p.risk_assessment || {}; p.criteria_responses = p.criteria_responses || {};
      p.findings = p.findings || []; p.conclusion = p.conclusion || {}; p.signatory = p.signatory || {};
      const names = ((state.boot && state.boot.audit_entity_types) || []).map(t => t.name);
      if (!names.includes('Other')) names.push('Other');
      const entityTypeOptions = [{ value: '', label: '— select —' }].concat(names.map(n => ({ value: n, label: n })));
      const RISK_OPTS = [{ value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }];
      const RISK_TAG = { low: 'tag tag-green', medium: 'tag tag-amber', high: 'tag tag-red' };

      let h = '<div class="card"><h3>Engagement setup</h3><div class="grid c3" id="sa-entity">' +
        field('Client / entity name', 'name', p.entity, 'text') +
        field('Entity type', 'entity_type', p.entity, 'select', { options: entityTypeOptions }) +
        (p.entity.entity_type === 'Other' ? field('Specify entity type', 'other_entity_type_label', p.entity, 'text') : '<div></div>') +
        field('Period', 'period_label', p.entity, 'text', { placeholder: 'FY ' + state.fy }) +
        '</div></div>';

      h += '<div class="card"><h3>Materiality</h3><div class="grid c3" id="sa-materiality">' +
        field('Benchmark', 'benchmark', p.materiality, 'text', { placeholder: 'e.g. Total revenue, PBT, Total assets' }) +
        field('Benchmark value', 'benchmark_value', p.materiality, 'number') +
        field('Materiality %', 'materiality_pct', p.materiality, 'number', { step: '0.01' }) +
        field('Performance materiality — % of overall', 'pm_pct', p.materiality, 'number') +
        field('Clearly trivial — % of performance materiality', 'ct_pct', p.materiality, 'number') +
        '</div>';
      if (res.materiality) {
        const m = res.materiality;
        h += '<div class="grid c3" style="margin-top:10px">' +
          kpi('Overall materiality', money(m.overall_materiality), 'accent') +
          kpi('Performance materiality', money(m.performance_materiality)) +
          kpi('Clearly trivial', money(m.clearly_trivial)) +
          '</div>';
      }
      h += '</div>';

      h += '<div class="card"><h3>Sample size (planning estimate)</h3><p class="card-note">A judgmental monetary-unit-style estimate for planning — not a substitute for a statistical sampling standard.</p><div class="grid c3" id="sa-sample">' +
        field('Population value', 'population_value', p.sample_size, 'number') +
        field('Population count', 'population_count', p.sample_size, 'number') +
        field('Tolerable misstatement (blank = performance materiality)', 'tolerable_misstatement', p.sample_size, 'number') +
        field('Expected misstatement', 'expected_misstatement', p.sample_size, 'number') +
        field('Risk level', 'risk_level', p.sample_size, 'select', { options: RISK_OPTS }) +
        '</div>';
      if (res.sample_size) {
        h += '<div class="grid c3" style="margin-top:10px">' +
          kpi('Sampling interval', money(res.sample_size.sampling_interval)) +
          kpi('Sample size', res.sample_size.sample_size, 'accent') +
          '</div>';
      }
      h += '</div>';

      h += '<div class="card"><h3>Risk assessment by area</h3><div class="table-wrap"><table><thead><tr>' +
        '<th>Area</th><th>Inherent</th><th>Control</th><th>Combined</th><th>Detection risk needed</th><th>Rationale</th></tr></thead><tbody>';
      (res.risk_assessment || []).forEach(ra => {
        h += '<tr><td>' + esc(ra.area) + '</td>' +
          '<td><select data-risk="' + esc(ra.area) + '" data-k="inherent">' +
          RISK_OPTS.map(o => '<option value="' + o.value + '"' + (ra.inherent === o.value ? ' selected' : '') + '>' + o.label + '</option>').join('') + '</select></td>' +
          '<td><select data-risk="' + esc(ra.area) + '" data-k="control">' +
          RISK_OPTS.map(o => '<option value="' + o.value + '"' + (ra.control === o.value ? ' selected' : '') + '>' + o.label + '</option>').join('') + '</select></td>' +
          '<td><span class="' + (RISK_TAG[ra.combined_risk] || 'tag') + '">' + ra.combined_risk.toUpperCase() + '</span></td>' +
          '<td><span class="' + (RISK_TAG[ra.detection_risk_required] || 'tag') + '">' + ra.detection_risk_required.toUpperCase() + '</span></td>' +
          '<td><input data-risk="' + esc(ra.area) + '" data-k="rationale" value="' + esc(ra.rationale || '') + '"></td></tr>';
      });
      h += '</tbody></table></div></div>';

      h += '<div class="card"><h3>Audit program</h3><p class="card-note">Same criterion bank as Internal Audit, extended with the procedure, sample basis and working-paper fields a statutory engagement needs.</p></div>';
      (res.areas || []).forEach((a, ai) => {
        const testedN = a.criteria.filter(c => c.status !== 'not_tested').length;
        h += '<details class="card" style="margin-top:10px"><summary><b>' + esc(a.area) + '</b> <span class="hint">' +
          testedN + ' / ' + a.criteria.length + ' tested</span></summary>' +
          '<div class="table-wrap" style="margin-top:12px"><table><thead><tr><th>Criterion</th><th>Procedure</th><th>Sample basis</th>' +
          '<th>Status</th><th>Evidence</th><th>W/P ref</th><th>Performed by</th><th>Result</th></tr></thead><tbody>';
        a.criteria.forEach(c => {
          h += '<tr><td>' + esc(c.criterion) + '</td>' +
            '<td><input data-prog="' + c.id + '" data-k="procedure" value="' + esc(c.procedure || '') + '"></td>' +
            '<td><input data-prog="' + c.id + '" data-k="sample_basis" value="' + esc(c.sample_basis || '') + '"></td>' +
            '<td><select data-prog="' + c.id + '" data-k="status">' +
            [['not_tested', 'Not tested'], ['satisfactory', 'Satisfactory'], ['exception', 'Exception'], ['not_applicable', 'N/A']]
              .map(o => '<option value="' + o[0] + '"' + (c.status === o[0] ? ' selected' : '') + '>' + o[1] + '</option>').join('') + '</select></td>' +
            '<td><input data-prog="' + c.id + '" data-k="evidence" value="' + esc(c.evidence || '') + '"></td>' +
            '<td><input data-prog="' + c.id + '" data-k="wp_ref" value="' + esc(c.wp_ref || '') + '"></td>' +
            '<td><input data-prog="' + c.id + '" data-k="performed_by" value="' + esc(c.performed_by || '') + '"></td>' +
            '<td><input data-prog="' + c.id + '" data-k="result" value="' + esc(c.result || '') + '"></td></tr>';
        });
        h += '</tbody></table></div></details>';
      });

      h += '<div class="card"><h3>Findings register</h3><div class="table-wrap"><table><thead><tr>' +
        '<th>Area</th><th>Observation</th><th>Risk</th><th>Recommendation</th><th>Management response</th>' +
        '<th>Responsible</th><th>Target date</th><th>Status</th><th></th></tr></thead><tbody>';
      p.findings.forEach((f, i) => {
        h += '<tr>' +
          '<td><input data-find="' + i + '" data-k="area" value="' + esc(f.area || '') + '"></td>' +
          '<td><input data-find="' + i + '" data-k="observation" value="' + esc(f.observation || '') + '"></td>' +
          '<td><select data-find="' + i + '" data-k="risk_rating">' +
          ['high', 'medium', 'low'].map(r => '<option value="' + r + '"' + (f.risk_rating === r ? ' selected' : '') + '>' + r.charAt(0).toUpperCase() + r.slice(1) + '</option>').join('') + '</select></td>' +
          '<td><input data-find="' + i + '" data-k="recommendation" value="' + esc(f.recommendation || '') + '"></td>' +
          '<td><input data-find="' + i + '" data-k="management_response" value="' + esc(f.management_response || '') + '"></td>' +
          '<td><input data-find="' + i + '" data-k="responsible_person" value="' + esc(f.responsible_person || '') + '"></td>' +
          '<td><input type="date" data-find="' + i + '" data-k="target_date" value="' + esc(f.target_date || '') + '"></td>' +
          '<td><select data-find="' + i + '" data-k="status">' +
          ['open', 'closed'].map(s => '<option value="' + s + '"' + (f.status === s ? ' selected' : '') + '>' + (s === 'open' ? 'Open' : 'Closed') + '</option>').join('') + '</select></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-find="' + i + '" title="Remove this row">Remove</button></td></tr>';
      });
      h += '</tbody></table></div><button class="btn btn-small" id="sa-add-find">Add a finding</button></div>';

      h += '<div class="card"><h3>Conclusion</h3><div class="grid c3" id="sa-conclusion">' +
        field('Overall rating', 'overall_rating', p.conclusion, 'select', { options: [
          { value: '', label: '— select —' }, { value: 'Satisfactory', label: 'Satisfactory' },
          { value: 'Needs improvement', label: 'Needs improvement' }, { value: 'Unsatisfactory', label: 'Unsatisfactory' }] }) +
        '<div class="field" style="grid-column:2/-1"><label>Summary</label><textarea data-key="summary" rows="2">' + esc(p.conclusion.summary || '') + '</textarea></div>' +
        '<div style="grid-column:1/-1;margin-top:6px"><button class="btn btn-small" id="sa-gen-opinion">Generate opinion report for this entity</button> ' +
        '<span class="hint">Opens the Auditor\'s Report module with the entity name, signatory and a suggested opinion type carried over — review before issuing.</span></div>' +
        '</div></div>';

      h += '<div class="card"><h3>Report signatory</h3><div class="grid c3" id="sa-signatory">' +
        field('Signatory name', 'name', p.signatory, 'text') +
        field('Designation', 'designation', p.signatory, 'select', { options: [
          { value: 'ca', label: 'Chartered Accountant (CA)' }, { value: 'ra', label: 'Registered Auditor (RA)' }] }) +
        field('Membership no.', 'membership_no', p.signatory, 'text') +
        field('Firm name', 'firm_name', p.signatory, 'text') +
        field('Place', 'place', p.signatory, 'text') +
        field('Date', 'date', p.signatory, 'date') +
        field('UDIN (if applicable)', 'udin', p.signatory, 'text') +
        '</div></div>';

      return h;
    },
    wire(root, file, rerender) {
      const p = file.payload;
      wireInto(root, 'sa-entity', p.entity, rerender);
      wireInto(root, 'sa-materiality', p.materiality, rerender);
      wireInto(root, 'sa-sample', p.sample_size, rerender);
      wireInto(root, 'sa-conclusion', p.conclusion, rerender);
      wireInto(root, 'sa-signatory', p.signatory, rerender);

      root.querySelectorAll('[data-risk]').forEach(el => el.addEventListener('change', e => {
        const area = e.target.dataset.risk, k = e.target.dataset.k;
        p.risk_assessment[area] = p.risk_assessment[area] || {};
        p.risk_assessment[area][k] = e.target.value;
        rerender();
      }));

      root.querySelectorAll('[data-prog]').forEach(el => el.addEventListener('change', e => {
        const id = e.target.dataset.prog, k = e.target.dataset.k;
        p.criteria_responses[id] = p.criteria_responses[id] || {};
        p.criteria_responses[id][k] = e.target.value;
        rerender();
      }));

      root.querySelectorAll('[data-find]').forEach(el => el.addEventListener('change', e => {
        const i = +e.target.dataset.find, k = e.target.dataset.k;
        p.findings[i][k] = e.target.value;
        rerender();
      }));
      root.querySelectorAll('[data-del-find]').forEach(el => el.addEventListener('click', e => {
        p.findings.splice(+e.target.dataset.delFind, 1); rerender();
      }));
      const addFind = root.querySelector('#sa-add-find');
      if (addFind) addFind.addEventListener('click', () => {
        p.findings.push({ area: '', observation: '', risk_rating: 'medium', recommendation: '',
          management_response: '', responsible_person: '', target_date: '', status: 'open' });
        rerender();
      });

      const genOpinionBtn = root.querySelector('#sa-gen-opinion');
      if (genOpinionBtn) genOpinionBtn.addEventListener('click', () => {
        if (!file.clientId) { toast('Select a client on this workfile first', true); return; }
        const target = getFile('audit_report', file.clientId, file.fy, MODULES.audit_report.seed());
        target.payload.entity = target.payload.entity || {};
        target.payload.entity.name = p.entity.name || target.payload.entity.name;
        const rating = (p.conclusion && p.conclusion.overall_rating) || '';
        target.payload.opinion_type = rating === 'Unsatisfactory' ? 'adverse'
          : rating === 'Needs improvement' ? 'qualified' : 'unqualified';
        target.payload.signatory = Object.assign({}, target.payload.signatory, p.signatory);
        touch(target);
        localStorage.setItem('ca.client.audit_report', file.clientId);
        toast('Draft opinion report created — opening Auditor\'s Report');
        location.hash = '#/audit_report';
      });
    },
    tabulate: res => {
      const m = res.materiality || {}, ss = res.sample_size || {};
      const tables = [];
      tables.push({
        title: 'Materiality & risk', headers: ['Particulars', 'Value'],
        rows: [
          ['Benchmark', m.benchmark || ''], ['Benchmark value', m.benchmark_value || 0],
          ['Materiality %', m.materiality_pct || 0], ['Overall materiality', m.overall_materiality || 0],
          ['Performance materiality %', m.pm_pct || 0], ['Performance materiality', m.performance_materiality || 0],
          ['Clearly trivial threshold', m.clearly_trivial || 0],
          ['', ''],
          ['Sample — population value', ss.population_value || 0], ['Sample — population count', ss.population_count || 0],
          ['Sample — tolerable misstatement', ss.tolerable_misstatement || 0],
          ['Sample — expected misstatement', ss.expected_misstatement || 0],
          ['Sample — risk level', ss.risk_level || ''], ['Sample — risk factor', ss.risk_factor || ''],
          ['Sample — sampling interval', ss.sampling_interval || 0], ['Sample size', ss.sample_size || 0],
        ]
      });

      tables.push({
        title: 'Risk by area', headers: ['Area', 'Inherent', 'Control', 'Combined risk', 'Detection risk needed', 'Rationale'],
        rows: (res.risk_assessment || []).map(ra => [ra.area, ra.inherent, ra.control, ra.combined_risk, ra.detection_risk_required, ra.rationale || ''])
      });

      const progRows = [];
      (res.areas || []).forEach(a => (a.criteria || []).forEach(c =>
        progRows.push([a.area, c.criterion, c.procedure || '', c.sample_basis || '', c.status, c.evidence || '',
          c.wp_ref || '', c.performed_by || '', c.result || ''])));
      tables.push({
        title: 'Audit program',
        headers: ['Area', 'Criterion', 'Procedure', 'Sample basis', 'Status', 'Evidence', 'W/P ref', 'Performed by', 'Result'],
        rows: progRows
      });

      tables.push({
        title: 'Findings',
        headers: ['Area', 'Observation', 'Risk', 'Recommendation', 'Management response', 'Responsible', 'Target date', 'Status'],
        rows: (res.findings || []).map(f => [f.area || '', f.observation || '', (f.risk_rating || '').toUpperCase(),
          f.recommendation || '', f.management_response || '', f.responsible_person || '', f.target_date || '', (f.status || 'open').toUpperCase()])
      });
      return tables;
    }
  };

  // ──────────────────────────────────────────────────────────── module page
  // A workfile only ever syncs once it has a valid engagement_id (the server
  // route requires one). This used to be resolved by grabbing "whichever
  // engagement happens to be first for this client" with no fiscal-year
  // match, and simply left a file unsynced forever if the client had none at
  // all. Now: prefer an engagement matching both client and fiscal year,
  // fall back to any engagement for the client, and — only if truly none
  // exists — create one automatically (a plain standalone engagement; the
  // user can later mark it "integrated" from the client record) so sync
  // isn't silently blocked. Runs in the background; the current render
  // already has whatever engagement (if any) was available synchronously.
  const _engagementCreatePending = new Set();
  function resolveEngagement(clientId, fyLabel, file) {
    if (!clientId) return;
    const engs = (state.boot && state.boot.engagements) || [];
    const fyRow = ((state.boot && state.boot.fiscal_years) || []).find(f => f.label_bs === fyLabel);
    let eng = engs.find(e => e.client_id == clientId && fyRow && e.fiscal_year_id == fyRow.id);
    if (!eng) eng = engs.find(e => e.client_id == clientId);
    if (eng) { file.engagementId = eng.id; return; }
    if (!state.online || state.offlineMode || !state.token) return;
    const pendingKey = clientId + ':' + fyLabel;
    if (_engagementCreatePending.has(pendingKey)) return;
    _engagementCreatePending.add(pendingKey);
    api('/api/engagements', { method: 'POST', body: {
      client_id: clientId, fiscal_year_id: fyRow ? fyRow.id : null, type: 'advisory',
      title: 'General engagement — ' + (clientNameOf(clientId) || 'client'), scope: 'standalone'
    } }).then(created => {
      state.boot = state.boot || {};
      state.boot.engagements = (state.boot.engagements || []).concat([created]);
      file.engagementId = created.id;
    }).catch(() => {}).finally(() => { _engagementCreatePending.delete(pendingKey); });
  }

  function renderModulePage(moduleKey) {
    const mod = MODULES[moduleKey];
    const view = document.getElementById('view');
    const clientId = localStorage.getItem('ca.client.' + moduleKey) || (clients()[0] && clients()[0].id) || '';
    const file = getFile(moduleKey, clientId, state.fy, mod.seed());
    file.clientId = clientId || null;
    file.clientName = clientNameOf(clientId);
    resolveEngagement(clientId, state.fy, file);

    function draw() {
      let res;
      let computeData = file.payload;
      if (mod.needsIndustries) computeData = Object.assign({}, computeData, { industries: (state.boot && state.boot.industry_defaults) || [] });
      if (mod.needsFdiSectors) computeData = Object.assign({}, computeData, { sectors: (state.boot && state.boot.fdi_sectors) || [] });
      if (mod.needsFiscalYears) computeData = Object.assign({}, computeData, { fiscal_years: (state.boot && state.boot.financial_periods) || [] });
      if (mod.needsAuditCatalog) computeData = Object.assign({}, computeData, { audit_criteria_bank: (state.boot && state.boot.audit_criteria) || [] });
      try { res = CAEngines.compute(moduleKey, state.rates, state.fy, computeData); }
      catch (e) { view.innerHTML = '<div class="banner">' + esc(e.message) + '</div>'; return; }

      const unverified = CAEngines.unverifiedOf(moduleKey, res);
      view.innerHTML =
        '<div class="page-head"><div><h2>' + esc(mod.name) + '</h2><p>' + esc(mod.blurb) + '</p></div>' +
        '<div class="page-actions">' +
        (mod.bulkImport ? '<a class="btn" href="/api/import/' + moduleKey + '/template" download title="A spreadsheet shaped for this module — fill it in and upload it below instead of typing row by row">Download import template</a>' +
          '<button class="btn" id="imp-upload" title="Upload a filled-in copy of the template above">Import from Excel</button>' +
          '<input type="file" id="imp-file" accept=".xlsx" hidden>' : '') +
        '<button class="btn" id="exp-xlsx" title="Generate this workfile as a formatted Excel file, saved to Reports">Excel</button>' +
        '<button class="btn" id="exp-docx" title="Generate this workfile as a formatted Word document, saved to Reports">Word</button>' +
        (mod.extraFormats || []).map(f => '<button class="btn" data-exp-extra="' + f.fmt + '">' + esc(f.label) + '</button>').join('') +
        '<button class="btn btn-primary" id="save-now" title="Push this workfile to the server right now, instead of waiting for the next autosave">Save now</button>' +
        '<button class="btn btn-danger" id="clear-data" title="Wipe everything typed into this workfile and start over — cannot be undone">Clear data</button></div></div>' +
        '<div class="card"><div class="grid c3">' +
        '<div class="field"><label title="Which client this workfile belongs to — switching clients opens a separate workfile, this one is untouched">Client</label><select id="pick-client">' +
        '<option value="">— no client selected —</option>' +
        clients().map(c => '<option value="' + c.id + '"' + (c.id == clientId ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') +
        '</select></div>' +
        '<div class="field"><label title="The fiscal year this workfile is for — a separate workfile exists for each year, so switching here opens a different one">Fiscal year</label><select id="pick-fy">' +
        (((state.boot && state.boot.financial_periods) || [{ label_bs: state.fy }]).map(f =>
          '<option value="' + f.label_bs + '"' + (f.label_bs === state.fy ? ' selected' : '') + '>' + f.label_bs + '</option>').join('')) +
        '</select></div>' +
        '<div class="field"><label title="Whether everything typed here has reached the server yet">Status</label><div>' +
        (file.dirty ? '<span class="tag tag-amber" title="Changes exist only on this device so far">Unsynced changes</span>' : '<span class="tag tag-green" title="Everything here matches the server">Synced</span>') +
        (unverified.length ? ' <span class="tag tag-red" title="This computation used a rate nobody has confirmed yet — check Rate master">' + unverified.length + ' unverified rates</span>' : '') +
        '</div></div></div></div>' +
        mod.view(file, res);

      // wire the shared chrome
      view.querySelector('#pick-client').addEventListener('change', e => {
        localStorage.setItem('ca.client.' + moduleKey, e.target.value);
        renderModulePage(moduleKey);
      });
      view.querySelector('#pick-fy').addEventListener('change', e => {
        state.fy = e.target.value; localStorage.setItem('ca.fy', state.fy);
        renderModulePage(moduleKey);
      });
      view.querySelector('#save-now').addEventListener('click', () => beat(true));
      view.querySelector('#clear-data').addEventListener('click', () => {
        if (!confirm('Clear everything in this workfile (' + mod.name + (file.clientName ? ' — ' + file.clientName : '') + ', FY ' + state.fy + ')? This cannot be undone.')) return;
        Object.keys(file.payload).forEach(k => delete file.payload[k]);
        Object.assign(file.payload, mod.seed());
        touch(file);
        toast('Cleared');
        draw();
      });
      view.querySelector('#exp-xlsx').addEventListener('click', () => generateReport(moduleKey, 'xlsx', file, res));
      view.querySelector('#exp-docx').addEventListener('click', () => generateReport(moduleKey, 'docx', file, res));
      view.querySelectorAll('[data-exp-extra]').forEach(btn => btn.addEventListener('click', () =>
        generateReport(moduleKey, btn.dataset.expExtra, file, res)));

      if (mod.bulkImport) {
        const uploadBtn = view.querySelector('#imp-upload');
        const fileInput = view.querySelector('#imp-file');
        uploadBtn.addEventListener('click', () => fileInput.click());
        fileInput.addEventListener('change', () => {
          const f = fileInput.files[0];
          if (!f) return;
          if (!state.online || !state.token || state.offlineMode) {
            toast('Importing needs a connection', true); fileInput.value = ''; return;
          }
          const reader = new FileReader();
          reader.onload = async () => {
            const base64 = reader.result.split(',')[1];
            uploadBtn.disabled = true; uploadBtn.textContent = 'Importing…';
            try {
              const result = await api('/api/import/' + moduleKey, { method: 'POST',
                body: { content_base64: base64, filename: f.name } });
              mod.bulkImport.apply(file, result.sheets);
              touch(file);
              if (result.errors && result.errors.length) {
                toast('Imported ' + result.row_count + ' rows — ' + result.errors.length +
                  ' skipped: ' + result.errors.slice(0, 3).join(' '), true);
              } else {
                toast('Imported ' + result.row_count + ' rows from ' + f.name);
              }
              draw();
            } catch (e) {
              toast(e.message, true);
            } finally {
              uploadBtn.disabled = false; uploadBtn.textContent = 'Import from Excel'; fileInput.value = '';
            }
          };
          reader.readAsDataURL(f);
        });
      }

      mod.wire(view, file, () => { touch(file); draw(); });
      wireSimple(view, file, () => { touch(file); draw(); });
    }
    draw();
  }

  // ──────────────────────────────────────────────────────────────── pages
  function deskDaysUntil(dateStr) {
    return Math.round((new Date(dateStr) - new Date()) / 86400000);
  }

  // A global, always-reachable fiscal-year switcher — every calculator page
  // already has its own FY picker in its shared chrome, but that only
  // switches which workfile is open for that one module, and the reactive
  // "you're behind the current FY" banner only ever offers switching
  // *forward* to the server's current FY, never back. This control goes the
  // other way too: any FY the firm has on file, from Desk or Admin, no need
  // to open a specific module first.
  function fySwitcherHtml() {
    const fys = ((state.boot && state.boot.financial_periods) || [{ label_bs: state.fy }]).slice()
      .sort((a, b) => a.label_bs.localeCompare(b.label_bs));
    return '<div class="field" style="max-width:220px"><label title="Every calculator, dashboard and rate lookup uses this fiscal year until you change it here — switch freely in either direction">Working fiscal year</label>' +
      '<select id="global-fy-switch">' + fys.map(f => '<option value="' + esc(f.label_bs) + '"' + (f.label_bs === state.fy ? ' selected' : '') +
        '>' + esc(f.label_bs) + (f.is_current ? ' (current)' : '') + '</option>').join('') + '</select></div>';
  }
  function wireFySwitcher(root) {
    const el = (root || document).querySelector('#global-fy-switch');
    if (!el) return;
    el.addEventListener('change', e => {
      state.fy = e.target.value; localStorage.setItem('ca.fy', state.fy);
      toast('Working fiscal year set to ' + state.fy);
      checkFyCurrency();
      route();
    });
  }

  // Country switcher — same prominence/pattern as the fiscal-year switcher
  // right next to it, but changing it is a bigger deal: every module list
  // (buildRail(), the "Add a user"/per-user grant checkboxes below) and
  // every rate/engine computation depends on it, so this saves via the
  // dedicated PUT /api/admin/firm-country endpoint (NOT firm-profile —
  // that one always rewrites every field of the firm's public profile from
  // the request body, so posting just {country_code} through it would
  // blank out name/address/phone/socials, see admin_update_firm_country's
  // own comment) and does a full bootstrap+rail refresh on success, not
  // just a local state flip. Async (needs the active-countries list, unlike
  // the FY switcher which reads from already-cached state.boot) — renders a
  // placeholder synchronously, then fills in once loaded, same lazy-load
  // convention every other Admin-page card here uses.
  function countrySwitcherHtml() {
    return '<div class="field" style="max-width:260px" id="global-country-switch-wrap">' +
      '<label title="Which country\'s tax/audit rules this firm\'s engines use by default, and which modules are shown for it">Firm country / jurisdiction</label>' +
      '<select id="global-country-switch" disabled><option>Loading…</option></select></div>';
  }
  function wireCountrySwitcher(root) {
    const wrap = (root || document).querySelector('#global-country-switch-wrap');
    if (!wrap) return;
    api('/api/admin/active-countries').then(countries => {
      const current = (state.boot && state.boot.firm && state.boot.firm.country_code) || 'NP';
      wrap.innerHTML = '<label title="Which country\'s tax/audit rules this firm\'s engines use by default, and which modules are shown for it">Firm country / jurisdiction</label>' +
        '<select id="global-country-switch">' + countries.map(c =>
          '<option value="' + esc(c.code) + '"' + (c.code === current ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') + '</select>';
      wrap.querySelector('#global-country-switch').addEventListener('change', async e => {
        const code = e.target.value;
        e.target.disabled = true;
        try {
          await api('/api/admin/firm-country', { method: 'PUT', body: { country_code: code } });
          const boot = await api('/api/bootstrap');
          state.boot = boot; state.rates = boot.rates || [];
          // The working fiscal year follows the country too — Nepal's BS
          // year, India's Apr-Mar year and UAE's calendar year are three
          // different labels for "now," and a label left over from the
          // previous country (e.g. Nepal's '2082-83' after switching to
          // UAE) would never match anything in the new country's own
          // financial_periods list.
          if (boot.current_fy) { state.fy = boot.current_fy; localStorage.setItem('ca.fy', state.fy); }
          await idbPut('kv', boot, 'boot'); await idbPut('kv', state.rates, 'rates');
          toast('Firm country set to ' + countries.find(c => c.code === code).name +
            ' — modules and working fiscal year (' + state.fy + ') refreshed');
          buildRail();
          route();
        } catch (err) { toast(err.message, true); e.target.disabled = false; }
      });
    }).catch(() => { wrap.innerHTML = '<label>Firm country / jurisdiction</label><div class="hint">Could not load</div>'; });
  }

  function renderDesk() {
    const view = document.getElementById('view');
    const dl = (state.boot && state.boot.deadlines) || [];
    const dirty = state.dirty.size;
    const today = new Date().toDateString();
    const beatsToday = state.beats.filter(b => b.t.toDateString() === today).length;

    // Grouped by client — a flat list of deadlines mixed with a flat list of
    // open files, all pooled together with no client context, is exactly
    // what made this page hard to scan; one card per client fixes that.
    const byClient = {};
    const bucket = (id, name) => {
      const key = id || 'none';
      if (!byClient[key]) byClient[key] = { id: id || null, name: name || 'No client', deadlines: [], files: [] };
      return byClient[key];
    };
    dl.forEach(d => { bucket(d.client_id, d.client_name).deadlines.push(d); });
    Object.values(state.files).forEach(f => { bucket(f.clientId, f.clientName).files.push(f); });

    const scoreOf = c => c.deadlines.reduce((m, d) => Math.min(m, deskDaysUntil(d.due_date_ad)), Infinity);
    const rows = Object.values(byClient).sort((a, b) => {
      const sa = scoreOf(a), sb = scoreOf(b);
      if (sa !== sb) return sa - sb;
      return (a.name || '').localeCompare(b.name || '');
    });

    let h = '<div class="page-head"><div><h2>Desk</h2><p>Where things stand right now, grouped by client — what falls due, what is waiting to sync, what still needs verifying before it can be relied on.</p></div>' +
      fySwitcherHtml() + '</div>' +
      '<div class="grid c4" style="margin-bottom:18px">' +
      kpi('Clients', clients().length) +
      kpi('Files with unsynced work', dirty, dirty ? 'alert' : '') +
      kpi('Autosave beats today', beatsToday) +
      kpi('Rates awaiting verification', (state.boot && state.boot.unverified_rates) || 0,
          (state.boot && state.boot.unverified_rates) ? 'alert' : '') +
      '</div>' +
      moduleDashboardHtml();

    const appts = (state.boot && state.boot.appointments) || [];
    if (appts.length) {
      const APPT_METHOD_LABELS = { phone: 'Phone call', video: 'Video call', in_person: 'In-person / office visit' };
      const APPT_SERVICE_LABELS = { fdi: 'FDI consultancy', tax: 'Tax advisory', valuation: 'Valuation',
        advisory: 'Corporate & regulatory advisory', transfer_pricing: 'Transfer pricing', other: 'Other / not sure' };
      h += '<div class="card" title="Booked from the public site\'s appointment page — confirm or cancel these in Admin"><h3>Upcoming appointments</h3>' +
        '<div class="rowlist">' + appts.slice(0, 8).map(a => '<div class="rl"><div class="rl-main"><b>' + esc(a.name) + '</b>' +
          '<small>' + esc(a.date) + ' ' + esc(a.start_time) + '–' + esc(a.end_time) + ' · ' + esc(APPT_SERVICE_LABELS[a.service] || a.service || 'General') +
          ' · ' + esc(APPT_METHOD_LABELS[a.method] || a.method) + '</small></div>' +
          '<span class="tag ' + (a.status === 'confirmed' ? 'tag-green' : 'tag-amber') + '">' + esc(a.status) + '</span></div>').join('') + '</div>' +
        (appts.length > 8 ? '<p class="card-note" style="margin-top:8px">' + (appts.length - 8) + ' more — see Admin → Appointments.</p>' : '') +
        '</div>';
    }

    if (!rows.length) {
      h += '<div class="empty"><b>Nothing to show yet</b>Add a client and open a module to get started — everything you type is written to this device within a second, whether or not there is a connection.</div>';
    } else {
      rows.forEach(c => {
        const overdueN = c.deadlines.filter(d => deskDaysUntil(d.due_date_ad) < 0).length;
        const dueSoonN = c.deadlines.filter(d => { const n = deskDaysUntil(d.due_date_ad); return n >= 0 && n <= 7; }).length;
        const unsyncedN = c.files.filter(f => f.dirty).length;
        h += '<details class="card" style="margin-top:10px"' + (overdueN || dueSoonN ? ' open' : '') + '>' +
          '<summary><b>' + esc(c.name) + '</b>' +
          (overdueN ? ' <span class="tag tag-red">' + overdueN + ' overdue</span>' : '') +
          (dueSoonN ? ' <span class="tag tag-amber">' + dueSoonN + ' due within 7 days</span>' : '') +
          (unsyncedN ? ' <span class="tag tag-amber">' + unsyncedN + ' unsynced</span>' : '') +
          '</summary>';
        if (c.deadlines.length) {
          h += '<div class="rowlist" style="margin-top:10px">' + c.deadlines.map(d => {
            const days = deskDaysUntil(d.due_date_ad);
            const tag = days < 0 ? 'tag-red' : (days <= 7 ? 'tag-amber' : 'tag-green');
            return '<div class="rl"><div class="rl-main"><b>' + esc(d.title) + '</b>' +
              (d.period_label ? '<small>' + esc(d.period_label) + '</small>' : '') + '</div>' +
              '<span class="tag ' + tag + '">' + (days < 0 ? Math.abs(days) + ' days overdue' : 'in ' + days + ' days') + '</span></div>';
          }).join('') + '</div>';
        }
        if (c.files.length) {
          h += '<div class="rowlist" style="margin-top:' + (c.deadlines.length ? '4px' : '10px') + '">' + c.files.map(f =>
            '<div class="rl"><div class="rl-main"><b>' + esc(MODULES[f.module] ? MODULES[f.module].name : f.module) + '</b>' +
            '<small>FY ' + esc(f.fy) + ' · updated ' + new Date(f.updatedAt).toLocaleString() + '</small></div>' +
            (f.dirty ? '<span class="tag tag-amber" title="Written to this device but not yet reached the server">Not yet synced</span>' : '<span class="tag tag-green">Synced</span>') +
            '<button class="btn btn-small" data-open="' + esc(f.module) + '" data-open-client="' + esc(f.clientId || '') + '" data-open-fy="' + esc(f.fy) + '" title="Open this exact workfile">Open</button></div>').join('') + '</div>';
        }
        if (!c.deadlines.length && !c.files.length) h += '<div class="empty" style="margin-top:10px">Nothing open for this client right now.</div>';
        h += '</details>';
      });
    }
    view.innerHTML = h;
    wireFySwitcher(view);
    wireModuleDashboard(view);

    view.querySelectorAll('[data-open]').forEach(b => b.addEventListener('click', e => {
      const moduleKey = e.target.dataset.open, clientId = e.target.dataset.openClient, fy = e.target.dataset.openFy;
      if (fy) { state.fy = fy; localStorage.setItem('ca.fy', fy); }
      localStorage.setItem('ca.client.' + moduleKey, clientId || '');
      location.hash = '#/' + moduleKey;
    }));
  }

  // Module directory + search, shown at the top of Desk — MODULE_CATALOG covers
  // every grantable module (the older workfile tools and the newer relational
  // ones alike), so this list and the Admin grant checkboxes never drift apart.
  function moduleDashboardHtml() {
    const groups = {};
    Object.keys(MODULE_CATALOG).forEach(k => {
      if (!isModuleGranted(k)) return;
      const m = MODULE_CATALOG[k];
      (groups[m.group || 'Other'] = groups[m.group || 'Other'] || []).push(
        { route: m.route, code: m.code, name: m.name, blurb: m.blurb || '' });
    });
    const groupNames = Object.keys(groups).sort();
    if (!groupNames.length) return '';
    return '<div class="card" id="module-dashboard"><h3>Modules</h3>' +
      '<div class="field" style="margin-bottom:12px"><input type="search" id="module-search" placeholder="Search what you want to do — e.g. ‘cash flow’, ‘lease’, ‘TDS’…"></div>' +
      '<div id="module-tiles">' + groupNames.map(g =>
        '<div class="rail-group module-group" style="margin-top:10px">' + esc(g) + '</div>' +
        '<div class="grid c3">' + groups[g].map(m =>
          '<a class="card module-tile" data-module-go="' + esc(m.route) + '" data-search="' + esc((m.name + ' ' + m.blurb).toLowerCase()) + '"' +
          ' style="display:block;cursor:pointer;margin-top:0"><b><code>' + esc(m.code) + '</code> ' + esc(m.name) + '</b>' +
          '<p class="card-note">' + esc(m.blurb) + '</p></a>').join('') + '</div>').join('') +
      '</div><div class="empty" id="module-search-empty" hidden>No module matches that search.</div></div>';
  }

  function wireModuleDashboard(view) {
    const box = view.querySelector('#module-dashboard');
    if (!box) return;
    box.querySelectorAll('[data-module-go]').forEach(a => a.addEventListener('click', () => {
      location.hash = '#/' + a.dataset.moduleGo;
    }));
    const search = box.querySelector('#module-search');
    const emptyMsg = box.querySelector('#module-search-empty');
    search.addEventListener('input', () => {
      const q = search.value.trim().toLowerCase();
      let anyVisible = false;
      box.querySelectorAll('.module-group').forEach(gh => {
        const grid = gh.nextElementSibling;
        let groupHasMatch = false;
        Array.from(grid.children).forEach(tile => {
          const match = !q || tile.dataset.search.indexOf(q) !== -1;
          tile.style.display = match ? '' : 'none';
          if (match) groupHasMatch = true;
        });
        gh.style.display = groupHasMatch ? '' : 'none';
        grid.style.display = groupHasMatch ? '' : 'none';
        if (groupHasMatch) anyVisible = true;
      });
      emptyMsg.hidden = anyVisible;
    });
  }

  const CLIENT_ENTITY_TYPES = [
    { value: '', label: '— select —' },
    { value: 'pvt_ltd', label: 'Private limited' }, { value: 'public_ltd', label: 'Public limited' },
    { value: 'firm', label: 'Partnership firm' }, { value: 'proprietorship', label: 'Proprietorship' },
    { value: 'ngo', label: 'NGO' }, { value: 'coop', label: 'Cooperative' }, { value: 'branch', label: 'Branch office' }
  ];
  const CLIENT_STATUSES = [{ value: 'active', label: 'Active' }, { value: 'inactive', label: 'Inactive' },
    { value: 'archived', label: 'Archived' }];
  const CLIENT_STAGES = [
    { value: 'lead', label: 'Lead / prospect' }, { value: 'incorporation', label: 'Incorporation in progress' },
    { value: 'pan_vat_registration', label: 'PAN / VAT registration' }, { value: 'compliance_setup', label: 'Compliance setup' },
    { value: 'active', label: 'Active — regular compliance' }, { value: 'audit_in_progress', label: 'Audit in progress' },
    { value: 'dormant', label: 'Dormant' }, { value: 'under_closure', label: 'Under closure / liquidation' },
    { value: 'closed', label: 'Closed / struck off' }
  ];
  const CLIENT_SERVICE_TYPES = [
    { value: 'audit', label: 'Audit' }, { value: 'tax', label: 'Tax' }, { value: 'bookkeeping', label: 'Bookkeeping' },
    { value: 'valuation', label: 'Valuation' }, { value: 'fdi', label: 'FDI' }, { value: 'due_diligence', label: 'Due diligence' },
    { value: 'advisory', label: 'Advisory' }, { value: 'payroll', label: 'Payroll' },
    { value: 'company_secretarial', label: 'Company secretarial' }
  ];
  const CLIENT_VAT_FREQ = [
    { value: '', label: '— not set —' }, { value: 'monthly', label: 'Monthly' },
    { value: 'four_monthly', label: 'Four-monthly (trimester)' }, { value: 'six_monthly', label: 'Six-monthly' },
    { value: 'not_registered', label: 'Not VAT registered' }
  ];
  const CLIENT_REMINDER_TYPES = [
    { value: 'VAT_MONTHLY', label: 'VAT filing' }, { value: 'STAT_AUDIT', label: 'Statutory audit' },
    { value: 'account_closing', label: 'Account closing date' }, { value: 'custom', label: 'Custom reminder' }
  ];
  const labelOf = (list, v) => (list.find(x => x.value === v) || {}).label || v || '';

  function clientFormHtml(c, idPrefix, staff, countries) {
    const assignOpts = [{ value: '', label: '— unassigned —' }].concat((staff || []).map(s => ({ value: String(s.id), label: s.full_name })));
    const countryOpts = (countries || []).map(co => ({ value: co.code, label: co.name }));
    return '<div class="grid c3" id="' + idPrefix + '">' +
      field('Name', 'name', c, 'text') +
      field('Code', 'code', c, 'text', { placeholder: 'auto-generated if left blank', hint: 'Only used when adding a new client' }) +
      field('Entity type', 'entity_type', c, 'select', { options: CLIENT_ENTITY_TYPES }) +
      field('Country / jurisdiction', 'country_code', c, 'select', { options: countryOpts,
        hint: 'Which country\'s tax/audit rules this client\'s own workfiles use — defaults to the firm\'s own country, but a client can be in a different one' }) +
      field('PAN', 'pan', c, 'text') +
      field('Industry', 'industry', c, 'text') +
      field('IRD office', 'ird_office', c, 'text') +
      field('Contact person (focal point)', 'contact_person', c, 'text') +
      field('Contact phone', 'contact_phone', c, 'text') +
      field('Contact email', 'contact_email', c, 'text') +
      field('Status', 'status', c, 'select', { options: CLIENT_STATUSES }) +
      field('Stage', 'stage', c, 'select', { options: CLIENT_STAGES }) +
      field('VAT filing frequency', 'vat_filing_frequency', c, 'select', { options: CLIENT_VAT_FREQ }) +
      field('Looked after by', 'assigned_to', c, 'select', { options: assignOpts }) +
      '<label class="check"><input type="checkbox" data-flag="vat_registered"' + (c.vat_registered ? ' checked' : '') + '> VAT registered</label>' +
      '<label class="check"><input type="checkbox" data-flag="special_industry"' + (c.special_industry ? ' checked' : '') + '> Special industry</label>' +
      '<label class="check"><input type="checkbox" data-flag="urgent"' + (c.urgent ? ' checked' : '') + '> Mark urgent</label>' +
      '<div class="field" style="grid-column:1/-1"><label>Service types</label><div style="display:flex;flex-wrap:wrap;gap:12px;margin-top:6px">' +
      CLIENT_SERVICE_TYPES.map(s => '<label class="check"><input type="checkbox" data-svc="' + s.value + '"' +
        ((c.service_types || []).includes(s.value) ? ' checked' : '') + '> ' + esc(s.label) + '</label>').join('') +
      '</div></div>' +
      '<div class="field" style="grid-column:1/-1"><label>Address</label><input data-key="address" value="' + esc(c.address || '') + '"></div>' +
      '<div class="field" style="grid-column:1/-1"><label>Notes</label><textarea data-key="notes" rows="2">' + esc(c.notes || '') + '</textarea></div>' +
      '<div style="grid-column:1/-1">' + imageUploadHtml('logo_path', 'Client logo', c.logo_path,
        'Used on reports/letterheads for this client. Paste an image URL, or upload a file (needs Site Settings access).') + '</div>' +
      '</div>';
  }

  function wireClientForm(root, idPrefix, obj) {
    const container = root.querySelector('#' + idPrefix);
    container.querySelectorAll('[data-key]').forEach(el => {
      el.addEventListener('change', () => { obj[el.dataset.key] = el.value; });
    });
    wireImageUpload(container);
    container.querySelectorAll('[data-flag]').forEach(el => {
      el.addEventListener('change', () => { obj[el.dataset.flag] = el.checked; });
    });
    container.querySelectorAll('[data-svc]').forEach(el => {
      el.addEventListener('change', () => {
        obj.service_types = obj.service_types || [];
        const v = el.dataset.svc;
        if (el.checked) { if (!obj.service_types.includes(v)) obj.service_types.push(v); }
        else obj.service_types = obj.service_types.filter(x => x !== v);
      });
    });
  }

  function cdDaysUntil(dateStr) {
    if (!dateStr) return null;
    const d = new Date(dateStr + 'T00:00:00');
    if (isNaN(d)) return null;
    const today = new Date(); today.setHours(0, 0, 0, 0);
    return Math.round((d - today) / 86400000);
  }

  function cdDeadlineTag(days, label) {
    const prefix = label ? esc(label) + ' ' : '';
    if (days == null) return '<span class="tag">' + prefix + 'no date</span>';
    if (days < 0) return '<span class="tag tag-red">' + prefix + (-days) + 'd overdue</span>';
    if (days <= 7) return '<span class="tag tag-amber">' + prefix + 'due in ' + days + 'd</span>';
    return '<span class="tag">' + prefix + 'due in ' + days + 'd</span>';
  }

  function cdAuditBadge(auditWorkfiles) {
    if (!auditWorkfiles || !auditWorkfiles.length) return '';
    const openFindings = auditWorkfiles.reduce((s, w) => s + ((w.summary && w.summary.findings_open) || 0), 0);
    const highRisk = auditWorkfiles.some(w => (w.risk_assessment || []).some(r => r.combined_risk === 'high'));
    let h = '';
    if (openFindings) h += '<span class="tag tag-red">' + openFindings + ' audit finding' + (openFindings > 1 ? 's' : '') + ' open</span>';
    if (highRisk) h += '<span class="tag tag-red">High audit risk</span>';
    return h;
  }

  function cdBarChart(title, rows) {
    const max = Math.max(1, ...rows.map(r => r.count));
    return '<div class="card"><h3>' + esc(title) + '</h3>' +
      (rows.length ? rows.map(r => '<div class="bar-row"><span class="bar-label">' + esc(r.label) + '</span>' +
        '<div class="bar-track"><div class="bar-fill" style="width:' + Math.round(r.count / max * 100) + '%"></div></div>' +
        '<span class="bar-count">' + r.count + '</span></div>').join('')
        : '<div class="empty">No data yet</div>') +
      '</div>';
  }

  function renderClients() {
    const view = document.getElementById('view');
    let dir = { clients: [], staff: [] };
    let loaded = false;
    let editingId = null;      // null | 'new' | client id
    let draft = {};
    let expandedIds = new Set();
    let selected = new Set();
    let revealed = {};         // credential id -> plaintext, shown until hidden or redrawn away
    let filters = { q: '', stage: '', service: '', urgentOnly: false, unclaimedOnly: false };
    const reminderDrafts = {}, followupDrafts = {}, credentialDrafts = {};
    const draftFor = (map, id, seed) => (map[id] = map[id] || Object.assign({}, seed));
    // Portal contacts aren't part of /api/client-directory's payload (a
    // separate, much smaller endpoint) — cache per client id and fetch only
    // when that client's card is actually expanded, same lazy-load posture
    // as the credentials/followups sections right next to it.
    const portalUsersCache = {};
    function portalUsersHtml(cid, rows) {
      return (rows.length ? '<div class="table-wrap"><table><thead><tr><th>Name</th><th>Email</th><th>Status</th><th></th></tr></thead><tbody>' +
        rows.map(pu => '<tr><td>' + esc(pu.name) + '</td><td>' + esc(pu.email) + '</td>' +
          '<td>' + (pu.is_active ? '<span class="tag tag-green">Active</span>' : '<span class="tag tag-red">Disabled</span>') +
          (pu.last_login_at ? '' : ' <span class="hint">— never signed in</span>') + '</td>' +
          '<td><button class="btn btn-small" data-portal-toggle="' + pu.id + '" data-val="' + (pu.is_active ? 0 : 1) + '">' +
          (pu.is_active ? 'Disable' : 'Re-enable') + '</button> ' +
          '<button class="btn btn-small btn-danger" data-portal-del="' + pu.id + '">Remove</button></td></tr>').join('') +
        '</tbody></table></div>' : '<div class="card-note">No portal contacts yet.</div>') +
        '<div class="grid c3" style="margin-top:8px">' +
        '<div class="field"><label>Name</label><input data-portal-new-name="' + cid + '"></div>' +
        '<div class="field"><label>Email</label><input type="email" data-portal-new-email="' + cid + '"></div>' +
        '<div class="field"><label>&nbsp;</label><button class="btn btn-small" data-portal-invite="' + cid + '">Invite to portal</button></div>' +
        '</div>';
    }
    async function loadPortalUsers(cid) {
      portalUsersCache[cid] = 'loading';
      try { portalUsersCache[cid] = await api('/api/clients/' + cid + '/portal-users'); }
      catch (e) { portalUsersCache[cid] = []; }
      const holder = document.getElementById('portal-users-' + cid);
      if (holder) { holder.innerHTML = portalUsersHtml(cid, portalUsersCache[cid]); wirePortalUsers(cid); }
    }
    function wirePortalUsers(cid) {
      const holder = document.getElementById('portal-users-' + cid);
      if (!holder) return;
      holder.querySelectorAll('[data-portal-toggle]').forEach(b => b.addEventListener('click', async () => {
        try { await api('/api/portal-users/' + b.dataset.portalToggle, { method: 'PUT', body: { is_active: b.dataset.val === '1' } });
          loadPortalUsers(cid); } catch (e) { toast(e.message, true); }
      }));
      holder.querySelectorAll('[data-portal-del]').forEach(b => b.addEventListener('click', async () => {
        if (!confirm('Remove this portal contact? They will no longer be able to sign in.')) return;
        try { await api('/api/portal-users/' + b.dataset.portalDel, { method: 'DELETE' }); loadPortalUsers(cid); }
        catch (e) { toast(e.message, true); }
      }));
      const inviteBtn = holder.querySelector('[data-portal-invite]');
      if (inviteBtn) inviteBtn.addEventListener('click', async () => {
        const name = holder.querySelector('[data-portal-new-name]').value.trim();
        const email = holder.querySelector('[data-portal-new-email]').value.trim();
        if (!name || !email) { toast('A name and email are required', true); return; }
        try { await api('/api/clients/' + cid + '/portal-users', { method: 'POST', body: { name, email } });
          toast('Invited — an email with a set-password link has been sent'); loadPortalUsers(cid); }
        catch (e) { toast(e.message, true); }
      });
    }

    let countries = [];
    async function refresh() {
      if (!state.online || state.offlineMode) {
        dir = { clients: clients().map(c => Object.assign({ deadlines: [], followups: [] }, c)), staff: [] };
        loaded = true; draw();
        return;
      }
      try {
        [dir, countries] = await Promise.all([api('/api/client-directory'), api('/api/admin/active-countries')]);
        state.boot.clients = dir.clients.map(c => { const x = Object.assign({}, c); delete x.deadlines; delete x.followups; return x; });
        await idbPut('kv', state.boot, 'boot');
      } catch (e) { toast(e.message, true); dir = { clients: [], staff: [] }; }
      loaded = true; draw();
    }

    function draw() {
      const focusWasSearch = document.activeElement && document.activeElement.id === 'cd-q';
      const caret = focusWasSearch ? document.activeElement.selectionStart : null;
      const all = dir.clients || [];
      const stageCount = {}, serviceCount = {};
      let urgentN = 0, overdueN = 0, dueSoonN = 0, followupN = 0;
      all.forEach(c => {
        stageCount[c.stage || 'active'] = (stageCount[c.stage || 'active'] || 0) + 1;
        (c.service_types || []).forEach(s => { serviceCount[s] = (serviceCount[s] || 0) + 1; });
        if (c.urgent) urgentN++;
        followupN += (c.followups || []).length;
        (c.deadlines || []).forEach(d => {
          const days = cdDaysUntil(d.due_date_ad);
          if (days != null && days < 0) overdueN++;
          else if (days != null && days <= 7) dueSoonN++;
        });
      });

      let rows = all.filter(c => {
        if (filters.urgentOnly && !c.urgent) return false;
        if (filters.unclaimedOnly && c.assigned_to) return false;
        if (filters.stage && c.stage !== filters.stage) return false;
        if (filters.service && !(c.service_types || []).includes(filters.service)) return false;
        if (filters.q) {
          const q = filters.q.toLowerCase();
          if (!((c.name || '').toLowerCase().includes(q) || (c.code || '').toLowerCase().includes(q) ||
                (c.pan || '').toLowerCase().includes(q))) return false;
        }
        return true;
      });
      const nextDue = c => (c.deadlines || []).reduce((min, d) => {
        const days = cdDaysUntil(d.due_date_ad);
        return days == null ? min : (min == null ? days : Math.min(min, days));
      }, null);
      rows.sort((a, b) => {
        if (!!b.urgent - !!a.urgent !== 0) return !!b.urgent - !!a.urgent;
        const da = nextDue(a), dbv = nextDue(b);
        if (da != null && dbv != null && da !== dbv) return da - dbv;
        if (da != null && dbv == null) return -1;
        if (da == null && dbv != null) return 1;
        return (a.name || '').localeCompare(b.name || '');
      });

      let h = '<div class="page-head"><div><h2>Client Directory</h2><p>Stage, urgency, VAT frequency, who\'s looking after each client, and every open reminder or follow-up — the dashboard the register feeds.</p></div>' +
        '<div class="page-actions"><button class="btn" id="refresh-dir">Refresh</button>' +
        (selected.size ? '<button class="btn btn-primary" id="export-selected">Export ' + selected.size + ' selected</button>' : '') +
        '<button class="btn btn-primary" id="new-client">Add a client</button></div></div>';

      if (!loaded) { h += '<div class="empty">Loading…</div>'; view.innerHTML = h; view.querySelector('#refresh-dir').addEventListener('click', refresh); return; }

      h += '<div class="grid c4">' +
        kpi('Clients', all.length) +
        kpi('Urgent', urgentN, urgentN ? 'alert' : '') +
        kpi('Due within 7 days', dueSoonN, dueSoonN ? 'accent' : '') +
        kpi('Overdue', overdueN, overdueN ? 'alert' : '') +
        '</div>';

      h += '<div class="grid c2" style="margin-top:16px">' +
        cdBarChart('Clients by stage', Object.keys(stageCount).sort().map(k => ({ label: labelOf(CLIENT_STAGES, k) || k, count: stageCount[k] }))) +
        cdBarChart('Clients by service', Object.keys(serviceCount).sort().map(k => ({ label: labelOf(CLIENT_SERVICE_TYPES, k) || k, count: serviceCount[k] }))) +
        '</div>';
      if (followupN) h += '<div class="card-note" style="margin-top:-8px">' + followupN + ' open document follow-up' + (followupN > 1 ? 's' : '') + ' across all clients.</div>';

      h += '<div class="card"><div class="grid c4">' +
        '<div class="field"><label>Search</label><input id="cd-q" value="' + esc(filters.q) + '" placeholder="Name, code or PAN"></div>' +
        '<div class="field"><label>Stage</label><select id="cd-stage"><option value="">All stages</option>' +
        CLIENT_STAGES.map(s => '<option value="' + s.value + '"' + (filters.stage === s.value ? ' selected' : '') + '>' + esc(s.label) + '</option>').join('') + '</select></div>' +
        '<div class="field"><label>Service</label><select id="cd-service"><option value="">All services</option>' +
        CLIENT_SERVICE_TYPES.map(s => '<option value="' + s.value + '"' + (filters.service === s.value ? ' selected' : '') + '>' + esc(s.label) + '</option>').join('') + '</select></div>' +
        '<div class="field"><label>&nbsp;</label><label class="check"><input type="checkbox" id="cd-urgent"' + (filters.urgentOnly ? ' checked' : '') + '> Urgent only</label></div>' +
        (state.boot && state.boot.user && state.boot.user.is_super_admin ? '<div class="field"><label>&nbsp;</label><label class="check"><input type="checkbox" id="cd-unclaimed"' +
          (filters.unclaimedOnly ? ' checked' : '') + '> Unclaimed only</label></div>' : '') +
        '</div></div>';

      if (editingId !== null) {
        h += '<div class="card"><h3>' + (editingId === 'new' ? 'Add a client' : 'Edit client') + '</h3>' +
          clientFormHtml(draft, 'client-form', dir.staff, countries) +
          '<div style="margin-top:12px"><button class="btn btn-primary" id="save-client">Save</button> ' +
          '<button class="btn" id="cancel-client">Cancel</button></div></div>';
      }

      if (!rows.length) {
        h += '<div class="empty"><b>No clients match</b>Adjust the filters, or add a client to get started.</div>';
      }

      rows.forEach(c => {
        const nd = (c.deadlines || [])[0] ? cdDaysUntil(c.deadlines[0].due_date_ad) : null;
        h += '<div class="card" style="margin-top:10px">' +
          '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
          '<input type="checkbox" data-sel="' + c.id + '"' + (selected.has(c.id) ? ' checked' : '') + '>' +
          (c.urgent ? '<span class="tag tag-red">URGENT</span>' : '') +
          '<b>' + esc(c.name) + '</b><span class="mono-tag">' + esc(c.code) + '</span>' +
          '<span class="tag">' + esc(labelOf(CLIENT_STAGES, c.stage) || c.stage || '—') + '</span>' +
          (c.vat_filing_frequency ? '<span class="tag">VAT ' + esc(labelOf(CLIENT_VAT_FREQ, c.vat_filing_frequency)) + '</span>' : '') +
          (c.assigned_to_name ? '<span class="hint">Looked by ' + esc(c.assigned_to_name) + '</span>' : '<span class="hint">Unassigned</span>') +
          ((c.deadlines || []).length ? cdDeadlineTag(nd) : '') +
          ((c.followups || []).length ? '<span class="tag tag-amber">' + c.followups.length + ' follow-up' + (c.followups.length > 1 ? 's' : '') + '</span>' : '') +
          cdAuditBadge(c.audit_workfiles) +
          '</div>';

        h += '<details' + (expandedIds.has(c.id) ? ' open' : '') + ' data-client-card="' + c.id + '" style="margin-top:10px">' +
          '<summary>Contact, reminders, follow-ups and credentials</summary>';

        h += '<div class="grid c3" style="margin-top:12px">' +
          '<div><span class="hint">Entity type</span><br>' + esc(labelOf(CLIENT_ENTITY_TYPES, c.entity_type) || '—') + '</div>' +
          '<div><span class="hint">PAN</span><br>' + esc(c.pan || '—') + '</div>' +
          '<div><span class="hint">Industry</span><br>' + esc(c.industry || '—') + '</div>' +
          '<div><span class="hint">IRD office</span><br>' + esc(c.ird_office || '—') + '</div>' +
          '<div><span class="hint">Focal person</span><br>' + esc(c.contact_person || '—') +
            (c.contact_phone ? ' · ' + esc(c.contact_phone) : '') + (c.contact_email ? ' · ' + esc(c.contact_email) : '') + '</div>' +
          '<div><span class="hint">Service types</span><br>' + ((c.service_types || []).map(s => labelOf(CLIENT_SERVICE_TYPES, s)).join(', ') || '—') + '</div>' +
          '</div>' +
          '<div style="margin-top:10px"><button class="btn btn-small" data-edit-client="' + c.id + '">Edit</button> ' +
          '<button class="btn btn-small btn-danger" data-del-client="' + c.id + '" title="Permanently delete this client and everything linked to it — engagements, workfiles, documents, reminders">Delete</button></div>';

        // sharing — only the owner (assigned_to) or a super admin can add/remove
        // collaborators; everyone who can see this card sees who else can.
        const isOwner = state.user && c.assigned_to && String(c.assigned_to) === String(state.user.id);
        const canManageSharing = isOwner || (state.boot && state.boot.user && state.boot.user.is_super_admin);
        const shareOpts = (dir.staff || []).filter(s =>
          String(s.id) !== String(c.assigned_to) && !(c.collaborators || []).some(cc => String(cc.user_id) === String(s.id)));
        h += '<div style="margin-top:14px" title="Nobody but the owner can see this client\'s data, files or computations until they\'re added here"><b style="font-size:12px">Shared with</b><div style="margin-top:6px">' +
          ((c.collaborators || []).length
            ? c.collaborators.map(cc => '<span class="tag">' + esc(cc.full_name) +
                (canManageSharing ? ' <a href="#" data-remove-collab="' + c.id + ',' + cc.user_id + '" style="margin-left:4px" title="Remove this person\'s access to this client">×</a>' : '') + '</span>').join(' ')
            : '<span class="hint">Nobody else — only ' + esc(c.assigned_to_name || 'the owner') + ' can see this client.</span>') +
          '</div>' +
          (canManageSharing && shareOpts.length ? '<div style="margin-top:8px"><select data-share-pick="' + c.id + '" title="Choose a colleague to give access to this client">' +
            '<option value="">— add a colleague —</option>' +
            shareOpts.map(s => '<option value="' + s.id + '">' + esc(s.full_name) + '</option>').join('') +
            '</select> <button class="btn btn-small" data-share-go="' + c.id + '" title="Grant the selected person full access to this client">Share</button></div>' : '') +
          '</div>';

        // reminders
        h += '<div class="table-wrap" style="margin-top:14px"><b style="font-size:12px">Reminders</b><table><thead><tr>' +
          '<th>Title</th><th>Due</th><th>Status</th><th></th></tr></thead><tbody>';
        (c.deadlines || []).forEach(d => {
          h += '<tr><td>' + esc(d.title) + (d.period_label ? ' <span class="hint">' + esc(d.period_label) + '</span>' : '') + '</td>' +
            '<td>' + esc(d.due_date_ad || '') + '</td><td>' + cdDeadlineTag(cdDaysUntil(d.due_date_ad)) + '</td>' +
            '<td><button class="btn btn-small" data-dl-filed="' + d.id + '">Mark filed</button> ' +
            '<button class="btn btn-small btn-danger" data-dl-del="' + d.id + '">Remove</button></td></tr>';
        });
        if (!(c.deadlines || []).length) h += '<tr><td colspan="4" class="card-note">No open reminders.</td></tr>';
        h += '</tbody></table></div>';
        const rd = draftFor(reminderDrafts, c.id, { type: 'VAT_MONTHLY', title: 'VAT filing', due_date_ad: '', period_label: '' });
        h += '<div class="grid c4" style="margin-top:8px">' +
          '<div class="field"><label>Type</label><select data-rd="' + c.id + '" data-k="type">' +
          CLIENT_REMINDER_TYPES.map(t => '<option value="' + t.value + '"' + (rd.type === t.value ? ' selected' : '') + '>' + esc(t.label) + '</option>').join('') + '</select></div>' +
          '<div class="field"><label>Title</label><input data-rd="' + c.id + '" data-k="title" value="' + esc(rd.title) + '"></div>' +
          '<div class="field"><label>Due date</label><input type="date" data-rd="' + c.id + '" data-k="due_date_ad" value="' + esc(rd.due_date_ad) + '"></div>' +
          '<div class="field"><label>&nbsp;</label><button class="btn btn-small" data-add-reminder="' + c.id + '">Add reminder</button></div>' +
          '</div>';

        // follow-ups
        h += '<div class="table-wrap" style="margin-top:14px"><b style="font-size:12px">Document follow-ups</b><table><thead><tr>' +
          '<th>Item</th><th>Due</th><th>Requested by</th><th></th></tr></thead><tbody>';
        (c.followups || []).forEach(f => {
          h += '<tr><td>' + esc(f.item) + (f.note ? ' <span class="hint">' + esc(f.note) + '</span>' : '') + '</td>' +
            '<td>' + esc(f.due_date || '—') + '</td><td>' + esc(f.created_by_name || '') + '</td>' +
            '<td><button class="btn btn-small" data-fu-received="' + f.id + '">Received</button> ' +
            '<button class="btn btn-small btn-danger" data-fu-del="' + f.id + '">Remove</button></td></tr>';
        });
        if (!(c.followups || []).length) h += '<tr><td colspan="4" class="card-note">Nothing outstanding.</td></tr>';
        h += '</tbody></table></div>';
        const fd = draftFor(followupDrafts, c.id, { item: '', due_date: '' });
        h += '<div class="grid c3" style="margin-top:8px">' +
          '<div class="field"><label>What\'s being chased</label><input data-fd="' + c.id + '" data-k="item" value="' + esc(fd.item) + '" placeholder="Signed management letter, bank statements..."></div>' +
          '<div class="field"><label>Due date</label><input type="date" data-fd="' + c.id + '" data-k="due_date" value="' + esc(fd.due_date) + '"></div>' +
          '<div class="field"><label>&nbsp;</label><button class="btn btn-small" data-add-followup="' + c.id + '">Add follow-up</button></div>' +
          '</div>';

        // credentials
        h += '<div class="table-wrap" style="margin-top:14px"><b style="font-size:12px">Portal credentials (IRD / OCR / excise / custom)</b><table><thead><tr>' +
          '<th>System</th><th>Username</th><th>Password</th><th></th></tr></thead><tbody>';
        (c.credentials || []).forEach(cr => {
          h += '<tr><td>' + esc(cr.system) + '</td><td>' + esc(cr.username || '') + '</td>' +
            '<td>' + (revealed[cr.id] !== undefined ? '<code>' + esc(revealed[cr.id]) + '</code>' : (cr.has_secret ? '••••••••' : '—')) + '</td>' +
            '<td>' + (cr.has_secret ? (revealed[cr.id] !== undefined
              ? '<button class="btn btn-small" data-cred-hide="' + cr.id + '">Hide</button>'
              : '<button class="btn btn-small" data-cred-reveal="' + cr.id + '">Reveal</button>') : '') + ' ' +
            '<button class="btn btn-small btn-danger" data-cred-del="' + cr.id + '">Remove</button></td></tr>';
        });
        if (!(c.credentials || []).length) h += '<tr><td colspan="4" class="card-note">No credentials stored yet — expand and add one below.</td></tr>';
        h += '</tbody></table></div>';
        const kd = draftFor(credentialDrafts, c.id, { system: 'IRD', username: '', password: '' });
        h += '<div class="grid c4" style="margin-top:8px">' +
          '<div class="field"><label>System</label><input data-kd="' + c.id + '" data-k="system" value="' + esc(kd.system) + '" placeholder="IRD / OCR / Excise / custom"></div>' +
          '<div class="field"><label>Username</label><input data-kd="' + c.id + '" data-k="username" value="' + esc(kd.username) + '"></div>' +
          '<div class="field"><label>Password</label><input type="password" data-kd="' + c.id + '" data-k="password" value="' + esc(kd.password) + '"></div>' +
          '<div class="field"><label>&nbsp;</label><button class="btn btn-small" data-add-credential="' + c.id + '">Save credential</button></div>' +
          '</div>';

        h += '<div style="margin-top:14px"><b style="font-size:12px">Client portal access</b>' +
          '<div id="portal-users-' + c.id + '" style="margin-top:6px">' +
          (portalUsersCache[c.id] && portalUsersCache[c.id] !== 'loading' ? portalUsersHtml(c.id, portalUsersCache[c.id]) : '<span class="hint">Expand to load</span>') +
          '</div></div>';

        if ((c.audit_workfiles || []).length) {
          h += '<div class="table-wrap" style="margin-top:14px"><b style="font-size:12px">Audit engagements</b><table><thead><tr>' +
            '<th>Workfile</th><th>Type</th><th class="num">Completion</th><th class="num">Findings open</th>' +
            '<th>Materiality</th><th>Highest risk</th></tr></thead><tbody>';
          c.audit_workfiles.forEach(w => {
            const s = w.summary || {};
            const highest = (w.risk_assessment || []).reduce((m, r) =>
              (['low', 'medium', 'high'].indexOf(r.combined_risk) > ['low', 'medium', 'high'].indexOf(m)) ? r.combined_risk : m, '');
            h += '<tr><td>' + esc(w.title || '') + '</td><td>' + esc(w.module_key === 'stat_audit' ? 'Statutory' : 'Internal') + '</td>' +
              '<td class="num">' + (s.completion_pct != null ? s.completion_pct.toFixed(0) + '%' : '—') + '</td>' +
              '<td class="num">' + (s.findings_open || 0) + '</td>' +
              '<td>' + (w.materiality ? money(w.materiality.performance_materiality) : '—') + '</td>' +
              '<td>' + (highest ? '<span class="tag ' + (highest === 'high' ? 'tag-red' : highest === 'medium' ? 'tag-amber' : 'tag-green') + '">' + highest.toUpperCase() + '</span>' : '—') + '</td></tr>';
          });
          h += '</tbody></table></div>';
        }

        h += '</details></div>';
      });

      view.innerHTML = h;
      wireDraw();
      if (focusWasSearch) {
        const el = view.querySelector('#cd-q');
        if (el) { el.focus(); el.setSelectionRange(caret, caret); }
      }
    }

    function wireDraw() {
      const byId = id => (dir.clients || []).find(x => x.id == id);

      const refreshBtn = view.querySelector('#refresh-dir'); if (refreshBtn) refreshBtn.addEventListener('click', refresh);

      const q = view.querySelector('#cd-q'); if (q) q.addEventListener('input', e => { filters.q = e.target.value; draw(); });
      const stageSel = view.querySelector('#cd-stage'); if (stageSel) stageSel.addEventListener('change', e => { filters.stage = e.target.value; draw(); });
      const svcSel = view.querySelector('#cd-service'); if (svcSel) svcSel.addEventListener('change', e => { filters.service = e.target.value; draw(); });
      const urgentChk = view.querySelector('#cd-urgent'); if (urgentChk) urgentChk.addEventListener('change', e => { filters.urgentOnly = e.target.checked; draw(); });
      const unclaimedChk = view.querySelector('#cd-unclaimed'); if (unclaimedChk) unclaimedChk.addEventListener('change', e => { filters.unclaimedOnly = e.target.checked; draw(); });

      view.querySelectorAll('[data-sel]').forEach(el => el.addEventListener('change', e => {
        const id = +e.target.dataset.sel;
        if (e.target.checked) selected.add(id); else selected.delete(id);
        draw();
      }));
      const expBtn = view.querySelector('#export-selected');
      if (expBtn) expBtn.addEventListener('click', async () => {
        if (!state.online || state.offlineMode) { toast('Exporting needs a connection', true); return; }
        try {
          const res = await api('/api/clients/export', { method: 'POST', body: { ids: Array.from(selected) } });
          const a = document.createElement('a'); a.href = res.download_url; a.download = res.filename;
          document.body.appendChild(a); a.click(); a.remove();
          toast('Exported ' + selected.size + ' client' + (selected.size > 1 ? 's' : ''));
        } catch (e) { toast(e.message, true); }
      });

      const newBtn = view.querySelector('#new-client');
      if (newBtn) newBtn.addEventListener('click', () => { editingId = 'new'; draft = { entity_type: '', status: 'active', stage: 'active', service_types: [] }; draw(); });

      if (editingId !== null) {
        wireClientForm(view, 'client-form', draft);
        view.querySelector('#cancel-client').addEventListener('click', () => { editingId = null; draft = {}; draw(); });
        view.querySelector('#save-client').addEventListener('click', async () => {
          if (!state.online || state.offlineMode) { toast('Saving a client needs a connection', true); return; }
          // A logo uploaded via wireImageUpload sets the URL field's value
          // programmatically (no native `change` event fires from that),
          // so wireClientForm's change-listener sync alone would miss it —
          // a final fresh sweep here, same as every other save button in
          // this app, catches it regardless of how the value got there.
          view.querySelectorAll('#client-form [data-key]').forEach(el => { draft[el.dataset.key] = el.value; });
          if (!(draft.name || '').trim()) { toast('A client needs a name', true); return; }
          try {
            if (editingId === 'new') { await api('/api/clients', { method: 'POST', body: draft }); toast('Added ' + draft.name); }
            else { await api('/api/clients/' + editingId, { method: 'PUT', body: draft }); toast('Saved ' + draft.name); }
            editingId = null; draft = {};
            await refresh();
          } catch (e) { toast(e.message, true); }
        });
      }

      view.querySelectorAll('[data-client-card]').forEach(det => det.addEventListener('toggle', () => {
        const id = +det.dataset.clientCard;
        if (det.open) { expandedIds.add(id); if (portalUsersCache[id] === undefined) loadPortalUsers(id); }
        else expandedIds.delete(id);
      }));
      // A card already expanded before this redraw (e.g. after saving a
      // credential) needs its portal-users section re-wired, not re-fetched
      // — the cache already has the data, draw() already re-rendered it.
      Array.from(expandedIds).forEach(id => { if (portalUsersCache[id] && portalUsersCache[id] !== 'loading') wirePortalUsers(id); });

      view.querySelectorAll('[data-edit-client]').forEach(b => b.addEventListener('click', e => {
        const c = byId(e.target.dataset.editClient);
        if (!c) return;
        editingId = c.id; draft = Object.assign({}, c, { service_types: (c.service_types || []).slice() }); draw();
      }));
      view.querySelectorAll('[data-del-client]').forEach(b => b.addEventListener('click', async e => {
        const id = e.target.dataset.delClient;
        const c = byId(id);
        if (!state.online || state.offlineMode) { toast('Deleting a client needs a connection', true); return; }
        if (!confirm('Delete ' + (c ? c.name : 'this client') + '? This also permanently removes every engagement, ' +
          'workfile, saved computation, document, reminder and credential linked to this client. This cannot be undone — ' +
          'consider setting status to Archived instead if you just want it out of the active list.')) return;
        try {
          await api('/api/clients/' + id, { method: 'DELETE' });
          if (editingId == id) { editingId = null; draft = {}; }
          selected.delete(+id); expandedIds.delete(+id);
          toast('Deleted');
          await refresh();
        } catch (err) { toast(err.message, true); }
      }));

      view.querySelectorAll('[data-share-go]').forEach(b => b.addEventListener('click', async e => {
        const cid = e.target.dataset.shareGo;
        const sel = view.querySelector('[data-share-pick="' + cid + '"]');
        if (!sel || !sel.value) { toast('Pick a colleague to share with first', true); return; }
        if (!state.online || state.offlineMode) { toast('Sharing needs a connection', true); return; }
        try {
          await api('/api/clients/' + cid + '/collaborators', { method: 'POST', body: { user_id: +sel.value } });
          expandedIds.add(+cid);
          toast('Shared'); await refresh();
        } catch (err) { toast(err.message, true); }
      }));
      view.querySelectorAll('[data-remove-collab]').forEach(a => a.addEventListener('click', async e => {
        e.preventDefault();
        const [cid, uid] = e.target.dataset.removeCollab.split(',');
        if (!state.online || state.offlineMode) { toast('This needs a connection', true); return; }
        try {
          await api('/api/clients/' + cid + '/collaborators/' + uid, { method: 'DELETE' });
          expandedIds.add(+cid);
          toast('Removed'); await refresh();
        } catch (err) { toast(err.message, true); }
      }));

      view.querySelectorAll('input[data-rd]').forEach(el => el.addEventListener('input', e => {
        draftFor(reminderDrafts, e.target.dataset.rd, {})[e.target.dataset.k] = e.target.value;
      }));
      view.querySelectorAll('select[data-rd]').forEach(el => el.addEventListener('change', e => {
        const rd = draftFor(reminderDrafts, e.target.dataset.rd, {});
        rd.type = e.target.value; rd.title = labelOf(CLIENT_REMINDER_TYPES, e.target.value);
        draw();
      }));
      view.querySelectorAll('[data-add-reminder]').forEach(b => b.addEventListener('click', async e => {
        const cid = e.target.dataset.addReminder;
        const rd = draftFor(reminderDrafts, cid, {});
        if (!rd.title || !rd.due_date_ad) { toast('A reminder needs a title and a due date', true); return; }
        if (!state.online || state.offlineMode) { toast('Adding a reminder needs a connection', true); return; }
        try {
          await api('/api/deadlines', { method: 'POST', body: {
            client_id: +cid, title: rd.title, due_date_ad: rd.due_date_ad,
            obligation_code: (rd.type === 'VAT_MONTHLY' || rd.type === 'STAT_AUDIT') ? rd.type : null } });
          delete reminderDrafts[cid];
          expandedIds.add(+cid);
          toast('Reminder added');
          await refresh();
        } catch (err) { toast(err.message, true); }
      }));
      view.querySelectorAll('[data-dl-filed]').forEach(b => b.addEventListener('click', async e => {
        if (!state.online || state.offlineMode) { toast('Needs a connection', true); return; }
        try { await api('/api/deadlines/' + e.target.dataset.dlFiled, { method: 'PUT', body: { status: 'filed' } }); toast('Marked filed'); await refresh(); }
        catch (err) { toast(err.message, true); }
      }));
      view.querySelectorAll('[data-dl-del]').forEach(b => b.addEventListener('click', async e => {
        if (!state.online || state.offlineMode) { toast('Needs a connection', true); return; }
        if (!confirm('Remove this reminder?')) return;
        try { await api('/api/deadlines/' + e.target.dataset.dlDel, { method: 'DELETE' }); toast('Removed'); await refresh(); }
        catch (err) { toast(err.message, true); }
      }));

      view.querySelectorAll('[data-fd]').forEach(el => el.addEventListener('input', e => {
        const cid = e.target.dataset.fd, k = e.target.dataset.k;
        draftFor(followupDrafts, cid, {})[k] = e.target.value;
      }));
      view.querySelectorAll('[data-add-followup]').forEach(b => b.addEventListener('click', async e => {
        const cid = e.target.dataset.addFollowup;
        const fd = draftFor(followupDrafts, cid, {});
        if (!(fd.item || '').trim()) { toast('Say what you\'re chasing', true); return; }
        if (!state.online || state.offlineMode) { toast('Adding a follow-up needs a connection', true); return; }
        try {
          await api('/api/clients/' + cid + '/followups', { method: 'POST', body: { item: fd.item, due_date: fd.due_date } });
          delete followupDrafts[cid];
          expandedIds.add(+cid);
          toast('Follow-up added');
          await refresh();
        } catch (err) { toast(err.message, true); }
      }));
      view.querySelectorAll('[data-fu-received]').forEach(b => b.addEventListener('click', async e => {
        if (!state.online || state.offlineMode) { toast('Needs a connection', true); return; }
        try { await api('/api/followups/' + e.target.dataset.fuReceived, { method: 'PUT', body: { status: 'received' } }); toast('Marked received'); await refresh(); }
        catch (err) { toast(err.message, true); }
      }));
      view.querySelectorAll('[data-fu-del]').forEach(b => b.addEventListener('click', async e => {
        if (!state.online || state.offlineMode) { toast('Needs a connection', true); return; }
        try { await api('/api/followups/' + e.target.dataset.fuDel, { method: 'DELETE' }); toast('Removed'); await refresh(); }
        catch (err) { toast(err.message, true); }
      }));

      view.querySelectorAll('[data-kd]').forEach(el => el.addEventListener('input', e => {
        const cid = e.target.dataset.kd, k = e.target.dataset.k;
        draftFor(credentialDrafts, cid, {})[k] = e.target.value;
      }));
      view.querySelectorAll('[data-add-credential]').forEach(b => b.addEventListener('click', async e => {
        const cid = e.target.dataset.addCredential;
        const kd = draftFor(credentialDrafts, cid, {});
        if (!(kd.system || '').trim()) { toast('Give the credential a system label', true); return; }
        if (!state.online || state.offlineMode) { toast('Saving a credential needs a connection', true); return; }
        try {
          await api('/api/clients/' + cid + '/credentials', { method: 'POST', body: kd });
          delete credentialDrafts[cid];
          expandedIds.add(+cid);
          toast('Credential saved');
          await refresh();
        } catch (err) { toast(err.message, true); }
      }));
      view.querySelectorAll('[data-cred-reveal]').forEach(b => b.addEventListener('click', async e => {
        const id = e.target.dataset.credReveal;
        if (!state.online || state.offlineMode) { toast('Revealing a credential needs a connection', true); return; }
        try { const r = await api('/api/credentials/' + id + '/reveal'); revealed[id] = r.password; draw(); }
        catch (err) { toast(err.message, true); }
      }));
      view.querySelectorAll('[data-cred-hide]').forEach(b => b.addEventListener('click', e => {
        delete revealed[e.target.dataset.credHide]; draw();
      }));
      view.querySelectorAll('[data-cred-del]').forEach(b => b.addEventListener('click', async e => {
        if (!state.online || state.offlineMode) { toast('Needs a connection', true); return; }
        if (!confirm('Remove this credential?')) return;
        try { await api('/api/credentials/' + e.target.dataset.credDel, { method: 'DELETE' }); toast('Removed'); await refresh(); }
        catch (err) { toast(err.message, true); }
      }));
    }

    refresh();
  }

  function renderRates() {
    const view = document.getElementById('view');
    const rows = state.rates;
    // Rate Master is shared, platform-wide statutory data (tax slabs, fees —
    // used by every tenant firm's computations), not a firm's own record —
    // so editing it is gated by is_platform_admin, not a firm's own
    // is_super_admin. A firm's own admin sees and remarks on it exactly
    // like any other staff member; only CA-Automator can change a rate.
    const isAdmin = !!(state.boot && state.boot.user && state.boot.user.is_platform_admin);
    const byDomain = {};
    rows.forEach(r => { (byDomain[r.domain] = byDomain[r.domain] || []).push(r); });

    const knownDomains = Array.from(new Set(rows.map(r => r.domain))).sort();

    // Offer a one-click carry-forward whenever the server's current FY has
    // noticeably fewer rate rows than the FY right before it — the signature
    // of a new fiscal year nobody has rolled rates into yet. Platform-admin-
    // only, like every other rate_master write.
    const currentFy = state.boot && state.boot.current_fy;
    const fyList = ((state.boot && state.boot.fiscal_years) || []).map(f => f.label_bs).sort();
    const currentIdx = fyList.indexOf(currentFy);
    const prevFy = currentIdx > 0 ? fyList[currentIdx - 1] : null;
    const currentCount = rows.filter(r => r.fiscal_year_label === currentFy).length;
    const prevCount = prevFy ? rows.filter(r => r.fiscal_year_label === prevFy).length : 0;
    const showRollover = isAdmin && currentFy && prevFy && currentCount < prevCount;

    view.innerHTML =
      '<div class="page-head"><div><h2>Rate master</h2><p>Every rate the engines use lives here, with its source and the date you last checked it. Nothing regulatory is written into the code, so a Finance Act change — a new slab, a new company-rate category, a new pool — is a data edit here, not a rebuild.</p></div></div>' +
      '<div class="banner"><b>' + rows.filter(r => r.verification_status === 'UNVERIFIED').length + ' rates are still unverified.</b> ' +
      'They are seeded placeholders so the engines run. Check each against the Act, correct the value, then mark it verified. Until you do, every computation and every exported report carries a warning.</div>' +

      (isAdmin ? '' : '<div class="card-note">Rate Master is maintained by CA-Automator — it\'s shared statutory data used by every firm\'s computations, so no firm (including your own super admin) can edit it. You can view every rate; if you think a figure is wrong, leave a remark and CA-Automator will review it.</div>') +

      (showRollover ?
        '<div class="banner"><b>FY ' + esc(currentFy) + ' has ' + currentCount + ' rate' + (currentCount === 1 ? '' : 's') +
        ', against ' + prevCount + ' in FY ' + esc(prevFy) + '.</b> Carry last year\'s rates forward as fresh, unverified placeholders for the new year — nothing is treated as confirmed until you check it against the FY ' +
        esc(currentFy) + ' Finance Act. ' +
        '<button class="btn btn-small" id="rate-rollover-go" title="Copies every rate from FY ' + esc(prevFy) + ' into FY ' + esc(currentFy) + ' as fresh unverified placeholders — a one-time step you do once at the start of each new fiscal year">Carry FY ' + esc(prevFy) + ' rates into FY ' + esc(currentFy) + '</button></div>' : '') +

      (isAdmin ?
        '<details class="card"><summary><b>Add a new rate</b><span class="hint"> — a slab, a category, a pool: anything the engines discover dynamically by code pattern</span></summary>' +
        '<div class="grid c3" style="margin-top:12px" id="rate-add">' +
        field('Domain', 'domain', {}, 'text', { placeholder: knownDomains.join(', ') }) +
        field('Code', 'code', {}, 'text', { hint: 'e.g. slab_6_ceiling, company_rate_telecom, pool_f_rate — must match the pattern the engine looks for' }) +
        field('Label', 'label', {}, 'text') +
        field('Value (number)', 'value_num', {}, 'number') +
        field('Value (text, if paired — e.g. a slab\'s rate)', 'value_text', {}, 'text') +
        field('Unit', 'unit', {}, 'text', { placeholder: 'percent / amount / ratio / years' }) +
        field('Applies to', 'applies_to', {}, 'text', { placeholder: 'all / individual / couple / company / pool_a' }) +
        field('Fiscal year', 'fiscal_year_label', { fiscal_year_label: state.fy }, 'text') +
        field('Source', 'source', {}, 'text') +
        field('Clause', 'clause', {}, 'text') +
        '</div><button class="btn btn-primary" id="rate-add-go" style="margin-top:10px">Add rate</button> ' +
        '<span class="hint" id="rate-add-msg"></span></details>' : '') +

      Object.keys(byDomain).map(d =>
        '<div class="card"><h3>' + esc(d.replace('_', ' ')) + '</h3><div class="table-wrap"><table><thead><tr>' +
        '<th>Rate</th><th>Code</th><th class="num">Value</th><th>Applies to</th><th>Clause</th><th>Status</th><th>Last verified</th><th></th></tr></thead><tbody>' +
        byDomain[d].map(r => {
          const valueCell = isAdmin
            ? '<td class="num"><input type="number" step="any" data-rate="' + r.id + '" value="' + (r.value_num !== null ? r.value_num : '') + '"></td>'
            : '<td class="num">' + esc(r.value_num !== null && r.value_num !== undefined ? r.value_num : (r.value_text || '—')) + '</td>';
          const actionCell = isAdmin
            ? '<td><button class="btn btn-small" data-verify="' + r.id + '" title="Confirm this value is correct against the current Act/circular, using whatever is in the box to the left">Mark verified</button> ' +
              '<button class="btn btn-small btn-danger" data-del-rate="' + r.id + '" title="Delete this rate row entirely — any computation still referring to its code falls back to zero">Remove</button></td>'
            : '<td><button class="btn btn-small" data-remark-toggle="' + r.id + '" title="Tell your admin you think this figure is wrong, and why — you can\'t edit it yourself">Remark</button></td>';
          return '<tr><td>' + esc(r.label) + '</td><td><code class="mono-tag">' + esc(r.code) + '</code></td>' +
            valueCell +
            '<td>' + esc(r.applies_to || '') + '</td><td>' + esc(r.clause || '') + '</td>' +
            '<td><span class="tag ' + (r.verification_status === 'PRIMARY' ? 'tag-green' : r.verification_status === 'SECONDARY' ? 'tag-amber' : 'tag-red') + '">' +
            esc(r.verification_status) + '</span></td>' +
            '<td>' + esc(r.last_verified_on || '—') + '</td>' + actionCell + '</tr>' +
            (isAdmin ? '' : '<tr class="remark-row" data-remark-panel="' + r.id + '" hidden><td colspan="8">' +
              '<div data-remark-body="' + r.id + '"></div></td></tr>');
        }).join('') +
        '</tbody></table></div></div>').join('');

    if (isAdmin) {
      view.querySelector('#rate-add-go').addEventListener('click', async () => {
        if (!state.online || state.offlineMode) { toast('Adding a rate needs a connection', true); return; }
        const container = view.querySelector('#rate-add');
        const body = {};
        container.querySelectorAll('[data-key]').forEach(el => { body[el.dataset.key] = el.value; });
        if (body.value_num !== '') body.value_num = Number(body.value_num);
        const msg = view.querySelector('#rate-add-msg');
        msg.textContent = '';
        try {
          const r = await api('/api/rates', { method: 'POST', body });
          state.rates.push(r);
          await idbPut('kv', state.rates, 'rates');
          renderRates(); toast('Added ' + r.domain + '.' + r.code);
        } catch (err) { msg.textContent = err.message; }
      });

      const rolloverBtn = view.querySelector('#rate-rollover-go');
      if (rolloverBtn) rolloverBtn.addEventListener('click', async () => {
        if (!state.online || state.offlineMode) { toast('Carrying rates forward needs a connection', true); return; }
        rolloverBtn.disabled = true; rolloverBtn.textContent = 'Carrying forward…';
        try {
          const r = await api('/api/rates/rollover', { method: 'POST', body: { from_fy: prevFy, to_fy: currentFy } });
          const boot = await api('/api/bootstrap');
          state.boot = boot; state.rates = boot.rates || [];
          await idbPut('kv', boot, 'boot');
          await idbPut('kv', state.rates, 'rates');
          renderRates();
          toast(r.inserted + ' rate' + (r.inserted === 1 ? '' : 's') + ' carried into FY ' + currentFy + ' as unverified');
        } catch (err) { toast(err.message, true); rolloverBtn.disabled = false; rolloverBtn.textContent = 'Carry FY ' + prevFy + ' rates into FY ' + currentFy; }
      });

      view.querySelectorAll('[data-verify]').forEach(b => b.addEventListener('click', async e => {
        const id = e.target.dataset.verify;
        const input = view.querySelector('[data-rate="' + id + '"]');
        if (!state.online || state.offlineMode) { toast('Verifying a rate needs a connection', true); return; }
        try {
          const r = await api('/api/rates/' + id, { method: 'PUT', body: {
            value_num: Number(input.value), verification_status: 'PRIMARY' } });
          const i = state.rates.findIndex(x => x.id == id);
          if (i >= 0) state.rates[i] = r;
          await idbPut('kv', state.rates, 'rates');
          renderRates(); toast('Marked verified');
        } catch (err) { toast(err.message, true); }
      }));

      view.querySelectorAll('[data-del-rate]').forEach(b => b.addEventListener('click', async e => {
        const id = e.target.dataset.delRate;
        if (!state.online || state.offlineMode) { toast('Removing a rate needs a connection', true); return; }
        if (!confirm('Remove this rate? Any computation still referring to its code will fall back to zero.')) return;
        try {
          await api('/api/rates/' + id, { method: 'DELETE' });
          state.rates = state.rates.filter(x => x.id != id);
          await idbPut('kv', state.rates, 'rates');
          renderRates(); toast('Removed');
        } catch (err) { toast(err.message, true); }
      }));
    } else {
      async function loadRemarkPanel(id) {
        const body = view.querySelector('[data-remark-body="' + id + '"]');
        if (!state.online || state.offlineMode) { body.innerHTML = '<div class="card-note">Remarks need a connection.</div>'; return; }
        body.innerHTML = '<div class="card-note">Loading…</div>';
        try {
          const remarks = await api('/api/rates/' + id + '/remarks');
          body.innerHTML =
            (remarks.length ? remarks.map(r =>
              '<div class="rl"><div class="rl-main"><b>' + esc(r.submitted_by_name) + '</b> · ' + esc(r.created_at) +
              (r.status === 'resolved' ? ' <span class="tag tag-green">Resolved</span>' : ' <span class="tag tag-amber">Open</span>') +
              '<p>' + esc(r.remark) + '</p>' +
              (r.resolution_note ? '<p class="hint">Admin: ' + esc(r.resolution_note) + '</p>' : '') +
              '</div></div>').join('') : '<div class="card-note">No remarks yet on this rate.</div>') +
            '<div class="grid c3" style="margin-top:8px"><textarea data-new-remark="' + id + '" placeholder="What do you think changed, and why?" style="grid-column:1/3"></textarea>' +
            '<button class="btn btn-small" data-submit-remark="' + id + '" style="align-self:end">Submit</button></div>';
          body.querySelector('[data-submit-remark]').addEventListener('click', async () => {
            const ta = body.querySelector('[data-new-remark]');
            const text = ta.value.trim();
            if (!text) { toast('Write what you think is wrong first', true); return; }
            try {
              await api('/api/rates/' + id + '/remarks', { method: 'POST', body: { remark: text } });
              toast('Remark sent to your admin');
              loadRemarkPanel(id);
            } catch (err) { toast(err.message, true); }
          });
        } catch (err) { body.innerHTML = '<div class="card-note">' + esc(err.message) + '</div>'; }
      }
      view.querySelectorAll('[data-remark-toggle]').forEach(b => b.addEventListener('click', e => {
        const id = e.target.dataset.remarkToggle;
        const panel = view.querySelector('[data-remark-panel="' + id + '"]');
        if (!panel.hidden) { panel.hidden = true; return; }
        panel.hidden = false;
        loadRemarkPanel(id);
      }));
    }
  }

  // CA-Automator's own operator screen — every tenant firm subscribed to
  // the platform, its seat cap/plan, which modules it's entitled to, and
  // (per the user's explicit requirement) the staff registered under each
  // firm's own name. Only reachable when state.boot.user.is_platform_admin.
  function SUB_STATUS_TAG(f) {
    if (!f.subscription_expires_at) return '<span class="tag">Perpetual</span>';
    if (f.subscription_status === 'grace') return '<span class="tag tag-amber">Grace — until ' + esc(f.subscription_grace_ends_at) + '</span>';
    if (f.subscription_status === 'suspended') return '<span class="tag tag-red">Suspended</span>';
    return '<span class="tag tag-green">Subscribed until ' + esc(f.subscription_expires_at) + '</span>';
  }

  function renderPlatform() {
    const view = document.getElementById('view');
    const grantableKeys = Object.keys(GRANTABLE_MODULES);
    view.innerHTML =
      '<div class="page-head"><div><h2>Platform Admin</h2><p>Every firm subscribed to CA-Automator, their seat limit and plan, and which modules each is entitled to. Only a platform admin sees this screen.</p></div></div>' +

      '<details class="card"><summary><b>Create a firm</b><span class="hint"> — creates the tenant and its first (owner) user, and hands you an invite link</span></summary>' +
      '<div class="grid c3" style="margin-top:12px" id="platform-add-firm">' +
      field('Firm name', 'name', {}, 'text') +
      field('Max seats', 'max_seats', { max_seats: 5 }, 'number') +
      field('Plan label', 'plan_label', {}, 'text', { placeholder: 'e.g. Starter, Growth, Enterprise' }) +
      field('Owner full name', 'owner_name', {}, 'text') +
      field('Owner email', 'owner_email', {}, 'text') +
      field('Subscription expires', 'subscription_expires_at', {}, 'date', { hint: 'Leave blank for a perpetual (never-expiring) subscription' }) +
      field('Phone', 'phone', {}, 'text') +
      field('PAN', 'pan', {}, 'text') +
      field('Business registration no.', 'registration_no', {}, 'text') +
      '</div>' +
      imageUploadHtml('logo_path', 'Firm logo', '', 'JPEG, PNG or WebP only, up to 5MB. Used in the "Our Clients" showcase below, if shown there.') +
      '<label class="check" style="margin-top:10px"><input type="checkbox" id="platform-add-firm-showcase"> Show this firm\'s logo on the "Our Clients" landing-page strip</label>' +
      '<div class="card-note" style="margin-top:10px">Modules this firm can grant its own staff — the ceiling their own admin can never exceed</div>' +
      '<div class="grid c4" id="platform-add-firm-modules">' +
      grantableKeys.map(k => '<label class="check"><input type="checkbox" data-new-firm-mod="' + k + '"> ' + esc(GRANTABLE_MODULES[k].name) + '</label>').join('') +
      '</div>' +
      '<button class="btn btn-primary" id="platform-add-firm-go" style="margin-top:10px">Create firm</button> ' +
      '<span class="hint" id="platform-add-firm-msg"></span><div id="platform-invite-result"></div></details>' +

      '<div class="card" id="platform-firms-body"><div class="empty">Loading…</div></div>';

    wireImageUpload(document.getElementById('platform-add-firm'));

    document.getElementById('platform-add-firm-go').addEventListener('click', async () => {
      const b = {};
      document.getElementById('platform-add-firm').querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
      b.module_keys = Array.from(document.querySelectorAll('[data-new-firm-mod]:checked')).map(i => i.dataset.newFirmMod);
      b.show_on_landing = document.getElementById('platform-add-firm-showcase').checked;
      const msg = document.getElementById('platform-add-firm-msg');
      msg.textContent = '';
      if (!b.name || !b.owner_name || !b.owner_email) { msg.textContent = 'Firm name and owner name/email are required'; return; }
      b.max_seats = Number(b.max_seats) || 5;
      try {
        const r = await api('/api/platform/firms', { method: 'POST', body: b });
        document.getElementById('platform-invite-result').innerHTML =
          '<div class="card-note" style="margin-top:8px">Owner invite link: <code>' + esc(r.owner_invite_link) + '</code></div>';
        toast('Firm created'); loadFirms();
      } catch (e) { msg.textContent = e.message; }
    });

    function loadFirmDetail(id) {
      const box = document.querySelector('[data-firm-detail-body="' + id + '"]');
      if (!box) return;
      box.innerHTML = '<div class="empty">Loading…</div>';
      api('/api/platform/firms/' + id).then(f => {
        box.innerHTML =
          '<div class="grid c3" data-firm-edit="' + id + '">' +
          field('Firm name', 'name', f, 'text') +
          field('Max seats', 'max_seats', f, 'number') +
          field('Plan label', 'plan_label', f, 'text') +
          field('Subscription expires', 'subscription_expires_at', f, 'date', { hint: 'Blank = perpetual, never expires' }) +
          field('Phone', 'phone', f, 'text') +
          field('PAN', 'pan', f, 'text') +
          field('Business registration no.', 'registration_no', f, 'text') +
          '</div>' +
          imageUploadHtml('logo_path', 'Firm logo', f.logo_path, 'JPEG, PNG or WebP only, up to 5MB. Used in the "Our Clients" showcase, if shown there.') +
          '<div class="hint">Status: ' + SUB_STATUS_TAG(f) + (f.subscription_grace_ends_at ? ' — grace ends ' + esc(f.subscription_grace_ends_at) : '') + '</div>' +
          '<label class="check" style="margin-top:8px"><input type="checkbox" data-firm-active' + (f.is_active ? ' checked' : '') + '> Active</label> ' +
          '<label class="check" style="margin-top:8px"><input type="checkbox" data-firm-showcase' + (f.show_on_landing ? ' checked' : '') +
          '> Show this firm\'s logo on the "Our Clients" landing-page strip</label>' +
          '<div><button class="btn btn-small" data-firm-save style="margin-top:10px">Save firm</button> <span class="hint" data-firm-msg></span></div>' +

          '<div class="card-note" style="margin-top:14px">Modules this firm is subscribed to — its own admin can only grant staff modules ticked here</div>' +
          '<div class="grid c4" data-firm-modules>' +
          grantableKeys.map(k => '<label class="check"><input type="checkbox" data-firm-mod="' + k + '"' +
            (f.modules.indexOf(k) !== -1 ? ' checked' : '') + '> ' + esc(GRANTABLE_MODULES[k].name) + '</label>').join('') +
          '</div>' +
          '<button class="btn btn-small" data-firm-modules-save style="margin-top:10px">Save modules</button> <span class="hint" data-firm-modules-msg></span>' +

          '<h4 style="margin-top:16px">Staff registered under this firm (' + f.staff.length + ')</h4>' +
          (f.staff.length ? '<div class="table-wrap"><table><thead><tr><th>Name</th><th>Email</th><th>Role</th><th>Status</th></tr></thead><tbody>' +
            f.staff.map(s => '<tr><td>' + esc(s.full_name) + (s.is_super_admin ? ' <span class="tag tag-green">Admin</span>' : '') + '</td>' +
              '<td>' + esc(s.email) + '</td><td>' + esc(s.role) + '</td>' +
              '<td>' + (s.is_active ? '<span class="tag tag-green">Active</span>' : '<span class="tag tag-red">Disabled</span>') + '</td></tr>').join('') +
            '</tbody></table></div>'
            : '<div class="empty">No staff registered yet.</div>');

        wireImageUpload(box);

        box.querySelector('[data-firm-save]').addEventListener('click', async () => {
          const edit = box.querySelector('[data-firm-edit="' + id + '"]');
          const b = {};
          box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          b.max_seats = Number(b.max_seats) || f.max_seats;
          b.is_active = box.querySelector('[data-firm-active]').checked;
          b.show_on_landing = box.querySelector('[data-firm-showcase]').checked;
          const msg = box.querySelector('[data-firm-msg]');
          msg.textContent = '';
          try { await api('/api/platform/firms/' + id, { method: 'PUT', body: b }); toast('Firm updated'); loadFirms(); }
          catch (e) { msg.textContent = e.message; }
        });

        box.querySelector('[data-firm-modules-save]').addEventListener('click', async () => {
          const mods = Array.from(box.querySelectorAll('[data-firm-mod]:checked')).map(i => i.dataset.firmMod);
          const msg = box.querySelector('[data-firm-modules-msg]');
          msg.textContent = '';
          try { await api('/api/platform/firms/' + id + '/modules', { method: 'PUT', body: { module_keys: mods } }); toast('Modules updated'); }
          catch (e) { msg.textContent = e.message; }
        });
      }).catch(e => { box.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawFirms(firms) {
      const body = document.getElementById('platform-firms-body');
      body.innerHTML = '<h3>Firms (' + firms.length + ')</h3>' + (firms.length ? firms.map(f =>
        '<details class="card" style="margin-top:10px" data-firm-details="' + f.id + '"><summary><b>' + esc(f.name) + '</b> ' +
        (f.is_active ? '<span class="tag tag-green">Active</span>' : '<span class="tag tag-red">Disabled</span>') + ' ' + SUB_STATUS_TAG(f) +
        '<span class="hint"> — ' + f.seats_used + '/' + f.max_seats + ' seats' + (f.plan_label ? ' · ' + esc(f.plan_label) : '') + '</span></summary>' +
        '<div data-firm-detail-body="' + f.id + '" class="empty">Loading…</div></details>'
      ).join('') : '<div class="empty">No firms yet.</div>');

      body.querySelectorAll('[data-firm-details]').forEach(det => det.addEventListener('toggle', () => {
        if (det.open) loadFirmDetail(det.dataset.firmDetails);
      }));
    }

    function loadFirms() {
      api('/api/platform/firms').then(drawFirms)
        .catch(e => { document.getElementById('platform-firms-body').innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }
    loadFirms();
  }

  function renderAdmin() {
    const view = document.getElementById('view');
    const moduleKeys = Object.keys(GRANTABLE_MODULES).filter(isFirmEntitled).filter(isModuleCountryApplicable);
    const isPlatformAdmin = !!(state.boot && state.boot.user && state.boot.user.is_platform_admin);
    view.innerHTML =
      '<div class="page-head"><div><h2>Admin</h2><p>Provision accounts, decide which automator each person can use, and resolve the corrections staff flag on Rate Master. Only a super admin sees this screen.</p></div>' +
      '<div style="display:flex;gap:16px;align-items:flex-end;flex-wrap:wrap">' + countrySwitcherHtml() + fySwitcherHtml() + '</div></div>' +

      '<details class="card"><summary><b>Add a user</b><span class="hint"> — creates the account and hands you an invite link (also emailed automatically if SMTP is configured)</span></summary>' +
      '<div class="grid c3" style="margin-top:12px" id="admin-add-user">' +
      field('Full name', 'full_name', {}, 'text') +
      field('Email', 'email', {}, 'text') +
      field('Role', 'role', { role: 'staff' }, 'select', { options: [
        { value: 'partner', label: 'Partner' }, { value: 'manager', label: 'Manager' },
        { value: 'staff', label: 'Staff' }, { value: 'readonly', label: 'Read-only' }] }) +
      '</div>' +
      '<div class="grid c3" style="margin-top:10px"><div><label>Start from a role template <span class="hint">(optional — ticks the matching modules below, still editable)</span>' +
      '<select id="admin-add-template"><option value="">— none —</option></select></div></div>' +
      '<div class="card-note" style="margin-top:10px">Modules this person can use — leave the date blank for permanent access, or set how long the grant should last' +
      (moduleKeys.length < Object.keys(GRANTABLE_MODULES).length ? ' <span class="hint">(only modules your firm is subscribed to are listed — ask CA-Automator to add more)</span>' : '') + '</div>' +
      '<div class="grid c4" id="admin-add-modules">' +
      moduleKeys.map(k => '<div><label class="check"><input type="checkbox" data-new-mod="' + k + '"> ' + esc(GRANTABLE_MODULES[k].name) + '</label>' +
        '<input type="date" data-new-mod-exp="' + k + '" style="margin-top:4px;width:100%" title="Valid until (blank = permanent)"></div>').join('') +
      '</div>' +
      '<button class="btn btn-primary" id="admin-add-go" style="margin-top:10px">Create user</button> ' +
      '<span class="hint" id="admin-add-msg"></span>' +
      '<div id="admin-invite-result"></div></details>' +

      '<div class="card" id="admin-users-body"><div class="empty">Loading…</div></div>' +
      '<div class="card" id="admin-templates-body"><div class="empty">Loading…</div></div>' +
      // Rate Master remarks name the submitting staff member across every
      // tenant firm on the platform — only CA-Automator's own platform
      // admin sees who flagged what (see admin_list_rate_remarks server-
      // side); a firm's own super admin no longer gets this card at all.
      (isPlatformAdmin ? '<div class="card" id="admin-remarks-body"><div class="empty">Loading…</div></div>' : '') +
      '<div class="card" id="admin-fy-body"><div class="empty">Loading…</div></div>' +
      '<div class="card" id="admin-fp-body"><div class="empty">Loading…</div></div>' +
      '<div class="card" id="admin-appt-body"><div class="empty">Loading…</div></div>';
    wireFySwitcher(view);
    wireCountrySwitcher(view);

    if (!state.online || !state.token || state.offlineMode) {
      document.getElementById('admin-users-body').innerHTML = '<div class="empty"><b>Offline</b>Admin actions need a connection.</div>';
      document.getElementById('admin-remarks-body').innerHTML = '';
      return;
    }

    function drawUsers(users) {
      const body = document.getElementById('admin-users-body');
      body.innerHTML = '<h3>Users</h3>' + users.map(u => {
        const statusTag = !u.is_active ? '<span class="tag tag-red">Disabled</span>'
          : u.pending_invite ? '<span class="tag tag-amber">Pending invite</span>' : '<span class="tag tag-green">Active</span>';
        return '<details class="card" style="margin-top:10px"><summary><b>' + esc(u.full_name) + '</b> ' +
          (u.is_super_admin ? '<span class="tag tag-green">Admin</span> ' : '') + statusTag +
          '<span class="hint"> — ' + esc(u.email) + '</span></summary>' +
          '<div class="grid c4" style="margin-top:12px" data-user-modules="' + u.id + '">' +
          (u.is_super_admin ? '<div class="card-note">Super admins automatically get every module your firm is subscribed to — no individual grant needed.</div>' :
            moduleKeys.map(k => {
              const exp = (u.module_grants || {})[k];
              const live = u.module_keys.indexOf(k) !== -1;
              const lapsed = exp && !live;
              return '<div><label class="check"' + (lapsed ? ' title="Expired ' + esc(exp) + ' — set a new date and save to renew"' : '') +
                '><input type="checkbox" data-mod="' + k + '"' + ((live || lapsed) ? ' checked' : '') + '> ' + esc(GRANTABLE_MODULES[k].name) +
                (lapsed ? ' <span class="tag tag-red">Expired ' + esc(exp) + '</span>' : '') + '</label>' +
                '<input type="date" data-mod-exp="' + k + '" value="' + esc(exp || '') + '" style="margin-top:4px;width:100%" title="Valid until (blank = permanent)"></div>';
            }).join('')) +
          '</div>' +
          (u.is_super_admin ? '' : '<button class="btn btn-small" data-save-grants="' + u.id + '" style="margin-top:10px">Save modules</button>') +
          '<div style="margin-top:10px">' +
          '<button class="btn btn-small" data-reissue="' + u.id + '">' + (u.pending_invite ? 'Resend invite' : 'Send reset link') + '</button> ' +
          (u.is_active ? '<button class="btn btn-small btn-danger" data-set-active="' + u.id + '" data-val="0">Disable</button>' :
            '<button class="btn btn-small" data-set-active="' + u.id + '" data-val="1">Re-enable</button>') + ' ' +
          (u.is_super_admin ? '<button class="btn btn-small" data-set-admin="' + u.id + '" data-val="0">Revoke admin</button>' :
            '<button class="btn btn-small" data-set-admin="' + u.id + '" data-val="1">Make admin</button>') +
          '</div><div class="hint" id="admin-user-msg-' + u.id + '"></div></details>';
      }).join('');

      body.querySelectorAll('[data-save-grants]').forEach(btn => btn.addEventListener('click', async () => {
        const uid = btn.dataset.saveGrants;
        const box = body.querySelector('[data-user-modules="' + uid + '"]');
        const grants = {};
        box.querySelectorAll('input[data-mod]:checked').forEach(i => {
          const expEl = box.querySelector('[data-mod-exp="' + i.dataset.mod + '"]');
          grants[i.dataset.mod] = (expEl && expEl.value) || null;
        });
        try { await api('/api/admin/users/' + uid, { method: 'PUT', body: { module_grants: grants } }); toast('Modules updated'); refresh(); }
        catch (e) { toast(e.message, true); }
      }));
      body.querySelectorAll('[data-set-active]').forEach(btn => btn.addEventListener('click', async () => {
        const uid = btn.dataset.setActive, val = btn.dataset.val === '1';
        if (!val && !confirm('Disable this user? Their session is revoked immediately and they can no longer sign in.')) return;
        try { await api('/api/admin/users/' + uid, { method: 'PUT', body: { is_active: val } });
          toast(val ? 'Re-enabled' : 'Disabled'); refresh(); }
        catch (e) { toast(e.message, true); }
      }));
      body.querySelectorAll('[data-set-admin]').forEach(btn => btn.addEventListener('click', async () => {
        const uid = btn.dataset.setAdmin, val = btn.dataset.val === '1';
        if (!val && !confirm('Remove super admin access from this user?')) return;
        try { await api('/api/admin/users/' + uid, { method: 'PUT', body: { is_super_admin: val } });
          toast(val ? 'Promoted to admin' : 'Admin access removed'); refresh(); }
        catch (e) { toast(e.message, true); }
      }));
      body.querySelectorAll('[data-reissue]').forEach(btn => btn.addEventListener('click', async () => {
        const uid = btn.dataset.reissue;
        try {
          const r = await api('/api/admin/users/' + uid + '/invite', { method: 'POST' });
          document.getElementById('admin-user-msg-' + uid).innerHTML =
            'Link (' + (r.kind === 'invite' ? 'account setup' : 'password reset') + '): <code>' + esc(r.link) + '</code>';
        } catch (e) { toast(e.message, true); }
      }));
    }

    function drawTemplates(templates) {
      const select = document.getElementById('admin-add-template');
      if (select) {
        select.innerHTML = '<option value="">— none —</option>' +
          templates.map(t => '<option value="' + t.id + '">' + esc(t.name) + (t.is_builtin ? ' (builtin)' : '') + '</option>').join('');
        select.onchange = () => {
          const t = templates.find(x => String(x.id) === select.value);
          const box = document.getElementById('admin-add-modules');
          box.querySelectorAll('input[data-new-mod]').forEach(i => { i.checked = !!(t && t.module_grants.indexOf(i.dataset.newMod) !== -1); });
        };
      }

      const body = document.getElementById('admin-templates-body');
      body.innerHTML = '<h3 title="A reusable, named set of module grants — pick one above when adding a user, or tick its checkboxes by hand. Builtin templates are managed by CA-Automator; your own custom ones are yours to edit.">Role templates</h3>' +
        (templates.length ? templates.map(t =>
          '<details class="card" style="margin-top:10px"><summary><b>' + esc(t.name) + '</b> ' +
          (t.is_builtin ? '<span class="tag">Builtin</span>' : '<span class="tag tag-green">Custom</span>') +
          (t.description ? '<span class="hint"> — ' + esc(t.description) + '</span>' : '') + '</summary>' +
          '<div style="margin-top:10px">' + t.module_grants.map(k => '<span class="tag" style="margin:0 6px 6px 0">' +
            esc((GRANTABLE_MODULES[k] && GRANTABLE_MODULES[k].name) || k) + '</span>').join('') + '</div>' +
          ((!t.is_builtin || isPlatformAdmin) ? '<button class="btn btn-small btn-danger" data-del-template="' + t.id + '" style="margin-top:10px">Delete template</button>' : '') +
          '</details>'
        ).join('') : '<div class="empty">No role templates yet.</div>') +

        '<details style="margin-top:14px"><summary><b>Add a role template</b></summary>' +
        '<div class="grid c3" style="margin-top:12px" id="admin-add-template-fields">' +
        field('Key', 'key', {}, 'text', { placeholder: 'e.g. senior_accountant' }) +
        field('Name', 'name', {}, 'text') +
        field('Description', 'description', {}, 'text') +
        '</div>' +
        (isPlatformAdmin ? '<label class="check" style="margin-top:8px"><input type="checkbox" id="admin-add-template-global"> Make this a global builtin (every firm sees it)</label>' : '') +
        '<div class="grid c4" style="margin-top:10px" id="admin-add-template-modules">' +
        moduleKeys.map(k => '<label class="check"><input type="checkbox" data-tmpl-mod="' + k + '"> ' + esc(GRANTABLE_MODULES[k].name) + '</label>').join('') +
        '</div>' +
        '<button class="btn btn-small" id="admin-add-template-go" style="margin-top:10px">Create template</button> ' +
        '<span class="hint" id="admin-add-template-msg"></span></details>';

      body.querySelectorAll('[data-del-template]').forEach(btn => btn.addEventListener('click', async () => {
        if (!confirm('Delete this role template?')) return;
        try { await api('/api/admin/role-templates/' + btn.dataset.delTemplate, { method: 'DELETE' }); toast('Template deleted'); refresh(); }
        catch (e) { toast(e.message, true); }
      }));

      const goBtn = body.querySelector('#admin-add-template-go');
      if (goBtn) goBtn.addEventListener('click', async () => {
        const fields = {};
        body.querySelector('#admin-add-template-fields').querySelectorAll('[data-key]').forEach(el => { fields[el.dataset.key] = el.value.trim(); });
        const mods = Array.from(body.querySelectorAll('[data-tmpl-mod]:checked')).map(i => i.dataset.tmplMod);
        const msg = body.querySelector('#admin-add-template-msg');
        msg.textContent = '';
        if (!fields.key || !fields.name) { msg.textContent = 'Key and name are required'; return; }
        const globalBox = body.querySelector('#admin-add-template-global');
        try {
          await api('/api/admin/role-templates', { method: 'POST', body: Object.assign({}, fields,
            { module_grants: mods, is_global: !!(globalBox && globalBox.checked) }) });
          toast('Template created'); refresh();
        } catch (e) { msg.textContent = e.message; }
      });
    }

    function drawRemarks(remarks) {
      const body = document.getElementById('admin-remarks-body');
      body.innerHTML = '<h3>Rate remarks</h3>' +
        (remarks.length ? remarks.map(r =>
          '<div class="rl"><div class="rl-main"><b>' + esc(r.domain) + '.' + esc(r.code) + '</b> — ' + esc(r.rate_label) +
          '<small>' + esc(r.submitted_by_name) + ' · ' + esc(r.created_at) + '</small>' +
          '<p>' + esc(r.remark) + '</p></div>' +
          '<div><input placeholder="Resolution note (optional)" data-note="' + r.id + '" style="width:220px"> ' +
          '<button class="btn btn-small" data-resolve="' + r.id + '">Mark resolved</button></div></div>'
        ).join('') : '<div class="empty">No open remarks.</div>');

      body.querySelectorAll('[data-resolve]').forEach(btn => btn.addEventListener('click', async () => {
        const rid = btn.dataset.resolve;
        const note = body.querySelector('[data-note="' + rid + '"]').value;
        try { await api('/api/admin/rate-remarks/' + rid, { method: 'PUT', body: { resolution_note: note } });
          toast('Marked resolved'); refresh(); }
        catch (e) { toast(e.message, true); }
      }));
    }

    function drawFiscalYears(fys) {
      const body = document.getElementById('admin-fy-body');
      const sorted = fys.slice().sort((a, b) => a.label_bs.localeCompare(b.label_bs));
      const latest = sorted.length ? sorted[sorted.length - 1] : null;
      body.innerHTML = '<h3 title="Add a new fiscal year here once it starts, or as soon as its official dates are published — everything else in the app picks it up automatically, no restart needed">Fiscal years</h3>' +
        (sorted.length ? '<div class="table-wrap"><table><thead><tr><th>Label (BS)</th><th>BS start</th><th>BS end</th>' +
          '<th>AD start</th><th>AD end</th><th>Current</th></tr></thead><tbody>' +
          sorted.map(f => '<tr><td>' + esc(f.label_bs) + '</td><td>' + esc(f.start_bs || '—') + '</td><td>' + esc(f.end_bs || '—') + '</td>' +
            '<td>' + esc(f.start_ad || '—') + '</td><td>' + esc(f.end_ad || '—') + '</td>' +
            '<td>' + (f.is_current ? '<span class="tag tag-green">Current</span>' : '') + '</td></tr>').join('') +
          '</tbody></table></div>' : '<div class="empty">No fiscal years yet.</div>') +
        '<div class="card-note" style="margin-top:10px">Whichever row\'s AD date range covers today becomes "Current" automatically on the next server check — nothing here is set by hand.</div>' +
        '<div class="grid c3" style="margin-top:12px" id="admin-add-fy">' +
        field('Label (BS)', 'label_bs', {}, 'text', { placeholder: latest ? nextFyLabel(latest.label_bs) : '2084-85', hint: 'e.g. 2084-85' }) +
        field('BS start', 'start_bs', {}, 'text', { placeholder: 'YYYY-MM-DD (BS)' }) +
        field('BS end', 'end_bs', {}, 'text', { placeholder: 'YYYY-MM-DD (BS)' }) +
        field('AD start', 'start_ad', {}, 'date') +
        field('AD end', 'end_ad', {}, 'date') +
        '</div><button class="btn btn-small" id="admin-add-fy-go" style="margin-top:10px" title="Both BS and AD dates are required — get them from the official calendar, not a guess">Add fiscal year</button> ' +
        '<span class="hint" id="admin-add-fy-msg"></span>';

      const fyGoBtn = body.querySelector('#admin-add-fy-go');
      if (fyGoBtn) fyGoBtn.addEventListener('click', async () => {
        const container = body.querySelector('#admin-add-fy');
        const fyBody = {};
        container.querySelectorAll('[data-key]').forEach(el => { fyBody[el.dataset.key] = el.value; });
        const msg = body.querySelector('#admin-add-fy-msg');
        msg.textContent = '';
        if (!fyBody.label_bs || !fyBody.start_bs || !fyBody.end_bs || !fyBody.start_ad || !fyBody.end_ad) {
          msg.textContent = 'Label, both BS dates and both AD dates are all required'; return;
        }
        try {
          await api('/api/admin/fiscal-years', { method: 'POST', body: fyBody });
          toast('Fiscal year added');
          const boot = await api('/api/bootstrap');
          state.boot = boot; state.rates = boot.rates || [];
          await idbPut('kv', boot, 'boot'); await idbPut('kv', state.rates, 'rates');
          refresh();
        } catch (e) { msg.textContent = e.message; }
      });
    }

    // Country-neutral counterpart to the Nepal fiscal-year card above — for
    // any firm whose country isn't NP. See schema.sql's comment on
    // financial_period for why this is a separate screen, not an extension
    // of the Bikram Sambat one.
    function drawFinancialPeriods() {
      const body = document.getElementById('admin-fp-body');
      const countryCode = (state.boot && state.boot.firm && state.boot.firm.country_code) || 'NP';
      if (countryCode === 'NP') { body.innerHTML = ''; return; }
      api('/api/admin/financial-periods?country_code=' + encodeURIComponent(countryCode)).then(periods => {
        body.innerHTML = '<h3 title="This firm\'s own jurisdiction (' + esc(countryCode) +
          ') financial years — AD-only, no Bikram Sambat involved">Financial periods (' + esc(countryCode) + ')</h3>' +
          (periods.length ? '<div class="table-wrap"><table><thead><tr><th>Label</th><th>Start</th><th>End</th><th>Current</th></tr></thead><tbody>' +
            periods.map(p => '<tr><td>' + esc(p.label) + '</td><td>' + esc(p.start_ad) + '</td><td>' + esc(p.end_ad) + '</td>' +
              '<td>' + (p.is_current ? '<span class="tag tag-green">Current</span>' : '') + '</td></tr>').join('') +
            '</tbody></table></div>' : '<div class="empty">No financial periods configured yet for this country.</div>') +
          '<button class="btn btn-small" id="admin-fp-generate" style="margin-top:10px">Generate a few years automatically</button> ' +
          '<span class="hint">Only works for a country with a known convention (India: Apr–Mar; UAE: calendar year) — anything else needs the manual form below.</span>' +
          '<div class="grid c4" style="margin-top:12px" id="admin-add-fp">' +
          field('Label', 'label', {}, 'text', { placeholder: 'e.g. 2026-27' }) +
          field('Start', 'start_ad', {}, 'date') + field('End', 'end_ad', {}, 'date') +
          '</div><button class="btn btn-small" id="admin-add-fp-go" style="margin-top:10px">Add period</button> ' +
          '<span class="hint" id="admin-add-fp-msg"></span>';

        document.getElementById('admin-fp-generate').addEventListener('click', async () => {
          try { await api('/api/admin/financial-periods/generate', { method: 'POST', body: { country_code: countryCode } }); toast('Periods generated'); drawFinancialPeriods(); }
          catch (e) { toast(e.message, true); }
        });
        document.getElementById('admin-add-fp-go').addEventListener('click', async () => {
          const container = document.getElementById('admin-add-fp');
          const b = { country_code: countryCode };
          container.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          const msg = document.getElementById('admin-add-fp-msg');
          if (!b.label || !b.start_ad || !b.end_ad) { msg.textContent = 'Label, start and end are all required'; return; }
          try { await api('/api/admin/financial-periods', { method: 'POST', body: b }); toast('Period added'); drawFinancialPeriods(); }
          catch (e) { msg.textContent = e.message; }
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    const APPT_METHOD_LABELS = { phone: 'Phone call', video: 'Video call', in_person: 'In-person / office visit' };
    const APPT_SERVICE_LABELS = { fdi: 'FDI consultancy', tax: 'Tax advisory', valuation: 'Valuation',
      advisory: 'Corporate & regulatory advisory', transfer_pricing: 'Transfer pricing', other: 'Other / not sure' };
    const APPT_WEEKDAYS = [[0, 'Sun'], [1, 'Mon'], [2, 'Tue'], [3, 'Wed'], [4, 'Thu'], [5, 'Fri'], [6, 'Sat']];
    const APPT_STATUS_TAG = { open: 'tag-green', booked: 'tag-amber', blocked: 'tag', pending: 'tag-amber', confirmed: 'tag-green', cancelled: 'tag-red' };

    function drawAppointments() {
      const abody = document.getElementById('admin-appt-body');
      abody.innerHTML =
        '<h3 title="Only slots opened here can ever be booked — the public page shows nothing until you add some">Appointments</h3>' +
        '<div class="grid c2">' +
          '<div><b class="card-note" style="display:block;margin-bottom:6px">Add a single slot</b>' +
          '<div class="grid c3" id="appt-add-fields">' +
          field('Date', 'date', {}, 'date') + field('Start', 'start_time', {}, 'time') + field('End', 'end_time', {}, 'time') +
          field('Price / hour (NPR)', 'price_per_hour', {}, 'number') +
          '</div><input id="appt-add-notes" placeholder="Notes (optional)" style="margin-top:6px;width:100%">' +
          '<button class="btn btn-small" id="appt-add-go" style="margin-top:8px" title="A single open slot on this date/time">Add slot</button> ' +
          '<span class="hint" id="appt-add-msg"></span></div>' +

          '<div><b class="card-note" style="display:block;margin-bottom:6px">Bulk-generate slots</b>' +
          '<div class="grid c3" id="appt-bulk-fields">' +
          field('From', 'date_from', {}, 'date') + field('To', 'date_to', {}, 'date') + field('Slot length (min)', 'slot_minutes', {}, 'number', { placeholder: '60' }) +
          field('Daily start', 'daily_start', {}, 'time') + field('Daily end', 'daily_end', {}, 'time') + field('Price / hour (NPR)', 'price_per_hour', {}, 'number') +
          '</div><div class="grid c4" style="margin-top:6px">' +
          APPT_WEEKDAYS.map(w => '<label class="check"><input type="checkbox" data-appt-wd="' + w[0] + '" checked> ' + w[1] + '</label>').join('') +
          '</div><button class="btn btn-small" id="appt-bulk-go" style="margin-top:8px" title="Fills in every matching weekday in the range, chopped into slots — existing slots are never disturbed">Generate</button> ' +
          '<span class="hint" id="appt-bulk-msg"></span></div>' +
        '</div>' +
        '<div class="grid c2" style="margin-top:16px">' +
          '<div><b class="card-note" style="display:block">Slots (next 60 days)</b><div id="appt-slots-list" class="table-wrap"></div></div>' +
          '<div><b class="card-note" style="display:block">Bookings</b><div id="appt-bookings-list" class="table-wrap"></div></div>' +
        '</div>';

      function readFields(containerId) {
        const out = {};
        document.getElementById(containerId).querySelectorAll('[data-key]').forEach(el => { out[el.dataset.key] = el.value; });
        return out;
      }

      function loadSlots() {
        const today = new Date().toISOString().slice(0, 10);
        const in60 = new Date(Date.now() + 60 * 86400000).toISOString().slice(0, 10);
        api('/api/admin/appointment-slots?from=' + today + '&to=' + in60).then(rows => {
          const list = document.getElementById('appt-slots-list');
          list.innerHTML = rows.length
            ? '<table><thead><tr><th>Date</th><th>Time</th><th class="num">Price</th><th>Status</th><th>Booking</th><th></th></tr></thead><tbody>' +
              rows.map(s => '<tr><td>' + esc(s.date) + '</td><td>' + esc(s.start_time) + '–' + esc(s.end_time) + '</td>' +
                '<td class="num">' + money(s.price) + '</td><td><span class="tag ' + (APPT_STATUS_TAG[s.status] || 'tag') + '">' + esc(s.status) + '</span></td>' +
                '<td>' + (s.booking_name ? esc(s.booking_name) + ' <span class="hint">(' + esc(s.booking_status) + ')</span>' : '—') + '</td>' +
                '<td>' + (s.status === 'open'
                  ? '<button class="btn btn-small btn-ghost" data-appt-block="' + s.id + '" title="Take this slot off the public page without deleting it">Block</button> ' +
                    '<button class="btn btn-small btn-danger" data-appt-del="' + s.id + '" title="Remove this slot entirely">Delete</button>'
                  : s.status === 'blocked'
                    ? '<button class="btn btn-small" data-appt-reopen="' + s.id + '" title="Make this slot bookable again">Reopen</button>'
                    : '') + '</td></tr>').join('') + '</tbody></table>'
            : '<div class="empty">No slots in this range yet.</div>';
          list.querySelectorAll('[data-appt-block]').forEach(b => b.addEventListener('click', async e => {
            try { await api('/api/admin/appointment-slots/' + e.target.dataset.apptBlock, { method: 'PUT', body: { status: 'blocked' } }); loadSlots(); }
            catch (err) { toast(err.message, true); }
          }));
          list.querySelectorAll('[data-appt-reopen]').forEach(b => b.addEventListener('click', async e => {
            try { await api('/api/admin/appointment-slots/' + e.target.dataset.apptReopen, { method: 'PUT', body: { status: 'open' } }); loadSlots(); }
            catch (err) { toast(err.message, true); }
          }));
          list.querySelectorAll('[data-appt-del]').forEach(b => b.addEventListener('click', async e => {
            if (!confirm('Delete this slot?')) return;
            try { await api('/api/admin/appointment-slots/' + e.target.dataset.apptDel, { method: 'DELETE' }); loadSlots(); }
            catch (err) { toast(err.message, true); }
          }));
        }).catch(e => { document.getElementById('appt-slots-list').innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
      }

      function loadBookings() {
        api('/api/admin/appointment-bookings').then(rows => {
          const list = document.getElementById('appt-bookings-list');
          list.innerHTML = rows.length
            ? '<table><thead><tr><th>Date</th><th>Name</th><th>Contact</th><th>Service</th><th>Method</th><th class="num">Price</th><th>Status</th><th></th></tr></thead><tbody>' +
              rows.map(b => '<tr><td>' + esc(b.date) + ' ' + esc(b.start_time) + '</td><td>' + esc(b.name) + (b.company ? '<br><span class="hint">' + esc(b.company) + '</span>' : '') + '</td>' +
                '<td>' + esc(b.email) + (b.phone ? '<br>' + esc(b.phone) : '') + '</td><td>' + esc(APPT_SERVICE_LABELS[b.service] || b.service || '—') + '</td>' +
                '<td>' + esc(APPT_METHOD_LABELS[b.method] || b.method) + '</td>' +
                '<td class="num">' + money(b.price) + '</td><td><span class="tag ' + (APPT_STATUS_TAG[b.status] || 'tag') + '">' + esc(b.status) + '</span></td>' +
                '<td>' + (b.status === 'pending' ? '<button class="btn btn-small" data-appt-confirm="' + b.id + '" title="Accept this booking">Confirm</button> ' : '') +
                (b.status !== 'cancelled' ? '<button class="btn btn-small btn-danger" data-appt-cancel="' + b.id + '" title="Cancel — the slot reopens for booking">Cancel</button>' : '') + '</td></tr>').join('') + '</tbody></table>'
            : '<div class="empty">No bookings yet.</div>';
          list.querySelectorAll('[data-appt-confirm]').forEach(b => b.addEventListener('click', async e => {
            try { await api('/api/admin/appointment-bookings/' + e.target.dataset.apptConfirm, { method: 'PUT', body: { status: 'confirmed' } }); toast('Confirmed'); loadBookings(); loadSlots(); }
            catch (err) { toast(err.message, true); }
          }));
          list.querySelectorAll('[data-appt-cancel]').forEach(b => b.addEventListener('click', async e => {
            if (!confirm('Cancel this booking? The slot will reopen.')) return;
            try { await api('/api/admin/appointment-bookings/' + e.target.dataset.apptCancel, { method: 'PUT', body: { status: 'cancelled' } }); toast('Cancelled'); loadBookings(); loadSlots(); }
            catch (err) { toast(err.message, true); }
          }));
        }).catch(e => { document.getElementById('appt-bookings-list').innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
      }

      document.getElementById('appt-add-go').addEventListener('click', async () => {
        const b = readFields('appt-add-fields');
        b.notes = document.getElementById('appt-add-notes').value.trim();
        const msg = document.getElementById('appt-add-msg');
        msg.textContent = '';
        if (!b.date || !b.start_time || !b.end_time || !b.price_per_hour) { msg.textContent = 'Date, start, end and price are all required'; return; }
        try { await api('/api/admin/appointment-slots', { method: 'POST', body: b }); toast('Slot added'); loadSlots(); }
        catch (e) { msg.textContent = e.message; }
      });

      document.getElementById('appt-bulk-go').addEventListener('click', async () => {
        const b = readFields('appt-bulk-fields');
        b.weekdays = Array.from(document.querySelectorAll('[data-appt-wd]:checked')).map(el => Number(el.dataset.apptWd));
        const msg = document.getElementById('appt-bulk-msg');
        msg.textContent = '';
        if (!b.date_from || !b.date_to || !b.daily_start || !b.daily_end || !b.slot_minutes || !b.price_per_hour) {
          msg.textContent = 'All fields are required'; return;
        }
        try { const r = await api('/api/admin/appointment-slots/bulk', { method: 'POST', body: b }); toast(r.created + ' slots created'); loadSlots(); }
        catch (e) { msg.textContent = e.message; }
      });

      loadSlots(); loadBookings();
    }

    function refresh() {
      Promise.all([api('/api/admin/users'), isPlatformAdmin ? api('/api/admin/rate-remarks') : Promise.resolve([]),
                  api('/api/admin/fiscal-years'), api('/api/admin/role-templates')])
        .then(([users, remarks, fys, templates]) => {
          drawUsers(users); drawTemplates(templates); if (isPlatformAdmin) drawRemarks(remarks);
          drawFiscalYears(fys); drawFinancialPeriods(); drawAppointments();
        }).catch(e => { document.getElementById('admin-users-body').innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }
    refresh();

    document.getElementById('admin-add-go').addEventListener('click', async () => {
      const container = document.getElementById('admin-add-user');
      const body = {};
      container.querySelectorAll('[data-key]').forEach(el => { body[el.dataset.key] = el.value; });
      body.module_grants = {};
      document.querySelectorAll('#admin-add-modules input[data-new-mod]:checked').forEach(i => {
        const expEl = document.querySelector('[data-new-mod-exp="' + i.dataset.newMod + '"]');
        body.module_grants[i.dataset.newMod] = (expEl && expEl.value) || null;
      });
      const msg = document.getElementById('admin-add-msg');
      msg.textContent = '';
      if (!body.full_name || !body.email) { msg.textContent = 'A name and email are required'; return; }
      try {
        const r = await api('/api/admin/users', { method: 'POST', body });
        document.getElementById('admin-invite-result').innerHTML =
          '<div class="card-note" style="margin-top:10px">Created. Invite link: <code>' + esc(r.invite_link) + '</code></div>';
        toast('User created');
        refresh();
      } catch (e) { msg.textContent = e.message; }
    });
  }

  // ──────────────────────────────────────────────────────── site settings
  // Services, pages, testimonials, firm profile/Maps, SMTP and payment
  // gateways — everything that shapes the public site but isn't itself
  // content (that's Blog) or recruitment (that's Careers). Split out of
  // Admin into its own screen so each of the three doesn't mean scrolling
  // past the other two to find anything.
  function renderSiteSettings() {
    const view = document.getElementById('view');
    view.innerHTML =
      '<div class="page-head"><div><h2>Site settings</h2><p>Everything shown on the public marketing site — nothing here is hardcoded, so a change saved below appears on the live site immediately.</p></div></div>' +
      '<h3>Menu</h3>' +
      '<p class="hint">The top navigation bar and the footer\'s Resources column — grouped below by which one each item belongs to, in the order it\'s shown. Reorder with the arrows, hide without deleting, or add a new link.</p>' +
      '<div class="card" id="admin-menu-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Media Gallery</h3>' +
      '<p class="hint">Upload an image once here, copy its URL, then paste that URL into any image field below (Services, Blog, Team, Milestones, Banner) — no need to upload the same file again. Every image field also still accepts a direct file upload.</p>' +
      '<div class="card" id="admin-gallery-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Services</h3>' +
      '<div class="card" id="admin-services-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Pages</h3>' +
      '<div class="card" id="admin-pages-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Testimonials</h3>' +
      '<div class="card" id="admin-testimonials-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">About — Timeline</h3>' +
      '<p class="hint">The firm\'s story, shown as the animated milestone timeline on the public About section.</p>' +
      '<div class="card" id="admin-milestones-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">About — Team</h3>' +
      '<div class="card" id="admin-team-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">YouTube Videos</h3>' +
      '<p class="hint">Shown on the public site\'s Videos section — hidden entirely there until at least one video is published.</p>' +
      '<div class="card" id="admin-youtube-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Downloads</h3>' +
      '<p class="hint">PDF, Word, Excel or PowerPoint files for visitors — listed at /downloads, newest first, each with its own page for search/AI discoverability.</p>' +
      '<div class="card" id="admin-downloads-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Popup Banner</h3>' +
      '<p class="hint">An announcement/notice popup on the landing page — optional image, optional link, auto-dismiss timer, closeable anytime.</p>' +
      '<div class="card" id="admin-banner-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Firm profile</h3>' +
      '<div class="card" id="admin-firm-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Email</h3>' +
      '<div class="card" id="admin-smtp-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Payment gateways</h3>' +
      '<div class="card" id="admin-payment-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">AI Providers</h3>' +
      '<p class="hint">Powers the landing-page chatbot and the internal CA Assistant. Add a key for any supported provider, mark one active — no restart needed, it\'s live on the next message.</p>' +
      '<div class="card" id="admin-ai-providers-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Social Login (Client Portal)</h3>' +
      '<p class="hint">"Sign in with Google" on the client portal — an alternative to a password, not a way to self-register: it only ever signs in an email that already has an active portal account (created via Admin → Client → Invite). Blank until you add real credentials below.</p>' +
      '<div class="card" id="admin-oauth-body"><div class="empty">Loading…</div></div>';

    if (!state.online || !state.token || state.offlineMode) {
      document.getElementById('admin-services-body').innerHTML = '<div class="empty"><b>Offline</b>Site settings live on the server. Reconnect to see or edit them.</div>';
      return;
    }

    // ── Media Gallery — upload once, reuse everywhere. Every thumbnail's
    // "Copy URL" button puts the exact string an image field's URL box
    // expects on the clipboard, so the whole "upload -> paste" loop never
    // needs the file itself a second time.
    // ── Menu management — one flat table from the API, grouped here into
    // its two sections (the "field" each item belongs to) so the order
    // within each is exactly what a visitor sees, top to bottom.
    const MENU_SECTIONS = [['primary', 'Top navigation'], ['footer', 'Footer — Resources column']];
    function drawMenu() {
      const body = document.getElementById('admin-menu-body');
      api('/api/admin/menu').then(rows => {
        const bySection = { primary: [], footer: [] };
        rows.forEach(r => { if (bySection[r.section]) bySection[r.section].push(r); });

        body.innerHTML = MENU_SECTIONS.map(([key, label]) => {
          const items = bySection[key];
          return '<div style="margin-bottom:18px"><div class="card-note">' + esc(label) + '</div>' +
            (items.length
              ? '<div class="table-wrap"><table><thead><tr><th></th><th>Label</th><th>URL</th><th>Status</th><th></th></tr></thead><tbody>' +
                items.map((it, i) => '<tr data-menu-row="' + it.id + '">' +
                  '<td style="white-space:nowrap">' +
                  '<button class="btn btn-small" data-move="up" data-id="' + it.id + '"' + (i === 0 ? ' disabled' : '') + ' title="Move up">↑</button> ' +
                  '<button class="btn btn-small" data-move="down" data-id="' + it.id + '"' + (i === items.length - 1 ? ' disabled' : '') + ' title="Move down">↓</button></td>' +
                  '<td>' + esc(it.label) + '</td><td class="hint">' + esc(it.url) + '</td>' +
                  '<td>' + (it.is_visible ? '<span class="tag tag-green">Shown</span>' : '<span class="tag">Hidden</span>') + '</td>' +
                  '<td style="white-space:nowrap">' +
                  '<button class="btn btn-small" data-toggle-menu="' + it.id + '" data-val="' + (it.is_visible ? 0 : 1) + '">' +
                  (it.is_visible ? 'Hide' : 'Show') + '</button> ' +
                  '<button class="btn btn-small btn-danger" data-del-menu="' + it.id + '">Delete</button></td></tr>').join('') +
                '</tbody></table></div>'
              : '<div class="empty">No items in this section.</div>') +
            '<div class="grid c3" style="margin-top:8px" data-menu-new="' + key + '">' +
            field('Label', 'label', {}, 'text') + field('URL', 'url', {}, 'text', { placeholder: '/blog, #contact, or a full https:// link' }) +
            '<div class="field"><label>&nbsp;</label><button class="btn btn-small" data-add-menu="' + key + '">Add to ' + esc(label) + '</button></div>' +
            '</div></div>';
        }).join('');

        body.querySelectorAll('[data-move]').forEach(btn => btn.addEventListener('click', async () => {
          try { await api('/api/admin/menu/' + btn.dataset.id + '/move', { method: 'POST', body: { direction: btn.dataset.move } }); drawMenu(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-toggle-menu]').forEach(btn => btn.addEventListener('click', async () => {
          try { await api('/api/admin/menu/' + btn.dataset.toggleMenu, { method: 'PUT', body: { is_visible: btn.dataset.val === '1' } }); drawMenu(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-del-menu]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this menu item?')) return;
          try { await api('/api/admin/menu/' + btn.dataset.delMenu, { method: 'DELETE' }); toast('Deleted'); drawMenu(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-add-menu]').forEach(btn => btn.addEventListener('click', async () => {
          const section = btn.dataset.addMenu;
          const box = body.querySelector('[data-menu-new="' + section + '"]');
          const b = { section }; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          if (!b.label || !b.url) { toast('A label and a URL are required', true); return; }
          try { await api('/api/admin/menu', { method: 'POST', body: b }); toast('Added'); drawMenu(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawGallery() {
      const body = document.getElementById('admin-gallery-body');
      api('/api/admin/media').then(rows => {
        body.innerHTML =
          '<div class="field"><label>Upload a new image</label><input type="file" accept="' + IMAGE_ACCEPT + '" id="admin-gallery-file"></div>' +
          '<p class="hint" id="admin-gallery-msg"></p>' +
          (rows.length
            ? '<div class="media-grid" style="margin-top:12px">' +
              rows.map(r => '<div class="media-tile" data-media-tile="' + r.id + '">' +
                '<img src="' + esc(r.url) + '" alt="' + esc(r.original_filename || '') + '" loading="lazy">' +
                '<div class="media-tile-name" title="' + esc(r.original_filename || '') + '">' + esc(r.original_filename || r.filename) + '</div>' +
                '<div class="media-tile-actions">' +
                '<button class="btn btn-small" data-copy-url="' + esc(r.url) + '">Copy URL</button>' +
                '<button class="btn btn-small btn-danger" data-del-media="' + r.id + '">Delete</button>' +
                '</div></div>').join('') +
              '</div>'
            : '<div class="empty" style="margin-top:12px">No images uploaded yet — add one above.</div>');

        document.getElementById('admin-gallery-file').addEventListener('change', async (e) => {
          const file = e.target.files[0];
          const msg = document.getElementById('admin-gallery-msg');
          if (!file) return;
          if (['image/jpeg', 'image/png', 'image/webp'].indexOf(file.type) === -1) {
            msg.textContent = 'Only JPEG, PNG or WebP images are allowed'; e.target.value = ''; return;
          }
          if (file.size > 5 * 1024 * 1024) { msg.textContent = 'Images must be 5MB or smaller'; e.target.value = ''; return; }
          const reader = new FileReader();
          reader.onload = async () => {
            const base64 = reader.result.split(',')[1];
            try {
              await api('/api/admin/media', { method: 'POST', body: { content_base64: base64, content_type: file.type, filename: file.name } });
              toast('Image uploaded'); drawGallery();
            } catch (err) { msg.textContent = err.message; }
            e.target.value = '';
          };
          reader.readAsDataURL(file);
        });
        body.querySelectorAll('[data-copy-url]').forEach(btn => btn.addEventListener('click', async () => {
          const url = btn.dataset.copyUrl;
          const absolute = location.origin + url;
          try {
            await navigator.clipboard.writeText(absolute);
            toast('URL copied — paste it into any image field');
          } catch (e) {
            toast(absolute, false);
          }
        }));
        body.querySelectorAll('[data-del-media]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this image? Any field still using its URL will show a broken image.')) return;
          try { await api('/api/admin/media/' + btn.dataset.delMedia, { method: 'DELETE' }); toast('Deleted'); drawGallery(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawServices() {
      const body = document.getElementById('admin-services-body');
      Promise.all([api('/api/admin/service-categories'), api('/api/admin/services')]).then(([cats, services]) => {
        const catOptions = [{ value: '', label: '(none)' }].concat(cats.map(c => ({ value: c.id, label: c.name })));
        body.innerHTML = '<h3>Services</h3>' +
          '<details class="card"><summary><b>Service categories</b></summary>' +
          '<div class="table-wrap" style="margin-top:10px"><table><thead><tr><th>Name</th><th>Slug</th><th></th></tr></thead><tbody>' +
          cats.map(c => '<tr><td>' + esc(c.name) + '</td><td>' + esc(c.slug) + '</td>' +
            '<td><button class="btn btn-small btn-danger" data-del-cat="' + c.id + '">Remove</button></td></tr>').join('') +
          '</tbody></table></div>' +
          '<div class="grid c3" style="margin-top:10px" id="admin-new-cat">' +
          field('Name', 'name', {}, 'text') + field('Slug', 'slug', {}, 'text', { hint: 'lowercase-with-hyphens' }) +
          '</div><button class="btn btn-small" id="admin-add-cat" style="margin-top:8px">Add category</button></details>' +

          '<details class="card" open style="margin-top:12px"><summary><b>Services</b><span class="hint"> — ' + services.length + '</span></summary>' +
          '<div style="margin-top:10px">' + services.map(s => {
            const pub = s.is_published ? '<span class="tag tag-green">Published</span>' : '<span class="tag">Draft</span>';
            return '<details class="card" style="margin-top:10px" data-service-details="' + s.id + '"><summary><b>' + esc(s.name) + '</b> ' + pub +
              '<span class="hint"> — /services/' + esc(s.slug) + '</span></summary>' +
              '<div id="admin-service-edit-' + s.id + '"><div class="empty">Loading…</div></div></details>';
          }).join('') + '</div>' +
          '<div class="card-note" style="margin-top:12px">New service</div>' +
          '<div class="grid c3" id="admin-new-service">' +
          field('Name', 'name', {}, 'text') + field('Slug', 'slug', {}, 'text') +
          field('Category', 'category_id', {}, 'select', { options: catOptions }) +
          '</div><button class="btn btn-small" id="admin-add-service" style="margin-top:8px">Add service</button></details>';

        body.querySelector('#admin-add-cat').addEventListener('click', async () => {
          const box = document.getElementById('admin-new-cat');
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          if (!b.name || !b.slug) { toast('Name and slug are required', true); return; }
          try { await api('/api/admin/service-categories', { method: 'POST', body: b }); toast('Category added'); drawServices(); }
          catch (e) { toast(e.message, true); }
        });
        body.querySelectorAll('[data-del-cat]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Remove this category? Services in it are not deleted, just uncategorised.')) return;
          try { await api('/api/admin/service-categories/' + btn.dataset.delCat, { method: 'DELETE' }); toast('Removed'); drawServices(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelector('#admin-add-service').addEventListener('click', async () => {
          const box = document.getElementById('admin-new-service');
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          if (!b.name || !b.slug) { toast('Name and slug are required', true); return; }
          try { await api('/api/admin/services', { method: 'POST', body: b }); toast('Service added — edit it below to publish'); drawServices(); }
          catch (e) { toast(e.message, true); }
        });

        // Same lazy-load posture as the blog editor right below: fetching
        // full detail (incl. FAQs) only when a service's row actually opens.
        body.querySelectorAll('[data-service-details]').forEach(det => {
          let loaded = false;
          det.addEventListener('toggle', async () => {
            if (!det.open || loaded) return;
            loaded = true;
            const sid = det.dataset.serviceDetails;
            const editHolder = document.getElementById('admin-service-edit-' + sid);
            try {
              const s = await api('/api/admin/services/' + sid);
              editHolder.innerHTML =
                '<div class="grid c3" style="margin-top:10px" data-service-fields="' + sid + '">' +
                field('Name', 'name', s, 'text') + field('Slug', 'slug', s, 'text') +
                field('Category', 'category_id', s, 'select', { options: catOptions }) +
                field('Price type', 'price_type', s, 'select', { options: [
                  { value: 'enquire', label: 'Enquire' }, { value: 'fixed', label: 'Fixed' }, { value: 'starting_from', label: 'Starting from' }] }) +
                field('Price', 'price', s, 'number') + field('Currency', 'currency', s, 'text') +
                field('Duration', 'duration_label', s, 'text') + field('Meta title', 'meta_title', s, 'text') +
                field('Meta description', 'meta_description', s, 'text') +
                '</div>' +
                imageUploadHtml('image_path', 'Image', s.image_path, 'JPEG, PNG or WebP only, up to 5MB — shown on the service card and its detail page.') +
                '<div class="field" style="margin-top:8px"><label>Summary</label><textarea data-key="summary" rows="2" style="width:100%">' + esc(s.summary || '') + '</textarea></div>' +
                '<div class="field"><label>What we deliver (plain text)</label><textarea data-key="description_html" rows="4" style="width:100%">' + esc(s.description_html || '') + '</textarea></div>' +
                '<div class="field"><label>The problem (plain text, optional)</label><textarea data-key="problem_html" rows="3" style="width:100%">' + esc(s.problem_html || '') + '</textarea></div>' +
                '<div class="field"><label>How we work / process (plain text, optional)</label><textarea data-key="process_html" rows="3" style="width:100%">' + esc(s.process_html || '') + '</textarea></div>' +
                '<div class="field"><label>Who this is for (plain text, optional)</label><textarea data-key="who_for_html" rows="3" style="width:100%">' + esc(s.who_for_html || '') + '</textarea></div>' +
                '<div class="field"><label>Key benefits (plain text, optional)</label><textarea data-key="benefits_html" rows="3" style="width:100%">' + esc(s.benefits_html || '') + '</textarea></div>' +
                '<div class="field"><label>Documents required (plain text, optional)</label><textarea data-key="documents_html" rows="3" style="width:100%">' + esc(s.documents_html || '') + '</textarea></div>' +
                '<div class="grid c3" style="margin-top:8px">' +
                field('Buyable online', 'is_buyable', s, 'check') +
                field('Requires appointment', 'requires_appointment', s, 'check') +
                field('Published', 'is_published', s, 'check') +
                '</div>' +
                (s.is_buyable ? '<p class="hint" style="margin-top:6px">Buyable services show a "Buy now" button once a payment gateway is configured in Payment gateways below.</p>' : '') +
                '<div class="card-note" style="margin-top:10px">FAQs — shown on the service\'s own page, and as FAQPage structured data</div>' +
                '<div id="admin-service-faqs-' + sid + '">' + (s.faqs || []).map((f, i) =>
                  '<div class="grid c2" data-faq-row="' + i + '" style="margin-top:6px">' +
                  '<input data-faq-q value="' + esc(f.question) + '" placeholder="Question">' +
                  '<input data-faq-a value="' + esc(f.answer) + '" placeholder="Answer">' +
                  '</div>').join('') + '</div>' +
                '<button class="btn btn-small" data-add-service-faq="' + sid + '" style="margin-top:6px">Add FAQ</button>' +
                '<div style="margin-top:10px"><button class="btn btn-small btn-primary" data-save-service="' + sid + '">Save</button> ' +
                '<button class="btn btn-small btn-danger" data-del-service="' + sid + '">Delete</button></div>';

              wireImageUpload(editHolder);
              editHolder.querySelector('[data-add-service-faq]').addEventListener('click', () => {
                const faqBox = document.getElementById('admin-service-faqs-' + sid);
                const i = faqBox.children.length;
                const row = document.createElement('div');
                row.className = 'grid c2'; row.dataset.faqRow = i; row.style.marginTop = '6px';
                row.innerHTML = '<input data-faq-q placeholder="Question"><input data-faq-a placeholder="Answer">';
                faqBox.appendChild(row);
              });
              editHolder.querySelector('[data-save-service]').addEventListener('click', async () => {
                const fieldBox = editHolder.querySelector('[data-service-fields]');
                const b = {}; fieldBox.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
                b.image_path = editHolder.querySelector('[data-cover-url]').value;
                ['summary', 'description_html', 'problem_html', 'process_html', 'who_for_html', 'benefits_html', 'documents_html'].forEach(k => {
                  b[k] = editHolder.querySelector('textarea[data-key="' + k + '"]').value;
                });
                editHolder.querySelectorAll('[data-bind]').forEach(el => { b[el.dataset.bind] = el.checked; });
                b.faqs = Array.from(editHolder.querySelectorAll('[data-faq-row]')).map(row => ({
                  question: row.querySelector('[data-faq-q]').value.trim(),
                  answer: row.querySelector('[data-faq-a]').value.trim(),
                })).filter(f => f.question && f.answer);
                try { await api('/api/admin/services/' + sid, { method: 'PUT', body: b }); toast('Service saved'); drawServices(); }
                catch (e) { toast(e.message, true); }
              });
              editHolder.querySelector('[data-del-service]').addEventListener('click', async () => {
                if (!confirm('Delete this service? This cannot be undone.')) return;
                try { await api('/api/admin/services/' + sid, { method: 'DELETE' }); toast('Deleted'); drawServices(); }
                catch (e) { toast(e.message, true); }
              });
            } catch (e) {
              editHolder.innerHTML = '<div class="empty">' + esc(e.message) + '</div>';
            }
          });
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawPages() {
      const body = document.getElementById('admin-pages-body');
      api('/api/admin/pages').then(pages => {
        body.innerHTML = '<h3>Static pages</h3><p class="hint">About, Privacy Policy, Terms, Refund Policy — anything reached at /page/&lt;slug&gt;.</p>' +
          pages.map(p => {
            const pub = p.is_published ? '<span class="tag tag-green">Published</span>' : '<span class="tag">Draft</span>';
            return '<details class="card" style="margin-top:10px"><summary><b>' + esc(p.title) + '</b> ' + pub +
              '<span class="hint"> — /page/' + esc(p.slug) + '</span></summary>' +
              '<div class="grid c2" style="margin-top:12px" data-page-row="' + p.id + '">' +
              field('Title', 'title', p, 'text') + field('Slug', 'slug', p, 'text') +
              field('Meta title', 'meta_title', p, 'text') + field('Meta description', 'meta_description', p, 'text') +
              '</div>' +
              '<div class="field" style="margin-top:8px"><label>Content (plain text)</label><textarea data-key="content_md" rows="8" style="width:100%" placeholder="Write plain paragraphs. Blank lines separate paragraphs. ## for a heading, **bold**, numbered or bulleted lists all work.">' + esc(p.content_md || '') + '</textarea></div>' +
              field('Published', 'is_published', p, 'check') +
              '<div style="margin-top:8px"><button class="btn btn-small btn-primary" data-save-page="' + p.id + '">Save</button> ' +
              '<button class="btn btn-small btn-danger" data-del-page="' + p.id + '">Delete</button></div></details>';
          }).join('') +
          '<div class="card-note" style="margin-top:12px">New page</div>' +
          '<div id="admin-new-page">' +
          '<div class="grid c2">' + field('Title', 'title', {}, 'text') + field('Slug', 'slug', {}, 'text') + '</div>' +
          '<div class="field" style="margin-top:8px"><label>Content (plain text)</label><textarea data-key="content_md" rows="8" style="width:100%" placeholder="Write plain paragraphs. Blank lines separate paragraphs. ## for a heading, **bold**, numbered or bulleted lists all work."></textarea></div>' +
          '</div>' +
          '<button class="btn btn-small" id="admin-add-page" style="margin-top:8px">Add page</button>';

        body.querySelectorAll('[data-save-page]').forEach(btn => btn.addEventListener('click', async () => {
          const pid = btn.dataset.savePage;
          const box = body.querySelector('[data-page-row="' + pid + '"]').closest('details');
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          box.querySelectorAll('[data-bind]').forEach(el => { b[el.dataset.bind] = el.checked; });
          try { await api('/api/admin/pages/' + pid, { method: 'PUT', body: b }); toast('Page saved'); drawPages(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-del-page]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this page?')) return;
          try { await api('/api/admin/pages/' + btn.dataset.delPage, { method: 'DELETE' }); toast('Deleted'); drawPages(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelector('#admin-add-page').addEventListener('click', async () => {
          const box = document.getElementById('admin-new-page');
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          if (!b.title || !b.slug) { toast('Title and slug are required', true); return; }
          try { await api('/api/admin/pages', { method: 'POST', body: b }); toast('Page added'); drawPages(); }
          catch (e) { toast(e.message, true); }
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawTestimonials() {
      const body = document.getElementById('admin-testimonials-body');
      api('/api/admin/testimonials').then(rows => {
        body.innerHTML = '<h3>Testimonials</h3>' +
          '<div class="table-wrap"><table><thead><tr><th>Client</th><th>Company</th><th>Rating</th><th>Status</th><th></th></tr></thead><tbody>' +
          rows.map(t => '<tr><td>' + esc(t.client_name) + '</td><td>' + esc(t.company || '') + '</td><td>' + esc(t.rating || '') + '</td>' +
            '<td>' + (t.is_published ? '<span class="tag tag-green">Published</span>' : '<span class="tag">Draft</span>') + '</td>' +
            '<td><button class="btn btn-small" data-toggle-testi="' + t.id + '" data-val="' + (t.is_published ? 0 : 1) + '">' +
            (t.is_published ? 'Unpublish' : 'Publish') + '</button> ' +
            '<button class="btn btn-small btn-danger" data-del-testi="' + t.id + '">Delete</button></td></tr>').join('') +
          '</tbody></table></div>' +
          '<div class="card-note" style="margin-top:12px">New testimonial</div>' +
          '<div class="grid c3" id="admin-new-testi">' +
          field('Client name', 'client_name', {}, 'text') + field('Client title', 'client_title', {}, 'text') +
          field('Company', 'company', {}, 'text') + field('Rating (1-5)', 'rating', {}, 'number') +
          '</div><div class="field"><label>Testimonial</label><textarea data-key="content" rows="3" style="width:100%"></textarea></div>' +
          '<button class="btn btn-small" id="admin-add-testi" style="margin-top:8px">Add testimonial</button>';

        body.querySelectorAll('[data-toggle-testi]').forEach(btn => btn.addEventListener('click', async () => {
          try { await api('/api/admin/testimonials/' + btn.dataset.toggleTesti, { method: 'PUT', body: { is_published: btn.dataset.val === '1' } });
            toast('Updated'); drawTestimonials(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-del-testi]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this testimonial?')) return;
          try { await api('/api/admin/testimonials/' + btn.dataset.delTesti, { method: 'DELETE' }); toast('Deleted'); drawTestimonials(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelector('#admin-add-testi').addEventListener('click', async () => {
          const box = document.getElementById('admin-new-testi');
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          b.content = document.querySelector('#admin-testimonials-body textarea[data-key="content"]').value;
          if (!b.client_name || !b.content) { toast('Client name and testimonial text are required', true); return; }
          try { await api('/api/admin/testimonials', { method: 'POST', body: b }); toast('Testimonial added'); drawTestimonials(); }
          catch (e) { toast(e.message, true); }
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    // ── About: Timeline — the firm's real history only. sort_order is a
    // plain number (lower first) rather than drag-and-drop, same posture as
    // service categories/testimonials above — reordering by typing a
    // number is less code and no less usable for a handful of milestones.
    function drawMilestones() {
      const body = document.getElementById('admin-milestones-body');
      api('/api/admin/about-milestones').then(rows => {
        body.innerHTML = rows.map(ms => {
          const pub = ms.is_published ? '<span class="tag tag-green">Published</span>' : '<span class="tag">Draft</span>';
          return '<details class="card" style="margin-top:10px"><summary><b>' + esc(ms.year) + ' — ' + esc(ms.title) + '</b> ' + pub + '</summary>' +
            '<div class="grid c3" style="margin-top:12px" data-ms-row="' + ms.id + '">' +
            field('Year', 'year', ms, 'text') + field('Title', 'title', ms, 'text') + field('Order', 'sort_order', ms, 'number') +
            '</div>' +
            '<div class="field" style="margin-top:8px"><label>Description</label><textarea data-key="description" rows="2" style="width:100%">' + esc(ms.description || '') + '</textarea></div>' +
            imageUploadHtml('image_path', 'Image (optional)', ms.image_path, 'JPEG, PNG or WebP only, up to 5MB.') +
            field('Published', 'is_published', ms, 'check') +
            '<div style="margin-top:8px"><button class="btn btn-small btn-primary" data-save-ms="' + ms.id + '">Save</button> ' +
            '<button class="btn btn-small btn-danger" data-del-ms="' + ms.id + '">Delete</button></div></details>';
        }).join('') +
          '<div class="card-note" style="margin-top:12px">New milestone</div>' +
          '<div class="grid c3" id="admin-new-ms">' +
          field('Year', 'year', {}, 'text', { placeholder: '2018' }) + field('Title', 'title', {}, 'text') +
          '</div><button class="btn btn-small" id="admin-add-ms" style="margin-top:8px">Add milestone</button>';

        body.querySelectorAll('[data-save-ms]').forEach(btn => btn.addEventListener('click', async () => {
          const mid = btn.dataset.saveMs;
          const box = body.querySelector('[data-ms-row="' + mid + '"]').closest('details');
          const b = {};
          box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          box.querySelectorAll('[data-bind]').forEach(el => { b[el.dataset.bind] = el.checked; });
          try { await api('/api/admin/about-milestones/' + mid, { method: 'PUT', body: b }); toast('Milestone saved'); drawMilestones(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-del-ms]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this milestone?')) return;
          try { await api('/api/admin/about-milestones/' + btn.dataset.delMs, { method: 'DELETE' }); toast('Deleted'); drawMilestones(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-cover-file]').forEach(el => { const c = el.closest('details') || body; wireImageUpload(c); });
        body.querySelector('#admin-add-ms').addEventListener('click', async () => {
          const box = document.getElementById('admin-new-ms');
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          if (!b.year || !b.title) { toast('Year and title are required', true); return; }
          try { await api('/api/admin/about-milestones', { method: 'POST', body: b }); toast('Milestone added — edit it below to publish'); drawMilestones(); }
          catch (e) { toast(e.message, true); }
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    // ── About: Team — real people only, never invented.
    function drawTeam() {
      const body = document.getElementById('admin-team-body');
      api('/api/admin/team-members').then(rows => {
        body.innerHTML = rows.map(t => {
          const pub = t.is_published ? '<span class="tag tag-green">Published</span>' : '<span class="tag">Draft</span>';
          return '<details class="card" style="margin-top:10px"><summary><b>' + esc(t.name) + '</b> ' + pub +
            (t.designation ? '<span class="hint"> — ' + esc(t.designation) + '</span>' : '') + '</summary>' +
            '<div class="grid c3" style="margin-top:12px" data-team-row="' + t.id + '">' +
            field('Name', 'name', t, 'text') + field('Designation', 'designation', t, 'text') + field('Order', 'sort_order', t, 'number') +
            field('Expertise', 'expertise', t, 'text', { hint: 'comma-separated' }) + field('LinkedIn URL', 'linkedin_url', t, 'text') +
            field('URL slug', 'slug', t, 'text', { hint: 'Their profile page: /team/' + (t.slug || '…') }) +
            '</div>' +
            '<div class="field" style="margin-top:8px"><label>Bio</label><p class="hint">Shown in full on their own profile page (/team/' + esc(t.slug || '') +
            ') — the front page only shows a short preview of this.</p><textarea data-key="bio" rows="4" style="width:100%">' + esc(t.bio || '') + '</textarea></div>' +
            imageUploadHtml('photo_path', 'Photo (optional)', t.photo_path, 'JPEG, PNG or WebP only, up to 5MB.') +
            field('Published', 'is_published', t, 'check') +
            '<div style="margin-top:8px"><button class="btn btn-small btn-primary" data-save-team="' + t.id + '">Save</button> ' +
            '<button class="btn btn-small btn-danger" data-del-team="' + t.id + '">Delete</button>' +
            (t.is_published && t.slug ? ' <a class="btn btn-small btn-ghost" href="/team/' + esc(t.slug) + '" target="_blank" rel="noopener">View profile page →</a>' : '') +
            '</div></details>';
        }).join('') +
          '<div class="card-note" style="margin-top:12px">New team member</div>' +
          '<div class="grid c3" id="admin-new-team">' +
          field('Name', 'name', {}, 'text') + field('Designation', 'designation', {}, 'text') +
          '</div><button class="btn btn-small" id="admin-add-team" style="margin-top:8px">Add team member</button>';

        body.querySelectorAll('[data-save-team]').forEach(btn => btn.addEventListener('click', async () => {
          const tid = btn.dataset.saveTeam;
          const box = body.querySelector('[data-team-row="' + tid + '"]').closest('details');
          const b = {};
          box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          box.querySelectorAll('[data-bind]').forEach(el => { b[el.dataset.bind] = el.checked; });
          try { await api('/api/admin/team-members/' + tid, { method: 'PUT', body: b }); toast('Team member saved'); drawTeam(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-del-team]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this team member?')) return;
          try { await api('/api/admin/team-members/' + btn.dataset.delTeam, { method: 'DELETE' }); toast('Deleted'); drawTeam(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-cover-file]').forEach(el => { const c = el.closest('details') || body; wireImageUpload(c); });
        body.querySelector('#admin-add-team').addEventListener('click', async () => {
          const box = document.getElementById('admin-new-team');
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          if (!b.name) { toast('A name is required', true); return; }
          try { await api('/api/admin/team-members', { method: 'POST', body: b }); toast('Team member added — edit them below to publish'); drawTeam(); }
          catch (e) { toast(e.message, true); }
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    // ── YouTube videos — title + a pasted link in any real YouTube format
    // (watch/share/embed/Shorts); the server extracts and validates the
    // video id, so a bad paste is rejected with a clear message rather than
    // silently saving a broken embed. "Deactivate" is the same publish
    // toggle every other content type here uses — the row stays, it just
    // stops showing on the public site.
    function drawYoutube() {
      const body = document.getElementById('admin-youtube-body');
      api('/api/admin/youtube-videos').then(rows => {
        body.innerHTML = (rows.length
          ? '<div class="table-wrap"><table><thead><tr><th>Title</th><th>Video</th><th>Order</th><th>Status</th><th></th></tr></thead><tbody>' +
            rows.map(v => '<tr><td>' + esc(v.title) + '</td>' +
              '<td><a href="https://www.youtube.com/watch?v=' + esc(v.video_id) + '" target="_blank" rel="noopener">' + esc(v.video_id) + '</a></td>' +
              '<td>' + v.sort_order + '</td>' +
              '<td>' + (v.is_published ? '<span class="tag tag-green">Published</span>' : '<span class="tag">Deactivated</span>') + '</td>' +
              '<td><button class="btn btn-small" data-toggle-yt="' + v.id + '" data-val="' + (v.is_published ? 0 : 1) + '">' +
              (v.is_published ? 'Deactivate' : 'Activate') + '</button> ' +
              '<button class="btn btn-small btn-danger" data-del-yt="' + v.id + '">Delete</button></td></tr>').join('') +
            '</tbody></table></div>'
          : '<div class="empty">No videos yet — add your channel\'s first video below.</div>') +
          '<div class="card-note" style="margin-top:12px">New video</div>' +
          '<div class="grid c3" id="admin-new-yt">' +
          field('Title', 'title', {}, 'text') +
          field('YouTube URL', 'youtube_url', {}, 'text', { placeholder: 'https://www.youtube.com/watch?v=...', hint: 'Watch, share (youtu.be), embed or Shorts link — any of them work' }) +
          field('Order', 'sort_order', {}, 'number') +
          '</div><button class="btn btn-small" id="admin-add-yt" style="margin-top:8px">Add video</button>' +
          '<p class="hint" id="admin-yt-msg" style="margin-top:6px"></p>';

        body.querySelectorAll('[data-toggle-yt]').forEach(btn => btn.addEventListener('click', async () => {
          try { await api('/api/admin/youtube-videos/' + btn.dataset.toggleYt, { method: 'PUT', body: { is_published: btn.dataset.val === '1' } });
            toast(btn.dataset.val === '1' ? 'Activated' : 'Deactivated'); drawYoutube(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-del-yt]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this video? This cannot be undone.')) return;
          try { await api('/api/admin/youtube-videos/' + btn.dataset.delYt, { method: 'DELETE' }); toast('Deleted'); drawYoutube(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelector('#admin-add-yt').addEventListener('click', async () => {
          const box = document.getElementById('admin-new-yt');
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          const msg = document.getElementById('admin-yt-msg');
          msg.textContent = '';
          if (!b.title || !b.youtube_url) { msg.textContent = 'Title and a YouTube URL are required'; return; }
          try { await api('/api/admin/youtube-videos', { method: 'POST', body: Object.assign({ is_published: true }, b) });
            toast('Video added'); drawYoutube(); }
          catch (e) { msg.textContent = e.message; }
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    // ── Downloads — a public document library. Upload is a one-shot action
    // (title + file, converted to base64 client-side like the ConnectIPS
    // certificate upload elsewhere in this page) rather than the
    // lazy-loaded-editor pattern used for services/blog, since there's no
    // "existing file" to keep editing — only title/description/publish
    // toggle change after upload.
    const DOWNLOAD_ACCEPT = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx';
    const DOWNLOAD_MIME_BY_EXT = {
      pdf: 'application/pdf', doc: 'application/msword',
      docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      xls: 'application/vnd.ms-excel',
      xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      ppt: 'application/vnd.ms-powerpoint',
      pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
    };
    function drawDownloads() {
      const body = document.getElementById('admin-downloads-body');
      api('/api/admin/downloads').then(rows => {
        body.innerHTML = (rows.length
          ? '<div class="table-wrap"><table><thead><tr><th>Title</th><th>Type</th><th>Uploaded</th><th>Downloads</th><th>Status</th><th></th></tr></thead><tbody>' +
            rows.map(d => '<tr><td>' + esc(d.title) + '<div class="hint">' + esc(d.description || '') + '</div></td>' +
              '<td>' + esc((d.mime_type || '').split('/').pop()) + '</td>' +
              '<td>' + esc((d.uploaded_at || '').slice(0, 10)) + '</td>' +
              '<td>' + d.download_count + '</td>' +
              '<td>' + (d.is_published ? '<span class="tag tag-green">Published</span>' : '<span class="tag">Draft</span>') + '</td>' +
              '<td><a class="btn btn-small" href="/downloads/' + esc(d.slug) + '" target="_blank" rel="noopener">View page</a> ' +
              '<button class="btn btn-small" data-toggle-dl="' + d.id + '" data-val="' + (d.is_published ? 0 : 1) + '">' +
              (d.is_published ? 'Unpublish' : 'Publish') + '</button> ' +
              '<button class="btn btn-small btn-danger" data-del-dl="' + d.id + '">Delete</button></td></tr>').join('') +
            '</tbody></table></div>'
          : '<div class="empty">No downloads yet — add your first file below.</div>') +
          '<div class="card-note" style="margin-top:12px">New download</div>' +
          '<div class="grid c2" id="admin-new-dl">' +
          field('Title', 'title', {}, 'text') + field('Slug (optional)', 'slug', {}, 'text', { hint: 'lowercase-with-hyphens — auto-generated from the title if left blank' }) +
          '</div>' +
          '<div class="field" style="margin-top:8px"><label>Description (for SEO and the file\'s own page)</label><textarea id="admin-new-dl-desc" rows="2" style="width:100%"></textarea></div>' +
          '<div class="grid c2" style="margin-top:8px">' +
          '<div class="field"><label>File</label><input type="file" accept="' + DOWNLOAD_ACCEPT + '" id="admin-new-dl-file"><p class="hint">PDF, Word, Excel or PowerPoint — up to 20MB.</p></div>' +
          field('Publish immediately', 'is_published', { is_published: true }, 'check') +
          '</div>' +
          '<button class="btn btn-small btn-primary" id="admin-add-dl" style="margin-top:8px">Upload</button>' +
          '<p class="hint" id="admin-dl-msg" style="margin-top:6px"></p>';

        body.querySelectorAll('[data-toggle-dl]').forEach(btn => btn.addEventListener('click', async () => {
          try { await api('/api/admin/downloads/' + btn.dataset.toggleDl, { method: 'PUT', body: { is_published: btn.dataset.val === '1' } });
            toast(btn.dataset.val === '1' ? 'Published' : 'Unpublished'); drawDownloads(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-del-dl]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this file? This cannot be undone.')) return;
          try { await api('/api/admin/downloads/' + btn.dataset.delDl, { method: 'DELETE' }); toast('Deleted'); drawDownloads(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelector('#admin-add-dl').addEventListener('click', async () => {
          const box = document.getElementById('admin-new-dl');
          const msg = document.getElementById('admin-dl-msg');
          msg.textContent = '';
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          b.description = document.getElementById('admin-new-dl-desc').value;
          b.is_published = box.querySelector('[data-bind="is_published"]') ? box.querySelector('[data-bind="is_published"]').checked : true;
          const fileInput = document.getElementById('admin-new-dl-file');
          const file = fileInput.files[0];
          if (!b.title) { msg.textContent = 'A title is required'; return; }
          if (!file) { msg.textContent = 'Choose a file first'; return; }
          const ext = file.name.split('.').pop().toLowerCase();
          const contentType = DOWNLOAD_MIME_BY_EXT[ext];
          if (!contentType) { msg.textContent = 'Only PDF, Word, Excel or PowerPoint files are allowed'; return; }
          if (file.size > 20 * 1024 * 1024) { msg.textContent = 'Files must be 20MB or smaller'; return; }
          const reader = new FileReader();
          reader.onload = async () => {
            const base64 = reader.result.split(',')[1];
            try {
              await api('/api/admin/downloads', { method: 'POST', body: Object.assign({}, b, {
                content_base64: base64, content_type: contentType, filename: file.name }) });
              toast('File uploaded'); drawDownloads();
            } catch (e) { msg.textContent = e.message; }
          };
          reader.readAsDataURL(file);
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    // ── Popup banner — a single admin-managed config (like SMTP), not a
    // list: a firm only ever has one notice live on the landing page at a
    // time. Reuses the shared image-upload component.
    function drawBanner() {
      const body = document.getElementById('admin-banner-body');
      api('/api/admin/banner').then(cfg => {
        body.innerHTML =
          '<div class="grid c2" id="admin-banner-form">' +
          field('Title', 'title', cfg, 'text') +
          field('Auto-dismiss after (seconds, 0 = never)', 'display_seconds', cfg, 'number') +
          '</div>' +
          '<div class="field" style="margin-top:8px"><label>Message</label><textarea data-key="message" rows="3" style="width:100%">' + esc(cfg.message || '') + '</textarea></div>' +
          imageUploadHtml('image_path', 'Image (optional)', cfg.image_path, 'JPEG, PNG or WebP only, up to 5MB.') +
          '<div class="grid c2" style="margin-top:8px">' +
          field('Link URL (optional)', 'link_url', cfg, 'text', { placeholder: 'https://... or /services' }) +
          field('Link label', 'link_label', cfg, 'text', { placeholder: 'e.g. Learn more' }) +
          '</div>' +
          field('Show this banner on the site', 'is_active', cfg, 'check') +
          '<div style="margin-top:8px"><button class="btn btn-small btn-primary" id="admin-save-banner">Save</button></div>';
        wireImageUpload(body);

        document.getElementById('admin-save-banner').addEventListener('click', async () => {
          const b = {}; body.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          b.is_active = body.querySelector('[data-bind="is_active"]').checked;
          try { await api('/api/admin/banner', { method: 'PUT', body: b }); toast('Banner saved'); drawBanner(); }
          catch (e) { toast(e.message, true); }
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawFirmProfile() {
      const body = document.getElementById('admin-firm-body');
      Promise.all([api('/api/bootstrap'), api('/api/admin/active-countries')]).then(([boot, countries]) => {
        const firm = boot.firm || {};
        body.innerHTML = '<h3>Firm profile &amp; Google Maps</h3>' +
          '<p class="hint">Shown on the public site header/footer, the contact section and the "Get directions" link — nothing here is hardcoded into the page.</p>' +
          '<div class="grid c3" id="admin-firm-form">' +
          field('Name', 'name', firm, 'text') + field('Short name', 'short_name', firm, 'text') + field('Tagline', 'tagline', firm, 'text') +
          field('PAN', 'pan', firm, 'text') + field('Phone', 'phone', firm, 'text') + field('Email', 'email', firm, 'text') +
          field('Website', 'website', firm, 'text') + field('Office hours', 'office_hours', firm, 'text', { placeholder: 'Sun–Fri, 10:00–17:00' }) +
          field('Country / jurisdiction', 'country_code', firm, 'select', {
            options: countries.map(c => ({ value: c.code, label: c.name })),
            hint: 'Which country\'s tax/audit rates and rules this firm\'s engines use. Only jurisdictions with real regulatory content are listed here.' }) +
          '</div>' +
          '<div class="field"><label>Address</label><textarea data-key="address" rows="2" style="width:100%">' + esc(firm.address || '') + '</textarea></div>' +
          '<div class="grid c2">' +
          field('Google Maps URL ("Get directions" link)', 'google_maps_url', firm, 'text', { placeholder: 'https://maps.google.com/?q=...' }) +
          field('Google Maps embed URL (iframe src)', 'google_maps_embed', firm, 'text', { placeholder: 'https://www.google.com/maps/embed?...',
            hint: 'From Google Maps itself: search your office, Share > Embed a map, then paste that whole code here — it\'ll be extracted automatically. Don\'t use a third-party "map embed generator" site; those can inject content you didn\'t ask for.' }) +
          '</div>' +
          '<div class="grid c4">' +
          field('Facebook URL', 'facebook_url', firm, 'text') + field('LinkedIn URL', 'linkedin_url', firm, 'text') +
          field('Twitter/X URL', 'twitter_url', firm, 'text') + field('Instagram URL', 'instagram_url', firm, 'text') +
          '</div>' +
          '<button class="btn btn-small btn-primary" id="admin-save-firm" style="margin-top:8px">Save firm profile</button>';

        document.getElementById('admin-save-firm').addEventListener('click', async () => {
          const b = {}; document.querySelectorAll('#admin-firm-body [data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          try { await api('/api/admin/firm-profile', { method: 'PUT', body: b }); toast('Firm profile saved'); }
          catch (e) { toast(e.message, true); }
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawSmtp() {
      const body = document.getElementById('admin-smtp-body');
      Promise.all([api('/api/admin/smtp'), api('/api/admin/email-log?limit=20')]).then(([cfg, log]) => {
        body.innerHTML = '<h3>Email (SMTP)</h3>' +
          (cfg.env_fallback_active ? '<div class="card-note">Using CA_SMTP_* environment variables — save a config below to switch to database-managed settings.</div>' : '') +
          '<div class="grid c3" id="admin-smtp-form">' +
          field('SMTP host', 'host', cfg, 'text') + field('Port', 'port', cfg, 'number') +
          field('Username', 'username', cfg, 'text') +
          field('Password' + (cfg.has_password ? ' (leave blank to keep the current one)' : ''), 'password', {}, 'password') +
          field('From name', 'from_name', cfg, 'text') + field('From email', 'from_email', cfg, 'text') +
          '</div>' + field('Use STARTTLS', 'use_tls', cfg, 'check') +
          '<div style="margin-top:8px"><button class="btn btn-small btn-primary" id="admin-save-smtp">Save</button> ' +
          '<button class="btn btn-small" id="admin-test-smtp">Send test email</button></div>' +
          '<h3 style="margin-top:20px">Recent email activity</h3>' +
          '<div class="table-wrap"><table><thead><tr><th>Time</th><th>To</th><th>Subject</th><th>Status</th></tr></thead><tbody>' +
          (log.length ? log.map(l => '<tr><td>' + esc(l.created_at) + '</td><td>' + esc(l.to_addr) + '</td><td>' + esc(l.subject) + '</td>' +
            '<td><span class="tag ' + (l.status === 'sent' ? 'tag-green' : l.status === 'failed' ? 'tag-red' : 'tag-amber') + '"' +
            (l.error ? ' title="' + esc(l.error) + '"' : '') + '>' + esc(l.status) + '</span></td></tr>').join('')
            : '<tr><td colspan="4"><div class="empty">No email sent yet</div></td></tr>') +
          '</tbody></table></div>';

        document.getElementById('admin-save-smtp').addEventListener('click', async () => {
          const b = {}; document.querySelectorAll('#admin-smtp-form [data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          b.use_tls = document.querySelector('#admin-smtp-body [data-bind="use_tls"]').checked;
          try { await api('/api/admin/smtp', { method: 'PUT', body: b }); toast('SMTP settings saved'); drawSmtp(); }
          catch (e) { toast(e.message, true); }
        });
        document.getElementById('admin-test-smtp').addEventListener('click', async () => {
          try { await api('/api/admin/smtp/test', { method: 'POST', body: {} }); toast('Test email sent — check the inbox'); drawSmtp(); }
          catch (e) { toast(e.message, true); }
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    // ── Payment gateways — Khalti and ConnectIPS, both entirely optional
    // and entirely inert (checkout/pay buttons stay disabled) until real
    // credentials are saved here. Every secret field is write-only, same
    // posture as SMTP above: a GET never returns a stored key/password, and
    // leaving a field blank on save keeps whatever's already stored.
    function drawPaymentGateway() {
      const body = document.getElementById('admin-payment-body');
      api('/api/admin/payment-gateway').then(cfg => {
        body.innerHTML = '<h3>Payment gateways</h3>' +
          '<p class="hint">Blank until you provide real credentials — checkout and invoice payment stay disabled for a gateway until its required fields are filled in here and saved. Ask your provider (Khalti/your bank for ConnectIPS) for these.</p>' +
          '<h4 style="margin-top:14px">Khalti</h4>' +
          '<div class="grid c3">' +
          field('Secret key' + (cfg.has_khalti_key ? ' (leave blank to keep the current one)' : ''), 'khalti_secret_key', {}, 'password') +
          field('Public key (optional)', 'khalti_public_key', cfg, 'text') +
          field('Mode', 'khalti_mode', cfg, 'select', { options: [{ value: 'test', label: 'Test' }, { value: 'live', label: 'Live' }] }) +
          '</div>' +
          '<h4 style="margin-top:16px">ConnectIPS</h4>' +
          (!cfg.crypto_available ? '<div class="card-note" style="color:var(--crimson)">The server is missing the "cryptography" package needed to sign ConnectIPS transactions — ask whoever manages the server to install it (see requirements.txt) before this gateway can go live.</div>' : '') +
          '<div class="grid c3">' +
          field('Merchant ID', 'connectips_merchant_id', cfg, 'text') +
          field('App ID', 'connectips_app_id', cfg, 'text') +
          field('App name', 'connectips_app_name', cfg, 'text') +
          field('API password' + (cfg.has_connectips_password ? ' (leave blank to keep the current one)' : ''), 'connectips_password', {}, 'password') +
          field('Mode', 'connectips_mode', cfg, 'select', { options: [{ value: 'test', label: 'Test (UAT)' }, { value: 'live', label: 'Live' }] }) +
          '</div>' +
          '<div class="card-note" style="margin-top:8px">Certificate (.pfx, issued by your bank)' +
          (cfg.has_connectips_cert ? ' — a certificate is already on file; upload a new one to replace it.' : ' — required for ConnectIPS to work.') + '</div>' +
          '<div class="grid c3">' +
          '<div class="field"><label>.pfx file</label><input type="file" accept=".pfx,.p12" id="admin-connectips-cert-file"></div>' +
          field('Certificate password', 'connectips_cert_password', {}, 'password') +
          '<div class="field"><label>&nbsp;</label><button class="btn btn-small" id="admin-upload-cert">Upload certificate</button></div>' +
          '</div>' +
          '<button class="btn btn-small btn-primary" id="admin-save-payment" style="margin-top:12px">Save payment gateway settings</button>' +
          '<h3 style="margin-top:20px">Recent transactions</h3><div id="admin-payment-txns"><div class="empty">Loading…</div></div>';

        document.getElementById('admin-save-payment').addEventListener('click', async () => {
          const b = {};
          document.querySelectorAll('#admin-payment-body [data-key]').forEach(el => {
            if (el.dataset.key === 'connectips_cert_password') return;
            b[el.dataset.key] = el.value;
          });
          try { await api('/api/admin/payment-gateway', { method: 'PUT', body: b }); toast('Payment gateway settings saved'); drawPaymentGateway(); }
          catch (e) { toast(e.message, true); }
        });
        document.getElementById('admin-upload-cert').addEventListener('click', async () => {
          const fileInput = document.getElementById('admin-connectips-cert-file');
          const file = fileInput.files[0];
          if (!file) { toast('Choose a .pfx file first', true); return; }
          const certPass = document.querySelector('#admin-payment-body [data-key="connectips_cert_password"]').value;
          const reader = new FileReader();
          reader.onload = async () => {
            const base64 = reader.result.split(',')[1];
            try {
              await api('/api/admin/payment-gateway/connectips-cert', { method: 'POST', body: { content_base64: base64, cert_password: certPass } });
              toast('Certificate uploaded'); drawPaymentGateway();
            } catch (e) { toast(e.message, true); }
          };
          reader.readAsDataURL(file);
        });

        api('/api/admin/payment-transactions?limit=30').then(txns => {
          const holder = document.getElementById('admin-payment-txns');
          holder.innerHTML = txns.length
            ? '<div class="table-wrap"><table><thead><tr><th>Time</th><th>Gateway</th><th>For</th><th class="num">Amount</th><th>Status</th></tr></thead><tbody>' +
              txns.map(t => '<tr><td>' + esc(t.created_at) + '</td><td>' + esc(t.gateway) + '</td><td>' + esc(t.reference_type) + ' #' + t.reference_id + '</td>' +
                '<td class="num">' + money(t.amount) + '</td><td><span class="tag ' +
                (t.status === 'success' ? 'tag-green' : t.status === 'failed' ? 'tag-red' : 'tag-amber') + '">' + esc(t.status) + '</span></td></tr>').join('') +
              '</tbody></table></div>'
            : '<div class="empty">No payment attempts yet.</div>';
        }).catch(() => {});
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    // ── AI providers — powers the public chatbot and the internal CA
    // Assistant. Every provider this build knows how to call (from
    // GET's `available` list) always shows a row, whether or not a key is
    // saved for it yet — configured ones show masked status (never the key
    // itself) with Activate/Delete; unconfigured ones show an inline
    // "add a key" form. No restart needed after saving — ai_provider.py
    // resolves live from the database on the next chat message.
    function drawAiProviders() {
      const body = document.getElementById('admin-ai-providers-body');
      const PROVIDER_LABELS = { groq: 'Groq (free tier)', anthropic: 'Anthropic (Claude)' };
      const PROVIDER_HELP = {
        groq: 'Free — no credit card. Get a key at console.groq.com > API Keys.',
        anthropic: 'Paid. Get a key at console.anthropic.com.',
      };
      api('/api/admin/ai-providers').then(res => {
        const configuredByProvider = {};
        res.configured.forEach(c => { configuredByProvider[c.provider] = c; });
        const activeProvider = res.status.provider;

        body.innerHTML =
          '<div class="card-note">' + (res.status.configured
            ? 'Currently powering chat: <b>' + esc(PROVIDER_LABELS[activeProvider] || activeProvider) + '</b> (' + esc(res.status.model || 'default model') + ')'
            : '<span style="color:var(--crimson)">Not configured</span> — the chatbot and CA Assistant stay off until a key is added below.') +
          '</div>' +
          res.available.map(p => {
            const cfg = configuredByProvider[p];
            const isActive = cfg && cfg.is_active;
            return '<div class="card" style="margin-top:10px" data-provider-row="' + p + '">' +
              '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
              '<b>' + esc(PROVIDER_LABELS[p] || p) + '</b>' +
              (cfg
                ? (isActive ? '<span class="tag tag-green">Active</span>' : '<span class="tag">Key saved</span>')
                : '<span class="hint">No key saved</span>') +
              (cfg && cfg.model ? '<span class="hint">model: ' + esc(cfg.model) + '</span>' : '') +
              '</div>' +
              '<p class="hint" style="margin:6px 0 0">' + esc(PROVIDER_HELP[p] || '') + '</p>' +
              (cfg
                ? '<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap">' +
                  (isActive ? '' : '<button class="btn btn-small" data-activate-provider="' + p + '">Make active</button>') +
                  '<button class="btn btn-small" data-toggle-replace="' + p + '">Replace key</button>' +
                  '<button class="btn btn-small btn-danger" data-delete-provider="' + p + '">Delete</button>' +
                  '</div>'
                : '') +
              '<div class="grid c3" data-provider-form="' + p + '" style="margin-top:10px' + (cfg ? ';display:none' : '') + '">' +
              field('API key', 'api_key', {}, 'password') +
              field('Model (optional override)', 'model', {}, 'text', { placeholder: 'leave blank for the default' }) +
              '<div class="field"><label>&nbsp;</label><button class="btn btn-small btn-primary" data-save-provider="' + p + '">' +
              (cfg ? 'Save new key' : 'Add key') + '</button></div>' +
              '</div>' +
              '</div>';
          }).join('');

        body.querySelectorAll('[data-toggle-replace]').forEach(btn => btn.addEventListener('click', () => {
          const p = btn.dataset.toggleReplace;
          const form = body.querySelector('[data-provider-form="' + p + '"]');
          form.style.display = form.style.display === 'none' ? '' : 'none';
        }));
        body.querySelectorAll('[data-save-provider]').forEach(btn => btn.addEventListener('click', async () => {
          const p = btn.dataset.saveProvider;
          const form = body.querySelector('[data-provider-form="' + p + '"]');
          const apiKey = form.querySelector('[data-key="api_key"]').value.trim();
          const model = form.querySelector('[data-key="model"]').value.trim();
          if (!apiKey) { toast('Paste an API key first', true); return; }
          try {
            await api('/api/admin/ai-providers/' + p, { method: 'PUT', body: { api_key: apiKey, model: model || null, make_active: true } });
            toast('Key saved and set active'); drawAiProviders();
          } catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-activate-provider]').forEach(btn => btn.addEventListener('click', async () => {
          try { await api('/api/admin/ai-providers/' + btn.dataset.activateProvider + '/activate', { method: 'POST', body: {} }); toast('Now active'); drawAiProviders(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-delete-provider]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this provider\'s key? Chat will fall back to any other configured provider, or turn off if none remain.')) return;
          try { await api('/api/admin/ai-providers/' + btn.dataset.deleteProvider, { method: 'DELETE' }); toast('Key deleted'); drawAiProviders(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    // ── Social login (client portal) — same masked-CRUD shape as AI
    // Providers, but with a redirect URI to hand to each provider's own
    // console (Google Cloud Console → Credentials → OAuth client), since
    // that's the one piece of setup the provider itself requires.
    function drawOAuthProviders() {
      const body = document.getElementById('admin-oauth-body');
      const PROVIDER_LABELS = { google: 'Google' };
      const PROVIDER_SETUP = {
        google: 'console.cloud.google.com → APIs & Services → Credentials → Create OAuth client ID (type: Web application).',
      };
      api('/api/admin/oauth-providers').then(res => {
        const configuredByProvider = {};
        res.configured.forEach(c => { configuredByProvider[c.provider] = c; });

        body.innerHTML = res.available.map(p => {
          const cfg = configuredByProvider[p];
          const redirectUri = location.origin + '/api/portal/oauth/' + p + '/callback';
          return '<div class="card" style="margin-top:10px" data-oauth-row="' + p + '">' +
            '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
            '<b>Sign in with ' + esc(PROVIDER_LABELS[p] || p) + '</b>' +
            (cfg
              ? (cfg.is_active ? '<span class="tag tag-green">Live on the portal</span>' : '<span class="tag">Saved, not shown</span>')
              : '<span class="hint">Not configured</span>') +
            '</div>' +
            '<p class="hint" style="margin:6px 0 0">' + esc(PROVIDER_SETUP[p] || '') + '</p>' +
            '<div class="field" style="margin-top:6px"><label>Redirect URI to register with ' + esc(PROVIDER_LABELS[p] || p) + '</label>' +
            '<input type="text" readonly value="' + esc(redirectUri) + '" onclick="this.select()" style="width:100%;font-family:var(--mono);font-size:12px"></div>' +
            (cfg ? '<div style="margin-top:8px"><button class="btn btn-small" data-toggle-oauth-replace="' + p + '">Replace credentials</button> ' +
              '<button class="btn btn-small btn-danger" data-delete-oauth="' + p + '">Delete</button></div>' : '') +
            '<div class="grid c2" data-oauth-form="' + p + '" style="margin-top:10px' + (cfg ? ';display:none' : '') + '">' +
            field('Client ID', 'client_id', cfg || {}, 'text') +
            field('Client secret' + (cfg ? ' (leave blank to keep the current one)' : ''), 'client_secret', {}, 'password') +
            '</div>' +
            '<div style="margin-top:8px"><button class="btn btn-small btn-primary" data-save-oauth="' + p + '">' +
            (cfg ? 'Save' : 'Enable Sign in with ' + esc(PROVIDER_LABELS[p] || p)) + '</button></div>' +
            '</div>';
        }).join('');

        body.querySelectorAll('[data-toggle-oauth-replace]').forEach(btn => btn.addEventListener('click', () => {
          const form = body.querySelector('[data-oauth-form="' + btn.dataset.toggleOauthReplace + '"]');
          form.style.display = form.style.display === 'none' ? '' : 'none';
        }));
        body.querySelectorAll('[data-save-oauth]').forEach(btn => btn.addEventListener('click', async () => {
          const p = btn.dataset.saveOauth;
          const form = body.querySelector('[data-oauth-form="' + p + '"]');
          const clientId = form.querySelector('[data-key="client_id"]').value.trim();
          const clientSecret = form.querySelector('[data-key="client_secret"]').value.trim();
          if (!clientId) { toast('A Client ID is required', true); return; }
          try {
            await api('/api/admin/oauth-providers/' + p, { method: 'PUT', body: { client_id: clientId, client_secret: clientSecret, is_active: true } });
            toast('Saved — Sign in with ' + (PROVIDER_LABELS[p] || p) + ' is live on the portal'); drawOAuthProviders();
          } catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-delete-oauth]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Remove these credentials? The button disappears from the portal immediately.')) return;
          try { await api('/api/admin/oauth-providers/' + btn.dataset.deleteOauth, { method: 'DELETE' }); toast('Removed'); drawOAuthProviders(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    drawMenu(); drawGallery(); drawServices(); drawPages(); drawTestimonials(); drawMilestones(); drawTeam(); drawYoutube(); drawDownloads(); drawBanner(); drawFirmProfile(); drawSmtp(); drawPaymentGateway(); drawAiProviders(); drawOAuthProviders();
  }

  // ─────────────────────────────────────────────────────────────── blog
  function renderBlogAdmin() {
    const view = document.getElementById('view');
    view.innerHTML =
      '<div class="page-head"><div><h2>Blog</h2><p>Categories and articles, live at /blog — Markdown body, an admin-editable cover image, FAQs that become FAQPage structured data, and a view count per article.</p></div></div>' +
      '<div class="card" id="admin-blog-body"><div class="empty">Loading…</div></div>';

    if (!state.online || !state.token || state.offlineMode) {
      document.getElementById('admin-blog-body').innerHTML = '<div class="empty"><b>Offline</b>The blog lives on the server. Reconnect to see or edit it.</div>';
      return;
    }


    function drawBlog() {
      const body = document.getElementById('admin-blog-body');
      Promise.all([api('/api/admin/blog-categories'), api('/api/admin/blog')]).then(([cats, posts]) => {
        const catOptions = [{ value: '', label: '(none)' }].concat(cats.map(c => ({ value: c.id, label: c.name })));
        body.innerHTML = '<h3>Categories</h3>' +
          '<div class="table-wrap"><table><thead><tr><th>Name</th><th>Slug</th><th></th></tr></thead><tbody>' +
          cats.map(c => '<tr><td>' + esc(c.name) + '</td><td>' + esc(c.slug) + '</td>' +
            '<td><button class="btn btn-small btn-danger" data-del-blogcat="' + c.id + '">Remove</button></td></tr>').join('') +
          '</tbody></table></div>' +
          '<div class="grid c2" style="margin-top:10px" id="admin-new-blogcat">' +
          field('Name', 'name', {}, 'text') + field('Slug', 'slug', {}, 'text') +
          '</div><button class="btn btn-small" id="admin-add-blogcat" style="margin-top:8px">Add category</button>' +

          '<h3 style="margin-top:20px">Articles</h3>' +
          posts.map(p => {
            const statusTag = p.status === 'published' ? '<span class="tag tag-green">Published</span>'
              : p.status === 'scheduled' ? '<span class="tag tag-amber">Scheduled</span>' : '<span class="tag">Draft</span>';
            return '<details class="card" style="margin-top:10px" data-blog-details="' + p.id + '"><summary><b>' + esc(p.title) + '</b> ' + statusTag +
              '<span class="hint"> — /blog/' + esc(p.slug) + ' · <b>' + p.views + ' view' + (p.views === 1 ? '' : 's') + '</b></span></summary>' +
              '<div id="admin-blog-edit-' + p.id + '"><div class="empty">Loading…</div></div></details>';
          }).join('') +
          '<div class="card-note" style="margin-top:12px">New article</div>' +
          '<div id="admin-new-blog">' +
          '<div class="grid c3">' +
          field('Title', 'title', {}, 'text') + field('Slug', 'slug', {}, 'text') +
          field('Category', 'category_id', {}, 'select', { options: catOptions }) +
          '</div><div class="field"><label>Body (Markdown)</label><textarea data-key="body_md" rows="6" style="width:100%"></textarea></div>' +
          '</div>' +
          '<button class="btn btn-small" id="admin-add-blog" style="margin-top:8px">Add article</button>';

        body.querySelector('#admin-add-blogcat').addEventListener('click', async () => {
          const box = document.getElementById('admin-new-blogcat');
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          if (!b.name || !b.slug) { toast('Name and slug are required', true); return; }
          try { await api('/api/admin/blog-categories', { method: 'POST', body: b }); toast('Category added'); drawBlog(); }
          catch (e) { toast(e.message, true); }
        });
        body.querySelectorAll('[data-del-blogcat]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Remove this category? Articles in it become uncategorised, not deleted.')) return;
          try { await api('/api/admin/blog-categories/' + btn.dataset.delBlogcat, { method: 'DELETE' }); toast('Removed'); drawBlog(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelector('#admin-add-blog').addEventListener('click', async () => {
          const box = document.getElementById('admin-new-blog');
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          if (!b.title || !b.slug || !b.body_md) { toast('Title, slug and body are required', true); return; }
          try { await api('/api/admin/blog', { method: 'POST', body: b }); toast('Article added as a draft'); drawBlog(); }
          catch (e) { toast(e.message, true); }
        });

        // Post editors load lazily (one API call per post, only when its
        // <details> opens) rather than all at once — a blog with fifty
        // posts shouldn't mean fifty fetches on every visit to this screen.
        body.querySelectorAll('[data-blog-details]').forEach(det => {
          let loaded = false;
          det.addEventListener('toggle', async () => {
            if (!det.open || loaded) return;
            loaded = true;
            const pid = det.dataset.blogDetails;
            const editHolder = document.getElementById('admin-blog-edit-' + pid);
            try {
              const post = await api('/api/admin/blog/' + pid);
              editHolder.innerHTML =
                '<div class="grid c3" style="margin-top:10px" data-blog-fields="' + pid + '">' +
                field('Title', 'title', post, 'text') + field('Slug', 'slug', post, 'text') +
                field('Category', 'category_id', post, 'select', { options: catOptions }) +
                field('Status', 'status', post, 'select', { options: [
                  { value: 'draft', label: 'Draft' }, { value: 'scheduled', label: 'Scheduled' }, { value: 'published', label: 'Published' }] }) +
                field('Meta title', 'focus_keyword', post, 'text', { hint: 'focus keyword' }) +
                field('Canonical URL', 'canonical_url', post, 'text') +
                '</div>' +
                imageUploadHtml('og_image_path', 'Cover image', post.og_image_path, 'JPEG, PNG or WebP only, up to 5MB — used as the article\'s card image and social-share preview.') +
                '<div class="field"><label>Meta description</label><textarea data-key="meta_description" rows="2" style="width:100%">' + esc(post.meta_description || '') + '</textarea></div>' +
                '<div class="field"><label>Body (Markdown)</label><textarea data-key="body_md" rows="10" style="width:100%">' + esc(post.body_md || '') + '</textarea></div>' +
                '<div class="card-note" style="margin-top:8px">FAQs (become FAQPage structured data)</div>' +
                '<div id="admin-blog-faqs-' + pid + '">' + (post.faqs || []).map((f, i) =>
                  '<div class="grid c2" data-faq-row="' + i + '" style="margin-top:6px">' +
                  '<input data-faq-q value="' + esc(f.question) + '" placeholder="Question">' +
                  '<input data-faq-a value="' + esc(f.answer) + '" placeholder="Answer">' +
                  '</div>').join('') + '</div>' +
                '<button class="btn btn-small" data-add-faq="' + pid + '" style="margin-top:6px">Add FAQ</button>' +
                '<div style="margin-top:10px"><button class="btn btn-small btn-primary" data-save-blog="' + pid + '">Save</button> ' +
                '<button class="btn btn-small btn-danger" data-del-blog="' + pid + '">Delete</button></div>';

              wireImageUpload(editHolder);
              editHolder.querySelector('[data-add-faq]').addEventListener('click', () => {
                const faqBox = document.getElementById('admin-blog-faqs-' + pid);
                const i = faqBox.children.length;
                const row = document.createElement('div');
                row.className = 'grid c2'; row.dataset.faqRow = i; row.style.marginTop = '6px';
                row.innerHTML = '<input data-faq-q placeholder="Question"><input data-faq-a placeholder="Answer">';
                faqBox.appendChild(row);
              });
              editHolder.querySelector('[data-save-blog]').addEventListener('click', async () => {
                const fieldBox = editHolder.querySelector('[data-blog-fields]');
                const b = {}; fieldBox.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
                b.og_image_path = editHolder.querySelector('[data-cover-url]').value;
                b.meta_description = editHolder.querySelector('textarea[data-key="meta_description"]').value;
                b.body_md = editHolder.querySelector('textarea[data-key="body_md"]').value;
                b.faqs = Array.from(editHolder.querySelectorAll('[data-faq-row]')).map(row => ({
                  question: row.querySelector('[data-faq-q]').value.trim(),
                  answer: row.querySelector('[data-faq-a]').value.trim(),
                })).filter(f => f.question && f.answer);
                try { await api('/api/admin/blog/' + pid, { method: 'PUT', body: b }); toast('Article saved'); drawBlog(); }
                catch (e) { toast(e.message, true); }
              });
              editHolder.querySelector('[data-del-blog]').addEventListener('click', async () => {
                if (!confirm('Delete this article? This cannot be undone.')) return;
                try { await api('/api/admin/blog/' + pid, { method: 'DELETE' }); toast('Deleted'); drawBlog(); }
                catch (e) { toast(e.message, true); }
              });
            } catch (e) {
              editHolder.innerHTML = '<div class="empty">' + esc(e.message) + '</div>';
            }
          });
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    drawBlog();
  }

  // ────────────────────────────────────────────────────────────── careers
  function renderCareerAdmin() {
    const view = document.getElementById('view');
    view.innerHTML =
      '<div class="page-head"><div><h2>Careers</h2><p>Job postings (live at /careers), the applications pipeline, and the emails each stage change sends automatically.</p></div></div>' +
      '<h3>Job postings</h3>' +
      '<div class="card" id="admin-careers-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Applications</h3>' +
      '<div class="card" id="admin-applications-body"><div class="empty">Loading…</div></div>' +
      '<h3 style="margin-top:24px">Email templates</h3>' +
      '<div class="card" id="admin-templates-body"><div class="empty">Loading…</div></div>';

    if (!state.online || !state.token || state.offlineMode) {
      document.getElementById('admin-careers-body').innerHTML = '<div class="empty"><b>Offline</b>Careers live on the server. Reconnect to see or edit them.</div>';
      return;
    }

    function drawCareers() {
      const body = document.getElementById('admin-careers-body');
      api('/api/admin/careers').then(jobs => {
        body.innerHTML = jobs.map(j => {
            const pub = j.is_published ? '<span class="tag tag-green">Published</span>' : '<span class="tag">Draft</span>';
            return '<details class="card" style="margin-top:10px"><summary><b>' + esc(j.title) + '</b> ' + pub +
              '<span class="hint"> — /careers/' + esc(j.slug) + ' · ' + j.application_count + ' application' + (j.application_count === 1 ? '' : 's') + '</span></summary>' +
              '<div class="grid c3" style="margin-top:12px" data-career-row="' + j.id + '">' +
              field('Title', 'title', j, 'text') + field('Slug', 'slug', j, 'text') + field('Department', 'department', j, 'text') +
              field('Location', 'location', j, 'text') +
              field('Type', 'employment_type', j, 'select', { options: [
                { value: 'full_time', label: 'Full time' }, { value: 'part_time', label: 'Part time' },
                { value: 'contract', label: 'Contract' }, { value: 'internship', label: 'Internship' }] }) +
              field('Salary range', 'salary_range', j, 'text') + field('Experience required', 'experience_required', j, 'text') +
              field('Education required', 'education_required', j, 'text') + field('Deadline', 'deadline', j, 'date') +
              '</div>' +
              '<div class="field" style="margin-top:8px"><label>Skills</label><input data-key="skills" value="' + esc(j.skills || '') + '" style="width:100%"></div>' +
              '<div class="field"><label>Description (plain text)</label><textarea data-key="description_html" rows="3" style="width:100%">' + esc(j.description_html || '') + '</textarea></div>' +
              '<div class="field"><label>Responsibilities (plain text)</label><textarea data-key="responsibilities_html" rows="3" style="width:100%">' + esc(j.responsibilities_html || '') + '</textarea></div>' +
              '<div class="field"><label>Requirements (plain text)</label><textarea data-key="requirements_html" rows="3" style="width:100%">' + esc(j.requirements_html || '') + '</textarea></div>' +
              field('Published', 'is_published', j, 'check') +
              '<div style="margin-top:8px"><button class="btn btn-small btn-primary" data-save-career="' + j.id + '">Save</button> ' +
              '<button class="btn btn-small btn-danger" data-del-career="' + j.id + '">Delete</button></div></details>';
          }).join('') +
          '<div class="card-note" style="margin-top:12px">New posting</div>' +
          '<div class="grid c3" id="admin-new-career">' +
          field('Title', 'title', {}, 'text') + field('Slug', 'slug', {}, 'text') + field('Department', 'department', {}, 'text') +
          '</div><button class="btn btn-small" id="admin-add-career" style="margin-top:8px">Add posting</button>';

        body.querySelectorAll('[data-save-career]').forEach(btn => btn.addEventListener('click', async () => {
          const jid = btn.dataset.saveCareer;
          const box = body.querySelector('[data-career-row="' + jid + '"]').closest('details');
          const b = {};
          box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          box.querySelectorAll('[data-bind]').forEach(el => { b[el.dataset.bind] = el.checked; });
          try { await api('/api/admin/careers/' + jid, { method: 'PUT', body: b }); toast('Posting saved'); drawCareers(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-del-career]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this posting? Its applications are deleted too.')) return;
          try { await api('/api/admin/careers/' + btn.dataset.delCareer, { method: 'DELETE' }); toast('Deleted'); drawCareers(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelector('#admin-add-career').addEventListener('click', async () => {
          const box = document.getElementById('admin-new-career');
          const b = {}; box.querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          if (!b.title || !b.slug) { toast('Title and slug are required', true); return; }
          try { await api('/api/admin/careers', { method: 'POST', body: b }); toast('Posting added — edit it below to publish'); drawCareers(); }
          catch (e) { toast(e.message, true); }
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    // ── Applications pipeline — stage changes here trigger the matching
    // admin-editable email template automatically (see Email templates card).
    const CAREER_STAGE_OPTIONS = [
      ['new', 'New'], ['screening', 'Screening'], ['under_review', 'Under review'],
      ['shortlisted', 'Shortlisted'], ['interview', 'Interview'], ['selected', 'Selected'], ['rejected', 'Rejected']
    ];
    function drawApplications() {
      const body = document.getElementById('admin-applications-body');
      api('/api/admin/applications').then(apps => {
        body.innerHTML =
          (apps.length ? '<div class="table-wrap"><table><thead><tr><th>Applicant</th><th>Position</th><th>Stage</th><th>Applied</th><th></th></tr></thead><tbody>' +
            apps.map(a => '<tr><td>' + esc(a.name) + '<div class="hint">' + esc(a.email) + '</div></td><td>' + esc(a.job_title) + '</td>' +
              '<td><select data-stage-select="' + a.id + '">' + CAREER_STAGE_OPTIONS.map(([v, l]) =>
                '<option value="' + v + '"' + (v === a.stage ? ' selected' : '') + '>' + l + '</option>').join('') + '</select></td>' +
              '<td>' + esc((a.created_at || '').slice(0, 10)) + '</td>' +
              '<td><button class="btn btn-small" data-dl-cv="' + a.id + '" data-cv-name="' + esc(a.name) + '">CV</button></td></tr>').join('') +
            '</tbody></table></div>' : '<div class="empty">No applications yet.</div>');

        body.querySelectorAll('[data-stage-select]').forEach(sel => sel.addEventListener('change', async () => {
          try {
            await api('/api/admin/applications/' + sel.dataset.stageSelect, { method: 'PUT', body: { stage: sel.value } });
            toast('Stage updated — the applicant has been emailed automatically');
          } catch (e) { toast(e.message, true); drawApplications(); }
        }));
        // A plain <a href> can't carry the Authorization header a download
        // route needs (only cookies ride along on a bare navigation, and
        // this app is bearer-token-only) — so the CV is fetched
        // authenticated, like every other export in this app, and handed to
        // the browser as a blob instead of linked directly.
        body.querySelectorAll('[data-dl-cv]').forEach(btn => btn.addEventListener('click', async () => {
          try {
            const res = await fetch('/api/admin/applications/' + btn.dataset.dlCv + '/cv',
              { headers: { Authorization: 'Bearer ' + state.token } });
            if (!res.ok) { const j = await res.json().catch(() => null); throw new Error((j && j.error) || 'Could not download the CV'); }
            const blob = await res.blob();
            const cd = res.headers.get('Content-Disposition') || '';
            const nameMatch = cd.match(/filename="?([^"]+)"?/);
            downloadBlob(nameMatch ? nameMatch[1] : (btn.dataset.cvName + ' - CV'), blob);
          } catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    // ── Email templates — the {{name}}/{{position}}/{{firm_name}} bodies
    // used by the careers pipeline above (and, for career_received, on
    // application submission itself).
    function drawEmailTemplates() {
      const body = document.getElementById('admin-templates-body');
      api('/api/admin/email-templates').then(templates => {
        body.innerHTML = '<p class="hint">Use {{name}}, {{position}} and {{firm_name}} — they are substituted automatically when an email is sent.</p>' +
          templates.map(t => '<details class="card" style="margin-top:10px"><summary><b>' + esc(t.key) + '</b></summary>' +
            '<div class="field" style="margin-top:10px"><label>Subject</label><input data-tpl-subject="' + esc(t.key) + '" value="' + esc(t.subject) + '" style="width:100%"></div>' +
            '<div class="field"><label>Body</label><textarea data-tpl-body="' + esc(t.key) + '" rows="4" style="width:100%">' + esc(t.body) + '</textarea></div>' +
            '<button class="btn btn-small btn-primary" data-save-tpl="' + esc(t.key) + '" style="margin-top:8px">Save</button></details>').join('');

        body.querySelectorAll('[data-save-tpl]').forEach(btn => btn.addEventListener('click', async () => {
          const key = btn.dataset.saveTpl;
          const subject = body.querySelector('[data-tpl-subject="' + key + '"]').value;
          const bodyText = body.querySelector('[data-tpl-body="' + key + '"]').value;
          try { await api('/api/admin/email-templates/' + key, { method: 'PUT', body: { subject, body: bodyText } }); toast('Template saved'); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    drawCareers(); drawApplications(); drawEmailTemplates();
  }

  function renderReports() {
    const view = document.getElementById('view');
    view.innerHTML = '<div class="page-head"><div><h2>Reports</h2><p>Everything generated from this account. Excel and Word come from the server; offline exports go straight to your downloads and are not listed here.</p></div></div><div class="card" id="rep-body"><div class="empty">Loading…</div></div>';
    if (!state.online || !state.token || state.offlineMode) {
      document.getElementById('rep-body').innerHTML = '<div class="empty"><b>Offline</b>The report history lives on the server. Your exports still work — they come out as CSV and Word-compatible files until the connection is back.</div>';
      return;
    }
    const isAdmin = !!(state.boot && state.boot.user && state.boot.user.is_super_admin);
    const selected = new Set();
    api('/api/reports').then(rows => {
      const canDelete = r => isAdmin || (state.user && String(r.generated_by) === String(state.user.id));
      const body = document.getElementById('rep-body');
      function draw() {
        const deletable = rows.filter(canDelete);
        body.innerHTML = (rows.length ? (
          '<div style="margin-bottom:10px;display:flex;align-items:center;gap:10px">' +
          '<button class="btn btn-small" id="rep-select-all">' + (deletable.length && deletable.every(r => selected.has(r.id)) ? 'Clear selection' : 'Select all') + '</button>' +
          '<button class="btn btn-small btn-danger" id="rep-bulk-del"' + (selected.size ? '' : ' disabled') + '>Delete selected' + (selected.size ? ' (' + selected.size + ')' : '') + '</button>' +
          '</div>' +
          '<div class="table-wrap"><table><thead><tr><th></th><th>File</th><th>Module</th><th>Format</th><th>Generated</th><th></th></tr></thead><tbody>' +
          rows.map(r => '<tr><td>' + (canDelete(r) ? '<input type="checkbox" data-sel-report="' + r.id + '"' + (selected.has(r.id) ? ' checked' : '') + '>' : '') + '</td>' +
            '<td>' + esc(r.filename) + '</td><td>' + esc(r.module_key) + '</td>' +
            '<td><span class="tag">' + esc(r.format.toUpperCase()) + '</span></td><td>' + esc(r.generated_at) + '</td>' +
            '<td><a class="btn btn-small" href="/api/reports/' + r.id + '/download">Download</a> ' +
            (canDelete(r) ? '<button class="btn btn-small btn-danger" data-del-report="' + r.id + '" title="Remove this generated file — you can always export it again from the module">Delete</button>' : '') +
            '</td></tr>').join('') +
          '</tbody></table></div>'
        ) : '<div class="empty"><b>Nothing generated yet</b>Open a module, fill it in, and use the Excel or Word button.</div>');

        const selectAllBtn = document.getElementById('rep-select-all');
        if (selectAllBtn) selectAllBtn.addEventListener('click', () => {
          if (deletable.length && deletable.every(r => selected.has(r.id))) selected.clear();
          else deletable.forEach(r => selected.add(r.id));
          draw();
        });
        const bulkBtn = document.getElementById('rep-bulk-del');
        if (bulkBtn) bulkBtn.addEventListener('click', async () => {
          if (!selected.size) return;
          if (!confirm('Delete ' + selected.size + ' selected report(s)? This removes the generated files — you can always export them again from their modules.')) return;
          try {
            const res = await api('/api/reports/bulk-delete', { method: 'POST', body: { ids: Array.from(selected) } });
            toast(res.deleted.length + ' deleted' + (res.skipped.length ? ', ' + res.skipped.length + ' skipped' : ''));
            renderReports();
          } catch (err) { toast(err.message, true); }
        });
        body.querySelectorAll('[data-sel-report]').forEach(cb => cb.addEventListener('change', e => {
          const id = Number(e.target.dataset.selReport);
          if (e.target.checked) selected.add(id); else selected.delete(id);
          draw();
        }));
        body.querySelectorAll('[data-del-report]').forEach(btn => btn.addEventListener('click', async e => {
          if (!state.online || state.offlineMode) { toast('Deleting a report needs a connection', true); return; }
          if (!confirm('Delete this report? This removes the generated file — you can always export it again from the module.')) return;
          try {
            await api('/api/reports/' + e.target.dataset.delReport, { method: 'DELETE' });
            toast('Deleted'); renderReports();
          } catch (err) { toast(err.message, true); }
        }));
      }
      draw();
    }).catch(e => { document.getElementById('rep-body').innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  const INVOICE_STATUS_TAG = { draft: 'tag', sent: 'tag tag-amber', paid: 'tag tag-green', void: 'tag tag-red' };

  const CA_ASSISTANT_COMMANDS = [
    'PRE-FINALISATION REVIEW', 'WHAT AM I MISSING?', 'CLIENT EXPLANATION',
    'WORKING PAPER', 'SOURCE CHECK', 'CHECK FOR CHANGES'
  ];

  function renderCaAssistant() {
    const view = document.getElementById('view');
    let status = null;
    let history = [];
    let thread = [];         // [{role, content}]
    let sending = false;
    let quickPending = false;
    let quickQuestions = [];
    let clientId = '';
    let engagementId = '';
    let draftText = '';
    const engagements = () => (state.boot && state.boot.engagements) || [];

    async function init() {
      if (!state.online || state.offlineMode) { draw(); return; }
      try { status = await api('/api/ca-assistant/status'); } catch (e) { status = { configured: false }; }
      try { history = await api('/api/ca-assistant/history'); } catch (e) { history = []; }
      try { quickQuestions = await api('/api/ca-assistant/quick-questions'); } catch (e) { quickQuestions = []; }
      draw();
    }

    function draw() {
      let h = '<div class="page-head"><div><h2>CA Assistant</h2>' +
        '<p>A professional decision-support chat — reasons like a senior CA, grounded in this firm\'s own Rate Master ' +
        'and, when you pick one below, the selected client\'s own facts. It never states a changing figure (VAT rate, ' +
        'TDS threshold, deadline) from memory alone — only from a verified Rate Master entry, flagged if unverified.</p></div></div>';

      if (!state.online || state.offlineMode) {
        h += '<div class="empty"><b>Offline</b>The assistant needs a live connection to the AI provider. Reconnect to chat.</div>';
        view.innerHTML = h; return;
      }
      if (status && !status.configured) {
        h += '<div class="banner"><b>Free-form chat isn\'t configured yet</b> — the server needs a ' +
          '<code>GROQ_API_KEY</code> (free — console.groq.com, no card required) or <code>ANTHROPIC_API_KEY</code> ' +
          'environment variable set, then a restart. ' +
          'The <b>Quick answers</b> below don\'t need it — pick a client and they work right now.</div>';
      }

      h += '<div class="card"><div class="grid c3">' +
        '<div class="field"><label>Client (optional — grounds the chat in their facts)</label><select id="ca-client">' +
        '<option value="">— general, no client —</option>' +
        clients().map(c => '<option value="' + c.id + '"' + (String(clientId) === String(c.id) ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') +
        '</select></div>' +
        '<div class="field"><label>Engagement (optional)</label><select id="ca-engagement">' +
        '<option value="">—</option>' +
        engagements().filter(e => !clientId || String(e.client_id) === String(clientId))
          .map(e => '<option value="' + e.id + '"' + (String(engagementId) === String(e.id) ? ' selected' : '') + '>' + esc(e.title || e.type) + '</option>').join('') +
        '</select></div>' +
        '<div class="field"><label>&nbsp;</label><button class="btn btn-small" id="ca-new-chat">New chat</button></div>' +
        '</div></div>';

      if (clientId) {
        h += '<div class="card"><h3>Quick answers</h3>' +
          '<p class="card-note">Instant, factual — pulled straight from this client\'s own records. No AI, no API key needed.</p>' +
          '<div style="display:flex;flex-wrap:wrap;gap:6px">' +
          quickQuestions.map(q => '<button class="btn btn-small" data-ca-quick="' + esc(q.key) + '"' + (quickPending ? ' disabled' : '') + '>' + esc(q.label) + '</button>').join('') +
          '</div></div>';
      }

      h += '<div class="card"><div class="table-wrap"><div id="ca-thread" style="max-height:480px;overflow-y:auto">';
      if (!thread.length) {
        h += '<div class="empty"><b>No messages yet</b>Ask a technical question, or try a command below.</div>';
      }
      thread.forEach(m => {
        h += '<div style="margin-bottom:16px">' +
          '<div class="hint" style="text-transform:uppercase;letter-spacing:.08em;font-size:10px;margin-bottom:4px">' +
          (m.role === 'user' ? 'You' : 'Assistant') + '</div>' +
          '<div style="white-space:pre-wrap;font-size:13.5px;line-height:1.5">' + esc(m.content) + '</div></div>';
      });
      if (sending) h += '<div class="hint">Thinking…</div>';
      if (quickPending) h += '<div class="hint">Looking it up…</div>';
      h += '</div></div></div>';

      h += '<div class="card">' +
        '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px">' +
        CA_ASSISTANT_COMMANDS.map(c => '<button class="btn btn-small" data-ca-cmd="' + esc(c) + '">' + esc(c) + '</button>').join('') +
        '</div>' +
        '<textarea id="ca-input" rows="3" placeholder="Ask a technical question — facts first, then the assistant reasons through the applicable Nepal law/standard, evidence, risk and conclusion...">' + esc(draftText) + '</textarea>' +
        '<div style="margin-top:8px"><button class="btn btn-primary" id="ca-send"' + (sending || (status && !status.configured) ? ' disabled' : '') + '>Send</button> ' +
        '<span class="hint">Enter to send, Shift+Enter for a new line</span></div>' +
        '</div>';

      if (history.length) {
        h += '<div class="card"><h3>Recent history</h3><p class="card-note">Every turn is logged with the exact Rate Master snapshot used, for audit purposes — this is a read-only log, not a live thread.</p>' +
          '<div class="table-wrap"><table><thead><tr><th>When</th><th>Client</th><th>You asked</th><th></th></tr></thead><tbody>' +
          history.slice(0, 20).map(hrow => '<tr><td>' + esc(hrow.created_at) + '</td><td>' + esc(clientNameOf(hrow.client_id) || '—') + '</td>' +
            '<td>' + esc((hrow.user_message || '').slice(0, 80)) + (hrow.user_message && hrow.user_message.length > 80 ? '…' : '') + '</td>' +
            '<td><button class="btn btn-small" data-ca-hist="' + hrow.id + '">View</button></td></tr>').join('') +
          '</tbody></table></div></div>';
      }

      view.innerHTML = h;
      wireDraw();
    }

    function wireDraw() {
      const clientSel = view.querySelector('#ca-client');
      if (clientSel) clientSel.addEventListener('change', e => { clientId = e.target.value; engagementId = ''; draw(); });
      const engSel = view.querySelector('#ca-engagement');
      if (engSel) engSel.addEventListener('change', e => { engagementId = e.target.value; });
      const newChatBtn = view.querySelector('#ca-new-chat');
      if (newChatBtn) newChatBtn.addEventListener('click', () => { thread = []; draw(); });

      const input = view.querySelector('#ca-input');
      if (input) {
        input.addEventListener('input', e => { draftText = e.target.value; });
        input.addEventListener('keydown', e => {
          if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
        });
        input.focus(); input.setSelectionRange(draftText.length, draftText.length);
      }
      view.querySelectorAll('[data-ca-cmd]').forEach(b => b.addEventListener('click', () => {
        draftText = (draftText ? draftText + '\n' : '') + b.dataset.caCmd;
        draw();
      }));
      view.querySelectorAll('[data-ca-quick]').forEach(b => b.addEventListener('click', () => askQuick(b.dataset.caQuick)));
      const sendBtn = view.querySelector('#ca-send');
      if (sendBtn) sendBtn.addEventListener('click', send);

      view.querySelectorAll('[data-ca-hist]').forEach(b => b.addEventListener('click', () => {
        const row = history.find(x => x.id == b.dataset.caHist);
        if (!row) return;
        alert('You asked:\n' + row.user_message + '\n\n— Assistant —\n' + (row.assistant_reply || '(no reply recorded)'));
      }));

      async function send() {
        const text = (draftText || '').trim();
        if (!text || sending) return;
        thread.push({ role: 'user', content: text });
        draftText = ''; sending = true; draw();
        try {
          const res = await api('/api/ca-assistant/chat', { method: 'POST', body: {
            messages: thread, client_id: clientId ? +clientId : null, engagement_id: engagementId ? +engagementId : null,
            fy: state.fy } });
          thread.push({ role: 'assistant', content: res.reply });
        } catch (e) {
          thread.push({ role: 'assistant', content: '[Error: ' + e.message + ']' });
        }
        sending = false;
        try { history = await api('/api/ca-assistant/history'); } catch (e) { /* ignore */ }
        draw();
      }

      async function askQuick(questionKey) {
        if (quickPending || !clientId) return;
        const q = quickQuestions.find(x => x.key === questionKey);
        thread.push({ role: 'user', content: q ? q.label : questionKey });
        quickPending = true; draw();
        try {
          const res = await api('/api/ca-assistant/quick', { method: 'POST', body: {
            client_id: +clientId, question_key: questionKey } });
          thread.push({ role: 'assistant', content: res.answer });
        } catch (e) {
          thread.push({ role: 'assistant', content: '[Error: ' + e.message + ']' });
        }
        quickPending = false;
        try { history = await api('/api/ca-assistant/history'); } catch (e) { /* ignore */ }
        draw();
      }
    }

    init();
  }

  function renderBilling() {
    const view = document.getElementById('view');
    let timeEntries = [], invoices = [];
    let loaded = false;
    const engagements = () => (state.boot && state.boot.engagements) || [];
    const engLabel = e => (e.client_name || '') + ' — ' + (e.title || e.type || ('Engagement #' + e.id));

    let teDraft = { engagement_id: '', work_date: new Date().toISOString().slice(0, 10), hours: '', rate: '', narrative: '', billable: true };
    let invDraft = { client_id: '', invoice_date: new Date().toISOString().slice(0, 10), due_date: '', apply_vat: false, notes: '' };
    let selectedTimeIds = new Set();
    let manualLines = [];

    async function refresh() {
      if (!state.online || state.offlineMode) { loaded = true; timeEntries = []; invoices = []; draw(); return; }
      try {
        [timeEntries, invoices] = await Promise.all([api('/api/time-entries'), api('/api/invoices')]);
      } catch (e) { toast(e.message, true); timeEntries = []; invoices = []; }
      loaded = true; draw();
    }

    function draw() {
      if (!state.online || state.offlineMode) {
        view.innerHTML = '<div class="page-head"><div><h2>Time &amp; Billing</h2></div></div>' +
          '<div class="empty"><b>Offline</b>Timesheets and invoices live on the server. Reconnect to log time or bill a client.</div>';
        return;
      }
      if (!loaded) { view.innerHTML = '<div class="empty">Loading…</div>'; return; }

      const unbilled = timeEntries.filter(t => !t.invoice_id);
      const unbilledHours = unbilled.reduce((s, t) => s + t.hours, 0);
      const unbilledValue = unbilled.reduce((s, t) => s + t.hours * (t.rate || 0), 0);
      const outstanding = invoices.filter(i => i.status === 'sent').reduce((s, i) => s + (i.total - i.paid_amount), 0);
      const today = new Date().toISOString().slice(0, 10);
      const overdueN = invoices.filter(i => i.status === 'sent' && i.due_date && i.due_date < today).length;

      let h = '<div class="page-head"><div><h2>Time &amp; Billing</h2><p>Log hours against an engagement, then pull the unbilled time — plus any flat-fee lines — onto an invoice.</p></div>' +
        '<div class="page-actions"><button class="btn" id="refresh-bill">Refresh</button></div></div>';

      h += '<div class="grid c4">' +
        kpi('Unbilled hours', unbilledHours.toFixed(1)) +
        kpi('Unbilled value', money(unbilledValue)) +
        kpi('Outstanding (sent)', money(outstanding), outstanding ? 'accent' : '') +
        kpi('Overdue invoices', overdueN, overdueN ? 'alert' : '') +
        '</div>';

      // ---- log time ----
      h += '<div class="card" style="margin-top:16px"><h3>Log time</h3><div class="grid c4" id="te-form">' +
        '<div class="field"><label>Engagement</label><select data-tek="engagement_id"><option value="">— choose —</option>' +
        engagements().map(e => '<option value="' + e.id + '"' + (String(teDraft.engagement_id) === String(e.id) ? ' selected' : '') + '>' + esc(engLabel(e)) + '</option>').join('') + '</select></div>' +
        '<div class="field"><label>Date</label><input type="date" data-tek="work_date" value="' + esc(teDraft.work_date) + '"></div>' +
        '<div class="field"><label>Hours</label><input type="number" step="0.25" data-tek="hours" value="' + esc(teDraft.hours) + '"></div>' +
        '<div class="field"><label>Rate per hour</label><input type="number" data-tek="rate" value="' + esc(teDraft.rate) + '"></div>' +
        '<div class="field" style="grid-column:1/-1"><label>Narrative</label><input data-tek="narrative" value="' + esc(teDraft.narrative) + '" placeholder="What was done"></div>' +
        '<label class="check"><input type="checkbox" id="te-billable"' + (teDraft.billable ? ' checked' : '') + '> Billable</label>' +
        '<div class="field"><label>&nbsp;</label><button class="btn btn-primary btn-small" id="add-te">Log time</button></div>' +
        '</div></div>';

      h += '<div class="card"><h3>Timesheet</h3>' +
        (timeEntries.length ? '<div class="table-wrap"><table><thead><tr><th>Date</th><th>Engagement</th><th>By</th><th>Narrative</th>' +
          '<th class="num">Hours</th><th class="num">Rate</th><th class="num">Amount</th><th>Billing</th><th></th></tr></thead><tbody>' +
          timeEntries.map(t => '<tr><td>' + esc(t.work_date) + '</td><td>' + esc(t.client_name) + ' — ' + esc(t.engagement_title || '') + '</td>' +
            '<td>' + esc(t.user_name || '') + '</td><td>' + esc(t.narrative || '') + '</td>' +
            '<td class="num">' + t.hours + '</td><td class="num">' + money(t.rate) + '</td><td class="num">' + money(t.hours * (t.rate || 0)) + '</td>' +
            '<td>' + (t.invoice_id ? '<span class="tag tag-green">Invoiced</span>' : (t.billable ? '<span class="tag">Unbilled</span>' : '<span class="tag">Non-billable</span>')) + '</td>' +
            '<td>' + (t.invoice_id ? '' : '<button class="btn btn-small btn-danger" data-del-te="' + t.id + '" title="Remove this row">Remove</button>') + '</td></tr>').join('') +
          '</tbody></table></div>'
          : '<div class="empty"><b>No time logged yet</b>Log an entry above.</div>') +
        '</div>';

      // ---- create invoice ----
      const clientUnbilled = invDraft.client_id ? unbilled.filter(t => String(t.client_id) === String(invDraft.client_id)) : [];
      h += '<div class="card"><h3>Create an invoice</h3><div class="grid c4" id="inv-form">' +
        '<div class="field"><label>Client</label><select data-ik="client_id"><option value="">— choose —</option>' +
        clients().map(c => '<option value="' + c.id + '"' + (String(invDraft.client_id) === String(c.id) ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') + '</select></div>' +
        '<div class="field"><label>Invoice date</label><input type="date" data-ik="invoice_date" value="' + esc(invDraft.invoice_date) + '"></div>' +
        '<div class="field"><label>Due date</label><input type="date" data-ik="due_date" value="' + esc(invDraft.due_date) + '"></div>' +
        '<label class="check" style="align-self:end"><input type="checkbox" id="inv-vat"' + (invDraft.apply_vat ? ' checked' : '') + '> Apply VAT</label>' +
        '<div class="field" style="grid-column:1/-1"><label>Notes</label><input data-ik="notes" value="' + esc(invDraft.notes) + '"></div>' +
        '</div>';

      if (invDraft.client_id) {
        h += '<div class="table-wrap" style="margin-top:12px"><b style="font-size:12px">Unbilled time for this client</b>' +
          (clientUnbilled.length ? '<table><thead><tr><th></th><th>Date</th><th>Engagement</th><th>Narrative</th><th class="num">Hours</th><th class="num">Rate</th><th class="num">Amount</th></tr></thead><tbody>' +
            clientUnbilled.map(t => '<tr><td><input type="checkbox" data-sel-te="' + t.id + '"' + (selectedTimeIds.has(t.id) ? ' checked' : '') + '></td>' +
              '<td>' + esc(t.work_date) + '</td><td>' + esc(t.engagement_title || '') + '</td><td>' + esc(t.narrative || '') + '</td>' +
              '<td class="num">' + t.hours + '</td><td class="num">' + money(t.rate) + '</td><td class="num">' + money(t.hours * (t.rate || 0)) + '</td></tr>').join('') +
            '</tbody></table>'
            : '<div class="card-note">No unbilled time for this client.</div>') + '</div>';

        h += '<div class="table-wrap" style="margin-top:12px"><b style="font-size:12px">Manual lines</b><table><thead><tr>' +
          '<th>Description</th><th class="num">Amount</th><th></th></tr></thead><tbody>';
        manualLines.forEach((l, i) => {
          h += '<tr><td><input data-ml="' + i + '" data-k="description" value="' + esc(l.description || '') + '"></td>' +
            '<td class="num"><input type="number" data-ml="' + i + '" data-k="amount" value="' + (l.amount || 0) + '"></td>' +
            '<td><button class="btn btn-small btn-danger" data-del-ml="' + i + '" title="Remove this row">Remove</button></td></tr>';
        });
        h += '</tbody></table></div><button class="btn btn-small" id="add-ml">Add a line</button>';

        const selTotal = clientUnbilled.filter(t => selectedTimeIds.has(t.id)).reduce((s, t) => s + t.hours * (t.rate || 0), 0)
          + manualLines.reduce((s, l) => s + (Number(l.amount) || 0), 0);
        h += '<div class="card-note" style="margin-top:10px">Subtotal so far: ' + money(selTotal) + '</div>' +
          '<button class="btn btn-primary" style="margin-top:6px" id="create-invoice">Create invoice</button>';
      }
      h += '</div>';

      // ---- invoices ----
      h += '<div class="card"><h3>Invoices</h3>' +
        (invoices.length ? '<div class="table-wrap"><table><thead><tr><th>No.</th><th>Client</th><th>Date</th><th>Due</th>' +
          '<th class="num">Total</th><th class="num">Paid</th><th>Status</th><th></th></tr></thead><tbody>' +
          invoices.map(i => '<tr><td>' + esc(i.invoice_no) + '</td><td>' + esc(i.client_name) + '</td><td>' + esc(i.invoice_date) + '</td>' +
            '<td>' + esc(i.due_date || '—') + (i.status === 'sent' && i.due_date && i.due_date < today ? ' <span class="tag tag-red">overdue</span>' : '') + '</td>' +
            '<td class="num">' + money(i.total) + '</td><td class="num">' + money(i.paid_amount) + '</td>' +
            '<td><span class="' + (INVOICE_STATUS_TAG[i.status] || 'tag') + '">' + esc(i.status.toUpperCase()) + '</span></td>' +
            '<td>' + '<button class="btn btn-small" data-inv-doc="' + i.id + '">Word</button> ' +
            (i.status === 'draft' ? '<button class="btn btn-small" data-inv-send="' + i.id + '">Mark sent</button> ' +
              '<button class="btn btn-small btn-danger" data-inv-del="' + i.id + '">Delete</button>' : '') +
            (i.status === 'sent' ? '<button class="btn btn-small" data-inv-paid="' + i.id + '">Mark paid</button> ' +
              '<button class="btn btn-small btn-danger" data-inv-void="' + i.id + '">Void</button>' : '') +
            '</td></tr>').join('') + '</tbody></table></div>'
          : '<div class="empty"><b>No invoices yet</b>Create one above once there\'s billable time or a flat fee to charge.</div>') +
        '</div>';

      view.innerHTML = h;
      wireDraw();
    }

    function wireDraw() {
      const refreshBtn = view.querySelector('#refresh-bill'); if (refreshBtn) refreshBtn.addEventListener('click', refresh);

      view.querySelectorAll('[data-tek]').forEach(el => {
        const ev = el.tagName === 'SELECT' ? 'change' : 'input';
        el.addEventListener(ev, e => { teDraft[e.target.dataset.tek] = e.target.value; });
      });
      const teBillable = view.querySelector('#te-billable'); if (teBillable) teBillable.addEventListener('change', e => { teDraft.billable = e.target.checked; });
      const addTe = view.querySelector('#add-te');
      if (addTe) addTe.addEventListener('click', async () => {
        if (!teDraft.engagement_id || !teDraft.work_date || !teDraft.hours) { toast('Choose an engagement, a date and hours', true); return; }
        try {
          await api('/api/time-entries', { method: 'POST', body: {
            engagement_id: +teDraft.engagement_id, work_date: teDraft.work_date, hours: Number(teDraft.hours),
            rate: teDraft.rate === '' ? null : Number(teDraft.rate), narrative: teDraft.narrative, billable: teDraft.billable } });
          teDraft = { engagement_id: teDraft.engagement_id, work_date: teDraft.work_date, hours: '', rate: teDraft.rate, narrative: '', billable: true };
          toast('Time logged');
          await refresh();
        } catch (e) { toast(e.message, true); }
      });
      view.querySelectorAll('[data-del-te]').forEach(b => b.addEventListener('click', async e => {
        if (!confirm('Remove this time entry?')) return;
        try { await api('/api/time-entries/' + e.target.dataset.delTe, { method: 'DELETE' }); toast('Removed'); await refresh(); }
        catch (err) { toast(err.message, true); }
      }));

      view.querySelectorAll('[data-ik]').forEach(el => {
        const ev = el.tagName === 'SELECT' ? 'change' : 'input';
        el.addEventListener(ev, e => {
          invDraft[e.target.dataset.ik] = e.target.value;
          if (e.target.dataset.ik === 'client_id') { selectedTimeIds = new Set(); manualLines = []; draw(); }
        });
      });
      const invVat = view.querySelector('#inv-vat'); if (invVat) invVat.addEventListener('change', e => { invDraft.apply_vat = e.target.checked; });

      view.querySelectorAll('[data-sel-te]').forEach(el => el.addEventListener('change', e => {
        const id = +e.target.dataset.selTe;
        if (e.target.checked) selectedTimeIds.add(id); else selectedTimeIds.delete(id);
        draw();
      }));
      view.querySelectorAll('[data-ml]').forEach(el => el.addEventListener('input', e => {
        const i = +e.target.dataset.ml, k = e.target.dataset.k;
        manualLines[i][k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
      }));
      view.querySelectorAll('[data-del-ml]').forEach(b => b.addEventListener('click', e => {
        manualLines.splice(+e.target.dataset.delMl, 1); draw();
      }));
      const addMl = view.querySelector('#add-ml');
      if (addMl) addMl.addEventListener('click', () => { manualLines.push({ description: '', amount: 0 }); draw(); });

      const createInv = view.querySelector('#create-invoice');
      if (createInv) createInv.addEventListener('click', async () => {
        if (!invDraft.client_id) { toast('Choose a client', true); return; }
        if (!selectedTimeIds.size && !manualLines.length) { toast('Select some time or add a manual line', true); return; }
        try {
          await api('/api/invoices', { method: 'POST', body: {
            client_id: +invDraft.client_id, invoice_date: invDraft.invoice_date, due_date: invDraft.due_date || null,
            apply_vat: invDraft.apply_vat, notes: invDraft.notes,
            time_entry_ids: Array.from(selectedTimeIds), lines: manualLines.filter(l => l.description && l.amount) } });
          invDraft = { client_id: '', invoice_date: new Date().toISOString().slice(0, 10), due_date: '', apply_vat: false, notes: '' };
          selectedTimeIds = new Set(); manualLines = [];
          toast('Invoice created');
          await refresh();
        } catch (e) { toast(e.message, true); }
      });

      view.querySelectorAll('[data-inv-doc]').forEach(b => b.addEventListener('click', async e => {
        try {
          const res = await api('/api/invoices/' + e.target.dataset.invDoc + '/document', { method: 'POST' });
          const a = document.createElement('a'); a.href = res.download_url; a.download = res.filename;
          document.body.appendChild(a); a.click(); a.remove();
        } catch (err) { toast(err.message, true); }
      }));
      view.querySelectorAll('[data-inv-send]').forEach(b => b.addEventListener('click', async e => {
        try { await api('/api/invoices/' + e.target.dataset.invSend, { method: 'PUT', body: { status: 'sent' } }); toast('Marked sent'); await refresh(); }
        catch (err) { toast(err.message, true); }
      }));
      view.querySelectorAll('[data-inv-paid]').forEach(b => b.addEventListener('click', async e => {
        try { await api('/api/invoices/' + e.target.dataset.invPaid, { method: 'PUT', body: { status: 'paid' } }); toast('Marked paid'); await refresh(); }
        catch (err) { toast(err.message, true); }
      }));
      view.querySelectorAll('[data-inv-void]').forEach(b => b.addEventListener('click', async e => {
        if (!confirm('Void this invoice?')) return;
        try { await api('/api/invoices/' + e.target.dataset.invVoid, { method: 'PUT', body: { status: 'void' } }); toast('Voided'); await refresh(); }
        catch (err) { toast(err.message, true); }
      }));
      view.querySelectorAll('[data-inv-del]').forEach(b => b.addEventListener('click', async e => {
        if (!confirm('Delete this draft invoice? Its time entries go back to unbilled.')) return;
        try { await api('/api/invoices/' + e.target.dataset.invDel, { method: 'DELETE' }); toast('Deleted'); await refresh(); }
        catch (err) { toast(err.message, true); }
      }));
    }

    refresh();
  }

  function renderDocuments() {
    const view = document.getElementById('view');
    view.innerHTML =
      '<div class="page-head"><div><h2>Documents</h2><p>The document register — trial balances, agreements, tax returns and anything else worth keeping against a client or an engagement.</p></div></div>' +
      '<div class="card"><h3>Upload</h3><div class="grid c3">' +
      '<div class="field"><label>Client</label><select id="doc-client"><option value="">— choose —</option>' +
      clients().map(c => '<option value="' + c.id + '">' + esc(c.name) + '</option>').join('') + '</select></div>' +
      '<div class="field"><label>Category</label><select id="doc-cat">' +
      [['tb', 'Trial balance'], ['gl', 'General ledger'], ['bank', 'Bank statement'], ['tax_return', 'Tax return'],
       ['agreement', 'Agreement'], ['other', 'Other']].map(o => '<option value="' + o[0] + '">' + o[1] + '</option>').join('') + '</select></div>' +
      '<div class="field"><label>File</label><input type="file" id="doc-file"></div>' +
      '</div><button class="btn btn-primary" id="doc-upload" style="margin-top:8px">Upload</button></div>' +
      '<div class="card" id="doc-body"><div class="empty">Loading…</div></div>';

    if (!state.online || !state.token || state.offlineMode) {
      document.getElementById('doc-body').innerHTML = '<div class="empty"><b>Offline</b>Documents live on the server. Reconnect to upload, list or download them.</div>';
      return;
    }

    function load() {
      api('/api/documents').then(rows => {
        document.getElementById('doc-body').innerHTML = rows.length
          ? '<div class="table-wrap"><table><thead><tr><th>File</th><th>Category</th><th class="num">Size</th><th>Uploaded</th><th>Client portal</th><th></th></tr></thead><tbody>' +
            rows.map(d => '<tr><td>' + esc(d.filename) + '</td><td><span class="tag">' + esc(d.category || 'other') + '</span></td>' +
              '<td class="num">' + (d.byte_size ? (d.byte_size / 1024).toFixed(1) + ' KB' : '—') + '</td><td>' + esc(d.uploaded_at) + '</td>' +
              '<td>' + (d.client_id ? '<label class="check"><input type="checkbox" data-visible-doc="' + d.id + '"' +
                (d.visible_to_client ? ' checked' : '') + '> Shared</label>' : '<span class="hint">—</span>') + '</td>' +
              '<td><button class="btn btn-small" data-dl-doc="' + d.id + '" data-doc-name="' + esc(d.filename) + '">Download</button> ' +
              '<button class="btn btn-small btn-danger" data-del-doc="' + d.id + '" title="Remove this row">Remove</button></td></tr>').join('') +
            '</tbody></table></div>'
          : '<div class="empty"><b>No documents yet</b>Choose a client, pick a file, and upload.</div>';
        document.querySelectorAll('[data-del-doc]').forEach(b => b.addEventListener('click', async e => {
          if (!confirm('Remove this document? This cannot be undone.')) return;
          try { await api('/api/documents/' + e.target.dataset.delDoc, { method: 'DELETE' }); toast('Removed'); load(); }
          catch (err) { toast(err.message, true); }
        }));
        // A plain <a href> can't carry the Authorization header this
        // download route needs (bearer-token auth, no session cookie), so
        // the file is fetched authenticated and handed to the browser as a
        // blob — same fix as the careers CV download.
        document.querySelectorAll('[data-dl-doc]').forEach(b => b.addEventListener('click', async () => {
          try {
            const res = await fetch('/api/documents/' + b.dataset.dlDoc + '/download', { headers: { Authorization: 'Bearer ' + state.token } });
            if (!res.ok) { const j = await res.json().catch(() => null); throw new Error((j && j.error) || 'Download failed'); }
            const blob = await res.blob();
            const cd = res.headers.get('Content-Disposition') || '';
            const nm = cd.match(/filename="?([^"]+)"?/);
            downloadBlob(nm ? nm[1] : b.dataset.docName, blob);
          } catch (e) { toast(e.message, true); }
        }));
        document.querySelectorAll('[data-visible-doc]').forEach(cb => cb.addEventListener('change', async () => {
          try { await api('/api/documents/' + cb.dataset.visibleDoc, { method: 'PUT', body: { visible_to_client: cb.checked } });
            toast(cb.checked ? 'Shared with the client portal' : 'No longer shared'); }
          catch (e) { toast(e.message, true); cb.checked = !cb.checked; }
        }));
      }).catch(e => { document.getElementById('doc-body').innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }
    load();

    document.getElementById('doc-upload').addEventListener('click', () => {
      const clientId = document.getElementById('doc-client').value;
      const category = document.getElementById('doc-cat').value;
      const fileInput = document.getElementById('doc-file');
      const file = fileInput.files[0];
      if (!clientId) { toast('Choose a client first', true); return; }
      if (!file) { toast('Choose a file first', true); return; }
      const reader = new FileReader();
      reader.onload = async () => {
        const base64 = reader.result.split(',')[1];
        try {
          await api('/api/documents', { method: 'POST', body: {
            client_id: Number(clientId), category, filename: file.name, mime: file.type, content_base64: base64 } });
          toast('Uploaded ' + file.name);
          fileInput.value = '';
          load();
        } catch (e) { toast(e.message, true); }
      };
      reader.readAsDataURL(file);
    });
  }

  // ───────────────────────────────────────────────────── transfer pricing
  // Deliberately not a MODULES[] entry — its data is relational (real
  // server tables), not one JSON workfile blob, so every edit here is a
  // live PUT/POST against /api/tp/... rather than the getFile/seed/compute
  // pattern every other calculator uses. Same posture this app already
  // accepts for Admin, Reports and Client Directory: online-first.
  const TP_TABS = [
    ['entities', 'Entities & relationships'], ['dashboard', 'Dashboard'], ['transactions', 'Transactions'],
    ['questionnaire', 'Questionnaire'], ['far', 'FAR'],
    ['method', 'Method & benchmarking'], ['risk', 'Risk'], ['matrix', 'Cross-border matrix'],
    ['compare', 'Country comparison'], ['docs', 'Documentation'], ['wp', 'Working papers'], ['reports', 'Reports'],
    ['examples', 'Examples & methods']
  ];
  const TP_CATEGORIES = ['goods', 'services', 'financing', 'royalty', 'guarantee', 'cost_allocation', 'lease', 'restructuring', 'other'];
  const TP_ENTITY_TYPES = ['distributor', 'manufacturer', 'service_provider', 'ip_owner', 'financing_entity', 'holding', 'other'];
  const TP_RELATIONSHIP_TYPES = ['parent', 'subsidiary', 'sister_company', 'joint_venture', 'branch', 'pe', 'common_control', 'other'];
  const TP_DIRECTIONS = ['import', 'export', 'payment', 'receipt'];
  const TP_PLI_OPTIONS = ['operating_margin', 'berry_ratio', 'return_on_assets', 'return_on_capital_employed', 'full_cost_markup', 'net_cost_plus', 'other'];
  // Any fixed, known set of values gets a <select>, never a free-text
  // <input> — a preparer typing "Subsidiary " with a trailing space or
  // "sister company" instead of "sister_company" would silently fail to
  // match anything downstream (matrix grouping, report filters). Free text
  // stays free text only where the value is genuinely open-ended (names,
  // narrative conclusions, working-paper references).
  function tpEnumOptions(list, selected, allowBlank) {
    const label = v => v.replace(/_/g, ' ').replace(/^./, c => c.toUpperCase());
    return (allowBlank ? '<option value="">— choose —</option>' : '') +
      list.map(v => '<option value="' + esc(v) + '"' + (v === selected ? ' selected' : '') + '>' + esc(label(v)) + '</option>').join('');
  }
  const TP_METHODS = [['cup', 'CUP'], ['rpm', 'Resale Price Method'], ['cpm', 'Cost Plus Method'],
                       ['tnmm', 'TNMM'], ['psm', 'Profit Split Method'], ['other', 'Other']];
  const TP_PROFILES = ['routine_distributor', 'limited_risk_distributor', 'full_risk_distributor', 'contract_manufacturer',
                        'full_risk_manufacturer', 'service_provider', 'limited_risk_service_provider',
                        'financing_entity', 'ip_owner', 'ip_licensee', 'entrepreneurial'];
  const TP_RISK_FLAGS = [['loss_making', 'Loss-making'], ['persistent_losses', 'Persistent losses'],
                          ['related_party_pct_high', 'High related-party concentration'], ['cross_border', 'Cross-border'],
                          ['sensitive_category', 'Sensitive category (mgmt fee / royalty / guarantee / interest-free)'],
                          ['documentation_gap', 'Documentation gap']];

  const tpState = {
    clientId: '', fy: '', tab: 'entities', countries: null,
    entities: [], relationships: [], transactions: [], selectedTxnId: null,
    far: {}, method: {}, comparables: [], benchmark: null, risk: null, adjustment: null
  };

  function tpNoTxnMsg() {
    return '<div class="empty"><b>No transaction selected</b>Go to the Transactions tab, then add or select a transaction first.</div>';
  }
  function tpClientCountry() {
    const mine = tpState.entities.find(e => e.is_the_client);
    return mine ? mine.country : '';
  }
  async function tpLoadCountries() {
    if (!tpState.countries) tpState.countries = await api('/api/tp/countries');
    return tpState.countries;
  }
  // Related parties can sit anywhere in the world, so every country picker
  // offers the full list — but only Nepal/India/UAE have configured
  // tp_regulatory_rule content (the tax_authority column is how this app
  // marks that), so those three are grouped first and labelled, keeping the
  // ~200-option list usable without a second table just for "is this core".
  function tpCoreCountries() { return (tpState.countries || []).filter(c => c.tax_authority); }
  function tpCountryOptions(selectedCode, labelMode) {
    const all = tpState.countries || [];
    const core = all.filter(c => c.tax_authority);
    const rest = all.filter(c => !c.tax_authority);
    const opt = c => '<option value="' + esc(c.code) + '"' + (c.code === selectedCode ? ' selected' : '') + '>' +
      esc(labelMode === 'code' ? c.code + ' — ' + c.name : c.name) + '</option>';
    return (core.length ? '<optgroup label="Configured (Nepal / India / UAE)">' + core.map(opt).join('') + '</optgroup>' : '') +
      (rest.length ? '<optgroup label="Other countries">' + rest.map(opt).join('') + '</optgroup>' : '');
  }
  const TP_MAJOR_CURRENCIES = ['NPR', 'INR', 'AED', 'USD', 'EUR', 'GBP'];
  // A currency picked by typing is one keystroke away from a silently wrong
  // FX conversion or a report that mislabels the amount — every currency
  // field in this module is a <select> built from the country list's own
  // currency column (already loaded, always in sync with tp_country), never
  // free text, so a typo simply can't happen.
  function tpCurrencyOptions(selectedCode) {
    const all = tpState.countries || [];
    const codes = Array.from(new Set(all.map(c => c.currency).filter(Boolean))).sort();
    const major = codes.filter(c => TP_MAJOR_CURRENCIES.indexOf(c) !== -1)
      .sort((a, b) => TP_MAJOR_CURRENCIES.indexOf(a) - TP_MAJOR_CURRENCIES.indexOf(b));
    const rest = codes.filter(c => TP_MAJOR_CURRENCIES.indexOf(c) === -1);
    const opt = c => '<option value="' + esc(c) + '"' + (c === selectedCode ? ' selected' : '') + '>' + esc(c) + '</option>';
    return (major.length ? '<optgroup label="Common">' + major.map(opt).join('') + '</optgroup>' : '') +
      (rest.length ? '<optgroup label="Other">' + rest.map(opt).join('') + '</optgroup>' : '');
  }
  async function tpLoadCore() {
    const [entities, relationships, transactions] = await Promise.all([
      api('/api/tp/clients/' + tpState.clientId + '/entities'),
      api('/api/tp/clients/' + tpState.clientId + '/relationships'),
      api('/api/tp/clients/' + tpState.clientId + '/transactions' + (tpState.fy ? '?fy=' + encodeURIComponent(tpState.fy) : ''))
    ]);
    tpState.entities = entities; tpState.relationships = relationships; tpState.transactions = transactions;
    if (tpState.selectedTxnId && !transactions.find(t => t.id === tpState.selectedTxnId)) tpState.selectedTxnId = null;
    if (!tpState.selectedTxnId && transactions.length) tpState.selectedTxnId = transactions[0].id;
  }
  async function tpLoadTxnDetail() {
    if (!tpState.selectedTxnId) {
      tpState.far = {}; tpState.method = {}; tpState.comparables = []; tpState.benchmark = null; tpState.risk = null; tpState.adjustment = null;
      return;
    }
    const tid = tpState.selectedTxnId;
    const [far, method, comparables, benchmark, risk, adjustment] = await Promise.all([
      api('/api/tp/transactions/' + tid + '/far'), api('/api/tp/transactions/' + tid + '/method'),
      api('/api/tp/transactions/' + tid + '/comparables'), api('/api/tp/transactions/' + tid + '/benchmark'),
      api('/api/tp/transactions/' + tid + '/risk'), api('/api/tp/transactions/' + tid + '/adjustment')
    ]);
    tpState.far = far || {}; tpState.method = method || {}; tpState.comparables = comparables || [];
    tpState.benchmark = (benchmark && benchmark.id) ? benchmark : null;
    tpState.risk = (risk && risk.id) ? risk : null;
    tpState.adjustment = (adjustment && adjustment.id) ? adjustment : null;
  }

  function renderTP() {
    const view = document.getElementById('view');
    if (!state.online || !state.token || state.offlineMode) {
      view.innerHTML = '<div class="page-head"><div><h2>Transfer Pricing</h2></div></div>' +
        '<div class="card"><div class="empty"><b>Offline</b>Transfer Pricing works against live server data — reconnect to use this module.</div></div>';
      return;
    }
    if (!tpState.fy) tpState.fy = state.fy;
    if (!tpState.clientId) tpState.clientId = localStorage.getItem('ca.client.tp') || (clients()[0] && clients()[0].id) || '';

    view.innerHTML =
      '<div class="page-head"><div><h2>Transfer Pricing</h2>' +
      '<p>Multi-jurisdiction (Nepal / India / UAE) related-party advisory and compliance — related-party identification, FAR analysis, method selection, benchmarking, risk scoring and adjustment exposure, all driven by the client\'s selected country.</p></div></div>' +
      '<div class="card"><div class="grid c3">' +
      '<div class="field"><label>Client</label><select id="tp-client" title="Which client this transfer pricing engagement belongs to"><option value="">— choose —</option>' +
      clients().map(c => '<option value="' + c.id + '"' + (String(tpState.clientId) === String(c.id) ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') + '</select></div>' +
      '<div class="field"><label>Financial year</label><select id="tp-fy" title="Transactions and the documentation checklist are scoped to this year">' +
      (((state.boot && state.boot.financial_periods) || [{ label_bs: state.fy }]).map(f =>
        '<option value="' + f.label_bs + '"' + (f.label_bs === tpState.fy ? ' selected' : '') + '>' + f.label_bs + '</option>').join('')) + '</select></div>' +
      '<div class="field"><label>Jurisdiction</label><div id="tp-country-badge" style="padding-top:8px"></div></div>' +
      '</div></div>' +
      '<div class="card" id="tp-body"><div class="empty">Loading…</div></div>';

    document.getElementById('tp-client').addEventListener('change', e => {
      tpState.clientId = e.target.value; localStorage.setItem('ca.client.tp', tpState.clientId);
      tpState.selectedTxnId = null; tpState.tab = 'entities';
      tpBoot();
    });
    document.getElementById('tp-fy').addEventListener('change', e => { tpState.fy = e.target.value; tpBoot(); });
    tpBoot();
  }

  async function tpBoot() {
    const badge = document.getElementById('tp-country-badge');
    const body = document.getElementById('tp-body');
    if (!tpState.clientId) {
      if (badge) badge.innerHTML = '<span class="tag">No client selected</span>';
      body.innerHTML = '<div class="empty"><b>Choose a client</b>Pick a client above to begin a transfer pricing engagement.</div>';
      return;
    }
    try { await tpLoadCountries(); await tpLoadCore(); await tpLoadTxnDetail(); }
    catch (e) { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; return; }

    const country = tpClientCountry();
    const countryRow = (tpState.countries || []).find(c => c.code === country);
    if (badge) badge.innerHTML = !country
      ? '<span class="tag tag-amber" title="Set it on the Entities tab, step 1">Not set yet</span>'
      : countryRow && countryRow.tax_authority
        ? '<span class="tag tag-green">' + esc(countryRow.name) + '</span>'
        : '<span class="tag tag-amber" title="Rules, checklist and method-recommendation basis are only configured for Nepal, India and UAE — add tp_regulatory_rule content for this country to unlock those for this client">' +
          esc(countryRow ? countryRow.name : country) + ' — not yet configured</span>';

    tpRenderBody();
  }

  function tpRenderBody() {
    const body = document.getElementById('tp-body');
    body.innerHTML = '<div style="margin-bottom:14px">' +
      TP_TABS.map(t => '<button class="btn btn-small' + (tpState.tab === t[0] ? ' btn-primary' : ' btn-ghost') +
        '" data-tp-tab="' + t[0] + '" style="margin:0 6px 6px 0">' + esc(t[1]) + '</button>').join('') +
      '</div><div id="tp-tab-body"></div>';
    body.querySelectorAll('[data-tp-tab]').forEach(b => b.addEventListener('click', e => {
      tpState.tab = e.target.dataset.tpTab; tpRenderBody();
    }));
    const tb = document.getElementById('tp-tab-body');
    ({ entities: tpRenderEntitiesTab, dashboard: tpRenderDashboardTab, transactions: tpRenderTransactionsTab,
       questionnaire: tpRenderQuestionnaireTab, far: tpRenderFarTab,
       method: tpRenderMethodTab, risk: tpRenderRiskTab, matrix: tpRenderMatrixTab, compare: tpRenderCompareTab,
       docs: tpRenderDocsTab, wp: tpRenderWpTab, reports: tpRenderReportsTab,
       examples: tpRenderExamplesTab }[tpState.tab] || tpRenderEntitiesTab)(tb);
  }

  function tpRenderEntitiesTab(root) {
    const mine = tpState.entities.find(e => e.is_the_client);
    let h = '';
    if (!mine) {
      h += '<div class="card"><h3>Step 1 — set this engagement\'s jurisdiction</h3>' +
        '<p class="card-note">Every downstream tab (applicable rules, documentation checklist, method-recommendation basis, report title page) depends on this being set first.</p>' +
        '<div class="grid c3">' +
        '<div class="field"><label>Entity name</label><input id="tp-new-self-name" value="' + esc(clientNameOf(tpState.clientId)) + '"></div>' +
        '<div class="field"><label>Country</label><select id="tp-new-self-country" title="United Arab Emirates covers Dubai and every other emirate — there is no separate Dubai jurisdiction"><option value="">— choose —</option>' +
        tpCountryOptions('') + '</select></div>' +
        '<div class="field"><label>Entity type</label><select id="tp-new-self-type">' + tpEnumOptions(TP_ENTITY_TYPES, '', true) + '</select></div>' +
        '</div><button class="btn btn-primary" id="tp-new-self-save" style="margin-top:8px">Save and continue</button></div>';
    } else {
      h += '<div class="card"><h3>This engagement\'s entity</h3><div class="grid c3">' +
        '<div class="field"><label>Name</label><input data-tp-self="name" value="' + esc(mine.name) + '"></div>' +
        '<div class="field"><label>Country</label><select data-tp-self="country" title="Changing this changes which jurisdiction\'s rules apply below">' +
        tpCountryOptions(mine.country) + '</select></div>' +
        '<div class="field"><label>PAN / TIN</label><input data-tp-self="pan_or_tin" value="' + esc(mine.pan_or_tin || '') + '"></div>' +
        '</div></div>';
    }

    const others = tpState.entities.filter(e => !e.is_the_client);
    h += '<div class="card"><h3>Related / counterparty entities</h3>' +
      '<p class="card-note">Group companies, subsidiaries and other counterparties this client transacts with — most of these are not themselves firm clients.</p>' +
      '<div class="table-wrap"><table><thead><tr><th>Name</th><th>Country</th><th>Type</th><th>PAN / TIN</th><th>Notes</th><th></th></tr></thead><tbody>' +
      others.map(e => '<tr>' +
        '<td><input data-tp-ent="' + e.id + '" data-k="name" value="' + esc(e.name) + '"></td>' +
        '<td><select data-tp-ent="' + e.id + '" data-k="country"><option value="">—</option>' +
        tpCountryOptions(e.country, 'code') + '</select></td>' +
        '<td><select data-tp-ent="' + e.id + '" data-k="entity_type">' + tpEnumOptions(TP_ENTITY_TYPES, e.entity_type, true) + '</select></td>' +
        '<td><input data-tp-ent="' + e.id + '" data-k="pan_or_tin" value="' + esc(e.pan_or_tin || '') + '"></td>' +
        '<td><input data-tp-ent="' + e.id + '" data-k="notes" value="' + esc(e.notes || '') + '"></td>' +
        '<td><button class="btn btn-small btn-danger" data-tp-del-ent="' + e.id + '" title="Remove this entity">Remove</button></td></tr>').join('') +
      '</tbody></table></div><button class="btn btn-small" id="tp-add-ent" title="Add a counterparty entity">Add entity</button></div>';

    const relOpts = tpState.entities;
    h += '<div class="card"><h3>Relationships</h3>' +
      '<div class="table-wrap"><table><thead><tr><th>Entity A</th><th>Entity B</th><th>Relationship</th><th>Ownership %</th><th>Voting %</th><th>Tested under</th><th></th></tr></thead><tbody>' +
      tpState.relationships.map(r => '<tr>' +
        '<td><select data-tp-rel="' + r.id + '" data-k="entity_a_id">' + relOpts.map(e => '<option value="' + e.id + '"' + (e.id === r.entity_a_id ? ' selected' : '') + '>' + esc(e.name) + '</option>').join('') + '</select></td>' +
        '<td><select data-tp-rel="' + r.id + '" data-k="entity_b_id">' + relOpts.map(e => '<option value="' + e.id + '"' + (e.id === r.entity_b_id ? ' selected' : '') + '>' + esc(e.name) + '</option>').join('') + '</select></td>' +
        '<td><select data-tp-rel="' + r.id + '" data-k="relationship_type">' + tpEnumOptions(TP_RELATIONSHIP_TYPES, r.relationship_type, true) + '</select></td>' +
        '<td class="num"><input type="number" step="any" data-tp-rel="' + r.id + '" data-k="ownership_pct" value="' + esc(r.ownership_pct == null ? '' : r.ownership_pct) + '"></td>' +
        '<td class="num"><input type="number" step="any" data-tp-rel="' + r.id + '" data-k="voting_rights_pct" value="' + esc(r.voting_rights_pct == null ? '' : r.voting_rights_pct) + '"></td>' +
        '<td><select data-tp-rel="' + r.id + '" data-k="country_test_basis" title="Which country\'s related-party definition this was evaluated under"><option value="">—</option>' +
        tpCountryOptions(r.country_test_basis, 'code') + '</select></td>' +
        '<td><button class="btn btn-small btn-danger" data-tp-del-rel="' + r.id + '" title="Remove this relationship">Remove</button></td></tr>').join('') +
      '</tbody></table></div><button class="btn btn-small" id="tp-add-rel"' + (relOpts.length < 2 ? ' disabled title="Add at least two entities first"' : ' title="Add a relationship"') + '>Add relationship</button></div>';

    root.innerHTML = h;

    if (!mine) {
      root.querySelector('#tp-new-self-save').addEventListener('click', async () => {
        const name = root.querySelector('#tp-new-self-name').value.trim();
        const country = root.querySelector('#tp-new-self-country').value;
        const entity_type = root.querySelector('#tp-new-self-type').value.trim();
        if (!name || !country) { toast('Name and country are required', true); return; }
        try {
          await api('/api/tp/clients/' + tpState.clientId + '/entities', { method: 'POST', body: { name, country, entity_type, is_the_client: true } });
          toast('Jurisdiction set'); await tpBoot();
        } catch (e) { toast(e.message, true); }
      });
    } else {
      root.querySelectorAll('[data-tp-self]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/tp/entities/' + mine.id, { method: 'PUT', body: { [e.target.dataset.tpSelf]: e.target.value } }); toast('Saved'); await tpBoot(); }
        catch (err) { toast(err.message, true); }
      }));
    }
    root.querySelectorAll('[data-tp-ent]').forEach(el => el.addEventListener('change', async e => {
      try { await api('/api/tp/entities/' + e.target.dataset.tpEnt, { method: 'PUT', body: { [e.target.dataset.k]: e.target.value } }); toast('Saved'); }
      catch (err) { toast(err.message, true); }
    }));
    root.querySelectorAll('[data-tp-del-ent]').forEach(el => el.addEventListener('click', async e => {
      if (!confirm('Remove this entity?')) return;
      try { await api('/api/tp/entities/' + e.target.dataset.tpDelEnt, { method: 'DELETE' }); toast('Removed'); await tpBoot(); }
      catch (err) { toast(err.message, true); }
    }));
    const addEntBtn = root.querySelector('#tp-add-ent');
    if (addEntBtn) addEntBtn.addEventListener('click', async () => {
      try { await api('/api/tp/clients/' + tpState.clientId + '/entities', { method: 'POST', body: { name: 'New entity' } }); await tpBoot(); }
      catch (e) { toast(e.message, true); }
    });
    root.querySelectorAll('[data-tp-rel]').forEach(el => el.addEventListener('change', async e => {
      const k = e.target.dataset.k; let v = e.target.value;
      if (k === 'entity_a_id' || k === 'entity_b_id') v = Number(v);
      if (k === 'ownership_pct' || k === 'voting_rights_pct') v = v === '' ? null : Number(v);
      try { await api('/api/tp/relationships/' + e.target.dataset.tpRel, { method: 'PUT', body: { [k]: v } }); toast('Saved'); }
      catch (err) { toast(err.message, true); }
    }));
    root.querySelectorAll('[data-tp-del-rel]').forEach(el => el.addEventListener('click', async e => {
      if (!confirm('Remove this relationship?')) return;
      try { await api('/api/tp/relationships/' + e.target.dataset.tpDelRel, { method: 'DELETE' }); toast('Removed'); await tpBoot(); }
      catch (err) { toast(err.message, true); }
    }));
    const addRelBtn = root.querySelector('#tp-add-rel');
    if (addRelBtn && !addRelBtn.disabled) addRelBtn.addEventListener('click', async () => {
      const [a, b] = relOpts;
      if (!a || !b) return;
      try { await api('/api/tp/clients/' + tpState.clientId + '/relationships', { method: 'POST', body: { entity_a_id: a.id, entity_b_id: b.id } }); await tpBoot(); }
      catch (e) { toast(e.message, true); }
    });
  }

  const TP_STATUS_LABEL = { jurisdiction_not_set: 'Jurisdiction not set', documentation_gaps: 'Documentation gaps',
                             attention_needed: 'Attention needed (high/very-high risk)', on_track: 'On track' };
  const TP_STATUS_TAG = { jurisdiction_not_set: 'tag', documentation_gaps: 'tag-amber', attention_needed: 'tag-red', on_track: 'tag-green' };

  function tpDashboardCards(d) {
    return '<div class="grid c3">' +
      '<div class="card"><h3>Related parties</h3><p style="font-size:26px;margin:0">' + d.related_parties + '</p></div>' +
      '<div class="card"><h3>Transactions — FY ' + esc(d.financial_year_label || 'all years') + '</h3><p style="font-size:26px;margin:0">' + d.transactions + '</p>' +
      '<p class="card-note">' + d.cross_border_transactions + ' cross-border</p></div>' +
      '<div class="card"><h3>Compliance status</h3><p><span class="tag ' + (TP_STATUS_TAG[d.compliance_status] || 'tag') + '">' +
      esc(TP_STATUS_LABEL[d.compliance_status] || d.compliance_status) + '</span></p></div>' +
      '<div class="card"><h3>Risk breakdown</h3><div class="table-wrap"><table><thead><tr><th>Low</th><th>Medium</th><th>High</th><th>Very High</th><th>Not assessed</th></tr></thead><tbody><tr>' +
      ['Low', 'Medium', 'High', 'Very High', 'Not assessed'].map(k => '<td>' + (d.risk_breakdown[k] || 0) + '</td>').join('') + '</tr></tbody></table></div></div>' +
      '<div class="card"><h3>Method &amp; benchmarking</h3><p class="card-note">' + d.method_selected_count + ' of ' + d.transactions + ' transactions have a method selected.</p>' +
      '<p class="card-note">' + d.benchmarked_count + ' of ' + d.transactions + ' transactions have a computed benchmark.</p></div>' +
      '<div class="card"><h3>Documentation</h3><p class="card-note">' + d.documentation.complete + ' complete, ' + d.documentation.in_progress +
      ' in progress, ' + d.documentation.missing + ' missing (of ' + d.documentation.total + ').</p></div>' +
      '<div class="card"><h3>Total adjustment exposure' + (d.exposure_currency ? ' (' + esc(d.exposure_currency) + ')' : '') + '</h3><p style="font-size:22px;margin:0">' + money(d.total_adjustment_exposure) + '</p>' +
      '<p class="card-note">Sum of the latest computed exposure across this client\'s transactions — only as complete as the underlying benchmarks and rates.' +
      ((d.exposure_unconverted_currencies || []).length ? ' Could not convert: ' + d.exposure_unconverted_currencies.join(', ') + ' — log an FX rate below to include them.' : '') + '</p></div>' +
      '</div>';
  }

  function tpFxRatesCard(rates) {
    return '<div class="card"><h3>Exchange rates (Multi-Currency Engine)</h3>' +
      '<p class="card-note">Manually logged rate snapshots, not a live feed — this app has no outbound internet access, so a rate is only as current as whoever logs it. Used to convert exposure figures across currencies on this dashboard.</p>' +
      (rates.length ? '<div class="table-wrap"><table><thead><tr><th>From</th><th>To</th><th class="num">Rate</th><th>Date</th><th>Source</th><th>Logged by</th><th></th></tr></thead><tbody>' +
        rates.map(r => '<tr><td>' + esc(r.from_currency) + '</td><td>' + esc(r.to_currency) + '</td><td class="num">' + r.rate + '</td>' +
          '<td>' + esc(r.rate_date) + '</td><td>' + esc(r.source || '') + '</td><td>' + esc(r.created_by_name || '') + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-tp-del-fx="' + r.id + '" title="Remove this rate">Remove</button></td></tr>').join('') + '</tbody></table></div>'
        : '<div class="empty">No exchange rates logged yet.</div>') +
      '<div class="grid c3" style="margin-top:10px">' +
      '<div class="field"><label>From currency</label><select id="tp-fx-from"><option value="">— choose —</option>' + tpCurrencyOptions('') + '</select></div>' +
      '<div class="field"><label>To currency</label><select id="tp-fx-to"><option value="">— choose —</option>' + tpCurrencyOptions('') + '</select></div>' +
      '<div class="field"><label>Rate (1 From = ? To)</label><input type="number" step="any" id="tp-fx-rate"></div>' +
      '<div class="field"><label>Date</label><input type="date" id="tp-fx-date"></div>' +
      '<div class="field"><label>Source</label><input id="tp-fx-source" placeholder="e.g. Nepal Rastra Bank"></div>' +
      '</div><button class="btn btn-small" id="tp-fx-add" style="margin-top:8px">Log rate</button></div>';
  }

  function tpWireFxCard(root, onChange) {
    root.querySelectorAll('[data-tp-del-fx]').forEach(b => b.addEventListener('click', async e => {
      try { await api('/api/tp/fx-rates/' + e.target.dataset.tpDelFx, { method: 'DELETE' }); toast('Removed'); onChange(); }
      catch (err) { toast(err.message, true); }
    }));
    const addBtn = root.querySelector('#tp-fx-add');
    if (addBtn) addBtn.addEventListener('click', async () => {
      const from = root.querySelector('#tp-fx-from').value.trim().toUpperCase();
      const to = root.querySelector('#tp-fx-to').value.trim().toUpperCase();
      const rate = root.querySelector('#tp-fx-rate').value;
      const date = root.querySelector('#tp-fx-date').value;
      const source = root.querySelector('#tp-fx-source').value.trim();
      if (!from || !to || rate === '' || !date) { toast('From, to, rate and date are all required', true); return; }
      try {
        await api('/api/tp/fx-rates', { method: 'POST', body: { from_currency: from, to_currency: to, rate: Number(rate), rate_date: date, source } });
        toast('Rate logged'); onChange();
      } catch (err) { toast(err.message, true); }
    });
  }

  function tpRenderDashboardTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([
      api('/api/tp/clients/' + tpState.clientId + '/dashboard?fy=' + encodeURIComponent(tpState.fy)),
      api('/api/tp/fx-rates')
    ]).then(([d, rates]) => {
      root.innerHTML = '<h3 style="margin:0 0 10px">This client — Country Dashboard</h3>' + tpDashboardCards(d) +
        tpFxRatesCard(rates) +
        '<div style="margin:16px 0"><button class="btn btn-small" id="tp-show-group" title="Roll this up across every client you can see">Show group dashboard (all clients)</button></div>' +
        '<div id="tp-group-body"></div>';
      tpWireFxCard(root, () => tpRenderDashboardTab(root));
      root.querySelector('#tp-show-group').addEventListener('click', async () => {
        const gb = root.querySelector('#tp-group-body');
        gb.innerHTML = '<div class="field" style="max-width:220px"><label>Reporting currency (optional)</label>' +
          '<select id="tp-group-currency"><option value="">— none (each client\'s own currency) —</option>' + tpCurrencyOptions('') + '</select></div>' +
          '<button class="btn btn-small" id="tp-group-load" style="margin:6px 0 10px">Load</button><div id="tp-group-table"></div>';
        const load = async () => {
          const rc = root.querySelector('#tp-group-currency').value.trim().toUpperCase();
          const t = root.querySelector('#tp-group-table');
          t.innerHTML = '<div class="empty">Loading…</div>';
          try {
            const g = await api('/api/tp/dashboard?fy=' + encodeURIComponent(tpState.fy) + (rc ? '&reporting_currency=' + rc : ''));
            t.innerHTML = '<h3 style="margin:6px 0 10px">Global Group Dashboard — FY ' + esc(tpState.fy) + '</h3>' +
              (g.clients.length ? '<div class="table-wrap"><table><thead><tr><th>Client</th><th>Jurisdiction</th><th class="num">Related parties</th>' +
                '<th class="num">Transactions</th><th class="num">Cross-border</th><th class="num">High/Very High risk</th><th>Status</th><th class="num">Total exposure</th></tr></thead><tbody>' +
                g.clients.map(c => '<tr><td>' + esc(c.client_name) + '</td><td>' + esc(c.country || '—') + '</td>' +
                  '<td class="num">' + c.related_parties + '</td><td class="num">' + c.transactions + '</td><td class="num">' + c.cross_border_transactions + '</td>' +
                  '<td class="num">' + (c.risk_breakdown.High + c.risk_breakdown['Very High']) + '</td>' +
                  '<td><span class="tag ' + (TP_STATUS_TAG[c.compliance_status] || 'tag') + '">' + esc(TP_STATUS_LABEL[c.compliance_status] || c.compliance_status) + '</span></td>' +
                  '<td class="num">' + money(c.total_adjustment_exposure) + (c.exposure_currency ? ' ' + esc(c.exposure_currency) : '') + '</td></tr>').join('') + '</tbody></table></div>' +
                '<p class="card-note" style="margin-top:8px">Totals: ' + g.totals.related_parties + ' related parties, ' + g.totals.transactions +
                ' transactions, ' + g.totals.cross_border_transactions + ' cross-border, ' + g.totals.high_risk_transactions +
                ' high/very-high risk, ' + money(g.totals.total_adjustment_exposure) + ' total exposure.' +
                (g.totals.note ? ' <span style="color:var(--crimson)">' + esc(g.totals.note) + '</span>' : '') + '</p>'
                : '<div class="empty"><b>No other clients have a transfer pricing engagement started yet</b></div>');
          } catch (e) { t.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; }
        };
        root.querySelector('#tp-group-load').addEventListener('click', load);
        load();
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function tpRenderTransactionsTab(root) {
    const ents = tpState.entities;
    let h = '<div class="card"><h3>Transaction register — FY ' + esc(tpState.fy) + '</h3>' +
      '<div class="table-wrap"><table><thead><tr><th>Tested entity</th><th>Counterparty</th><th>Source</th><th>Dest.</th>' +
      '<th>Category</th><th>Currency</th><th class="num">Amount</th><th>Direction</th><th></th></tr></thead><tbody>' +
      tpState.transactions.map(t => '<tr style="cursor:pointer' + (t.id === tpState.selectedTxnId ? ';background:var(--teal-10)' : '') +
        '" data-tp-select-txn="' + t.id + '" title="Click to work on this transaction in the tabs above">' +
        '<td>' + esc(t.tested_entity_name || '—') + '</td><td>' + esc(t.counterparty_entity_name || '—') + '</td>' +
        '<td>' + esc(t.source_country || '') + '</td><td>' + esc(t.destination_country || '') + '</td>' +
        '<td>' + esc(t.transaction_category) + '</td><td>' + esc(t.currency || '') + '</td>' +
        '<td class="num">' + money(t.amount) + '</td><td>' + esc(t.direction || '') + '</td>' +
        '<td><button class="btn btn-small btn-danger" data-tp-del-txn="' + t.id + '" title="Remove this transaction">Remove</button></td></tr>').join('') +
      '</tbody></table></div><button class="btn btn-small" id="tp-add-txn" title="Add a transaction to the register">Add transaction</button>' +
      (tpState.transactions.length ? '<p class="card-note">Click a row to work on it in the FAR, Method &amp; benchmarking, Risk and Documentation tabs.</p>' : '') +
      '</div>';

    const t = tpState.transactions.find(x => x.id === tpState.selectedTxnId);
    if (t) {
      h += '<div class="card"><h3>Edit selected transaction</h3><div class="grid c3">' +
        '<div class="field"><label>Tested entity</label><select data-tp-txn-f="tested_entity_id"><option value="">—</option>' +
        ents.map(e => '<option value="' + e.id + '"' + (e.id === t.tested_entity_id ? ' selected' : '') + '>' + esc(e.name) + '</option>').join('') + '</select></div>' +
        '<div class="field"><label>Counterparty</label><select data-tp-txn-f="counterparty_entity_id"><option value="">—</option>' +
        ents.map(e => '<option value="' + e.id + '"' + (e.id === t.counterparty_entity_id ? ' selected' : '') + '>' + esc(e.name) + '</option>').join('') + '</select></div>' +
        '<div class="field"><label>Category</label><select data-tp-txn-f="transaction_category">' +
        TP_CATEGORIES.map(c => '<option value="' + c + '"' + (c === t.transaction_category ? ' selected' : '') + '>' + c.replace(/_/g, ' ') + '</option>').join('') + '</select></div>' +
        '<div class="field"><label>Source country</label><select data-tp-txn-f="source_country"><option value="">—</option>' +
        tpCountryOptions(t.source_country, 'code') + '</select></div>' +
        '<div class="field"><label>Destination country</label><select data-tp-txn-f="destination_country"><option value="">—</option>' +
        tpCountryOptions(t.destination_country, 'code') + '</select></div>' +
        '<div class="field"><label>Currency</label><select data-tp-txn-f="currency"><option value="">— choose —</option>' + tpCurrencyOptions(t.currency) + '</select></div>' +
        '<div class="field"><label>Amount</label><input type="number" step="any" data-tp-txn-f="amount" value="' + esc(t.amount == null ? '' : t.amount) + '"></div>' +
        '<div class="field"><label>Direction</label><select data-tp-txn-f="direction">' + tpEnumOptions(TP_DIRECTIONS, t.direction, true) + '</select></div>' +
        '<div class="field"><label>Description</label><input data-tp-txn-f="description" value="' + esc(t.description || '') + '"></div>' +
        '</div></div>';
    }
    root.innerHTML = h;

    root.querySelectorAll('[data-tp-select-txn]').forEach(el => el.addEventListener('click', () => {
      tpState.selectedTxnId = Number(el.dataset.tpSelectTxn);
      tpLoadTxnDetail().then(tpRenderBody);
    }));
    root.querySelectorAll('[data-tp-del-txn]').forEach(el => el.addEventListener('click', async e => {
      e.stopPropagation();
      if (!confirm('Remove this transaction and all its FAR / method / benchmark / risk data?')) return;
      try {
        await api('/api/tp/transactions/' + e.target.dataset.tpDelTxn, { method: 'DELETE' });
        toast('Removed'); tpState.selectedTxnId = null; await tpBoot();
      } catch (err) { toast(err.message, true); }
    }));
    const addBtn = root.querySelector('#tp-add-txn');
    if (addBtn) addBtn.addEventListener('click', async () => {
      try {
        const created = await api('/api/tp/clients/' + tpState.clientId + '/transactions', { method: 'POST',
          body: { financial_year_label: tpState.fy, transaction_category: 'goods' } });
        tpState.selectedTxnId = created.id; await tpBoot();
      } catch (e) { toast(e.message, true); }
    });
    root.querySelectorAll('[data-tp-txn-f]').forEach(el => el.addEventListener('change', async e => {
      const k = e.target.dataset.tpTxnF; let v = e.target.value;
      if (k === 'tested_entity_id' || k === 'counterparty_entity_id') v = v === '' ? null : Number(v);
      if (k === 'amount') v = v === '' ? null : Number(v);
      try { await api('/api/tp/transactions/' + tpState.selectedTxnId, { method: 'PUT', body: { [k]: v } }); toast('Saved'); await tpBoot(); }
      catch (err) { toast(err.message, true); }
    }));
  }

  // The Client Questionnaire Engine — different facts matter for a loan than
  // for a royalty than for a management-fee arrangement, so the question set
  // is keyed by transaction_category rather than one generic form. Answers
  // are facts the preparer records, not legal content, so no UNVERIFIED
  // marking applies here — they're saved straight into the transaction's own
  // detail_json (the field this schema already reserves for category-specific
  // structured facts) via the same PUT route the Transactions tab uses.
  const TP_QUESTIONNAIRE = {
    goods: [
      ['product_description', 'Product description', 'text'],
      ['incoterms', 'Incoterms (e.g. FOB, CIF, EXW)', 'text'],
      ['finished_or_raw', 'Finished good or raw material / component?', 'sel', ['Finished good', 'Raw material', 'Component', 'Other']],
      ['who_bears_inventory_risk', 'Which party bears inventory / obsolescence risk?', 'text'],
      ['comparability_adjustments', 'Any known product/spec differences from the comparables used?', 'text']
    ],
    services: [
      ['service_description', 'Nature of the service', 'text'],
      ['shareholder_activity', 'Could this be a non-chargeable shareholder/stewardship activity rather than a genuine service?', 'sel', ['No — genuine service', 'Possibly — needs review', 'Yes — likely non-chargeable']],
      ['benefit_test_performed', 'Has a benefit test been performed and documented?', 'sel', ['Yes', 'No', 'In progress']],
      ['duplication_check', 'Does the recipient already perform this function in-house (duplication)?', 'sel', ['No duplication identified', 'Possible duplication — needs review']],
      ['allocation_basis', 'Cost allocation / charge-out basis used', 'text']
    ],
    financing: [
      ['principal_currency', 'Loan/deposit currency', 'text'],
      ['interest_rate', 'Stated interest rate (or "interest-free")', 'text'],
      ['tenure', 'Tenure / repayment terms', 'text'],
      ['security', 'Security or collateral provided?', 'text'],
      ['credit_rating_basis', 'Basis used for the borrower\'s credit rating (standalone vs. implicit group support)', 'text'],
      ['would_get_loan_standalone', 'Would the borrower obtain this loan on these terms from an unrelated lender standalone?', 'sel', ['Yes', 'No', 'Uncertain']]
    ],
    royalty: [
      ['ip_description', 'IP description (trademark, patent, know-how, software, etc.)', 'text'],
      ['legal_owner', 'Legal owner of the IP', 'text'],
      ['dempe_functions', 'Which party performs Development, Enhancement, Maintenance, Protection and Exploitation (DEMPE) functions?', 'text'],
      ['royalty_base', 'Royalty base (net sales / gross sales / per unit / other)', 'text'],
      ['exclusivity', 'Exclusive or non-exclusive licence?', 'sel', ['Exclusive', 'Non-exclusive']]
    ],
    guarantee: [
      ['guaranteed_amount', 'Amount guaranteed', 'text'],
      ['guarantee_fee_pct', 'Guarantee fee (%), if any', 'text'],
      ['explicit_or_implicit', 'Explicit guarantee or implicit group support?', 'sel', ['Explicit', 'Implicit']],
      ['would_borrow_without_guarantee', 'Would the guaranteed entity obtain financing without the guarantee, and on what terms?', 'text']
    ],
    cost_allocation: [
      ['services_covered', 'Services/costs covered by the allocation', 'text'],
      ['allocation_key', 'Allocation key used (headcount, revenue, transaction volume, etc.)', 'text'],
      ['key_consistently_applied', 'Is the same key applied consistently across all recipient entities?', 'sel', ['Yes', 'No', 'Not confirmed']],
      ['benefit_test_performed', 'Has a benefit test been performed for each recipient?', 'sel', ['Yes', 'No', 'In progress']]
    ],
    lease: [
      ['asset_description', 'Asset description', 'text'],
      ['lease_term', 'Lease term', 'text'],
      ['payment_structure', 'Fixed or variable payments?', 'sel', ['Fixed', 'Variable', 'Mixed']],
      ['obsolescence_risk', 'Which party bears the risk of the asset\'s obsolescence?', 'text']
    ],
    restructuring: [
      ['restructuring_description', 'Nature of the restructuring (function, asset or risk transfer)', 'text'],
      ['compensation_for_transfer', 'Was compensation paid for value/profit potential transferred? If not, why not?', 'text'],
      ['prior_arrangement_terminated', 'Was a prior arrangement terminated or substantially renegotiated as part of this?', 'sel', ['Yes', 'No']]
    ],
    other: [
      ['description', 'Describe the transaction and the key facts relevant to pricing it', 'text']
    ]
  };

  function tpRenderQuestionnaireTab(root) {
    if (!tpState.selectedTxnId) { root.innerHTML = tpNoTxnMsg(); return; }
    const t = tpState.transactions.find(x => x.id === tpState.selectedTxnId);
    if (!t) { root.innerHTML = tpNoTxnMsg(); return; }
    const questions = TP_QUESTIONNAIRE[t.transaction_category] || TP_QUESTIONNAIRE.other;
    let detail = {};
    try { detail = JSON.parse(t.detail_json || '{}') || {}; } catch (e) { detail = {}; }

    root.innerHTML = '<div class="card"><h3>Questionnaire — ' + esc(t.transaction_category.replace(/_/g, ' ')) + '</h3>' +
      '<p class="card-note">Different facts matter for different transaction types — this set is chosen automatically from the category set on the Transactions tab. Answers save into this transaction\'s own record and can inform the FAR and method-selection tabs.</p>' +
      questions.map(q => {
        const [key, label, type, options] = q;
        const v = detail[key] || '';
        if (type === 'sel') return '<div class="field"><label>' + esc(label) + '</label><select data-tp-q="' + esc(key) + '"><option value="">— choose —</option>' +
          options.map(o => '<option value="' + esc(o) + '"' + (v === o ? ' selected' : '') + '>' + esc(o) + '</option>').join('') + '</select></div>';
        return '<div class="field"><label>' + esc(label) + '</label><input data-tp-q="' + esc(key) + '" value="' + esc(v) + '"></div>';
      }).join('') +
      '<button class="btn btn-primary" id="tp-q-save" style="margin-top:8px">Save answers</button></div>';

    root.querySelector('#tp-q-save').addEventListener('click', async () => {
      const answers = Object.assign({}, detail);
      root.querySelectorAll('[data-tp-q]').forEach(el => { answers[el.dataset.tpQ] = el.value; });
      try {
        await api('/api/tp/transactions/' + tpState.selectedTxnId, { method: 'PUT', body: { detail: answers } });
        toast('Questionnaire saved'); await tpLoadCore();
      } catch (e) { toast(e.message, true); }
    });
  }

  function tpRenderFarTab(root) {
    if (!tpState.selectedTxnId) { root.innerHTML = tpNoTxnMsg(); return; }
    const f = tpState.far || {};
    root.innerHTML = '<div class="card"><h3>FAR analysis</h3><div class="grid c2">' +
      '<div class="field"><label>Functional profile</label><select id="tp-far-profile"><option value="">— choose —</option>' +
      TP_PROFILES.map(p => '<option value="' + p + '"' + (p === f.functional_profile ? ' selected' : '') + '>' + p.replace(/_/g, ' ') + '</option>').join('') + '</select></div>' +
      '<div class="field"><label>Economic substance</label><input id="tp-far-subst" value="' + esc(f.economic_substance || '') + '"></div>' +
      '</div><div class="field"><label>Conclusion</label><textarea id="tp-far-conclusion" rows="4">' + esc(f.conclusion || '') + '</textarea></div>' +
      '<button class="btn btn-primary" id="tp-far-save" style="margin-top:8px">Save FAR</button></div>';
    root.querySelector('#tp-far-save').addEventListener('click', async () => {
      try {
        await api('/api/tp/transactions/' + tpState.selectedTxnId + '/far', { method: 'PUT', body: {
          functional_profile: root.querySelector('#tp-far-profile').value,
          economic_substance: root.querySelector('#tp-far-subst').value,
          conclusion: root.querySelector('#tp-far-conclusion').value } });
        toast('FAR saved'); await tpLoadTxnDetail();
      } catch (e) { toast(e.message, true); }
    });
  }

  function tpRenderMethodTab(root) {
    if (!tpState.selectedTxnId) { root.innerHTML = tpNoTxnMsg(); return; }
    const m = tpState.method || {};
    let h = '<div class="card"><h3>Method selection</h3><div class="grid c3">' +
      '<div class="field"><label>Internal CUP available?</label><select id="tp-ca-cup"><option value="">No</option><option value="1">Yes</option></select></div>' +
      '<div class="field"><label>External gross-margin data?</label><select id="tp-ca-gross"><option value="">No</option><option value="1">Yes</option></select></div>' +
      '<div class="field"><label>Unique intangible involved?</label><select id="tp-ca-uniq"><option value="">No</option><option value="1">Yes</option></select></div>' +
      '</div><button class="btn btn-small" id="tp-recommend" title="Runs the rule-based method-recommendation engine — never a silent pick, always explained">Recommend method</button>';
    if (m.recommended_method) h += '<p class="card-note" style="margin-top:10px"><b>Recommended: ' + esc(String(m.recommended_method).toUpperCase()) +
      '.</b> ' + esc(m.recommendation_basis || '') + '</p>';
    h += '<div class="grid c2" style="margin-top:10px">' +
      '<div class="field"><label>Selected method</label><select id="tp-method-selected"><option value="">— choose —</option>' +
      TP_METHODS.map(o => '<option value="' + o[0] + '"' + (o[0] === m.selected_method ? ' selected' : '') + '>' + o[1] + '</option>').join('') + '</select></div>' +
      '<div class="field"><label>PLI</label><select id="tp-method-pli">' + tpEnumOptions(TP_PLI_OPTIONS, m.pli, true) + '</select></div></div>' +
      '<div class="field"><label>Override justification (required if selected method differs from the recommendation)</label>' +
      '<textarea id="tp-method-just" rows="2">' + esc(m.override_justification || '') + '</textarea></div>' +
      '<button class="btn btn-primary" id="tp-method-save" style="margin-top:8px">Save method</button></div>';

    h += '<div class="card"><h3>Comparables</h3><div class="table-wrap"><table><thead><tr><th>Company</th><th>Country</th><th>FY</th>' +
      '<th class="num">PLI value</th><th>In set?</th><th>Exclusion reason</th><th></th></tr></thead><tbody>' +
      tpState.comparables.map(c => '<tr>' +
        '<td><input data-tp-comp="' + c.id + '" data-k="company_name" value="' + esc(c.company_name || '') + '"></td>' +
        '<td><select data-tp-comp="' + c.id + '" data-k="country"><option value="">—</option>' + tpCountryOptions(c.country, 'code') + '</select></td>' +
        '<td><input data-tp-comp="' + c.id + '" data-k="financial_year_label" value="' + esc(c.financial_year_label || '') + '"></td>' +
        '<td class="num"><input type="number" step="any" data-tp-comp="' + c.id + '" data-k="pli_value" value="' + esc(c.pli_value == null ? '' : c.pli_value) + '"></td>' +
        '<td><select data-tp-comp="' + c.id + '" data-k="include_in_set"><option value="1"' + (c.include_in_set ? ' selected' : '') + '>Yes</option>' +
        '<option value="0"' + (!c.include_in_set ? ' selected' : '') + '>No</option></select></td>' +
        '<td><input data-tp-comp="' + c.id + '" data-k="exclusion_reason" value="' + esc(c.exclusion_reason || '') + '"></td>' +
        '<td><button class="btn btn-small btn-danger" data-tp-del-comp="' + c.id + '" title="Remove this comparable">Remove</button></td></tr>').join('') +
      '</tbody></table></div><button class="btn btn-small" id="tp-add-comp" title="Add a comparable company">Add comparable</button> ' +
      '<button class="btn btn-small btn-primary" id="tp-compute-bench" title="Statistics over every comparable currently marked \'in set\'">Compute benchmark</button></div>';

    if (tpState.benchmark && tpState.benchmark.stats) {
      const s = tpState.benchmark.stats;
      h += '<div class="card"><h3>Benchmark result</h3><div class="table-wrap"><table><thead><tr><th>N</th><th>Min</th><th>Q1</th>' +
        '<th>Median</th><th>Q3</th><th>Max</th><th>Weighted avg</th></tr></thead><tbody><tr><td>' + (s.count || 0) + '</td><td>' +
        (s.min == null ? '—' : s.min) + '</td><td>' + (s.q1 == null ? '—' : s.q1) + '</td><td>' + (s.median == null ? '—' : s.median) +
        '</td><td>' + (s.q3 == null ? '—' : s.q3) + '</td><td>' + (s.max == null ? '—' : s.max) + '</td><td>' +
        (s.weighted_average == null ? '—' : s.weighted_average) + '</td></tr></tbody></table></div></div>';
    }

    h += '<div class="card"><h3>Adjustment exposure</h3><div class="grid c3">' +
      '<div class="field"><label>Actual result (%)</label><input type="number" step="any" id="tp-adj-actual"></div>' +
      '<div class="field"><label>Interest rate % (optional)</label><input type="number" step="any" id="tp-adj-interest"></div>' +
      '<div class="field"><label>Penalty % (optional)</label><input type="number" step="any" id="tp-adj-penalty"></div></div>' +
      '<button class="btn btn-small" id="tp-compute-adj" title="Arm\'s-length result comes from the benchmark median; tax rate is auto-pulled from Rules for this jurisdiction where available">Compute exposure</button>';
    if (tpState.adjustment && tpState.adjustment.id) {
      const a = tpState.adjustment;
      h += '<div class="table-wrap" style="margin-top:10px"><table><thead><tr><th>Actual</th><th>Arm\'s length</th><th class="num">Adjustment</th>' +
        '<th class="num">Tax impact</th><th class="num">Interest</th><th class="num">Penalty</th><th class="num">Total exposure</th></tr></thead><tbody><tr><td>' +
        (a.actual_result == null ? '—' : a.actual_result) + '</td><td>' + (a.arm_length_result == null ? '—' : a.arm_length_result) +
        '</td><td class="num">' + money(a.adjustment_amount) + '</td><td class="num">' + money(a.tax_impact) + '</td><td class="num">' +
        money(a.interest_exposure) + '</td><td class="num">' + money(a.penalty_exposure) + '</td><td class="num">' + money(a.total_exposure) +
        '</td></tr></tbody></table></div>' + (a.basis_notes ? '<p class="card-note">' + esc(a.basis_notes) + '</p>' : '') + '</div>';
    } else h += '</div>';

    h += '<div class="card"><h3>Scenario analysis</h3>' +
      '<p class="card-note">Base Case (no adjustment), Low/Median/High (the benchmark\'s Q1/median/Q3) and an optional User-Defined point — a range, not a single guess, using the actual result entered above.</p>' +
      '<div class="grid c2"><div class="field"><label>User-defined arm\'s-length point (%, optional)</label><input type="number" step="any" id="tp-scn-user"></div></div>' +
      '<button class="btn btn-small" id="tp-run-scenarios" title="Uses the actual result entered in Adjustment exposure above">Run scenarios</button>' +
      '<div id="tp-scenarios-body" style="margin-top:10px"></div></div>';

    root.innerHTML = h;

    root.querySelector('#tp-recommend').addEventListener('click', async () => {
      try {
        const rec = await api('/api/tp/transactions/' + tpState.selectedTxnId + '/method/recommend', { method: 'POST', body: { comparables_available: {
          internal_cup: !!root.querySelector('#tp-ca-cup').value, external_gross_margin: !!root.querySelector('#tp-ca-gross').value,
          unique_intangible: !!root.querySelector('#tp-ca-uniq').value } } });
        toast('Recommended: ' + rec.label); await tpLoadTxnDetail(); tpRenderBody();
      } catch (e) { toast(e.message, true); }
    });
    root.querySelector('#tp-method-save').addEventListener('click', async () => {
      try {
        await api('/api/tp/transactions/' + tpState.selectedTxnId + '/method', { method: 'PUT', body: {
          selected_method: root.querySelector('#tp-method-selected').value, pli: root.querySelector('#tp-method-pli').value,
          override_justification: root.querySelector('#tp-method-just').value } });
        toast('Method saved'); await tpLoadTxnDetail(); await tpLoadCore();
      } catch (e) { toast(e.message, true); }
    });
    root.querySelectorAll('[data-tp-comp]').forEach(el => el.addEventListener('change', async e => {
      const k = e.target.dataset.k; let v = e.target.value;
      if (k === 'pli_value') v = v === '' ? null : Number(v);
      if (k === 'include_in_set') v = v === '1';
      try { await api('/api/tp/comparables/' + e.target.dataset.tpComp, { method: 'PUT', body: { [k]: v } }); toast('Saved'); }
      catch (err) { toast(err.message, true); }
    }));
    root.querySelectorAll('[data-tp-del-comp]').forEach(el => el.addEventListener('click', async e => {
      try { await api('/api/tp/comparables/' + e.target.dataset.tpDelComp, { method: 'DELETE' }); await tpLoadTxnDetail(); tpRenderBody(); }
      catch (err) { toast(err.message, true); }
    }));
    root.querySelector('#tp-add-comp').addEventListener('click', async () => {
      try { await api('/api/tp/transactions/' + tpState.selectedTxnId + '/comparables', { method: 'POST', body: { company_name: 'New comparable' } }); await tpLoadTxnDetail(); tpRenderBody(); }
      catch (e) { toast(e.message, true); }
    });
    root.querySelector('#tp-compute-bench').addEventListener('click', async () => {
      try { await api('/api/tp/transactions/' + tpState.selectedTxnId + '/benchmark', { method: 'POST', body: {} }); toast('Benchmark computed'); await tpLoadTxnDetail(); tpRenderBody(); }
      catch (e) { toast(e.message, true); }
    });
    root.querySelector('#tp-compute-adj').addEventListener('click', async () => {
      const actual = root.querySelector('#tp-adj-actual').value, interest = root.querySelector('#tp-adj-interest').value, penalty = root.querySelector('#tp-adj-penalty').value;
      try {
        await api('/api/tp/transactions/' + tpState.selectedTxnId + '/adjustment', { method: 'POST', body: {
          actual_result: actual === '' ? null : Number(actual),
          interest_rate_pct: interest === '' ? undefined : Number(interest),
          penalty_pct: penalty === '' ? undefined : Number(penalty) } });
        toast('Adjustment computed'); await tpLoadTxnDetail(); tpRenderBody();
      } catch (e) { toast(e.message, true); }
    });
    root.querySelector('#tp-run-scenarios').addEventListener('click', async () => {
      const actual = root.querySelector('#tp-adj-actual').value;
      const userDefined = root.querySelector('#tp-scn-user').value;
      const interest = root.querySelector('#tp-adj-interest').value, penalty = root.querySelector('#tp-adj-penalty').value;
      const out = root.querySelector('#tp-scenarios-body');
      out.innerHTML = '<div class="empty">Loading…</div>';
      try {
        const res = await api('/api/tp/transactions/' + tpState.selectedTxnId + '/scenarios', { method: 'POST', body: {
          actual_result: actual === '' ? null : Number(actual),
          user_defined_arm_length: userDefined === '' ? undefined : Number(userDefined),
          interest_rate_pct: interest === '' ? undefined : Number(interest),
          penalty_pct: penalty === '' ? undefined : Number(penalty) } });
        out.innerHTML = '<div class="table-wrap"><table><thead><tr><th>Scenario</th><th>Arm\'s length</th><th class="num">Adjustment</th>' +
          '<th class="num">Tax impact</th><th class="num">Total exposure</th></tr></thead><tbody>' +
          res.scenarios.map(s => '<tr title="' + esc(s.description) + '"><td>' + esc(s.scenario) + '</td>' +
            '<td>' + (s.arm_length_result == null ? '—' : s.arm_length_result) + '</td>' +
            '<td class="num">' + money(s.adjustment_amount) + '</td><td class="num">' + money(s.tax_impact) +
            '</td><td class="num">' + money(s.total_exposure) + '</td></tr>').join('') + '</tbody></table></div>';
      } catch (e) { out.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; }
    });
  }

  function tpRenderRiskTab(root) {
    if (!tpState.selectedTxnId) { root.innerHTML = tpNoTxnMsg(); return; }
    let h = '<div class="card"><h3>Risk scoring</h3><div class="grid c3">' +
      TP_RISK_FLAGS.map(f => '<label style="display:flex;align-items:center;gap:6px;font-size:13px"><input type="checkbox" id="tp-flag-' + f[0] + '">' + esc(f[1]) + '</label>').join('') +
      '</div><button class="btn btn-small btn-primary" id="tp-compute-risk" style="margin-top:10px" title="A triage aid to prioritise review — not a conclusion on whether an adjustment is warranted">Compute risk</button></div>';
    const r = tpState.risk;
    if (r && r.risk_level) {
      h += '<div class="card"><h3>Result: ' + esc(r.risk_level) + ' (score ' + r.score + ')</h3><p class="card-note">' + esc(r.explanation || '') + '</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Factor</th><th class="num">Points</th><th>Note</th></tr></thead><tbody>' +
        (r.factors || []).map(f => '<tr><td>' + esc(f.factor) + '</td><td class="num">' + f.points + '</td><td>' + esc(f.note || '') + '</td></tr>').join('') +
        '</tbody></table></div></div>';
    }
    root.innerHTML = h;
    root.querySelector('#tp-compute-risk').addEventListener('click', async () => {
      const flags = {}; TP_RISK_FLAGS.forEach(f => { flags[f[0]] = root.querySelector('#tp-flag-' + f[0]).checked; });
      try { await api('/api/tp/transactions/' + tpState.selectedTxnId + '/risk', { method: 'POST', body: { flags } }); toast('Risk computed'); await tpLoadTxnDetail(); tpRenderBody(); }
      catch (e) { toast(e.message, true); }
    });
  }

  function tpRulesMatrixTable(countries, rulesByCountry) {
    const byKey = {};
    countries.forEach(c => (rulesByCountry[c] || []).forEach(r => {
      const key = r.domain + '::' + r.code;
      byKey[key] = byKey[key] || { domain: r.domain, code: r.code, label: r.label, cells: {} };
      byKey[key].cells[c] = r;
    }));
    const rows = Object.keys(byKey).map(k => byKey[k]).sort((a, b) => (a.domain + a.code).localeCompare(b.domain + b.code));
    if (!rows.length) return '<div class="empty"><b>No rules seeded yet for these jurisdictions.</b></div>';
    return '<div class="table-wrap"><table><thead><tr><th>Domain</th><th>Topic</th>' +
      countries.map(c => '<th>' + esc(c) + '</th>').join('') + '</tr></thead><tbody>' +
      rows.map(row => '<tr><td>' + esc(row.domain) + '</td><td>' + esc(row.label) + '</td>' +
        countries.map(c => {
          const r = row.cells[c];
          if (!r) return '<td>—</td>';
          const value = r.value_num != null ? r.value_num : (r.value_text || 'not recorded');
          const unit = r.unit && r.unit !== 'text' && r.value_num != null ? ' ' + r.unit : '';
          const stale = r.verification_status === 'UNVERIFIED' ? ' <span class="tag tag-amber">UNVERIFIED</span>' : '';
          return '<td title="' + esc((r.law_name || '') + ' ' + (r.section_article || '')) + '">' + esc(String(value) + unit) + stale + '</td>';
        }).join('') + '</tr>').join('') + '</tbody></table></div>';
  }

  function tpRenderMatrixTab(root) {
    if (!tpState.selectedTxnId) { root.innerHTML = tpNoTxnMsg(); return; }
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/tp/transactions/' + tpState.selectedTxnId + '/compliance-matrix').then(res => {
      if (!res.is_cross_border) {
        root.innerHTML = '<div class="card"><h3>Cross-border compliance matrix</h3>' +
          '<div class="empty"><b>Not a cross-border transaction</b>Set different source and destination countries on this transaction (Transactions tab) to see both sides\' rules side by side.</div></div>';
        return;
      }
      let h = '<div class="card"><h3>Cross-border compliance matrix — ' + esc(res.source_country) + ' ↔ ' + esc(res.destination_country) + '</h3>' +
        '<p class="card-note">Both sides\' applicable rules for this transaction (regulatory rules AND withholding — both live in the same Rules table, so both show up here), so neither jurisdiction\'s requirements get missed. Every figure here is only as reliable as its verification status.</p>';
      const t = res.treaty;
      h += '<div style="margin-bottom:14px"><b>Treaty position</b><br>';
      if (!t) {
        h += '<span class="card-note">No treaty entry recorded for this country pair yet — this does not mean no treaty exists, only that this system has nothing on file. Check with the relevant tax authority before assuming no treaty relief is available.</span>';
      } else {
        const status = t.in_force === 1 ? 'In force (per this record — still confirm the current text)' :
          t.in_force === 0 ? 'Not in force (per this record)' : 'Status unconfirmed';
        h += '<span class="tag ' + (t.in_force === 1 ? 'tag-green' : t.in_force === 0 ? 'tag-red' : 'tag-amber') + '">' + esc(status) + '</span> ' +
          esc(t.treaty_name || '') + '<br><span class="card-note">' + esc(t.notes || '') + '</span>';
      }
      h += '</div>' + tpRulesMatrixTable([res.source_country, res.destination_country], res.rules_by_country) + '</div>';
      root.innerHTML = h;
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  const tpCompareExtra = [];  // country codes added beyond the 3 core ones, this session only

  function tpRenderCompareTab(root) {
    const core = tpCoreCountries();
    root.innerHTML = '<div class="card"><h3>Country comparison</h3>' +
      '<p class="card-note">Compare how the same topic is treated across jurisdictions — same underlying Rules data as everywhere else in this module. Nepal, India and UAE have configured rule content; any other country can be added too, but will show as not yet configured until rules are added for it.</p>' +
      '<div class="grid c3">' + core.map(c => '<label style="display:flex;align-items:center;gap:6px;font-size:13px">' +
        '<input type="checkbox" class="tp-compare-cb" value="' + esc(c.code) + '" checked>' + esc(c.name) + '</label>').join('') + '</div>' +
      (tpCompareExtra.length ? '<p style="margin-top:10px"><b>Added:</b> ' + tpCompareExtra.map(code => {
        const c = (tpState.countries || []).find(x => x.code === code);
        return '<span class="tag" style="margin-right:6px">' + esc(c ? c.name : code) +
          ' <a href="#" data-tp-compare-remove="' + esc(code) + '" title="Remove from comparison">×</a></span>';
      }).join('') + '</p>' : '') +
      '<div class="grid c3" style="margin-top:10px"><div class="field"><label>Add another country</label><select id="tp-compare-add"><option value="">— choose —</option>' +
      tpCountryOptions('') + '</select></div></div>' +
      '<button class="btn btn-small" id="tp-compare-run" style="margin-top:10px">Compare</button></div>' +
      '<div id="tp-compare-body"></div>';
    root.querySelectorAll('[data-tp-compare-remove]').forEach(a => a.addEventListener('click', e => {
      e.preventDefault();
      const i = tpCompareExtra.indexOf(e.target.dataset.tpCompareRemove);
      if (i !== -1) tpCompareExtra.splice(i, 1);
      tpRenderCompareTab(root);
    }));
    root.querySelector('#tp-compare-add').addEventListener('change', e => {
      const code = e.target.value;
      if (code && tpCompareExtra.indexOf(code) === -1 && !core.find(c => c.code === code)) tpCompareExtra.push(code);
      tpRenderCompareTab(root);
    });
    root.querySelector('#tp-compare-run').addEventListener('click', async () => {
      const picked = Array.from(root.querySelectorAll('.tp-compare-cb:checked')).map(cb => cb.value).concat(tpCompareExtra);
      const body = root.querySelector('#tp-compare-body');
      if (picked.length < 2) { toast('Choose at least two countries', true); return; }
      body.innerHTML = '<div class="empty">Loading…</div>';
      try {
        const res = await api('/api/tp/rules/compare?countries=' + picked.join(','));
        body.innerHTML = '<div class="card">' + tpRulesMatrixTable(res.countries, res.rules_by_country) + '</div>';
      } catch (e) { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; }
    });
  }

  function tpRenderDocsTab(root) {
    root.innerHTML = '<div class="card"><h3>Documentation checklist — FY ' + esc(tpState.fy) + '</h3><div class="empty">Loading…</div></div>';
    api('/api/tp/clients/' + tpState.clientId + '/checklist?fy=' + encodeURIComponent(tpState.fy)).then(rows => {
      const STATUSES = ['missing', 'in_progress', 'complete'];
      root.innerHTML = '<div class="card"><h3>Documentation checklist — FY ' + esc(tpState.fy) + '</h3>' +
        (rows.length ? '<div class="table-wrap"><table><thead><tr><th>Requirement</th><th>Status</th><th>Notes</th></tr></thead><tbody>' +
          rows.map(r => '<tr><td>' + esc(r.label) + '</td>' +
            '<td><select data-tp-check="' + r.id + '" data-k="status">' + STATUSES.map(s => '<option value="' + s + '"' + (s === r.status ? ' selected' : '') + '>' + s.replace('_', ' ') + '</option>').join('') + '</select></td>' +
            '<td><input data-tp-check="' + r.id + '" data-k="notes" value="' + esc(r.notes || '') + '"></td></tr>').join('') + '</tbody></table></div>'
          : '<div class="empty"><b>No checklist yet</b>Set this client\'s jurisdiction on the Entities tab — the checklist auto-populates from that country\'s documentation rules.</div>') +
        '</div>';
      root.querySelectorAll('[data-tp-check]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/tp/checklist/' + e.target.dataset.tpCheck, { method: 'PUT', body: { [e.target.dataset.k]: e.target.value } }); toast('Saved'); }
        catch (err) { toast(err.message, true); }
      }));
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function tpRenderWpTab(root) {
    root.innerHTML = '<div class="card"><div class="empty">Loading…</div></div>';
    api('/api/tp/clients/' + tpState.clientId + '/working-papers').then(rows => {
      const STATUSES = ['open', 'approved', 'rejected', 'sent_back'];
      root.innerHTML = '<div class="card"><h3>Working papers</h3><div class="table-wrap"><table><thead><tr><th>Ref</th><th>Title</th>' +
        '<th>Prepared by</th><th>Review status</th><th></th></tr></thead><tbody>' +
        rows.map(w => '<tr><td>' + esc(w.ref) + '</td><td>' + esc(w.title || '') + '</td><td>' + esc(w.prepared_by_name || '') + '</td>' +
          '<td><select data-tp-wp-status="' + w.id + '">' + STATUSES.map(s => '<option value="' + s + '"' + (s === w.review_status ? ' selected' : '') + '>' + s.replace('_', ' ') + '</option>').join('') + '</select></td>' +
          '<td><button class="btn btn-small" data-tp-wp-view="' + w.id + '" title="Open this working paper\'s content">View / edit</button></td></tr>').join('') +
        '</tbody></table></div><div class="grid c3" style="margin-top:10px">' +
        '<div class="field"><label>Ref</label><input id="tp-wp-ref" placeholder="TP-01"></div>' +
        '<div class="field"><label>Title</label><input id="tp-wp-title"></div></div>' +
        '<button class="btn btn-small" id="tp-wp-add" style="margin-top:8px" title="e.g. TP-01, FAR-01, BENCH-01, ADJ-01, DOC-01">Add working paper</button></div>' +
        '<div class="card" id="tp-wp-detail" hidden></div>';

      root.querySelectorAll('[data-tp-wp-status]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/tp/working-papers/' + e.target.dataset.tpWpStatus, { method: 'PUT', body: { review_status: e.target.value } }); toast('Saved'); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-tp-wp-view]').forEach(el => el.addEventListener('click', () => {
        const w = rows.find(x => x.id === Number(el.dataset.tpWpView));
        const d = root.querySelector('#tp-wp-detail');
        d.hidden = false;
        d.innerHTML = '<h3>' + esc(w.ref) + ' — ' + esc(w.title || '') + '</h3>' +
          '<div class="field"><label>Content</label><textarea id="tp-wp-content" rows="8">' + esc(w.content || '') + '</textarea></div>' +
          '<button class="btn btn-small btn-primary" id="tp-wp-save-content" style="margin-top:8px">Save content</button>' +
          '<div class="field" style="margin-top:8px"><label>Review comment</label><input id="tp-wp-comment" value="' + esc(w.review_comment || '') + '"></div>';
        d.querySelector('#tp-wp-save-content').addEventListener('click', async () => {
          try { await api('/api/tp/working-papers/' + w.id, { method: 'PUT', body: { content: d.querySelector('#tp-wp-content').value } }); toast('Saved'); }
          catch (err) { toast(err.message, true); }
        });
      }));
      root.querySelector('#tp-wp-add').addEventListener('click', async () => {
        const ref = root.querySelector('#tp-wp-ref').value.trim(), title = root.querySelector('#tp-wp-title').value.trim();
        if (!ref) { toast('A reference is required', true); return; }
        try { await api('/api/tp/clients/' + tpState.clientId + '/working-papers', { method: 'POST', body: { ref, title, financial_year_label: tpState.fy } }); tpRenderBody(); }
        catch (e) { toast(e.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function tpRenderReportsTab(root) {
    // formats: which buttons to show — most reports offer both; the three
    // memo-style reports (advisory memo, client info request, working
    // paper pack) are inherently prose documents, Word only.
    const REPORT_TYPES = [
      ['related_party', 'Related party transaction report', ['xlsx', 'docx']],
      ['far_method_benchmark', 'FAR, method & benchmarking report', ['xlsx', 'docx']],
      ['risk_assessment', 'Risk assessment report', ['xlsx', 'docx']],
      ['tp_study', 'Transfer Pricing Study Report (comprehensive)', ['xlsx', 'docx']],
      ['adjustment_exposure', 'Adjustment Exposure Report', ['xlsx', 'docx']],
      ['documentation_gap', 'Documentation Gap Report', ['xlsx', 'docx']],
      ['advisory_memo', 'Management Advisory Memo', ['docx']],
      ['client_info_request', 'Client Information Request', ['docx']],
      ['working_paper_pack', 'Audit Working Paper Pack', ['docx']]
    ];
    root.innerHTML = '<div class="card"><h3>Reports</h3><p class="card-note">Generated for the client and financial year selected above. Every report states the jurisdiction, and flags a professional-review banner wherever it cites an unverified rule.</p>' +
      REPORT_TYPES.map(rt => '<div style="margin-bottom:10px"><b>' + esc(rt[1]) + '</b> — ' +
        rt[2].map(fmt => '<button class="btn btn-small" data-tp-report="' + rt[0] + '" data-fmt="' + fmt + '">' + (fmt === 'xlsx' ? 'Excel' : 'Word') + '</button> ').join('') +
        '</div>').join('') + '</div>';
    root.querySelectorAll('[data-tp-report]').forEach(el => el.addEventListener('click', async e => {
      try {
        const res = await api('/api/tp/clients/' + tpState.clientId + '/reports', { method: 'POST', body: {
          report_type: e.target.dataset.tpReport, format: e.target.dataset.fmt, fy: tpState.fy } });
        toast('Generated ' + res.filename);
        window.open(res.download_url, '_blank');
      } catch (err) { toast(err.message, true); }
    }));
  }

  // Method definitions are standard, internationally-recognised TP
  // terminology (the OECD/UN five-method taxonomy this module's own
  // recommend_method() engine already uses) — not a jurisdiction-specific
  // legal position, so unlike everything in Rules these aren't UNVERIFIED
  // placeholders. Jurisdiction-specific acceptance/ordering of methods still
  // lives only in Rules, cited there with its own source.
  const TP_METHOD_GLOSSARY = [
    { code: 'cup', name: 'Comparable Uncontrolled Price (CUP)',
      def: 'Compares the price charged in the controlled transaction to the price charged in a comparable transaction between independent parties under similar circumstances.',
      when: 'The most direct method when a genuinely comparable internal or external price exists — commodity goods, intercompany loans and interest rates, royalty rates with comparable licences, service fees with a comparable market rate.',
      pli: 'Not margin-based — compares price or rate directly.' },
    { code: 'rpm', name: 'Resale Price Method (RPM)',
      def: 'Starts from the price at which a product bought from an associated enterprise is resold to an independent party, then works back by subtracting an appropriate resale (gross) margin.',
      when: 'A distributor that resells goods largely unchanged and adds limited value before resale — routine or limited-risk distribution — with reliable external gross-margin comparables.',
      pli: 'Gross margin on resale (resale price − cost of goods sold) / resale price.' },
    { code: 'cpm', name: 'Cost Plus Method',
      def: 'Starts from the costs incurred by the supplier in the controlled transaction, then adds an appropriate gross mark-up.',
      when: 'Manufacturing, contract manufacturing, or low-risk service provision where the tested party is a routine producer/service provider, with reliable cost-plus mark-up comparables.',
      pli: 'Gross mark-up on cost (gross profit / cost of goods sold).' },
    { code: 'tnmm', name: 'Transactional Net Margin Method (TNMM)',
      def: 'Examines the net profit margin (relative to an appropriate base — costs, sales, or assets) that the tested party earns from the controlled transaction, benchmarked against net margins earned by independent parties in comparable transactions.',
      when: 'The most commonly used fallback when reliable CUP/RPM/CPM data isn\'t available — net margins are more tolerant of product and minor functional differences than gross margins or price comparisons.',
      pli: 'Operating margin, Berry ratio, return on total costs, return on assets, return on capital employed.' },
    { code: 'psm', name: 'Profit Split Method',
      def: 'Identifies the combined profit from the controlled transactions and splits it between the associated enterprises based on the relative value of each party\'s contribution.',
      when: 'Highly integrated operations, transactions where both parties contribute unique and valuable intangibles, or where no one-sided method (RPM/CPM/TNMM) can reliably capture how value was created.',
      pli: 'Not a single ratio — a contribution or residual-profit split, usually keyed to functions, assets and risks (FAR) or capital/development-cost contribution.' },
    { code: 'other', name: 'Other Method',
      def: 'Any other approach that produces a result consistent with the arm\'s length principle, used only when none of the five recognised methods can be reliably applied on the facts.',
      when: 'Rare — genuinely novel fact patterns (e.g. certain business restructurings, unique intangible valuations) where a standard method breaks down. Needs unusually strong documentation of why the standard methods were rejected.',
      pli: 'Facts-and-circumstances specific.' }
  ];

  // Fictional, illustrative scenarios only — no real client's facts. Loading
  // one creates a real transaction row (clearly labelled as an example in
  // its description) that the preparer can edit or delete like any other.
  const TP_EXAMPLES = [
    { key: 'np_in_goods', title: 'Nepal → India — export of finished goods to a subsidiary',
      scenario: 'A Nepal-based garment manufacturer (the tested party) exports finished garments to its 100%-owned Indian distribution subsidiary, which resells them to independent retailers across India.',
      source_country: 'NP', destination_country: 'IN', category: 'goods', direction: 'export', currency: 'NPR', amount: 8500000,
      far: 'Nepal entity: full-risk manufacturer. India entity: limited-risk distributor bearing minimal inventory and market risk.',
      method: 'Resale Price Method on the India side if reliable external gross-margin comparables exist; TNMM (operating margin) as the common fallback.' },
    { key: 'np_ae_services', title: 'Nepal → UAE — management and technical service fees',
      scenario: 'A Nepal-based operating company (the tested party) pays an annual management and technical service fee to its UAE parent for group finance, IT and technical support functions.',
      source_country: 'NP', destination_country: 'AE', category: 'services', direction: 'payment', currency: 'NPR', amount: 3200000,
      far: 'Nepal entity: recipient of centrally-provided services, limited own functions in this area. UAE entity: service provider bearing the cost and delivery risk of the shared function.',
      method: 'Cost Plus Method on the UAE service provider\'s cost base, or TNMM if a reliable cost-plus comparable set isn\'t available. Benefit test and duplication-of-cost analysis should be documented before any deduction is claimed.' },
    { key: 'in_ae_financing', title: 'India → UAE — intercompany loan to a group treasury entity',
      scenario: 'An Indian operating subsidiary places surplus cash as an intercompany loan with the UAE group treasury/holding entity, at a stated interest rate.',
      source_country: 'IN', destination_country: 'AE', category: 'financing', direction: 'payment', currency: 'INR', amount: 45000000,
      far: 'India entity: lender, bearing credit risk on the UAE counterparty. UAE entity: borrower/treasury function.',
      method: 'CUP — benchmarked against comparable unrelated lending rates for a borrower of similar credit risk, tenure and currency. Thin-capitalisation and interest-limitation rules on both sides should be checked separately from the pricing question.' },
    { key: 'np_domestic_royalty', title: 'Domestic Nepal — royalty for use of a group trademark',
      scenario: 'A Nepal operating company pays a royalty to a Nepal-incorporated group IP-holding entity for the right to use a shared trademark and brand standards domestically.',
      source_country: 'NP', destination_country: 'NP', category: 'royalty', direction: 'payment', currency: 'NPR', amount: 900000,
      far: 'Operating entity: brand user, no legal ownership of the IP. IP-holding entity: legal owner, but the DEMPE (development, enhancement, maintenance, protection, exploitation) functions actually performed should be checked before accepting legal ownership as the whole story.',
      method: 'CUP against comparable third-party trademark licences where available; Profit Split may be more appropriate if the operating entity\'s own marketing effort has materially built the brand\'s local value.' },
    { key: 'in_domestic_goods', title: 'Domestic India — purchase of raw materials from a sister company',
      scenario: 'An Indian manufacturing company purchases a key raw material from a domestic sister company under common ownership, rather than from an unrelated supplier.',
      source_country: 'IN', destination_country: 'IN', category: 'goods', direction: 'import', currency: 'INR', amount: 12500000,
      far: 'Both entities: routine manufacturing/trading functions, no unique intangibles on this transaction.',
      method: 'CUP if a comparable unrelated-party price for the same or a closely similar raw material exists (often the case for standardised inputs); Cost Plus Method on the sister company\'s cost base otherwise.' },
    { key: 'ae_domestic_services', title: 'Domestic UAE — shared services from a group service centre',
      scenario: 'A UAE operating company receives HR, payroll and IT-helpdesk services from a UAE-based group shared-service centre serving several affiliates in the same country.',
      source_country: 'AE', destination_country: 'AE', category: 'services', direction: 'payment', currency: 'AED', amount: 480000,
      far: 'Operating entity: service recipient. Shared-service centre: low-risk service provider performing routine, well-defined functions for multiple recipients.',
      method: 'Cost Plus Method (or TNMM) on the shared-service centre\'s allocated cost base, with the allocation key (headcount, revenue, transaction volume) documented and consistently applied across all recipient entities.' }
  ];

  // Industry templates — professional/methodological orientation only
  // (typical transaction types and functional profiles seen in practice),
  // never a jurisdiction-specific legal position. Picking one doesn't change
  // any stored data; it's a reference card to help scope the FAR and
  // questionnaire faster for a first-time engagement in that industry.
  const TP_INDUSTRY_TEMPLATES = [
    ['Manufacturing', 'goods, royalty, services', 'Full-risk or contract manufacturer; watch for toll/contract manufacturing fact patterns and IP embedded in process know-how.'],
    ['Trading / distribution', 'goods', 'Routine, limited-risk or full-risk distributor — the single biggest driver of method choice here is how much market/inventory/credit risk the distributor actually carries.'],
    ['Pharmaceuticals', 'goods, royalty, services', 'Heavy IP content (patents, formulations, regulatory dossiers); DEMPE analysis on R&D functions is usually the crux.'],
    ['IT / software', 'services, royalty', 'Watch for disguised royalty in "service" fees for software use; contract R&D vs. full-risk product-owner distinction matters a lot.'],
    ['Consultancy / professional services', 'services', 'Usually people-driven; TNMM on a cost or revenue base is common when no reliable CUP exists for the specific expertise.'],
    ['Construction', 'services, goods, cost_allocation', 'Long-duration contracts raise timing/percentage-of-completion questions alongside the pricing question; equipment/plant leasing between group entities is common.'],
    ['Hydropower / energy', 'financing, services, goods', 'Heavy intercompany financing and EPC/O&M service arrangements; long project life makes benchmark comparability period selection important.'],
    ['Hotels / hospitality', 'services, royalty', 'Management and franchise fee structures are common — check whether a "management fee" is really a disguised royalty for brand use.'],
    ['Airlines / aviation', 'services, lease, financing', 'Aircraft/engine leasing between group entities, and cost-sharing of pooled maintenance/crew functions, are frequent fact patterns.'],
    ['Education', 'services, royalty'],
    ['Healthcare', 'services, royalty, goods'],
    ['Banking / financial services', 'financing, guarantee, services', 'Intragroup treasury, cash-pooling and guarantee fees dominate; interest-rate CUP benchmarking is usually central.'],
    ['Insurance', 'services, financing', 'Reinsurance and shared-service cost allocations (claims processing, underwriting support) are common intercompany items.'],
    ['Telecommunications', 'services, royalty, cost_allocation', 'Network/infrastructure cost-sharing and brand/technology royalties are frequent.'],
    ['FMCG', 'goods, royalty, services', 'Brand royalties alongside distribution — check that the same brand value isn\'t being paid for twice through both a royalty and a thin distribution margin.'],
    ['Automotive', 'goods, royalty, services', 'Component/parts trading, technology licensing, and warranty cost-sharing are common.'],
    ['Real estate', 'services, financing'],
    ['Logistics', 'services, cost_allocation'],
    ['Import / export trading houses', 'goods'],
    ['E-commerce', 'services, royalty, cost_allocation', 'Platform/technology royalties and centralised marketing cost-allocation are frequent; where value is actually created (local market vs. central platform) is usually the key FAR question.']
  ];

  function tpRenderExamplesTab(root) {
    root.innerHTML =
      '<div class="card"><h3>Types of transfer pricing methods</h3>' +
      '<p class="card-note">General, internationally-recognised method definitions — not a jurisdiction-specific legal position. Which methods a given country formally accepts, and in what order of preference, is recorded per-country in Rules.</p>' +
      TP_METHOD_GLOSSARY.map(m => '<div style="margin-bottom:14px"><b>' + esc(m.name) + '</b><br>' +
        '<span style="font-size:13px">' + esc(m.def) + '</span><br>' +
        '<span class="card-note" style="margin:4px 0 0"><b>When it fits:</b> ' + esc(m.when) + '</span><br>' +
        '<span class="card-note" style="margin:2px 0 0"><b>Typical PLI / basis:</b> ' + esc(m.pli) + '</span></div>').join('') +
      '</div>' +
      '<div class="card"><h3>Industry reference</h3>' +
      '<p class="card-note">Typical transaction types and functional-profile orientation seen in practice for each industry — a starting scope, never a jurisdiction-specific legal position or a substitute for the actual FAR analysis on this client\'s facts.</p>' +
      '<div class="table-wrap"><table><thead><tr><th>Industry</th><th>Typical transaction types</th><th>Notes</th></tr></thead><tbody>' +
      TP_INDUSTRY_TEMPLATES.map(t => '<tr><td>' + esc(t[0]) + '</td><td>' + esc(t[1]) + '</td><td style="font-size:12px">' + esc(t[2] || '') + '</td></tr>').join('') +
      '</tbody></table></div></div>' +
      '<div class="card"><h3>Example scenarios</h3>' +
      '<p class="card-note">Fictional, illustrative fact patterns only — not drawn from any real engagement. "Load as transaction" adds a real, editable transaction to the currently selected client, clearly labelled as an example so it can\'t be mistaken for real client data.</p>' +
      TP_EXAMPLES.map(ex => '<div class="card" style="margin:10px 0;padding:12px"><b>' + esc(ex.title) + '</b>' +
        '<p style="font-size:13px;margin:6px 0">' + esc(ex.scenario) + '</p>' +
        '<p class="card-note" style="margin:2px 0"><b>FAR:</b> ' + esc(ex.far) + '</p>' +
        '<p class="card-note" style="margin:2px 0"><b>Likely method:</b> ' + esc(ex.method) + '</p>' +
        '<button class="btn btn-small" data-tp-load-example="' + esc(ex.key) + '" title="Add this as a real transaction on the selected client">Load as transaction</button></div>').join('') +
      '</div>';
    root.querySelectorAll('[data-tp-load-example]').forEach(b => b.addEventListener('click', async e => {
      if (!tpState.clientId) { toast('Choose a client first', true); return; }
      const ex = TP_EXAMPLES.find(x => x.key === e.target.dataset.tpLoadExample);
      try {
        const created = await api('/api/tp/clients/' + tpState.clientId + '/transactions', { method: 'POST', body: {
          financial_year_label: tpState.fy, source_country: ex.source_country, destination_country: ex.destination_country,
          transaction_category: ex.category, direction: ex.direction, currency: ex.currency, amount: ex.amount,
          description: '[EXAMPLE — fictional, for reference only] ' + ex.scenario } });
        tpState.selectedTxnId = created.id; tpState.tab = 'transactions';
        toast('Example loaded as a transaction — edit or delete it like any other');
        await tpBoot();
      } catch (err) { toast(err.message, true); }
    }));
  }

  // ─────────────────────────────────────────────────── practice management
  // Same posture as Transfer Pricing: relational data (cross-client staff
  // allocation, AR aging), not a single JSON workfile — a top-level rail
  // page, online-first, tab-based.
  const PM_TABS = [
    ['dashboard', 'Dashboard'], ['onboarding', 'Onboarding'], ['acceptance', 'Acceptance & risk'],
    ['proposals', 'Proposals & letters'], ['documents', 'Document requests'], ['assignments', 'Assignments'],
    ['budget', 'Time budget'], ['closure', 'Closure']
  ];
  const pmState = { clientId: '', tab: 'dashboard' };

  function pmStaffOptions(selected) {
    const staff = (state.boot && state.boot.staff) || [];
    return '<option value="">— unassigned —</option>' +
      staff.map(s => '<option value="' + s.id + '"' + (String(s.id) === String(selected) ? ' selected' : '') + '>' + esc(s.full_name) + '</option>').join('');
  }
  function pmClientEngagements() {
    return ((state.boot && state.boot.engagements) || []).filter(e => String(e.client_id) === String(pmState.clientId));
  }
  function pmEngagementOptions(selected) {
    return '<option value="">— none —</option>' +
      pmClientEngagements().map(e => '<option value="' + e.id + '"' + (String(e.id) === String(selected) ? ' selected' : '') +
        '>' + esc(e.title || e.type) + ' (' + esc(e.status) + ')</option>').join('');
  }

  function renderPM() {
    const view = document.getElementById('view');
    if (!state.online || !state.token || state.offlineMode) {
      view.innerHTML = '<div class="page-head"><div><h2>Practice Management</h2></div></div>' +
        '<div class="card"><div class="empty"><b>Offline</b>Practice Management works against live server data — reconnect to use this module.</div></div>';
      return;
    }
    if (!pmState.clientId) pmState.clientId = localStorage.getItem('ca.client.pm') || (clients()[0] && clients()[0].id) || '';

    view.innerHTML =
      '<div class="page-head"><div><h2>Practice Management</h2>' +
      '<p>Onboarding, KYC &amp; conflict checks, engagement risk, proposals, fee quotations, engagement letters, document requests, staff assignments, time budgets and engagement closure — one place, one client at a time.</p></div></div>' +
      '<div class="card"><div class="grid c3">' +
      '<div class="field"><label>Client</label><select id="pm-client"><option value="">— choose —</option>' +
      clients().map(c => '<option value="' + c.id + '"' + (String(pmState.clientId) === String(c.id) ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') + '</select></div>' +
      '</div></div>' +
      '<div class="card" id="pm-body"><div class="empty">Loading…</div></div>';

    document.getElementById('pm-client').addEventListener('change', e => {
      pmState.clientId = e.target.value; localStorage.setItem('ca.client.pm', pmState.clientId);
      pmState.tab = 'dashboard';
      pmRenderBody();
    });
    pmRenderBody();
  }

  function pmRenderBody() {
    const body = document.getElementById('pm-body');
    if (!pmState.clientId) { body.innerHTML = '<div class="empty"><b>Choose a client</b>Pick a client above to begin.</div>'; return; }
    body.innerHTML = '<div style="margin-bottom:14px">' +
      PM_TABS.map(t => '<button class="btn btn-small' + (pmState.tab === t[0] ? ' btn-primary' : ' btn-ghost') +
        '" data-pm-tab="' + t[0] + '" style="margin:0 6px 6px 0">' + esc(t[1]) + '</button>').join('') +
      '</div><div id="pm-tab-body"></div>';
    body.querySelectorAll('[data-pm-tab]').forEach(b => b.addEventListener('click', e => {
      pmState.tab = e.target.dataset.pmTab; pmRenderBody();
    }));
    const tb = document.getElementById('pm-tab-body');
    ({ dashboard: pmRenderDashboardTab, onboarding: root => pmRenderChecklistTab(root, 'onboarding'),
       acceptance: pmRenderAcceptanceTab, proposals: pmRenderProposalsTab,
       documents: root => pmRenderChecklistTab(root, 'document_request'), assignments: pmRenderAssignmentsTab,
       budget: pmRenderBudgetTab, closure: pmRenderClosureTab }[pmState.tab] || pmRenderDashboardTab)(tb);
  }

  function pmRenderDashboardTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([
      api('/api/pm/clients/' + pmState.clientId + '/dashboard'),
      api('/api/pm/dashboard/staff-allocation'),
      api('/api/pm/dashboard/outstanding-fees')
    ]).then(([cd, staffAlloc, fees]) => {
      root.innerHTML =
        '<div class="grid c4">' +
        kpi('Open assignments', cd.open_assignments) +
        kpi('Outstanding fees', money(cd.outstanding_fees), cd.outstanding_fees ? 'alert' : '') +
        kpi('Open follow-ups', cd.open_followups) +
        kpi('Risk level', cd.risk_level || 'Not assessed', cd.risk_level === 'High' ? 'alert' : '') +
        '</div>' +
        '<div class="card"><h3>Checklist progress</h3><div class="grid c3">' +
        ['onboarding', 'document_request', 'closure'].map(stage => {
          const c = cd.checklist[stage] || { total: 0, done: 0 };
          return '<div><b>' + esc(stage.replace('_', ' ')) + '</b><p class="card-note">' + c.done + ' of ' + c.total + ' complete</p></div>';
        }).join('') + '</div>' +
        '<p class="card-note" style="margin-top:8px">Acceptance decision: <b>' + esc(cd.acceptance_decision || 'Not yet recorded') + '</b></p></div>' +
        '<div class="card"><h3>Staff allocation (firm-wide)</h3><div class="table-wrap"><table><thead><tr><th>Staff</th><th class="num">Open assignments</th><th class="num">Open engagements</th><th class="num">Hours (30 days)</th></tr></thead><tbody>' +
        staffAlloc.map(s => '<tr><td>' + esc(s.name) + '</td><td class="num">' + s.open_assignments + '</td><td class="num">' + s.open_engagements + '</td><td class="num">' + s.hours_last_30_days + '</td></tr>').join('') +
        '</tbody></table></div></div>' +
        '<div class="card"><h3>Outstanding fees (firm-wide)</h3><p class="card-note">Total outstanding: <b>' + money(fees.total_outstanding) + '</b></p>' +
        (fees.invoices.length ? '<div class="table-wrap"><table><thead><tr><th>Invoice</th><th>Client</th><th>Due</th><th class="num">Outstanding</th><th class="num">Days overdue</th></tr></thead><tbody>' +
          fees.invoices.map(i => '<tr><td>' + esc(i.invoice_no) + '</td><td>' + esc(i.client_name) + '</td><td>' + esc(i.due_date || '—') + '</td>' +
            '<td class="num">' + money(i.outstanding) + '</td><td class="num">' + (i.days_overdue ? '<span class="tag tag-red">' + i.days_overdue + '</span>' : '—') + '</td></tr>').join('') + '</tbody></table></div>'
          : '<div class="empty">Nothing outstanding.</div>') + '</div>';
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  const PM_CHECKLIST_TITLES = { onboarding: 'Onboarding checklist', document_request: 'Document request list', closure: 'Closure checklist' };
  const PM_CHECKLIST_STATUS = { onboarding: ['pending', 'done', 'na'], document_request: ['pending', 'received', 'na'], closure: ['pending', 'done', 'na'] };

  function pmRenderChecklistTab(root, stage) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/pm/clients/' + pmState.clientId + '/checklist?stage=' + stage).then(rows => {
      const statuses = PM_CHECKLIST_STATUS[stage];
      root.innerHTML = '<div class="card"><h3>' + esc(PM_CHECKLIST_TITLES[stage]) + '</h3>' +
        (rows.length ? '<div class="table-wrap"><table><thead><tr><th>Item</th><th>Status</th><th>Due</th><th>Notes</th><th></th></tr></thead><tbody>' +
          rows.map(r => '<tr><td>' + esc(r.label) + '</td>' +
            '<td><select data-pm-check="' + r.id + '" data-k="status">' + statuses.map(s => '<option value="' + s + '"' + (s === r.status ? ' selected' : '') + '>' + s.replace('_', ' ') + '</option>').join('') + '</select></td>' +
            '<td><input type="date" data-pm-check="' + r.id + '" data-k="due_date" value="' + esc(r.due_date || '') + '"></td>' +
            '<td><input data-pm-check="' + r.id + '" data-k="notes" value="' + esc(r.notes || '') + '"></td>' +
            '<td><button class="btn btn-small btn-danger" data-pm-del-check="' + r.id + '" title="Remove this item">Remove</button></td></tr>').join('') + '</tbody></table></div>'
          : '<div class="empty">No items yet — seed the standard list below or add your own.</div>') +
        '<button class="btn btn-small" id="pm-seed-checklist" title="Add the standard items, skipping any already present">Seed standard checklist</button> ' +
        '<button class="btn btn-small btn-ghost" id="pm-add-checklist" title="Add a custom item">Add item</button></div>';

      root.querySelectorAll('[data-pm-check]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/pm/checklist/' + e.target.dataset.pmCheck, { method: 'PUT', body: { [e.target.dataset.k]: e.target.value } }); toast('Saved'); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-pm-del-check]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/pm/checklist/' + e.target.dataset.pmDelCheck, { method: 'DELETE' }); pmRenderChecklistTab(root, stage); }
        catch (err) { toast(err.message, true); }
      }));
      document.getElementById('pm-seed-checklist').addEventListener('click', async () => {
        try { const r = await api('/api/pm/clients/' + pmState.clientId + '/checklist/seed', { method: 'POST', body: { stage } }); toast(r.added + ' items added'); pmRenderChecklistTab(root, stage); }
        catch (e) { toast(e.message, true); }
      });
      document.getElementById('pm-add-checklist').addEventListener('click', async () => {
        const label = prompt('Item label?');
        if (!label) return;
        try { await api('/api/pm/clients/' + pmState.clientId + '/checklist', { method: 'POST', body: { stage, label } }); pmRenderChecklistTab(root, stage); }
        catch (e) { toast(e.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function pmRenderAcceptanceTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/pm/clients/' + pmState.clientId + '/acceptance').then(a => {
      a = a || {};
      root.innerHTML = '<div class="card"><h3>Know Your Client (KYC)</h3><div class="grid c2">' +
        ['kyc_identity_verified', 'kyc_beneficial_ownership', 'kyc_source_of_funds', 'kyc_pep_check'].map(k =>
          '<label class="check"><input type="checkbox" id="pm-' + k + '"' + (a[k] ? ' checked' : '') + '> ' +
          esc(k.replace('kyc_', '').replace(/_/g, ' ')) + '</label>').join('') +
        '</div><label>Notes</label><textarea id="pm-kyc_notes" rows="2" style="width:100%">' + esc(a.kyc_notes || '') + '</textarea></div>' +

        '<div class="card"><h3>Conflict of interest check</h3>' +
        '<div class="field"><label>Search existing clients / related parties</label><input id="pm-conflict-search" placeholder="Type a name…"></div>' +
        '<div id="pm-conflict-results"></div>' +
        '<label>Names searched (record what you checked)</label><input id="pm-conflict_search_names" style="width:100%" value="' + esc(a.conflict_search_names || '') + '">' +
        '<div class="grid c2" style="margin-top:8px">' +
        '<div class="field"><label>Decision</label><select id="pm-conflict_decision">' +
        [['', '— choose —'], ['none_found', 'No conflict found'], ['cleared_with_safeguards', 'Conflict found — cleared with safeguards'], ['declined', 'Declined due to conflict']]
          .map(o => '<option value="' + o[0] + '"' + (a.conflict_decision === o[0] ? ' selected' : '') + '>' + o[1] + '</option>').join('') + '</select></div></div>' +
        '<label>Notes</label><textarea id="pm-conflict_notes" rows="2" style="width:100%">' + esc(a.conflict_notes || '') + '</textarea></div>' +

        '<div class="card"><h3>Engagement risk assessment</h3><div class="grid c3">' +
        ['new_client', 'complex_structure', 'cash_intensive', 'going_concern_doubt', 'aggressive_deadlines', 'unusual_fee_pressure'].map(k =>
          '<label class="check"><input type="checkbox" data-pm-risk="' + k + '"' + ((a.risk_factors || {})[k] ? ' checked' : '') + '> ' + esc(k.replace(/_/g, ' ')) + '</label>').join('') +
        '</div><div class="field" style="max-width:220px"><label>Overall risk level</label><select id="pm-risk_level">' +
        ['', 'Low', 'Medium', 'High'].map(v => '<option value="' + v + '"' + (a.risk_level === v ? ' selected' : '') + '>' + (v || '— choose —') + '</option>').join('') + '</select></div>' +
        '<label>Notes</label><textarea id="pm-risk_notes" rows="2" style="width:100%">' + esc(a.risk_notes || '') + '</textarea></div>' +

        '<div class="card"><h3>Acceptance decision</h3><div class="grid c2">' +
        '<div class="field"><label>Decision</label><select id="pm-acceptance_decision">' +
        [['pending', 'Pending'], ['accepted', 'Accepted'], ['accepted_with_conditions', 'Accepted with conditions'], ['declined', 'Declined']]
          .map(o => '<option value="' + o[0] + '"' + ((a.acceptance_decision || 'pending') === o[0] ? ' selected' : '') + '>' + o[1] + '</option>').join('') + '</select></div></div>' +
        '<label>Conditions (if any)</label><textarea id="pm-acceptance_conditions" rows="2" style="width:100%">' + esc(a.acceptance_conditions || '') + '</textarea>' +
        (a.decided_by ? '<p class="card-note" style="margin-top:8px">Last decided ' + esc(a.decided_at || '') + '</p>' : '') +
        '<button class="btn btn-primary" id="pm-acceptance-save" style="margin-top:10px">Save</button></div>';

      let conflictMatches = a.conflict_matches || [];
      const searchBox = document.getElementById('pm-conflict-search');
      let searchTimer;
      searchBox.addEventListener('input', () => {
        clearTimeout(searchTimer);
        searchTimer = setTimeout(async () => {
          const q = searchBox.value.trim();
          const results = document.getElementById('pm-conflict-results');
          if (q.length < 2) { results.innerHTML = ''; return; }
          try {
            const rows = await api('/api/pm/conflict-search?q=' + encodeURIComponent(q) + '&exclude_client_id=' + pmState.clientId);
            conflictMatches = rows;
            results.innerHTML = rows.length
              ? '<div class="table-wrap"><table><thead><tr><th>Name</th><th>Code</th></tr></thead><tbody>' +
                rows.map(r => '<tr><td>' + esc(r.name) + '</td><td>' + esc(r.code || '') + '</td></tr>').join('') + '</tbody></table></div>'
              : '<div class="empty">No matches among existing clients.</div>';
          } catch (e) { /* non-critical */ }
        }, 300);
      });

      document.getElementById('pm-acceptance-save').addEventListener('click', async () => {
        const body = {
          kyc_identity_verified: document.getElementById('pm-kyc_identity_verified').checked,
          kyc_beneficial_ownership: document.getElementById('pm-kyc_beneficial_ownership').checked,
          kyc_source_of_funds: document.getElementById('pm-kyc_source_of_funds').checked,
          kyc_pep_check: document.getElementById('pm-kyc_pep_check').checked,
          kyc_notes: document.getElementById('pm-kyc_notes').value,
          conflict_search_names: document.getElementById('pm-conflict_search_names').value,
          conflict_matches: conflictMatches,
          conflict_decision: document.getElementById('pm-conflict_decision').value,
          conflict_notes: document.getElementById('pm-conflict_notes').value,
          risk_factors: Object.fromEntries(Array.from(root.querySelectorAll('[data-pm-risk]')).map(el => [el.dataset.pmRisk, el.checked])),
          risk_level: document.getElementById('pm-risk_level').value,
          risk_notes: document.getElementById('pm-risk_notes').value,
          acceptance_decision: document.getElementById('pm-acceptance_decision').value,
          acceptance_conditions: document.getElementById('pm-acceptance_conditions').value,
        };
        try { await api('/api/pm/clients/' + pmState.clientId + '/acceptance', { method: 'PUT', body }); toast('Saved'); pmRenderAcceptanceTab(root); }
        catch (e) { toast(e.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  const PM_PROPOSAL_TYPE_LABELS = { proposal: 'Proposal', fee_quote: 'Fee quotation', engagement_letter: 'Engagement letter' };

  function pmRenderProposalsTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/pm/clients/' + pmState.clientId + '/proposals').then(rows => {
      root.innerHTML = '<div class="card"><h3>Proposals, fee quotations &amp; engagement letters</h3>' +
        (rows.length ? '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Title</th><th class="num">Fee</th><th>Valid until</th><th>Status</th><th></th></tr></thead><tbody>' +
          rows.map(p => '<tr><td>' + esc(PM_PROPOSAL_TYPE_LABELS[p.type] || p.type) + '</td><td>' + esc(p.title || '') + '</td>' +
            '<td class="num">' + money(p.fee_amount) + '</td><td>' + esc(p.validity_date || '—') + '</td>' +
            '<td><select data-pm-prop-status="' + p.id + '"><option value="draft"' + (p.status === 'draft' ? ' selected' : '') + '>Draft</option>' +
            '<option value="sent"' + (p.status === 'sent' ? ' selected' : '') + '>Sent</option>' +
            '<option value="accepted"' + (p.status === 'accepted' ? ' selected' : '') + '>Accepted</option>' +
            '<option value="declined"' + (p.status === 'declined' ? ' selected' : '') + '>Declined</option>' +
            '<option value="expired"' + (p.status === 'expired' ? ' selected' : '') + '>Expired</option></select></td>' +
            '<td><button class="btn btn-small" data-pm-prop-doc="' + p.id + '" title="Generate the Word document">Document</button> ' +
            '<button class="btn btn-small btn-danger" data-pm-prop-del="' + p.id + '" title="Delete this record">Delete</button></td></tr>').join('') + '</tbody></table></div>'
          : '<div class="empty">None yet.</div>') + '</div>' +
        '<div class="card"><h3>New</h3><div class="grid c3">' +
        '<div class="field"><label>Type</label><select id="pm-new-prop-type">' +
        Object.entries(PM_PROPOSAL_TYPE_LABELS).map(([k, v]) => '<option value="' + k + '">' + v + '</option>').join('') + '</select></div>' +
        '<div class="field"><label>Title</label><input id="pm-new-prop-title"></div>' +
        '<div class="field"><label>Fee amount (NPR)</label><input type="number" id="pm-new-prop-fee"></div>' +
        '<div class="field"><label>Fee structure</label><input id="pm-new-prop-structure" placeholder="Fixed / hourly / retainer"></div>' +
        '<div class="field"><label>Valid until</label><input type="date" id="pm-new-prop-validity"></div>' +
        '<div class="field"><label>Engagement (optional)</label><select id="pm-new-prop-eng">' + pmEngagementOptions('') + '</select></div>' +
        '</div><div class="field"><label>Scope of services</label><textarea id="pm-new-prop-scope" rows="3" style="width:100%"></textarea></div>' +
        '<button class="btn btn-primary" id="pm-add-prop" style="margin-top:8px">Create</button></div>';

      root.querySelectorAll('[data-pm-prop-status]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/pm/proposals/' + e.target.dataset.pmPropStatus, { method: 'PUT', body: { status: e.target.value } }); toast('Saved'); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-pm-prop-del]').forEach(el => el.addEventListener('click', async e => {
        if (!confirm('Delete this record?')) return;
        try { await api('/api/pm/proposals/' + e.target.dataset.pmPropDel, { method: 'DELETE' }); pmRenderProposalsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-pm-prop-doc]').forEach(el => el.addEventListener('click', async e => {
        try { const r = await api('/api/pm/proposals/' + e.target.dataset.pmPropDoc + '/document', { method: 'POST', body: { format: 'docx' } }); toast('Generated ' + r.filename); window.open(r.download_url, '_blank'); }
        catch (err) { toast(err.message, true); }
      }));
      document.getElementById('pm-add-prop').addEventListener('click', async () => {
        const body = {
          type: document.getElementById('pm-new-prop-type').value, title: document.getElementById('pm-new-prop-title').value,
          fee_amount: document.getElementById('pm-new-prop-fee').value || null, fee_structure: document.getElementById('pm-new-prop-structure').value,
          validity_date: document.getElementById('pm-new-prop-validity').value, engagement_id: document.getElementById('pm-new-prop-eng').value || null,
          scope_of_services: document.getElementById('pm-new-prop-scope').value,
        };
        try { await api('/api/pm/clients/' + pmState.clientId + '/proposals', { method: 'POST', body }); toast('Created'); pmRenderProposalsTab(root); }
        catch (e) { toast(e.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function pmRenderAssignmentsTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/pm/clients/' + pmState.clientId + '/assignments').then(rows => {
      root.innerHTML = '<div class="card"><h3>Assignments</h3>' +
        (rows.length ? '<div class="table-wrap"><table><thead><tr><th>Task</th><th>Assigned to</th><th>Due</th><th>Priority</th><th>Status</th><th></th></tr></thead><tbody>' +
          rows.map(a => '<tr><td>' + esc(a.task) + '</td>' +
            '<td><select data-pm-asg="' + a.id + '" data-k="assigned_to">' + pmStaffOptions(a.assigned_to) + '</select></td>' +
            '<td><input type="date" data-pm-asg="' + a.id + '" data-k="due_date" value="' + esc(a.due_date || '') + '"></td>' +
            '<td><select data-pm-asg="' + a.id + '" data-k="priority">' + ['low', 'normal', 'high', 'urgent'].map(p => '<option value="' + p + '"' + (a.priority === p ? ' selected' : '') + '>' + p + '</option>').join('') + '</select></td>' +
            '<td><select data-pm-asg="' + a.id + '" data-k="status">' + ['pending', 'in_progress', 'done'].map(s => '<option value="' + s + '"' + (a.status === s ? ' selected' : '') + '>' + s.replace('_', ' ') + '</option>').join('') + '</select></td>' +
            '<td><button class="btn btn-small btn-danger" data-pm-del-asg="' + a.id + '" title="Remove">Remove</button></td></tr>').join('') + '</tbody></table></div>'
          : '<div class="empty">No assignments yet.</div>') + '</div>' +
        '<div class="card"><h3>Add assignment</h3><div class="grid c3">' +
        '<div class="field"><label>Task</label><input id="pm-new-asg-task"></div>' +
        '<div class="field"><label>Assigned to</label><select id="pm-new-asg-staff">' + pmStaffOptions('') + '</select></div>' +
        '<div class="field"><label>Due date</label><input type="date" id="pm-new-asg-due"></div>' +
        '<div class="field"><label>Priority</label><select id="pm-new-asg-priority">' + ['low', 'normal', 'high', 'urgent'].map(p => '<option value="' + p + '"' + (p === 'normal' ? ' selected' : '') + '>' + p + '</option>').join('') + '</select></div>' +
        '<div class="field"><label>Engagement (optional)</label><select id="pm-new-asg-eng">' + pmEngagementOptions('') + '</select></div>' +
        '</div><button class="btn btn-small" id="pm-add-asg" style="margin-top:8px">Add</button></div>';

      root.querySelectorAll('[data-pm-asg]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k; let v = e.target.value;
        if (k === 'assigned_to') v = v === '' ? null : Number(v);
        try { await api('/api/pm/assignments/' + e.target.dataset.pmAsg, { method: 'PUT', body: { [k]: v } }); toast('Saved'); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-pm-del-asg]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/pm/assignments/' + e.target.dataset.pmDelAsg, { method: 'DELETE' }); pmRenderAssignmentsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      document.getElementById('pm-add-asg').addEventListener('click', async () => {
        const task = document.getElementById('pm-new-asg-task').value.trim();
        if (!task) { toast('A task description is required', true); return; }
        const body = { task, assigned_to: document.getElementById('pm-new-asg-staff').value || null,
          due_date: document.getElementById('pm-new-asg-due').value, priority: document.getElementById('pm-new-asg-priority').value,
          engagement_id: document.getElementById('pm-new-asg-eng').value || null };
        try { await api('/api/pm/clients/' + pmState.clientId + '/assignments', { method: 'POST', body }); pmRenderAssignmentsTab(root); }
        catch (e) { toast(e.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function pmRenderBudgetTab(root) {
    const engs = pmClientEngagements();
    if (!engs.length) { root.innerHTML = '<div class="empty"><b>No engagements for this client yet</b>Time budgets are set per engagement — create one from Client Directory first.</div>'; return; }
    if (!pmState.budgetEngId || !engs.find(e => String(e.id) === String(pmState.budgetEngId))) pmState.budgetEngId = engs[0].id;
    root.innerHTML = '<div class="field" style="max-width:320px"><label>Engagement</label><select id="pm-budget-eng">' +
      engs.map(e => '<option value="' + e.id + '"' + (String(e.id) === String(pmState.budgetEngId) ? ' selected' : '') + '>' + esc(e.title || e.type) + '</option>').join('') +
      '</select></div><div id="pm-budget-body" style="margin-top:12px"></div>';
    document.getElementById('pm-budget-eng').addEventListener('change', e => { pmState.budgetEngId = e.target.value; pmRenderBudgetTab(root); });
    loadBudget();

    function loadBudget() {
      const bbody = document.getElementById('pm-budget-body');
      bbody.innerHTML = '<div class="empty">Loading…</div>';
      api('/api/pm/engagements/' + pmState.budgetEngId + '/time-budget').then(d => {
        bbody.innerHTML = '<div class="card"><h3>Budget vs actual</h3>' +
          (d.budgets.length ? '<div class="table-wrap"><table><thead><tr><th>Task</th><th>Staff</th><th class="num">Budgeted</th><th class="num">Actual</th><th class="num">Variance</th><th></th></tr></thead><tbody>' +
            d.budgets.map(b => '<tr><td>' + esc(b.task) + '</td><td>' + esc(b.staff_name || 'Whole engagement') + '</td>' +
              '<td class="num">' + b.budgeted_hours + '</td><td class="num">' + b.actual_hours + '</td>' +
              '<td class="num">' + (b.budgeted_hours - b.actual_hours) + '</td>' +
              '<td><button class="btn btn-small btn-danger" data-pm-del-budget="' + b.id + '" title="Remove">Remove</button></td></tr>').join('') + '</tbody></table></div>' +
            '<p class="card-note" style="margin-top:8px">Total budgeted: ' + d.budgeted_total + 'h · Total actual (whole engagement): ' + d.actual_total + 'h</p>'
            : '<div class="empty">No budget lines yet.</div>') + '</div>' +
          '<div class="card"><h3>Add budget line</h3><div class="grid c3">' +
          '<div class="field"><label>Task</label><input id="pm-new-budget-task"></div>' +
          '<div class="field"><label>Staff (optional — blank = whole engagement)</label><select id="pm-new-budget-staff">' + pmStaffOptions('') + '</select></div>' +
          '<div class="field"><label>Budgeted hours</label><input type="number" id="pm-new-budget-hours"></div>' +
          '</div><button class="btn btn-small" id="pm-add-budget" style="margin-top:8px">Add</button></div>';

        bbody.querySelectorAll('[data-pm-del-budget]').forEach(el => el.addEventListener('click', async e => {
          try { await api('/api/pm/time-budget/' + e.target.dataset.pmDelBudget, { method: 'DELETE' }); loadBudget(); }
          catch (err) { toast(err.message, true); }
        }));
        document.getElementById('pm-add-budget').addEventListener('click', async () => {
          const task = document.getElementById('pm-new-budget-task').value.trim();
          const hours = document.getElementById('pm-new-budget-hours').value;
          if (!task || !hours) { toast('Task and budgeted hours are required', true); return; }
          const body = { task, budgeted_hours: Number(hours), staff_id: document.getElementById('pm-new-budget-staff').value || null };
          try { await api('/api/pm/engagements/' + pmState.budgetEngId + '/time-budget', { method: 'POST', body }); loadBudget(); }
          catch (e) { toast(e.message, true); }
        });
      }).catch(e => { bbody.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }
  }

  function pmRenderClosureTab(root) {
    const engs = pmClientEngagements();
    pmRenderChecklistTab(root, 'closure');
    if (engs.length) {
      const extra = document.createElement('div');
      extra.className = 'card';
      extra.innerHTML = '<h3>Close an engagement</h3><div class="grid c2">' +
        '<div class="field"><label>Engagement</label><select id="pm-close-eng">' +
        engs.filter(e => e.status !== 'closed').map(e => '<option value="' + e.id + '">' + esc(e.title || e.type) + '</option>').join('') + '</select></div></div>' +
        (engs.every(e => e.status === 'closed') ? '<p class="card-note">Every engagement for this client is already closed.</p>' :
        '<button class="btn btn-primary" id="pm-close-go" style="margin-top:8px" title="Sets the engagement status to closed">Mark closed</button>');
      root.appendChild(extra);
      const closeBtn = document.getElementById('pm-close-go');
      if (closeBtn) closeBtn.addEventListener('click', async () => {
        const engId = document.getElementById('pm-close-eng').value;
        if (!confirm('Close this engagement? This can be reopened later from Client Directory if needed.')) return;
        try {
          await api('/api/engagements/' + engId, { method: 'PUT', body: { status: 'closed' } });
          toast('Engagement closed');
          const boot = await api('/api/bootstrap'); state.boot = boot; await idbPut('kv', boot, 'boot');
          pmRenderClosureTab(root);
        } catch (e) { toast(e.message, true); }
      });
    }
  }

  // ──────────────────────────────────────────── forensic audit & investigation
  // Sixteen named tools, a handful of real algorithms underneath — see the
  // comment above fa_dataset in schema.sql and the module docstring in
  // forensic_engines.py. Relational, client-scoped, tab-based, same posture
  // as Transfer Pricing and Practice Management.
  const FA_TABS = [
    ['dashboard', 'Dashboard'], ['datasets', 'Datasets'], ['analyze', 'Run detectors'],
    ['findings', 'Findings register'], ['beneish', 'Financial statement (Beneish)'],
    ['papers', 'Working papers'], ['reports', 'Reports']
  ];
  const faState = { clientId: '', tab: 'dashboard', selectedDatasetId: null };

  const FA_DATASET_TYPE_LABELS = {
    journal_entries: 'Journal entries — feeds Benford\'s Law, Round Number and Unusual Journal Entry detectors',
    payments: 'Payments — feeds Benford\'s Law, Duplicate Payment and Round Number detectors',
    invoices: 'Invoices — feeds Benford\'s Law, Duplicate Invoice and Round Number detectors',
    payroll: 'Payroll register — feeds Ghost Employee and Payroll Fraud detectors',
    vendors: 'Vendor master — feeds Vendor Fraud detector (optionally cross-checked against a payroll dataset)',
    expenses: 'Expense claims — feeds Expense Fraud detector',
  };
  const FA_DETECTOR_LABELS = {
    benford: "Benford's Law Analyzer", duplicate_payment: 'Duplicate Payment Detector', duplicate_invoice: 'Duplicate Invoice Detector',
    round_number: 'Round Number Transaction Detector', related_party: 'Related Party Fraud Detector',
    unusual_je: 'Unusual Journal Entry Detector', ghost_employee: 'Ghost Employee Detector', payroll_fraud: 'Payroll Fraud Analyzer',
    vendor_fraud: 'Vendor Fraud Analyzer', expense_fraud: 'Expense Fraud Analyzer', revenue_manipulation: 'Revenue Manipulation Detector',
    cash_leakage: 'Cash Leakage Detector', beneish_m_score: 'Financial Statement Fraud Analyzer (Beneish M-Score)',
  };
  const FA_SEVERITY_TAG = { High: 'tag-red', Medium: 'tag-amber', Low: 'tag' };

  function renderFA() {
    const view = document.getElementById('view');
    if (!state.online || !state.token || state.offlineMode) {
      view.innerHTML = '<div class="page-head"><div><h2>Forensic Audit &amp; Investigation</h2></div></div>' +
        '<div class="card"><div class="empty"><b>Offline</b>This module works against live server data — reconnect to use it.</div></div>';
      return;
    }
    if (!faState.clientId) faState.clientId = localStorage.getItem('ca.client.fa') || (clients()[0] && clients()[0].id) || '';

    view.innerHTML =
      '<div class="page-head"><div><h2>Forensic Audit &amp; Investigation</h2>' +
      '<p>Upload transaction data and run fraud-screening detectors — Benford\'s Law, duplicate payments/invoices, round numbers, related-party transactions, unusual journal entries, ghost employees, payroll/vendor/expense fraud, plus a Beneish M-Score screen on the financial statements. Every result is a screening signal for professional follow-up, never a finding of fraud on its own.</p></div></div>' +
      '<div class="card"><div class="grid c3">' +
      '<div class="field"><label>Client</label><select id="fa-client"><option value="">— choose —</option>' +
      clients().map(c => '<option value="' + c.id + '"' + (String(faState.clientId) === String(c.id) ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') + '</select></div>' +
      '</div></div>' +
      '<div class="card" id="fa-body"><div class="empty">Loading…</div></div>';

    document.getElementById('fa-client').addEventListener('change', e => {
      faState.clientId = e.target.value; localStorage.setItem('ca.client.fa', faState.clientId);
      faState.tab = 'dashboard'; faState.selectedDatasetId = null;
      faRenderBody();
    });
    faRenderBody();
  }

  function faRenderBody() {
    const body = document.getElementById('fa-body');
    if (!faState.clientId) { body.innerHTML = '<div class="empty"><b>Choose a client</b>Pick a client above to begin.</div>'; return; }
    body.innerHTML = '<div style="margin-bottom:14px">' +
      FA_TABS.map(t => '<button class="btn btn-small' + (faState.tab === t[0] ? ' btn-primary' : ' btn-ghost') +
        '" data-fa-tab="' + t[0] + '" style="margin:0 6px 6px 0">' + esc(t[1]) + '</button>').join('') +
      '</div><div id="fa-tab-body"></div>';
    body.querySelectorAll('[data-fa-tab]').forEach(b => b.addEventListener('click', e => {
      faState.tab = e.target.dataset.faTab; faRenderBody();
    }));
    const tb = document.getElementById('fa-tab-body');
    ({ dashboard: faRenderDashboardTab, datasets: faRenderDatasetsTab, analyze: faRenderAnalyzeTab,
       findings: faRenderFindingsTab, beneish: faRenderBeneishTab, papers: faRenderPapersTab,
       reports: faRenderReportsTab }[faState.tab] || faRenderDashboardTab)(tb);
  }

  function faRenderDashboardTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/fa/clients/' + faState.clientId + '/dashboard').then(d => {
      root.innerHTML = '<div class="grid c4">' +
        kpi('Datasets uploaded', d.datasets) +
        kpi('Open findings', d.open_findings, d.open_findings ? 'alert' : '') +
        kpi('High severity', d.by_severity.High, d.by_severity.High ? 'alert' : '') +
        kpi('Medium severity', d.by_severity.Medium) +
        '</div>' +
        '<div class="card"><h3>Findings by detector</h3>' +
        (d.by_detector.length ? '<div class="table-wrap"><table><thead><tr><th>Detector</th><th class="num">Findings</th></tr></thead><tbody>' +
          d.by_detector.map(r => '<tr><td>' + esc(FA_DETECTOR_LABELS[r.detector] || r.detector) + '</td><td class="num">' + r.c + '</td></tr>').join('') + '</tbody></table></div>'
          : '<div class="empty">Nothing analysed yet — upload a dataset and run detectors.</div>') + '</div>';
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function faRenderDatasetsTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/fa/clients/' + faState.clientId + '/datasets').then(rows => {
      root.innerHTML = '<div class="card"><h3>Datasets</h3>' +
        (rows.length ? '<div class="table-wrap"><table><thead><tr><th>Name</th><th>Type</th><th class="num">Rows</th><th>Uploaded</th><th></th></tr></thead><tbody>' +
          rows.map(d => '<tr><td>' + esc(d.name) + '</td><td>' + esc(d.dataset_type.replace('_', ' ')) + '</td><td class="num">' + d.row_count + '</td>' +
            '<td>' + esc(d.uploaded_at) + '</td><td><button class="btn btn-small" data-fa-analyze-ds="' + d.id + '" title="Go run detectors on this dataset">Analyze</button> ' +
            '<button class="btn btn-small btn-danger" data-fa-del-ds="' + d.id + '" title="Delete this dataset and its transactions">Delete</button></td></tr>').join('') + '</tbody></table></div>'
          : '<div class="empty">No datasets uploaded yet.</div>') + '</div>' +
        '<div class="card"><h3>Upload a dataset</h3>' +
        '<div class="grid c3">' +
        '<div class="field"><label>Name</label><input id="fa-new-ds-name" placeholder="e.g. Payments Q1 2083-84"></div>' +
        '<div class="field"><label>Type</label><select id="fa-new-ds-type">' +
        Object.entries(FA_DATASET_TYPE_LABELS).map(([k, v]) => '<option value="' + k + '" title="' + esc(v) + '">' + k.replace('_', ' ') + '</option>').join('') + '</select></div>' +
        '<div class="field"><label>Period (optional)</label><input id="fa-new-ds-period" placeholder="e.g. FY2082-83"></div>' +
        '</div>' +
        '<p class="card-note" id="fa-ds-type-hint">' + esc(Object.values(FA_DATASET_TYPE_LABELS)[0]) + '</p>' +
        '<p class="card-note">CSV columns, in this exact order (a header row is fine, it\'s skipped automatically): <code>date, reference_no, party, description, amount, account, actor</code>' +
        ' — plus, for payroll/vendors only: <code>pan_no, bank_account, address</code>.</p>' +
        '<div class="field"><label>Choose a CSV file</label><input type="file" accept=".csv,text/csv" id="fa-new-ds-file"></div>' +
        '<p class="card-note">…or paste CSV text directly:</p>' +
        '<textarea id="fa-new-ds-csv" rows="6" style="width:100%;font-family:var(--mono, monospace)" placeholder="date,reference_no,party,description,amount,account,actor"></textarea>' +
        '<button class="btn btn-primary" id="fa-upload-ds" style="margin-top:8px">Upload</button> ' +
        '<span class="hint" id="fa-upload-msg"></span></div>';

      document.getElementById('fa-new-ds-type').addEventListener('change', e => {
        document.getElementById('fa-ds-type-hint').textContent = FA_DATASET_TYPE_LABELS[e.target.value];
      });
      document.getElementById('fa-new-ds-file').addEventListener('change', e => {
        const file = e.target.files[0];
        if (!file) return;
        const reader = new FileReader();
        reader.onload = () => { document.getElementById('fa-new-ds-csv').value = reader.result; };
        reader.readAsText(file);
      });
      root.querySelectorAll('[data-fa-analyze-ds]').forEach(el => el.addEventListener('click', e => {
        faState.selectedDatasetId = Number(e.target.dataset.faAnalyzeDs); faState.tab = 'analyze'; faRenderBody();
      }));
      root.querySelectorAll('[data-fa-del-ds]').forEach(el => el.addEventListener('click', async e => {
        if (!confirm('Delete this dataset? Its transactions and any linked findings will be removed too.')) return;
        try { await api('/api/fa/datasets/' + e.target.dataset.faDelDs, { method: 'DELETE' }); faRenderDatasetsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      document.getElementById('fa-upload-ds').addEventListener('click', async () => {
        const name = document.getElementById('fa-new-ds-name').value.trim();
        const csv_text = document.getElementById('fa-new-ds-csv').value;
        const msg = document.getElementById('fa-upload-msg');
        msg.textContent = '';
        if (!name || !csv_text.trim()) { msg.textContent = 'A name and some CSV data are required'; return; }
        try {
          const ds = await api('/api/fa/clients/' + faState.clientId + '/datasets', { method: 'POST', body: {
            name, dataset_type: document.getElementById('fa-new-ds-type').value,
            period_label: document.getElementById('fa-new-ds-period').value, csv_text } });
          toast(ds.row_count + ' rows uploaded');
          faState.selectedDatasetId = ds.id;
          faRenderDatasetsTab(root);
        } catch (e) { msg.textContent = e.message; }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function faRenderAnalyzeTab(root) {
    api('/api/fa/clients/' + faState.clientId + '/datasets').then(datasets => {
      if (!datasets.length) { root.innerHTML = '<div class="empty"><b>No datasets yet</b>Upload one on the Datasets tab first.</div>'; return; }
      if (!faState.selectedDatasetId || !datasets.find(d => d.id === faState.selectedDatasetId)) faState.selectedDatasetId = datasets[0].id;
      const ds = datasets.find(d => d.id === faState.selectedDatasetId);
      root.innerHTML = '<div class="card"><div class="grid c2">' +
        '<div class="field"><label>Dataset</label><select id="fa-analyze-ds-pick">' +
        datasets.map(d => '<option value="' + d.id + '"' + (d.id === faState.selectedDatasetId ? ' selected' : '') + '>' + esc(d.name) + ' (' + esc(d.dataset_type.replace('_', ' ')) + ')</option>').join('') + '</select></div>' +
        (ds.dataset_type === 'vendors' ? '<div class="field"><label>Cross-check against payroll dataset (optional — for fictitious-vendor matching)</label><select id="fa-analyze-related">' +
          '<option value="">— none —</option>' + datasets.filter(d => d.dataset_type === 'payroll').map(d => '<option value="' + d.id + '">' + esc(d.name) + '</option>').join('') + '</select></div>' : '<div></div>') +
        '</div>' +
        '<p class="card-note">' + esc(FA_DATASET_TYPE_LABELS[ds.dataset_type]) + '</p>' +
        (ds.dataset_type === 'related_needs_names' ? '' : '') +
        '<div class="grid c3">' +
        '<div class="field"><label>Round-to (for Round Number detector)</label><input type="number" id="fa-param-round-to" value="1000"></div>' +
        '<div class="field"><label>Minimum amount</label><input type="number" id="fa-param-min-amount" value="10000"></div>' +
        (ds.dataset_type === 'expenses' ? '<div class="field"><label>Approval threshold</label><input type="number" id="fa-param-threshold"></div>' : '<div></div>') +
        '</div>' +
        '<div class="field"><label>Related-party names (comma-separated, optional — for Related Party Fraud Detector)</label><input id="fa-param-related-names" style="width:100%"></div>' +
        '<button class="btn btn-primary" id="fa-run-analyze" style="margin-top:8px">Run detectors</button> ' +
        '<span class="hint" id="fa-analyze-msg"></span>' +
        '<div id="fa-analyze-result" style="margin-top:10px"></div></div>';

      document.getElementById('fa-analyze-ds-pick').addEventListener('change', e => {
        faState.selectedDatasetId = Number(e.target.value); faRenderAnalyzeTab(root);
      });
      document.getElementById('fa-run-analyze').addEventListener('click', async () => {
        const relatedNames = document.getElementById('fa-param-related-names').value.split(',').map(s => s.trim()).filter(Boolean);
        const params = { round_to: Number(document.getElementById('fa-param-round-to').value) || 1000,
          min_amount: Number(document.getElementById('fa-param-min-amount').value) || 10000, related_party_names: relatedNames };
        const thresholdEl = document.getElementById('fa-param-threshold');
        if (thresholdEl && thresholdEl.value) params.approval_threshold = Number(thresholdEl.value);
        const relatedEl = document.getElementById('fa-analyze-related');
        const body = { params };
        if (relatedEl && relatedEl.value) body.related_dataset_id = Number(relatedEl.value);
        const resultEl = document.getElementById('fa-analyze-result');
        resultEl.innerHTML = '<div class="empty">Running…</div>';
        try {
          const res = await api('/api/fa/datasets/' + faState.selectedDatasetId + '/analyze', { method: 'POST', body });
          toast(res.count + ' findings recorded');
          resultEl.innerHTML = res.findings.length
            ? '<div class="table-wrap"><table><thead><tr><th>Detector</th><th>Severity</th><th>Title</th><th>Description</th></tr></thead><tbody>' +
              res.findings.map(f => '<tr><td>' + esc(FA_DETECTOR_LABELS[f.detector] || f.detector) + '</td>' +
                '<td><span class="tag ' + (FA_SEVERITY_TAG[f.severity] || 'tag') + '">' + esc(f.severity) + '</span></td>' +
                '<td>' + esc(f.title) + '</td><td>' + esc(f.description || '') + '</td></tr>').join('') + '</tbody></table></div>'
            : '<div class="empty">No findings — nothing tripped the checks that apply to this dataset type.</div>';
        } catch (e) { document.getElementById('fa-analyze-msg').textContent = e.message; resultEl.innerHTML = ''; }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function faRenderFindingsTab(root) {
    root.innerHTML = '<div class="grid c3" style="margin-bottom:10px">' +
      '<div class="field"><label>Severity</label><select id="fa-filter-severity"><option value="">All</option><option>High</option><option>Medium</option><option>Low</option></select></div>' +
      '<div class="field"><label>Status</label><select id="fa-filter-status"><option value="">All</option><option value="open">Open</option><option value="reviewed">Reviewed</option><option value="confirmed">Confirmed</option><option value="false_positive">False positive</option></select></div>' +
      '</div><div id="fa-findings-list"><div class="empty">Loading…</div></div>';
    document.getElementById('fa-filter-severity').addEventListener('change', load);
    document.getElementById('fa-filter-status').addEventListener('change', load);
    load();

    function load() {
      const severity = document.getElementById('fa-filter-severity').value;
      const status = document.getElementById('fa-filter-status').value;
      const parts = []; if (severity) parts.push('severity=' + severity); if (status) parts.push('status=' + status);
      const qs = parts.length ? '?' + parts.join('&') : '';
      const list = document.getElementById('fa-findings-list');
      list.innerHTML = '<div class="empty">Loading…</div>';
      api('/api/fa/clients/' + faState.clientId + '/findings' + qs).then(rows => {
        list.innerHTML = rows.length ? '<div class="table-wrap"><table><thead><tr><th>Detector</th><th>Severity</th><th>Title</th><th>Description</th><th>Status</th><th>Notes</th></tr></thead><tbody>' +
          rows.map(f => '<tr><td>' + esc(FA_DETECTOR_LABELS[f.detector] || f.detector) + '</td>' +
            '<td><span class="tag ' + (FA_SEVERITY_TAG[f.severity] || 'tag') + '">' + esc(f.severity) + '</span></td>' +
            '<td>' + esc(f.title) + '</td><td>' + esc(f.description || '') + '</td>' +
            '<td><select data-fa-finding-status="' + f.id + '"><option value="open"' + (f.status === 'open' ? ' selected' : '') + '>Open</option>' +
            '<option value="reviewed"' + (f.status === 'reviewed' ? ' selected' : '') + '>Reviewed</option>' +
            '<option value="confirmed"' + (f.status === 'confirmed' ? ' selected' : '') + '>Confirmed</option>' +
            '<option value="false_positive"' + (f.status === 'false_positive' ? ' selected' : '') + '>False positive</option></select></td>' +
            '<td><input data-fa-finding-notes="' + f.id + '" value="' + esc(f.notes || '') + '" placeholder="Add a note…"></td></tr>').join('') + '</tbody></table></div>'
          : '<div class="empty">No findings match this filter.</div>';
        list.querySelectorAll('[data-fa-finding-status]').forEach(el => el.addEventListener('change', async e => {
          try { await api('/api/fa/findings/' + e.target.dataset.faFindingStatus, { method: 'PUT', body: { status: e.target.value } }); toast('Saved'); }
          catch (err) { toast(err.message, true); }
        }));
        list.querySelectorAll('[data-fa-finding-notes]').forEach(el => el.addEventListener('change', async e => {
          try { await api('/api/fa/findings/' + e.target.dataset.faFindingNotes, { method: 'PUT', body: { notes: e.target.value } }); toast('Saved'); }
          catch (err) { toast(err.message, true); }
        }));
      }).catch(e => { list.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }
  }

  function faRenderBeneishTab(root) {
    const fields = ['receivables', 'sales', 'cogs', 'current_assets', 'ppe', 'total_assets', 'depreciation',
                    'sga', 'current_liabilities', 'total_debt', 'net_income', 'cfo'];
    const labels = { receivables: 'Receivables', sales: 'Sales / revenue', cogs: 'Cost of goods sold',
      current_assets: 'Current assets', ppe: 'Net PP&E', total_assets: 'Total assets', depreciation: 'Depreciation',
      sga: 'SG&A expenses', current_liabilities: 'Current liabilities', total_debt: 'Total debt', net_income: 'Net income', cfo: 'Cash flow from operations' };
    root.innerHTML = '<div class="card"><h3>Financial Statement Fraud Analyzer — Beneish M-Score</h3>' +
      '<p class="card-note">Beneish (1999)\'s published 8-ratio earnings-manipulation screen. This uses your own two years of figures — nothing here is a statutory rate, so nothing is UNVERIFIED the way a tax figure would be. Result is a screening signal, never a conclusion.</p>' +
      '<div class="table-wrap"><table><thead><tr><th>Figure</th><th>Current year</th><th>Prior year</th></tr></thead><tbody>' +
      fields.map(f => '<tr><td>' + esc(labels[f]) + '</td>' +
        '<td class="num"><input type="number" id="fa-ben-curr-' + f + '"></td>' +
        '<td class="num"><input type="number" id="fa-ben-prior-' + f + '"></td></tr>').join('') + '</tbody></table></div>' +
      '<button class="btn btn-primary" id="fa-ben-run" style="margin-top:8px">Compute M-Score</button> ' +
      '<label class="check" style="display:inline-flex;margin-left:10px"><input type="checkbox" id="fa-ben-save" checked> Save as a finding</label>' +
      '<div id="fa-ben-result" style="margin-top:10px"></div></div>';

    document.getElementById('fa-ben-run').addEventListener('click', async () => {
      const curr = {}, prior = {};
      fields.forEach(f => {
        const c = document.getElementById('fa-ben-curr-' + f).value, p = document.getElementById('fa-ben-prior-' + f).value;
        if (c !== '') curr[f] = Number(c);
        if (p !== '') prior[f] = Number(p);
      });
      const resultEl = document.getElementById('fa-ben-result');
      resultEl.innerHTML = '<div class="empty">Computing…</div>';
      try {
        const res = await api('/api/fa/clients/' + faState.clientId + '/beneish', { method: 'POST', body: { curr, prior, save: document.getElementById('fa-ben-save').checked } });
        if (res.m_score === null) { resultEl.innerHTML = '<div class="empty">' + esc(res.note) + '</div>'; return; }
        resultEl.innerHTML = '<div class="grid c4">' + kpi('M-Score', res.m_score, res.m_score > res.threshold ? 'alert' : '') +
          kpi('Threshold', res.threshold) + kpi('DSRI', res.components.dsri) + kpi('SGI', res.components.sgi) + '</div>' +
          '<p class="card-note" style="margin-top:8px"><b>' + esc(res.interpretation) + '</b></p>' +
          '<div class="table-wrap"><table><thead><tr>' + Object.keys(res.components).map(k => '<th>' + k.toUpperCase() + '</th>').join('') + '</tr></thead><tbody><tr>' +
          Object.values(res.components).map(v => '<td class="num">' + v + '</td>').join('') + '</tr></tbody></table></div>' +
          '<p class="card-note" style="margin-top:8px">' + esc(res.note) + '</p>';
        if (res.finding_id) toast('Saved as a finding');
      } catch (e) { resultEl.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; }
    });
  }

  function faRenderPapersTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/fa/clients/' + faState.clientId + '/working-papers').then(rows => {
      const STATUSES = ['open', 'approved', 'rejected', 'sent_back'];
      root.innerHTML = '<div class="card"><h3>Working papers</h3>' +
        '<div class="table-wrap"><table><thead><tr><th>Ref</th><th>Title</th><th>Prepared by</th><th>Review status</th><th></th></tr></thead><tbody>' +
        rows.map(w => '<tr><td>' + esc(w.ref) + '</td><td>' + esc(w.title || '') + '</td><td>' + esc(w.prepared_by_name || '') + '</td>' +
          '<td><select data-fa-wp-status="' + w.id + '">' + STATUSES.map(s => '<option value="' + s + '"' + (s === w.review_status ? ' selected' : '') + '>' + s.replace('_', ' ') + '</option>').join('') + '</select></td>' +
          '<td><button class="btn btn-small" data-fa-wp-view="' + w.id + '">View / edit</button></td></tr>').join('') + '</tbody></table></div>' +
        '<div class="grid c3" style="margin-top:10px">' +
        '<div class="field"><label>Ref</label><input id="fa-wp-ref" placeholder="FA-01"></div>' +
        '<div class="field"><label>Title</label><input id="fa-wp-title"></div></div>' +
        '<button class="btn btn-small" id="fa-wp-add" style="margin-top:8px" title="e.g. FA-01 scope, FA-02 detector results, FA-03 conclusion">Add working paper</button></div>' +
        '<div class="card" id="fa-wp-detail" hidden></div>';

      root.querySelectorAll('[data-fa-wp-status]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/fa/working-papers/' + e.target.dataset.faWpStatus, { method: 'PUT', body: { review_status: e.target.value } }); toast('Saved'); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-fa-wp-view]').forEach(el => el.addEventListener('click', () => {
        const w = rows.find(x => x.id === Number(el.dataset.faWpView));
        const d = root.querySelector('#fa-wp-detail');
        d.hidden = false;
        d.innerHTML = '<h3>' + esc(w.ref) + ' — ' + esc(w.title || '') + '</h3>' +
          '<div class="field"><label>Content</label><textarea id="fa-wp-content" rows="8">' + esc(w.content || '') + '</textarea></div>' +
          '<button class="btn btn-small btn-primary" id="fa-wp-save-content" style="margin-top:8px">Save content</button>';
        d.querySelector('#fa-wp-save-content').addEventListener('click', async () => {
          try { await api('/api/fa/working-papers/' + w.id, { method: 'PUT', body: { content: d.querySelector('#fa-wp-content').value } }); toast('Saved'); }
          catch (err) { toast(err.message, true); }
        });
      }));
      document.getElementById('fa-wp-add').addEventListener('click', async () => {
        const ref = document.getElementById('fa-wp-ref').value.trim(), title = document.getElementById('fa-wp-title').value.trim();
        if (!ref) { toast('A reference is required', true); return; }
        try { await api('/api/fa/clients/' + faState.clientId + '/working-papers', { method: 'POST', body: { ref, title } }); faRenderPapersTab(root); }
        catch (e) { toast(e.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function faRenderReportsTab(root) {
    const REPORT_TYPES = [['investigation_report', 'Investigation Report', ['xlsx', 'docx']], ['working_paper_pack', 'Working Paper Pack', ['docx']]];
    root.innerHTML = '<div class="card"><h3>Reports</h3><p class="card-note">Every report states that findings are screening signals for professional follow-up, never a conclusion of fraud.</p>' +
      REPORT_TYPES.map(rt => '<div style="margin-bottom:10px"><b>' + esc(rt[1]) + '</b> — ' +
        rt[2].map(fmt => '<button class="btn btn-small" data-fa-report="' + rt[0] + '" data-fmt="' + fmt + '">' + (fmt === 'xlsx' ? 'Excel' : 'Word') + '</button> ').join('') + '</div>').join('') + '</div>';
    root.querySelectorAll('[data-fa-report]').forEach(el => el.addEventListener('click', async e => {
      try {
        const res = await api('/api/fa/clients/' + faState.clientId + '/reports', { method: 'POST', body: {
          report_type: e.target.dataset.faReport, format: e.target.dataset.fmt } });
        toast('Generated ' + res.filename);
        window.open(res.download_url, '_blank');
      } catch (err) { toast(err.message, true); }
    }));
  }

  // ------------------------------------------------------------ corporate advisory
  const CA_TABS = [
    ['dashboard', 'Dashboard'], ['profile', 'Entity profile'], ['shareholders', 'Shareholding'],
    ['events', 'Capital events'], ['calculators', 'Calculators'], ['meetings', 'Board meetings'],
    ['resolutions', 'Resolutions'], ['checklist', 'Compliance checklist'], ['reports', 'Reports'],
  ];
  const caState = { clientId: '', tab: 'dashboard' };
  const CA_EVENT_TYPES = [
    ['allotment', 'Allotment'], ['rights_issue', 'Rights issue'], ['bonus_issue', 'Bonus issue'],
    ['private_placement', 'Private placement'], ['share_transfer', 'Share transfer'],
    ['capital_reduction', 'Capital reduction'], ['buyback', 'Buyback'],
  ];
  const CA_EVENT_LABEL = Object.fromEntries(CA_EVENT_TYPES);

  function renderCA() {
    const view = document.getElementById('view');
    if (!state.online || !state.token || state.offlineMode) {
      view.innerHTML = '<div class="page-head"><div><h2>Corporate Advisory</h2></div></div>' +
        '<div class="card"><div class="empty"><b>Offline</b>Corporate Advisory works against live server data — reconnect to use this module.</div></div>';
      return;
    }
    if (!caState.clientId) caState.clientId = localStorage.getItem('ca.client.corp') || (clients()[0] && clients()[0].id) || '';

    view.innerHTML =
      '<div class="page-head"><div><h2>Corporate Advisory</h2>' +
      '<p>Company-secretarial and Companies Act compliance — shareholding, capital events (rights/bonus/private placement/transfer/reduction), board meetings, board and shareholder resolutions, and a company-law compliance checklist, one client at a time. For the compliance calendar, use the deadline tracker; for the debt-vs-equity capital-structure decision, use Capital &amp; Debt Advisory.</p></div></div>' +
      '<div class="card"><div class="grid c3">' +
      '<div class="field"><label>Client</label><select id="ca-client"><option value="">— choose —</option>' +
      clients().map(c => '<option value="' + c.id + '"' + (String(caState.clientId) === String(c.id) ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') + '</select></div>' +
      '</div></div>' +
      '<div class="card" id="ca-body"><div class="empty">Loading…</div></div>';

    document.getElementById('ca-client').addEventListener('change', e => {
      caState.clientId = e.target.value; localStorage.setItem('ca.client.corp', caState.clientId);
      caState.tab = 'dashboard';
      caRenderBody();
    });
    caRenderBody();
  }

  function caRenderBody() {
    const body = document.getElementById('ca-body');
    if (!caState.clientId) { body.innerHTML = '<div class="empty"><b>Choose a client</b>Pick a client above to begin.</div>'; return; }
    body.innerHTML = '<div style="margin-bottom:14px">' +
      CA_TABS.map(t => '<button class="btn btn-small' + (caState.tab === t[0] ? ' btn-primary' : ' btn-ghost') +
        '" data-ca-tab="' + t[0] + '" style="margin:0 6px 6px 0">' + esc(t[1]) + '</button>').join('') +
      '</div><div id="ca-tab-body"></div>';
    body.querySelectorAll('[data-ca-tab]').forEach(b => b.addEventListener('click', e => {
      caState.tab = e.target.dataset.caTab; caRenderBody();
    }));
    const tb = document.getElementById('ca-tab-body');
    ({ dashboard: caRenderDashboardTab, profile: caRenderProfileTab, shareholders: caRenderShareholdersTab,
       events: caRenderEventsTab, calculators: caRenderCalculatorsTab, meetings: caRenderMeetingsTab,
       resolutions: caRenderResolutionsTab, checklist: caRenderChecklistTab,
       reports: caRenderReportsTab }[caState.tab] || caRenderDashboardTab)(tb);
  }

  function caRenderDashboardTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ca/clients/' + caState.clientId + '/dashboard').then(d => {
      const bc = d.board_compliance, stc = d.share_transaction_compliance, sh = d.shareholding;
      root.innerHTML = '<div class="grid c4">' +
        kpi('Shareholders', sh.holder_count) +
        kpi('Top 5 concentration', sh.top5_concentration_pct.toFixed(1) + '%') +
        kpi('Board minutes filed', bc.minutes_filed_pct.toFixed(1) + '%', bc.minutes_filed_pct < 100 ? 'alert' : 'accent') +
        kpi('Checklist complete', d.checklist_pct.toFixed(1) + '%', d.checklist_pct >= 100 ? 'accent' : '') +
        '</div>' +
        '<div class="grid c3">' +
        kpi('Related-party holding', sh.related_party_pct.toFixed(1) + '%') +
        kpi('Board meeting gaps over max', bc.gap_breaches_count, bc.gap_breaches_count ? 'alert' : '') +
        kpi('Capital events complete', stc.complete_count + ' / ' + stc.total, stc.complete_count < stc.total ? 'alert' : 'accent') +
        '</div>' +
        (d.profile && d.profile.registration_no ? '<div class="card"><b>OCR registration:</b> ' + esc(d.profile.registration_no) +
          (d.profile.entity_class ? ' · ' + esc(d.profile.entity_class) : '') +
          (d.profile.paid_up_capital ? ' · Paid-up capital ' + money(d.profile.paid_up_capital) : '') + '</div>'
          : '<div class="msg">No entity profile recorded yet — add one in the Entity profile tab.</div>') +
        '<div class="card"><b>Cross-links:</b> Deadlines/compliance calendar and the Debt vs Equity Funding Analyzer (Capital &amp; Debt Advisory) are separate modules — this dashboard does not duplicate them.</div>';
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function caRenderProfileTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ca/clients/' + caState.clientId + '/profile').then(p => {
      p = p || {};
      root.innerHTML = '<div class="card"><h3>Entity profile</h3><div class="grid c3" id="ca-profile-form">' +
        field('OCR registration no.', 'registration_no', p, 'text') +
        field('Incorporation date', 'incorporation_date', p, 'date') +
        field('Entity class', 'entity_class', p, 'select', { options: [
          { value: '', label: '— select —' }, { value: 'private', label: 'Private limited' },
          { value: 'public', label: 'Public limited' }, { value: 'listed_public', label: 'Listed public' }] }) +
        field('Authorized capital', 'authorized_capital', p, 'number') +
        field('Issued capital', 'issued_capital', p, 'number') +
        field('Paid-up capital', 'paid_up_capital', p, 'number') +
        field('Share face value', 'share_face_value', p, 'number') +
        field('Minimum directors (per AOA)', 'min_directors', p, 'number') +
        field('Maximum directors (per AOA)', 'max_directors', p, 'number') +
        field('Max days between board meetings (working assumption)', 'board_meeting_gap_days_max', p, 'number', { hint: 'Firm\'s own working assumption used by the Board Compliance Tracker — confirm against the entity\'s own AOA before relying on it.' }) +
        field('Auditor', 'auditor_name', p, 'text') +
        '<div class="field" style="grid-column:1/-1"><label>Notes</label><textarea data-key="notes" rows="2">' + esc(p.notes || '') + '</textarea></div>' +
        '</div><button class="btn btn-small" id="ca-save-profile" style="margin-top:10px">Save</button></div>';
      wireInto(root, 'ca-profile-form', p, () => {});
      document.getElementById('ca-save-profile').addEventListener('click', async () => {
        try { await api('/api/ca/clients/' + caState.clientId + '/profile', { method: 'PUT', body: p }); toast('Saved'); }
        catch (e) { toast(e.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function caRenderShareholdersTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/ca/clients/' + caState.clientId + '/shareholders'), api('/api/ca/clients/' + caState.clientId + '/shareholding-analysis')])
      .then(([rows, analysis]) => {
      root.innerHTML = '<div class="grid c3">' +
        kpi('Total shares', money(analysis.total_shares)) +
        kpi('Top 5 concentration', analysis.top5_concentration_pct.toFixed(1) + '%') +
        kpi('Related-party holding', analysis.related_party_pct.toFixed(1) + '%') +
        '</div>' +
        '<div class="card"><h3>Shareholder register</h3><div class="table-wrap"><table><thead><tr>' +
        '<th>Name</th><th>PAN</th><th>Class</th><th class="num">Shares held</th><th class="num">Ownership %</th>' +
        '<th>Related party</th><th>Director</th><th></th></tr></thead><tbody>' +
        rows.map(r => {
          const a = analysis.holders.find(h => h.id === r.id) || {};
          return '<tr><td><input data-sh="' + r.id + '" data-k="name" value="' + esc(r.name || '') + '"></td>' +
          '<td><input data-sh="' + r.id + '" data-k="pan" value="' + esc(r.pan || '') + '"></td>' +
          '<td><select data-sh="' + r.id + '" data-k="share_class"><option value="equity"' + (r.share_class === 'equity' ? ' selected' : '') + '>Equity</option><option value="preference"' + (r.share_class === 'preference' ? ' selected' : '') + '>Preference</option></select></td>' +
          '<td class="num"><input type="number" data-sh="' + r.id + '" data-k="shares_held" value="' + (r.shares_held || 0) + '"></td>' +
          '<td class="num">' + (a.ownership_pct != null ? a.ownership_pct.toFixed(2) : '0.00') + '%</td>' +
          '<td><input type="checkbox" data-sh-flag="' + r.id + '" data-k="is_related_party"' + (r.is_related_party ? ' checked' : '') + '></td>' +
          '<td><input type="checkbox" data-sh-flag="' + r.id + '" data-k="is_director"' + (r.is_director ? ' checked' : '') + '></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-sh="' + r.id + '" title="Remove">Remove</button></td></tr>';
        }).join('') + '</tbody></table></div>' +
        '<button class="btn btn-small" id="ca-add-sh" style="margin-top:10px">Add a shareholder</button></div>';

      root.querySelectorAll('[data-sh]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/ca/shareholders/' + e.target.dataset.sh, { method: 'PUT', body: { [k]: v } }); caRenderShareholdersTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-sh-flag]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/ca/shareholders/' + e.target.dataset.shFlag, { method: 'PUT', body: { [e.target.dataset.k]: e.target.checked } }); caRenderShareholdersTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-sh]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ca/shareholders/' + e.target.dataset.delSh, { method: 'DELETE' }); caRenderShareholdersTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addSh = document.getElementById('ca-add-sh');
      if (addSh) addSh.addEventListener('click', async () => {
        try { await api('/api/ca/clients/' + caState.clientId + '/shareholders', { method: 'POST', body: { name: 'New shareholder', shares_held: 0 } }); caRenderShareholdersTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function caRenderEventsTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/ca/clients/' + caState.clientId + '/share-transactions'), api('/api/ca/clients/' + caState.clientId + '/share-transaction-compliance')])
      .then(([rows, compliance]) => {
      const byId = {}; compliance.rows.forEach(r => { byId[r.id] = r; });
      root.innerHTML = '<div class="card"><h3>Capital events register</h3><p class="card-note">Rights issue, bonus issue, private placement, share transfer, capital reduction and buyback — one register. Completeness (approval/filing dates) is checked automatically against what each event type normally needs.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Shares</th><th class="num">Price/share</th>' +
        '<th>Txn date</th><th>Board approval</th><th>AGM/EGM approval</th><th>OCR filed</th><th>Status</th><th>Complete</th><th></th></tr></thead><tbody>' +
        rows.map(r => {
          const c = byId[r.id] || { complete: true, missing: [] };
          return '<tr><td><select data-ev="' + r.id + '" data-k="event_type">' + CA_EVENT_TYPES.map(t => '<option value="' + t[0] + '"' + (r.event_type === t[0] ? ' selected' : '') + '>' + t[1] + '</option>').join('') + '</select></td>' +
          '<td class="num"><input type="number" data-ev="' + r.id + '" data-k="shares" value="' + (r.shares || 0) + '"></td>' +
          '<td class="num"><input type="number" data-ev="' + r.id + '" data-k="price_per_share" value="' + (r.price_per_share || '') + '"></td>' +
          '<td><input type="date" data-ev="' + r.id + '" data-k="transaction_date" value="' + esc(r.transaction_date || '') + '"></td>' +
          '<td><input type="date" data-ev="' + r.id + '" data-k="board_approval_date" value="' + esc(r.board_approval_date || '') + '"></td>' +
          '<td><input type="date" data-ev="' + r.id + '" data-k="agm_approval_date" value="' + esc(r.agm_approval_date || '') + '"></td>' +
          '<td><input type="date" data-ev="' + r.id + '" data-k="ocr_filed_date" value="' + esc(r.ocr_filed_date || '') + '"></td>' +
          '<td><select data-ev="' + r.id + '" data-k="status">' + ['proposed', 'approved', 'filed', 'completed'].map(s => '<option value="' + s + '"' + (r.status === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>' +
          '<td>' + (c.complete ? '<span class="tag tag-green">Complete</span>' : '<span class="tag tag-amber" title="' + esc(c.missing.join(', ')) + '">Missing: ' + esc(c.missing.join(', ')) + '</span>') + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-ev="' + r.id + '" title="Remove">Remove</button></td></tr>';
        }).join('') + '</tbody></table></div>' +
        '<button class="btn btn-small" id="ca-add-ev" style="margin-top:10px">Add a capital event</button></div>';

      root.querySelectorAll('[data-ev]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/ca/share-transactions/' + e.target.dataset.ev, { method: 'PUT', body: { [k]: v } }); caRenderEventsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-ev]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ca/share-transactions/' + e.target.dataset.delEv, { method: 'DELETE' }); caRenderEventsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addEv = document.getElementById('ca-add-ev');
      if (addEv) addEv.addEventListener('click', async () => {
        try { await api('/api/ca/clients/' + caState.clientId + '/share-transactions', { method: 'POST', body: { event_type: 'allotment', shares: 0 } }); caRenderEventsTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  const CA_CALC_SPECS = {
    'rights-issue': { title: 'Rights Issue Calculator', endpoint: 'rights-issue',
      fields: [['existing_shares', 'Existing shares'], ['ratio_new', 'New shares (ratio numerator)'],
               ['ratio_held', 'Per shares held (ratio denominator)'], ['subscription_price', 'Subscription price'],
               ['cum_rights_price', 'Cum-rights market price']],
      results: [['New shares', 'new_shares'], ['Total shares after', 'total_shares_after'], ['Proceeds', 'proceeds'],
               ['Theoretical ex-rights price (TERP)', 'theoretical_ex_rights_price'], ['Value of a right', 'value_of_right']] },
    'bonus-issue': { title: 'Bonus Share Calculator', endpoint: 'bonus-issue',
      fields: [['existing_shares', 'Existing shares'], ['ratio_new', 'Bonus shares (ratio numerator)'],
               ['ratio_held', 'Per shares held (ratio denominator)'], ['face_value', 'Face value'],
               ['reserves_available', 'Free reserves available for capitalisation']],
      results: [['Bonus shares', 'bonus_shares'], ['Post-bonus shares', 'post_bonus_shares'],
               ['Capitalised amount', 'capitalized_amount'], ['Reserves sufficient', 'reserves_sufficient'],
               ['Reserves shortfall', 'reserves_shortfall']] },
    'capital-reduction': { title: 'Capital Reduction Analyzer', endpoint: 'capital-reduction',
      fields: [['existing_capital', 'Existing capital'], ['existing_shares', 'Existing shares'],
               ['face_value', 'Face value'], ['mode', 'Mode', 'select', [['amount', 'Flat amount'], ['ratio', 'Percentage']]],
               ['reduction_amount', 'Reduction amount (if flat amount mode)'], ['reduction_pct', 'Reduction % (if percentage mode)']],
      results: [['New capital', 'new_capital'], ['Reduction amount', 'reduction_amount'], ['Reduction %', 'reduction_pct'],
               ['New shares', 'new_shares'], ['Shares cancelled', 'shares_cancelled']] },
    'dividend-check': { title: 'Dividend Distribution Checker', endpoint: 'dividend-check',
      fields: [['net_profit_current_year', 'Net profit — current year'], ['accumulated_losses_bf', 'Accumulated losses b/f'],
               ['free_reserves', 'Free reserves'], ['mandatory_reserve_pct', 'Mandatory reserve % (confirm against the entity\'s own AOA/statute)'],
               ['proposed_dividend_amount', 'Proposed dividend amount']],
      results: [['Distributable profit', 'distributable_profit'], ['Proposed dividend', 'proposed_dividend_amount'],
               ['Sufficient', 'sufficient'], ['Shortfall', 'shortfall']] },
  };

  function caRenderCalculatorsTab(root) {
    if (!root.dataset.caCalcState) root.dataset.caCalcState = '{}';
    const inputs = {};
    Object.keys(CA_CALC_SPECS).forEach(k => { inputs[k] = {}; });
    let h = '';
    Object.keys(CA_CALC_SPECS).forEach(key => {
      const spec = CA_CALC_SPECS[key];
      h += '<div class="card"><h3>' + esc(spec.title) + '</h3><div class="grid c3" id="ca-calc-' + key + '">' +
        spec.fields.map(f => {
          if (f[2] === 'select') return '<div class="field"><label>' + esc(f[1]) + '</label><select data-calcf="' + key + '" data-k="' + f[0] + '">' +
            f[3].map(o => '<option value="' + o[0] + '">' + o[1] + '</option>').join('') + '</select></div>';
          return '<div class="field"><label>' + esc(f[1]) + '</label><input type="number" data-calcf="' + key + '" data-k="' + f[0] + '"></div>';
        }).join('') + '</div>' +
        '<button class="btn btn-small" style="margin-top:10px" data-calc-run="' + key + '">Compute</button>' +
        '<div id="ca-calc-out-' + key + '" style="margin-top:10px"></div></div>';
    });
    root.innerHTML = h;

    root.querySelectorAll('[data-calc-run]').forEach(btn => btn.addEventListener('click', async e => {
      const key = e.target.dataset.calcRun, spec = CA_CALC_SPECS[key];
      const body = {};
      root.querySelectorAll('[data-calcf="' + key + '"]').forEach(el => {
        body[el.dataset.k] = el.tagName === 'SELECT' ? el.value : Number(el.value);
      });
      try {
        const res = await api('/api/ca/calculators/' + spec.endpoint, { method: 'POST', body });
        const out = document.getElementById('ca-calc-out-' + key);
        out.innerHTML = '<div class="table-wrap"><table><tbody>' +
          spec.results.map(r => '<tr><td>' + esc(r[0]) + '</td><td class="num">' +
            (typeof res[r[1]] === 'number' ? money(res[r[1]]) : (res[r[1]] === true ? 'Yes' : (res[r[1]] === false ? 'No' : esc(res[r[1]] || '—')))) +
            '</td></tr>').join('') + '</tbody></table></div>' +
          (res.flags && res.flags.length ? res.flags.map(f => '<div class="msg ' + (f.severity === 'High' ? 'bad' : '') + '">' + esc(f.message) + '</div>').join('') : '');
      } catch (err) { toast(err.message, true); }
    }));
  }

  function caRenderMeetingsTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/ca/clients/' + caState.clientId + '/meetings'), api('/api/ca/clients/' + caState.clientId + '/board-compliance')])
      .then(([rows, bc]) => {
      root.innerHTML = '<div class="grid c4">' +
        kpi('Meetings recorded', bc.meetings_count) +
        kpi('Board meetings', bc.board_meetings_count) +
        kpi('Minutes filed', bc.minutes_filed_pct.toFixed(1) + '%') +
        kpi('Gaps over max', bc.gap_breaches_count, bc.gap_breaches_count ? 'alert' : '') +
        '</div>' +
        '<div class="card"><h3>Board / general meetings</h3><div class="table-wrap"><table><thead><tr>' +
        '<th>Type</th><th>Date</th><th>Notice date</th><th class="num">Quorum</th><th>Minutes recorded</th><th>Minutes filed</th><th>Status</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><select data-mt="' + r.id + '" data-k="meeting_type"><option value="board"' + (r.meeting_type === 'board' ? ' selected' : '') + '>Board</option><option value="agm"' + (r.meeting_type === 'agm' ? ' selected' : '') + '>AGM</option><option value="egm"' + (r.meeting_type === 'egm' ? ' selected' : '') + '>EGM</option></select></td>' +
          '<td><input type="date" data-mt="' + r.id + '" data-k="meeting_date" value="' + esc(r.meeting_date || '') + '"></td>' +
          '<td><input type="date" data-mt="' + r.id + '" data-k="notice_date" value="' + esc(r.notice_date || '') + '"></td>' +
          '<td class="num"><input type="number" data-mt="' + r.id + '" data-k="quorum_present" value="' + (r.quorum_present ?? '') + '"></td>' +
          '<td><input type="checkbox" data-mt-flag="' + r.id + '" data-k="minutes_recorded"' + (r.minutes_recorded ? ' checked' : '') + '></td>' +
          '<td><input type="date" data-mt="' + r.id + '" data-k="minutes_filed_date" value="' + esc(r.minutes_filed_date || '') + '"></td>' +
          '<td><select data-mt="' + r.id + '" data-k="status">' + ['scheduled', 'held', 'cancelled'].map(s => '<option value="' + s + '"' + (r.status === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-mt="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="ca-add-mt" style="margin-top:10px">Add a meeting</button></div>';

      root.querySelectorAll('[data-mt]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/ca/meetings/' + e.target.dataset.mt, { method: 'PUT', body: { [k]: v } }); caRenderMeetingsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-mt-flag]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/ca/meetings/' + e.target.dataset.mtFlag, { method: 'PUT', body: { [e.target.dataset.k]: e.target.checked } }); caRenderMeetingsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-mt]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ca/meetings/' + e.target.dataset.delMt, { method: 'DELETE' }); caRenderMeetingsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addMt = document.getElementById('ca-add-mt');
      if (addMt) addMt.addEventListener('click', async () => {
        try { await api('/api/ca/clients/' + caState.clientId + '/meetings', { method: 'POST', body: { meeting_type: 'board', meeting_date: new Date().toISOString().slice(0, 10) } }); caRenderMeetingsTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function caRenderResolutionsTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ca/clients/' + caState.clientId + '/resolutions').then(rows => {
      root.innerHTML = '<div class="card"><h3>Board &amp; shareholder resolutions</h3><p class="card-note">One register — the type field carries both the Board Resolution Generator and the Shareholder (ordinary/special) Resolution Generator.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Category</th><th>Title</th><th>Date</th><th>Status</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><select data-rs="' + r.id + '" data-k="resolution_type"><option value="board"' + (r.resolution_type === 'board' ? ' selected' : '') + '>Board</option><option value="ordinary"' + (r.resolution_type === 'ordinary' ? ' selected' : '') + '>Ordinary</option><option value="special"' + (r.resolution_type === 'special' ? ' selected' : '') + '>Special</option></select></td>' +
          '<td><input data-rs="' + r.id + '" data-k="category" value="' + esc(r.category || '') + '"></td>' +
          '<td><input data-rs="' + r.id + '" data-k="title" value="' + esc(r.title || '') + '"></td>' +
          '<td><input type="date" data-rs="' + r.id + '" data-k="resolution_date" value="' + esc(r.resolution_date || '') + '"></td>' +
          '<td><select data-rs="' + r.id + '" data-k="status">' + ['draft', 'passed', 'filed'].map(s => '<option value="' + s + '"' + (r.status === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>' +
          '<td><button class="btn btn-small" data-rs-doc="' + r.id + '">Generate document</button> <button class="btn btn-small btn-danger" data-del-rs="' + r.id + '" title="Remove">Remove</button></td></tr>' +
          '<tr><td colspan="6"><textarea data-rs="' + r.id + '" data-k="resolution_text" rows="2" placeholder="Resolution text (RESOLVED THAT...)" style="width:100%">' + esc(r.resolution_text || '') + '</textarea></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="ca-add-rs" style="margin-top:10px">Add a resolution</button></div>';

      root.querySelectorAll('[data-rs]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.value;
        try { await api('/api/ca/resolutions/' + e.target.dataset.rs, { method: 'PUT', body: { [k]: v } }); if (k !== 'resolution_text') caRenderResolutionsTab(root); else toast('Saved'); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-rs]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ca/resolutions/' + e.target.dataset.delRs, { method: 'DELETE' }); caRenderResolutionsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-rs-doc]').forEach(el => el.addEventListener('click', async e => {
        try { const res = await api('/api/ca/resolutions/' + e.target.dataset.rsDoc + '/document', { method: 'POST' }); toast('Generated ' + res.filename); window.open(res.download_url, '_blank'); }
        catch (err) { toast(err.message, true); }
      }));
      const addRs = document.getElementById('ca-add-rs');
      if (addRs) addRs.addEventListener('click', async () => {
        try { await api('/api/ca/clients/' + caState.clientId + '/resolutions', { method: 'POST', body: { resolution_type: 'board', title: 'New resolution' } }); caRenderResolutionsTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  const CA_CHECKLIST_AREAS = ['Incorporation', 'Statutory registers', 'AGM', 'Board meetings', 'Filings'];

  function caRenderChecklistTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ca/clients/' + caState.clientId + '/checklist').then(rows => {
      const byArea = {};
      CA_CHECKLIST_AREAS.forEach(a => { byArea[a] = []; });
      rows.forEach(r => { (byArea[r.area] = byArea[r.area] || []).push(r); });

      root.innerHTML = Object.keys(byArea).map(area => {
        const items = byArea[area];
        const done = items.filter(i => i.status === 'done').length;
        return '<details class="card" style="margin-top:10px"><summary><b>' + esc(area) + '</b> <span class="hint">' + done + ' / ' + items.length + ' done</span></summary>' +
          '<div class="table-wrap" style="margin-top:12px"><table><thead><tr><th>Criterion</th><th>Status</th><th>Due</th><th>Remark</th><th></th></tr></thead><tbody>' +
          items.map(i => '<tr><td>' + esc(i.criterion) + '</td>' +
            '<td><select data-cc="' + i.id + '" data-k="status">' + ['pending', 'done', 'na'].map(s => '<option value="' + s + '"' + (i.status === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>' +
            '<td><input type="date" data-cc="' + i.id + '" data-k="due_date" value="' + esc(i.due_date || '') + '"></td>' +
            '<td><input data-cc="' + i.id + '" data-k="remark" value="' + esc(i.remark || '') + '"></td>' +
            '<td><button class="btn btn-small btn-danger" data-del-cc="' + i.id + '" title="Remove">Remove</button></td></tr>').join('') +
          '</tbody></table></div>' +
          '<button class="btn btn-small" data-seed-cc="' + area + '" style="margin-top:10px" title="Add the standard items, skipping any already present">Seed standard checklist</button></details>';
      }).join('');

      root.querySelectorAll('[data-cc]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k;
        try { await api('/api/ca/checklist/' + e.target.dataset.cc, { method: 'PUT', body: { [k]: e.target.value } }); toast('Saved'); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-cc]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ca/checklist/' + e.target.dataset.delCc, { method: 'DELETE' }); caRenderChecklistTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-seed-cc]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ca/clients/' + caState.clientId + '/checklist/seed', { method: 'POST', body: { area: e.target.dataset.seedCc } }); caRenderChecklistTab(root); }
        catch (err) { toast(err.message, true); }
      }));
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function caRenderReportsTab(root) {
    root.innerHTML = '<div class="card"><h3>Reports</h3><p class="card-note">Corporate Advisory Compliance Report — shareholding, board compliance, capital-events completeness, resolutions and the company-law checklist, in one document.</p>' +
      '<button class="btn btn-small" data-ca-report="xlsx">Excel</button> <button class="btn btn-small" data-ca-report="docx">Word</button></div>';
    root.querySelectorAll('[data-ca-report]').forEach(el => el.addEventListener('click', async e => {
      try {
        const res = await api('/api/ca/clients/' + caState.clientId + '/reports', { method: 'POST', body: { format: e.target.dataset.caReport } });
        toast('Generated ' + res.filename);
        window.open(res.download_url, '_blank');
      } catch (err) { toast(err.message, true); }
    }));
  }

  // ------------------------------------------------------------ business advisory
  const BA_TABS = [
    ['dashboard', 'Dashboard'], ['profitability', 'Profitability'], ['pricing', 'Pricing'],
    ['budget', 'Budget vs actual'], ['kpis', 'KPIs'], ['model', 'Business model'],
    ['plan', 'Business plan'], ['reports', 'Reports'],
  ];
  const baState = { clientId: '', tab: 'dashboard', profitDimension: 'product' };
  const BA_DIMENSIONS = [['product', 'Product'], ['customer', 'Customer'], ['branch', 'Branch'], ['overall', 'Overall']];
  const BA_PLAN_SECTIONS = [
    ['executive_summary', 'Executive Summary'], ['company_description', 'Company Description'],
    ['market_analysis', 'Market Analysis'], ['business_model', 'Business Model'],
    ['marketing_sales', 'Marketing & Sales'], ['operations', 'Operations'],
    ['management_team', 'Management Team'], ['financial_plan', 'Financial Plan'],
    ['risk_analysis', 'Risk Analysis'], ['appendix', 'Appendix'],
  ];

  function renderBA() {
    const view = document.getElementById('view');
    if (!state.online || !state.token || state.offlineMode) {
      view.innerHTML = '<div class="page-head"><div><h2>Business Advisory</h2></div></div>' +
        '<div class="card"><div class="empty"><b>Offline</b>Business Advisory works against live server data — reconnect to use this module.</div></div>';
      return;
    }
    if (!baState.clientId) baState.clientId = localStorage.getItem('ca.client.ba') || (clients()[0] && clients()[0].id) || '';

    view.innerHTML =
      '<div class="page-head"><div><h2>Business Advisory</h2>' +
      '<p>Product/customer/branch profitability, cost structure, pricing, budgeting and a KPI/management dashboard, one client at a time. For feasibility studies and financial projections, use the Projection module — it isn\'t duplicated here.</p></div></div>' +
      '<div class="card"><div class="grid c3">' +
      '<div class="field"><label>Client</label><select id="ba-client"><option value="">— choose —</option>' +
      clients().map(c => '<option value="' + c.id + '"' + (String(baState.clientId) === String(c.id) ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') + '</select></div>' +
      '</div></div>' +
      '<div class="card" id="ba-body"><div class="empty">Loading…</div></div>';

    document.getElementById('ba-client').addEventListener('change', e => {
      baState.clientId = e.target.value; localStorage.setItem('ca.client.ba', baState.clientId);
      baState.tab = 'dashboard';
      baRenderBody();
    });
    baRenderBody();
  }

  function baRenderBody() {
    const body = document.getElementById('ba-body');
    if (!baState.clientId) { body.innerHTML = '<div class="empty"><b>Choose a client</b>Pick a client above to begin.</div>'; return; }
    body.innerHTML = '<div style="margin-bottom:14px">' +
      BA_TABS.map(t => '<button class="btn btn-small' + (baState.tab === t[0] ? ' btn-primary' : ' btn-ghost') +
        '" data-ba-tab="' + t[0] + '" style="margin:0 6px 6px 0">' + esc(t[1]) + '</button>').join('') +
      '</div><div id="ba-tab-body"></div>';
    body.querySelectorAll('[data-ba-tab]').forEach(b => b.addEventListener('click', e => {
      baState.tab = e.target.dataset.baTab; baRenderBody();
    }));
    const tb = document.getElementById('ba-tab-body');
    ({ dashboard: baRenderDashboardTab, profitability: baRenderProfitabilityTab, pricing: baRenderPricingTab,
       budget: baRenderBudgetTab, kpis: baRenderKpiTab, model: baRenderBusinessModelTab,
       plan: baRenderBusinessPlanTab, reports: baRenderReportsTab }[baState.tab] || baRenderDashboardTab)(tb);
  }

  function baRenderDashboardTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ba/clients/' + baState.clientId + '/dashboard').then(d => {
      const cs = d.profitability.cost_structure, bv = d.budget_variance.summary, kd = d.kpi_dashboard;
      root.innerHTML = '<div class="grid c4">' +
        kpi('Revenue', money(cs.total_revenue)) +
        kpi('Gross margin', cs.total_gross_margin_pct.toFixed(1) + '%') +
        kpi('Net margin', cs.total_net_margin_pct.toFixed(1) + '%', cs.total_net_margin_pct < 0 ? 'alert' : 'accent') +
        kpi('Loss-making lines', d.profitability.loss_makers.length, d.profitability.loss_makers.length ? 'alert' : '') +
        '</div>' +
        '<div class="grid c3">' +
        kpi('Budgeted vs actual profit', money(bv.budgeted_profit) + ' → ' + money(bv.actual_profit)) +
        kpi('Unfavorable budget lines', d.budget_variance.unfavorable_count, d.budget_variance.unfavorable_count ? 'alert' : '') +
        kpi('KPIs off target', kd.off_target_count, kd.off_target_count ? 'alert' : 'accent') +
        '</div>' +
        (d.profitability.best_performer ? '<div class="card"><b>Best performer:</b> ' + esc(d.profitability.best_performer.dimension_name) +
          ' (' + esc(d.profitability.best_performer.dimension_type) + ') — net margin ' + d.profitability.best_performer.net_margin_pct.toFixed(1) + '%</div>' : '') +
        '<div class="card"><b>Cross-link:</b> For feasibility studies, financial projections and DSCR sensitivity, use the Projection module.</div>';
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function baRenderProfitabilityTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([
      api('/api/ba/clients/' + baState.clientId + '/profitability?dimension_type=' + baState.profitDimension),
      api('/api/ba/clients/' + baState.clientId + '/profitability-analysis?dimension_type=' + baState.profitDimension),
    ]).then(([rows, analysis]) => {
      const byKey = {}; analysis.rows.forEach(a => { byKey[a.dimension_name] = a; });
      root.innerHTML = '<div style="margin-bottom:12px">' +
        BA_DIMENSIONS.map(d => '<button class="btn btn-small' + (baState.profitDimension === d[0] ? ' btn-primary' : ' btn-ghost') +
          '" data-ba-dim="' + d[0] + '" style="margin:0 6px 6px 0">' + d[1] + '-wise</button>').join('') + '</div>' +
        '<div class="grid c3">' +
        kpi('Revenue', money(analysis.cost_structure.total_revenue)) +
        kpi('Gross margin', analysis.cost_structure.total_gross_margin_pct.toFixed(1) + '%') +
        kpi('Net margin', analysis.cost_structure.total_net_margin_pct.toFixed(1) + '%', analysis.cost_structure.total_net_margin_pct < 0 ? 'alert' : 'accent') +
        '</div>' +
        '<div class="card"><h3>' + esc(BA_DIMENSIONS.find(d => d[0] === baState.profitDimension)[1]) + '-wise profitability</h3><div class="table-wrap"><table><thead><tr>' +
        '<th>Period</th><th>Name</th><th class="num">Revenue</th><th class="num">Direct cost</th><th class="num">Overhead</th>' +
        '<th class="num">Net margin</th><th class="num">Net margin %</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-pl="' + r.id + '" data-k="period_label" value="' + esc(r.period_label || '') + '"></td>' +
          '<td><input data-pl="' + r.id + '" data-k="dimension_name" value="' + esc(r.dimension_name || '') + '"></td>' +
          '<td class="num"><input type="number" data-pl="' + r.id + '" data-k="revenue" value="' + (r.revenue || 0) + '"></td>' +
          '<td class="num"><input type="number" data-pl="' + r.id + '" data-k="direct_cost" value="' + (r.direct_cost || 0) + '"></td>' +
          '<td class="num"><input type="number" data-pl="' + r.id + '" data-k="allocated_overhead" value="' + (r.allocated_overhead || 0) + '"></td>' +
          '<td class="num">' + (byKey[r.dimension_name] ? money(byKey[r.dimension_name].net_margin) : '—') + '</td>' +
          '<td class="num">' + (byKey[r.dimension_name] ? byKey[r.dimension_name].net_margin_pct.toFixed(1) + '%' : '—') + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-pl="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="ba-add-pl" style="margin-top:10px">Add a line</button></div>';

      root.querySelectorAll('[data-ba-dim]').forEach(el => el.addEventListener('click', e => {
        baState.profitDimension = e.target.dataset.baDim; baRenderProfitabilityTab(root);
      }));
      root.querySelectorAll('[data-pl]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/ba/profitability/' + e.target.dataset.pl, { method: 'PUT', body: { [k]: v } }); baRenderProfitabilityTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-pl]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ba/profitability/' + e.target.dataset.delPl, { method: 'DELETE' }); baRenderProfitabilityTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addPl = document.getElementById('ba-add-pl');
      if (addPl) addPl.addEventListener('click', async () => {
        try {
          await api('/api/ba/clients/' + baState.clientId + '/profitability', { method: 'POST', body: {
            dimension_type: baState.profitDimension, dimension_name: 'New ' + baState.profitDimension, period_label: '' } });
          baRenderProfitabilityTab(root);
        } catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function baRenderPricingTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ba/clients/' + baState.clientId + '/pricing').then(rows => {
      root.innerHTML = '<div class="card"><h3>Pricing strategy</h3><p class="card-note">Cost-plus recommended price off your target margin, benchmarked against a competitor price and the current price.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Product</th><th class="num">Unit cost</th><th class="num">Target margin %</th>' +
        '<th class="num">Competitor price</th><th class="num">Current price</th><th class="num">Recommended price</th><th class="num">Markup %</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-pr="' + r.id + '" data-k="product_name" value="' + esc(r.product_name || '') + '"></td>' +
          '<td class="num"><input type="number" data-pr="' + r.id + '" data-k="unit_cost" value="' + (r.unit_cost || 0) + '"></td>' +
          '<td class="num"><input type="number" data-pr="' + r.id + '" data-k="target_margin_pct" value="' + (r.target_margin_pct || 0) + '"></td>' +
          '<td class="num"><input type="number" data-pr="' + r.id + '" data-k="competitor_price" value="' + (r.competitor_price ?? '') + '"></td>' +
          '<td class="num"><input type="number" data-pr="' + r.id + '" data-k="current_price" value="' + (r.current_price ?? '') + '"></td>' +
          '<td class="num">' + (r.computed.recommended_price != null ? money(r.computed.recommended_price) : '—') + '</td>' +
          '<td class="num">' + (r.computed.markup_pct != null ? r.computed.markup_pct.toFixed(1) + '%' : '—') + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-pr="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="ba-add-pr" style="margin-top:10px">Add a pricing scenario</button></div>';

      root.querySelectorAll('[data-pr]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/ba/pricing/' + e.target.dataset.pr, { method: 'PUT', body: { [k]: v } }); baRenderPricingTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-pr]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ba/pricing/' + e.target.dataset.delPr, { method: 'DELETE' }); baRenderPricingTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addPr = document.getElementById('ba-add-pr');
      if (addPr) addPr.addEventListener('click', async () => {
        try { await api('/api/ba/clients/' + baState.clientId + '/pricing', { method: 'POST', body: { product_name: 'New product', unit_cost: 0, target_margin_pct: 20 } }); baRenderPricingTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function baRenderBudgetTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/ba/clients/' + baState.clientId + '/budget'), api('/api/ba/clients/' + baState.clientId + '/budget-variance')])
      .then(([rows, variance]) => {
      root.innerHTML = '<div class="grid c3">' +
        kpi('Budgeted profit', money(variance.summary.budgeted_profit)) +
        kpi('Actual profit', money(variance.summary.actual_profit)) +
        kpi('Unfavorable lines', variance.unfavorable_count, variance.unfavorable_count ? 'alert' : '') +
        '</div>' +
        '<div class="card"><h3>Budget vs actual</h3><div class="table-wrap"><table><thead><tr>' +
        '<th>FY</th><th>Period</th><th>Type</th><th>Category</th><th class="num">Budgeted</th><th class="num">Actual</th>' +
        '<th class="num">Variance</th><th></th></tr></thead><tbody>' +
        rows.map(r => {
          const v = variance.rows.find(x => x.id === r.id) || {};
          return '<tr><td><input data-bg="' + r.id + '" data-k="fiscal_year_label" value="' + esc(r.fiscal_year_label || '') + '"></td>' +
          '<td><input data-bg="' + r.id + '" data-k="period_label" value="' + esc(r.period_label || '') + '"></td>' +
          '<td><select data-bg="' + r.id + '" data-k="line_type"><option value="revenue"' + (r.line_type === 'revenue' ? ' selected' : '') + '>Revenue</option><option value="expense"' + (r.line_type === 'expense' ? ' selected' : '') + '>Expense</option></select></td>' +
          '<td><input data-bg="' + r.id + '" data-k="category" value="' + esc(r.category || '') + '"></td>' +
          '<td class="num"><input type="number" data-bg="' + r.id + '" data-k="budgeted_amount" value="' + (r.budgeted_amount || 0) + '"></td>' +
          '<td class="num"><input type="number" data-bg="' + r.id + '" data-k="actual_amount" value="' + (r.actual_amount ?? '') + '"></td>' +
          '<td class="num">' + (v.variance != null ? '<span class="tag' + (v.favorable === false ? ' alert' : '') + '">' + money(v.variance) + '</span>' : '—') + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-bg="' + r.id + '" title="Remove">Remove</button></td></tr>';
        }).join('') + '</tbody></table></div><button class="btn btn-small" id="ba-add-bg" style="margin-top:10px">Add a budget line</button></div>';

      root.querySelectorAll('[data-bg]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = (k === 'budgeted_amount' || k === 'actual_amount') ? (e.target.value === '' ? null : Number(e.target.value)) : e.target.value;
        try { await api('/api/ba/budget/' + e.target.dataset.bg, { method: 'PUT', body: { [k]: v } }); baRenderBudgetTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-bg]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ba/budget/' + e.target.dataset.delBg, { method: 'DELETE' }); baRenderBudgetTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addBg = document.getElementById('ba-add-bg');
      if (addBg) addBg.addEventListener('click', async () => {
        try { await api('/api/ba/clients/' + baState.clientId + '/budget', { method: 'POST', body: { fiscal_year_label: state.fy, line_type: 'expense', category: 'New category', budgeted_amount: 0 } }); baRenderBudgetTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function baRenderKpiTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ba/clients/' + baState.clientId + '/kpi-dashboard').then(kd => {
      root.innerHTML = '<div class="card"><h3>KPI dashboard</h3><p class="card-note">Add a KPI, then record its value period by period — trend and target variance compute automatically.</p>' +
        kd.kpis.map(k => '<details class="card" style="margin-top:10px"><summary><b>' + esc(k.name) + '</b>' +
          (k.latest_value != null ? ' <span class="hint"> — ' + k.latest_value + esc(k.unit || '') +
            (k.trend ? ' (' + k.trend + ')' : '') + (k.on_target != null ? (k.on_target ? ' — on target' : ' — off target') : '') + '</span>' : '') +
          '</summary><div class="grid c4" style="margin-top:10px">' +
          '<div class="field"><label>Name</label><input data-kpi="' + k.id + '" data-k="name" value="' + esc(k.name || '') + '"></div>' +
          '<div class="field"><label>Category</label><input data-kpi="' + k.id + '" data-k="category" value="' + esc(k.category || '') + '"></div>' +
          '<div class="field"><label>Unit</label><input data-kpi="' + k.id + '" data-k="unit" value="' + esc(k.unit || '') + '"></div>' +
          '<div class="field"><label>Target</label><input type="number" data-kpi="' + k.id + '" data-k="target_value" value="' + (k.target_value ?? '') + '"></div>' +
          '</div><label class="check" style="margin-top:8px"><input type="checkbox" data-kpi-flag="' + k.id + '"' + (k.higher_is_better ? ' checked' : '') + '> Higher is better</label>' +
          '<div class="table-wrap" style="margin-top:10px"><table><thead><tr><th>Period</th><th class="num">Value</th></tr></thead><tbody>' +
          k.history.map(h => '<tr><td>' + esc(h.period_label) + '</td><td class="num">' + h.actual_value + '</td></tr>').join('') +
          '</tbody></table></div>' +
          '<div class="grid c3" style="margin-top:10px"><div class="field"><label>New period</label><input id="kpi-period-' + k.id + '" placeholder="2081-Q1"></div>' +
          '<div class="field"><label>Value</label><input type="number" id="kpi-value-' + k.id + '"></div>' +
          '<div class="field"><label>&nbsp;</label><button class="btn btn-small" data-kpi-add-value="' + k.id + '">Record value</button></div></div>' +
          '<button class="btn btn-small btn-danger" style="margin-top:10px" data-del-kpi="' + k.id + '">Remove KPI</button></details>').join('') +
        '<button class="btn btn-small" id="ba-add-kpi" style="margin-top:10px">Add a KPI</button></div>';

      root.querySelectorAll('[data-kpi]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/ba/kpis/' + e.target.dataset.kpi, { method: 'PUT', body: { [k]: v } }); baRenderKpiTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-kpi-flag]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/ba/kpis/' + e.target.dataset.kpiFlag, { method: 'PUT', body: { higher_is_better: e.target.checked } }); baRenderKpiTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-kpi]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ba/kpis/' + e.target.dataset.delKpi, { method: 'DELETE' }); baRenderKpiTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-kpi-add-value]').forEach(el => el.addEventListener('click', async e => {
        const id = e.target.dataset.kpiAddValue;
        const period = document.getElementById('kpi-period-' + id).value.trim();
        const value = Number(document.getElementById('kpi-value-' + id).value || 0);
        if (!period) { toast('Enter a period label first', true); return; }
        try { await api('/api/ba/kpis/' + id + '/values', { method: 'POST', body: { period_label: period, actual_value: value } }); baRenderKpiTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addKpi = document.getElementById('ba-add-kpi');
      if (addKpi) addKpi.addEventListener('click', async () => {
        try { await api('/api/ba/clients/' + baState.clientId + '/kpis', { method: 'POST', body: { name: 'New KPI', higher_is_better: true } }); baRenderKpiTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function baRenderBusinessModelTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ba/clients/' + baState.clientId + '/business-model').then(bm => {
      bm = bm || {};
      const fields = [
        ['value_proposition', 'Value proposition'], ['customer_segments', 'Customer segments'],
        ['channels', 'Channels'], ['customer_relationships', 'Customer relationships'],
        ['revenue_streams', 'Revenue streams'], ['key_resources', 'Key resources'],
        ['key_activities', 'Key activities'], ['key_partners', 'Key partners'],
        ['cost_structure_notes', 'Cost structure'],
      ];
      root.innerHTML = '<div class="card"><h3>Business model canvas</h3>' +
        '<div class="grid c2" id="ba-model-form">' +
        fields.map(f => '<div class="field"><label>' + esc(f[1]) + '</label><textarea data-key="' + f[0] + '" rows="3">' + esc(bm[f[0]] || '') + '</textarea></div>').join('') +
        '</div><button class="btn btn-small" id="ba-save-model" style="margin-top:10px">Save</button></div>';
      wireInto(root, 'ba-model-form', bm, () => {});
      document.getElementById('ba-save-model').addEventListener('click', async () => {
        try { await api('/api/ba/clients/' + baState.clientId + '/business-model', { method: 'PUT', body: bm }); toast('Saved'); }
        catch (e) { toast(e.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function baRenderBusinessPlanTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ba/clients/' + baState.clientId + '/plan-sections').then(rows => {
      const byKey = {}; rows.forEach(r => { byKey[r.section_key] = r.content; });
      root.innerHTML = '<div class="card"><h3>Business plan</h3><p class="card-note">Standard sections — draft each here, then generate the assembled Word document from Reports. Financial Plan pulls current profitability figures automatically at report time.</p></div>' +
        BA_PLAN_SECTIONS.map(s => '<div class="card"><h3>' + esc(s[1]) + '</h3>' +
          '<textarea data-plan-section="' + s[0] + '" rows="4" style="width:100%">' + esc(byKey[s[0]] || '') + '</textarea>' +
          '<button class="btn btn-small" style="margin-top:8px" data-save-plan-section="' + s[0] + '">Save</button></div>').join('');

      root.querySelectorAll('[data-save-plan-section]').forEach(el => el.addEventListener('click', async e => {
        const key = e.target.dataset.savePlanSection;
        const content = root.querySelector('[data-plan-section="' + key + '"]').value;
        try { await api('/api/ba/clients/' + baState.clientId + '/plan-sections/' + key, { method: 'PUT', body: { content } }); toast('Saved'); }
        catch (err) { toast(err.message, true); }
      }));
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function baRenderReportsTab(root) {
    root.innerHTML = '<div class="card"><h3>Reports</h3>' +
      '<p class="card-note"><b>Management Dashboard</b> — profitability, cost structure, budget vs actual and the KPI dashboard, in one document.</p>' +
      '<button class="btn btn-small" data-ba-report="management_dashboard" data-fmt="xlsx">Excel</button> ' +
      '<button class="btn btn-small" data-ba-report="management_dashboard" data-fmt="docx">Word</button>' +
      '<p class="card-note" style="margin-top:14px"><b>Business Plan</b> — the drafted sections, the business model canvas and current financial figures, assembled into one document.</p>' +
      '<button class="btn btn-small" data-ba-report="business_plan" data-fmt="docx">Word</button></div>';
    root.querySelectorAll('[data-ba-report]').forEach(el => el.addEventListener('click', async e => {
      try {
        const res = await api('/api/ba/clients/' + baState.clientId + '/reports', { method: 'POST', body: {
          report_type: e.target.dataset.baReport, format: e.target.dataset.fmt } });
        toast('Generated ' + res.filename);
        window.open(res.download_url, '_blank');
      } catch (err) { toast(err.message, true); }
    }));
  }

  // ------------------------------------------------------------- ipo advisory
  const IPO_TABS = [
    ['dashboard', 'Dashboard'], ['profile', 'IPO profile'], ['checklist', 'Readiness & eligibility'],
    ['restatement', 'Restatement'], ['workingcapital', 'Working capital'], ['peers', 'Peer comparison'],
    ['pricing', 'Pricing'], ['risks', 'Risk factors'], ['reports', 'Reports'],
  ];
  const ipoState = { clientId: '', tab: 'dashboard', checklistArea: 'Readiness' };
  const IPO_CHECKLIST_AREAS = ['Readiness', 'Eligibility', 'SEBON Compliance'];

  function renderIPO() {
    const view = document.getElementById('view');
    if (!state.online || !state.token || state.offlineMode) {
      view.innerHTML = '<div class="page-head"><div><h2>IPO / Capital Market Advisory</h2></div></div>' +
        '<div class="card"><div class="empty"><b>Offline</b>IPO Advisory works against live server data — reconnect to use this module.</div></div>';
      return;
    }
    if (!ipoState.clientId) ipoState.clientId = localStorage.getItem('ca.client.ipo') || (clients()[0] && clients()[0].id) || '';

    view.innerHTML =
      '<div class="page-head"><div><h2>IPO / Capital Market Advisory</h2>' +
      '<p>Readiness, SEBON eligibility, restatement analysis, working capital adequacy, peer comparison, pricing and risk factors for an IPO. Financial due diligence, projections and valuation are NOT duplicated here — use the Due Diligence, Projection and Valuation modules and carry the key figures over by hand.</p></div></div>' +
      '<div class="card"><div class="grid c3">' +
      '<div class="field"><label>Client</label><select id="ipo-client"><option value="">— choose —</option>' +
      clients().map(c => '<option value="' + c.id + '"' + (String(ipoState.clientId) === String(c.id) ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') + '</select></div>' +
      '</div></div>' +
      '<div class="card" id="ipo-body"><div class="empty">Loading…</div></div>';

    document.getElementById('ipo-client').addEventListener('change', e => {
      ipoState.clientId = e.target.value; localStorage.setItem('ca.client.ipo', ipoState.clientId);
      ipoState.tab = 'dashboard';
      ipoRenderBody();
    });
    ipoRenderBody();
  }

  function ipoRenderBody() {
    const body = document.getElementById('ipo-body');
    if (!ipoState.clientId) { body.innerHTML = '<div class="empty"><b>Choose a client</b>Pick a client above to begin.</div>'; return; }
    body.innerHTML = '<div style="margin-bottom:14px">' +
      IPO_TABS.map(t => '<button class="btn btn-small' + (ipoState.tab === t[0] ? ' btn-primary' : ' btn-ghost') +
        '" data-ipo-tab="' + t[0] + '" style="margin:0 6px 6px 0">' + esc(t[1]) + '</button>').join('') +
      '</div><div id="ipo-tab-body"></div>';
    body.querySelectorAll('[data-ipo-tab]').forEach(b => b.addEventListener('click', e => {
      ipoState.tab = e.target.dataset.ipoTab; ipoRenderBody();
    }));
    const tb = document.getElementById('ipo-tab-body');
    ({ dashboard: ipoRenderDashboardTab, profile: ipoRenderProfileTab, checklist: ipoRenderChecklistTab,
       restatement: ipoRenderRestatementTab, workingcapital: ipoRenderWorkingCapitalTab, peers: ipoRenderPeersTab,
       pricing: ipoRenderPricingTab, risks: ipoRenderRisksTab, reports: ipoRenderReportsTab }[ipoState.tab] || ipoRenderDashboardTab)(tb);
  }

  function ipoRenderDashboardTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ipo/clients/' + ipoState.clientId + '/dashboard').then(d => {
      const areas = d.readiness.areas || {};
      const readinessPct = areas.Readiness ? areas.Readiness.completion_pct : 0;
      root.innerHTML = '<div class="grid c4">' +
        kpi('Readiness complete', readinessPct.toFixed(1) + '%', readinessPct >= 100 ? 'accent' : '') +
        kpi('Eligibility not met', d.readiness.eligibility_not_met_count, d.readiness.eligibility_not_met_count ? 'alert' : 'accent') +
        kpi('Eligibility unconfirmed', d.readiness.eligibility_unconfirmed_count, d.readiness.eligibility_unconfirmed_count ? 'alert' : '') +
        kpi('High risk factors', d.high_risk_count, d.high_risk_count ? 'alert' : '') +
        '</div>' +
        '<div class="grid c3">' +
        kpi('Restatement lines', d.restatement_analysis.lines_restated) +
        kpi('Working capital', d.working_capital_adequacy.overall_adequate ? 'Adequate' : 'Shortfall projected', d.working_capital_adequacy.overall_adequate ? 'accent' : 'alert') +
        kpi('Risk factors logged', d.risk_count) +
        '</div>' +
        (d.profile && d.profile.target_listing_date ? '<div class="card"><b>Target listing:</b> ' + esc(d.profile.target_listing_date) +
          (d.profile.proposed_price_per_share ? ' · Proposed price ' + money(d.profile.proposed_price_per_share) : '') + '</div>'
          : '<div class="msg">No IPO profile recorded yet — add one in the IPO profile tab.</div>') +
        '<div class="card"><b>Cross-links:</b> Financial due diligence — Due Diligence module. Financial projections — Projection module. Share valuation — Valuation module.</div>';
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function ipoRenderProfileTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ipo/clients/' + ipoState.clientId + '/profile').then(p => {
      p = p || {};
      root.innerHTML = '<div class="card"><h3>IPO profile</h3><div class="grid c3" id="ipo-profile-form">' +
        field('Target listing date', 'target_listing_date', p, 'date') +
        field('Face value', 'face_value', p, 'number') +
        field('Proposed issue size', 'proposed_issue_size', p, 'number') +
        field('Proposed price per share', 'proposed_price_per_share', p, 'number') +
        field('Pricing method', 'pricing_method', p, 'select', { options: [
          { value: '', label: '— select —' }, { value: 'fixed_price', label: 'Fixed price' },
          { value: 'book_building', label: 'Book building' }] }) +
        field('Lead manager', 'lead_manager', p, 'text') +
        '<div class="field" style="grid-column:1/-1"><label>Notes</label><textarea data-key="notes" rows="2">' + esc(p.notes || '') + '</textarea></div>' +
        '</div><button class="btn btn-small" id="ipo-save-profile" style="margin-top:10px">Save</button></div>';
      wireInto(root, 'ipo-profile-form', p, () => {});
      document.getElementById('ipo-save-profile').addEventListener('click', async () => {
        try { await api('/api/ipo/clients/' + ipoState.clientId + '/profile', { method: 'PUT', body: p }); toast('Saved'); }
        catch (e) { toast(e.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function ipoRenderChecklistTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/ipo/clients/' + ipoState.clientId + '/checklist'), api('/api/ipo/clients/' + ipoState.clientId + '/readiness')])
      .then(([rows, readiness]) => {
      const isEligibility = ipoState.checklistArea === 'Eligibility';
      const areaRows = rows.filter(r => r.area === ipoState.checklistArea);
      const areaStats = readiness.areas[ipoState.checklistArea] || { done: 0, total: 0, completion_pct: 0 };
      root.innerHTML = '<div style="margin-bottom:12px">' +
        IPO_CHECKLIST_AREAS.map(a => '<button class="btn btn-small' + (ipoState.checklistArea === a ? ' btn-primary' : ' btn-ghost') +
          '" data-ipo-area="' + a + '" style="margin:0 6px 6px 0">' + a + '</button>').join('') + '</div>' +
        '<div class="card"><h3>' + esc(ipoState.checklistArea) + '</h3><span class="hint">' + areaStats.done + ' / ' + areaStats.total + ' done</span>' +
        (isEligibility ? '<p class="card-note">Enter the currently-effective SEBON threshold yourself before relying on the Met/Not met column — no threshold is assumed by this app.</p>' : '') +
        '<div class="table-wrap" style="margin-top:10px"><table><thead><tr><th>Criterion</th>' +
        (isEligibility ? '<th>Threshold</th><th>Actual</th>' : '<th>Status</th>') +
        '<th>Remark</th><th></th></tr></thead><tbody>' +
        areaRows.map(r => {
          const elig = readiness.eligibility.find(e => e.id === r.id);
          return '<tr><td><input data-ic="' + r.id + '" data-k="criterion" value="' + esc(r.criterion || '') + '"></td>' +
          (isEligibility
            ? '<td><input data-ic="' + r.id + '" data-k="threshold_value" value="' + esc(r.threshold_value || '') + '"></td>' +
              '<td><input data-ic="' + r.id + '" data-k="actual_value" value="' + esc(r.actual_value || '') + '">' +
              (elig && elig.met != null ? ' <span class="tag' + (elig.met ? ' tag-green' : ' alert') + '">' + (elig.met ? 'Met' : 'Not met') + '</span>' : '') + '</td>'
            : '<td><select data-ic="' + r.id + '" data-k="status">' + ['pending', 'done', 'na', 'not_met'].map(s => '<option value="' + s + '"' + (r.status === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>') +
          '<td><input data-ic="' + r.id + '" data-k="remark" value="' + esc(r.remark || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-ic="' + r.id + '" title="Remove">Remove</button></td></tr>';
        }).join('') + '</tbody></table></div>' +
        '<button class="btn btn-small" id="ipo-add-ic" style="margin-top:10px">Add item</button> ' +
        '<button class="btn btn-small btn-ghost" id="ipo-seed-ic" title="Add the standard items, skipping any already present">Seed standard checklist</button></div>';

      root.querySelectorAll('[data-ipo-area]').forEach(el => el.addEventListener('click', e => {
        ipoState.checklistArea = e.target.dataset.ipoArea; ipoRenderChecklistTab(root);
      }));
      root.querySelectorAll('[data-ic]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k;
        try { await api('/api/ipo/checklist/' + e.target.dataset.ic, { method: 'PUT', body: { [k]: e.target.value } }); ipoRenderChecklistTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-ic]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ipo/checklist/' + e.target.dataset.delIc, { method: 'DELETE' }); ipoRenderChecklistTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addIc = document.getElementById('ipo-add-ic');
      if (addIc) addIc.addEventListener('click', async () => {
        try { await api('/api/ipo/clients/' + ipoState.clientId + '/checklist', { method: 'POST', body: { area: ipoState.checklistArea, criterion: 'New criterion' } }); ipoRenderChecklistTab(root); }
        catch (err) { toast(err.message, true); }
      });
      const seedIc = document.getElementById('ipo-seed-ic');
      if (seedIc) seedIc.addEventListener('click', async () => {
        try { const r = await api('/api/ipo/clients/' + ipoState.clientId + '/checklist/seed', { method: 'POST', body: { area: ipoState.checklistArea } }); toast(r.added + ' items added'); ipoRenderChecklistTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function ipoRenderRestatementTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/ipo/clients/' + ipoState.clientId + '/restatements'), api('/api/ipo/clients/' + ipoState.clientId + '/restatement-analysis')])
      .then(([rows, analysis]) => {
      root.innerHTML = '<div class="grid c3">' +
        kpi('Lines restated', analysis.lines_restated) +
        kpi('Total absolute impact', money(analysis.total_absolute_impact)) +
        kpi('Fiscal years affected', Object.keys(analysis.impact_by_fy).length) +
        '</div>' +
        '<div class="card"><h3>Restatement register</h3><p class="card-note">As-previously-reported vs restated figures, and why each changed — standard prospectus disclosure.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>FY</th><th>Account</th><th class="num">Previously reported</th>' +
        '<th class="num">Restated</th><th class="num">Variance</th><th>Reason</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-rl="' + r.id + '" data-k="fy_label" value="' + esc(r.fy_label || '') + '"></td>' +
          '<td><input data-rl="' + r.id + '" data-k="account" value="' + esc(r.account || '') + '"></td>' +
          '<td class="num"><input type="number" data-rl="' + r.id + '" data-k="previously_reported" value="' + (r.previously_reported || 0) + '"></td>' +
          '<td class="num"><input type="number" data-rl="' + r.id + '" data-k="restated" value="' + (r.restated || 0) + '"></td>' +
          '<td class="num">' + money(r.restated - r.previously_reported) + '</td>' +
          '<td><input data-rl="' + r.id + '" data-k="reason" value="' + esc(r.reason || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-rl="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="ipo-add-rl" style="margin-top:10px">Add a restatement line</button></div>';

      root.querySelectorAll('[data-rl]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = (k === 'previously_reported' || k === 'restated') ? Number(e.target.value) : e.target.value;
        try { await api('/api/ipo/restatements/' + e.target.dataset.rl, { method: 'PUT', body: { [k]: v } }); ipoRenderRestatementTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-rl]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ipo/restatements/' + e.target.dataset.delRl, { method: 'DELETE' }); ipoRenderRestatementTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addRl = document.getElementById('ipo-add-rl');
      if (addRl) addRl.addEventListener('click', async () => {
        try { await api('/api/ipo/clients/' + ipoState.clientId + '/restatements', { method: 'POST', body: { fy_label: state.fy, account: 'New account' } }); ipoRenderRestatementTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function ipoRenderWorkingCapitalTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/ipo/clients/' + ipoState.clientId + '/working-capital'), api('/api/ipo/clients/' + ipoState.clientId + '/working-capital-adequacy')])
      .then(([rows, adequacy]) => {
      root.innerHTML = '<div class="grid c3">' +
        kpi('Overall adequacy', adequacy.overall_adequate ? 'Adequate' : 'Shortfall', adequacy.overall_adequate ? 'accent' : 'alert') +
        kpi('Shortfall periods', adequacy.shortfall_periods, adequacy.shortfall_periods ? 'alert' : '') +
        kpi('Max cumulative gap', money(adequacy.max_cumulative_gap)) +
        '</div>' +
        '<div class="card"><h3>Working capital adequacy for the offer document</h3><p class="card-note">Does internal accrual plus the IPO proceeds allocated to working capital cover the projected requirement, period by period?</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Period</th><th class="num">Projected requirement</th>' +
        '<th class="num">From operations</th><th class="num">IPO proceeds allocated</th><th class="num">Gap</th><th></th></tr></thead><tbody>' +
        rows.map(r => {
          const a = adequacy.rows.find(x => x.id === r.id) || {};
          return '<tr><td><input data-wc="' + r.id + '" data-k="period_label" value="' + esc(r.period_label || '') + '"></td>' +
          '<td class="num"><input type="number" data-wc="' + r.id + '" data-k="projected_requirement" value="' + (r.projected_requirement || 0) + '"></td>' +
          '<td class="num"><input type="number" data-wc="' + r.id + '" data-k="available_from_operations" value="' + (r.available_from_operations || 0) + '"></td>' +
          '<td class="num"><input type="number" data-wc="' + r.id + '" data-k="ipo_proceeds_allocated" value="' + (r.ipo_proceeds_allocated || 0) + '"></td>' +
          '<td class="num"><span class="tag' + (a.adequate === false ? ' alert' : '') + '">' + (a.gap != null ? money(a.gap) : '—') + '</span></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-wc="' + r.id + '" title="Remove">Remove</button></td></tr>';
        }).join('') + '</tbody></table></div><button class="btn btn-small" id="ipo-add-wc" style="margin-top:10px">Add a period</button></div>';

      root.querySelectorAll('[data-wc]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/ipo/working-capital/' + e.target.dataset.wc, { method: 'PUT', body: { [k]: v } }); ipoRenderWorkingCapitalTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-wc]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ipo/working-capital/' + e.target.dataset.delWc, { method: 'DELETE' }); ipoRenderWorkingCapitalTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addWc = document.getElementById('ipo-add-wc');
      if (addWc) addWc.addEventListener('click', async () => {
        try { await api('/api/ipo/clients/' + ipoState.clientId + '/working-capital', { method: 'POST', body: { period_label: 'New period' } }); ipoRenderWorkingCapitalTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function ipoRenderPeersTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/ipo/clients/' + ipoState.clientId + '/peers'), api('/api/ipo/clients/' + ipoState.clientId + '/peer-comparison')])
      .then(([rows, comparison]) => {
      root.innerHTML = '<div class="grid c3">' +
        kpi('Average peer P/E', comparison.avg_pe != null ? comparison.avg_pe : '—') +
        kpi('Median peer P/E', comparison.median_pe != null ? comparison.median_pe : '—') +
        kpi('Average peer P/BV', comparison.avg_pbv != null ? comparison.avg_pbv : '—') +
        '</div>' +
        '<div class="card"><h3>Peer register</h3><div class="table-wrap"><table><thead><tr><th>Peer</th><th>Listed</th>' +
        '<th class="num">Market price</th><th class="num">EPS</th><th class="num">Book value/share</th><th class="num">P/E</th><th class="num">P/BV</th><th></th></tr></thead><tbody>' +
        rows.map(r => {
          const c = comparison.peers.find(p => p.id === r.id) || {};
          return '<tr><td><input data-pe="' + r.id + '" data-k="peer_name" value="' + esc(r.peer_name || '') + '"></td>' +
          '<td><input type="checkbox" data-pe-flag="' + r.id + '"' + (r.listed ? ' checked' : '') + '></td>' +
          '<td class="num"><input type="number" data-pe="' + r.id + '" data-k="market_price" value="' + (r.market_price ?? '') + '"></td>' +
          '<td class="num"><input type="number" data-pe="' + r.id + '" data-k="eps" value="' + (r.eps ?? '') + '"></td>' +
          '<td class="num"><input type="number" data-pe="' + r.id + '" data-k="book_value_per_share" value="' + (r.book_value_per_share ?? '') + '"></td>' +
          '<td class="num">' + (c.pe_ratio != null ? c.pe_ratio : '—') + '</td>' +
          '<td class="num">' + (c.pbv_ratio != null ? c.pbv_ratio : '—') + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-pe="' + r.id + '" title="Remove">Remove</button></td></tr>';
        }).join('') + '</tbody></table></div><button class="btn btn-small" id="ipo-add-pe" style="margin-top:10px">Add a peer</button></div>';

      root.querySelectorAll('[data-pe]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/ipo/peers/' + e.target.dataset.pe, { method: 'PUT', body: { [k]: v } }); ipoRenderPeersTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-pe-flag]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/ipo/peers/' + e.target.dataset.peFlag, { method: 'PUT', body: { listed: e.target.checked } }); ipoRenderPeersTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-pe]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ipo/peers/' + e.target.dataset.delPe, { method: 'DELETE' }); ipoRenderPeersTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addPe = document.getElementById('ipo-add-pe');
      if (addPe) addPe.addEventListener('click', async () => {
        try { await api('/api/ipo/clients/' + ipoState.clientId + '/peers', { method: 'POST', body: { peer_name: 'New peer' } }); ipoRenderPeersTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function ipoRenderPricingTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ipo/clients/' + ipoState.clientId + '/pricing').then(rows => {
      root.innerHTML = '<div class="card"><h3>Pricing analysis</h3><p class="card-note">Enter the valuation-per-share figure yourself, carried over from the Valuation module\'s concluded value — never read automatically.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Scenario</th><th class="num">Face value</th><th class="num">Proposed price</th>' +
        '<th class="num">Subject EPS</th><th class="num">Valuation/share</th><th class="num">Implied P/E</th><th class="num">Gap vs valuation</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-pr="' + r.id + '" data-k="label" value="' + esc(r.label || '') + '"></td>' +
          '<td class="num"><input type="number" data-pr="' + r.id + '" data-k="face_value" value="' + (r.face_value || 100) + '"></td>' +
          '<td class="num"><input type="number" data-pr="' + r.id + '" data-k="proposed_price" value="' + (r.proposed_price || 0) + '"></td>' +
          '<td class="num"><input type="number" data-pr="' + r.id + '" data-k="subject_eps" value="' + (r.subject_eps ?? '') + '"></td>' +
          '<td class="num"><input type="number" data-pr="' + r.id + '" data-k="valuation_per_share" value="' + (r.valuation_per_share ?? '') + '"></td>' +
          '<td class="num">' + (r.computed.implied_pe != null ? r.computed.implied_pe : '—') + '</td>' +
          '<td class="num">' + (r.computed.gap_vs_valuation != null ? money(r.computed.gap_vs_valuation) : '—') + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-pr="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="ipo-add-pr" style="margin-top:10px">Add a pricing scenario</button></div>';

      root.querySelectorAll('[data-pr]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = k === 'label' ? e.target.value : Number(e.target.value);
        try { await api('/api/ipo/pricing/' + e.target.dataset.pr, { method: 'PUT', body: { [k]: v } }); ipoRenderPricingTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-pr]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ipo/pricing/' + e.target.dataset.delPr, { method: 'DELETE' }); ipoRenderPricingTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addPr = document.getElementById('ipo-add-pr');
      if (addPr) addPr.addEventListener('click', async () => {
        try { await api('/api/ipo/clients/' + ipoState.clientId + '/pricing', { method: 'POST', body: { label: 'New scenario', face_value: 100 } }); ipoRenderPricingTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function ipoRenderRisksTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/ipo/clients/' + ipoState.clientId + '/risks').then(rows => {
      root.innerHTML = '<div class="card"><h3>Risk factors</h3><p class="card-note">Standard prospectus "Risk Factors" content — one row per risk.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Severity</th><th>Category</th><th>Description</th><th>Mitigation</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><select data-rf="' + r.id + '" data-k="severity">' + ['Low', 'Medium', 'High'].map(s => '<option value="' + s + '"' + (r.severity === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>' +
          '<td><input data-rf="' + r.id + '" data-k="category" value="' + esc(r.category || '') + '"></td>' +
          '<td><input data-rf="' + r.id + '" data-k="description" value="' + esc(r.description || '') + '"></td>' +
          '<td><input data-rf="' + r.id + '" data-k="mitigation" value="' + esc(r.mitigation || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-rf="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="ipo-add-rf" style="margin-top:10px">Add a risk factor</button></div>';

      root.querySelectorAll('[data-rf]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/ipo/risks/' + e.target.dataset.rf, { method: 'PUT', body: { [e.target.dataset.k]: e.target.value } }); ipoRenderRisksTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-rf]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/ipo/risks/' + e.target.dataset.delRf, { method: 'DELETE' }); ipoRenderRisksTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addRf = document.getElementById('ipo-add-rf');
      if (addRf) addRf.addEventListener('click', async () => {
        try { await api('/api/ipo/clients/' + ipoState.clientId + '/risks', { method: 'POST', body: { description: 'New risk factor', severity: 'Medium' } }); ipoRenderRisksTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function ipoRenderReportsTab(root) {
    root.innerHTML = '<div class="card"><h3>Reports</h3><p class="card-note"><b>IPO Consultation Report</b> — readiness, eligibility, restatement, working capital adequacy, peer comparison, pricing and risk factors, in one document.</p>' +
      '<button class="btn btn-small" data-ipo-report="xlsx">Excel</button> <button class="btn btn-small" data-ipo-report="docx">Word</button></div>';
    root.querySelectorAll('[data-ipo-report]').forEach(el => el.addEventListener('click', async e => {
      try {
        const res = await api('/api/ipo/clients/' + ipoState.clientId + '/reports', { method: 'POST', body: { format: e.target.dataset.ipoReport } });
        toast('Generated ' + res.filename);
        window.open(res.download_url, '_blank');
      } catch (err) { toast(err.message, true); }
    }));
  }

  // ------------------------------------------------------------------ tax advisory
  const TAX_TABS = [
    ['dashboard', 'Dashboard'], ['vatsales', 'VAT sales'], ['vatpurchases', 'VAT purchases'],
    ['tds', 'TDS reconciliation'], ['provision', 'Deferred tax'], ['summary', 'Computation summary'],
    ['deductibility', 'Deductibility'], ['perisk', 'PE risk'], ['cases', 'Cases & notices'], ['reports', 'Reports'],
  ];
  const taxState = { clientId: '', tab: 'dashboard', vatPeriod: '', activeCaseId: null };

  function renderTaxAdvisory() {
    const view = document.getElementById('view');
    if (!state.online || !state.token || state.offlineMode) {
      view.innerHTML = '<div class="page-head"><div><h2>Tax Advisory</h2></div></div>' +
        '<div class="card"><div class="empty"><b>Offline</b>Tax Advisory works against live server data — reconnect to use this module.</div></div>';
      return;
    }
    if (!taxState.clientId) taxState.clientId = localStorage.getItem('ca.client.tax') || (clients()[0] && clients()[0].id) || '';

    view.innerHTML =
      '<div class="page-head"><div><h2>Tax Advisory</h2>' +
      '<p>VAT and TDS reconciliation, deferred tax provision, minimum tax check, a deductibility register, permanent establishment risk and tax notice/appeal case management. Income tax, TDS, VAT and depreciation computation themselves are NOT duplicated here — use the Business/Personal Income Tax, TDS, VAT and Depreciation modules; this module works alongside them.</p></div></div>' +
      '<div class="card"><div class="grid c3">' +
      '<div class="field"><label>Client</label><select id="tax-client"><option value="">— choose —</option>' +
      clients().map(c => '<option value="' + c.id + '"' + (String(taxState.clientId) === String(c.id) ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') + '</select></div>' +
      '</div></div>' +
      '<div class="card" id="tax-body"><div class="empty">Loading…</div></div>';

    document.getElementById('tax-client').addEventListener('change', e => {
      taxState.clientId = e.target.value; localStorage.setItem('ca.client.tax', taxState.clientId);
      taxState.tab = 'dashboard';
      taxRenderBody();
    });
    taxRenderBody();
  }

  function taxRenderBody() {
    const body = document.getElementById('tax-body');
    if (!taxState.clientId) { body.innerHTML = '<div class="empty"><b>Choose a client</b>Pick a client above to begin.</div>'; return; }
    body.innerHTML = '<div style="margin-bottom:14px">' +
      TAX_TABS.map(t => '<button class="btn btn-small' + (taxState.tab === t[0] ? ' btn-primary' : ' btn-ghost') +
        '" data-tax-tab="' + t[0] + '" style="margin:0 6px 6px 0">' + esc(t[1]) + '</button>').join('') +
      '</div><div id="tax-tab-body"></div>';
    body.querySelectorAll('[data-tax-tab]').forEach(b => b.addEventListener('click', e => {
      taxState.tab = e.target.dataset.taxTab; taxRenderBody();
    }));
    const tb = document.getElementById('tax-tab-body');
    ({ dashboard: taxRenderDashboardTab, vatsales: taxRenderVatSalesTab, vatpurchases: taxRenderVatPurchasesTab,
       tds: taxRenderTdsTab, provision: taxRenderProvisionTab, summary: taxRenderSummaryTab,
       deductibility: taxRenderDeductibilityTab, perisk: taxRenderPeRiskTab, cases: taxRenderCasesTab,
       reports: taxRenderReportsTab }[taxState.tab] || taxRenderDashboardTab)(tb);
  }

  function taxRenderDashboardTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/tax/clients/' + taxState.clientId + '/dashboard').then(d => {
      root.innerHTML = '<div class="grid c4">' +
        kpi('TDS short-deposited', d.tds_reconciliation.short_deposited_count, d.tds_reconciliation.short_deposited_count ? 'alert' : 'accent') +
        kpi('TDS returns not filed', d.tds_reconciliation.not_filed_count, d.tds_reconciliation.not_filed_count ? 'alert' : '') +
        kpi('PE risk', d.pe_risk.overall_risk, d.pe_risk.overall_risk === 'High' ? 'alert' : '') +
        kpi('Open tax cases', d.open_cases, d.open_cases ? 'alert' : '') +
        '</div>' +
        '<div class="card"><b>Deductibility items under review:</b> ' + d.deductibility_under_review + '</div>' +
        '<div class="card"><b>Cross-links:</b> Income tax computation — Business/Personal Income Tax modules. TDS/VAT return figures themselves — TDS and VAT modules. Related-party tax exposure — Transfer Pricing module.</div>';
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function taxRenderVatSalesTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/tax/clients/' + taxState.clientId + '/vat-sales'), api('/api/tax/clients/' + taxState.clientId + '/vat-sales-reconciliation')])
      .then(([rows, recon]) => {
      root.innerHTML = '<div class="grid c3">' +
        kpi('Total output VAT (books)', money(recon.total_output_vat)) +
        kpi('Undeclared', recon.undeclared_count, recon.undeclared_count ? 'alert' : 'accent') +
        kpi('Undeclared output VAT', money(recon.undeclared_output_vat), recon.undeclared_output_vat ? 'alert' : '') +
        '</div>' +
        '<div class="card"><h3>Sales VAT register</h3><p class="card-note">Marking a line "declared in return" is how this compares books to what was actually filed — Sales VAT Reconciliation Tool.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Period</th><th>Invoice</th><th>Party</th><th class="num">Taxable sales</th>' +
        '<th class="num">Output VAT</th><th>Declared</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-vs="' + r.id + '" data-k="period_label" value="' + esc(r.period_label || '') + '"></td>' +
          '<td><input data-vs="' + r.id + '" data-k="invoice_no" value="' + esc(r.invoice_no || '') + '"></td>' +
          '<td><input data-vs="' + r.id + '" data-k="party" value="' + esc(r.party || '') + '"></td>' +
          '<td class="num"><input type="number" data-vs="' + r.id + '" data-k="taxable_sales" value="' + (r.taxable_sales || 0) + '"></td>' +
          '<td class="num"><input type="number" data-vs="' + r.id + '" data-k="output_vat" value="' + (r.output_vat || 0) + '"></td>' +
          '<td><input type="checkbox" data-vs-flag="' + r.id + '"' + (r.declared_in_return ? ' checked' : '') + '></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-vs="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="tax-add-vs" style="margin-top:10px">Add a sales line</button></div>';

      root.querySelectorAll('[data-vs]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/tax/vat-sales/' + e.target.dataset.vs, { method: 'PUT', body: { [k]: v } }); taxRenderVatSalesTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-vs-flag]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/tax/vat-sales/' + e.target.dataset.vsFlag, { method: 'PUT', body: { declared_in_return: e.target.checked } }); taxRenderVatSalesTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-vs]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/tax/vat-sales/' + e.target.dataset.delVs, { method: 'DELETE' }); taxRenderVatSalesTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addVs = document.getElementById('tax-add-vs');
      if (addVs) addVs.addEventListener('click', async () => {
        try { await api('/api/tax/clients/' + taxState.clientId + '/vat-sales', { method: 'POST', body: { period_label: 'New period' } }); taxRenderVatSalesTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function taxRenderVatPurchasesTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/tax/clients/' + taxState.clientId + '/vat-purchases'), api('/api/tax/clients/' + taxState.clientId + '/vat-purchases-reconciliation')])
      .then(([rows, recon]) => {
      root.innerHTML = '<div class="grid c4">' +
        kpi('Total input VAT (books)', money(recon.total_input_vat)) +
        kpi('Eligible input VAT', money(recon.total_eligible_input_vat)) +
        kpi('Ineligible', money(recon.ineligible_input_vat), recon.ineligible_count ? 'alert' : '') +
        kpi('Unmatched with supplier', money(recon.unmatched_input_vat), recon.unmatched_count ? 'alert' : '') +
        '</div>' +
        '<div class="card"><h3>Purchase VAT register</h3><p class="card-note">"Eligible" is the Purchase VAT Eligibility Checker; "Matched with supplier" is the Input VAT Matching Automator — same register, two flags.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Period</th><th>Invoice</th><th>Party</th><th class="num">Taxable purchase</th>' +
        '<th class="num">Input VAT</th><th>Eligible</th><th>Matched</th><th>Ineligibility reason</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-vp="' + r.id + '" data-k="period_label" value="' + esc(r.period_label || '') + '"></td>' +
          '<td><input data-vp="' + r.id + '" data-k="invoice_no" value="' + esc(r.invoice_no || '') + '"></td>' +
          '<td><input data-vp="' + r.id + '" data-k="party" value="' + esc(r.party || '') + '"></td>' +
          '<td class="num"><input type="number" data-vp="' + r.id + '" data-k="taxable_purchase" value="' + (r.taxable_purchase || 0) + '"></td>' +
          '<td class="num"><input type="number" data-vp="' + r.id + '" data-k="input_vat" value="' + (r.input_vat || 0) + '"></td>' +
          '<td><input type="checkbox" data-vp-flag="' + r.id + '" data-k="eligible"' + (r.eligible ? ' checked' : '') + '></td>' +
          '<td><input type="checkbox" data-vp-flag="' + r.id + '" data-k="matched_with_supplier"' + (r.matched_with_supplier ? ' checked' : '') + '></td>' +
          '<td><input data-vp="' + r.id + '" data-k="ineligibility_reason" value="' + esc(r.ineligibility_reason || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-vp="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="tax-add-vp" style="margin-top:10px">Add a purchase line</button></div>';

      root.querySelectorAll('[data-vp]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/tax/vat-purchases/' + e.target.dataset.vp, { method: 'PUT', body: { [k]: v } }); taxRenderVatPurchasesTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-vp-flag]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/tax/vat-purchases/' + e.target.dataset.vpFlag, { method: 'PUT', body: { [e.target.dataset.k]: e.target.checked } }); taxRenderVatPurchasesTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-vp]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/tax/vat-purchases/' + e.target.dataset.delVp, { method: 'DELETE' }); taxRenderVatPurchasesTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addVp = document.getElementById('tax-add-vp');
      if (addVp) addVp.addEventListener('click', async () => {
        try { await api('/api/tax/clients/' + taxState.clientId + '/vat-purchases', { method: 'POST', body: { period_label: 'New period', eligible: true } }); taxRenderVatPurchasesTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function taxRenderTdsTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/tax/clients/' + taxState.clientId + '/tds-reconciliation'), api('/api/tax/clients/' + taxState.clientId + '/tds-reconciliation-summary')])
      .then(([rows, summary]) => {
      root.innerHTML = '<div class="grid c4">' +
        kpi('Total deducted', money(summary.total_deducted)) +
        kpi('Total deposited', money(summary.total_deposited)) +
        kpi('Short-deposited lines', summary.short_deposited_count, summary.short_deposited_count ? 'alert' : 'accent') +
        kpi('Returns not filed', summary.not_filed_count, summary.not_filed_count ? 'alert' : '') +
        '</div>' +
        '<div class="card"><h3>TDS reconciliation</h3><div class="table-wrap"><table><thead><tr><th>FY</th><th>Section</th><th>Payee</th>' +
        '<th class="num">Base amount</th><th class="num">Deducted</th><th class="num">Deposited</th><th>Return filed</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-tr="' + r.id + '" data-k="fy_label" value="' + esc(r.fy_label || '') + '"></td>' +
          '<td><input data-tr="' + r.id + '" data-k="section" value="' + esc(r.section || '') + '"></td>' +
          '<td><input data-tr="' + r.id + '" data-k="payee" value="' + esc(r.payee || '') + '"></td>' +
          '<td class="num"><input type="number" data-tr="' + r.id + '" data-k="base_amount" value="' + (r.base_amount || 0) + '"></td>' +
          '<td class="num"><input type="number" data-tr="' + r.id + '" data-k="tds_deducted" value="' + (r.tds_deducted || 0) + '"></td>' +
          '<td class="num"><input type="number" data-tr="' + r.id + '" data-k="tds_deposited" value="' + (r.tds_deposited || 0) + '"></td>' +
          '<td><input type="checkbox" data-tr-flag="' + r.id + '"' + (r.return_filed ? ' checked' : '') + '></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-tr="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="tax-add-tr" style="margin-top:10px">Add a line</button></div>';

      root.querySelectorAll('[data-tr]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/tax/tds-reconciliation/' + e.target.dataset.tr, { method: 'PUT', body: { [k]: v } }); taxRenderTdsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-tr-flag]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/tax/tds-reconciliation/' + e.target.dataset.trFlag, { method: 'PUT', body: { return_filed: e.target.checked } }); taxRenderTdsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-tr]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/tax/tds-reconciliation/' + e.target.dataset.delTr, { method: 'DELETE' }); taxRenderTdsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addTr = document.getElementById('tax-add-tr');
      if (addTr) addTr.addEventListener('click', async () => {
        try { await api('/api/tax/clients/' + taxState.clientId + '/tds-reconciliation', { method: 'POST', body: { fy_label: state.fy } }); taxRenderTdsTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function taxRenderProvisionTab(root, taxRatePct) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    const rate = taxRatePct || 25;
    const rateInputId = 'tax-prov-rate';
    Promise.all([api('/api/tax/clients/' + taxState.clientId + '/provision'), api('/api/tax/clients/' + taxState.clientId + '/deferred-tax?tax_rate_pct=' + rate)])
      .then(([rows, dt]) => {
      root.innerHTML = '<div class="card"><h3>Deferred tax provision</h3><p class="card-note">Book value vs tax value per item — a positive net temporary difference (book above tax) computes as a net deferred tax liability, negative as an asset.</p>' +
        '<div class="grid c3"><div class="field"><label>Tax rate %</label><input type="number" id="' + rateInputId + '" value="' + rate + '"></div></div>' +
        '<div class="grid c3" style="margin-top:10px">' +
        kpi('Net temporary difference', money(dt.net_temporary_difference)) +
        kpi(dt.classification, money(dt.net_deferred_tax), dt.classification.indexOf('liability') !== -1 ? 'alert' : 'accent') +
        '<div></div></div>' +
        '<div class="table-wrap" style="margin-top:10px"><table><thead><tr><th>Particular</th><th class="num">Book value</th>' +
        '<th class="num">Tax value</th><th class="num">Temporary difference</th><th class="num">Deferred tax</th><th></th></tr></thead><tbody>' +
        rows.map(r => {
          const c = dt.rows.find(x => x.id === r.id) || {};
          return '<tr><td><input data-pv="' + r.id + '" data-k="particular" value="' + esc(r.particular || '') + '"></td>' +
          '<td class="num"><input type="number" data-pv="' + r.id + '" data-k="book_value" value="' + (r.book_value || 0) + '"></td>' +
          '<td class="num"><input type="number" data-pv="' + r.id + '" data-k="tax_value" value="' + (r.tax_value || 0) + '"></td>' +
          '<td class="num">' + (c.temporary_difference != null ? money(c.temporary_difference) : '—') + '</td>' +
          '<td class="num">' + (c.deferred_tax != null ? money(c.deferred_tax) : '—') + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-pv="' + r.id + '" title="Remove">Remove</button></td></tr>';
        }).join('') + '</tbody></table></div><button class="btn btn-small" id="tax-add-pv" style="margin-top:10px">Add an item</button></div>';

      const rateEl = document.getElementById(rateInputId);
      rateEl.addEventListener('change', () => { taxRenderProvisionTab(root, Number(rateEl.value) || 25); });
      root.querySelectorAll('[data-pv]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/tax/provision/' + e.target.dataset.pv, { method: 'PUT', body: { [k]: v } }); taxRenderProvisionTab(root, rate); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-pv]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/tax/provision/' + e.target.dataset.delPv, { method: 'DELETE' }); taxRenderProvisionTab(root, rate); }
        catch (err) { toast(err.message, true); }
      }));
      const addPv = document.getElementById('tax-add-pv');
      if (addPv) addPv.addEventListener('click', async () => {
        try { await api('/api/tax/clients/' + taxState.clientId + '/provision', { method: 'POST', body: { fy_label: state.fy, particular: 'New item' } }); taxRenderProvisionTab(root, rate); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function taxRenderSummaryTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/tax/clients/' + taxState.clientId + '/computation-summary?fy_label=' + encodeURIComponent(state.fy)).then(s => {
      root.innerHTML = '<div class="card"><h3>Computation summary — FY ' + esc(state.fy) + '</h3><p class="card-note">Carried over by hand from the Business/Personal Income Tax module\'s own computation — used for the audit support pack and the minimum tax check. Never read automatically from that module.</p>' +
        '<div class="grid c3" id="tax-summary-form">' +
        field('Entity type', 'entity_type', s, 'text') +
        field('Book profit', 'book_profit', s, 'number') +
        field('Total adjustments', 'total_adjustments', s, 'number') +
        field('Taxable income', 'taxable_income', s, 'number') +
        field('Tax rate %', 'tax_rate_pct', s, 'number') +
        field('Tax liability', 'tax_liability', s, 'number') +
        field('Turnover', 'turnover', s, 'number') +
        field('Minimum tax % (confirm before relying on this)', 'minimum_tax_pct', s, 'number') +
        '<div class="field" style="grid-column:1/-1"><label>Notes</label><textarea data-key="notes" rows="2">' + esc(s.notes || '') + '</textarea></div>' +
        '</div><button class="btn btn-small" id="tax-save-summary" style="margin-top:10px">Save</button></div>';

      if (s.minimum_tax_check && s.minimum_tax_check.minimum_tax_pct != null) {
        root.innerHTML += '<div class="card"><h3>Minimum tax check</h3><div class="grid c3">' +
          kpi('Minimum tax amount', money(s.minimum_tax_check.minimum_tax_amount)) +
          kpi('Shortfall', money(s.minimum_tax_check.shortfall), s.minimum_tax_check.minimum_tax_binding ? 'alert' : 'accent') +
          kpi('Binding', s.minimum_tax_check.minimum_tax_binding ? 'Yes' : 'No', s.minimum_tax_check.minimum_tax_binding ? 'alert' : '') +
          '</div></div>';
      }
      wireInto(root, 'tax-summary-form', s, () => {});
      document.getElementById('tax-save-summary').addEventListener('click', async () => {
        s.fy_label = state.fy;
        try { await api('/api/tax/clients/' + taxState.clientId + '/computation-summary', { method: 'PUT', body: s }); toast('Saved'); taxRenderSummaryTab(root); }
        catch (e) { toast(e.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function taxRenderDeductibilityTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/tax/clients/' + taxState.clientId + '/deductibility').then(rows => {
      root.innerHTML = '<div class="card"><h3>Tax deductibility checker</h3><p class="card-note">Facts decide deductibility — this is a structured register to work through, not an automatic rule.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>FY</th><th>Description</th><th class="num">Amount</th><th>Category</th><th>Status</th><th>Reason</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-dc="' + r.id + '" data-k="fy_label" value="' + esc(r.fy_label || '') + '"></td>' +
          '<td><input data-dc="' + r.id + '" data-k="description" value="' + esc(r.description || '') + '"></td>' +
          '<td class="num"><input type="number" data-dc="' + r.id + '" data-k="amount" value="' + (r.amount || 0) + '"></td>' +
          '<td><select data-dc="' + r.id + '" data-k="category"><option value="">— select —</option>' +
          ['Domestic/personal', 'Not wholly for business', 'Fine or penalty', 'General provision', 'Capital nature',
           'Entertainment/gift', "Related-party not arm's length", 'Other'].map(c => '<option value="' + c + '"' + (r.category === c ? ' selected' : '') + '>' + c + '</option>').join('') + '</select></td>' +
          '<td><select data-dc="' + r.id + '" data-k="status">' + ['under_review', 'deductible', 'not_deductible'].map(s => '<option value="' + s + '"' + (r.status === s ? ' selected' : '') + '>' + s.replace('_', ' ') + '</option>').join('') + '</select></td>' +
          '<td><input data-dc="' + r.id + '" data-k="reason" value="' + esc(r.reason || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-dc="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="tax-add-dc" style="margin-top:10px">Add an item</button></div>';

      root.querySelectorAll('[data-dc]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/tax/deductibility/' + e.target.dataset.dc, { method: 'PUT', body: { [k]: v } }); taxRenderDeductibilityTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-dc]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/tax/deductibility/' + e.target.dataset.delDc, { method: 'DELETE' }); taxRenderDeductibilityTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addDc = document.getElementById('tax-add-dc');
      if (addDc) addDc.addEventListener('click', async () => {
        try { await api('/api/tax/clients/' + taxState.clientId + '/deductibility', { method: 'POST', body: { fy_label: state.fy, description: 'New item' } }); taxRenderDeductibilityTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function taxRenderPeRiskTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/tax/clients/' + taxState.clientId + '/pe-risk'), api('/api/tax/clients/' + taxState.clientId + '/pe-risk-assessment')])
      .then(([rows, assessment]) => {
      root.innerHTML = '<div class="grid c3">' +
        kpi('Overall PE risk', assessment.overall_risk, assessment.overall_risk === 'High' ? 'alert' : (assessment.overall_risk === 'Medium' ? '' : 'accent')) +
        kpi('Factors present', assessment.present_count, assessment.present_count ? 'alert' : '') +
        '<div></div></div>' +
        '<div class="card"><h3>Permanent establishment risk factors</h3>' +
        '<div class="table-wrap"><table><thead><tr><th>Factor</th><th>Present</th><th>Risk level</th><th>Details</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-pf="' + r.id + '" data-k="factor" value="' + esc(r.factor || '') + '"></td>' +
          '<td><input type="checkbox" data-pf-flag="' + r.id + '"' + (r.present ? ' checked' : '') + '></td>' +
          '<td><select data-pf="' + r.id + '" data-k="risk_level">' + ['Low', 'Medium', 'High'].map(l => '<option value="' + l + '"' + (r.risk_level === l ? ' selected' : '') + '>' + l + '</option>').join('') + '</select></td>' +
          '<td><input data-pf="' + r.id + '" data-k="details" value="' + esc(r.details || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-pf="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="tax-add-pf">Add a factor</button> ' +
        '<button class="btn btn-small btn-ghost" id="tax-seed-pf" title="Add the standard PE risk factors, skipping any already present">Seed standard factors</button></div>';

      root.querySelectorAll('[data-pf]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k;
        try { await api('/api/tax/pe-risk/' + e.target.dataset.pf, { method: 'PUT', body: { [k]: e.target.value } }); taxRenderPeRiskTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-pf-flag]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/tax/pe-risk/' + e.target.dataset.pfFlag, { method: 'PUT', body: { present: e.target.checked } }); taxRenderPeRiskTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-pf]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/tax/pe-risk/' + e.target.dataset.delPf, { method: 'DELETE' }); taxRenderPeRiskTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addPf = document.getElementById('tax-add-pf');
      if (addPf) addPf.addEventListener('click', async () => {
        try { await api('/api/tax/clients/' + taxState.clientId + '/pe-risk', { method: 'POST', body: { factor: 'New factor' } }); taxRenderPeRiskTab(root); }
        catch (err) { toast(err.message, true); }
      });
      const seedPf = document.getElementById('tax-seed-pf');
      if (seedPf) seedPf.addEventListener('click', async () => {
        try { const r = await api('/api/tax/clients/' + taxState.clientId + '/pe-risk/seed', { method: 'POST' }); toast(r.added + ' factors added'); taxRenderPeRiskTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  const TAX_CASE_STAGES = ['notice_received', 'response_filed', 'assessment_order', 'administrative_review', 'revenue_tribunal', 'supreme_court', 'closed'];

  function taxRenderCasesTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/tax/clients/' + taxState.clientId + '/cases').then(cases => {
      root.innerHTML = '<div class="card"><h3>Tax notices, assessments &amp; appeals</h3><p class="card-note">Notice, assessment and appeal are stages of one case — track them here through Administrative Review, Revenue Tribunal and Supreme Court.</p>' +
        '<button class="btn btn-small" id="tax-add-case">Add a case</button></div>' +
        cases.map(c => '<details class="card" style="margin-top:10px"' + (taxState.activeCaseId === c.id ? ' open' : '') + '><summary><b>' + esc(c.subject) + '</b>' +
          ' <span class="tag' + (c.status === 'open' ? '' : ' tag-green') + '">' + esc(c.stage.replace(/_/g, ' ')) + '</span></summary>' +
          '<div class="grid c3" style="margin-top:10px">' +
          '<div class="field"><label>Case type</label><select data-case="' + c.id + '" data-k="case_type"><option value="notice"' + (c.case_type === 'notice' ? ' selected' : '') + '>Notice</option><option value="assessment"' + (c.case_type === 'assessment' ? ' selected' : '') + '>Assessment</option><option value="appeal"' + (c.case_type === 'appeal' ? ' selected' : '') + '>Appeal</option></select></div>' +
          '<div class="field"><label>Reference no.</label><input data-case="' + c.id + '" data-k="reference_no" value="' + esc(c.reference_no || '') + '"></div>' +
          '<div class="field"><label>Authority</label><input data-case="' + c.id + '" data-k="authority" value="' + esc(c.authority || '') + '"></div>' +
          '<div class="field"><label>FY</label><input data-case="' + c.id + '" data-k="fy_label" value="' + esc(c.fy_label || '') + '"></div>' +
          '<div class="field"><label>Amount demanded</label><input type="number" data-case="' + c.id + '" data-k="amount_demanded" value="' + (c.amount_demanded ?? '') + '"></div>' +
          '<div class="field"><label>Due date</label><input type="date" data-case="' + c.id + '" data-k="due_date" value="' + esc(c.due_date || '') + '"></div>' +
          '<div class="field"><label>Stage</label><select data-case="' + c.id + '" data-k="stage">' + TAX_CASE_STAGES.map(s => '<option value="' + s + '"' + (c.stage === s ? ' selected' : '') + '>' + s.replace(/_/g, ' ') + '</option>').join('') + '</select></div>' +
          '<div class="field"><label>Status</label><select data-case="' + c.id + '" data-k="status"><option value="open"' + (c.status === 'open' ? ' selected' : '') + '>Open</option><option value="closed"' + (c.status === 'closed' ? ' selected' : '') + '>Closed</option></select></div>' +
          '</div>' +
          '<div class="field" style="margin-top:10px"><label>Response text (for the Tax Notice Response document)</label><textarea data-case="' + c.id + '" data-k="response_text" rows="3" style="width:100%">' + esc(c.response_text || '') + '</textarea></div>' +
          '<div style="margin-top:10px"><button class="btn btn-small" data-view-issues="' + c.id + '">Issues</button> ' +
          '<button class="btn btn-small" data-case-doc="' + c.id + '">Generate response document</button> ' +
          '<button class="btn btn-small btn-danger" data-del-case="' + c.id + '">Remove case</button></div>' +
          (taxState.activeCaseId === c.id ? '<div id="tax-case-issues-' + c.id + '" style="margin-top:10px"><div class="empty">Loading…</div></div>' : '') +
          '</details>').join('');

      const addCase = document.getElementById('tax-add-case');
      if (addCase) addCase.addEventListener('click', async () => {
        try { await api('/api/tax/clients/' + taxState.clientId + '/cases', { method: 'POST', body: { subject: 'New case' } }); taxRenderCasesTab(root); }
        catch (err) { toast(err.message, true); }
      });
      root.querySelectorAll('[data-case]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = k === 'amount_demanded' ? (e.target.value === '' ? null : Number(e.target.value)) : e.target.value;
        try { await api('/api/tax/cases/' + e.target.dataset.case, { method: 'PUT', body: { [k]: v } }); toast('Saved'); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-case]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/tax/cases/' + e.target.dataset.delCase, { method: 'DELETE' }); taxRenderCasesTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-case-doc]').forEach(el => el.addEventListener('click', async e => {
        try { const res = await api('/api/tax/cases/' + e.target.dataset.caseDoc + '/response-document', { method: 'POST' }); toast('Generated ' + res.filename); window.open(res.download_url, '_blank'); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-view-issues]').forEach(el => el.addEventListener('click', e => {
        const id = Number(e.target.dataset.viewIssues);
        taxState.activeCaseId = taxState.activeCaseId === id ? null : id;
        taxRenderCasesTab(root);
        if (taxState.activeCaseId === id) taxRenderCaseIssues(id);
      }));
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function taxRenderCaseIssues(caseId) {
    const target = document.getElementById('tax-case-issues-' + caseId);
    if (!target) return;
    api('/api/tax/cases/' + caseId + '/issues').then(issues => {
      target.innerHTML = '<h4>Issues</h4><div class="table-wrap"><table><thead><tr><th>Issue</th><th>Our position</th>' +
        '<th>Department position</th><th class="num">Amount</th><th>Likelihood</th><th></th></tr></thead><tbody>' +
        issues.map(i => '<tr><td><input data-issue="' + i.id + '" data-k="issue_description" value="' + esc(i.issue_description || '') + '"></td>' +
          '<td><input data-issue="' + i.id + '" data-k="our_position" value="' + esc(i.our_position || '') + '"></td>' +
          '<td><input data-issue="' + i.id + '" data-k="department_position" value="' + esc(i.department_position || '') + '"></td>' +
          '<td class="num"><input type="number" data-issue="' + i.id + '" data-k="amount_involved" value="' + (i.amount_involved ?? '') + '"></td>' +
          '<td><select data-issue="' + i.id + '" data-k="likelihood">' + ['favorable', 'unfavorable', 'uncertain'].map(l => '<option value="' + l + '"' + (i.likelihood === l ? ' selected' : '') + '>' + l + '</option>').join('') + '</select></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-issue="' + i.id + '">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="tax-add-issue-' + caseId + '" style="margin-top:8px">Add an issue</button>';

      target.querySelectorAll('[data-issue]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = k === 'amount_involved' ? (e.target.value === '' ? null : Number(e.target.value)) : e.target.value;
        try { await api('/api/tax/case-issues/' + e.target.dataset.issue, { method: 'PUT', body: { [k]: v } }); toast('Saved'); }
        catch (err) { toast(err.message, true); }
      }));
      target.querySelectorAll('[data-del-issue]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/tax/case-issues/' + e.target.dataset.delIssue, { method: 'DELETE' }); taxRenderCaseIssues(caseId); }
        catch (err) { toast(err.message, true); }
      }));
      const addIssue = document.getElementById('tax-add-issue-' + caseId);
      if (addIssue) addIssue.addEventListener('click', async () => {
        try { await api('/api/tax/cases/' + caseId + '/issues', { method: 'POST', body: { issue_description: 'New issue' } }); taxRenderCaseIssues(caseId); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { target.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function taxRenderReportsTab(root) {
    root.innerHTML = '<div class="card"><h3>Reports</h3><p class="card-note"><b>Tax Audit Support Pack</b> — book-to-taxable reconciliation, minimum tax check, deferred tax, deductibility, TDS and VAT reconciliation for FY ' + esc(state.fy) + ', in one document.</p>' +
      '<button class="btn btn-small" data-tax-report="xlsx">Excel</button> <button class="btn btn-small" data-tax-report="docx">Word</button>' +
      '<p class="card-note" style="margin-top:14px">Tax notice response documents are generated per-case from the Cases &amp; notices tab.</p></div>';
    root.querySelectorAll('[data-tax-report]').forEach(el => el.addEventListener('click', async e => {
      try {
        const res = await api('/api/tax/clients/' + taxState.clientId + '/reports', { method: 'POST', body: { format: e.target.dataset.taxReport, fy_label: state.fy } });
        toast('Generated ' + res.filename);
        window.open(res.download_url, '_blank');
      } catch (err) { toast(err.message, true); }
    }));
  }

  // -------------------------------------------------------------- audit execution
  const AUDX_TABS = [
    ['dashboard', 'Dashboard'], ['sampling', 'Sampling'], ['testitems', 'Test items'],
    ['analytical', 'Analytical review'], ['goingconcern', 'Going concern'], ['checklist', 'Checklists'],
    ['reports', 'Reports'],
  ];
  const audxState = { clientId: '', tab: 'dashboard', testArea: '', checklistArea: 'Completion' };
  const AUDX_TEST_AREAS = ['Bank', 'Debtors', 'Creditors', 'Inventory', 'FixedAssets', 'CWIP', 'Revenue', 'Expense', 'Payroll', 'Vouching', 'Invoice', 'CutOff', 'Other'];
  const AUDX_CHECKLIST_AREAS = ['Subsequent Events', 'NFRS Compliance', 'FS Disclosure', 'Completion'];

  function renderAuditExecution() {
    const view = document.getElementById('view');
    if (!state.online || !state.token || state.offlineMode) {
      view.innerHTML = '<div class="page-head"><div><h2>Audit Execution</h2></div></div>' +
        '<div class="card"><div class="empty"><b>Offline</b>Audit Execution works against live server data — reconnect to use this module.</div></div>';
      return;
    }
    if (!audxState.clientId) audxState.clientId = localStorage.getItem('ca.client.audx') || (clients()[0] && clients()[0].id) || '';

    view.innerHTML =
      '<div class="page-head"><div><h2>Audit Execution</h2>' +
      '<p>Sampling selection, substantive testing (vouching, confirmations, reconciliations, physical verification, cut-off), analytical review, going concern and completion/disclosure/NFRS checklists. Planning, materiality, risk assessment (RMM) and the findings register are NOT duplicated here — use the Statutory Audit and Internal Audit modules; this is the fieldwork layer alongside them.</p></div></div>' +
      '<div class="card"><div class="grid c3">' +
      '<div class="field"><label>Client</label><select id="audx-client"><option value="">— choose —</option>' +
      clients().map(c => '<option value="' + c.id + '"' + (String(audxState.clientId) === String(c.id) ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') + '</select></div>' +
      '</div></div>' +
      '<div class="card" id="audx-body"><div class="empty">Loading…</div></div>';

    document.getElementById('audx-client').addEventListener('change', e => {
      audxState.clientId = e.target.value; localStorage.setItem('ca.client.audx', audxState.clientId);
      audxState.tab = 'dashboard';
      audxRenderBody();
    });
    audxRenderBody();
  }

  function audxRenderBody() {
    const body = document.getElementById('audx-body');
    if (!audxState.clientId) { body.innerHTML = '<div class="empty"><b>Choose a client</b>Pick a client above to begin.</div>'; return; }
    body.innerHTML = '<div style="margin-bottom:14px">' +
      AUDX_TABS.map(t => '<button class="btn btn-small' + (audxState.tab === t[0] ? ' btn-primary' : ' btn-ghost') +
        '" data-audx-tab="' + t[0] + '" style="margin:0 6px 6px 0">' + esc(t[1]) + '</button>').join('') +
      '</div><div id="audx-tab-body"></div>';
    body.querySelectorAll('[data-audx-tab]').forEach(b => b.addEventListener('click', e => {
      audxState.tab = e.target.dataset.audxTab; audxRenderBody();
    }));
    const tb = document.getElementById('audx-tab-body');
    ({ dashboard: audxRenderDashboardTab, sampling: audxRenderSamplingTab, testitems: audxRenderTestItemsTab,
       analytical: audxRenderAnalyticalTab, goingconcern: audxRenderGoingConcernTab, checklist: audxRenderChecklistTab,
       reports: audxRenderReportsTab }[audxState.tab] || audxRenderDashboardTab)(tb);
  }

  function audxRenderDashboardTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/audx/clients/' + audxState.clientId + '/dashboard').then(d => {
      root.innerHTML = '<div class="grid c4">' +
        kpi('Test items', d.test_items.total_items) +
        kpi('Exceptions', d.test_items.total_exceptions, d.test_items.total_exceptions ? 'alert' : 'accent') +
        kpi('Analytical flags', d.analytical.flagged_count, d.analytical.flagged_count ? 'alert' : '') +
        kpi('Going concern indicators present', d.going_concern.present_count, d.going_concern.present_count ? 'alert' : 'accent') +
        '</div>' +
        '<div class="card"><h3>Checklist completion</h3><div class="grid c4">' +
        Object.keys(d.checklist).map(area => '<div><b>' + esc(area) + '</b><p class="card-note">' + d.checklist[area].done + ' of ' + d.checklist[area].total + ' — ' + d.checklist[area].completion_pct.toFixed(0) + '%</p></div>').join('') +
        '</div></div>' +
        '<div class="card"><b>Cross-links:</b> Materiality, sample size and inherent/control risk (RMM) — Statutory Audit module. Risk-based planning, RACM and the findings register — Internal Audit module.</div>';
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function audxRenderSamplingTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/audx/clients/' + audxState.clientId + '/samples').then(rows => {
      root.innerHTML = '<div class="card"><h3>Sampling selection</h3><p class="card-note">Systematic sampling computes a deterministic interval and item list. Random, haphazard, monetary-unit and judgmental methods record the method and the items actually selected — this app does not simulate randomness.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>FY</th><th>Area</th><th>Population</th><th class="num">Pop. size</th>' +
        '<th>Method</th><th class="num">Sample size</th><th class="num">Start</th><th>Selected items</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-sm="' + r.id + '" data-k="fy_label" value="' + esc(r.fy_label || '') + '"></td>' +
          '<td><select data-sm="' + r.id + '" data-k="area">' + AUDX_TEST_AREAS.map(a => '<option value="' + a + '"' + (r.area === a ? ' selected' : '') + '>' + a + '</option>').join('') + '</select></td>' +
          '<td><input data-sm="' + r.id + '" data-k="population_description" value="' + esc(r.population_description || '') + '"></td>' +
          '<td class="num"><input type="number" data-sm="' + r.id + '" data-k="population_size" value="' + (r.population_size || 0) + '"></td>' +
          '<td><select data-sm="' + r.id + '" data-k="method">' + ['systematic', 'random', 'haphazard', 'monetary_unit', 'judgmental'].map(mth => '<option value="' + mth + '"' + (r.method === mth ? ' selected' : '') + '>' + mth.replace('_', ' ') + '</option>').join('') + '</select></td>' +
          '<td class="num"><input type="number" data-sm="' + r.id + '" data-k="sample_size" value="' + (r.sample_size || 0) + '"></td>' +
          '<td class="num"><input type="number" data-sm="' + r.id + '" data-k="start_point" value="' + (r.start_point ?? '') + '"></td>' +
          '<td><input data-sm="' + r.id + '" data-k="selected_items_note" value="' + esc(r.selected_items_note || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-sm="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="audx-add-sm" style="margin-top:10px">Add a sample selection</button></div>';

      root.querySelectorAll('[data-sm]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
        try { await api('/api/audx/samples/' + e.target.dataset.sm, { method: 'PUT', body: { [k]: v } }); audxRenderSamplingTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-sm]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/audx/samples/' + e.target.dataset.delSm, { method: 'DELETE' }); audxRenderSamplingTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addSm = document.getElementById('audx-add-sm');
      if (addSm) addSm.addEventListener('click', async () => {
        try { await api('/api/audx/clients/' + audxState.clientId + '/samples', { method: 'POST', body: { fy_label: state.fy, area: 'Bank', method: 'systematic', population_size: 0, sample_size: 0, start_point: 1 } }); audxRenderSamplingTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function audxRenderTestItemsTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    const qs = audxState.testArea ? '?area=' + encodeURIComponent(audxState.testArea) : '';
    Promise.all([api('/api/audx/clients/' + audxState.clientId + '/test-items' + qs), api('/api/audx/clients/' + audxState.clientId + '/test-item-summary')])
      .then(([rows, summary]) => {
      root.innerHTML = '<div style="margin-bottom:12px"><select id="audx-area-filter"><option value="">— all areas —</option>' +
        AUDX_TEST_AREAS.map(a => '<option value="' + a + '"' + (audxState.testArea === a ? ' selected' : '') + '>' + a + '</option>').join('') + '</select></div>' +
        '<div class="grid c3">' +
        kpi('Total items', summary.total_items) +
        kpi('Exceptions', summary.total_exceptions, summary.total_exceptions ? 'alert' : 'accent') +
        kpi('Overall exception rate', summary.overall_exception_rate_pct.toFixed(1) + '%', summary.overall_exception_rate_pct ? 'alert' : '') +
        '</div>' +
        '<div class="card"><h3>Test items — vouching, confirmation, reconciliation, verification, cut-off, substantive testing</h3>' +
        '<p class="card-note">One register for Vouching, Invoice Verification, Bank Reconciliation, Bank/Debtor/Creditor Confirmation Trackers, Inventory/Fixed Asset/CWIP Verification, Cut-off Testing and Revenue/Expense/Payroll Testing — the area and test type below say which.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>FY</th><th>Area</th><th>Test type</th><th>Item ref</th><th>Description</th>' +
        '<th class="num">Book</th><th class="num">Tested</th><th>Confirmation</th><th>Status</th><th>Remark</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-ti="' + r.id + '" data-k="fy_label" value="' + esc(r.fy_label || '') + '"></td>' +
          '<td><select data-ti="' + r.id + '" data-k="area">' + AUDX_TEST_AREAS.map(a => '<option value="' + a + '"' + (r.area === a ? ' selected' : '') + '>' + a + '</option>').join('') + '</select></td>' +
          '<td><select data-ti="' + r.id + '" data-k="test_type">' + ['vouching', 'confirmation', 'reconciliation', 'physical_verification', 'cutoff', 'analytical', 'other'].map(t => '<option value="' + t + '"' + (r.test_type === t ? ' selected' : '') + '>' + t.replace('_', ' ') + '</option>').join('') + '</select></td>' +
          '<td><input data-ti="' + r.id + '" data-k="item_ref" value="' + esc(r.item_ref || '') + '"></td>' +
          '<td><input data-ti="' + r.id + '" data-k="description" value="' + esc(r.description || '') + '"></td>' +
          '<td class="num"><input type="number" data-ti="' + r.id + '" data-k="book_amount" value="' + (r.book_amount || 0) + '"></td>' +
          '<td class="num"><input type="number" data-ti="' + r.id + '" data-k="tested_amount" value="' + (r.tested_amount ?? '') + '"></td>' +
          '<td><select data-ti="' + r.id + '" data-k="confirmation_status"><option value="">n/a</option>' + ['not_sent', 'sent', 'received_agreed', 'received_discrepancy'].map(cs => '<option value="' + cs + '"' + (r.confirmation_status === cs ? ' selected' : '') + '>' + cs.replace('_', ' ') + '</option>').join('') + '</select></td>' +
          '<td><select data-ti="' + r.id + '" data-k="status">' + ['open', 'matched', 'exception'].map(s => '<option value="' + s + '"' + (r.status === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>' +
          '<td><input data-ti="' + r.id + '" data-k="remark" value="' + esc(r.remark || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-ti="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="audx-add-ti" style="margin-top:10px">Add a test item</button></div>';

      document.getElementById('audx-area-filter').addEventListener('change', e => { audxState.testArea = e.target.value; audxRenderTestItemsTab(root); });
      root.querySelectorAll('[data-ti]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = (k === 'book_amount' || k === 'tested_amount') ? (e.target.value === '' ? null : Number(e.target.value)) : e.target.value;
        try { await api('/api/audx/test-items/' + e.target.dataset.ti, { method: 'PUT', body: { [k]: v } }); audxRenderTestItemsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-ti]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/audx/test-items/' + e.target.dataset.delTi, { method: 'DELETE' }); audxRenderTestItemsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addTi = document.getElementById('audx-add-ti');
      if (addTi) addTi.addEventListener('click', async () => {
        try { await api('/api/audx/clients/' + audxState.clientId + '/test-items', { method: 'POST', body: { fy_label: state.fy, area: audxState.testArea || 'Bank', test_type: 'vouching' } }); audxRenderTestItemsTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function audxRenderAnalyticalTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/audx/clients/' + audxState.clientId + '/analytical').then(rows => {
      root.innerHTML = '<div class="card"><h3>Analytical review</h3><p class="card-note">A variance beyond the threshold flags automatically — record the explanation once investigated.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>FY</th><th>Metric</th><th class="num">Prior period</th>' +
        '<th class="num">Current period</th><th class="num">Expected</th><th class="num">Threshold %</th><th>Explanation</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-ap="' + r.id + '" data-k="fy_label" value="' + esc(r.fy_label || '') + '"></td>' +
          '<td><input data-ap="' + r.id + '" data-k="metric" value="' + esc(r.metric || '') + '"></td>' +
          '<td class="num"><input type="number" data-ap="' + r.id + '" data-k="prior_period_value" value="' + (r.prior_period_value ?? '') + '"></td>' +
          '<td class="num"><input type="number" data-ap="' + r.id + '" data-k="current_period_value" value="' + (r.current_period_value || 0) + '"></td>' +
          '<td class="num"><input type="number" data-ap="' + r.id + '" data-k="expected_value" value="' + (r.expected_value ?? '') + '"></td>' +
          '<td class="num"><input type="number" data-ap="' + r.id + '" data-k="threshold_pct" value="' + (r.threshold_pct || 10) + '"></td>' +
          '<td><input data-ap="' + r.id + '" data-k="explanation" value="' + esc(r.explanation || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-ap="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="audx-add-ap" style="margin-top:10px">Add a procedure</button></div>';

      root.querySelectorAll('[data-ap]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = (k === 'metric' || k === 'fy_label' || k === 'explanation') ? e.target.value : (e.target.value === '' ? null : Number(e.target.value));
        try { await api('/api/audx/analytical/' + e.target.dataset.ap, { method: 'PUT', body: { [k]: v } }); audxRenderAnalyticalTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-ap]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/audx/analytical/' + e.target.dataset.delAp, { method: 'DELETE' }); audxRenderAnalyticalTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addAp = document.getElementById('audx-add-ap');
      if (addAp) addAp.addEventListener('click', async () => {
        try { await api('/api/audx/clients/' + audxState.clientId + '/analytical', { method: 'POST', body: { fy_label: state.fy, metric: 'New metric', threshold_pct: 10 } }); audxRenderAnalyticalTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function audxRenderGoingConcernTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    Promise.all([api('/api/audx/clients/' + audxState.clientId + '/going-concern-indicators'), api('/api/audx/clients/' + audxState.clientId + '/going-concern-assessment?fy_label=' + encodeURIComponent(state.fy))])
      .then(([rows, assessment]) => {
      root.innerHTML = '<div class="grid c3">' +
        kpi('Indicators present', assessment.summary.present_count) +
        kpi('Total indicators', assessment.summary.total_count) +
        kpi('Material uncertainty', assessment.material_uncertainty_exists ? 'Yes' : 'No', assessment.material_uncertainty_exists ? 'alert' : 'accent') +
        '</div>' +
        '<div class="card"><h3>Going concern indicators</h3><div class="table-wrap"><table><thead><tr><th>FY</th><th>Category</th>' +
        '<th>Indicator</th><th>Present</th><th>Details</th><th>Mitigating factor</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-gc="' + r.id + '" data-k="fy_label" value="' + esc(r.fy_label || '') + '"></td>' +
          '<td><select data-gc="' + r.id + '" data-k="category"><option value="Financial"' + (r.category === 'Financial' ? ' selected' : '') + '>Financial</option><option value="Operating"' + (r.category === 'Operating' ? ' selected' : '') + '>Operating</option><option value="Other"' + (r.category === 'Other' ? ' selected' : '') + '>Other</option></select></td>' +
          '<td><input data-gc="' + r.id + '" data-k="indicator" value="' + esc(r.indicator || '') + '"></td>' +
          '<td><input type="checkbox" data-gc-flag="' + r.id + '"' + (r.present ? ' checked' : '') + '></td>' +
          '<td><input data-gc="' + r.id + '" data-k="details" value="' + esc(r.details || '') + '"></td>' +
          '<td><input data-gc="' + r.id + '" data-k="mitigating_factor" value="' + esc(r.mitigating_factor || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-gc="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="audx-add-gc" style="margin-top:10px">Add an indicator</button></div>' +
        '<div class="card"><h3>Overall assessment — FY ' + esc(state.fy) + '</h3><div class="grid c2" id="audx-gc-form">' +
        '<div class="field" style="grid-column:1/-1"><label>Conclusion</label><textarea data-key="conclusion" rows="3">' + esc(assessment.conclusion || '') + '</textarea></div>' +
        '<label class="check"><input type="checkbox" id="audx-gc-mu"' + (assessment.material_uncertainty_exists ? ' checked' : '') + '> Material uncertainty related to going concern exists</label>' +
        '</div><button class="btn btn-small" id="audx-save-gc" style="margin-top:10px">Save</button></div>';

      root.querySelectorAll('[data-gc]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k;
        try { await api('/api/audx/going-concern-indicators/' + e.target.dataset.gc, { method: 'PUT', body: { [k]: e.target.value } }); audxRenderGoingConcernTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-gc-flag]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/audx/going-concern-indicators/' + e.target.dataset.gcFlag, { method: 'PUT', body: { present: e.target.checked } }); audxRenderGoingConcernTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-gc]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/audx/going-concern-indicators/' + e.target.dataset.delGc, { method: 'DELETE' }); audxRenderGoingConcernTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addGc = document.getElementById('audx-add-gc');
      if (addGc) addGc.addEventListener('click', async () => {
        try { await api('/api/audx/clients/' + audxState.clientId + '/going-concern-indicators', { method: 'POST', body: { fy_label: state.fy, category: 'Financial', indicator: 'New indicator' } }); audxRenderGoingConcernTab(root); }
        catch (err) { toast(err.message, true); }
      });
      const conclusionEl = root.querySelector('[data-key="conclusion"]');
      document.getElementById('audx-save-gc').addEventListener('click', async () => {
        try {
          await api('/api/audx/clients/' + audxState.clientId + '/going-concern-assessment', { method: 'PUT', body: {
            fy_label: state.fy, conclusion: conclusionEl.value, material_uncertainty_exists: document.getElementById('audx-gc-mu').checked } });
          toast('Saved');
        } catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function audxRenderChecklistTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/audx/clients/' + audxState.clientId + '/checklist?fy_label=' + encodeURIComponent(state.fy)).then(rows => {
      const areaRows = rows.filter(r => r.area === audxState.checklistArea);
      const done = areaRows.filter(r => r.status === 'done').length;
      root.innerHTML = '<div style="margin-bottom:12px">' +
        AUDX_CHECKLIST_AREAS.map(a => '<button class="btn btn-small' + (audxState.checklistArea === a ? ' btn-primary' : ' btn-ghost') +
          '" data-audx-carea="' + a + '" style="margin:0 6px 6px 0">' + a + '</button>').join('') + '</div>' +
        '<div class="card"><h3>' + esc(audxState.checklistArea) + ' — FY ' + esc(state.fy) + '</h3><span class="hint">' + done + ' / ' + areaRows.length + ' done</span>' +
        '<div class="table-wrap" style="margin-top:10px"><table><thead><tr><th>Criterion</th><th>Status</th><th>Evidence</th><th>Remark</th><th></th></tr></thead><tbody>' +
        areaRows.map(r => '<tr><td><input data-cc="' + r.id + '" data-k="criterion" value="' + esc(r.criterion || '') + '"></td>' +
          '<td><select data-cc="' + r.id + '" data-k="status">' + ['pending', 'done', 'na'].map(s => '<option value="' + s + '"' + (r.status === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>' +
          '<td><input data-cc="' + r.id + '" data-k="evidence" value="' + esc(r.evidence || '') + '"></td>' +
          '<td><input data-cc="' + r.id + '" data-k="remark" value="' + esc(r.remark || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-cc="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div>' +
        '<button class="btn btn-small" id="audx-seed-cc" style="margin-top:10px" title="Add the standard items, skipping any already present">Seed standard checklist</button></div>';

      root.querySelectorAll('[data-audx-carea]').forEach(el => el.addEventListener('click', e => {
        audxState.checklistArea = e.target.dataset.audxCarea; audxRenderChecklistTab(root);
      }));
      root.querySelectorAll('[data-cc]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k;
        try { await api('/api/audx/checklist/' + e.target.dataset.cc, { method: 'PUT', body: { [k]: e.target.value } }); audxRenderChecklistTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-cc]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/audx/checklist/' + e.target.dataset.delCc, { method: 'DELETE' }); audxRenderChecklistTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const seedCc = document.getElementById('audx-seed-cc');
      if (seedCc) seedCc.addEventListener('click', async () => {
        try { const r = await api('/api/audx/clients/' + audxState.clientId + '/checklist/seed', { method: 'POST', body: { area: audxState.checklistArea, fy_label: state.fy } }); toast(r.added + ' items added'); audxRenderChecklistTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function audxRenderReportsTab(root) {
    root.innerHTML = '<div class="card"><h3>Reports</h3>' +
      '<p class="card-note"><b>Management Letter</b> — exceptions from testing, unexplained analytical variances, going concern and incomplete checklist areas, for FY ' + esc(state.fy) + '.</p>' +
      '<button class="btn btn-small" data-audx-report="management_letter" data-fmt="docx">Word</button>' +
      '<p class="card-note" style="margin-top:14px"><b>Audit Execution Summary</b> — full detail across all the tabs above.</p>' +
      '<button class="btn btn-small" data-audx-report="execution_summary" data-fmt="xlsx">Excel</button> ' +
      '<button class="btn btn-small" data-audx-report="execution_summary" data-fmt="docx">Word</button></div>';
    root.querySelectorAll('[data-audx-report]').forEach(el => el.addEventListener('click', async e => {
      try {
        const res = await api('/api/audx/clients/' + audxState.clientId + '/reports', { method: 'POST', body: {
          report_type: e.target.dataset.audxReport, format: e.target.dataset.fmt, fy_label: state.fy } });
        toast('Generated ' + res.filename);
        window.open(res.download_url, '_blank');
      } catch (err) { toast(err.message, true); }
    }));
  }

  // -------------------------------------------------------------- accounting automation
  const ACCT_TABS = [
    ['dashboard', 'Dashboard'], ['coa', 'Chart of accounts'], ['tbdiag', 'TB diagnostics'], ['policies', 'Policies'],
    ['cashflow', 'Cash flow'], ['equity', 'Equity'], ['provisions', 'Accruals & provisions'],
    ['prepayments', 'Prepayments'], ['fx', 'FX revaluation'], ['interest', 'Interest accrual'],
    ['borrowcap', 'Borrowing costs'], ['closing', 'Closing checklist'], ['workpapers', 'Working papers'], ['reports', 'Reports'],
  ];
  const acctState = { clientId: '', tab: 'dashboard' };
  const ACCT_POLICY_HEADS = [
    ['cash', 'Cash and cash equivalents'], ['revenue', 'Revenue'], ['ppe', 'Property, plant and equipment'],
    ['inventories', 'Inventories'], ['intangible', 'Intangible assets'], ['trade_rec', 'Trade receivables'],
    ['lt_borrow', 'Long-term borrowings'], ['st_borrow', 'Short-term borrowings'], ['lt_prov', 'Long-term provisions'],
    ['st_prov', 'Short-term provisions'], ['current_tax', 'Current tax'], ['deferred_tax', 'Deferred tax'],
    ['nc_inv', 'Investments (foreign currency exposure)'], ['share_capital', 'Share capital'], ['reserves', 'Reserves and surplus'],
  ];
  const ACCT_CF_SECTIONS = [['operating_pat_start', 'Operating — PAT (starting point)'], ['operating_adjustment', 'Operating — non-cash adjustment'],
    ['working_capital', 'Operating — working capital change'], ['operating_other', 'Operating — other'],
    ['investing', 'Investing'], ['financing', 'Financing']];

  function renderAcctAutomation() {
    const view = document.getElementById('view');
    if (!state.online || !state.token || state.offlineMode) {
      view.innerHTML = '<div class="page-head"><div><h2>Accounting Automation</h2></div></div>' +
        '<div class="card"><div class="empty"><b>Offline</b>Accounting Automation works against live server data — reconnect to use this module.</div></div>';
      return;
    }
    if (!acctState.clientId) acctState.clientId = localStorage.getItem('ca.client.acct') || (clients()[0] && clients()[0].id) || '';

    view.innerHTML =
      '<div class="page-head"><div><h2>Accounting Automation</h2>' +
      '<p>Trial balance diagnostics, tailored accounting policies, cash flow and changes-in-equity statements, ' +
      'accruals/provisions, prepayment amortisation, foreign currency revaluation, interest accrual, NAS 23 borrowing-cost ' +
      'capitalisation and a year-end closing checklist. The financial statements themselves, notes to accounts and TB-to-FS ' +
      'mapping stay in Financial Statements; depreciation schedules in Depreciation (Accounts); lease accounting in Lease (NFRS 16).</p></div></div>' +
      '<div class="card"><div class="grid c3">' +
      '<div class="field"><label>Client</label><select id="acct-client"><option value="">— choose —</option>' +
      clients().map(c => '<option value="' + c.id + '"' + (String(acctState.clientId) === String(c.id) ? ' selected' : '') + '>' + esc(c.name) + '</option>').join('') + '</select></div>' +
      '</div></div>' +
      '<div class="card" id="acct-body"><div class="empty">Loading…</div></div>';

    document.getElementById('acct-client').addEventListener('change', e => {
      acctState.clientId = e.target.value; localStorage.setItem('ca.client.acct', acctState.clientId);
      acctState.tab = 'dashboard';
      acctRenderBody();
    });
    acctRenderBody();
  }

  function acctRenderBody() {
    const body = document.getElementById('acct-body');
    if (!acctState.clientId) { body.innerHTML = '<div class="empty"><b>Choose a client</b>Pick a client above to begin.</div>'; return; }
    body.innerHTML = '<div style="margin-bottom:14px">' +
      ACCT_TABS.map(t => '<button class="btn btn-small' + (acctState.tab === t[0] ? ' btn-primary' : ' btn-ghost') +
        '" data-acct-tab="' + t[0] + '" style="margin:0 6px 6px 0">' + esc(t[1]) + '</button>').join('') +
      '</div><div id="acct-tab-body"></div>';
    body.querySelectorAll('[data-acct-tab]').forEach(b => b.addEventListener('click', e => {
      acctState.tab = e.target.dataset.acctTab; acctRenderBody();
    }));
    const tb = document.getElementById('acct-tab-body');
    ({ dashboard: acctRenderDashboardTab, coa: acctRenderCoaTab, tbdiag: acctRenderTbDiagTab, policies: acctRenderPoliciesTab,
       cashflow: acctRenderCashFlowTab, equity: acctRenderEquityTab, provisions: acctRenderProvisionsTab,
       prepayments: acctRenderPrepaymentsTab, fx: acctRenderFxTab, interest: acctRenderInterestTab,
       borrowcap: acctRenderBorrowCapTab, closing: acctRenderClosingTab, workpapers: acctRenderWorkpapersTab,
       reports: acctRenderReportsTab }[acctState.tab] || acctRenderDashboardTab)(tb);
  }

  function acctRenderDashboardTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/acct/clients/' + acctState.clientId + '/dashboard?fy_label=' + encodeURIComponent(state.fy)).then(d => {
      root.innerHTML = '<div class="grid c4">' +
        kpi('Provisions closing', money(d.provisions.total_closing)) +
        kpi('Prepayments b/f', money(d.prepayments.total_balance_carried_forward)) +
        kpi('FX gain / (loss)', money(d.fx.total_gain_loss), d.fx.total_gain_loss < 0 ? 'alert' : 'accent') +
        kpi('Interest accrued', money(d.interest.total_accrued_interest)) +
        '</div>' +
        '<div class="card"><h3>Cash flow — FY ' + esc(d.fy_label) + '</h3><div class="grid c4">' +
        kpi('Net operating', money(d.cash_flow.net_operating)) +
        kpi('Net investing', money(d.cash_flow.net_investing)) +
        kpi('Net financing', money(d.cash_flow.net_financing)) +
        kpi('Net change in cash', money(d.cash_flow.net_change), 'accent') +
        '</div>' + ('ties_out' in d.cash_flow ? '<p class="card-note" style="margin-top:8px">' +
          (d.cash_flow.ties_out ? 'Ties out to the actual closing cash balance.' : 'Does not tie out — difference ' + money(d.cash_flow.reconciliation_diff) + '.') +
          '</p>' : '') + '</div>' +
        '<div class="card"><h3>Closing checklist</h3>' +
        Object.keys(d.checklist).map(area => '<p class="card-note">' + esc(area) + ': ' + d.checklist[area].done + ' of ' + d.checklist[area].total + ' — ' + d.checklist[area].completion_pct.toFixed(0) + '%</p>').join('') +
        (!Object.keys(d.checklist).length ? '<div class="empty">Nothing seeded yet — see the Closing checklist tab.</div>' : '') + '</div>';
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function acctRenderCoaTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/acct/clients/' + acctState.clientId + '/coa').then(rows => {
      root.innerHTML = '<div class="card"><h3>Chart of accounts</h3><p class="card-note">A client-specific, standing account master — separate from the fixed FS-head taxonomy used for TB-to-FS mapping in Financial Statements.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Code</th><th>Name</th><th>FS head</th><th>Normal side</th><th>Active</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-coa="' + r.id + '" data-k="code" value="' + esc(r.code || '') + '"></td>' +
          '<td><input data-coa="' + r.id + '" data-k="name" value="' + esc(r.name || '') + '"></td>' +
          '<td><input data-coa="' + r.id + '" data-k="fs_head" value="' + esc(r.fs_head || '') + '"></td>' +
          '<td><select data-coa="' + r.id + '" data-k="normal_side"><option value="debit"' + (r.normal_side === 'debit' ? ' selected' : '') + '>Debit</option><option value="credit"' + (r.normal_side === 'credit' ? ' selected' : '') + '>Credit</option></select></td>' +
          '<td><input type="checkbox" data-coa-flag="' + r.id + '"' + (r.is_active ? ' checked' : '') + '></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-coa="' + r.id + '" title="Remove">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="acct-add-coa" style="margin-top:10px">Add an account</button></div>';

      root.querySelectorAll('[data-coa]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k;
        try { await api('/api/acct/coa/' + e.target.dataset.coa, { method: 'PUT', body: { [k]: e.target.value } }); acctRenderCoaTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-coa-flag]').forEach(el => el.addEventListener('change', async e => {
        try { await api('/api/acct/coa/' + e.target.dataset.coaFlag, { method: 'PUT', body: { is_active: e.target.checked } }); acctRenderCoaTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-coa]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/acct/coa/' + e.target.dataset.delCoa, { method: 'DELETE' }); acctRenderCoaTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addCoa = document.getElementById('acct-add-coa');
      if (addCoa) addCoa.addEventListener('click', async () => {
        try { await api('/api/acct/clients/' + acctState.clientId + '/coa', { method: 'POST', body: { name: 'New account', normal_side: 'debit' } }); acctRenderCoaTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function acctRenderTbDiagTab(root) {
    if (!root.dataset.acctTbRows) root.dataset.acctTbRows = JSON.stringify([{ name: '', group: '', head: '', dr: 0, cr: 0, prev: '' }]);
    const rows = JSON.parse(root.dataset.acctTbRows);
    root.innerHTML = '<div class="card"><h3>Trial balance diagnostics</h3>' +
      '<p class="card-note">Paste the same TB you use in Financial Statements (name / group / head / dr / cr / prior-year closing net). ' +
      'Flags a suspense/clearing name, a balance on the wrong natural side for its head, an unclassified balance, and a large movement vs last year — nothing is read automatically from Financial Statements.</p>' +
      '<div class="table-wrap"><table><thead><tr><th>Name</th><th>Group</th><th>Head</th><th class="num">Dr</th><th class="num">Cr</th><th class="num">Prior net</th><th></th></tr></thead><tbody>' +
      rows.map((r, i) => '<tr><td><input data-tb="' + i + '" data-k="name" value="' + esc(r.name || '') + '"></td>' +
        '<td><input data-tb="' + i + '" data-k="group" value="' + esc(r.group || '') + '"></td>' +
        '<td><input data-tb="' + i + '" data-k="head" value="' + esc(r.head || '') + '" placeholder="e.g. trade_pay"></td>' +
        '<td class="num"><input type="number" data-tb="' + i + '" data-k="dr" value="' + (r.dr || 0) + '"></td>' +
        '<td class="num"><input type="number" data-tb="' + i + '" data-k="cr" value="' + (r.cr || 0) + '"></td>' +
        '<td class="num"><input type="number" data-tb="' + i + '" data-k="prev" value="' + (r.prev ?? '') + '"></td>' +
        '<td><button class="btn btn-small btn-danger" data-del-tb="' + i + '">Remove</button></td></tr>').join('') +
      '</tbody></table></div>' +
      '<button class="btn btn-small" id="acct-add-tb" style="margin-top:10px">Add a row</button> ' +
      '<button class="btn btn-small btn-primary" id="acct-run-tb" style="margin-top:10px">Run diagnostics</button>' +
      '<div id="acct-tb-result" style="margin-top:14px"></div></div>';

    const persist = () => { root.dataset.acctTbRows = JSON.stringify(rows); };
    root.querySelectorAll('[data-tb]').forEach(el => el.addEventListener('change', e => {
      const i = Number(e.target.dataset.tb), k = e.target.dataset.k;
      rows[i][k] = e.target.type === 'number' ? Number(e.target.value) : e.target.value;
      persist();
    }));
    root.querySelectorAll('[data-del-tb]').forEach(el => el.addEventListener('click', e => {
      rows.splice(Number(e.target.dataset.delTb), 1); persist(); acctRenderTbDiagTab(root);
    }));
    document.getElementById('acct-add-tb').addEventListener('click', () => {
      rows.push({ name: '', group: '', head: '', dr: 0, cr: 0, prev: '' }); persist(); acctRenderTbDiagTab(root);
    });
    document.getElementById('acct-run-tb').addEventListener('click', async () => {
      try {
        const res = await api('/api/acct/tb-diagnostics', { method: 'POST', body: { rows, materiality: 0, movement_threshold_pct: 20 } });
        const box = document.getElementById('acct-tb-result');
        box.innerHTML = '<div class="grid c3">' +
          kpi('Debit = Credit', res.balanced ? 'Yes' : 'No', res.balanced ? 'accent' : 'alert') +
          kpi('Suspense candidates', res.suspense_candidates.length, res.suspense_candidates.length ? 'alert' : '') +
          kpi('Unusual balances', res.unusual_balances.length, res.unusual_balances.length ? 'alert' : '') +
          '</div>' +
          (res.unclassified.length ? '<p class="card-note"><b>Unclassified:</b> ' + res.unclassified.map(x => esc(x.name)).join(', ') + '</p>' : '') +
          (res.large_movements.length ? '<p class="card-note"><b>Large movements (&gt;20%):</b> ' + res.large_movements.map(x => esc(x.name) + ' ' + x.variance_pct + '%').join(', ') + '</p>' : '') +
          (res.suspense_candidates.length ? '<p class="card-note"><b>Suspense:</b> ' + res.suspense_candidates.map(x => esc(x.name)).join(', ') + '</p>' : '') +
          (res.unusual_balances.length ? '<p class="card-note"><b>Unusual balance:</b> ' + res.unusual_balances.map(x => esc(x.name) + ' (expected ' + x.expected_side + ', is ' + x.actual_side + ')').join(', ') + '</p>' : '');
      } catch (err) { toast(err.message, true); }
    });
  }

  function acctRenderPoliciesTab(root) {
    if (!root.dataset.acctHeads) root.dataset.acctHeads = JSON.stringify([]);
    const selected = new Set(JSON.parse(root.dataset.acctHeads));
    root.innerHTML = '<div class="card"><h3>Accounting policy generator</h3><p class="card-note">Tick the FS heads with a non-zero balance this year — the suggested policy paragraphs are tailored to what is actually on the trial balance, as a starting draft to edit.</p>' +
      '<div class="grid c3">' + ACCT_POLICY_HEADS.map(([id, label]) => '<label class="check"><input type="checkbox" data-acct-head="' + id + '"' +
        (selected.has(id) ? ' checked' : '') + '> ' + esc(label) + '</label>').join('') + '</div>' +
      '<button class="btn btn-small btn-primary" id="acct-run-policy" style="margin-top:10px">Suggest policies</button>' +
      '<div id="acct-policy-result" style="margin-top:14px"></div></div>';

    root.querySelectorAll('[data-acct-head]').forEach(el => el.addEventListener('change', e => {
      if (e.target.checked) selected.add(e.target.dataset.acctHead); else selected.delete(e.target.dataset.acctHead);
      root.dataset.acctHeads = JSON.stringify(Array.from(selected));
    }));
    document.getElementById('acct-run-policy').addEventListener('click', async () => {
      try {
        const res = await api('/api/acct/policy-suggestions', { method: 'POST', body: { heads_present: Array.from(selected) } });
        document.getElementById('acct-policy-result').innerHTML = res.policies.map(p =>
          '<div style="margin-top:10px"><b>' + esc(p.title) + '</b><p class="card-note">' + esc(p.text) + '</p></div>').join('');
      } catch (err) { toast(err.message, true); }
    });
  }

  function acctRenderCashFlowTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/acct/clients/' + acctState.clientId + '/cash-flow?fy_label=' + encodeURIComponent(state.fy)).then(d => {
      const c = d.computed;
      root.innerHTML = '<div class="card"><h3>Cash flow statement (indirect method) — FY ' + esc(d.fy_label) + '</h3>' +
        '<div class="grid c3">' +
        '<div class="field"><label>Opening cash</label><input type="number" id="acct-cf-open" value="' + (d.opening_cash || 0) + '"></div>' +
        '<div class="field"><label>Actual closing cash</label><input type="number" id="acct-cf-close" value="' + (d.closing_cash_actual ?? '') + '"></div>' +
        '<div class="field"><label>&nbsp;</label><button class="btn btn-small" id="acct-cf-save">Save</button></div></div>' +
        '<div class="grid c4" style="margin-top:10px">' +
        kpi('Net operating', money(c.net_operating)) + kpi('Net investing', money(c.net_investing)) +
        kpi('Net financing', money(c.net_financing)) + kpi('Computed closing cash', money(c.computed_closing_cash), 'accent') +
        '</div>' + ('ties_out' in c ? '<p class="card-note" style="margin-top:8px">' +
          (c.ties_out ? 'Ties out.' : 'Does not tie out — difference ' + money(c.reconciliation_diff) + '.') + '</p>' : '') +
        '<div class="table-wrap" style="margin-top:10px"><table><thead><tr><th>Section</th><th>Label</th><th class="num">Amount</th><th>Note</th><th></th></tr></thead><tbody>' +
        d.lines.map(r => '<tr><td><select data-cf="' + r.id + '" data-k="section">' + ACCT_CF_SECTIONS.map(([v, l]) => '<option value="' + v + '"' + (r.section === v ? ' selected' : '') + '>' + esc(l) + '</option>').join('') + '</select></td>' +
          '<td><input data-cf="' + r.id + '" data-k="label" value="' + esc(r.label || '') + '"></td>' +
          '<td class="num"><input type="number" data-cf="' + r.id + '" data-k="amount" value="' + (r.amount || 0) + '"></td>' +
          '<td><input data-cf="' + r.id + '" data-k="note" value="' + esc(r.note || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-cf="' + r.id + '">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="acct-add-cf" style="margin-top:10px">Add a line</button></div>';

      document.getElementById('acct-cf-save').addEventListener('click', async () => {
        try {
          await api('/api/acct/clients/' + acctState.clientId + '/cash-flow-setting', { method: 'PUT', body: {
            fy_label: state.fy, opening_cash: Number(document.getElementById('acct-cf-open').value) || 0,
            closing_cash_actual: document.getElementById('acct-cf-close').value === '' ? null : Number(document.getElementById('acct-cf-close').value) } });
          acctRenderCashFlowTab(root);
        } catch (err) { toast(err.message, true); }
      });
      root.querySelectorAll('[data-cf]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = k === 'amount' ? Number(e.target.value) : e.target.value;
        try { await api('/api/acct/cash-flow-lines/' + e.target.dataset.cf, { method: 'PUT', body: { [k]: v } }); acctRenderCashFlowTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-cf]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/acct/cash-flow-lines/' + e.target.dataset.delCf, { method: 'DELETE' }); acctRenderCashFlowTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addCf = document.getElementById('acct-add-cf');
      if (addCf) addCf.addEventListener('click', async () => {
        try { await api('/api/acct/clients/' + acctState.clientId + '/cash-flow-lines', { method: 'POST', body: { fy_label: state.fy, section: 'operating_adjustment', label: 'New line', amount: 0 } }); acctRenderCashFlowTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function acctRenderEquityTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/acct/clients/' + acctState.clientId + '/equity?fy_label=' + encodeURIComponent(state.fy)).then(d => {
      const cols = ['opening_balance', 'profit_for_year', 'oci', 'dividend', 'share_issued', 'share_buyback', 'transfer_in', 'transfer_out', 'other_movement'];
      const labels = ['Opening', 'Profit for year', 'OCI', 'Dividend', 'Shares issued', 'Buyback', 'Transfer in', 'Transfer out', 'Other'];
      root.innerHTML = '<div class="card"><h3>Statement of changes in equity — FY ' + esc(d.fy_label) + '</h3>' +
        '<div class="table-wrap"><table><thead><tr><th>Component</th>' + labels.map(l => '<th class="num">' + l + '</th>').join('') + '<th class="num">Closing</th><th></th></tr></thead><tbody>' +
        d.components.map(r => '<tr><td><input data-eq="' + r.id + '" data-k="name" value="' + esc(r.name || '') + '"></td>' +
          cols.map(c => '<td class="num"><input type="number" data-eq="' + r.id + '" data-k="' + c + '" value="' + (r[c] || 0) + '"></td>').join('') +
          '<td class="num">' + money(r.opening_balance + r.profit_for_year + r.oci - r.dividend + r.share_issued - r.share_buyback + r.transfer_in - r.transfer_out + r.other_movement) + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-eq="' + r.id + '">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="acct-add-eq" style="margin-top:10px">Add a component</button>' +
        '<p class="card-note" style="margin-top:10px">Total closing equity: ' + money(d.computed.totals.closing_balance) + '</p></div>';

      root.querySelectorAll('[data-eq]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = k === 'name' ? e.target.value : Number(e.target.value);
        try { await api('/api/acct/equity/' + e.target.dataset.eq, { method: 'PUT', body: { [k]: v } }); acctRenderEquityTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-eq]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/acct/equity/' + e.target.dataset.delEq, { method: 'DELETE' }); acctRenderEquityTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addEq = document.getElementById('acct-add-eq');
      if (addEq) addEq.addEventListener('click', async () => {
        try { await api('/api/acct/clients/' + acctState.clientId + '/equity', { method: 'POST', body: { fy_label: state.fy, name: 'New component' } }); acctRenderEquityTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function acctRenderProvisionsTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/acct/clients/' + acctState.clientId + '/provisions?fy_label=' + encodeURIComponent(state.fy)).then(d => {
      root.innerHTML = '<div class="card"><h3>Accruals and provisions — FY ' + esc(d.fy_label) + '</h3>' +
        '<div class="table-wrap"><table><thead><tr><th>Category</th><th>Item</th><th>Basis</th><th class="num">Opening</th>' +
        '<th class="num">Charge</th><th class="num">Utilized</th><th class="num">Reversed</th><th class="num">Closing</th><th></th></tr></thead><tbody>' +
        d.computed.rows.map(r => '<tr><td><select data-pv="' + r.id + '" data-k="category"><option value="accrual"' + (r.category === 'accrual' ? ' selected' : '') + '>Accrual</option><option value="provision"' + (r.category === 'provision' ? ' selected' : '') + '>Provision</option></select></td>' +
          '<td><input data-pv="' + r.id + '" data-k="item" value="' + esc(r.item || '') + '"></td>' +
          '<td><input data-pv="' + r.id + '" data-k="basis" value="' + esc(r.basis || '') + '"></td>' +
          '<td class="num"><input type="number" data-pv="' + r.id + '" data-k="opening_balance" value="' + (r.opening_balance || 0) + '"></td>' +
          '<td class="num"><input type="number" data-pv="' + r.id + '" data-k="charge_for_year" value="' + (r.charge_for_year || 0) + '"></td>' +
          '<td class="num"><input type="number" data-pv="' + r.id + '" data-k="utilized" value="' + (r.utilized || 0) + '"></td>' +
          '<td class="num"><input type="number" data-pv="' + r.id + '" data-k="reversed" value="' + (r.reversed || 0) + '"></td>' +
          '<td class="num">' + money(r.closing_balance) + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-pv="' + r.id + '">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="acct-add-pv" style="margin-top:10px">Add an item</button>' +
        '<p class="card-note" style="margin-top:10px">Total closing: ' + money(d.computed.total_closing) + '</p></div>';

      root.querySelectorAll('[data-pv]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = ['opening_balance', 'charge_for_year', 'utilized', 'reversed'].includes(k) ? Number(e.target.value) : e.target.value;
        try { await api('/api/acct/provisions/' + e.target.dataset.pv, { method: 'PUT', body: { [k]: v } }); acctRenderProvisionsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-pv]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/acct/provisions/' + e.target.dataset.delPv, { method: 'DELETE' }); acctRenderProvisionsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addPv = document.getElementById('acct-add-pv');
      if (addPv) addPv.addEventListener('click', async () => {
        try { await api('/api/acct/clients/' + acctState.clientId + '/provisions', { method: 'POST', body: { fy_label: state.fy, category: 'provision', item: 'New item' } }); acctRenderProvisionsTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function acctRenderPrepaymentsTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    const asOf = new Date().toISOString().slice(0, 10);
    api('/api/acct/clients/' + acctState.clientId + '/prepayments?fy_label=' + encodeURIComponent(state.fy) + '&as_of=' + asOf).then(d => {
      root.innerHTML = '<div class="card"><h3>Prepayment amortisation — FY ' + esc(d.fy_label) + '</h3>' +
        '<p class="card-note">Straight-line, whole months, as of ' + esc(asOf) + '.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Item</th><th class="num">Total amount</th><th>Start date</th><th>End date</th>' +
        '<th class="num">Months</th><th class="num">Amortised to date</th><th class="num">Balance c/f</th><th></th></tr></thead><tbody>' +
        d.rows.map((r, i) => { const c = d.computed.rows[i] || {}; return '<tr><td><input data-pp="' + r.id + '" data-k="item" value="' + esc(r.item || '') + '"></td>' +
          '<td class="num"><input type="number" data-pp="' + r.id + '" data-k="total_amount" value="' + (r.total_amount || 0) + '"></td>' +
          '<td><input data-pp="' + r.id + '" data-k="start_date" value="' + esc(r.start_date || '') + '" placeholder="YYYY-MM-DD"></td>' +
          '<td><input data-pp="' + r.id + '" data-k="end_date" value="' + esc(r.end_date || '') + '" placeholder="YYYY-MM-DD"></td>' +
          '<td class="num">' + (c.total_months || 0) + '</td><td class="num">' + money(c.amortized_to_date) + '</td><td class="num">' + money(c.balance_carried_forward) + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-pp="' + r.id + '">Remove</button></td></tr>'; }).join('') +
        '</tbody></table></div>' +
        '<button class="btn btn-small" id="acct-add-pp" style="margin-top:10px">Add a prepayment</button>' +
        '<p class="card-note" style="margin-top:10px">Total balance carried forward: ' + money(d.computed.total_balance_carried_forward) + '</p></div>';

      root.querySelectorAll('[data-pp]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = k === 'total_amount' ? Number(e.target.value) : e.target.value;
        try { await api('/api/acct/prepayments/' + e.target.dataset.pp, { method: 'PUT', body: { [k]: v } }); acctRenderPrepaymentsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-pp]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/acct/prepayments/' + e.target.dataset.delPp, { method: 'DELETE' }); acctRenderPrepaymentsTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addPp = document.getElementById('acct-add-pp');
      if (addPp) addPp.addEventListener('click', async () => {
        try {
          await api('/api/acct/clients/' + acctState.clientId + '/prepayments', { method: 'POST', body: {
            fy_label: state.fy, item: 'New prepayment', total_amount: 0, start_date: asOf, end_date: asOf } });
          acctRenderPrepaymentsTab(root);
        } catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function acctRenderFxTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/acct/clients/' + acctState.clientId + '/fx?fy_label=' + encodeURIComponent(state.fy)).then(d => {
      root.innerHTML = '<div class="card"><h3>Foreign currency revaluation (NAS 21) — FY ' + esc(d.fy_label) + '</h3>' +
        '<div class="table-wrap"><table><thead><tr><th>Item</th><th>Currency</th><th>Classification</th>' +
        '<th class="num">Foreign amount</th><th class="num">Booked rate</th><th class="num">Closing rate</th><th class="num">Gain/(loss)</th><th></th></tr></thead><tbody>' +
        d.computed.rows.map(r => '<tr><td><input data-fx="' + r.id + '" data-k="item" value="' + esc(r.item || '') + '"></td>' +
          '<td><input data-fx="' + r.id + '" data-k="currency" value="' + esc(r.currency || '') + '"></td>' +
          '<td><select data-fx="' + r.id + '" data-k="classification"><option value="asset"' + (r.classification === 'asset' ? ' selected' : '') + '>Asset</option><option value="liability"' + (r.classification === 'liability' ? ' selected' : '') + '>Liability</option></select></td>' +
          '<td class="num"><input type="number" data-fx="' + r.id + '" data-k="foreign_amount" value="' + (r.foreign_amount || 0) + '"></td>' +
          '<td class="num"><input type="number" data-fx="' + r.id + '" data-k="booked_rate" value="' + (r.booked_rate || 0) + '"></td>' +
          '<td class="num"><input type="number" data-fx="' + r.id + '" data-k="closing_rate" value="' + (r.closing_rate || 0) + '"></td>' +
          '<td class="num">' + money(r.gain_loss) + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-fx="' + r.id + '">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="acct-add-fx" style="margin-top:10px">Add an item</button>' +
        '<p class="card-note" style="margin-top:10px">Net gain/(loss): ' + money(d.computed.total_gain_loss) + '</p></div>';

      root.querySelectorAll('[data-fx]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = ['foreign_amount', 'booked_rate', 'closing_rate'].includes(k) ? Number(e.target.value) : e.target.value;
        try { await api('/api/acct/fx/' + e.target.dataset.fx, { method: 'PUT', body: { [k]: v } }); acctRenderFxTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-fx]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/acct/fx/' + e.target.dataset.delFx, { method: 'DELETE' }); acctRenderFxTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addFx = document.getElementById('acct-add-fx');
      if (addFx) addFx.addEventListener('click', async () => {
        try { await api('/api/acct/clients/' + acctState.clientId + '/fx', { method: 'POST', body: { fy_label: state.fy, item: 'New item', currency: 'USD' } }); acctRenderFxTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function acctRenderInterestTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/acct/clients/' + acctState.clientId + '/interest-accrual?fy_label=' + encodeURIComponent(state.fy)).then(d => {
      root.innerHTML = '<div class="card"><h3>Interest accrual — FY ' + esc(d.fy_label) + '</h3>' +
        '<p class="card-note">Fixed deposits, intercompany balances, staff loans — a loan\'s own instalment schedule stays in Loan &amp; EMI.</p>' +
        '<div class="table-wrap"><table><thead><tr><th>Item</th><th class="num">Principal</th><th class="num">Rate %</th>' +
        '<th>From</th><th>To</th><th>Basis</th><th>Compounding</th><th class="num">Accrued</th><th></th></tr></thead><tbody>' +
        d.computed.rows.map(r => '<tr><td><input data-ia="' + r.id + '" data-k="item" value="' + esc(r.item || '') + '"></td>' +
          '<td class="num"><input type="number" data-ia="' + r.id + '" data-k="principal" value="' + (r.principal || 0) + '"></td>' +
          '<td class="num"><input type="number" data-ia="' + r.id + '" data-k="rate_pct" value="' + (r.rate_pct || 0) + '"></td>' +
          '<td><input data-ia="' + r.id + '" data-k="from_date" value="' + esc(r.from_date || '') + '" placeholder="YYYY-MM-DD"></td>' +
          '<td><input data-ia="' + r.id + '" data-k="to_date" value="' + esc(r.to_date || '') + '" placeholder="YYYY-MM-DD"></td>' +
          '<td><select data-ia="' + r.id + '" data-k="day_count_basis"><option value="365"' + (String(r.day_count_basis) === '365' ? ' selected' : '') + '>365</option><option value="360"' + (String(r.day_count_basis) === '360' ? ' selected' : '') + '>360</option></select></td>' +
          '<td><select data-ia="' + r.id + '" data-k="compounding"><option value="simple"' + (r.compounding === 'simple' ? ' selected' : '') + '>Simple</option><option value="annual"' + (r.compounding === 'annual' ? ' selected' : '') + '>Annual</option></select></td>' +
          '<td class="num">' + money(r.accrued_interest) + '</td>' +
          '<td><button class="btn btn-small btn-danger" data-del-ia="' + r.id + '">Remove</button></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="acct-add-ia" style="margin-top:10px">Add an item</button>' +
        '<p class="card-note" style="margin-top:10px">Total accrued interest: ' + money(d.computed.total_accrued_interest) + '</p></div>';

      root.querySelectorAll('[data-ia]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = ['principal', 'rate_pct'].includes(k) ? Number(e.target.value) : e.target.value;
        try { await api('/api/acct/interest-accrual/' + e.target.dataset.ia, { method: 'PUT', body: { [k]: v } }); acctRenderInterestTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-ia]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/acct/interest-accrual/' + e.target.dataset.delIa, { method: 'DELETE' }); acctRenderInterestTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addIa = document.getElementById('acct-add-ia');
      if (addIa) addIa.addEventListener('click', async () => {
        try {
          await api('/api/acct/clients/' + acctState.clientId + '/interest-accrual', { method: 'POST', body: {
            fy_label: state.fy, item: 'New item', from_date: state.fy, to_date: state.fy } });
          acctRenderInterestTab(root);
        } catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function acctRenderBorrowCapTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/acct/clients/' + acctState.clientId + '/borrowing-capitalization?fy_label=' + encodeURIComponent(state.fy)).then(rows => {
      root.innerHTML = '<div class="card"><h3>Borrowing cost capitalisation (NAS 23) — FY ' + esc(state.fy) + '</h3>' +
        rows.map(r => acctBorrowCapCard(r)).join('') +
        '<button class="btn btn-small" id="acct-add-bc" style="margin-top:10px">Add a qualifying asset</button></div>';

      root.querySelectorAll('[data-bc]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k, v = ['period_days', 'capitalizable_expenditure_general', 'income_from_temp_investment'].includes(k) ? Number(e.target.value) : e.target.value;
        try { await api('/api/acct/borrowing-capitalization/' + e.target.dataset.bc, { method: 'PUT', body: { [k]: v } }); acctRenderBorrowCapTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-bc]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/acct/borrowing-capitalization/' + e.target.dataset.delBc, { method: 'DELETE' }); acctRenderBorrowCapTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-bc-addrow]').forEach(el => el.addEventListener('click', async e => {
        const id = e.target.dataset.bcAddrow, kind = e.target.dataset.kind;
        const row = rows.find(r => String(r.id) === String(id));
        const list = kind === 'specific' ? row.specific_borrowings : row.general_borrowings;
        list.push({ principal: 0, rate_pct: 0 });
        try {
          await api('/api/acct/borrowing-capitalization/' + id, { method: 'PUT', body: { [kind === 'specific' ? 'specific_borrowings' : 'general_borrowings']: list } });
          acctRenderBorrowCapTab(root);
        } catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-bc-row]').forEach(el => el.addEventListener('change', async e => {
        const [id, kind, idx, field] = e.target.dataset.bcRow.split('|');
        const row = rows.find(r => String(r.id) === String(id));
        const list = kind === 'specific' ? row.specific_borrowings : row.general_borrowings;
        list[Number(idx)][field] = Number(e.target.value);
        try {
          await api('/api/acct/borrowing-capitalization/' + id, { method: 'PUT', body: { [kind === 'specific' ? 'specific_borrowings' : 'general_borrowings']: list } });
          acctRenderBorrowCapTab(root);
        } catch (err) { toast(err.message, true); }
      }));
      const addBc = document.getElementById('acct-add-bc');
      if (addBc) addBc.addEventListener('click', async () => {
        try {
          await api('/api/acct/clients/' + acctState.clientId + '/borrowing-capitalization', { method: 'POST', body: {
            fy_label: state.fy, qualifying_asset: 'New qualifying asset', period_days: 365 } });
          acctRenderBorrowCapTab(root);
        } catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function acctBorrowCapCard(r) {
    const bl = (list, kind) => (list || []).map((b, i) =>
      '<div style="display:flex;gap:8px;margin-top:4px"><input type="number" data-bc-row="' + r.id + '|' + kind + '|' + i + '|principal" value="' + (b.principal || 0) + '" placeholder="Principal" style="width:140px">' +
      '<input type="number" data-bc-row="' + r.id + '|' + kind + '|' + i + '|rate_pct" value="' + (b.rate_pct || 0) + '" placeholder="Rate %" style="width:100px"></div>').join('');
    return '<details class="card" style="margin-top:10px" open><summary><b>' + esc(r.qualifying_asset) + '</b> — capitalised ' + money(r.computed.total_capitalized) + '</summary>' +
      '<div class="grid c3" style="margin-top:10px">' +
      '<div class="field"><label>Qualifying asset</label><input data-bc="' + r.id + '" data-k="qualifying_asset" value="' + esc(r.qualifying_asset || '') + '"></div>' +
      '<div class="field"><label>Period (days)</label><input type="number" data-bc="' + r.id + '" data-k="period_days" value="' + (r.period_days || 365) + '"></div>' +
      '<div class="field"><label>Capitalisable expenditure (general pool)</label><input type="number" data-bc="' + r.id + '" data-k="capitalizable_expenditure_general" value="' + (r.capitalizable_expenditure_general || 0) + '"></div>' +
      '<div class="field"><label>Income from temp. investment (specific)</label><input type="number" data-bc="' + r.id + '" data-k="income_from_temp_investment" value="' + (r.income_from_temp_investment || 0) + '"></div>' +
      '</div>' +
      '<p class="card-note" style="margin-top:10px"><b>Specific borrowings</b> (principal / rate %)</p>' + bl(r.specific_borrowings, 'specific') +
      '<button class="btn btn-small" data-bc-addrow="' + r.id + '" data-kind="specific" style="margin-top:6px">Add specific borrowing</button>' +
      '<p class="card-note" style="margin-top:10px"><b>General borrowings</b> (principal / rate %)</p>' + bl(r.general_borrowings, 'general') +
      '<button class="btn btn-small" data-bc-addrow="' + r.id + '" data-kind="general" style="margin-top:6px">Add general borrowing</button>' +
      '<p class="card-note" style="margin-top:10px">Weighted average capitalisation rate: ' + r.computed.weighted_avg_capitalization_rate_pct + '% · Specific cost: ' + money(r.computed.specific_cost) +
      ' · General capitalised: ' + money(r.computed.general_capitalized) + ' · <b>Total capitalised: ' + money(r.computed.total_capitalized) + '</b></p>' +
      '<button class="btn btn-small btn-danger" data-del-bc="' + r.id + '" style="margin-top:10px">Remove this asset</button></details>';
  }

  function acctRenderClosingTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/acct/clients/' + acctState.clientId + '/closing-checklist?fy_label=' + encodeURIComponent(state.fy)).then(rows => {
      const done = rows.filter(r => r.status === 'done').length;
      root.innerHTML = '<div class="card"><h3>Year-end closing checklist — FY ' + esc(state.fy) + '</h3><span class="hint">' + done + ' / ' + rows.length + ' done</span>' +
        '<div class="table-wrap" style="margin-top:10px"><table><thead><tr><th>Criterion</th><th>Status</th><th>Evidence</th><th>Remark</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-cc="' + r.id + '" data-k="criterion" value="' + esc(r.criterion || '') + '"></td>' +
          '<td><select data-cc="' + r.id + '" data-k="status">' + ['pending', 'done', 'na'].map(s => '<option value="' + s + '"' + (r.status === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>' +
          '<td><input data-cc="' + r.id + '" data-k="evidence" value="' + esc(r.evidence || '') + '"></td>' +
          '<td><input data-cc="' + r.id + '" data-k="remark" value="' + esc(r.remark || '') + '"></td>' +
          '<td><button class="btn btn-small btn-danger" data-del-cc="' + r.id + '">Remove</button></td></tr>').join('') +
        '</tbody></table></div>' +
        '<button class="btn btn-small" id="acct-seed-cc" style="margin-top:10px">Seed standard checklist</button></div>';

      root.querySelectorAll('[data-cc]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k;
        try { await api('/api/acct/closing-checklist/' + e.target.dataset.cc, { method: 'PUT', body: { [k]: e.target.value } }); acctRenderClosingTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      root.querySelectorAll('[data-del-cc]').forEach(el => el.addEventListener('click', async e => {
        try { await api('/api/acct/closing-checklist/' + e.target.dataset.delCc, { method: 'DELETE' }); acctRenderClosingTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const seedCc = document.getElementById('acct-seed-cc');
      if (seedCc) seedCc.addEventListener('click', async () => {
        try { const r = await api('/api/acct/clients/' + acctState.clientId + '/closing-checklist/seed', { method: 'POST', body: { fy_label: state.fy } }); toast(r.added + ' items added'); acctRenderClosingTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function acctRenderWorkpapersTab(root) {
    root.innerHTML = '<div class="empty">Loading…</div>';
    api('/api/acct/clients/' + acctState.clientId + '/working-papers').then(rows => {
      root.innerHTML = '<div class="card"><h3>Working papers</h3>' +
        '<div class="table-wrap"><table><thead><tr><th>Ref</th><th>Title</th><th>Content</th><th>Review status</th><th></th></tr></thead><tbody>' +
        rows.map(r => '<tr><td><input data-wp="' + r.id + '" data-k="ref" value="' + esc(r.ref || '') + '"></td>' +
          '<td><input data-wp="' + r.id + '" data-k="title" value="' + esc(r.title || '') + '"></td>' +
          '<td><input data-wp="' + r.id + '" data-k="content" value="' + esc(r.content || '') + '"></td>' +
          '<td><select data-wp="' + r.id + '" data-k="review_status">' + ['open', 'approved', 'rejected', 'sent_back'].map(s => '<option value="' + s + '"' + (r.review_status === s ? ' selected' : '') + '>' + s + '</option>').join('') + '</select></td>' +
          '<td></td></tr>').join('') +
        '</tbody></table></div><button class="btn btn-small" id="acct-add-wp" style="margin-top:10px">Add a working paper</button></div>';

      root.querySelectorAll('[data-wp]').forEach(el => el.addEventListener('change', async e => {
        const k = e.target.dataset.k;
        try { await api('/api/acct/working-papers/' + e.target.dataset.wp, { method: 'PUT', body: { [k]: e.target.value } }); acctRenderWorkpapersTab(root); }
        catch (err) { toast(err.message, true); }
      }));
      const addWp = document.getElementById('acct-add-wp');
      if (addWp) addWp.addEventListener('click', async () => {
        try { await api('/api/acct/clients/' + acctState.clientId + '/working-papers', { method: 'POST', body: { fy_label: state.fy, ref: 'ACC-01' } }); acctRenderWorkpapersTab(root); }
        catch (err) { toast(err.message, true); }
      });
    }).catch(e => { root.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
  }

  function acctRenderReportsTab(root) {
    root.innerHTML = '<div class="card"><h3>Reports</h3>' +
      '<p class="card-note"><b>Financial Statement Adjustments &amp; Schedules</b> — provisions, prepayments, FX revaluation, interest accrual, equity movement and cash flow, for FY ' + esc(state.fy) + '.</p>' +
      '<button class="btn btn-small" data-acct-report="adjustments_schedule" data-fmt="docx">Word</button>' +
      '<p class="card-note" style="margin-top:14px"><b>Accounting Automation Workbook</b> — full detail across all schedules.</p>' +
      '<button class="btn btn-small" data-acct-report="automation_workbook" data-fmt="xlsx">Excel</button> ' +
      '<button class="btn btn-small" data-acct-report="automation_workbook" data-fmt="docx">Word</button></div>';
    root.querySelectorAll('[data-acct-report]').forEach(el => el.addEventListener('click', async e => {
      try {
        const res = await api('/api/acct/clients/' + acctState.clientId + '/reports', { method: 'POST', body: {
          report_type: e.target.dataset.acctReport, format: e.target.dataset.fmt, fy_label: state.fy } });
        toast('Generated ' + res.filename);
        window.open(res.download_url, '_blank');
      } catch (err) { toast(err.message, true); }
    }));
  }

  const LEAD_STATUSES = [
    ['new', 'New'], ['contacted', 'Contacted'], ['qualified', 'Qualified'], ['proposal', 'Proposal'],
    ['converted', 'Converted'], ['lost', 'Lost'], ['archived', 'Archived']
  ];
  function renderLeads() {
    const view = document.getElementById('view');
    view.innerHTML =
      '<div class="page-head"><div><h2>Leads / CRM</h2><p>Every "Request access", contact-form and service-enquiry submission from the public site, plus requests raised from inside the client portal — one pipeline, from first contact to converted client.</p></div></div>' +
      '<div class="grid c3" style="margin-bottom:14px" id="lead-filters">' +
      '<div class="field"><label>Status</label><select id="lead-filter-status"><option value="">All</option>' +
      LEAD_STATUSES.map(([v, l]) => '<option value="' + v + '">' + l + '</option>').join('') + '</select></div>' +
      '</div>' +
      '<div class="card" id="lead-body"><div class="empty">Loading…</div></div>';

    if (!state.online || !state.token || state.offlineMode) {
      document.getElementById('lead-body').innerHTML = '<div class="empty"><b>Offline</b>Requests live on the server. Reconnect to see them.</div>';
      return;
    }

    const staffOptions = (state.boot && state.boot.staff) || [];
    function load() {
      const statusFilter = document.getElementById('lead-filter-status').value;
      api('/api/leads' + (statusFilter ? '?status=' + statusFilter : '')).then(rows => {
        document.getElementById('lead-body').innerHTML = rows.length
          ? rows.map(l => {
              const statusTag = l.status === 'converted' ? 'tag-green' : l.status === 'lost' || l.status === 'archived' ? 'tag-red' : 'tag-amber';
              return '<details class="card" style="margin-top:10px" data-lead-row="' + l.id + '"><summary><b>' + esc(l.name || l.email) + '</b> ' +
                '<span class="tag ' + statusTag + '">' + (LEAD_STATUSES.find(s => s[0] === l.status) || [l.status, l.status])[1] + '</span>' +
                '<span class="hint"> — ' + esc(l.email) + (l.subject ? ' · ' + esc(l.subject) : '') + '</span></summary>' +
                '<p style="margin-top:10px"><b>Message:</b> ' + esc(l.message || '—') + '</p>' +
                '<div class="grid c4">' +
                '<div class="field"><label>Status</label><select data-lead-status="' + l.id + '">' +
                LEAD_STATUSES.map(([v, lb]) => '<option value="' + v + '"' + (v === l.status ? ' selected' : '') + '>' + lb + '</option>').join('') + '</select></div>' +
                '<div class="field"><label>Assigned to</label><select data-lead-assign="' + l.id + '"><option value="">— unassigned —</option>' +
                staffOptions.map(s => '<option value="' + s.id + '"' + (s.id === l.assigned_to ? ' selected' : '') + '>' + esc(s.full_name) + '</option>').join('') + '</select></div>' +
                '<div class="field"><label>Follow-up date</label><input type="date" data-lead-followup="' + l.id + '" value="' + esc(l.follow_up_date || '') + '"></div>' +
                '<div class="field"><label>Phone</label><input value="' + esc(l.phone || '') + '" disabled></div>' +
                '</div>' +
                '<div class="field" style="margin-top:8px"><label>Notes</label><textarea data-lead-notes="' + l.id + '" rows="2" style="width:100%">' + esc(l.notes || '') + '</textarea></div>' +
                '<div style="margin-top:8px">' +
                '<button class="btn btn-small btn-primary" data-lead-save="' + l.id + '">Save</button> ' +
                (l.client_id ? '<span class="tag tag-green">Client #' + l.client_id + '</span>' :
                  '<button class="btn btn-small" data-lead-convert="' + l.id + '">Convert to client</button>') +
                '</div></details>';
            }).join('')
          : '<div class="empty"><b>No requests yet</b>They will show up here as visitors submit a form on the public site, or a client raises one from the portal.</div>';

        document.querySelectorAll('[data-lead-save]').forEach(btn => btn.addEventListener('click', async () => {
          const lid = btn.dataset.leadSave;
          const box = document.querySelector('[data-lead-row="' + lid + '"]');
          const body = {
            status: box.querySelector('[data-lead-status]').value,
            assigned_to: box.querySelector('[data-lead-assign]').value || null,
            follow_up_date: box.querySelector('[data-lead-followup]').value || null,
            notes: box.querySelector('[data-lead-notes]').value,
          };
          try { await api('/api/leads/' + lid, { method: 'PUT', body }); toast('Saved'); load(); }
          catch (e) { toast(e.message, true); }
        }));
        document.querySelectorAll('[data-lead-convert]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Convert this request into a client record?')) return;
          try { const c = await api('/api/leads/' + btn.dataset.leadConvert + '/convert', { method: 'POST', body: {} });
            toast('Created client ' + c.code); load(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { document.getElementById('lead-body').innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }
    document.getElementById('lead-filter-status').addEventListener('change', load);
    load();
  }

  const CHATBOT_STATUSES = [['open', 'Open'], ['needs_follow_up', 'Needs follow-up'], ['converted', 'Converted'], ['closed', 'Closed']];

  function renderChatbotAdmin() {
    const view = document.getElementById('view');
    view.innerHTML =
      '<div class="page-head"><div><h2>Chatbot Leads</h2><p>Every conversation from the landing-page chatbot — visitor questions, captured contact details, and the full transcript. Convert a captured lead straight into a client, enquiry or service request.</p></div></div>' +
      '<div class="grid c3" style="margin-bottom:14px" id="chatbot-filters">' +
      '<div class="field"><label>Status</label><select id="chatbot-filter-status"><option value="">All</option>' +
      CHATBOT_STATUSES.map(([v, l]) => '<option value="' + v + '">' + l + '</option>').join('') + '</select></div>' +
      '</div>' +
      '<div class="card" id="chatbot-body"><div class="empty">Loading…</div></div>';

    if (!state.online || !state.token || state.offlineMode) {
      document.getElementById('chatbot-body').innerHTML = '<div class="empty"><b>Offline</b>Chatbot conversations live on the server. Reconnect to see them.</div>';
      return;
    }

    const staffOptions = (state.boot && state.boot.staff) || [];
    function load() {
      const statusFilter = document.getElementById('chatbot-filter-status').value;
      api('/api/admin/chatbot/conversations' + (statusFilter ? '?status=' + statusFilter : '')).then(rows => {
        document.getElementById('chatbot-body').innerHTML = rows.length
          ? rows.map(c => {
              const statusTag = c.status === 'converted' ? 'tag-green' : c.status === 'closed' ? 'tag' : c.status === 'needs_follow_up' ? 'tag-amber' : 'tag';
              const who = c.name ? esc(c.name) : '(no lead captured yet)';
              return '<details class="card" style="margin-top:10px" data-chatbot-row="' + c.id + '"><summary><b>' + who + '</b> ' +
                '<span class="tag ' + statusTag + '">' + (CHATBOT_STATUSES.find(s => s[0] === c.status) || [c.status, c.status])[1] + '</span>' +
                '<span class="hint"> — ' + (c.email ? esc(c.email) + ' · ' : '') + (c.topic ? esc(c.topic) : 'no topic') +
                ' · ' + c.message_count + ' message' + (c.message_count === 1 ? '' : 's') + '</span></summary>' +
                '<div class="chatbot-detail" data-chatbot-detail="' + c.id + '" style="margin-top:10px"><div class="empty">Loading transcript…</div></div>' +
                '</details>';
            }).join('')
          : '<div class="empty"><b>No chatbot conversations yet</b>They will show up here once a visitor uses the chat widget on the landing page.</div>';

        document.querySelectorAll('[data-chatbot-row]').forEach(details => {
          details.addEventListener('toggle', async () => {
            if (!details.open) return;
            const cid = details.dataset.chatbotRow;
            const box = details.querySelector('[data-chatbot-detail]');
            if (box.dataset.loaded) return;
            try {
              const c = await api('/api/admin/chatbot/conversations/' + cid);
              box.dataset.loaded = '1';
              box.innerHTML =
                '<div class="chatbot-transcript">' +
                (c.messages.length ? c.messages.map(m => '<div class="chatbot-msg chatbot-msg-' + m.role + '"><b>' + (m.role === 'user' ? 'Visitor' : 'Assistant') + '</b><span>' + esc(m.content) + '</span></div>').join('') : '<p class="hint">No messages recorded.</p>') +
                '</div>' +
                '<div class="grid c4" style="margin-top:12px">' +
                '<div class="field"><label>Status</label><select data-chatbot-status="' + cid + '">' +
                CHATBOT_STATUSES.map(([v, l]) => '<option value="' + v + '"' + (v === c.status ? ' selected' : '') + '>' + l + '</option>').join('') + '</select></div>' +
                '<div class="field"><label>Assigned to</label><select data-chatbot-assign="' + cid + '"><option value="">— unassigned —</option>' +
                staffOptions.map(s => '<option value="' + s.id + '"' + (s.id === c.assigned_to ? ' selected' : '') + '>' + esc(s.full_name) + '</option>').join('') + '</select></div>' +
                '<div class="field"><label>Phone</label><input value="' + esc(c.phone || '—') + '" disabled></div>' +
                '<div class="field"><label>Company</label><input value="' + esc(c.company || '—') + '" disabled></div>' +
                '</div>' +
                '<div class="field" style="margin-top:8px"><label>Internal note</label><textarea data-chatbot-note="' + cid + '" rows="2" style="width:100%">' + esc(c.internal_note || '') + '</textarea></div>' +
                '<div style="margin-top:8px">' +
                '<button class="btn btn-small btn-primary" data-chatbot-save="' + cid + '">Save</button> ' +
                (c.converted_to
                  ? '<span class="tag tag-green">Converted to ' + esc(c.converted_to) + (c.converted_id ? ' #' + c.converted_id : '') + '</span>'
                  : (c.email
                    ? '<button class="btn btn-small" data-chatbot-convert="' + cid + '" data-target="client">Convert to client</button> ' +
                      '<button class="btn btn-small" data-chatbot-convert="' + cid + '" data-target="enquiry">Convert to enquiry</button> ' +
                      '<button class="btn btn-small" data-chatbot-convert="' + cid + '" data-target="service_request">Convert to service request</button> ' +
                      '<button class="btn btn-small" data-chatbot-convert="' + cid + '" data-target="appointment">Mark for appointment</button>'
                    : '<span class="hint">No contact details captured yet — nothing to convert.</span>')) +
                '</div>';
              box.querySelector('[data-chatbot-save]').addEventListener('click', async () => {
                const body = {
                  status: box.querySelector('[data-chatbot-status]').value,
                  assigned_to: box.querySelector('[data-chatbot-assign]').value || null,
                  internal_note: box.querySelector('[data-chatbot-note]').value,
                };
                try { await api('/api/admin/chatbot/conversations/' + cid, { method: 'PUT', body }); toast('Saved'); box.dataset.loaded = ''; details.open = false; details.open = true; load(); }
                catch (e) { toast(e.message, true); }
              });
              box.querySelectorAll('[data-chatbot-convert]').forEach(btn => btn.addEventListener('click', async () => {
                const target = btn.dataset.target;
                if (!confirm('Convert this lead to a ' + target.replace('_', ' ') + '?')) return;
                try {
                  const r = await api('/api/admin/chatbot/conversations/' + cid + '/convert', { method: 'POST', body: { target } });
                  toast(r.note || ('Converted to ' + target)); box.dataset.loaded = ''; load();
                } catch (e) { toast(e.message, true); }
              }));
            } catch (e) {
              box.innerHTML = '<div class="empty">' + esc(e.message) + '</div>';
            }
          });
        });
      }).catch(e => { document.getElementById('chatbot-body').innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }
    document.getElementById('chatbot-filter-status').addEventListener('change', load);
    load();
  }

  // ────────────────────────────────────────────────────────────── routing
  // null granted_modules means "every module" (a super admin, or offline
  // before boot has ever told us otherwise) — an array is the exact allow-list.
  function isModuleGranted(key) {
    const gm = state.boot && state.boot.granted_modules;
    return gm == null || gm.indexOf(key) !== -1;
  }

  // Some modules only make sense for certain countries (e.g. personal_tax
  // doesn't exist as a concept in the UAE, uae_corporate_tax doesn't exist
  // in Nepal) — server/country_service.py's MODULE_COUNTRY_APPLICABILITY is
  // the source of truth, sent down as bootstrap.module_country_applicability
  // so this stays data-driven rather than a second hard-coded copy here. A
  // key absent from the map applies to every country.
  function isModuleCountryApplicable(key) {
    const map = state.boot && state.boot.module_country_applicability;
    const limitedTo = map && map[key];
    if (!limitedTo) return true;
    const cc = (state.boot && state.boot.firm && state.boot.firm.country_code) || 'NP';
    return limitedTo.indexOf(cc) !== -1;
  }

  // One-line "what is this and what do I do here" for every fixed rail
  // entry — module tiles instead reuse the blurb already written for that
  // module's own page header, so this is the only place these need writing.
  const RAIL_HELP = {
    admin: 'Only visible to a super admin. Create staff accounts, decide which automators each person can open, review and resolve rate-correction remarks.',
    desk: 'Your landing page: what still needs syncing, what falls due next, and the files you have open on this device.',
    clients: 'Every client in one register — stage, urgency, VAT frequency, who looks after each one, and their open reminders and follow-ups. Add or edit a client here.',
    assistant: 'Ask questions about a specific client\'s status, deadlines or documents — answered straight from this app\'s own data, no setup needed.',
    billing: 'Log time against an engagement, then turn unbilled hours into a draft, sent, or paid invoice.',
    reports: 'Every Excel and Word file this account has generated, with a re-download link for each.',
    documents: 'Upload, browse and download files kept against a client or engagement.',
    leads: '"Request access" signups from the public site, so you can review and respond to them.',
    rates: 'Every statutory figure the calculators use — tax slabs, fees, thresholds — each with its source and verification status. Only a super admin can edit; anyone can flag a rate they think is wrong.',
    tp: 'Multi-jurisdiction (Nepal / India / UAE) transfer pricing — related parties, transaction register, FAR, method selection, benchmarking, risk scoring and adjustment exposure. Works against live server data, not an offline file.',
    pm: 'Client onboarding, KYC & conflict checks, engagement risk, proposals, fee quotations, engagement letters, document requests, staff assignments, time budgets and engagement closure.',
    fa: 'Upload transaction data and run fraud-screening detectors — Benford\'s Law, duplicates, round numbers, related-party transactions, unusual journal entries, ghost employees, payroll/vendor/expense fraud and a Beneish M-Score screen.',
    corp: 'Company-secretarial and Companies Act compliance — shareholding structure, capital events (rights/bonus/private placement/transfer/reduction/buyback), board meetings, board and shareholder resolutions, and a company-law compliance checklist.',
    ba: 'Product/customer/branch-wise profitability, cost structure, cost-plus pricing, budget vs actual, a KPI dashboard, a business model canvas and a business plan generator.',
    ipo: 'IPO readiness, SEBON eligibility checklist, restatement analysis, working-capital adequacy for the offer document, peer comparison, pricing analysis and risk factors — cross-linked to Due Diligence, Projection and Valuation rather than duplicating them.',
    taxadv: 'VAT and TDS reconciliation, deferred tax provision, minimum tax check, a deductibility register, permanent establishment risk and tax notice/appeal case management — alongside, not instead of, the Income Tax, TDS, VAT and Depreciation modules.',
    audx: 'Sampling selection, substantive testing (vouching, confirmations, reconciliations, physical verification, cut-off), analytical review, going concern and completion/disclosure/NFRS checklists — alongside, not instead of, Statutory Audit and Internal Audit.',
    acct: 'Trial balance diagnostics, tailored accounting policies, cash flow and changes-in-equity statements, accruals/provisions, prepayment amortisation, FX revaluation, interest accrual, NAS 23 borrowing-cost capitalisation and a year-end closing checklist — alongside, not instead of, Financial Statements, Depreciation and Lease (NFRS 16).',
  };

  // These 9 module_keys are enforced server-side (_require_module) and wired into
  // buildRail(), but never lived in the MODULES dict — that dict only ever held the
  // older workfile-driven tools. Grantable-modules lists (Admin, the module
  // dashboard below) must include them too, or an admin has no way to grant them
  // and a searching user has no way to find them.
  const EXTRA_MODULES = {
    transfer_pricing:  { route: 'tp',     code: 'TP', name: 'Transfer Pricing',      group: 'Advisory',        blurb: RAIL_HELP.tp },
    practice_mgmt:      { route: 'pm',     code: 'PM', name: 'Practice Management',   group: 'Practice',        blurb: RAIL_HELP.pm },
    forensic_audit:     { route: 'fa',     code: 'FA', name: 'Forensic Audit',        group: 'Assurance',       blurb: RAIL_HELP.fa },
    corporate_advisory: { route: 'corp',   code: 'CP', name: 'Corporate Advisory',    group: 'Advisory',        blurb: RAIL_HELP.corp },
    business_advisory:  { route: 'ba',     code: 'BA', name: 'Business Advisory',     group: 'Advisory',        blurb: RAIL_HELP.ba },
    ipo_advisory:       { route: 'ipo',    code: 'IP', name: 'IPO Advisory',          group: 'Advisory',        blurb: RAIL_HELP.ipo },
    tax_advisory:       { route: 'taxadv', code: 'TX', name: 'Tax Advisory',          group: 'Tax computation', blurb: RAIL_HELP.taxadv },
    audit_execution:    { route: 'audx',   code: 'AX', name: 'Audit Execution',       group: 'Assurance',       blurb: RAIL_HELP.audx },
    acct_automation:    { route: 'acct',   code: 'AA', name: 'Accounting Automation', group: 'Accounting',      blurb: RAIL_HELP.acct },
  };

  // Every grantable module in one place — the 21 MODULES-dict tools plus the 9
  // EXTRA_MODULES above. Used by renderAdmin()'s grant checkboxes and the module
  // directory/search on the Desk page, so both stay complete as new modules are added.
  const MODULE_CATALOG = {};
  Object.keys(MODULES).forEach(k => { MODULE_CATALOG[k] = Object.assign({ route: k }, MODULES[k]); });
  Object.keys(EXTRA_MODULES).forEach(k => { MODULE_CATALOG[k] = EXTRA_MODULES[k]; });

  // The 5 admin content-area gates (server: _require_module(u, "…")) — not
  // real "modules" with their own workfile/dashboard, but grantable exactly
  // like one so a role (e.g. "Content Creator") can be scoped to just these.
  const ADMIN_AREA_MODULES = {
    site_settings: { name: 'Site Settings', blurb: 'Services, pages, testimonials, firm profile/Maps, email and payment gateways' },
    blog_admin:    { name: 'Blog', blurb: 'Blog categories and articles' },
    career_admin:  { name: 'Careers', blurb: 'Job postings, applications and email templates' },
    chatbot_admin: { name: 'Chatbot Leads', blurb: 'Landing-page chatbot conversations and captured leads' },
    billing:       { name: 'Time & Billing', blurb: RAIL_HELP.billing },
    org_admin:     { name: 'Organization & People', blurb: 'Branches, departments, teams, positions, employee profiles, skills and workflow rules' },
  };
  // India-specific engines (server/india_engines.py) — callable via the same
  // POST /api/compute every other engine uses (module_key: india_depreciation/
  // india_gst/india_interest_penalty), but with no dedicated module-page UI
  // yet, unlike everything in MODULES/EXTRA_MODULES above (no `route`, so
  // this intentionally never appears as a nav link — see PROJECT_STATE.md).
  // Grantable here so an admin has somewhere to turn on API access ahead of
  // that UI existing.
  const INDIA_ENGINE_MODULES = {
    india_depreciation:     { name: 'India Depreciation (Sec 32)', blurb: 'Block-of-assets WDV depreciation — API only, no module page yet' },
    india_gst:              { name: 'India GST', blurb: 'Multi-slab GST return with CGST/SGST/IGST and input tax credit — API only, no module page yet' },
    india_interest_penalty: { name: 'India Interest & Late Fee (234A/B/C/F)', blurb: 'API only, no module page yet' },
  };
  // Every key an admin can grant, including the 6 admin areas and 3 India
  // engines above — must stay in sync with server/db.py's
  // ALL_GRANTABLE_MODULE_KEYS (39 keys).
  const GRANTABLE_MODULES = Object.assign({}, MODULE_CATALOG, ADMIN_AREA_MODULES, INDIA_ENGINE_MODULES);

  // A firm can never grant its staff a module beyond what CA-Automator's
  // platform admin has entitled it to (firm_module_grant) — null/missing
  // entitled_modules means an older cached boot payload, treated as
  // unrestricted rather than hiding every checkbox.
  function isFirmEntitled(key) {
    const em = state.boot && state.boot.firm && state.boot.firm.entitled_modules;
    return !em || em.indexOf(key) !== -1;
  }

  // Deliberate, fixed visual order — not object-key insertion order, which
  // depended on where each module happened to be defined in this file.
  // "Assurance" (EXTRA_MODULES' name for forensic_audit/audit_execution)
  // folds into "Audit" (the name the 3 older MODULES-dict audit tools
  // already use) so there's one Audit section, not two differently-named
  // ones in different places.
  const RAIL_GROUP_ORDER = ['Platform', 'Overview', 'Admin', 'Tax computation', 'Accounting', 'Advisory', 'Audit', 'Practice'];
  const RAIL_GROUP_ALIAS = { Assurance: 'Audit' };

  function buildRail() {
    const isAdmin = !!(state.boot && state.boot.user && state.boot.user.is_super_admin);
    const isPlatformAdmin = !!(state.boot && state.boot.user && state.boot.user.is_platform_admin);
    const sections = {};
    function addItem(group, key, code, name, blurb, extraHtml) {
      (sections[group] = sections[group] || []).push({ key, code, name, blurb, extraHtml: extraHtml || '' });
    }

    if (isPlatformAdmin) addItem('Platform', 'platform', 'PL', 'Platform Admin',
      'CA-Automator\'s own operator view — every tenant firm, its seat cap/plan, module entitlements and registered staff.');

    addItem('Overview', 'desk', 'DK', 'Desk', RAIL_HELP.desk);
    addItem('Overview', 'clients', 'CL', 'Client Directory', RAIL_HELP.clients);

    // The Admin screen itself (staff/user management) is gated purely by
    // is_super_admin, not by module entitlement — it's firm self-management,
    // orthogonal to which modules the firm's plan includes.
    if (isAdmin) addItem('Admin', 'admin', 'AD', 'Admin', RAIL_HELP.admin);
    // Site Settings/Blog/Careers/Chatbot Leads/Billing are each gated purely
    // by isModuleGranted() — which for a firm's own super admin now
    // correctly resolves to "everything the firm's plan includes" (see
    // server/app.py bootstrap()), NOT an unconditional bypass. A firm
    // subscribed to a single module must not see every other module just
    // because the person happens to be that firm's admin.
    if (isModuleGranted('site_settings'))
      addItem('Admin', 'site-settings', 'SS', 'Site Settings', 'Services, pages, testimonials, firm profile/Maps, email and payment gateways');
    // Blog has no per-firm isolation (blog_post has no firm_id at all — it's
    // CA-Automator's own single shared blog), so it's gated on platform
    // admin here too, not isModuleGranted — a tenant firm never sees a link
    // that would just 403 (see _require_platform_admin on the blog routes).
    if (isPlatformAdmin)
      addItem('Admin', 'blog-admin', 'BG', 'Blog', 'Blog categories and articles — CA-Automator\'s own shared blog');
    if (isModuleGranted('career_admin'))
      addItem('Admin', 'career-admin', 'CR', 'Careers', 'Job postings, applications and email templates');
    if (isModuleGranted('chatbot_admin'))
      addItem('Admin', 'chatbot-admin', 'CB', 'Chatbot Leads', 'Landing-page chatbot conversations and captured leads');
    if (isModuleGranted('org_admin'))
      addItem('Admin', 'org-admin', 'OP', 'Organization & People', 'Branches, departments, teams, positions, employee profiles, skills, workflow rules and resource workload');

    Object.keys(MODULES).forEach(k => {
      if (!isModuleGranted(k) || !isModuleCountryApplicable(k)) return;
      const m = MODULES[k];
      addItem(m.group, k, m.code, m.name, m.blurb);
    });
    Object.keys(EXTRA_MODULES).forEach(k => {
      if (!isModuleGranted(k) || !isModuleCountryApplicable(k)) return;
      const m = EXTRA_MODULES[k];
      addItem(RAIL_GROUP_ALIAS[m.group] || m.group, m.route, m.code, m.name, m.blurb);
    });

    addItem('Practice', 'tasks', 'TK', 'Tasks', 'Work items, checklists, dependencies and subtasks across your clients and engagements');
    addItem('Practice', 'calendar', 'CA', 'Calendar', 'The firm\'s internal calendar — meetings and events, separate from client consultation bookings');
    addItem('Practice', 'assistant', 'AI', 'CA Assistant', RAIL_HELP.assistant);
    if (isModuleGranted('billing'))
      addItem('Practice', 'billing', 'BL', 'Time & Billing', RAIL_HELP.billing);
    addItem('Practice', 'reports', 'RP', 'Reports', RAIL_HELP.reports);
    addItem('Practice', 'documents', 'DC', 'Documents', RAIL_HELP.documents);
    addItem('Practice', 'leads', 'RQ', 'Requests', RAIL_HELP.leads);
    addItem('Practice', 'rates', 'RM', 'Rate master', RAIL_HELP.rates,
      (state.boot && state.boot.unverified_rates) ? '<span class="tally">' + state.boot.unverified_rates + '</span>' : '');

    let collapsedGroups = {};
    try { collapsedGroups = JSON.parse(localStorage.getItem('ca.railCollapsedGroups') || '{}'); } catch (e) { /* ignore */ }

    let h = '';
    RAIL_GROUP_ORDER.forEach(g => {
      const items = sections[g];
      if (!items || !items.length) return;
      const isCollapsed = !!collapsedGroups[g];
      h += '<div class="rail-group' + (isCollapsed ? ' collapsed' : '') + '" data-rail-group="' + esc(g) + '">' + esc(g) + '</div>' +
        '<div class="rail-group-items' + (isCollapsed ? ' collapsed' : '') + '" data-rail-group-items="' + esc(g) + '">' +
        items.map(m => '<a data-go="' + m.key + '" title="' + esc(m.blurb || m.name) + '"><code>' + esc(m.code) + '</code>' + esc(m.name) + m.extraHtml + '</a>').join('') +
        '</div>';
    });

    const rail = document.getElementById('rail');
    rail.innerHTML = h;
    rail.querySelectorAll('[data-go]').forEach(a => a.addEventListener('click', () => {
      location.hash = '#/' + a.dataset.go;
      closeRailOnMobile();
    }));
    rail.querySelectorAll('[data-rail-group]').forEach(header => header.addEventListener('click', () => {
      const g = header.dataset.railGroup;
      const target = Array.from(rail.querySelectorAll('[data-rail-group-items]')).find(el => el.dataset.railGroupItems === g);
      const nowCollapsed = !header.classList.contains('collapsed');
      header.classList.toggle('collapsed', nowCollapsed);
      if (target) target.classList.toggle('collapsed', nowCollapsed);
      collapsedGroups[g] = nowCollapsed;
      try { localStorage.setItem('ca.railCollapsedGroups', JSON.stringify(collapsedGroups)); } catch (e) { /* ignore */ }
    }));
  }

  // ── Whole-rail hide/show — a collapsed sidebar on desktop (more room for
  // the stage), an off-canvas drawer on tablet/mobile (never a squeezed
  // sidebar or a horizontal-scrolling 30-item strip). One toggle, one
  // persisted preference; CSS decides which behaviour applies at the
  // current width (see .rail-closed/.rail-open in style.css).
  const RAIL_MOBILE_QUERY = '(max-width:820px)';
  function isRailMobile() { return window.matchMedia(RAIL_MOBILE_QUERY).matches; }
  function setRailVisible(visible) {
    document.body.classList.toggle('rail-open', visible);
    document.body.classList.toggle('rail-closed', !visible);
    const btn = document.getElementById('rail-toggle');
    if (btn) btn.setAttribute('aria-expanded', String(visible));
    const backdrop = document.getElementById('rail-backdrop');
    if (backdrop) backdrop.hidden = !(visible && isRailMobile());
    try { if (!isRailMobile()) localStorage.setItem('ca.railVisible', visible ? '1' : '0'); } catch (e) { /* ignore */ }
  }
  function closeRailOnMobile() {
    if (isRailMobile()) setRailVisible(false);
  }
  function initRailToggle() {
    const btn = document.getElementById('rail-toggle');
    const backdrop = document.getElementById('rail-backdrop');
    if (!btn) return;
    let stored = null;
    try { stored = localStorage.getItem('ca.railVisible'); } catch (e) { /* ignore */ }
    setRailVisible(isRailMobile() ? false : (stored === null ? true : stored === '1'));
    btn.addEventListener('click', () => {
      const currentlyOpen = document.body.classList.contains('rail-open');
      setRailVisible(!currentlyOpen);
    });
    if (backdrop) backdrop.addEventListener('click', () => setRailVisible(false));
    window.addEventListener('resize', () => {
      // Crossing the mobile breakpoint mid-session (rotating a tablet, a
      // resized window) re-applies the right default for the new width
      // rather than leaving a desktop-collapsed rail stuck hidden on
      // mobile, or a mobile-open drawer stuck open on desktop.
      setRailVisible(isRailMobile() ? false : true);
    });
  }

  // ======================================================================
  // ORGANIZATION & PEOPLE — branches, departments, teams, positions,
  // employees, skills, workflow rules, resource workload. One page, several
  // independently-loading card sections, same shape as renderSiteSettings().
  // ======================================================================

  function renderOrgAdmin() {
    const view = document.getElementById('view');
    view.innerHTML =
      '<div class="page-head"><div><h2>Organization &amp; People</h2>' +
      '<p>Branches, departments, teams, positions, employee profiles, skills and workflow rules — the firm\'s own structure, not hard-coded into the app.</p></div></div>' +
      '<div class="card" id="org-branches-body"><div class="empty">Loading…</div></div>' +
      '<div class="card" id="org-departments-body"><div class="empty">Loading…</div></div>' +
      '<div class="card" id="org-teams-body"><div class="empty">Loading…</div></div>' +
      '<div class="card" id="org-positions-body"><div class="empty">Loading…</div></div>' +
      '<div class="card" id="org-employees-body"><div class="empty">Loading…</div></div>' +
      '<div class="card" id="org-skills-body"><div class="empty">Loading…</div></div>' +
      '<div class="card" id="org-workflow-body"><div class="empty">Loading…</div></div>' +
      '<div class="card" id="org-workload-body"><div class="empty">Loading…</div></div>';

    if (!state.online || !state.token || state.offlineMode) {
      view.querySelectorAll('.card').forEach(c => c.innerHTML = '<div class="empty"><b>Offline</b>This screen needs a connection.</div>');
      return;
    }

    let branchesCache = [], departmentsCache = [], usersCache = [], positionsCache = [], skillsCache = [];

    function drawBranches() {
      const body = document.getElementById('org-branches-body');
      api('/api/admin/org/branches').then(branches => {
        branchesCache = branches;
        body.innerHTML = '<h3>Branches</h3>' +
          branches.map(b => '<div class="row-line">' + esc(b.name) + (b.is_head_office ? ' <span class="tag tag-green">Head office</span>' : '') +
            (b.address ? ' <span class="hint">' + esc(b.address) + '</span>' : '') +
            ' <button class="btn btn-small btn-danger" data-del-branch="' + b.id + '">Delete</button></div>').join('') +
          '<div class="grid c3" style="margin-top:10px" id="org-new-branch">' +
          field('Name', 'name', {}, 'text') + field('Address', 'address', {}, 'text') +
          field('Head office', 'is_head_office', {}, 'check') + '</div>' +
          '<button class="btn btn-small btn-primary" id="org-add-branch" style="margin-top:8px">Add branch</button>';
        document.getElementById('org-add-branch').addEventListener('click', async () => {
          const b = {}; document.querySelectorAll('#org-new-branch [data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          b.is_head_office = document.querySelector('#org-new-branch [data-bind="is_head_office"]').checked;
          if (!b.name) { toast('Name is required', true); return; }
          try { await api('/api/admin/org/branches', { method: 'POST', body: b }); toast('Branch added'); drawBranches(); drawDepartments(); drawEmployees(); }
          catch (e) { toast(e.message, true); }
        });
        body.querySelectorAll('[data-del-branch]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this branch?')) return;
          try { await api('/api/admin/org/branches/' + btn.dataset.delBranch, { method: 'DELETE' }); drawBranches(); drawDepartments(); drawEmployees(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawDepartments() {
      const body = document.getElementById('org-departments-body');
      api('/api/admin/org/departments').then(departments => {
        departmentsCache = departments;
        body.innerHTML = '<h3>Departments</h3>' +
          departments.map(d => '<div class="row-line">' + esc(d.name) +
            (d.branch_name ? ' <span class="hint">' + esc(d.branch_name) + '</span>' : '') +
            ' <button class="btn btn-small btn-danger" data-del-dept="' + d.id + '">Delete</button></div>').join('') +
          '<div class="grid c3" style="margin-top:10px" id="org-new-dept">' +
          field('Name', 'name', {}, 'text') +
          field('Branch', 'branch_id', {}, 'select', { options: [{ value: '', label: '— none —' }].concat(branchesCache.map(b => ({ value: b.id, label: b.name }))) }) +
          '</div>' +
          '<button class="btn btn-small btn-primary" id="org-add-dept" style="margin-top:8px">Add department</button>';
        document.getElementById('org-add-dept').addEventListener('click', async () => {
          const b = {}; document.querySelectorAll('#org-new-dept [data-key]').forEach(el => { b[el.dataset.key] = el.value || null; });
          if (!b.name) { toast('Name is required', true); return; }
          try { await api('/api/admin/org/departments', { method: 'POST', body: b }); toast('Department added'); drawDepartments(); drawTeams(); drawEmployees(); }
          catch (e) { toast(e.message, true); }
        });
        body.querySelectorAll('[data-del-dept]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this department?')) return;
          try { await api('/api/admin/org/departments/' + btn.dataset.delDept, { method: 'DELETE' }); drawDepartments(); drawTeams(); drawEmployees(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawTeams() {
      const body = document.getElementById('org-teams-body');
      api('/api/admin/org/teams').then(teams => {
        body.innerHTML = '<h3>Teams</h3>' +
          teams.map(t => '<details class="card" style="margin-top:8px"><summary><b>' + esc(t.name) + '</b>' +
            (t.department_name ? ' <span class="hint">' + esc(t.department_name) + '</span>' : '') + '</summary>' +
            '<div style="margin-top:8px">' + (t.members.length ? t.members.map(m =>
              '<span class="tag">' + esc(m.full_name) + ' <a href="#" data-rm-member="' + t.id + ':' + m.user_id + '" style="color:inherit">×</a></span> ').join('') : '<span class="hint">No members yet</span>') + '</div>' +
            '<div class="grid c2" style="margin-top:8px">' +
            '<select data-add-member-select="' + t.id + '"><option value="">Add a member…</option>' +
            usersCache.map(u => '<option value="' + u.id + '">' + esc(u.full_name) + '</option>').join('') + '</select>' +
            '<button class="btn btn-small" data-add-member-btn="' + t.id + '">Add</button></div>' +
            '<button class="btn btn-small btn-danger" data-del-team="' + t.id + '" style="margin-top:8px">Delete team</button></details>').join('') +
          '<div class="grid c3" style="margin-top:10px" id="org-new-team">' +
          field('Name', 'name', {}, 'text') +
          field('Department', 'department_id', {}, 'select', { options: [{ value: '', label: '— none —' }].concat(departmentsCache.map(d => ({ value: d.id, label: d.name }))) }) +
          '</div>' +
          '<button class="btn btn-small btn-primary" id="org-add-team" style="margin-top:8px">Add team</button>';
        document.getElementById('org-add-team').addEventListener('click', async () => {
          const b = {}; document.querySelectorAll('#org-new-team [data-key]').forEach(el => { b[el.dataset.key] = el.value || null; });
          if (!b.name) { toast('Name is required', true); return; }
          try { await api('/api/admin/org/teams', { method: 'POST', body: b }); toast('Team added'); drawTeams(); }
          catch (e) { toast(e.message, true); }
        });
        body.querySelectorAll('[data-del-team]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this team?')) return;
          try { await api('/api/admin/org/teams/' + btn.dataset.delTeam, { method: 'DELETE' }); drawTeams(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-add-member-btn]').forEach(btn => btn.addEventListener('click', async () => {
          const teamId = btn.dataset.addMemberBtn;
          const sel = body.querySelector('[data-add-member-select="' + teamId + '"]');
          if (!sel.value) return;
          try { await api('/api/admin/org/teams/' + teamId + '/members', { method: 'POST', body: { user_id: sel.value } }); drawTeams(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-rm-member]').forEach(a => a.addEventListener('click', async (ev) => {
          ev.preventDefault();
          const [teamId, userId] = a.dataset.rmMember.split(':');
          try { await api('/api/admin/org/teams/' + teamId + '/members/' + userId, { method: 'DELETE' }); drawTeams(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawPositions() {
      const body = document.getElementById('org-positions-body');
      api('/api/admin/positions').then(positions => {
        positionsCache = positions;
        body.innerHTML = '<h3>Positions</h3><p class="hint">Managing Partner → Intern by default — rename, reorder (lower number = more senior) or add your own; nothing about these names is hard-coded into the app.</p>' +
          positions.map(p => '<div class="row-line">' + esc(p.name) + ' <span class="hint">level ' + p.hierarchy_level + '</span>' +
            ' <button class="btn btn-small btn-danger" data-del-pos="' + p.id + '">Delete</button></div>').join('') +
          '<div class="grid c3" style="margin-top:10px" id="org-new-pos">' +
          field('Name', 'name', {}, 'text') + field('Hierarchy level (0 = most senior)', 'hierarchy_level', { hierarchy_level: positions.length }, 'number') +
          '</div>' +
          '<button class="btn btn-small btn-primary" id="org-add-pos" style="margin-top:8px">Add position</button>';
        document.getElementById('org-add-pos').addEventListener('click', async () => {
          const b = {}; document.querySelectorAll('#org-new-pos [data-key]').forEach(el => { b[el.dataset.key] = el.value; });
          if (!b.name) { toast('Name is required', true); return; }
          try { await api('/api/admin/positions', { method: 'POST', body: b }); toast('Position added'); drawPositions(); drawEmployees(); }
          catch (e) { toast(e.message, true); }
        });
        body.querySelectorAll('[data-del-pos]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this position?')) return;
          try { await api('/api/admin/positions/' + btn.dataset.delPos, { method: 'DELETE' }); drawPositions(); drawEmployees(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawEmployees() {
      const body = document.getElementById('org-employees-body');
      api('/api/admin/employees').then(employees => {
        usersCache = employees.map(e => ({ id: e.user_id, full_name: e.full_name }));
        const posOptions = [{ value: '', label: '— none —' }].concat(positionsCache.map(p => ({ value: p.id, label: p.name })));
        const deptOptions = [{ value: '', label: '— none —' }].concat(departmentsCache.map(d => ({ value: d.id, label: d.name })));
        const branchOptions = [{ value: '', label: '— none —' }].concat(branchesCache.map(b => ({ value: b.id, label: b.name })));
        const reportsToOptions = [{ value: '', label: '— none —' }].concat(employees.map(e => ({ value: e.user_id, label: e.full_name })));
        body.innerHTML = '<h3>Employees</h3>' +
          employees.map(e => '<details class="card" style="margin-top:8px" id="emp-' + e.user_id + '"><summary><b>' + esc(e.full_name) +
            '</b> <span class="hint">' + esc(e.job_position_name || 'no position set') + '</span></summary>' +
            '<div class="grid c3" style="margin-top:10px" data-emp-form="' + e.user_id + '">' +
            field('Position', 'job_position_id', e, 'select', { options: posOptions }) +
            field('Department', 'department_id', e, 'select', { options: deptOptions }) +
            field('Branch', 'branch_id', e, 'select', { options: branchOptions }) +
            field('Reports to', 'reports_to_user_id', e, 'select', { options: reportsToOptions.filter(o => o.value !== e.user_id) }) +
            field('Employee code', 'employee_code', e, 'text') +
            field('Employment type', 'employment_type', Object.assign({}, e, { employment_type: e.employment_type || 'full_time' }), 'select', { options: [
              { value: 'full_time', label: 'Full time' }, { value: 'part_time', label: 'Part time' },
              { value: 'contract', label: 'Contract' }, { value: 'intern', label: 'Intern' }] }) +
            field('Weekly capacity (hours)', 'weekly_capacity_hours', Object.assign({}, e, { weekly_capacity_hours: e.weekly_capacity_hours || 40 }), 'number') +
            field('Cost rate / hour', 'cost_rate', e, 'number') + field('Billing rate / hour', 'billing_rate', e, 'number') +
            field('Phone', 'phone', e, 'text') + field('Date of joining', 'date_of_joining', e, 'date') +
            '</div>' +
            '<button class="btn btn-small btn-primary" data-save-emp="' + e.user_id + '" style="margin-top:8px">Save</button></details>').join('');
        body.querySelectorAll('[data-save-emp]').forEach(btn => btn.addEventListener('click', async () => {
          const uid = btn.dataset.saveEmp;
          const b = {}; body.querySelector('[data-emp-form="' + uid + '"]').querySelectorAll('[data-key]').forEach(el => { b[el.dataset.key] = el.value || null; });
          try { await api('/api/admin/employees/' + uid, { method: 'PUT', body: b }); toast('Employee profile saved'); drawEmployees(); drawWorkload(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawSkills() {
      const body = document.getElementById('org-skills-body');
      api('/api/admin/skills').then(skills => {
        skillsCache = skills;
        body.innerHTML = '<h3>Skills</h3>' +
          skills.map(s => '<span class="tag">' + esc(s.name) + ' <a href="#" data-del-skill="' + s.id + '" style="color:inherit">×</a></span> ').join('') +
          '<div class="grid c2" style="margin-top:10px" id="org-new-skill">' + field('New skill name', 'name', {}, 'text') + '</div>' +
          '<button class="btn btn-small btn-primary" id="org-add-skill" style="margin-top:8px">Add skill</button>' +
          '<hr style="margin:14px 0"><h4>Assign skills to an employee</h4>' +
          '<div class="grid c2"><select id="org-skill-emp-select"><option value="">Choose an employee…</option>' +
          usersCache.map(u => '<option value="' + u.id + '">' + esc(u.full_name) + '</option>').join('') + '</select>' +
          '<div id="org-skill-emp-list"></div></div>';
        document.getElementById('org-add-skill').addEventListener('click', async () => {
          const name = document.querySelector('#org-new-skill [data-key="name"]').value;
          if (!name) { toast('Name is required', true); return; }
          try { await api('/api/admin/skills', { method: 'POST', body: { name } }); toast('Skill added'); drawSkills(); }
          catch (e) { toast(e.message, true); }
        });
        body.querySelectorAll('[data-del-skill]').forEach(a => a.addEventListener('click', async (ev) => {
          ev.preventDefault();
          if (!confirm('Delete this skill? It will be removed from every employee who has it.')) return;
          try { await api('/api/admin/skills/' + a.dataset.delSkill, { method: 'DELETE' }); drawSkills(); }
          catch (e) { toast(e.message, true); }
        }));
        const empSelect = document.getElementById('org-skill-emp-select');
        empSelect.addEventListener('change', async () => {
          const list = document.getElementById('org-skill-emp-list');
          if (!empSelect.value) { list.innerHTML = ''; return; }
          const current = await api('/api/employees/' + empSelect.value + '/skills');
          const byId = {}; current.forEach(c => { byId[c.skill_id] = c.proficiency; });
          list.innerHTML = skillsCache.map(s => '<label class="check"><input type="checkbox" data-skill-check="' + s.id + '"' +
            (byId[s.id] ? ' checked' : '') + '> ' + esc(s.name) +
            ' <input type="number" min="1" max="5" data-skill-prof="' + s.id + '" value="' + (byId[s.id] || 3) + '" style="width:50px;margin-left:6px"></label>').join('<br>') +
            '<button class="btn btn-small btn-primary" id="org-save-emp-skills" style="margin-top:8px">Save</button>';
          document.getElementById('org-save-emp-skills').addEventListener('click', async () => {
            const skills = [];
            list.querySelectorAll('[data-skill-check]:checked').forEach(cb => {
              skills.push({ skill_id: cb.dataset.skillCheck, proficiency: list.querySelector('[data-skill-prof="' + cb.dataset.skillCheck + '"]').value });
            });
            try { await api('/api/employees/' + empSelect.value + '/skills', { method: 'PUT', body: { skills } }); toast('Skills saved'); }
            catch (e) { toast(e.message, true); }
          });
        });
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawWorkflowRules() {
      const body = document.getElementById('org-workflow-body');
      const TRIGGER_EVENTS = ['engagement.created', 'task.completed', 'task.overdue', 'deadline.approaching'];
      const ACTION_TYPES = ['create_task', 'notify_user', 'notify_role'];
      api('/api/admin/workflow-rules').then(rules => {
        body.innerHTML = '<h3>Workflow rules</h3><p class="hint">Event → condition → action. E.g. "when a task is marked completed, notify every partner."</p>' +
          rules.map(r => '<div class="row-line"><b>' + esc(r.name) + '</b> <span class="hint">' + esc(r.trigger_event) + ' → ' + esc(r.action_type) + '</span>' +
            (r.is_active ? ' <span class="tag tag-green">Active</span>' : ' <span class="tag">Inactive</span>') +
            ' <button class="btn btn-small btn-danger" data-del-rule="' + r.id + '">Delete</button></div>').join('') +
          '<div class="grid c3" style="margin-top:10px" id="org-new-rule">' +
          field('Name', 'name', {}, 'text') +
          field('When (trigger)', 'trigger_event', {}, 'select', { options: TRIGGER_EVENTS.map(t => ({ value: t, label: t })) }) +
          field('Then (action)', 'action_type', {}, 'select', { options: ACTION_TYPES.map(t => ({ value: t, label: t })) }) +
          field('Notify role (for notify_role)', 'role', {}, 'select', { options: [{ value: '', label: '—' }, { value: 'partner', label: 'Partner' }, { value: 'manager', label: 'Manager' }, { value: 'staff', label: 'Staff' }] }) +
          field('Message body', 'body', {}, 'text') +
          '</div>' +
          '<button class="btn btn-small btn-primary" id="org-add-rule" style="margin-top:8px">Add rule</button>';
        document.getElementById('org-add-rule').addEventListener('click', async () => {
          const name = document.querySelector('#org-new-rule [data-key="name"]').value;
          const trigger_event = document.querySelector('#org-new-rule [data-key="trigger_event"]').value;
          const action_type = document.querySelector('#org-new-rule [data-key="action_type"]').value;
          const role = document.querySelector('#org-new-rule [data-key="role"]').value;
          const bodyText = document.querySelector('#org-new-rule [data-key="body"]').value;
          if (!name) { toast('Name is required', true); return; }
          const action_json = action_type === 'notify_role' ? { role, body: bodyText } : { body: bodyText, title: bodyText };
          try {
            await api('/api/admin/workflow-rules', { method: 'POST', body: { name, trigger_event, action_type, action_json } });
            toast('Rule added'); drawWorkflowRules();
          } catch (e) { toast(e.message, true); }
        });
        body.querySelectorAll('[data-del-rule]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this rule?')) return;
          try { await api('/api/admin/workflow-rules/' + btn.dataset.delRule, { method: 'DELETE' }); drawWorkflowRules(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    function drawWorkload() {
      const body = document.getElementById('org-workload-body');
      api('/api/resource/workload').then(rows => {
        body.innerHTML = '<h3>Workload</h3><p class="hint">Allocated hours (still-open engagements) against weekly capacity — who\'s overloaded, who has room.</p>' +
          '<table class="data-table"><thead><tr><th>Employee</th><th>Position</th><th>Capacity</th><th>Allocated</th><th>Logged (7d)</th><th>Utilization</th></tr></thead><tbody>' +
          rows.map(r => '<tr><td>' + esc(r.full_name) + '</td><td>' + esc(r.job_position_name || '') + '</td>' +
            '<td>' + r.weekly_capacity_hours + '</td><td>' + r.allocated_hours + '</td><td>' + r.logged_hours_last_7_days + '</td>' +
            '<td>' + (r.utilization_pct == null ? '—' : (r.utilization_pct >= 100 ? '<span class="tag tag-red">' : r.utilization_pct >= 80 ? '<span class="tag tag-amber">' : '<span class="tag tag-green">') + r.utilization_pct + '%</span>') + '</td></tr>').join('') +
          '</tbody></table>';
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }

    // Load order matters — later sections' dropdowns (department picker
    // needs branches, employees needs positions/departments/branches) need
    // the earlier ones' caches already populated, so this isn't parallel.
    Promise.all([api('/api/admin/org/branches'), api('/api/admin/org/departments'), api('/api/admin/positions'), api('/api/admin/employees')])
      .then(([branches, departments, positions, employees]) => {
        branchesCache = branches; departmentsCache = departments; positionsCache = positions;
        usersCache = employees.map(e => ({ id: e.user_id, full_name: e.full_name }));
        drawBranches(); drawDepartments(); drawTeams(); drawPositions(); drawEmployees(); drawSkills(); drawWorkflowRules(); drawWorkload();
      }).catch(e => { view.querySelectorAll('.card').forEach(c => c.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'); });
  }

  // ======================================================================
  // TASKS
  // ======================================================================

  function renderTasks() {
    const view = document.getElementById('view');
    view.innerHTML =
      '<div class="page-head"><div><h2>Tasks</h2><p>Work items across your clients and engagements, with checklists, dependencies and subtasks.</p></div></div>' +
      '<details class="card"><summary><b>New task</b></summary><div class="grid c3" style="margin-top:10px" id="task-new-form">' +
      field('Title', 'title', {}, 'text') + field('Due date', 'due_date', {}, 'date') +
      field('Priority', 'priority', { priority: 'normal' }, 'select', { options: [{ value: 'low', label: 'Low' }, { value: 'normal', label: 'Normal' }, { value: 'high', label: 'High' }] }) +
      '</div><button class="btn btn-small btn-primary" id="task-add" style="margin-top:8px">Add task</button></details>' +
      '<div class="card" id="tasks-body"><div class="empty">Loading…</div></div>';

    if (!state.online || !state.token || state.offlineMode) {
      document.getElementById('tasks-body').innerHTML = '<div class="empty"><b>Offline</b>Tasks need a connection.</div>';
      return;
    }

    document.getElementById('task-add').addEventListener('click', async () => {
      const b = {}; document.querySelectorAll('#task-new-form [data-key]').forEach(el => { b[el.dataset.key] = el.value || null; });
      if (!b.title) { toast('Title is required', true); return; }
      try { await api('/api/tasks', { method: 'POST', body: b }); toast('Task added'); drawTasks(); }
      catch (e) { toast(e.message, true); }
    });

    function drawTasks() {
      const body = document.getElementById('tasks-body');
      api('/api/tasks').then(tasks => {
        body.innerHTML = tasks.length ? tasks.map(t => {
          const done = t.status === 'done';
          return '<details class="card" style="margin-top:8px"><summary>' +
            '<input type="checkbox" data-toggle-done="' + t.id + '"' + (done ? ' checked' : '') + '> ' +
            '<b' + (done ? ' style="text-decoration:line-through"' : '') + '>' + esc(t.title) + '</b>' +
            (t.due_date ? ' <span class="hint">due ' + esc(t.due_date) + '</span>' : '') +
            (t.assignee_name ? ' <span class="tag">' + esc(t.assignee_name) + '</span>' : '') +
            (t.priority === 'high' ? ' <span class="tag tag-red">High</span>' : '') +
            '</summary>' +
            (t.client_name ? '<p class="hint">Client: ' + esc(t.client_name) + '</p>' : '') +
            (t.reviewer_name ? '<p class="hint">Reviewer: ' + esc(t.reviewer_name) + '</p>' : '') +
            (t.estimated_hours ? '<p class="hint">Estimated ' + t.estimated_hours + 'h, actual ' + t.actual_hours + 'h</p>' : '') +
            '<div><b>Checklist</b>' + (t.checklist.length ? t.checklist.map(c =>
              '<div><label class="check"><input type="checkbox" data-toggle-check="' + c.id + '"' + (c.is_done ? ' checked' : '') + '> ' +
              (c.is_done ? '<span style="text-decoration:line-through">' + esc(c.label) + '</span>' : esc(c.label)) + '</label></div>').join('') : '<span class="hint">None</span>') +
            '<div class="grid c2" style="margin-top:6px"><input type="text" placeholder="Add checklist item…" data-new-check="' + t.id + '">' +
            '<button class="btn btn-small" data-add-check="' + t.id + '">Add</button></div></div>' +
            '<button class="btn btn-small btn-danger" data-del-task="' + t.id + '" style="margin-top:8px">Delete</button></details>';
        }).join('') : '<div class="empty">No tasks yet</div>';

        body.querySelectorAll('[data-toggle-done]').forEach(cb => cb.addEventListener('click', async (ev) => {
          ev.stopPropagation();
          try { await api('/api/tasks/' + cb.dataset.toggleDone, { method: 'PUT', body: { status: cb.checked ? 'done' : 'open' } }); drawTasks(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-toggle-check]').forEach(cb => cb.addEventListener('click', async (ev) => {
          ev.stopPropagation();
          try { await api('/api/checklist-items/' + cb.dataset.toggleCheck, { method: 'PUT', body: { is_done: cb.checked } }); drawTasks(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-add-check]').forEach(btn => btn.addEventListener('click', async () => {
          const input = body.querySelector('[data-new-check="' + btn.dataset.addCheck + '"]');
          if (!input.value) return;
          try { await api('/api/tasks/' + btn.dataset.addCheck + '/checklist', { method: 'POST', body: { label: input.value } }); drawTasks(); }
          catch (e) { toast(e.message, true); }
        }));
        body.querySelectorAll('[data-del-task]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this task?')) return;
          try { await api('/api/tasks/' + btn.dataset.delTask, { method: 'DELETE' }); drawTasks(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }
    drawTasks();
  }

  // ======================================================================
  // CALENDAR — internal firm calendar, distinct from public appointment booking.
  // ======================================================================

  function renderCalendarPage() {
    const view = document.getElementById('view');
    view.innerHTML =
      '<div class="page-head"><div><h2>Calendar</h2><p>The firm\'s internal calendar.</p></div></div>' +
      '<details class="card"><summary><b>New event</b></summary><div class="grid c3" style="margin-top:10px" id="cal-new-form">' +
      field('Title', 'title', {}, 'text') + field('Start', 'start_at', {}, 'datetime-local') + field('End', 'end_at', {}, 'datetime-local') +
      '</div><button class="btn btn-small btn-primary" id="cal-add" style="margin-top:8px">Add event</button></details>' +
      '<div class="card" id="cal-body"><div class="empty">Loading…</div></div>';

    if (!state.online || !state.token || state.offlineMode) {
      document.getElementById('cal-body').innerHTML = '<div class="empty"><b>Offline</b>Calendar needs a connection.</div>';
      return;
    }

    document.getElementById('cal-add').addEventListener('click', async () => {
      const b = {}; document.querySelectorAll('#cal-new-form [data-key]').forEach(el => { b[el.dataset.key] = el.value || null; });
      if (!b.title || !b.start_at) { toast('Title and start are required', true); return; }
      try { await api('/api/calendar/events', { method: 'POST', body: b }); toast('Event added'); drawEvents(); }
      catch (e) { toast(e.message, true); }
    });

    function drawEvents() {
      const body = document.getElementById('cal-body');
      api('/api/calendar/events').then(events => {
        body.innerHTML = events.length ? events.map(e => '<div class="row-line"><b>' + esc(e.title) + '</b> ' +
          '<span class="hint">' + esc(e.start_at) + (e.end_at ? ' – ' + esc(e.end_at) : '') + '</span>' +
          (e.owner_name ? ' <span class="tag">' + esc(e.owner_name) + '</span>' : ' <span class="tag">Firm-wide</span>') +
          ' <button class="btn btn-small btn-danger" data-del-event="' + e.id + '">Delete</button></div>').join('') : '<div class="empty">No events yet</div>';
        body.querySelectorAll('[data-del-event]').forEach(btn => btn.addEventListener('click', async () => {
          if (!confirm('Delete this event?')) return;
          try { await api('/api/calendar/events/' + btn.dataset.delEvent, { method: 'DELETE' }); drawEvents(); }
          catch (e) { toast(e.message, true); }
        }));
      }).catch(e => { body.innerHTML = '<div class="empty">' + esc(e.message) + '</div>'; });
    }
    drawEvents();
  }

  function route() {
    const r = location.hash.replace('#/', '') || 'desk';
    state.route = r;
    document.querySelectorAll('#rail a').forEach(a => a.classList.toggle('on', a.dataset.go === r));
    // Never leave the active page's nav link hidden inside a collapsed
    // group — expand whichever section it's actually in.
    const activeLink = document.querySelector('#rail a.on');
    const activeGroupItems = activeLink && activeLink.closest('.rail-group-items.collapsed');
    if (activeGroupItems) {
      activeGroupItems.classList.remove('collapsed');
      const header = document.querySelector('.rail-group[data-rail-group="' + activeGroupItems.dataset.railGroupItems + '"]');
      if (header) header.classList.remove('collapsed');
    }
    if (MODULES[r]) renderModulePage(r);
    else if (r === 'clients') renderClients();
    else if (r === 'rates') renderRates();
    else if (r === 'reports') renderReports();
    else if (r === 'documents') renderDocuments();
    else if (r === 'billing') renderBilling();
    else if (r === 'assistant') renderCaAssistant();
    else if (r === 'leads') renderLeads();
    else if (r === 'admin') renderAdmin();
    else if (r === 'platform') renderPlatform();
    else if (r === 'site-settings') renderSiteSettings();
    else if (r === 'blog-admin') renderBlogAdmin();
    else if (r === 'career-admin') renderCareerAdmin();
    else if (r === 'chatbot-admin') renderChatbotAdmin();
    else if (r === 'org-admin') renderOrgAdmin();
    else if (r === 'tasks') renderTasks();
    else if (r === 'calendar') renderCalendarPage();
    else if (r === 'tp') renderTP();
    else if (r === 'pm') renderPM();
    else if (r === 'fa') renderFA();
    else if (r === 'corp') renderCA();
    else if (r === 'ba') renderBA();
    else if (r === 'ipo') renderIPO();
    else if (r === 'taxadv') renderTaxAdvisory();
    else if (r === 'audx') renderAuditExecution();
    else if (r === 'acct') renderAcctAutomation();
    else renderDesk();
  }

  // Compares the FY the user is currently working in against the server's
  // authoritative current_fy (from /api/bootstrap) and, on a mismatch, offers
  // a one-click switch via the shared #banner element — never a silent
  // override, since a user reviewing a prior year's data mid-session
  // shouldn't get yanked out of it without asking.
  function checkFyCurrency() {
    const banner = document.getElementById('banner');
    if (!banner) return;
    const currentFy = state.boot && state.boot.current_fy;
    if (!currentFy || currentFy === state.fy) { banner.hidden = true; banner.innerHTML = ''; return; }
    banner.hidden = false;
    banner.innerHTML = '<b>Working in FY ' + esc(state.fy) + '.</b> FY ' + esc(currentFy) +
      ' is the current fiscal year — rates for it may still need review. ' +
      '<button class="btn btn-small" id="fy-switch-btn">Switch to FY ' + esc(currentFy) + '</button> ' +
      '<button class="btn btn-small btn-ghost" id="fy-dismiss-btn">Dismiss</button>';
    banner.querySelector('#fy-switch-btn').addEventListener('click', () => {
      state.fy = currentFy;
      localStorage.setItem('ca.fy', currentFy);
      banner.hidden = true; banner.innerHTML = '';
      route();
      toast('Switched to FY ' + currentFy);
    });
    banner.querySelector('#fy-dismiss-btn').addEventListener('click', () => { banner.hidden = true; });
  }

  // Subscription lifecycle banner — a firm on a term plan (subscription_
  // expires_at set; a perpetual firm has it null and never sees any of
  // this) gets a persistent warning once it's past that date but still
  // inside the grace window. Returns true if the firm is fully suspended
  // (past grace) so start() knows to show the paused-service screen
  // instead of the normal app. Takes priority over the FY banner — a
  // lapsed subscription matters more than which fiscal year you're in.
  // Returns 'suspended', 'grace', or null (nothing to show / no term
  // subscription set). Grace sets the #banner itself — callers must skip
  // checkFyCurrency() in that case, or it'll immediately clear the banner
  // this just wrote.
  function checkSubscriptionStatus() {
    const firm = state.boot && state.boot.firm;
    const status = firm && firm.subscription_status;
    if (!status || status === 'active') return null;
    if (status === 'suspended') return 'suspended';
    const banner = document.getElementById('banner');
    if (banner) {
      banner.hidden = false;
      banner.innerHTML = '<b>Your CA-Automator subscription expired on ' + esc(firm.subscription_expires_at) + '.</b> ' +
        'Renew by ' + esc(firm.subscription_grace_ends_at) + ' to avoid an interruption — after that, staff won\'t ' +
        'be able to sign in until it\'s renewed. Your client data stays safe either way.';
    }
    return 'grace';
  }

  // Shown instead of the whole app once a firm's grace period has run out.
  // Every other API call would 402 anyway (see server's _user()), so this
  // deliberately makes no further requests — just explains why, reassures
  // that nothing was deleted, and offers to sign out.
  function renderServicePaused() {
    document.getElementById('rail-toggle').hidden = true;
    document.querySelector('.shell').innerHTML =
      '<div style="max-width:560px;margin:12vh auto;text-align:center;padding:0 20px">' +
      '<h2>Service paused</h2>' +
      '<p>Your firm\'s CA-Automator subscription expired' +
      (state.boot.firm.subscription_expires_at ? ' on ' + esc(state.boot.firm.subscription_expires_at) : '') +
      ' and the grace period has ended, so staff can\'t sign in to use the app right now.</p>' +
      '<p><b>Nothing has been deleted.</b> Every client, workfile and document is still safely stored, exactly as ' +
      'it was — access resumes immediately once the subscription is renewed.</p>' +
      '<p>Contact CA-Automator to restore access.</p>' +
      '<button class="btn btn-primary" id="paused-signout" style="margin-top:12px">Sign out</button></div>';
    const btn = document.getElementById('paused-signout');
    if (btn) btn.addEventListener('click', () => signOut());
  }

  // ──────────────────────────────────────────────────────────────── boot
  async function start(offline) {
    state.offlineMode = !!offline;
    document.getElementById('gate').hidden = true;
    document.getElementById('app').hidden = false;

    state.boot = (await idbGet('kv', 'boot')) || null;
    state.rates = (await idbGet('kv', 'rates')) || [];
    await loadFiles();

    if (!offline && state.token && navigator.onLine) {
      try {
        const boot = await api('/api/bootstrap');
        state.boot = boot; state.rates = boot.rates || [];
        await idbPut('kv', boot, 'boot');
        await idbPut('kv', state.rates, 'rates');
      } catch (e) { toast('Working from the cached copy — ' + e.message, true); }
    }

    if (state.boot && state.boot.firm) document.getElementById('firm-name').textContent = state.boot.firm.name;

    // A suspended firm (lapsed subscription, past its grace period) gets a
    // dedicated paused screen instead of the app — every other API call
    // would 402 anyway (server/app.py's _user()), so skip building the
    // rail/routing/polling entirely rather than let each of them fail.
    const subState = checkSubscriptionStatus();
    if (subState === 'suspended') { renderServicePaused(); return; }

    const rateChip = document.getElementById('chip-rates');
    const n = (state.boot && state.boot.unverified_rates) || state.rates.filter(r => r.verification_status === 'UNVERIFIED').length;
    if (n) { rateChip.hidden = false; rateChip.textContent = n + ' rates unverified'; }
    if (subState !== 'grace') checkFyCurrency();

    buildRail();
    initRailToggle();
    route();
    renderDial();

    setInterval(() => beat(false), AUTOSAVE_MS);
    setInterval(renderDial, 30000);
    if (!offline) {
      refreshNotifications();
      setInterval(refreshNotifications, 120000);
    }
  }

  function signOut(silent) {
    state.token = null; state.user = null;
    localStorage.removeItem('ca.token'); localStorage.removeItem('ca.user');
    document.getElementById('app').hidden = true;
    document.getElementById('gate').hidden = false;
    if (!silent) toast('Signed out — your work stays on this device');
  }

  // events --------------------------------------------------------------------
  window.addEventListener('hashchange', route);
  window.addEventListener('online', () => { state.online = true; renderDial(); beat(false); });
  window.addEventListener('offline', () => { state.online = false; renderDial(); });
  document.addEventListener('visibilitychange', () => { if (document.hidden) saveAllLocal(); });
  window.addEventListener('beforeunload', () => {
    Object.values(state.files).forEach(f => { try { idbPut('files', JSON.parse(JSON.stringify(f))); } catch (e) {} });
  });

  // Reached via #/invite/<token> or #/reset/<token>, present even before any
  // session exists — landing.js's tab-switching only knows about sign-in and
  // sign-up, so this bypasses it entirely and drives the #pane-setpw markup
  // (added specifically for this flow) directly.
  function checkAccountSetupHash() {
    const m = location.hash.match(/^#\/(invite|reset)\/(.+)$/);
    if (!m) return false;
    const kind = m[1], token = decodeURIComponent(m[2]);
    const overlay = document.getElementById('auth-overlay');
    const pane = document.getElementById('pane-setpw');
    if (!overlay || !pane) return false;
    const tabsEl = document.querySelector('.auth-tabs');
    const signinPane = document.getElementById('pane-signin');
    const signupPane = document.getElementById('pane-signup');
    if (tabsEl) tabsEl.hidden = true;
    if (signinPane) signinPane.hidden = true;
    if (signupPane) signupPane.hidden = true;
    pane.hidden = false;
    overlay.hidden = false;
    document.body.style.overflow = 'hidden';
    document.getElementById('setpw-title').textContent = kind === 'invite' ? 'Welcome — set your password' : 'Reset your password';
    document.getElementById('setpw-sub').textContent = kind === 'invite'
      ? 'Your firm has set up your account. Choose a password to finish signing in.'
      : 'Choose a new password for your account.';
    const go = document.getElementById('setpw-go');
    const msg = document.getElementById('setpw-msg');
    go.addEventListener('click', async () => {
      const p1 = document.getElementById('setpw-pass').value;
      const p2 = document.getElementById('setpw-pass2').value;
      msg.textContent = '';
      if (p1.length < 8) { msg.textContent = 'Use at least 8 characters.'; return; }
      if (p1 !== p2) { msg.textContent = "Those didn't match."; return; }
      go.disabled = true; go.textContent = 'Setting…';
      try {
        const path = kind === 'invite' ? '/api/invite/accept' : '/api/auth/reset';
        const res = await api(path, { method: 'POST', body: { token, password: p1, device_id: state.deviceId } });
        state.token = res.token; state.user = res.user;
        localStorage.setItem('ca.token', res.token);
        localStorage.setItem('ca.user', JSON.stringify(res.user));
        location.hash = '';
        overlay.hidden = true;
        document.body.style.overflow = '';
        start(false);
      } catch (e) {
        msg.textContent = e.message;
        go.disabled = false; go.textContent = 'Set password and sign in';
      }
    });
    return true;
  }

  document.getElementById('g-go').addEventListener('click', async () => {
    const msg = document.getElementById('g-msg');
    msg.textContent = '';
    try {
      const res = await api('/api/auth/login', { method: 'POST', body: {
        email: document.getElementById('g-email').value,
        password: document.getElementById('g-pass').value,
        device_id: state.deviceId } });
      state.token = res.token; state.user = res.user;
      localStorage.setItem('ca.token', res.token);
      localStorage.setItem('ca.user', JSON.stringify(res.user));
      start(false);
    } catch (e) { msg.textContent = e.message; }
  });
  document.getElementById('g-pass').addEventListener('keydown', e => {
    if (e.key === 'Enter') document.getElementById('g-go').click();
  });
  const forgotBtn = document.getElementById('g-forgot');
  if (forgotBtn) forgotBtn.addEventListener('click', async () => {
    const email = document.getElementById('g-email').value.trim();
    const msg = document.getElementById('g-msg');
    if (!email) { msg.textContent = 'Enter your email above first, then click "Forgot password?" again.'; return; }
    forgotBtn.disabled = true;
    try {
      const res = await api('/api/auth/forgot', { method: 'POST', body: { email } });
      msg.textContent = res.message;
    } catch (e) { msg.textContent = e.message; }
    finally { forgotBtn.disabled = false; }
  });
  // ── offline PIN — offline mode has no server to check credentials against,
  // so this is a local-device deterrent (SHA-256 + random salt in localStorage),
  // not account security. Signing in online never touches it.
  const PIN_SALT_KEY = 'ca.offlinePin.salt', PIN_HASH_KEY = 'ca.offlinePin.hash';

  async function sha256Hex(text) {
    const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
    return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
  }
  function randomHex(bytes) {
    return Array.from(crypto.getRandomValues(new Uint8Array(bytes))).map(b => b.toString(16).padStart(2, '0')).join('');
  }
  function hasOfflinePin() { return !!localStorage.getItem(PIN_HASH_KEY); }
  async function setOfflinePin(pin) {
    const salt = randomHex(16);
    localStorage.setItem(PIN_SALT_KEY, salt);
    localStorage.setItem(PIN_HASH_KEY, await sha256Hex(salt + ':' + pin));
  }
  async function checkOfflinePin(pin) {
    const salt = localStorage.getItem(PIN_SALT_KEY) || '';
    return (await sha256Hex(salt + ':' + pin)) === localStorage.getItem(PIN_HASH_KEY);
  }

  function openPinModal(mode) { // 'setup' | 'verify' | 'change'
    return new Promise(resolve => {
      const overlay = document.getElementById('pin-overlay');
      const title = document.getElementById('pin-title');
      const sub = document.getElementById('pin-sub');
      const input = document.getElementById('pin-input');
      const confirmWrap = document.getElementById('pin-confirm-wrap');
      const confirmInput = document.getElementById('pin-confirm');
      const msg = document.getElementById('pin-msg');
      const submit = document.getElementById('pin-submit');
      const cancel = document.getElementById('pin-cancel');
      const needsConfirm = mode !== 'verify';

      input.value = ''; confirmInput.value = ''; msg.textContent = '';
      confirmWrap.hidden = !needsConfirm;
      title.textContent = mode === 'verify' ? 'Enter offline PIN'
        : mode === 'change' ? 'Set a new offline PIN' : 'Protect offline access';
      sub.textContent = mode === 'verify'
        ? 'This device has cached client data. Enter the PIN to continue offline.'
        : 'This PIN protects offline access on this device only — anyone with it can open cached client data without signing in.';

      overlay.hidden = false;
      setTimeout(() => input.focus(), 50);

      function cleanup(result) {
        overlay.hidden = true;
        submit.removeEventListener('click', onSubmit);
        cancel.removeEventListener('click', onCancel);
        overlay.removeEventListener('click', onBackdrop);
        document.removeEventListener('keydown', onKey);
        resolve(result);
      }
      async function onSubmit() {
        const pin = input.value.trim();
        if (!/^\d{4,6}$/.test(pin)) { msg.textContent = 'PIN must be 4–6 digits.'; return; }
        if (needsConfirm) {
          if (pin !== confirmInput.value.trim()) { msg.textContent = "PINs don't match."; return; }
          await setOfflinePin(pin);
          cleanup(true);
          return;
        }
        if (!(await checkOfflinePin(pin))) { msg.textContent = 'Wrong PIN.'; input.value = ''; input.focus(); return; }
        cleanup(true);
      }
      function onCancel() { cleanup(false); }
      function onBackdrop(e) { if (e.target === overlay) cleanup(false); }
      function onKey(e) {
        if (e.key === 'Escape') cleanup(false);
        else if (e.key === 'Enter') onSubmit();
      }
      submit.addEventListener('click', onSubmit);
      cancel.addEventListener('click', onCancel);
      overlay.addEventListener('click', onBackdrop);
      document.addEventListener('keydown', onKey);
    });
  }

  document.getElementById('g-offline').addEventListener('click', async () => {
    const ok = await openPinModal(hasOfflinePin() ? 'verify' : 'setup');
    if (ok) start(true);
  });
  document.getElementById('btn-offline-pin').addEventListener('click', async () => {
    if (await openPinModal('change')) toast('Offline PIN updated');
  });
  document.getElementById('btn-sync').addEventListener('click', () => beat(true));
  document.getElementById('btn-signout').addEventListener('click', () => signOut());

  document.getElementById('btn-notifications').addEventListener('click', () => {
    const panel = document.getElementById('notif-panel');
    notifState.panelOpen = panel.hidden;
    panel.hidden = !notifState.panelOpen;
    if (notifState.panelOpen) { renderNotifPanel(); refreshNotifications(); }
  });
  document.addEventListener('click', e => {
    const wrap = document.querySelector('.notif-wrap');
    const panel = document.getElementById('notif-panel');
    if (wrap && panel && !panel.hidden && !wrap.contains(e.target)) { panel.hidden = true; notifState.panelOpen = false; }
  });

  // ───────────────────────────── AD → BS hint on every date field, app-wide
  // Every date input anywhere in the app (module forms, table cells, the
  // Client Directory, Billing...) picks up a small "BS ≈ ..." readout next
  // to it — computed client-side via CAEngines.bsApprox, so it works
  // offline and never needs per-module wiring. A MutationObserver on #view
  // catches every page's render (they all just replace innerHTML) so this
  // needed no changes to any individual module.
  function updateBsHint(el) {
    if (!el || el.tagName !== 'INPUT' || el.type !== 'date') return;
    let hint = el.nextElementSibling;
    if (!hint || !hint.classList || !hint.classList.contains('bs-inline-hint')) {
      hint = document.createElement('span');
      hint.className = 'bs-inline-hint hint';
      el.insertAdjacentElement('afterend', hint);
    }
    let label = '';
    if (el.value) {
      try { label = 'BS ' + CAEngines.bsApprox(el.value).label; } catch (e) { label = ''; }
    }
    if (hint.textContent !== label) hint.textContent = label;
  }
  document.addEventListener('input', e => updateBsHint(e.target));
  document.addEventListener('change', e => updateBsHint(e.target));
  const bsHintRoot = document.getElementById('view');
  if (bsHintRoot) {
    new MutationObserver(() => {
      bsHintRoot.querySelectorAll('input[type="date"]').forEach(updateBsHint);
    }).observe(bsHintRoot, { childList: true, subtree: true });
  }

  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js').catch(() => {});
  }

  if (!checkAccountSetupHash() && state.token) start(false);
})();
