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 } }) %>
