Index: src/assets/js/mod/admin-media.js
===================================================================
--- src/assets/js/mod/admin-media.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
+++ src/assets/js/mod/admin-media.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -0,0 +1,38 @@
+// Media in beheer -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script wordt door de CSP geweigerd zodra deze pagina via een link
+// BINNEN de site binnenkomt (shaer-0i6). Servergegevens komen uit pageData();
+// interpolatie kan niet in een statisch bestand.
+
+import { pageData } from './lib.js';
+
+(function () {
+  if (window.__mediaWired) return; window.__mediaWired = true;
+  var T = pageData();
+  document.addEventListener('click', function (e) {
+    var c = e.target.closest('[data-copy]');
+    if (c) {
+      var u = location.origin + c.getAttribute('data-copy');
+      var done = function () { var o = c.textContent; c.textContent = '✓'; setTimeout(function () { c.textContent = o === '✓' ? T.copy : o; }, 1200); };
+      if (navigator.clipboard) navigator.clipboard.writeText(u).then(done).catch(function () { window.prompt('URL', u); });
+      else window.prompt('URL', u);
+      return;
+    }
+    var d = e.target.closest('[data-del]');
+    if (d) {
+      if (!window.confirm(T.delC)) return;
+      fetch('/admin/media/delete', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file: d.getAttribute('data-del') }) })
+        .then(function (r) { return r.json(); })
+        .then(function (j) { if (j && j.ok) { var card = d.closest('.media-card'); if (card) card.remove(); } else window.alert((j && j.error) || 'Error'); })
+        .catch(function () { window.alert('Error'); });
+      return;
+    }
+    if (e.target.closest('#media-cleanup')) {
+      if (!window.confirm(T.cleanC)) return;
+      fetch('/admin/media/cleanup', { method: 'POST', credentials: 'same-origin' })
+        .then(function (r) { return r.json(); })
+        .then(function (j) { location.href = '/admin/media?success=' + encodeURIComponent(((j && j.removed) || 0) + ' file(s) removed'); })
+        .catch(function () { window.alert('Error'); });
+    }
+  });
+})();
Index: src/assets/js/mod/admin-playlists.js
===================================================================
--- src/assets/js/mod/admin-playlists.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
+++ src/assets/js/mod/admin-playlists.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -0,0 +1,75 @@
+// Afspeellijsten in beheer -- verplaatst uit inline script, shaer-bqr.
+//
+// Servergegevens (het csrf-token en drie teksten) komen uit pageData();
+// interpolatie kan niet in een statisch bestand.
+
+import { pageData } from './lib.js';
+
+(function() {
+  const _d = pageData();
+  const csrf = _d.csrf || '';
+
+  document.getElementById('pl-new-btn')?.addEventListener('click', () => {
+    if (typeof window.openPlaylistEditor === 'function') {
+      window.openPlaylistEditor({ mode: 'create', onSaved: () => location.reload() });
+    }
+  });
+
+  // Click-to-copy on shortcodes
+  document.querySelectorAll('[data-copy]').forEach(el => {
+    el.addEventListener('click', async () => {
+      try {
+        await navigator.clipboard.writeText(el.dataset.copy);
+        el.classList.add('is-copied');
+        const original = el.textContent;
+        el.textContent = '✓ ' + (_d.copied || '');
+        setTimeout(() => { el.classList.remove('is-copied'); el.textContent = original; }, 1200);
+      } catch (_) { /* fall back to selection */ }
+    });
+  });
+
+  document.querySelectorAll('[data-pl-edit]').forEach(btn => {
+    btn.addEventListener('click', () => {
+      if (typeof window.openPlaylistEditor === 'function') {
+        window.openPlaylistEditor({ mode: 'edit', id: btn.dataset.id, onSaved: () => location.reload() });
+      }
+    });
+  });
+
+  document.querySelectorAll('[data-pl-delete]').forEach(btn => {
+    btn.addEventListener('click', async () => {
+      const id = btn.dataset.id;
+      const title = btn.dataset.title || id;
+      if (!confirm((_d.delConfirm || '').replace('{title}', title))) return;
+      try {
+        const r = await fetch(`/admin/playlists/api/${encodeURIComponent(id)}/delete`, {
+          method: 'POST',
+          headers: { 'X-CSRF-Token': csrf },
+          credentials: 'same-origin',
+        });
+        const j = await r.json();
+        if (j.ok) location.reload();
+        else alert((_d.delFailed || '') + ': ' + (j.error || ''));
+      } catch (err) {
+        alert((_d.delFailed || '') + ': ' + err.message);
+      }
+    });
+  });
+
+  // P52 — deep-link from playlist embed (?edit=<id>) auto-opens the editor.
+  // openPlaylistEditor is defined synchronously by the included partial, so
+  // it's available by the time this IIFE runs.
+  (function deepLinkEdit() {
+    const params = new URLSearchParams(location.search);
+    const editId = params.get('edit');
+    if (!editId) return;
+    if (typeof window.openPlaylistEditor !== 'function') return;
+    // Strip the query param immediately so reload after save doesn't re-open.
+    history.replaceState({}, '', location.pathname);
+    window.openPlaylistEditor({
+      mode: 'edit',
+      id: editId,
+      onSaved: () => location.reload(),
+    });
+  })();
+})();
Index: src/assets/js/mod/admin-videos.js
===================================================================
--- src/assets/js/mod/admin-videos.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
+++ src/assets/js/mod/admin-videos.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -0,0 +1,30 @@
+// Video's in beheer -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script wordt door de CSP geweigerd zodra deze pagina via een link
+// BINNEN de site binnenkomt (shaer-0i6). Servergegevens komen uit pageData();
+// interpolatie kan niet in een statisch bestand.
+
+import { pageData } from './lib.js';
+
+(function () {
+  if (window.__videosWired) return; window.__videosWired = true;
+  var T = pageData();
+  document.addEventListener('click', function (e) {
+    var c = e.target.closest('[data-copy]');
+    if (c) {
+      var u = location.origin + c.getAttribute('data-copy');
+      var done = function () { var o = c.textContent; c.textContent = '✓'; setTimeout(function () { c.textContent = o === '✓' ? T.copy : o; }, 1200); };
+      if (navigator.clipboard) navigator.clipboard.writeText(u).then(done).catch(function () { window.prompt('URL', u); });
+      else window.prompt('URL', u);
+      return;
+    }
+    var d = e.target.closest('[data-del]');
+    if (d) {
+      if (!window.confirm(T.delC)) return;
+      fetch('/admin/media/videos/delete', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file: d.getAttribute('data-del') }) })
+        .then(function (r) { return r.json(); })
+        .then(function (j) { if (j && j.ok) { var card = d.closest('.media-card'); if (card) card.remove(); } })
+        .catch(function () {});
+    }
+  });
+})();
Index: src/assets/js/mod/chrome.js
===================================================================
--- src/assets/js/mod/chrome.js	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/assets/js/mod/chrome.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -100,2 +100,185 @@
   document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
 })();
+
+// ── de topnav ──────────────────────────────────────────────────
+// Zat inline in partials/topnav.ejs. De zoekteksten komen nu van het
+// overlay-element (data-i18n) in plaats van uit interpolatie.
+(function() {
+  // Wired ONCE. This chrome (topnav) is re-inserted out-of-band during htmx navigation
+  // → without this guard the script would stack EXTRA listeners on every navigation,
+  // causing the theme toggle to fire 2× (or more) = no net change ("toggle stops working").
+  // Everything below uses event delegation on body/document, so it also works for
+  // buttons that appear after this run (OOB).
+  if (window.__pcmsChromeWired) return;
+  window.__pcmsChromeWired = true;
+
+  function toggleTheme() {
+    var cur = document.documentElement.getAttribute('data-theme') || 'dark';
+    var next = cur === 'dark' ? 'light' : 'dark';
+    document.documentElement.setAttribute('data-theme', next);
+    try { localStorage.setItem('pcms-theme', next); } catch (e) {}
+  }
+  function overlay() { return document.getElementById('search-overlay'); }
+  function openSearch() { var o = overlay(); if (o) { o.hidden = false; var i = o.querySelector('input'); if (i) i.focus(); } }
+  function closeSearch() { var o = overlay(); if (o) { o.hidden = true; var b = document.getElementById('search-suggest'); if (b) b.innerHTML = ''; } }
+
+  document.body.addEventListener('click', function(e) {
+    if (e.target.closest('#theme-toggle, #theme-toggle-mobile, #theme-toggle-footer')) { toggleTheme(); return; }
+    if (e.target.closest('#search-toggle')) { openSearch(); return; }
+    if (e.target.closest('#search-close')) { closeSearch(); return; }
+    // Close open dropdowns (user menu + language picker) on click outside.
+    document.querySelectorAll('.user-menu[open], .lang-menu[open]').forEach(function(d) {
+      if (!d.contains(e.target)) d.removeAttribute('open');
+    });
+  });
+
+  document.addEventListener('keydown', function(e) {
+    if (e.key === 'Escape') { var o = overlay(); if (o && !o.hidden) closeSearch(); }
+  });
+
+  // ── Live results while typing ───────────────────────────────────
+  // De teksten komen van de overlay zelf, elke keer opnieuw: bij een
+  // htmx-navigatie wordt die vervangen en kan de taal gewisseld zijn.
+  function _st() {
+    var o = document.getElementById('search-overlay');
+    try { return JSON.parse((o && o.getAttribute('data-i18n')) || '{}'); } catch (e) { return {}; }
+  }
+  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }
+  function renderSuggest(box, d, q, ov) {
+    var html = '';
+    function grp(title, items, fmt) {
+      if (!items || !items.length) return;
+      html += '<div class="ss-group"><div class="ss-title">' + esc(title) + '</div>' + items.map(fmt).join('') + '</div>';
+    }
+    grp(_st().posts, d.posts, function(p){ return '<a class="ss-item" href="' + esc(p.url) + '">' + esc(p.title) + '</a>'; });
+    grp(_st().tracks, d.tracks, function(tk){ var sub = tk.artist ? ' <span class="ss-sub">' + esc(tk.artist) + '</span>' : ''; return tk.url ? '<a class="ss-item" href="' + esc(tk.url) + '">' + esc(tk.title) + sub + '</a>' : '<span class="ss-item ss-noclick">' + esc(tk.title) + sub + '</span>'; });
+    grp(_st().events, d.events, function(ev){ return '<a class="ss-item" href="' + esc(ev.url) + '">' + esc(ev.where || ev.when) + ' <span class="ss-sub">' + esc(ev.when) + '</span></a>'; });
+    grp(_st().pages, d.pages, function(pg){ return '<a class="ss-item" href="' + esc(pg.url) + '">' + esc(pg.label) + '</a>'; });
+    var hasAny = (d.posts && d.posts.length) || (d.tracks && d.tracks.length) || (d.events && d.events.length) || (d.pages && d.pages.length);
+    if (!hasAny) { box.innerHTML = '<div class="ss-empty">' + esc(_st().none) + '</div>'; return; }
+    var action = (ov && ov.getAttribute('data-action')) || '/search';
+    html += '<a class="ss-all" href="' + esc(action) + '?q=' + encodeURIComponent(q) + '">' + esc(_st().all) + '</a>';
+    box.innerHTML = html;
+  }
+  var _sTimer;
+  document.addEventListener('input', function(e) {
+    var inp = e.target.closest && e.target.closest('#search-overlay input[name="q"]');
+    if (!inp) return;
+    var box = document.getElementById('search-suggest');
+    if (!box) return;
+    var q = inp.value.trim();
+    clearTimeout(_sTimer);
+    if (q.length < 2) { box.innerHTML = ''; return; }
+    _sTimer = setTimeout(function() {
+      var ov = document.getElementById('search-overlay');
+      var url = (ov && ov.getAttribute('data-suggest')) || '/search/suggest';
+      fetch(url + '?q=' + encodeURIComponent(q))
+        .then(function(r){ return r.ok ? r.json() : null; })
+        .then(function(d){ if (d) renderSuggest(box, d, q, ov); })
+        .catch(function(){});
+    }, 200);
+  });
+  // Click on a suggestion → close the overlay (the link/boost handles navigation).
+  document.addEventListener('click', function(e) {
+    if (e.target.closest && e.target.closest('#search-suggest a')) { var o = overlay(); if (o) o.hidden = true; }
+  });
+})();
+
+// ── het profielblad ────────────────────────────────────────────
+// Zat inline in partials/profile-sheet.ejs, dat de shell opneemt.
+(function() {
+  const sheet = document.getElementById('profile-sheet');
+  if (!sheet) return;
+
+  const backdrop  = document.getElementById('profile-sheet-backdrop');
+  const panel     = sheet.querySelector('.profile-sheet-panel');
+  const closeBtn  = document.getElementById('profile-sheet-close');
+  const dragZone  = document.getElementById('profile-sheet-drag-zone');
+  const themeBtn  = document.getElementById('profile-sheet-theme');
+  const themeLbl  = document.getElementById('profile-sheet-theme-state');
+
+  function openSheet() {
+    sheet.classList.add('is-open');
+    sheet.setAttribute('aria-hidden', 'false');
+    document.body.classList.add('profile-sheet-locked');
+    syncTheme();
+  }
+  function closeSheet() {
+    sheet.classList.remove('is-open');
+    sheet.setAttribute('aria-hidden', 'true');
+    document.body.classList.remove('profile-sheet-locked');
+    panel.style.removeProperty('--pcms-drag-y');
+  }
+
+  // Open: any element with [data-profile-sheet-toggle]
+  document.addEventListener('click', function(e) {
+    const trigger = e.target.closest('[data-profile-sheet-toggle]');
+    if (trigger) {
+      e.preventDefault();
+      openSheet();
+    }
+  });
+
+  // Close: backdrop tap, handle tap, ESC, or any [data-close-sheet] item
+  if (backdrop) backdrop.addEventListener('click', closeSheet);
+  if (closeBtn) closeBtn.addEventListener('click', closeSheet);
+  document.addEventListener('keydown', (e) => {
+    if (e.key === 'Escape' && sheet.classList.contains('is-open')) closeSheet();
+  });
+
+  // Menu items that navigate: close synchronously before nav fires
+  sheet.querySelectorAll('[data-close-sheet]').forEach((el) => {
+    el.addEventListener('click', closeSheet);
+  });
+
+  // Drag-down-to-close on touch.
+  // Uses --pcms-drag-y custom property rather than overwriting the panel's
+  // transform string. This composes correctly with any horizontal centering
+  // (currently none here, but the pattern matches audio-sheet for safety).
+  let startY = 0, lastY = 0, dragging = false;
+  function onDown(e) {
+    if (e.pointerType !== 'touch') return;
+    if (panel.scrollTop > 0) return;
+    startY = lastY = e.clientY;
+    dragging = true;
+    panel.classList.add('is-dragging');
+  }
+  function onMove(e) {
+    if (!dragging) return;
+    lastY = e.clientY;
+    const dy = Math.max(0, lastY - startY);
+    panel.style.setProperty('--pcms-drag-y', dy + 'px');
+  }
+  function onUp() {
+    if (!dragging) return;
+    dragging = false;
+    panel.classList.remove('is-dragging');
+    const dy = lastY - startY;
+    if (dy > 80) closeSheet();
+    else panel.style.removeProperty('--pcms-drag-y');
+  }
+  if (window.PointerEvent && dragZone) {
+    dragZone.addEventListener('pointerdown', onDown);
+    document.addEventListener('pointermove', onMove);
+    document.addEventListener('pointerup', onUp);
+    document.addEventListener('pointercancel', onUp);
+  }
+
+  // Theme toggle inside sheet — syncs label
+  function syncTheme() {
+    if (!themeLbl) return;
+    const t = document.documentElement.getAttribute('data-theme') || 'dark';
+    // De twee labels staan op het element zelf: een module kan geen vertaling
+    // interpoleren, en zo hoort de tekst bij het ding dat hem toont.
+    themeLbl.textContent = themeLbl.getAttribute(t === 'dark' ? 'data-dark' : 'data-light') || themeLbl.textContent;
+  }
+  if (themeBtn) {
+    themeBtn.addEventListener('click', function() {
+      const cur = document.documentElement.getAttribute('data-theme') || 'dark';
+      const next = cur === 'dark' ? 'light' : 'dark';
+      document.documentElement.setAttribute('data-theme', next);
+      try { localStorage.setItem('pcms-theme', next); } catch (e) {}
+      syncTheme();
+    });
+  }
+})();
Index: src/assets/js/mod/paid-gate.js
===================================================================
--- src/assets/js/mod/paid-gate.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
+++ src/assets/js/mod/paid-gate.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -0,0 +1,61 @@
+// De betaalmuur -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script wordt door de CSP geweigerd zodra deze pagina via een link
+// BINNEN de site binnenkomt (shaer-0i6). Servergegevens komen uit pageData();
+// interpolatie kan niet in een statisch bestand.
+
+import { pageData } from './lib.js';
+
+(function () {
+  var _d = pageData();
+  var base = _d.base || "";
+  var slug = _d.slug || "";
+  var hasPatron = !!_d.hasPatron;
+  var I = _d.i18n || {};
+  var btn = document.getElementById('pg-unlock');
+  var status = document.getElementById('pg-status');
+  function say(msg, err) { status.hidden = false; status.textContent = msg; status.classList.toggle('is-err', !!err); }
+  function toLink() { location.href = base + '/paid/link?post=' + encodeURIComponent(slug); }
+
+  // No WebAuthn here: an assertion is impossible. With a Patreon page there is
+  // already a "Word supporter" button, so hide the (dead) unlock button rather
+  // than turn it into a second "Word supporter". Without one, this IS the button.
+  if (!window.SimpleWebAuthnBrowser || !window.PublicKeyCredential) {
+    if (hasPatron) { btn.style.display = 'none'; }
+    else { btn.textContent = I.join; btn.addEventListener('click', toLink); }
+    return;
+  }
+
+  btn.addEventListener('click', function () {
+    btn.disabled = true;
+    say(I.confirm);
+    fetch(base + '/paid/challenge?post=' + encodeURIComponent(slug))
+      .then(function (r) { if (!r.ok) throw { link: true }; return r.json(); })
+      .then(function (data) {
+        return window.SimpleWebAuthnBrowser.startAuthentication({ optionsJSON: data.options })
+          .then(function (response) {
+            return fetch(base + '/paid/unlock', {
+              method: 'POST', headers: { 'Content-Type': 'application/json' },
+              body: JSON.stringify({ response: response, blob: data.blob }),
+            });
+          });
+      })
+      .then(function (r) { return r.json().then(function (j) { return { status: r.status, j: j }; }); })
+      .then(function (res) {
+        if (res.j && res.j.ok && res.j.redirect) {
+          // Reload the real post page via the one-shot unlock capability, so it
+          // renders through its normal template (layout, styles, audio).
+          location.href = res.j.redirect;
+        } else if (res.status === 403) {
+          toLink();   // no valid passkey yet (or lapsed tier): link via Patreon
+        } else {
+          btn.disabled = false; say(I.failed, true);
+        }
+      })
+      .catch(function (e) {
+        if (e && e.link) { toLink(); return; }
+        if (e && e.name === 'NotAllowedError') { toLink(); return; }   // cancelled / no passkey -> link
+        btn.disabled = false; say(I.error, true);
+      });
+  });
+})();
Index: src/assets/js/mod/paid-passkey.js
===================================================================
--- src/assets/js/mod/paid-passkey.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
+++ src/assets/js/mod/paid-passkey.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -0,0 +1,41 @@
+// De passkey-ontgrendeling -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script wordt door de CSP geweigerd zodra deze pagina via een link
+// BINNEN de site binnenkomt (shaer-0i6). Servergegevens komen uit pageData();
+// interpolatie kan niet in een statisch bestand.
+
+import { pageData } from './lib.js';
+
+(function () {
+  var _d = pageData();
+  var options = _d.options || {};
+  var blob = _d.blob || "";
+  var I = _d.i18n || {};
+  var postUrl = _d.postUrl || "";
+  var btn = document.getElementById('pk-go');
+  var status = document.getElementById('pk-status');
+  function say(msg, err) { status.hidden = false; status.textContent = msg; status.classList.toggle('is-err', !!err); }
+
+  if (!window.SimpleWebAuthnBrowser || !window.PublicKeyCredential) {
+    btn.disabled = true;
+    say(I.unsupported, true);
+    return;
+  }
+  btn.addEventListener('click', function () {
+    btn.disabled = true;
+    say(I.follow);
+    window.SimpleWebAuthnBrowser.startRegistration({ optionsJSON: options })
+      .then(function (response) {
+        return fetch('/paid/register', {
+          method: 'POST', headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({ response: response, blob: blob }),
+        });
+      })
+      .then(function (r) { return r.json(); })
+      .then(function (j) {
+        if (j && j.ok) { say(I.done); setTimeout(function () { location.href = postUrl; }, 900); }
+        else { btn.disabled = false; say(I.failed.replace('{err}', (j && j.error) || '?'), true); }
+      })
+      .catch(function (e) { btn.disabled = false; say(e && e.name === 'NotAllowedError' ? I.cancelled : I.error, true); });
+  });
+})();
Index: src/routes/admin-media.js
===================================================================
--- src/routes/admin-media.js	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/routes/admin-media.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -81,4 +81,5 @@
     .sort((a, b) => b._mtime - a._mtime); // newest first
   renderPage(req, res, 'pages/admin-media', {
+    pageJs: 'admin-media',
     pageTitleKey: 'admin.t_media',
     bodyClass: 'on-admin',
@@ -122,4 +123,5 @@
   if (!site) return res.status(404).send('Site required');
   renderPage(req, res, 'pages/admin-videos', {
+    pageJs: 'admin-videos',
     pageTitleKey: 'admin.t_media',
     bodyClass: 'on-admin',
Index: src/routes/admin-playlists.js
===================================================================
--- src/routes/admin-playlists.js	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/routes/admin-playlists.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -62,4 +62,5 @@
   const playlists = PlaylistService.list(site.id);
   renderPage(req, res, 'pages/admin-playlists', {
+    pageJs: 'admin-playlists',
     pageTitleKey: 'admin.t_playlists',
     playlists,
Index: src/routes/paid.js
===================================================================
--- src/routes/paid.js	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/routes/paid.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -76,4 +76,5 @@
   const blob = signBlob({ purpose: 'reg', siteId: r.site.id, cents, challenge: options.challenge }, 900);
   renderPage(req, res, 'pages/paid-passkey', {
+    pageJs: 'paid-passkey',
     pageTitleKey: 'ppk.t', bodyClass: 'on-post',
     optionsJson: JSON.stringify(options), regBlob: blob, postSlug: payload.post,
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/routes/posts.js	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -1392,4 +1392,5 @@
     const { newerPost, olderPost } = postNeighbors(site, post);
     return renderPage(req, res, 'pages/paid-gate', {
+    pageJs: 'paid-gate',
       pageTitle: post.title || 'Voor supporters',
       bodyClass: 'on-special',
Index: src/views/pages/admin-media.ejs
===================================================================
--- src/views/pages/admin-media.ejs	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/views/pages/admin-media.ejs	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -41,34 +41,4 @@
 </div>
 
-<script>
-(function () {
-  if (window.__mediaWired) return; window.__mediaWired = true;
-  var T = <%- JSON.stringify({ copy: t('admin.media_copy'), delC: t('admin.media_del_confirm'), cleanC: t('admin.media_cleanup_confirm') }) %>;
-  document.addEventListener('click', function (e) {
-    var c = e.target.closest('[data-copy]');
-    if (c) {
-      var u = location.origin + c.getAttribute('data-copy');
-      var done = function () { var o = c.textContent; c.textContent = '✓'; setTimeout(function () { c.textContent = o === '✓' ? T.copy : o; }, 1200); };
-      if (navigator.clipboard) navigator.clipboard.writeText(u).then(done).catch(function () { window.prompt('URL', u); });
-      else window.prompt('URL', u);
-      return;
-    }
-    var d = e.target.closest('[data-del]');
-    if (d) {
-      if (!window.confirm(T.delC)) return;
-      fetch('/admin/media/delete', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file: d.getAttribute('data-del') }) })
-        .then(function (r) { return r.json(); })
-        .then(function (j) { if (j && j.ok) { var card = d.closest('.media-card'); if (card) card.remove(); } else window.alert((j && j.error) || 'Error'); })
-        .catch(function () { window.alert('Error'); });
-      return;
-    }
-    if (e.target.closest('#media-cleanup')) {
-      if (!window.confirm(T.cleanC)) return;
-      fetch('/admin/media/cleanup', { method: 'POST', credentials: 'same-origin' })
-        .then(function (r) { return r.json(); })
-        .then(function (j) { location.href = '/admin/media?success=' + encodeURIComponent(((j && j.removed) || 0) + ' file(s) removed'); })
-        .catch(function () { window.alert('Error'); });
-    }
-  });
-})();
-</script>
+<%# Het script staat in assets/js/mod/admin-media.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
+<%- include('../partials/page-data', { pageData: { copy: t('admin.media_copy'), delC: t('admin.media_del_confirm'), cleanC: t('admin.media_cleanup_confirm') } }) %>
Index: src/views/pages/admin-playlists.ejs
===================================================================
--- src/views/pages/admin-playlists.ejs	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/views/pages/admin-playlists.ejs	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -222,71 +222,4 @@
 </style>
 
-<script>
-(function() {
-  const csrf = '<%= _csrf %>';
-
-  document.getElementById('pl-new-btn')?.addEventListener('click', () => {
-    if (typeof window.openPlaylistEditor === 'function') {
-      window.openPlaylistEditor({ mode: 'create', onSaved: () => location.reload() });
-    }
-  });
-
-  // Click-to-copy on shortcodes
-  document.querySelectorAll('[data-copy]').forEach(el => {
-    el.addEventListener('click', async () => {
-      try {
-        await navigator.clipboard.writeText(el.dataset.copy);
-        el.classList.add('is-copied');
-        const original = el.textContent;
-        el.textContent = '✓ <%= t('apl.copied') %>';
-        setTimeout(() => { el.classList.remove('is-copied'); el.textContent = original; }, 1200);
-      } catch (_) { /* fall back to selection */ }
-    });
-  });
-
-  document.querySelectorAll('[data-pl-edit]').forEach(btn => {
-    btn.addEventListener('click', () => {
-      if (typeof window.openPlaylistEditor === 'function') {
-        window.openPlaylistEditor({ mode: 'edit', id: btn.dataset.id, onSaved: () => location.reload() });
-      }
-    });
-  });
-
-  document.querySelectorAll('[data-pl-delete]').forEach(btn => {
-    btn.addEventListener('click', async () => {
-      const id = btn.dataset.id;
-      const title = btn.dataset.title || id;
-      if (!confirm('<%= t('apl.delete_confirm') %>'.replace('{title}', title))) return;
-      try {
-        const r = await fetch(`/admin/playlists/api/${encodeURIComponent(id)}/delete`, {
-          method: 'POST',
-          headers: { 'X-CSRF-Token': csrf },
-          credentials: 'same-origin',
-        });
-        const j = await r.json();
-        if (j.ok) location.reload();
-        else alert('<%= t('apl.delete_failed') %>: ' + (j.error || ''));
-      } catch (err) {
-        alert('<%= t('apl.delete_failed') %>: ' + err.message);
-      }
-    });
-  });
-
-  // P52 — deep-link from playlist embed (?edit=<id>) auto-opens the editor.
-  // openPlaylistEditor is defined synchronously by the included partial, so
-  // it's available by the time this IIFE runs.
-  (function deepLinkEdit() {
-    const params = new URLSearchParams(location.search);
-    const editId = params.get('edit');
-    if (!editId) return;
-    if (typeof window.openPlaylistEditor !== 'function') return;
-    // Strip the query param immediately so reload after save doesn't re-open.
-    history.replaceState({}, '', location.pathname);
-    window.openPlaylistEditor({
-      mode: 'edit',
-      id: editId,
-      onSaved: () => location.reload(),
-    });
-  })();
-})();
-</script>
+<%# Het script staat in assets/js/mod/admin-playlists.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
+<%- include('../partials/page-data', { pageData: { csrf: _csrf, copied: t('apl.copied'), delConfirm: t('apl.delete_confirm'), delFailed: t('apl.delete_failed') } }) %>
Index: src/views/pages/admin-videos.ejs
===================================================================
--- src/views/pages/admin-videos.ejs	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/views/pages/admin-videos.ejs	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -40,26 +40,4 @@
 </div>
 
-<script>
-(function () {
-  if (window.__videosWired) return; window.__videosWired = true;
-  var T = <%- JSON.stringify({ copy: t('admin.media_copy'), delC: t('admin.videos_del_confirm') }) %>;
-  document.addEventListener('click', function (e) {
-    var c = e.target.closest('[data-copy]');
-    if (c) {
-      var u = location.origin + c.getAttribute('data-copy');
-      var done = function () { var o = c.textContent; c.textContent = '✓'; setTimeout(function () { c.textContent = o === '✓' ? T.copy : o; }, 1200); };
-      if (navigator.clipboard) navigator.clipboard.writeText(u).then(done).catch(function () { window.prompt('URL', u); });
-      else window.prompt('URL', u);
-      return;
-    }
-    var d = e.target.closest('[data-del]');
-    if (d) {
-      if (!window.confirm(T.delC)) return;
-      fetch('/admin/media/videos/delete', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file: d.getAttribute('data-del') }) })
-        .then(function (r) { return r.json(); })
-        .then(function (j) { if (j && j.ok) { var card = d.closest('.media-card'); if (card) card.remove(); } })
-        .catch(function () {});
-    }
-  });
-})();
-</script>
+<%# Het script staat in assets/js/mod/admin-videos.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
+<%- include('../partials/page-data', { pageData: { copy: t('admin.media_copy'), delC: t('admin.videos_del_confirm') } }) %>
Index: src/views/pages/paid-gate.ejs
===================================================================
--- src/views/pages/paid-gate.ejs	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/views/pages/paid-gate.ejs	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -31,58 +31,5 @@
 
 <script src="/assets/vendor/simplewebauthn-browser.umd.min.js"></script>
-<script>
-(function () {
-  var base = "<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : '') %>";
-  var slug = "<%= pgSlug %>";
-  var hasPatron = <%= _hasPatron ? 'true' : 'false' %>;
-  var I = { join: "<%= t('pgate.join_short') %>", confirm: "<%= t('pgate.confirm') %>", failed: "<%= t('pgate.failed') %>", error: "<%= t('pgate.error') %>" };
-  var btn = document.getElementById('pg-unlock');
-  var status = document.getElementById('pg-status');
-  function say(msg, err) { status.hidden = false; status.textContent = msg; status.classList.toggle('is-err', !!err); }
-  function toLink() { location.href = base + '/paid/link?post=' + encodeURIComponent(slug); }
-
-  // No WebAuthn here: an assertion is impossible. With a Patreon page there is
-  // already a "Word supporter" button, so hide the (dead) unlock button rather
-  // than turn it into a second "Word supporter". Without one, this IS the button.
-  if (!window.SimpleWebAuthnBrowser || !window.PublicKeyCredential) {
-    if (hasPatron) { btn.style.display = 'none'; }
-    else { btn.textContent = I.join; btn.addEventListener('click', toLink); }
-    return;
-  }
-
-  btn.addEventListener('click', function () {
-    btn.disabled = true;
-    say(I.confirm);
-    fetch(base + '/paid/challenge?post=' + encodeURIComponent(slug))
-      .then(function (r) { if (!r.ok) throw { link: true }; return r.json(); })
-      .then(function (data) {
-        return window.SimpleWebAuthnBrowser.startAuthentication({ optionsJSON: data.options })
-          .then(function (response) {
-            return fetch(base + '/paid/unlock', {
-              method: 'POST', headers: { 'Content-Type': 'application/json' },
-              body: JSON.stringify({ response: response, blob: data.blob }),
-            });
-          });
-      })
-      .then(function (r) { return r.json().then(function (j) { return { status: r.status, j: j }; }); })
-      .then(function (res) {
-        if (res.j && res.j.ok && res.j.redirect) {
-          // Reload the real post page via the one-shot unlock capability, so it
-          // renders through its normal template (layout, styles, audio).
-          location.href = res.j.redirect;
-        } else if (res.status === 403) {
-          toLink();   // no valid passkey yet (or lapsed tier): link via Patreon
-        } else {
-          btn.disabled = false; say(I.failed, true);
-        }
-      })
-      .catch(function (e) {
-        if (e && e.link) { toLink(); return; }
-        if (e && e.name === 'NotAllowedError') { toLink(); return; }   // cancelled / no passkey -> link
-        btn.disabled = false; say(I.error, true);
-      });
-  });
-})();
-</script>
+<%# Het script staat in assets/js/mod/paid-gate.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
 
 <style>
@@ -105,2 +52,3 @@
   .pg-status.is-err { color: #c0392b; }
 </style>
+<%- include('../partials/page-data', { pageData: { base: (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : ''), slug: pgSlug, hasPatron: !!_hasPatron, i18n: { join: t('pgate.join_short'), confirm: t('pgate.confirm'), failed: t('pgate.failed'), error: t('pgate.error') } } }) %>
Index: src/views/pages/paid-passkey.ejs
===================================================================
--- src/views/pages/paid-passkey.ejs	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/views/pages/paid-passkey.ejs	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -12,38 +12,5 @@
 
 <script src="/assets/vendor/simplewebauthn-browser.umd.min.js"></script>
-<script>
-(function () {
-  var options = <%- optionsJson %>;
-  var blob = "<%= regBlob %>";
-  var I = { unsupported: "<%= t('ppk.unsupported') %>", follow: "<%= t('ppk.follow') %>", done: "<%= t('ppk.done') %>", failed: "<%= t('ppk.failed') %>", cancelled: "<%= t('ppk.cancelled') %>", error: "<%= t('pgate.error') %>" };
-  var postUrl = "<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : '') %>/<%= postSlug %>";
-  var btn = document.getElementById('pk-go');
-  var status = document.getElementById('pk-status');
-  function say(msg, err) { status.hidden = false; status.textContent = msg; status.classList.toggle('is-err', !!err); }
-
-  if (!window.SimpleWebAuthnBrowser || !window.PublicKeyCredential) {
-    btn.disabled = true;
-    say(I.unsupported, true);
-    return;
-  }
-  btn.addEventListener('click', function () {
-    btn.disabled = true;
-    say(I.follow);
-    window.SimpleWebAuthnBrowser.startRegistration({ optionsJSON: options })
-      .then(function (response) {
-        return fetch('/paid/register', {
-          method: 'POST', headers: { 'Content-Type': 'application/json' },
-          body: JSON.stringify({ response: response, blob: blob }),
-        });
-      })
-      .then(function (r) { return r.json(); })
-      .then(function (j) {
-        if (j && j.ok) { say(I.done); setTimeout(function () { location.href = postUrl; }, 900); }
-        else { btn.disabled = false; say(I.failed.replace('{err}', (j && j.error) || '?'), true); }
-      })
-      .catch(function (e) { btn.disabled = false; say(e && e.name === 'NotAllowedError' ? I.cancelled : I.error, true); });
-  });
-})();
-</script>
+<%# Het script staat in assets/js/mod/paid-passkey.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
 
 <style>
@@ -58,2 +25,3 @@
   .pk-status.is-err { color: #c0392b; }
 </style>
+<%- include('../partials/page-data', { pageData: { options: JSON.parse(optionsJson), blob: regBlob, i18n: { unsupported: t('ppk.unsupported'), follow: t('ppk.follow'), done: t('ppk.done'), failed: t('ppk.failed'), cancelled: t('ppk.cancelled'), error: t('pgate.error') }, postUrl: (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : '') + '/' + postSlug } }) %>
Index: src/views/partials/profile-sheet.ejs
===================================================================
--- src/views/partials/profile-sheet.ejs	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/views/partials/profile-sheet.ejs	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -105,5 +105,5 @@
           <svg class="psi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
           <span class="psi-label"><%= t('nav.theme_label') %></span>
-          <span class="psi-meta" id="profile-sheet-theme-state"><%= t('nav.dark') %></span>
+          <span class="psi-meta" id="profile-sheet-theme-state" data-dark="<%= t('nav.dark') %>" data-light="<%= t('nav.light') %>"><%= t('nav.dark') %></span>
         </button>
       </li>
@@ -326,98 +326,3 @@
 </style>
 
-<script>
-(function() {
-  const sheet = document.getElementById('profile-sheet');
-  if (!sheet) return;
-
-  const backdrop  = document.getElementById('profile-sheet-backdrop');
-  const panel     = sheet.querySelector('.profile-sheet-panel');
-  const closeBtn  = document.getElementById('profile-sheet-close');
-  const dragZone  = document.getElementById('profile-sheet-drag-zone');
-  const themeBtn  = document.getElementById('profile-sheet-theme');
-  const themeLbl  = document.getElementById('profile-sheet-theme-state');
-
-  function openSheet() {
-    sheet.classList.add('is-open');
-    sheet.setAttribute('aria-hidden', 'false');
-    document.body.classList.add('profile-sheet-locked');
-    syncTheme();
-  }
-  function closeSheet() {
-    sheet.classList.remove('is-open');
-    sheet.setAttribute('aria-hidden', 'true');
-    document.body.classList.remove('profile-sheet-locked');
-    panel.style.removeProperty('--pcms-drag-y');
-  }
-
-  // Open: any element with [data-profile-sheet-toggle]
-  document.addEventListener('click', function(e) {
-    const trigger = e.target.closest('[data-profile-sheet-toggle]');
-    if (trigger) {
-      e.preventDefault();
-      openSheet();
-    }
-  });
-
-  // Close: backdrop tap, handle tap, ESC, or any [data-close-sheet] item
-  if (backdrop) backdrop.addEventListener('click', closeSheet);
-  if (closeBtn) closeBtn.addEventListener('click', closeSheet);
-  document.addEventListener('keydown', (e) => {
-    if (e.key === 'Escape' && sheet.classList.contains('is-open')) closeSheet();
-  });
-
-  // Menu items that navigate: close synchronously before nav fires
-  sheet.querySelectorAll('[data-close-sheet]').forEach((el) => {
-    el.addEventListener('click', closeSheet);
-  });
-
-  // Drag-down-to-close on touch.
-  // Uses --pcms-drag-y custom property rather than overwriting the panel's
-  // transform string. This composes correctly with any horizontal centering
-  // (currently none here, but the pattern matches audio-sheet for safety).
-  let startY = 0, lastY = 0, dragging = false;
-  function onDown(e) {
-    if (e.pointerType !== 'touch') return;
-    if (panel.scrollTop > 0) return;
-    startY = lastY = e.clientY;
-    dragging = true;
-    panel.classList.add('is-dragging');
-  }
-  function onMove(e) {
-    if (!dragging) return;
-    lastY = e.clientY;
-    const dy = Math.max(0, lastY - startY);
-    panel.style.setProperty('--pcms-drag-y', dy + 'px');
-  }
-  function onUp() {
-    if (!dragging) return;
-    dragging = false;
-    panel.classList.remove('is-dragging');
-    const dy = lastY - startY;
-    if (dy > 80) closeSheet();
-    else panel.style.removeProperty('--pcms-drag-y');
-  }
-  if (window.PointerEvent && dragZone) {
-    dragZone.addEventListener('pointerdown', onDown);
-    document.addEventListener('pointermove', onMove);
-    document.addEventListener('pointerup', onUp);
-    document.addEventListener('pointercancel', onUp);
-  }
-
-  // Theme toggle inside sheet — syncs label
-  function syncTheme() {
-    if (!themeLbl) return;
-    const t = document.documentElement.getAttribute('data-theme') || 'dark';
-    themeLbl.textContent = t === 'dark' ? '<%= t('nav.dark') %>' : '<%= t('nav.light') %>';
-  }
-  if (themeBtn) {
-    themeBtn.addEventListener('click', function() {
-      const cur = document.documentElement.getAttribute('data-theme') || 'dark';
-      const next = cur === 'dark' ? 'light' : 'dark';
-      document.documentElement.setAttribute('data-theme', next);
-      try { localStorage.setItem('pcms-theme', next); } catch (e) {}
-      syncTheme();
-    });
-  }
-})();
-</script>
+<%# Het script van het profielblad staat in assets/js/mod/chrome.js (shaer-bqr). %>
Index: src/views/partials/topnav.ejs
===================================================================
--- src/views/partials/topnav.ejs	(revision 52fc278d3043952817f815db997e8fad5ed17b3b)
+++ src/views/partials/topnav.ejs	(revision 156baa32c3292b1c6ba350f1fede79d7cbeeea4d)
@@ -176,5 +176,8 @@
 
 <!-- Search overlay (shared by mobile bottom-tab + desktop top-nav) -->
-<div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data-action="<%= _siteUrlBase %>/search">
+<%# De teksten staan op het element dat ze gebruikt, niet in het script: een
+    module is een statisch bestand en kan geen vertaling interpoleren. %>
+<div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data-action="<%= _siteUrlBase %>/search"
+     data-i18n="<%= JSON.stringify({ posts: t('search.section_posts'), tracks: t('search.section_tracks'), events: t('search.section_events'), pages: t('search.section_pages'), all: t('search.suggest_all'), none: t('search.suggest_empty') }) %>">
   <div class="container">
     <form class="search-overlay-form" method="get" action="<%= _siteUrlBase %>/search">
@@ -191,85 +194,5 @@
 </div>
 
-<script>
-(function() {
-  // Wired ONCE. This chrome (topnav) is re-inserted out-of-band during htmx navigation
-  // → without this guard the script would stack EXTRA listeners on every navigation,
-  // causing the theme toggle to fire 2× (or more) = no net change ("toggle stops working").
-  // Everything below uses event delegation on body/document, so it also works for
-  // buttons that appear after this run (OOB).
-  if (window.__pcmsChromeWired) return;
-  window.__pcmsChromeWired = true;
-
-  function toggleTheme() {
-    var cur = document.documentElement.getAttribute('data-theme') || 'dark';
-    var next = cur === 'dark' ? 'light' : 'dark';
-    document.documentElement.setAttribute('data-theme', next);
-    try { localStorage.setItem('pcms-theme', next); } catch (e) {}
-  }
-  function overlay() { return document.getElementById('search-overlay'); }
-  function openSearch() { var o = overlay(); if (o) { o.hidden = false; var i = o.querySelector('input'); if (i) i.focus(); } }
-  function closeSearch() { var o = overlay(); if (o) { o.hidden = true; var b = document.getElementById('search-suggest'); if (b) b.innerHTML = ''; } }
-
-  document.body.addEventListener('click', function(e) {
-    if (e.target.closest('#theme-toggle, #theme-toggle-mobile, #theme-toggle-footer')) { toggleTheme(); return; }
-    if (e.target.closest('#search-toggle')) { openSearch(); return; }
-    if (e.target.closest('#search-close')) { closeSearch(); return; }
-    // Close open dropdowns (user menu + language picker) on click outside.
-    document.querySelectorAll('.user-menu[open], .lang-menu[open]').forEach(function(d) {
-      if (!d.contains(e.target)) d.removeAttribute('open');
-    });
-  });
-
-  document.addEventListener('keydown', function(e) {
-    if (e.key === 'Escape') { var o = overlay(); if (o && !o.hidden) closeSearch(); }
-  });
-
-  // ── Live results while typing ───────────────────────────────────
-  var _ST = {
-    posts: '<%= t('search.section_posts') %>', tracks: '<%= t('search.section_tracks') %>',
-    events: '<%= t('search.section_events') %>', pages: '<%= t('search.section_pages') %>',
-    all: '<%= t('search.suggest_all') %>', none: '<%= t('search.suggest_empty') %>'
-  };
-  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }
-  function renderSuggest(box, d, q, ov) {
-    var html = '';
-    function grp(title, items, fmt) {
-      if (!items || !items.length) return;
-      html += '<div class="ss-group"><div class="ss-title">' + esc(title) + '</div>' + items.map(fmt).join('') + '</div>';
-    }
-    grp(_ST.posts, d.posts, function(p){ return '<a class="ss-item" href="' + esc(p.url) + '">' + esc(p.title) + '</a>'; });
-    grp(_ST.tracks, d.tracks, function(tk){ var sub = tk.artist ? ' <span class="ss-sub">' + esc(tk.artist) + '</span>' : ''; return tk.url ? '<a class="ss-item" href="' + esc(tk.url) + '">' + esc(tk.title) + sub + '</a>' : '<span class="ss-item ss-noclick">' + esc(tk.title) + sub + '</span>'; });
-    grp(_ST.events, d.events, function(ev){ return '<a class="ss-item" href="' + esc(ev.url) + '">' + esc(ev.where || ev.when) + ' <span class="ss-sub">' + esc(ev.when) + '</span></a>'; });
-    grp(_ST.pages, d.pages, function(pg){ return '<a class="ss-item" href="' + esc(pg.url) + '">' + esc(pg.label) + '</a>'; });
-    var hasAny = (d.posts && d.posts.length) || (d.tracks && d.tracks.length) || (d.events && d.events.length) || (d.pages && d.pages.length);
-    if (!hasAny) { box.innerHTML = '<div class="ss-empty">' + esc(_ST.none) + '</div>'; return; }
-    var action = (ov && ov.getAttribute('data-action')) || '/search';
-    html += '<a class="ss-all" href="' + esc(action) + '?q=' + encodeURIComponent(q) + '">' + esc(_ST.all) + '</a>';
-    box.innerHTML = html;
-  }
-  var _sTimer;
-  document.addEventListener('input', function(e) {
-    var inp = e.target.closest && e.target.closest('#search-overlay input[name="q"]');
-    if (!inp) return;
-    var box = document.getElementById('search-suggest');
-    if (!box) return;
-    var q = inp.value.trim();
-    clearTimeout(_sTimer);
-    if (q.length < 2) { box.innerHTML = ''; return; }
-    _sTimer = setTimeout(function() {
-      var ov = document.getElementById('search-overlay');
-      var url = (ov && ov.getAttribute('data-suggest')) || '/search/suggest';
-      fetch(url + '?q=' + encodeURIComponent(q))
-        .then(function(r){ return r.ok ? r.json() : null; })
-        .then(function(d){ if (d) renderSuggest(box, d, q, ov); })
-        .catch(function(){});
-    }, 200);
-  });
-  // Click on a suggestion → close the overlay (the link/boost handles navigation).
-  document.addEventListener('click', function(e) {
-    if (e.target.closest && e.target.closest('#search-suggest a')) { var o = overlay(); if (o) o.hidden = true; }
-  });
-})();
-</script>
+<%# Het script van de topnav staat in assets/js/mod/chrome.js (shaer-bqr). %>
 
 <style>
