Index: src/assets/js/mod/admin-epk.js
===================================================================
--- src/assets/js/mod/admin-epk.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
+++ src/assets/js/mod/admin-epk.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -0,0 +1,21 @@
+// Het EPK-scherm in beheer (pages/admin-epk.ejs) -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script in een pagina wordt door de CSP geweigerd zodra je die pagina via
+// een link BINNEN de site opent: de nonce rouleert per verzoek (shaer-0i6). Dit
+// bestand wordt door de bootstrap in shell.ejs geladen en heeft dat probleem niet.
+//
+// Alles hier hoort GEDELEGEERD te luisteren (op document, niet op een element dat
+// er nu staat) en tegen een tweede aanroep te kunnen.
+
+(function () {
+  var box = document.querySelector('.epk-track-pick');
+  if (!box) return;
+  var max = parseInt(box.dataset.max, 10) || 5;
+  function sync() {
+    var checks = box.querySelectorAll('input[type=checkbox]');
+    var n = box.querySelectorAll('input[type=checkbox]:checked').length;
+    checks.forEach(function (c) { c.disabled = (!c.checked && n >= max); });
+  }
+  box.addEventListener('change', sync);
+  sync();
+})();
Index: src/assets/js/mod/admin-help.js
===================================================================
--- src/assets/js/mod/admin-help.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
+++ src/assets/js/mod/admin-help.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -0,0 +1,30 @@
+// Het hulpscherm in beheer (pages/admin-help.ejs) -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script in een pagina wordt door de CSP geweigerd zodra je die pagina via
+// een link BINNEN de site opent: de nonce rouleert per verzoek (shaer-0i6). Dit
+// bestand wordt door de bootstrap in shell.ejs geladen en heeft dat probleem niet.
+//
+// Alles hier hoort GEDELEGEERD te luisteren (op document, niet op een element dat
+// er nu staat) en tegen een tweede aanroep te kunnen.
+
+(function () {
+  var q = document.getElementById('hb-q');
+  var items = [].slice.call(document.querySelectorAll('.hb-item'));
+  var empty = document.getElementById('hb-empty');
+  var count = document.getElementById('hb-count');
+  if (!q) return;
+  // Bewaar de originele tekst per item (voor highlight-reset).
+  items.forEach(function (it) { it._txt = it.textContent.toLowerCase(); });
+  function run() {
+    var term = q.value.trim().toLowerCase();
+    var shown = 0;
+    items.forEach(function (it) {
+      var hit = !term || it._txt.indexOf(term) >= 0;
+      it.classList.toggle('hb-hidden', !hit);
+      if (hit) shown++;
+    });
+    empty.hidden = shown !== 0;
+    count.textContent = term ? (shown + ' onderwerp' + (shown === 1 ? '' : 'en') + ' gevonden') : '';
+  }
+  q.addEventListener('input', run);
+})();
Index: src/assets/js/mod/admin-paid.js
===================================================================
--- src/assets/js/mod/admin-paid.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
+++ src/assets/js/mod/admin-paid.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -0,0 +1,21 @@
+// Betaalde posts in beheer (pages/admin-paid.ejs) -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script in een pagina wordt door de CSP geweigerd zodra je die pagina via
+// een link BINNEN de site opent: de nonce rouleert per verzoek (shaer-0i6). Dit
+// bestand wordt door de bootstrap in shell.ejs geladen en heeft dat probleem niet.
+//
+// Alles hier hoort GEDELEGEERD te luisteren (op document, niet op een element dat
+// er nu staat) en tegen een tweede aanroep te kunnen.
+
+(function () {
+  var field = document.getElementById('pd-redirect');
+  var btn = document.getElementById('pd-copy');
+  if (!field || !btn) return;
+  field.addEventListener('focus', function () { field.select(); });
+  btn.addEventListener('click', function () {
+    field.select();
+    var done = function () { var t = btn.textContent; btn.textContent = document.getElementById('pd-copy').getAttribute('data-copied'); setTimeout(function () { btn.textContent = t; }, 1400); };
+    if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(field.value).then(done, function () { try { document.execCommand('copy'); done(); } catch (e) {} }); }
+    else { try { document.execCommand('copy'); done(); } catch (e) {} }
+  });
+})();
Index: src/assets/js/mod/admin-push.js
===================================================================
--- src/assets/js/mod/admin-push.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
+++ src/assets/js/mod/admin-push.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -0,0 +1,110 @@
+// Pushmeldingen in beheer (pages/admin-push.ejs) -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script in een pagina wordt door de CSP geweigerd zodra je die pagina via
+// een link BINNEN de site opent: de nonce rouleert per verzoek (shaer-0i6). Dit
+// bestand wordt door de bootstrap in shell.ejs geladen en heeft dat probleem niet.
+//
+// Alles hier hoort GEDELEGEERD te luisteren (op document, niet op een element dat
+// er nu staat) en tegen een tweede aanroep te kunnen.
+
+(function () {
+  var cfg = JSON.parse(document.getElementById('np-data').textContent);
+  var elState = document.getElementById('np-state');
+  var btnOn = document.getElementById('np-on'), btnOff = document.getElementById('np-off'), btnTest = document.getElementById('np-test');
+  var alertsBox = document.getElementById('np-alerts'), savedMsg = document.getElementById('np-saved');
+  var currentEndpoint = null;
+
+  var isIos = /iPad|iPhone|iPod/.test(navigator.userAgent);
+  var standalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;
+  if (isIos && !standalone) document.getElementById('np-ios-hint').hidden = false;
+
+  if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) {
+    document.getElementById('np-unsupported').hidden = false;
+    elState.textContent = cfg.i18n.unsupported;
+    return;
+  }
+
+  function b64ToU8(s) {
+    var pad = '='.repeat((4 - (s.length % 4)) % 4);
+    var raw = atob((s + pad).replace(/-/g, '+').replace(/_/g, '/'));
+    var out = new Uint8Array(raw.length);
+    for (var i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
+    return out;
+  }
+  function post(url, body) {
+    return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
+  }
+  function deviceLabel() {
+    var ua = navigator.userAgent;
+    var browser = /Firefox\//.test(ua) ? 'Firefox' : /Edg\//.test(ua) ? 'Edge' : /Chrome\//.test(ua) ? 'Chrome' : /Safari\//.test(ua) ? 'Safari' : 'Browser';
+    var os = /Android/.test(ua) ? 'Android' : /iPad|iPhone|iPod/.test(ua) ? 'iOS' : /Mac/.test(ua) ? 'macOS' : /Win/.test(ua) ? 'Windows' : /Linux/.test(ua) ? 'Linux' : '';
+    return (browser + (os ? ' op ' + os : ''));
+  }
+  function readAlertBoxes() {
+    var out = {};
+    alertsBox.querySelectorAll('input[data-alert]').forEach(function (cb) { out[cb.getAttribute('data-alert')] = cb.checked ? 1 : 0; });
+    return out;
+  }
+  function setAlertBoxes(alerts) {
+    alertsBox.querySelectorAll('input[data-alert]').forEach(function (cb) {
+      cb.checked = !!alerts[cb.getAttribute('data-alert')];
+    });
+  }
+
+  function render(sub) {
+    currentEndpoint = sub ? sub.endpoint : null;
+    elState.textContent = sub ? cfg.i18n.on : (Notification.permission === 'denied' ? cfg.i18n.denied : cfg.i18n.off);
+    btnOn.hidden = !!sub || Notification.permission === 'denied';
+    btnOff.hidden = !sub;
+    btnTest.hidden = !sub;
+    alertsBox.hidden = !sub;
+  }
+
+  navigator.serviceWorker.ready.then(function (reg) {
+    return reg.pushManager.getSubscription();
+  }).then(function (sub) {
+    // Show this device's SAVED prefs when we know them, defaults otherwise.
+    setAlertBoxes((sub && cfg.saved[sub.endpoint]) ? Object.assign({}, cfg.alerts, cfg.saved[sub.endpoint]) : cfg.alerts);
+    render(sub);
+  }).catch(function () { elState.textContent = cfg.i18n.unknown; });
+
+  btnOn.addEventListener('click', function () {
+    btnOn.disabled = true;
+    Notification.requestPermission().then(function (perm) {
+      if (perm !== 'granted') { btnOn.disabled = false; render(null); return; }
+      navigator.serviceWorker.ready.then(function (reg) {
+        return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: b64ToU8(cfg.vapid) });
+      }).then(function (sub) {
+        return post('/push/subscribe', { subscription: sub.toJSON(), alerts: readAlertBoxes(), uaLabel: deviceLabel() })
+          .then(function (r) { if (!r.ok) throw new Error('subscribe failed'); render(sub); location.reload(); });
+      }).catch(function () { btnOn.disabled = false; elState.textContent = cfg.i18n.failed; });
+    });
+  });
+
+  btnOff.addEventListener('click', function () {
+    navigator.serviceWorker.ready.then(function (reg) { return reg.pushManager.getSubscription(); }).then(function (sub) {
+      if (!sub) { render(null); return; }
+      var ep = sub.endpoint;
+      sub.unsubscribe().then(function () { return post('/push/unsubscribe', { endpoint: ep }); })
+        .then(function () { location.reload(); });
+    });
+  });
+
+  btnTest.addEventListener('click', function () {
+    btnTest.disabled = true;
+    post('/push/test').then(function () { setTimeout(function () { btnTest.disabled = false; }, 2000); });
+  });
+
+  alertsBox.addEventListener('change', function () {
+    if (!currentEndpoint) return;
+    post('/push/alerts', { endpoint: currentEndpoint, alerts: readAlertBoxes() }).then(function (r) {
+      if (r.ok) { savedMsg.hidden = false; setTimeout(function () { savedMsg.hidden = true; }, 1500); }
+    });
+  });
+
+  document.querySelectorAll('.np-remove').forEach(function (btn) {
+    btn.addEventListener('click', function () {
+      post('/push/unsubscribe', { endpoint: btn.getAttribute('data-endpoint') }).then(function () { location.reload(); });
+    });
+  });
+})();
Index: src/assets/js/mod/admin-settings.js
===================================================================
--- src/assets/js/mod/admin-settings.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
+++ src/assets/js/mod/admin-settings.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -0,0 +1,20 @@
+// Instellingen in beheer (pages/admin-settings.ejs) -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script in een pagina wordt door de CSP geweigerd zodra je die pagina via
+// een link BINNEN de site opent: de nonce rouleert per verzoek (shaer-0i6). Dit
+// bestand wordt door de bootstrap in shell.ejs geladen en heeft dat probleem niet.
+//
+// Alles hier hoort GEDELEGEERD te luisteren (op document, niet op een element dat
+// er nu staat) en tegen een tweede aanroep te kunnen.
+
+    (function () {
+      var f = document.getElementById('mode-form');
+      if (!f || f.__age18wired) return; f.__age18wired = true;
+      f.addEventListener('submit', function (e) {
+        var sel = f.querySelector('input[name=mode]:checked');
+        if (sel && sel.value === 'cirkels' && f.getAttribute('data-was-cirkels') === '0') {
+          if (!window.confirm(f.getAttribute('data-confirm'))) e.preventDefault();
+        }
+      });
+    })();
+    
Index: src/assets/js/mod/admin-site-edit.js
===================================================================
--- src/assets/js/mod/admin-site-edit.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
+++ src/assets/js/mod/admin-site-edit.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -0,0 +1,130 @@
+// Site bewerken in beheer (pages/admin-site-edit.ejs) -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script in een pagina wordt door de CSP geweigerd zodra je die pagina via
+// een link BINNEN de site opent: de nonce rouleert per verzoek (shaer-0i6). Dit
+// bestand wordt door de bootstrap in shell.ejs geladen en heeft dat probleem niet.
+//
+// Alles hier hoort GEDELEGEERD te luisteren (op document, niet op een element dat
+// er nu staat) en tegen een tweede aanroep te kunnen.
+
+    (function(){
+      if (window.__themePreviewWired) return; window.__themePreviewWired = true;
+      var html = document.documentElement;
+      function applyAccent(hex){
+        if(!/^#[0-9a-fA-F]{6}$/.test(hex)) return;
+        var sa = document.getElementById('pcms-site-accent');
+        if(!sa){ sa = document.createElement('style'); sa.id = 'pcms-site-accent'; document.head.appendChild(sa); }
+        sa.textContent = ':root,[data-palette]{--accent:'+hex+';--accent-soft:color-mix(in srgb,'+hex+' 80%,white);--accent-tint:color-mix(in srgb,'+hex+' 12%,transparent);}';
+      }
+      document.addEventListener('change', function(e){
+        var t = e.target; if(!t || !t.name) return;
+        if(t.name === 'palette'){ html.setAttribute('data-palette', t.value); }
+        else if(t.name === 'accent'){ applyAccent(t.value); }
+        else if(t.name === 'theme_override'){
+          if(t.value === 'light' || t.value === 'dark'){ html.setAttribute('data-theme', t.value); }
+          else { var dk = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; html.setAttribute('data-theme', dk ? 'dark' : 'light'); }
+        }
+      });
+    })();
+    
+
+// ── volgend blok ──
+
+(function() {
+  // Slug URL field: show the real host as a dimmed prefix (the slug itself is
+  // coloured via CSS). Hub → host/user/<slug>, otherwise host/<slug>.
+  var sh = document.getElementById('slug-host');
+  if (sh) sh.textContent = location.host + (sh.dataset.prefix || '/');
+})();
+(function() {
+  // Profile-links repeater: add row from <template>, remove on click.
+  var rows = document.getElementById('profile-links-rows');
+  var tpl  = document.getElementById('profile-link-template');
+  var add  = document.getElementById('profile-link-add');
+  if (!rows || !tpl || !add) return;
+
+  add.addEventListener('click', function() {
+    var clone = tpl.content.cloneNode(true);
+    rows.appendChild(clone);
+  });
+  rows.addEventListener('click', function(e) {
+    if (e.target && e.target.classList.contains('pl-remove')) {
+      var row = e.target.closest('.profile-link-row');
+      if (row) row.remove();
+    }
+  });
+})();
+
+// P63 — Profile photo picker: upload via /admin/sites/upload-photo,
+// then write the returned URL into the visible input. Live thumb preview.
+(function() {
+  var picker  = document.getElementById('photo-picker');
+  if (!picker) return;
+  var thumb   = document.getElementById('photo-thumb');
+  var preview = document.getElementById('photo-preview');
+  var urlEl   = document.getElementById('photo-url');
+  var trigger = document.getElementById('photo-upload-trigger');
+  var clear   = document.getElementById('photo-clear');
+  var field   = document.getElementById('photo-upload-field');
+  var status  = document.getElementById('photo-status');
+
+  function showPreview(url) {
+    if (url) {
+      preview.src = url;
+      preview.hidden = false;
+      thumb.removeAttribute('data-empty');
+      clear.hidden = false;
+    } else {
+      preview.src = '';
+      preview.hidden = true;
+      thumb.setAttribute('data-empty', '');
+      clear.hidden = true;
+    }
+  }
+
+  if (urlEl) {
+    urlEl.addEventListener('input', function() {
+      showPreview(urlEl.value.trim());
+    });
+  }
+
+  if (trigger && field) {
+    trigger.addEventListener('click', function() { field.click(); });
+    field.addEventListener('change', async function() {
+      var file = field.files && field.files[0];
+      if (!file) return;
+      status.classList.remove('is-error');
+      status.textContent = 'Uploaden…';
+      try {
+        var fd = new FormData();
+        fd.append('photo', file);
+        var r = await fetch('/admin/sites/upload-photo', {
+          method: 'POST',
+          body: fd,
+          credentials: 'same-origin',
+        });
+        var j = await r.json();
+        if (!r.ok || !j.ok) throw new Error(j.error || ('Upload mislukt (' + r.status + ')'));
+        urlEl.value = j.url;
+        showPreview(j.url);
+        status.textContent = 'Geüpload ✓';
+        setTimeout(function() { status.textContent = ''; }, 2000);
+      } catch (e) {
+        status.classList.add('is-error');
+        status.textContent = 'Mislukt: ' + e.message;
+      } finally {
+        field.value = '';
+      }
+    });
+  }
+
+  if (clear) {
+    clear.addEventListener('click', function() {
+      urlEl.value = '';
+      showPreview('');
+      status.textContent = '';
+      // Note: doesn't delete the file from disk — saving the form with empty
+      // URL leaves the file orphaned on the server. Acceptable for now.
+    });
+  }
+})();
Index: src/assets/js/mod/auth-register.js
===================================================================
--- src/assets/js/mod/auth-register.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
+++ src/assets/js/mod/auth-register.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -0,0 +1,19 @@
+// Het registratieformulier (pages/auth-register.ejs) -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script in een pagina wordt door de CSP geweigerd zodra je die pagina via
+// een link BINNEN de site opent: de nonce rouleert per verzoek (shaer-0i6). Dit
+// bestand wordt door de bootstrap in shell.ejs geladen en heeft dat probleem niet.
+//
+// Alles hier hoort GEDELEGEERD te luisteren (op document, niet op een element dat
+// er nu staat) en tegen een tweede aanroep te kunnen.
+
+(function () {
+  var u = document.getElementById('setup-username'), out = document.getElementById('setup-handle');
+  if (!u || !out) return;
+  function upd() {
+    var v = (u.value || '').toLowerCase().replace(/[^a-z0-9_-]/g, '') || 'username';
+    out.textContent = '@' + v + '@' + location.host;
+  }
+  u.addEventListener('input', upd);
+  upd();
+})();
Index: src/assets/js/mod/authorize-interaction.js
===================================================================
--- src/assets/js/mod/authorize-interaction.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
+++ src/assets/js/mod/authorize-interaction.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -0,0 +1,61 @@
+// Interactie autoriseren (pages/authorize-interaction.ejs) -- verplaatst uit inline script, shaer-bqr.
+//
+// Inline script in een pagina wordt door de CSP geweigerd zodra je die pagina via
+// een link BINNEN de site opent: de nonce rouleert per verzoek (shaer-0i6). Dit
+// bestand wordt door de bootstrap in shell.ejs geladen en heeft dat probleem niet.
+//
+// Alles hier hoort GEDELEGEERD te luisteren (op document, niet op een element dat
+// er nu staat) en tegen een tweede aanroep te kunnen.
+
+    (function () {
+      if (window.__fediBmWired) return; window.__fediBmWired = true;
+      var a = document.getElementById('fedi-bm-btn'); if (!a) return;
+      a.setAttribute('href', "javascript:void(window.open('" + location.origin + "/authorize_interaction?uri='+encodeURIComponent(window.location.href)))");
+      a.addEventListener('click', function (e) { e.preventDefault(); a.classList.add('nudge'); setTimeout(function(){ a.classList.remove('nudge'); }, 600); });
+    })();
+    
+
+// ── volgend blok ──
+
+      (function () {
+        if (window.__fediEditWired) return; window.__fediEditWired = true;
+        document.addEventListener('click', function (e) {
+          var b = e.target.closest && e.target.closest('.fedi-edit-btn');
+          if (!b) return;
+          var li = b.closest('.fedi-manage-item'); if (!li) return;
+          var form = li.querySelector('.fedi-edit-form'); if (!form) return;
+          var open = form.classList.toggle('is-open');
+          b.classList.toggle('is-open', open);
+          b.setAttribute('aria-expanded', open ? 'true' : 'false');
+          if (open) { var ed = form.querySelector('.re-editor') || form.querySelector('textarea'); if (ed) ed.focus(); }
+        });
+      })();
+      
+
+// ── volgend blok ──
+
+/* Like/Boost toggle in place — POST via fetch, flip the button, stay on the page. */
+(function(){
+  if (window.__fediReactWired) return; window.__fediReactWired = true;
+  document.addEventListener('submit', function(e){
+    var f = e.target.closest && e.target.closest('.fedi-react-form');
+    if (!f) return;
+    e.preventDefault();
+    var btn = f.querySelector('button'); if (!btn || btn.disabled) return;
+    btn.disabled = true;
+    var body = new URLSearchParams();
+    new FormData(f).forEach(function(v, k){ body.append(k, v); });
+    fetch(f.action, { method: 'POST', body: body, headers: { 'X-Requested-With': 'fetch' }, credentials: 'same-origin' })
+      .then(function(r){ return r.ok ? r.json() : null; })
+      .then(function(j){
+        if (j) {
+          var on = !!j.on;
+          btn.classList.toggle('is-on', on);
+          var lbl = btn.querySelector('.fedi-bigact-label');
+          if (lbl) lbl.textContent = on ? (btn.getAttribute('data-on') || lbl.textContent) : (btn.getAttribute('data-off') || lbl.textContent);
+        }
+      })
+      .catch(function(){})
+      .then(function(){ btn.disabled = false; });
+  });
+})();
Index: src/routes/admin-epk.js
===================================================================
--- src/routes/admin-epk.js	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/routes/admin-epk.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -33,4 +33,5 @@
   try { const r = JSON.parse(getSetting('epk_tracks_' + site.id, '') || '[]'); if (Array.isArray(r)) chosen = r; } catch (e) {}
   renderPage(req, res, 'pages/admin-epk', {
+    pageJs: 'admin-epk',
     pageTitleKey: 'admin.t_epk',
     bodyClass: 'on-admin',
Index: src/routes/admin-paid.js
===================================================================
--- src/routes/admin-paid.js	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/routes/admin-paid.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -36,4 +36,5 @@
   if (!gate(req, res)) return;
   renderPage(req, res, 'pages/admin-paid', {
+    pageJs: 'admin-paid',
     pageTitleKey: 'apaid.t',
     bodyClass: 'on-admin',
Index: src/routes/admin-push.js
===================================================================
--- src/routes/admin-push.js	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/routes/admin-push.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -12,4 +12,5 @@
 router.get('/', requireSiteManager, async (req, res) => {
   renderPage(req, res, 'pages/admin-push', {
+    pageJs: 'admin-push',
     pageTitleKey: 'push.t',
     bodyClass: 'on-admin',
Index: src/routes/admin-settings.js
===================================================================
--- src/routes/admin-settings.js	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/routes/admin-settings.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -66,4 +66,5 @@
 router.get('/', requireGod, (req, res) => {
   renderPage(req, res, 'pages/admin-settings', {
+    pageJs: 'admin-settings',
     pageTitleKey: 'admin.t_settings',
     bodyClass: 'on-admin',
Index: src/routes/admin-sites.js
===================================================================
--- src/routes/admin-sites.js	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/routes/admin-sites.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -200,4 +200,5 @@
 router.get('/new', requireGod, (req, res) => {
   renderPage(req, res, 'pages/admin-site-edit', {
+    pageJs: 'admin-site-edit',
     pageTitleKey: 'admin.t_newsite',
     bodyClass: 'on-admin',
@@ -283,4 +284,5 @@
 
   renderPage(req, res, 'pages/admin-site-edit', {
+    pageJs: 'admin-site-edit',
     pageTitleKey: 'admin.t_editsite', pageTitleVars: { title: site.title },
     bodyClass: 'on-admin',
Index: src/routes/admin.js
===================================================================
--- src/routes/admin.js	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/routes/admin.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -87,4 +87,5 @@
 router.get('/handleiding', requireAuth, (req, res) => {
   renderPage(req, res, 'pages/admin-help', {
+    pageJs: 'admin-help',
     pageTitleKey: 'admin.t_manual',
     bodyClass: 'on-admin',
Index: src/routes/auth.js
===================================================================
--- src/routes/auth.js	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/routes/auth.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -115,4 +115,5 @@
   if (!isSetupMode()) return res.redirect('/auth/login' + (next ? '?next=' + encodeURIComponent(next) : ''));
   renderPage(req, res, 'pages/auth-register', {
+    pageJs: 'auth-register',
     pageTitle: t(resolveLang(req), 'setup.title'), bodyClass: 'on-special',
     error: null, username: '', email: '', siteName: '', next,
@@ -124,4 +125,5 @@
   const next = safeNext(req.body.next) || '';
   const renderErr = (error) => renderPage(req, res, 'pages/auth-register', {
+    pageJs: 'auth-register',
     pageTitle: t(resolveLang(req), 'setup.title'), bodyClass: 'on-special',
     error, username: username || '', email: email || '', siteName: siteName || '', next,
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/routes/posts.js	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -795,4 +795,5 @@
   }
   renderPage(req, res, 'pages/authorize-interaction', {
+    pageJs: 'authorize-interaction',
     pageTitleKey: 'fedi.remote_interact', // i18n: was hardcoded Dutch on non-NL sites
     bodyClass: 'on-special',
Index: src/views/pages/admin-epk.ejs
===================================================================
--- src/views/pages/admin-epk.ejs	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/views/pages/admin-epk.ejs	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -90,16 +90,3 @@
   .admin-page .epk-pick-item input:disabled ~ .epk-pick-title { opacity: 0.45; }
 </style>
-<script>
-(function () {
-  var box = document.querySelector('.epk-track-pick');
-  if (!box) return;
-  var max = parseInt(box.dataset.max, 10) || 5;
-  function sync() {
-    var checks = box.querySelectorAll('input[type=checkbox]');
-    var n = box.querySelectorAll('input[type=checkbox]:checked').length;
-    checks.forEach(function (c) { c.disabled = (!c.checked && n >= max); });
-  }
-  box.addEventListener('change', sync);
-  sync();
-})();
-</script>
+<%# Het script van deze pagina staat in assets/js/mod/admin-epk.js (shaer-bqr). %>
Index: src/views/pages/admin-help.ejs
===================================================================
--- src/views/pages/admin-help.ejs	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/views/pages/admin-help.ejs	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -145,25 +145,3 @@
   .hb-empty { color: var(--ink-muted, var(--ink-soft)); padding: 1rem; text-align: center; }
 </style>
-<script>
-(function () {
-  var q = document.getElementById('hb-q');
-  var items = [].slice.call(document.querySelectorAll('.hb-item'));
-  var empty = document.getElementById('hb-empty');
-  var count = document.getElementById('hb-count');
-  if (!q) return;
-  // Bewaar de originele tekst per item (voor highlight-reset).
-  items.forEach(function (it) { it._txt = it.textContent.toLowerCase(); });
-  function run() {
-    var term = q.value.trim().toLowerCase();
-    var shown = 0;
-    items.forEach(function (it) {
-      var hit = !term || it._txt.indexOf(term) >= 0;
-      it.classList.toggle('hb-hidden', !hit);
-      if (hit) shown++;
-    });
-    empty.hidden = shown !== 0;
-    count.textContent = term ? (shown + ' onderwerp' + (shown === 1 ? '' : 'en') + ' gevonden') : '';
-  }
-  q.addEventListener('input', run);
-})();
-</script>
+<%# Het script van deze pagina staat in assets/js/mod/admin-help.js (shaer-bqr). %>
Index: src/views/pages/admin-paid.ejs
===================================================================
--- src/views/pages/admin-paid.ejs	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/views/pages/admin-paid.ejs	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -72,16 +72,3 @@
 </div>
 
-<script>
-(function () {
-  var field = document.getElementById('pd-redirect');
-  var btn = document.getElementById('pd-copy');
-  if (!field || !btn) return;
-  field.addEventListener('focus', function () { field.select(); });
-  btn.addEventListener('click', function () {
-    field.select();
-    var done = function () { var t = btn.textContent; btn.textContent = document.getElementById('pd-copy').getAttribute('data-copied'); setTimeout(function () { btn.textContent = t; }, 1400); };
-    if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(field.value).then(done, function () { try { document.execCommand('copy'); done(); } catch (e) {} }); }
-    else { try { document.execCommand('copy'); done(); } catch (e) {} }
-  });
-})();
-</script>
+<%# Het script van deze pagina staat in assets/js/mod/admin-paid.js (shaer-bqr). %>
Index: src/views/pages/admin-push.ejs
===================================================================
--- src/views/pages/admin-push.ejs	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/views/pages/admin-push.ejs	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -58,106 +58,4 @@
   i18n: { on: t('push.state_on'), off: t('push.state_off'), denied: t('push.state_denied'), unknown: t('push.state_unknown'), unsupported: t('push.state_unsupported'), failed: t('push.enable_failed') },
 }).replace(/</g, '\\u003c') %></script>
-<script>
-(function () {
-  var cfg = JSON.parse(document.getElementById('np-data').textContent);
-  var elState = document.getElementById('np-state');
-  var btnOn = document.getElementById('np-on'), btnOff = document.getElementById('np-off'), btnTest = document.getElementById('np-test');
-  var alertsBox = document.getElementById('np-alerts'), savedMsg = document.getElementById('np-saved');
-  var currentEndpoint = null;
-
-  var isIos = /iPad|iPhone|iPod/.test(navigator.userAgent);
-  var standalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;
-  if (isIos && !standalone) document.getElementById('np-ios-hint').hidden = false;
-
-  if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) {
-    document.getElementById('np-unsupported').hidden = false;
-    elState.textContent = cfg.i18n.unsupported;
-    return;
-  }
-
-  function b64ToU8(s) {
-    var pad = '='.repeat((4 - (s.length % 4)) % 4);
-    var raw = atob((s + pad).replace(/-/g, '+').replace(/_/g, '/'));
-    var out = new Uint8Array(raw.length);
-    for (var i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
-    return out;
-  }
-  function post(url, body) {
-    return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
-  }
-  function deviceLabel() {
-    var ua = navigator.userAgent;
-    var browser = /Firefox\//.test(ua) ? 'Firefox' : /Edg\//.test(ua) ? 'Edge' : /Chrome\//.test(ua) ? 'Chrome' : /Safari\//.test(ua) ? 'Safari' : 'Browser';
-    var os = /Android/.test(ua) ? 'Android' : /iPad|iPhone|iPod/.test(ua) ? 'iOS' : /Mac/.test(ua) ? 'macOS' : /Win/.test(ua) ? 'Windows' : /Linux/.test(ua) ? 'Linux' : '';
-    return (browser + (os ? ' op ' + os : ''));
-  }
-  function readAlertBoxes() {
-    var out = {};
-    alertsBox.querySelectorAll('input[data-alert]').forEach(function (cb) { out[cb.getAttribute('data-alert')] = cb.checked ? 1 : 0; });
-    return out;
-  }
-  function setAlertBoxes(alerts) {
-    alertsBox.querySelectorAll('input[data-alert]').forEach(function (cb) {
-      cb.checked = !!alerts[cb.getAttribute('data-alert')];
-    });
-  }
-
-  function render(sub) {
-    currentEndpoint = sub ? sub.endpoint : null;
-    elState.textContent = sub ? cfg.i18n.on : (Notification.permission === 'denied' ? cfg.i18n.denied : cfg.i18n.off);
-    btnOn.hidden = !!sub || Notification.permission === 'denied';
-    btnOff.hidden = !sub;
-    btnTest.hidden = !sub;
-    alertsBox.hidden = !sub;
-  }
-
-  navigator.serviceWorker.ready.then(function (reg) {
-    return reg.pushManager.getSubscription();
-  }).then(function (sub) {
-    // Show this device's SAVED prefs when we know them, defaults otherwise.
-    setAlertBoxes((sub && cfg.saved[sub.endpoint]) ? Object.assign({}, cfg.alerts, cfg.saved[sub.endpoint]) : cfg.alerts);
-    render(sub);
-  }).catch(function () { elState.textContent = cfg.i18n.unknown; });
-
-  btnOn.addEventListener('click', function () {
-    btnOn.disabled = true;
-    Notification.requestPermission().then(function (perm) {
-      if (perm !== 'granted') { btnOn.disabled = false; render(null); return; }
-      navigator.serviceWorker.ready.then(function (reg) {
-        return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: b64ToU8(cfg.vapid) });
-      }).then(function (sub) {
-        return post('/push/subscribe', { subscription: sub.toJSON(), alerts: readAlertBoxes(), uaLabel: deviceLabel() })
-          .then(function (r) { if (!r.ok) throw new Error('subscribe failed'); render(sub); location.reload(); });
-      }).catch(function () { btnOn.disabled = false; elState.textContent = cfg.i18n.failed; });
-    });
-  });
-
-  btnOff.addEventListener('click', function () {
-    navigator.serviceWorker.ready.then(function (reg) { return reg.pushManager.getSubscription(); }).then(function (sub) {
-      if (!sub) { render(null); return; }
-      var ep = sub.endpoint;
-      sub.unsubscribe().then(function () { return post('/push/unsubscribe', { endpoint: ep }); })
-        .then(function () { location.reload(); });
-    });
-  });
-
-  btnTest.addEventListener('click', function () {
-    btnTest.disabled = true;
-    post('/push/test').then(function () { setTimeout(function () { btnTest.disabled = false; }, 2000); });
-  });
-
-  alertsBox.addEventListener('change', function () {
-    if (!currentEndpoint) return;
-    post('/push/alerts', { endpoint: currentEndpoint, alerts: readAlertBoxes() }).then(function (r) {
-      if (r.ok) { savedMsg.hidden = false; setTimeout(function () { savedMsg.hidden = true; }, 1500); }
-    });
-  });
-
-  document.querySelectorAll('.np-remove').forEach(function (btn) {
-    btn.addEventListener('click', function () {
-      post('/push/unsubscribe', { endpoint: btn.getAttribute('data-endpoint') }).then(function () { location.reload(); });
-    });
-  });
-})();
-</script>
+<%# Het script van deze pagina staat in assets/js/mod/admin-push.js (shaer-bqr). %>
 <% } %>
Index: src/views/pages/admin-settings.ejs
===================================================================
--- src/views/pages/admin-settings.ejs	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/views/pages/admin-settings.ejs	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -27,16 +27,5 @@
       <div class="set-actions"><button type="submit" class="btn btn-primary"><%= t('aset.save') %></button></div>
     </form>
-    <script>
-    (function () {
-      var f = document.getElementById('mode-form');
-      if (!f || f.__age18wired) return; f.__age18wired = true;
-      f.addEventListener('submit', function (e) {
-        var sel = f.querySelector('input[name=mode]:checked');
-        if (sel && sel.value === 'cirkels' && f.getAttribute('data-was-cirkels') === '0') {
-          if (!window.confirm(f.getAttribute('data-confirm'))) e.preventDefault();
-        }
-      });
-    })();
-    </script>
+    <%# Het script van deze pagina staat in assets/js/mod/admin-settings.js (shaer-bqr). %>
   </section>
 
Index: src/views/pages/admin-site-edit.ejs
===================================================================
--- src/views/pages/admin-site-edit.ejs	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/views/pages/admin-site-edit.ejs	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -126,25 +126,5 @@
     <%# Live preview: selecting an accent / theme / palette re-themes this page
         instantly (reverts on reload; persists on save). %>
-    <script>
-    (function(){
-      if (window.__themePreviewWired) return; window.__themePreviewWired = true;
-      var html = document.documentElement;
-      function applyAccent(hex){
-        if(!/^#[0-9a-fA-F]{6}$/.test(hex)) return;
-        var sa = document.getElementById('pcms-site-accent');
-        if(!sa){ sa = document.createElement('style'); sa.id = 'pcms-site-accent'; document.head.appendChild(sa); }
-        sa.textContent = ':root,[data-palette]{--accent:'+hex+';--accent-soft:color-mix(in srgb,'+hex+' 80%,white);--accent-tint:color-mix(in srgb,'+hex+' 12%,transparent);}';
-      }
-      document.addEventListener('change', function(e){
-        var t = e.target; if(!t || !t.name) return;
-        if(t.name === 'palette'){ html.setAttribute('data-palette', t.value); }
-        else if(t.name === 'accent'){ applyAccent(t.value); }
-        else if(t.name === 'theme_override'){
-          if(t.value === 'light' || t.value === 'dark'){ html.setAttribute('data-theme', t.value); }
-          else { var dk = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; html.setAttribute('data-theme', dk ? 'dark' : 'light'); }
-        }
-      });
-    })();
-    </script>
+    <%# Het script van deze pagina staat in assets/js/mod/admin-site-edit.js (shaer-bqr). %>
 
     <fieldset>
@@ -275,104 +255,5 @@
 </div>
 
-<script>
-(function() {
-  // Slug URL field: show the real host as a dimmed prefix (the slug itself is
-  // coloured via CSS). Hub → host/user/<slug>, otherwise host/<slug>.
-  var sh = document.getElementById('slug-host');
-  if (sh) sh.textContent = location.host + (sh.dataset.prefix || '/');
-})();
-(function() {
-  // Profile-links repeater: add row from <template>, remove on click.
-  var rows = document.getElementById('profile-links-rows');
-  var tpl  = document.getElementById('profile-link-template');
-  var add  = document.getElementById('profile-link-add');
-  if (!rows || !tpl || !add) return;
-
-  add.addEventListener('click', function() {
-    var clone = tpl.content.cloneNode(true);
-    rows.appendChild(clone);
-  });
-  rows.addEventListener('click', function(e) {
-    if (e.target && e.target.classList.contains('pl-remove')) {
-      var row = e.target.closest('.profile-link-row');
-      if (row) row.remove();
-    }
-  });
-})();
-
-// P63 — Profile photo picker: upload via /admin/sites/upload-photo,
-// then write the returned URL into the visible input. Live thumb preview.
-(function() {
-  var picker  = document.getElementById('photo-picker');
-  if (!picker) return;
-  var thumb   = document.getElementById('photo-thumb');
-  var preview = document.getElementById('photo-preview');
-  var urlEl   = document.getElementById('photo-url');
-  var trigger = document.getElementById('photo-upload-trigger');
-  var clear   = document.getElementById('photo-clear');
-  var field   = document.getElementById('photo-upload-field');
-  var status  = document.getElementById('photo-status');
-
-  function showPreview(url) {
-    if (url) {
-      preview.src = url;
-      preview.hidden = false;
-      thumb.removeAttribute('data-empty');
-      clear.hidden = false;
-    } else {
-      preview.src = '';
-      preview.hidden = true;
-      thumb.setAttribute('data-empty', '');
-      clear.hidden = true;
-    }
-  }
-
-  if (urlEl) {
-    urlEl.addEventListener('input', function() {
-      showPreview(urlEl.value.trim());
-    });
-  }
-
-  if (trigger && field) {
-    trigger.addEventListener('click', function() { field.click(); });
-    field.addEventListener('change', async function() {
-      var file = field.files && field.files[0];
-      if (!file) return;
-      status.classList.remove('is-error');
-      status.textContent = 'Uploaden…';
-      try {
-        var fd = new FormData();
-        fd.append('photo', file);
-        var r = await fetch('/admin/sites/upload-photo', {
-          method: 'POST',
-          body: fd,
-          credentials: 'same-origin',
-        });
-        var j = await r.json();
-        if (!r.ok || !j.ok) throw new Error(j.error || ('Upload mislukt (' + r.status + ')'));
-        urlEl.value = j.url;
-        showPreview(j.url);
-        status.textContent = 'Geüpload ✓';
-        setTimeout(function() { status.textContent = ''; }, 2000);
-      } catch (e) {
-        status.classList.add('is-error');
-        status.textContent = 'Mislukt: ' + e.message;
-      } finally {
-        field.value = '';
-      }
-    });
-  }
-
-  if (clear) {
-    clear.addEventListener('click', function() {
-      urlEl.value = '';
-      showPreview('');
-      status.textContent = '';
-      // Note: doesn't delete the file from disk — saving the form with empty
-      // URL leaves the file orphaned on the server. Acceptable for now.
-    });
-  }
-})();
-</script>
+
 
 <style>
Index: src/views/pages/auth-register.ejs
===================================================================
--- src/views/pages/auth-register.ejs	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/views/pages/auth-register.ejs	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -41,14 +41,3 @@
   .setup-handle-hint code { color: var(--accent); font-weight: 600; background: color-mix(in srgb, var(--accent) 12%, transparent); padding: .05rem .35rem; border-radius: 6px; white-space: nowrap; }
 </style>
-<script>
-(function () {
-  var u = document.getElementById('setup-username'), out = document.getElementById('setup-handle');
-  if (!u || !out) return;
-  function upd() {
-    var v = (u.value || '').toLowerCase().replace(/[^a-z0-9_-]/g, '') || 'username';
-    out.textContent = '@' + v + '@' + location.host;
-  }
-  u.addEventListener('input', upd);
-  upd();
-})();
-</script>
+<%# Het script van deze pagina staat in assets/js/mod/auth-register.js (shaer-bqr). %>
Index: src/views/pages/authorize-interaction.ejs
===================================================================
--- src/views/pages/authorize-interaction.ejs	(revision 792af539e3b665ec03cb8927cd16b3af858defec)
+++ src/views/pages/authorize-interaction.ejs	(revision 0475b134ee78a96bfc7991c65992ddb6eb3d0f19)
@@ -12,12 +12,5 @@
       <p class="fedi-bm-help"><%= t('fedi.bm_help') %></p>
     </div>
-    <script>
-    (function () {
-      if (window.__fediBmWired) return; window.__fediBmWired = true;
-      var a = document.getElementById('fedi-bm-btn'); if (!a) return;
-      a.setAttribute('href', "javascript:void(window.open('" + location.origin + "/authorize_interaction?uri='+encodeURIComponent(window.location.href)))");
-      a.addEventListener('click', function (e) { e.preventDefault(); a.classList.add('nudge'); setTimeout(function(){ a.classList.remove('nudge'); }, 600); });
-    })();
-    </script>
+    <%# Het script van deze pagina staat in assets/js/mod/authorize-interaction.js (shaer-bqr). %>
     <% if (!manage.length) { %>
       <p class="auth-interact-note"><%= t('fedi.manage_empty') %></p>
@@ -56,19 +49,5 @@
         <% }); %>
       </ul>
-      <script>
-      (function () {
-        if (window.__fediEditWired) return; window.__fediEditWired = true;
-        document.addEventListener('click', function (e) {
-          var b = e.target.closest && e.target.closest('.fedi-edit-btn');
-          if (!b) return;
-          var li = b.closest('.fedi-manage-item'); if (!li) return;
-          var form = li.querySelector('.fedi-edit-form'); if (!form) return;
-          var open = form.classList.toggle('is-open');
-          b.classList.toggle('is-open', open);
-          b.setAttribute('aria-expanded', open ? 'true' : 'false');
-          if (open) { var ed = form.querySelector('.re-editor') || form.querySelector('textarea'); if (ed) ed.focus(); }
-        });
-      })();
-      </script>
+      
     <% } %>
     <p class="auth-interact-note"><a href="/"><%= t('fedi.remote_back') %></a></p>
@@ -331,29 +310,3 @@
 </style>
 
-<script>
-/* Like/Boost toggle in place — POST via fetch, flip the button, stay on the page. */
-(function(){
-  if (window.__fediReactWired) return; window.__fediReactWired = true;
-  document.addEventListener('submit', function(e){
-    var f = e.target.closest && e.target.closest('.fedi-react-form');
-    if (!f) return;
-    e.preventDefault();
-    var btn = f.querySelector('button'); if (!btn || btn.disabled) return;
-    btn.disabled = true;
-    var body = new URLSearchParams();
-    new FormData(f).forEach(function(v, k){ body.append(k, v); });
-    fetch(f.action, { method: 'POST', body: body, headers: { 'X-Requested-With': 'fetch' }, credentials: 'same-origin' })
-      .then(function(r){ return r.ok ? r.json() : null; })
-      .then(function(j){
-        if (j) {
-          var on = !!j.on;
-          btn.classList.toggle('is-on', on);
-          var lbl = btn.querySelector('.fedi-bigact-label');
-          if (lbl) lbl.textContent = on ? (btn.getAttribute('data-on') || lbl.textContent) : (btn.getAttribute('data-off') || lbl.textContent);
-        }
-      })
-      .catch(function(){})
-      .then(function(){ btn.disabled = false; });
-  });
-})();
-</script>
+
