/* Admin dashboard interactions: editor tabs + live SEO preview. */

/* ── NTPPA icon helper (admin) ── draws from window.NTPPA_ICONS, the shared
   server-side registry injected by app/icons.php icons_js(). Same 24×24 / 1.75
   family as the public site, so JS-built admin UI stays on-system. */
function aic(name, cls) {
  var map = window.NTPPA_ICONS || {}, d = map[name];
  if (!d) return '';
  var fill = false, inner;
  if (d && typeof d === 'object') { fill = (d.m === 'fill'); inner = d.p || ''; } else { inner = d || ''; }
  if (inner && inner.charAt(0) !== '<') inner = '<path d="' + inner + '"/>';
  var paint = fill ? 'fill="currentColor"'
    : 'fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"';
  return '<svg class="nic ' + (cls || 'ic') + (fill ? ' nic-fill' : '') + '" viewBox="0 0 24 24" ' + paint + ' aria-hidden="true">' + inner + '</svg>';
}

document.querySelectorAll('.ed-tab').forEach(function (t) {
  t.addEventListener('click', function () {
    var tab = t.dataset.tab;
    document.querySelectorAll('.ed-tab').forEach(function (x) { x.classList.toggle('on', x === t); });
    document.querySelectorAll('.ed-panel').forEach(function (x) { x.classList.toggle('on', x.dataset.panel === tab); });
  });
});

function seoUpdate() {
  var T = (document.getElementById('seoTitle') || {}).value || '';
  var D = (document.getElementById('seoDesc') || {}).value || '';
  var K = ((document.getElementById('seoKw') || {}).value || '').toLowerCase();
  var pt = document.getElementById('pvTitle'), pd = document.getElementById('pvDesc');
  if (pt) pt.textContent = T || 'Untitled page';
  if (pd) pd.textContent = D || 'No meta description set yet…';
  var ct = document.getElementById('ctTitle');
  if (ct) { ct.textContent = T.length + '/60'; ct.className = 'counter' + (T.length > 60 ? ' bad' : T.length > 55 ? ' warn' : ''); }
  var cd = document.getElementById('ctDesc');
  if (cd) { cd.textContent = D.length + '/160'; cd.className = 'counter' + (D.length > 160 ? ' bad' : D.length > 150 ? ' warn' : ''); }
  var checks = [
    ['Title length ideal (30–60)', T.length >= 30 && T.length <= 60],
    ['Description length ideal (120–160)', D.length >= 120 && D.length <= 160],
    ['Focus keyword in title', !!K && T.toLowerCase().indexOf(K) >= 0],
    ['Focus keyword in description', !!K && D.toLowerCase().indexOf(K) >= 0],
    ['Has a meta description', D.length > 0]
  ];
  var pass = checks.filter(function (c) { return c[1]; }).length;
  var pct = Math.round(pass / checks.length * 100);
  var cc = document.getElementById('seoChecks');
  if (cc) cc.innerHTML = checks.map(function (c) {
    return '<div class="check ' + (c[1] ? 'pass' : 'fail') + '"><span class="ci">' +
      (c[1] ? aic('check', 'ic ic-sm') : aic('info', 'ic ic-sm')) +
      '</span><span>' + c[0] + '</span></div>';
  }).join('');
  var b = document.getElementById('seoPct');
  if (b) { b.textContent = pct + '%'; b.className = 'badge ' + (pct >= 80 ? 'b-ok' : pct >= 50 ? 'b-warn' : 'b-bad'); }
}
window.seoUpdate = seoUpdate;
if (document.getElementById('seoTitle')) seoUpdate();

/* Mobile sidebar (hamburger) */
(function () {
  var t = document.getElementById('sbToggle');
  var sb = document.querySelector('.sidebar');
  var ov = document.getElementById('sbOvl');
  if (!t || !sb || !ov) return;
  function close() { sb.classList.remove('open'); ov.classList.remove('on'); ov.hidden = true; t.setAttribute('aria-label', 'Open menu'); }
  t.addEventListener('click', function () {
    var open = sb.classList.toggle('open');
    ov.classList.toggle('on', open);
    ov.hidden = !open;
    t.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
  });
  ov.addEventListener('click', close);
  document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
})();

/* ═══════════════════════════════════════════════════════════════════
   VISUAL EDITOR — a Word-style rich-text editor for every [data-rich]
   textarea. Non-technical clients format text with buttons; the raw
   HTML is written back to the hidden textarea on every change.
   • <style>/<script> in migrated pages are hidden from the editing
     surface but PRESERVED on save, so the live page never breaks.
   • Paste from Word/Google Docs is auto-cleaned.
   • "Import Word" converts an uploaded .docx to clean content.
   ═══════════════════════════════════════════════════════════════════ */
(function () {
  var ICON = {
    bold: 'B', italic: 'I', under: 'U',
    ul: aic('list-ul', 'ic ic-sm'),
    ol: aic('list-ol', 'ic ic-sm'),
    link: aic('link', 'ic ic-sm'),
    unlink: aic('unlink', 'ic ic-sm'),
    img: aic('media', 'ic ic-sm'),
    clear: aic('format-clear', 'ic ic-sm'),
    plain: aic('paste', 'ic ic-sm'),
    undo: aic('undo', 'ic ic-sm'),
    redo: aic('redo', 'ic ic-sm'),
    word: aic('doc', 'ic ic-sm')
  };

  function splitPreserved(html) {
    var pre = '';
    var visible = String(html || '')
      .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, function (m) { pre += m + '\n'; return ''; })
      .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, function (m) { pre += m + '\n'; return ''; });
    return { pre: pre.trim(), visible: visible.trim() };
  }

  /* ── Paste / formatting sanitiser ───────────────────────────────────────────
     Content pasted from Word / Google Docs / the old site drags along its own
     fonts, sizes and colours (inline style=, <span>, <font>, mso-* classes,
     <o:p> …), which clash with the site typography. cleanPastedHtml() keeps ONLY
     semantic structure and forces the site's own fonts: it whitelists a small set
     of tags, DROPS every attribute (href kept on links, with safe rel/target),
     UNWRAPS span/font/div and any other wrapper (keeping the text), removes Word
     cruft + empty tags, and promotes "heading-looking" lines to <h3>. */
  var RTE_ALLOWED = { A: 1, P: 1, BR: 1, H2: 1, H3: 1, STRONG: 1, EM: 1, U: 1, UL: 1, OL: 1, LI: 1, BLOCKQUOTE: 1 };
  var RTE_MAP = { B: 'STRONG', I: 'EM', H1: 'H2', H4: 'H3', H5: 'H3', H6: 'H3' };
  var RTE_BLOCK = { P: 1, H2: 1, H3: 1, UL: 1, OL: 1, LI: 1, BLOCKQUOTE: 1, DIV: 1 };

  function escHtml(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
  function rteRename(el, tag) {
    var n = document.createElement(tag);
    while (el.firstChild) n.appendChild(el.firstChild);
    if (el.parentNode) el.parentNode.replaceChild(n, el);
    return n;
  }
  function rteUnwrap(el) {
    var p = el.parentNode; if (!p) return;
    while (el.firstChild) p.insertBefore(el.firstChild, el);
    p.removeChild(el);
  }
  function rteStripAttrs(el) {
    if (!el.attributes) return;
    for (var i = el.attributes.length - 1; i >= 0; i--) el.removeAttribute(el.attributes[i].name);
  }
  function rteHasBlockChild(el) {
    for (var c = el.firstChild; c; c = c.nextSibling)
      if (c.nodeType === 1 && RTE_BLOCK[c.tagName]) return true;
    return false;
  }
  // a <p> whose whole content is one short bold run (no sentence punctuation) reads as a heading
  function rteLooksHeading(p) {
    var txt = (p.textContent || '').trim();
    if (!txt || txt.length > 120 || /[.!?,;:]$/.test(txt)) return false;
    var kids = [].slice.call(p.childNodes).filter(function (n) { return !(n.nodeType === 3 && !n.textContent.trim()); });
    if (kids.length !== 1 || kids[0].nodeType !== 1) return false;
    return kids[0].tagName === 'STRONG' && (kids[0].textContent || '').trim() === txt;
  }
  function rteClean(node) {
    var child = node.firstChild;
    while (child) {
      var next = child.nextSibling;
      if (child.nodeType === 8) { node.removeChild(child); }             // comment (incl. Word conditional)
      else if (child.nodeType === 1) {
        var tag = child.tagName;
        // capture Word/GDocs heading hints BEFORE the attributes are dropped
        var cls = (child.getAttribute && child.getAttribute('class')) || '';
        var stl = (child.getAttribute && child.getAttribute('style')) || '';
        var headingHint = /mso[a-z]*heading|(^|\s)heading\d?/i.test(cls) || /mso-outline-level:\s*[1-3]\b/i.test(stl);
        if (RTE_MAP[tag]) { child = rteRename(child, RTE_MAP[tag]); tag = child.tagName; }
        if (tag === 'DIV') { rteClean(child); rteUnwrap(child); child = next; continue; }
        if (tag === 'A') {
          var href = child.getAttribute('href') || '';
          rteStripAttrs(child);
          if (href && /^(https?:|mailto:|tel:|\/|#)/i.test(href) && !/^javascript:/i.test(href)) {
            child.setAttribute('href', href);
            child.setAttribute('rel', 'noopener noreferrer nofollow');
            if (/^https?:/i.test(href)) child.setAttribute('target', '_blank');
            rteClean(child);
          } else { rteClean(child); rteUnwrap(child); }
          child = next; continue;
        }
        if (RTE_ALLOWED[tag]) {
          // an inline wrapper (strong/em/u) around block content (a GDocs quirk) → unwrap it
          if ((tag === 'STRONG' || tag === 'EM' || tag === 'U') && rteHasBlockChild(child)) {
            rteClean(child); rteUnwrap(child); child = next; continue;
          }
          rteStripAttrs(child);
          rteClean(child);
          if (tag === 'P' && (headingHint || rteLooksHeading(child))) rteRename(child, 'h3');
        } else {
          rteClean(child);              // clean descendants, then drop the wrapper (keep its text)
          rteUnwrap(child);
        }
      }
      child = next;
    }
  }
  function cleanPastedHtml(html) {
    html = String(html || '')
      .replace(/<!--[\s\S]*?-->/g, '')
      .replace(/<\?xml[\s\S]*?\?>/gi, '')
      .replace(/<\/?(o:[a-z]+|w:[a-z]+|xml|meta|link|style|script|title|head|html|body|font)[^>]*>/gi, '');
    var root = null;
    try {
      var doc = new DOMParser().parseFromString('<div id="__rte">' + html + '</div>', 'text/html');
      root = doc.getElementById('__rte');
    } catch (e) { root = null; }
    if (!root) return stripInlineFonts(html);        // parser unavailable → regex fallback
    rteClean(root);
    return root.innerHTML
      .replace(/&nbsp;/g, ' ')
      .replace(/ /g, ' ')
      .replace(/[ \t]{2,}/g, ' ')
      .replace(/<(p|h2|h3|blockquote|li)>\s*<\/\1>/gi, '')     // empty blocks
      .replace(/(?:\s*<br\s*\/?>\s*)+<\/(p|h2|h3|li|blockquote)>/gi, '</$1>')
      .trim();
  }
  // Belt-and-suspenders regex pass (used on SAVE + as a DOMParser fallback):
  // unwrap span/font, drop only FONT/COLOUR style declarations (keep e.g. an
  // image's max-width), normalise b/i → strong/em. Structure + text untouched.
  function stripInlineFonts(html) {
    return String(html || '')
      .replace(/<\/?(span|font)\b[^>]*>/gi, '')
      .replace(/style=("|')([^"']*)\1/gi, function (m, q, decls) {
        var kept = decls.split(';').filter(function (d) {
          var prop = (d.split(':')[0] || '').trim().toLowerCase();
          if (!prop) return false;
          return !/^(font|font-[a-z-]+|color|background|background-[a-z-]+|line-height|letter-spacing|word-spacing|text-transform|text-indent|white-space|mso-[a-z-]+)$/.test(prop);
        });
        return kept.length ? 'style=' + q + kept.join('; ') + q : '';
      })
      .replace(/<(\/?)b\b[^>]*>/gi, '<$1strong>')
      .replace(/<(\/?)i\b[^>]*>/gi, '<$1em>');
  }
  // Back-compat alias — the Word-import path routes through the same sanitiser.
  function cleanWord(html) { return cleanPastedHtml(html); }

  function btn(inner, title, onClick, cls) {
    var b = document.createElement('button');
    b.type = 'button'; b.className = 'rte-b' + (cls ? ' ' + cls : ''); b.title = title; b.innerHTML = inner;
    b.addEventListener('mousedown', function (e) { e.preventDefault(); }); // keep the text selection
    b.addEventListener('click', onClick);
    return b;
  }

  function loadMammoth() {
    return new Promise(function (resolve, reject) {
      if (window.mammoth) return resolve(window.mammoth);
      var base = window.CMS_ASSETS || 'assets/';
      var s = document.createElement('script');
      s.src = base + 'vendor/mammoth.browser.min.js';
      s.onload = function () { window.mammoth ? resolve(window.mammoth) : reject(); };
      s.onerror = reject;
      document.head.appendChild(s);
    });
  }

  function enhance(ta) {
    if (ta.dataset.rteReady) return; ta.dataset.rteReady = '1';
    var split = splitPreserved(ta.value);
    var state = { pre: split.pre, mode: 'visual' };
    var dirty = false; // only write back blocks the user actually changed (keeps untouched HTML byte-for-byte)

    var wrap = document.createElement('div'); wrap.className = 'rte';
    var bar = document.createElement('div'); bar.className = 'rte-bar';
    var surface = document.createElement('div');
    surface.className = 'rte-surface'; surface.contentEditable = 'true'; surface.spellcheck = true;
    surface.innerHTML = split.visible || '';
    var code = document.createElement('textarea');
    code.className = 'rte-code inp'; code.style.display = 'none'; code.value = ta.value; code.rows = 12;

    // Custom HTML sections are the trusted raw-HTML escape hatch (migrated pages
    // rely on intentional inline styles) → store them verbatim. Every other rich
    // field gets the belt-and-suspenders font strip so stored HTML never carries
    // pasted inline fonts/colours.
    var stripOnSync = ta.dataset.sectype !== 'html';
    function sync() {
      if (state.mode === 'code') { ta.value = code.value; }
      else { ta.value = (state.pre ? state.pre + '\n' : '') + (stripOnSync ? stripInlineFonts(surface.innerHTML) : surface.innerHTML); }
    }
    function exec(cmd, val) { surface.focus(); document.execCommand(cmd, false, val || null); dirty = true; sync(); }
    function pickImage(cb) {
      if (window.openMediaPicker) window.openMediaPicker(function (m) { cb(m.url || m.path); });
      else { var u = prompt('Image address or path (e.g. /uploads/photo.jpg or https://…):', '/uploads/'); if (u) cb(u); }
    }

    // ── format dropdown ──
    var fmt = document.createElement('select'); fmt.className = 'rte-sel'; fmt.title = 'Text style';
    [['p', 'Normal text'], ['h2', 'Heading'], ['h3', 'Sub-heading'], ['blockquote', 'Quote']].forEach(function (o) {
      var op = document.createElement('option'); op.value = o[0]; op.textContent = o[1]; fmt.appendChild(op);
    });
    fmt.addEventListener('mousedown', function () { surface.focus(); });
    fmt.addEventListener('change', function () { exec('formatBlock', '<' + fmt.value + '>'); });

    bar.appendChild(fmt);
    bar.appendChild(sep());
    bar.appendChild(btn(ICON.bold, 'Bold (Ctrl+B)', function () { exec('bold'); }, 'b-b'));
    bar.appendChild(btn(ICON.italic, 'Italic (Ctrl+I)', function () { exec('italic'); }, 'b-i'));
    bar.appendChild(btn(ICON.under, 'Underline (Ctrl+U)', function () { exec('underline'); }, 'b-u'));
    bar.appendChild(sep());
    bar.appendChild(btn(ICON.ul, 'Bulleted list', function () { exec('insertUnorderedList'); }));
    bar.appendChild(btn(ICON.ol, 'Numbered list', function () { exec('insertOrderedList'); }));
    bar.appendChild(sep());
    bar.appendChild(btn(ICON.link, 'Add link', function () {
      var url = prompt('Link address (https://…):', 'https://'); if (url) exec('createLink', url);
    }));
    bar.appendChild(btn(ICON.unlink, 'Remove link', function () { exec('unlink'); }));
    bar.appendChild(btn(ICON.img, 'Insert image', function () {
      pickImage(function (src) { surface.focus(); document.execCommand('insertHTML', false, '<img src="' + String(src).replace(/"/g, '&quot;') + '" alt="" style="max-width:100%;height:auto">'); dirty = true; sync(); });
    }));
    bar.appendChild(sep());
    // Clear formatting → sanitise the WHOLE block: strip inline fonts/colours,
    // unwrap span/font, keep only semantic structure so it adopts the site fonts.
    bar.appendChild(btn(ICON.clear, 'Clear formatting — adopt the site fonts', function () {
      surface.focus();
      surface.innerHTML = cleanPastedHtml(surface.innerHTML);
      dirty = true; sync();
    }));
    // Paste as plain text → insert clipboard text with zero formatting, as paragraphs.
    bar.appendChild(btn(ICON.plain, 'Paste as plain text', function () {
      surface.focus();
      function insertPlain(text) {
        text = String(text || '').replace(/\r\n?/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
        if (!text) return;
        var html = text.split(/\n{2,}/).map(function (para) {
          return '<p>' + para.split('\n').map(escHtml).join('<br>') + '</p>';
        }).join('');
        document.execCommand('insertHTML', false, html); dirty = true; sync();
      }
      if (navigator.clipboard && navigator.clipboard.readText) {
        navigator.clipboard.readText().then(insertPlain).catch(function () {
          var t = prompt('Paste your text here — it will be inserted with no formatting:', ''); if (t != null) insertPlain(t);
        });
      } else {
        var t = prompt('Paste your text here — it will be inserted with no formatting:', ''); if (t != null) insertPlain(t);
      }
    }, 'rte-plain'));
    bar.appendChild(sep());
    bar.appendChild(btn(ICON.undo, 'Undo', function () { exec('undo'); }));
    bar.appendChild(btn(ICON.redo, 'Redo', function () { exec('redo'); }));

    var spacer = document.createElement('div'); spacer.className = 'rte-spacer'; bar.appendChild(spacer);

    // ── Import Word ──
    var wbtn = btn(ICON.word + '<span>Word</span>', 'Import a Word (.docx) file', function () {
      var fi = document.createElement('input'); fi.type = 'file'; fi.accept = '.docx';
      fi.addEventListener('change', function () {
        var file = fi.files && fi.files[0]; if (!file) return;
        wbtn.disabled = true; wbtn.classList.add('loading');
        file.arrayBuffer().then(function (buf) {
          return loadMammoth().then(function (m) { return m.convertToHtml({ arrayBuffer: buf }); });
        }).then(function (res) {
          surface.focus();
          document.execCommand('insertHTML', false, cleanWord(res.value || ''));
          dirty = true; sync(); wbtn.disabled = false; wbtn.classList.remove('loading');
        }).catch(function () {
          wbtn.disabled = false; wbtn.classList.remove('loading');
          alert('Could not read that Word file here. Tip: open the document, select all, and paste directly into the editor — Word formatting is cleaned automatically.');
        });
      });
      fi.click();
    }, 'rte-word');
    bar.appendChild(wbtn);

    // ── Code toggle ──
    var cbtn = btn('&lt;/&gt;', 'Switch between visual and HTML code', function () {
      if (state.mode === 'visual') {
        code.value = (state.pre ? state.pre + '\n' : '') + surface.innerHTML;
        surface.style.display = 'none'; code.style.display = ''; state.mode = 'code';
        cbtn.classList.add('on'); wrap.classList.add('code-on');
      } else {
        var sp = splitPreserved(code.value); state.pre = sp.pre; surface.innerHTML = sp.visible;
        surface.style.display = ''; code.style.display = 'none'; state.mode = 'visual';
        cbtn.classList.remove('on'); wrap.classList.remove('code-on'); if (dirty) sync(); refreshClean();
      }
    }, 'rte-code-toggle');
    bar.appendChild(cbtn);

    // ── Remove leftover code (only shown when preserved <style>/<script> exists) ──
    var clean = btn('Clean code', 'Remove leftover style/script code from this block', function () {
      if (!confirm('Remove the leftover style/script code kept from the old page? The text stays; some custom styling on THIS block may change. You can undo by not saving.')) return;
      state.pre = ''; dirty = true; sync(); refreshClean();
    }, 'rte-clean');
    bar.appendChild(clean);
    function refreshClean() { clean.style.display = state.pre ? '' : 'none'; }
    refreshClean();

    // paste cleaning — sanitise pasted HTML so it always adopts the site fonts;
    // plain-text paste becomes paragraphs (blank line = new paragraph).
    surface.addEventListener('paste', function (e) {
      var cd = e.clipboardData || window.clipboardData; if (!cd) return;
      var html = cd.getData('text/html'), text = cd.getData('text/plain');
      e.preventDefault();
      if (html) { document.execCommand('insertHTML', false, cleanPastedHtml(html)); }
      else if (text) {
        var frag = text.replace(/\r\n?/g, '\n').split(/\n{2,}/).map(function (para) {
          return '<p>' + para.split('\n').map(escHtml).join('<br>') + '</p>';
        }).join('');
        document.execCommand('insertHTML', false, frag);
      }
      dirty = true; sync();
    });
    surface.addEventListener('input', function () { dirty = true; sync(); });
    surface.addEventListener('blur', function () { if (dirty) sync(); });
    // click any image in the content to swap it out
    surface.addEventListener('click', function (e) {
      var img = e.target.closest('img'); if (!img) return;
      e.preventDefault();
      pickImage(function (src) { img.setAttribute('src', src); dirty = true; sync(); });
    });
    surface.querySelectorAll('img').forEach(function (im) { im.title = 'Click to replace this image'; im.style.cursor = 'pointer'; });
    code.addEventListener('input', function () { dirty = true; ta.value = code.value; });

    var form = ta.closest('form'); if (form) form.addEventListener('submit', function () { if (dirty) sync(); });

    wrap.appendChild(bar); wrap.appendChild(surface); wrap.appendChild(code);
    ta.style.display = 'none';
    ta.parentNode.insertBefore(wrap, ta.nextSibling);
  }
  function sep() { var s = document.createElement('span'); s.className = 'rte-sep'; return s; }

  function initRte() {
    try { document.execCommand('defaultParagraphSeparator', false, 'p'); } catch (e) {}
    try { document.execCommand('styleWithCSS', false, false); } catch (e) {}
    document.querySelectorAll('textarea[data-rich]').forEach(enhance);
  }
  // Exposed so dynamically-added rich fields (e.g. a newly-added job opening's
  // Details) get the Word-style toolbar too. enhance() no-ops on already-ready
  // textareas, so this is safe to call repeatedly.
  window.CMS_initRich = initRte;
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initRte); else initRte();
})();

/* ═══════════════════════════════════════════════════════════════════
   MEDIA PICKER — a friendly "choose an image" modal fed by window.CMS_MEDIA
   (the uploaded images). Used by every "Choose" button and by the editor's
   insert/replace-image actions. Falls back to a URL box if nothing uploaded.
   ═══════════════════════════════════════════════════════════════════ */
(function () {
  window.openMediaPicker = function (onPick) {
    var media = window.CMS_MEDIA || [];
    var ov = document.createElement('div'); ov.className = 'cms-modal';
    var box = document.createElement('div'); box.className = 'cms-modal-box';
    box.innerHTML = '<div class="cms-modal-hd"><b>Choose an image</b><button type="button" class="cms-modal-x" aria-label="Close">&times;</button></div>';

    /* search across the library (name or folder) — big libraries stay usable */
    var tools = document.createElement('div'); tools.className = 'cms-modal-tools';
    tools.innerHTML = aic('search', 'ic ic-sm cms-msearch-ic')
      + '<input type="search" class="inp cms-msearch" placeholder="Search ' + media.length + ' files by name or folder…" aria-label="Search media">'
      + '<span class="cms-modal-n"></span>';
    if (media.length) box.appendChild(tools);

    var grid = document.createElement('div'); grid.className = 'cms-modal-grid';
    var tiles = [];
    if (!media.length) {
      grid.innerHTML = '<div class="cms-modal-empty">No images uploaded yet.<br>Open <b>Media Library</b> in the sidebar to upload some, then come back — or paste a URL below.</div>';
    } else {
      media.forEach(function (m) {
        var it = document.createElement('button'); it.type = 'button'; it.className = 'cms-mi'; it.title = (m.name || '') + '\n' + (m.path || '');
        var img = document.createElement('img'); img.src = m.url; img.loading = 'lazy'; img.alt = '';
        var cap = document.createElement('span'); cap.textContent = m.name || '';
        it.appendChild(img); it.appendChild(cap);
        it.dataset.q = ((m.name || '') + ' ' + (m.path || '')).toLowerCase();
        it.addEventListener('click', function () { done(m); });
        grid.appendChild(it);
        tiles.push(it);
      });
    }
    box.appendChild(grid);

    var counter = tools.querySelector('.cms-modal-n');
    function applyFilter() {
      var q = (tools.querySelector('.cms-msearch').value || '').trim().toLowerCase();
      var shown = 0;
      tiles.forEach(function (t) {
        var hit = q === '' || t.dataset.q.indexOf(q) !== -1;
        t.style.display = hit ? '' : 'none';
        if (hit) shown++;
      });
      if (counter) counter.textContent = q === '' ? media.length + ' files' : shown + ' of ' + media.length;
    }
    if (media.length) {
      tools.querySelector('.cms-msearch').addEventListener('input', applyFilter);
      applyFilter();
    }

    var foot = document.createElement('div'); foot.className = 'cms-modal-ft';
    foot.innerHTML = '<input type="text" class="inp cms-url" placeholder="…or paste an image URL / path"><button type="button" class="btn btn-primary btn-sm cms-url-go">Use</button>';
    box.appendChild(foot);
    ov.appendChild(box); document.body.appendChild(ov);

    function done(m) { cleanup(); if (typeof onPick === 'function') onPick(m); }
    function cleanup() { ov.remove(); document.removeEventListener('keydown', esc); }
    function esc(e) { if (e.key === 'Escape') cleanup(); }
    document.addEventListener('keydown', esc);
    ov.addEventListener('click', function (e) { if (e.target === ov) cleanup(); });
    box.querySelector('.cms-modal-x').addEventListener('click', cleanup);
    var urlin = box.querySelector('.cms-url'), go = box.querySelector('.cms-url-go');
    go.addEventListener('click', function () { var v = urlin.value.trim(); if (v) done({ path: v, url: v, name: v }); });
    urlin.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); go.click(); } });
    var searchBox = box.querySelector('.cms-msearch');
    setTimeout(function () { (searchBox || urlin).focus(); }, 30);
  };

  // "Choose" buttons next to path fields (section image, banner, post banner)
  document.addEventListener('click', function (e) {
    var b = e.target.closest('.cms-pick'); if (!b) return;
    e.preventDefault();
    var input = b.parentNode.querySelector('input'); if (!input) return;
    window.openMediaPicker(function (m) {
      input.value = m.path || m.url;
      input.dispatchEvent(new Event('input', { bubbles: true }));
    });
  });

  /* ─────────────────────────────────────────────────────────────────
     MULTI-IMAGE HERO BANNER repeater (page editor Banner tab + Banners
     manager). Add / remove / reorder banner images; the first is "Main".
     Rows are named hero_images[] and submit as an ordered list.
     ───────────────────────────────────────────────────────────────── */
  function murl(p) {
    p = (p || '').trim();
    if (p === '') return '';
    if (/^https?:\/\//i.test(p) || /^data:/i.test(p)) return p;
    return (window.CMS_BASE || '/') + p.replace(/^\/+/, '');
  }
  function heroRowMarkup() {
    // Markup mirrors hero_img_row_html() in admin/index.php so a JS-added row is
    // indistinguishable from a server-rendered one (inline SVGs, same classes).
    return '<div class="hero-img-main">'
      + '<span class="hero-img-badge"></span>'
      + '<span class="hero-img-thumb"><img alt="" loading="lazy" onerror="this.style.visibility=\'hidden\'"></span>'
      + '<input class="inp hero-img-inp" name="hero_images[]" value="" placeholder="choose or paste an image path">'
      + '<button type="button" class="btn btn-ghost btn-sm cms-pick" title="Choose from Media Library"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg></button>'
      + '<button type="button" class="btn btn-ghost btn-sm hero-img-up" title="Move up" aria-label="Move up"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button>'
      + '<button type="button" class="btn btn-ghost btn-sm hero-img-down" title="Move down" aria-label="Move down"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M12 5v14M5 12l7 7 7-7"/></svg></button>'
      + '<button type="button" class="btn btn-ghost btn-sm hero-img-del" title="Remove image" aria-label="Remove image"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M6 6 18 18M18 6 6 18"/></svg></button>'
      + '</div>'
      + '<div class="hero-img-text">'
      + '<input class="inp" name="hero_eyebrow_s[]" value="" placeholder="Eyebrow — optional, overrides the page" maxlength="200">'
      + '<input class="inp" name="hero_heading_s[]" value="" placeholder="Heading — optional" maxlength="255">'
      + '<input class="inp" name="hero_subtext_s[]" value="" placeholder="Sub-text — optional" maxlength="500">'
      + '</div>';
  }
  function heroRefresh(box) {
    var rows = box.querySelectorAll('.hero-img-row');
    rows.forEach(function (row, i) {
      var badge = row.querySelector('.hero-img-badge');
      if (badge) { badge.textContent = i === 0 ? 'Main' : ''; badge.classList.toggle('is-first', i === 0); }
      var up = row.querySelector('.hero-img-up'), dn = row.querySelector('.hero-img-down');
      if (up) up.disabled = i === 0;
      if (dn) dn.disabled = i === rows.length - 1;
      heroThumb(row);
    });
  }
  function heroThumb(row) {
    var inp = row.querySelector('.hero-img-inp'), img = row.querySelector('.hero-img-thumb img');
    if (!inp || !img) return;
    var u = murl(inp.value);
    if (u) { img.src = u; img.style.visibility = ''; } else { img.removeAttribute('src'); img.style.visibility = 'hidden'; }
  }
  document.addEventListener('click', function (e) {
    var box = e.target.closest('[data-hero-imgs]'); if (!box) return;
    var list = box.querySelector('.hero-imgs-list');
    var row = e.target.closest('.hero-img-row');
    if (e.target.closest('.hero-img-add')) {
      e.preventDefault();
      var nr = document.createElement('div'); nr.className = 'hero-img-row'; nr.innerHTML = heroRowMarkup();
      list.appendChild(nr); heroRefresh(box);
      var inp = nr.querySelector('.hero-img-inp');
      window.openMediaPicker(function (m) { inp.value = m.path || m.url; heroThumb(nr); });
      return;
    }
    if (!row) return;
    if (e.target.closest('.hero-img-del')) {
      e.preventDefault();
      if (list.querySelectorAll('.hero-img-row').length <= 1) { row.querySelector('.hero-img-inp').value = ''; heroThumb(row); }
      else row.remove();
      heroRefresh(box);
    } else if (e.target.closest('.hero-img-up')) {
      e.preventDefault();
      if (row.previousElementSibling) row.parentNode.insertBefore(row, row.previousElementSibling);
      heroRefresh(box);
    } else if (e.target.closest('.hero-img-down')) {
      e.preventDefault();
      if (row.nextElementSibling) row.parentNode.insertBefore(row.nextElementSibling, row);
      heroRefresh(box);
    }
  });
  // keep thumbnails in sync when a path is typed or picked
  document.addEventListener('input', function (e) {
    if (e.target.classList && e.target.classList.contains('hero-img-inp')) {
      var row = e.target.closest('.hero-img-row'); if (row) heroThumb(row);
    }
  });
  // initialise any repeaters present on load (badges + disabled arrows)
  document.querySelectorAll('[data-hero-imgs]').forEach(heroRefresh);

  // "Add image from Media Library" under list textareas (photo galleries):
  // appends the picked path as a new "path | " line, cursor ready for the caption.
  document.addEventListener('click', function (e) {
    var b = e.target.closest('.cms-pick-line'); if (!b) return;
    e.preventDefault();
    var ta = b.closest('.field, div') && b.closest('.field, div').querySelector('textarea'); if (!ta) return;
    window.openMediaPicker(function (m) {
      var line = (m.path || m.url) + ' | ';
      ta.value = ta.value.replace(/\s+$/, '');
      ta.value += (ta.value ? '\n' : '') + line;
      ta.dispatchEvent(new Event('input', { bubbles: true }));
      ta.focus();
      ta.selectionStart = ta.selectionEnd = ta.value.length;
    });
  });
})();

/* ── Light / dark mode toggle (persisted per browser) ── */
(function () {
  document.addEventListener('click', function (e) {
    var b = e.target.closest('#themeToggle'); if (!b) return;
    var el = document.documentElement;
    var dark = el.getAttribute('data-theme') === 'dark';
    el.setAttribute('data-theme', dark ? 'light' : 'dark');
    try { localStorage.setItem('ea-cms-theme', dark ? 'light' : 'dark'); } catch (err) {}
  });
})();

/* ── Live date & time in the top bar — shown in the SITE's timezone
      (Admin → Settings → Timezone; exposed as window.CMS_TZ). ── */
(function () {
  var el = document.getElementById('tbClock');
  if (!el) return;
  var fmt = null;
  try {
    fmt = new Intl.DateTimeFormat('en-US', {
      timeZone: window.CMS_TZ || undefined,
      weekday: 'short', day: 'numeric', month: 'short', year: 'numeric',
      hour: 'numeric', minute: '2-digit', second: '2-digit'
    });
  } catch (err) { /* unknown zone on this browser — fall back to local time */ }
  function two(n) { return (n < 10 ? '0' : '') + n; }
  function tick() {
    var d = new Date();
    if (fmt) { el.textContent = fmt.format(d).replace(/,\s(\d{4}),/, ' $1 ·'); return; }
    var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
    var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    el.textContent = days[d.getDay()] + ', ' + d.getDate() + ' ' + months[d.getMonth()] + ' ' + d.getFullYear()
      + ' · ' + two(d.getHours()) + ':' + two(d.getMinutes()) + ':' + two(d.getSeconds());
  }
  tick();
  setInterval(tick, 1000);
})();

/* ── PAGES screen: live search across every group card.
      Filters page rows by title or /slug, hides empty cards, opens the
      collapsed Press list while searching. "/" focuses, Escape clears. ── */
(function () {
  var input = document.getElementById('pgSearch');
  var wrap = document.getElementById('pgWrap');
  if (!input || !wrap) return;
  var counter = document.getElementById('pgSearchN');
  var noRes = document.getElementById('pgNoResults');
  var total = parseInt(wrap.getAttribute('data-total') || '0', 10) || 0;
  var rows = [].slice.call(wrap.querySelectorAll('.pgrow')).filter(function (r) {
    return !r.classList.contains('pgc-note');           // skip the "managed under Blog Posts" note row
  });
  rows.forEach(function (r) {
    var b = r.querySelector('b'), s = r.querySelector('.slug');
    r.dataset.q = ((b ? b.textContent : '') + ' ' + (s ? s.textContent : '')).toLowerCase();
  });
  function apply() {
    var q = input.value.trim().toLowerCase();
    wrap.classList.toggle('pg-searching', q !== '');
    var shown = 0;
    rows.forEach(function (r) {
      var hit = q === '' || r.dataset.q.indexOf(q) !== -1;
      r.classList.toggle('hide', !hit);
      if (hit && q !== '') shown++;
    });
    wrap.querySelectorAll('.pgc').forEach(function (card) {
      if (q === '') { card.classList.remove('hide'); return; }
      var any = card.querySelector('.pgrow:not(.hide):not(.pgc-note)');
      card.classList.toggle('hide', !any);
    });
    wrap.querySelectorAll('details.pmore').forEach(function (d) { d.open = q !== ''; });
    if (counter) counter.textContent = q === '' ? total + ' pages' : shown + ' of ' + total + ' match';
    if (noRes) noRes.hidden = !(q !== '' && shown === 0);
  }
  input.addEventListener('input', apply);
  input.addEventListener('keydown', function (e) {
    if (e.key === 'Escape') { input.value = ''; apply(); input.blur(); }
  });
  apply();
  // arriving via the top-bar search button / Ctrl+K (?focus=search) → jump straight in
  try {
    if (new URLSearchParams(location.search).get('focus') === 'search') { input.focus(); input.select(); }
  } catch (err) {}
})();

/* ── Keyboard shortcut: "/" focuses the pages search (when not typing) ── */
document.addEventListener('keydown', function (e) {
  if (e.key !== '/' || e.ctrlKey || e.metaKey || e.altKey) return;
  var t = e.target;
  if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.isContentEditable)) return;
  var s = document.getElementById('pgSearch');
  if (!s) return;
  e.preventDefault();
  s.focus();
  s.select();
});

/* ── History dropdown (page/post editors): close when clicking outside ── */
document.addEventListener('click', function (e) {
  document.querySelectorAll('details.rev-dd[open]').forEach(function (d) {
    if (!d.contains(e.target)) d.removeAttribute('open');
  });
});

/* ── Ctrl/⌘ + K → the pages search, from anywhere in the admin.
      On the Pages screen it focuses the box directly; on every other screen
      it follows the top-bar search button to ?p=pages&focus=search. The
      button's kbd hint adapts to the platform (⌘K on Mac, Ctrl K elsewhere). ── */
(function () {
  var k = document.getElementById('tbFindK');
  if (k && !/mac|iphone|ipad/i.test(navigator.platform || '')) k.textContent = 'Ctrl K';
  document.addEventListener('keydown', function (e) {
    if (!(e.metaKey || e.ctrlKey) || e.altKey || (e.key !== 'k' && e.key !== 'K')) return;
    e.preventDefault();
    var s = document.getElementById('pgSearch');
    if (s) { s.focus(); s.select(); return; }
    var b = document.getElementById('tbSearch');
    if (b) location.href = b.getAttribute('href');
  });
})();

/* ── Dashboard: 30-day analytics chart — hover / touch tooltip.
      The data comes from PHP as window.CMS_DASH (real daily counts from the
      `visits` table); this only maps pointer position → nearest day. ── */
(function () {
  var svg = document.getElementById('dbChart');
  var d = window.CMS_DASH;
  if (!svg || !d || !d.days || d.days.length < 2) return;
  var wrap = document.getElementById('dbChartWrap');
  var tip = document.getElementById('dbTip');
  var vline = document.getElementById('dbVline');
  var dotV = document.getElementById('dbDotV');
  var dotS = document.getElementById('dbDotS');
  if (!wrap || !tip || !vline || !dotV || !dotS) return;
  var n = d.days.length;
  function xAt(i) { return d.x0 + i * (d.x1 - d.x0) / (n - 1); }
  function yAt(v) { return d.y1 - (v / d.max) * (d.y1 - d.y0); }
  function show(i, clientX) {
    var x = xAt(i);
    vline.setAttribute('x1', x); vline.setAttribute('x2', x);
    dotV.setAttribute('cx', x); dotV.setAttribute('cy', yAt(d.views[i]));
    dotS.setAttribute('cx', x); dotS.setAttribute('cy', yAt(d.visits[i]));
    vline.style.display = dotV.style.display = dotS.style.display = '';
    tip.innerHTML = '<b>' + d.days[i] + '</b> · ' + d.views[i] + ' page view' + (d.views[i] === 1 ? '' : 's')
                  + ' · ' + d.visits[i] + ' visit' + (d.visits[i] === 1 ? '' : 's');
    tip.hidden = false;
    var wr = wrap.getBoundingClientRect();
    var left = clientX - wr.left + 14;
    if (left + tip.offsetWidth > wr.width - 6) left = clientX - wr.left - tip.offsetWidth - 14;
    tip.style.left = Math.max(6, left) + 'px';
  }
  function onMove(e) {
    var r = svg.getBoundingClientRect();
    if (!r.width) return;
    var vx = (e.clientX - r.left) / r.width * d.vw;
    var i = Math.round((vx - d.x0) * (n - 1) / (d.x1 - d.x0));
    show(Math.max(0, Math.min(n - 1, i)), e.clientX);
  }
  function onLeave() {
    tip.hidden = true;
    vline.style.display = dotV.style.display = dotS.style.display = 'none';
  }
  svg.addEventListener('pointermove', onMove);
  svg.addEventListener('pointerdown', onMove);
  svg.addEventListener('pointerleave', onLeave);
})();

/* ── Unsaved-changes warning on the page/post editors (form[data-guard]).
      Warns before leaving with edits pending; never blocks a real save. ── */
(function () {
  var forms = [].slice.call(document.querySelectorAll('form[data-guard]'));
  if (!forms.length) return;
  var dirty = false, submitting = false;
  forms.forEach(function (f) {
    f.addEventListener('input', function () { dirty = true; });
    f.addEventListener('change', function () { dirty = true; });
    f.addEventListener('submit', function () { submitting = true; });
  });
  // Any real form submission on the page — the guarded editor, the "Delete this
  // page" form, the Activation-bar Publish/Draft buttons — is an intentional
  // navigation. Mark it so the unsaved-changes prompt can never intercept and
  // silently cancel it (root cause of "deleted but the page still shows").
  document.addEventListener('submit', function () { submitting = true; }, true);
  window.addEventListener('beforeunload', function (e) {
    if (!dirty || submitting) return;
    e.preventDefault();
    e.returnValue = '';   // Chrome requires returnValue to show the prompt
  });
})();

/* ═══════════════════════════════════════════════════════════════════
   JOB OPENINGS repeater (page editor → a "jobs" section). A friendly,
   card-based editor: add / delete / reorder (arrows) / enable-disable
   each opening, with the Details field using the same Word-style rich
   editor as the section bodies. NO JSON hand-editing.
   • Storage is unchanged: these structured sections[<id>][jobs][<idx>][…]
     fields are re-encoded to the JSON array render.php reads, server-side
     (admin/index.php → jobs_json_from_post). DOM order = opening order.
   • Titleless cards are dropped on save; an empty list renders the site's
     friendly "no open positions" message.
   ═══════════════════════════════════════════════════════════════════ */
(function () {
  function markTitle(row) {
    var t = row.querySelector('[data-job-title]');
    if (t) row.classList.toggle('no-title', t.value.trim() === '');
  }
  function refresh(box) {
    var rows = box.querySelectorAll('.jobrow');
    rows.forEach(function (row, i) {
      var num = row.querySelector('.jobrow-num');
      if (num) num.textContent = i + 1;
      var up = row.querySelector('.jobrow-up'), dn = row.querySelector('.jobrow-down');
      if (up) up.disabled = (i === 0);
      if (dn) dn.disabled = (i === rows.length - 1);
      markTitle(row);
    });
    var empty = box.querySelector('.jobrow-empty');
    if (empty) empty.hidden = rows.length > 0;
  }
  function setShow(row, on) {
    var h = row.querySelector('[data-job-show]');
    if (h) h.value = on ? '1' : '0';
    row.classList.toggle('is-off', !on);
    var btn = row.querySelector('[data-job-toggle]');
    if (btn) {
      btn.setAttribute('aria-pressed', on ? 'true' : 'false');
      var t = btn.querySelector('.jobrow-toggle-t');
      if (t) t.textContent = on ? 'Enabled' : 'Disabled';
    }
  }
  document.addEventListener('click', function (e) {
    var box = e.target.closest('[data-jobs]'); if (!box) return;
    var list = box.querySelector('.jobs-list');
    if (e.target.closest('.jobrow-add')) {
      e.preventDefault();
      var tpl = box.querySelector('.jobrow-tpl'); if (!tpl) return;
      var idx = parseInt(box.getAttribute('data-jobs-next') || '0', 10) || 0;
      box.setAttribute('data-jobs-next', idx + 1);      // unique index → no POST-key collisions
      var tmp = document.createElement('div');
      tmp.innerHTML = tpl.innerHTML.replace(/__IDX__/g, idx);
      var row = tmp.firstElementChild;
      list.appendChild(row);
      if (window.CMS_initRich) window.CMS_initRich();    // give the new Details field its toolbar
      refresh(box);
      var ti = row.querySelector('[data-job-title]'); if (ti) ti.focus();
      return;
    }
    var row = e.target.closest('.jobrow'); if (!row) return;
    if (e.target.closest('.jobrow-del')) {
      e.preventDefault();
      if (confirm('Delete this job opening? It will be removed when you save.')) { row.remove(); refresh(box); }
    } else if (e.target.closest('.jobrow-up')) {
      e.preventDefault();
      if (row.previousElementSibling) row.parentNode.insertBefore(row, row.previousElementSibling);
      refresh(box);
    } else if (e.target.closest('.jobrow-down')) {
      e.preventDefault();
      if (row.nextElementSibling) row.parentNode.insertBefore(row.nextElementSibling, row);
      refresh(box);
    } else if (e.target.closest('[data-job-toggle]')) {
      e.preventDefault();
      var h = row.querySelector('[data-job-show]');
      setShow(row, !(h && h.value === '1'));
    }
  });
  document.addEventListener('input', function (e) {
    if (e.target.matches && e.target.matches('[data-job-title]')) {
      var row = e.target.closest('.jobrow'); if (row) markTitle(row);
    }
  });
  document.querySelectorAll('[data-jobs]').forEach(refresh);
})();

/* ── Media Library: "Copy path" → clipboard (v1.2) ──
   Copies the file's uploads/ path so the client can paste it into a page
   banner or image field. Uses the async Clipboard API with a legacy fallback. */
(function () {
  function flash(btn) {
    var span = btn.querySelector('span'), old = span ? span.textContent : '';
    btn.classList.add('copied'); if (span) span.textContent = 'Copied!';
    setTimeout(function () { btn.classList.remove('copied'); if (span) span.textContent = old; }, 1400);
  }
  function copyText(text) {
    if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text);
    return new Promise(function (res, rej) {
      try {
        var ta = document.createElement('textarea'); ta.value = text;
        ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta);
        ta.focus(); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); res();
      } catch (e) { rej(e); }
    });
  }
  document.addEventListener('click', function (e) {
    var btn = e.target.closest('.m-copy'); if (!btn) return;
    e.preventDefault();
    copyText(btn.getAttribute('data-path') || '').then(function () { flash(btn); }).catch(function () {
      window.prompt('Copy this path:', btn.getAttribute('data-path') || '');
    });
  });
})();
