Index: src/assets/js/mod/admin-audio.js
===================================================================
--- src/assets/js/mod/admin-audio.js	(revision c1df3e9e512d5945947d452659529f8866f65d9b)
+++ src/assets/js/mod/admin-audio.js	(revision 4b5db37be989c52df56e680e3acb98ee0cd84ac0)
@@ -5,7 +5,18 @@
 // link BINNEN de site binnenkomt (shaer-0i6).
 
-import { pageData } from './lib.js';
-
-  const T = pageData();
+import { pageData, makeSweeper } from './lib.js';
+
+// Zie post-edit.js: init() per paginawissel, de veger haalt de window-
+// listeners van de vorige lichting weg (shaer-5s1).
+const doc = makeSweeper();
+let T = {};
+
+export function init() {
+  doc.sweep();
+  T = pageData();
+  run();
+}
+
+function run() {
 
 (function() {
@@ -197,5 +208,5 @@
   // explicitly handles them.
   ['dragover', 'drop'].forEach(ev => {
-    window.addEventListener(ev, e => {
+    doc.on(window, ev, e => {
       // Allow drops INSIDE the dropzone — its own listener handles those.
       if (dropzone.contains(e.target)) return;
@@ -441,2 +452,3 @@
   });
 })();
+}
Index: src/assets/js/mod/admin-media.js
===================================================================
--- src/assets/js/mod/admin-media.js	(revision c1df3e9e512d5945947d452659529f8866f65d9b)
+++ src/assets/js/mod/admin-media.js	(revision 4b5db37be989c52df56e680e3acb98ee0cd84ac0)
@@ -9,6 +9,13 @@
 (function () {
   if (window.__mediaWired) return; window.__mediaWired = true;
-  var T = pageData();
+  // Gedelegeerd op document, dus dit overleeft elke paginawissel -- ook op
+  // pagina's waar [data-copy] iets ANDERS betekent (audio en playlists: een
+  // shortcode, geen pad). Zonder deze wacht kaapt dit daar de kopieerklik en
+  // plakt er een origin voor (shaer-5s1). T per klik gelezen, want pageData
+  // wisselt mee met de pagina.
+  function active() { return (document.body.getAttribute('data-js') || '').split(/\s+/).indexOf('admin-media') !== -1; }
   document.addEventListener('click', function (e) {
+    if (!active()) return;
+    var T = pageData();
     var c = e.target.closest('[data-copy]');
     if (c) {
Index: src/assets/js/mod/admin-playlists.js
===================================================================
--- src/assets/js/mod/admin-playlists.js	(revision c1df3e9e512d5945947d452659529f8866f65d9b)
+++ src/assets/js/mod/admin-playlists.js	(revision 4b5db37be989c52df56e680e3acb98ee0cd84ac0)
@@ -6,4 +6,8 @@
 import { pageData } from './lib.js';
 
+// Element-bedrading per render, dus init() per paginawissel (shaer-5s1).
+export function init() { run(); }
+
+function run() {
 (function() {
   const _d = pageData();
@@ -74,2 +78,3 @@
   })();
 })();
+}
Index: src/assets/js/mod/admin-videos.js
===================================================================
--- src/assets/js/mod/admin-videos.js	(revision c1df3e9e512d5945947d452659529f8866f65d9b)
+++ src/assets/js/mod/admin-videos.js	(revision 4b5db37be989c52df56e680e3acb98ee0cd84ac0)
@@ -9,6 +9,13 @@
 (function () {
   if (window.__videosWired) return; window.__videosWired = true;
-  var T = pageData();
+  // Gedelegeerd op document, dus dit overleeft elke paginawissel -- ook op
+  // pagina's waar [data-copy] iets ANDERS betekent (audio en playlists: een
+  // shortcode, geen pad). Zonder deze wacht kaapt dit daar de kopieerklik en
+  // plakt er een origin voor (shaer-5s1). T per klik gelezen, want pageData
+  // wisselt mee met de pagina.
+  function active() { return (document.body.getAttribute('data-js') || '').split(/\s+/).indexOf('admin-videos') !== -1; }
   document.addEventListener('click', function (e) {
+    if (!active()) return;
+    var T = pageData();
     var c = e.target.closest('[data-copy]');
     if (c) {
Index: src/assets/js/mod/lib.js
===================================================================
--- src/assets/js/mod/lib.js	(revision c1df3e9e512d5945947d452659529f8866f65d9b)
+++ src/assets/js/mod/lib.js	(revision 4b5db37be989c52df56e680e3acb98ee0cd84ac0)
@@ -35,4 +35,21 @@
 
 /**
+ * Listeners op document en window overleven een paginawissel; de pagina zelf
+ * niet. Een module die per navigatie opnieuw start (export init) registreert
+ * zulke listeners hier, en veegt bij elke start de vorige lichting weg --
+ * anders stapelen ze, elk met een closure naar elementen die al weg zijn.
+ *
+ * Listeners op ELEMENTEN hoeven hier niet doorheen: die sterven met hun
+ * element bij de swap. Alleen wat het document overleeft, moet geveegd.
+ */
+export function makeSweeper() {
+  let list = [];
+  return {
+    on(target, ev, fn, opts) { target.addEventListener(ev, fn, opts); list.push([target, ev, fn, opts]); },
+    sweep() { for (const [t, ev, fn, opts] of list) t.removeEventListener(ev, fn, opts); list = []; },
+  };
+}
+
+/**
  * Een tekst veilig in HTML zetten.
  *
Index: src/assets/js/mod/post-edit.js
===================================================================
--- src/assets/js/mod/post-edit.js	(revision c1df3e9e512d5945947d452659529f8866f65d9b)
+++ src/assets/js/mod/post-edit.js	(revision 4b5db37be989c52df56e680e3acb98ee0cd84ac0)
@@ -5,7 +5,23 @@
 // het houdt een apostrof in een vertaling uit de HTML die hier geplakt wordt.
 
-import { pageData, esc } from './lib.js';
-
-const T = pageData();
+import { pageData, esc, makeSweeper } from './lib.js';
+
+// De oude inline scripts draaiden bij ELKE render; een module draait zijn
+// top-level een keer per sessie. Vandaar init(): de bootstrap roept hem aan
+// bij elke paginawissel waarop deze module actief is, en de veger haalt eerst
+// de document/window-listeners van de vorige pagina weg -- die overleven de
+// swap, met closures naar elementen die al verdwenen zijn (shaer-5s1).
+const doc = makeSweeper();
+let T = {};
+let _barObserver = null;
+
+export function init() {
+  doc.sweep();
+  if (_barObserver) { _barObserver.disconnect(); _barObserver = null; }
+  T = pageData();
+  run();
+}
+
+function run() {
 
           (function () {
@@ -631,9 +647,9 @@
   }
   function toggleFullscreen() { if (isFs()) closeFs(); else openFs(); }
-  window.addEventListener('popstate', function () { if (isFs()) applyFs(false); });
+  doc.on(window, 'popstate', function () { if (isFs()) applyFs(false); });
   if (fsBtn) fsBtn.addEventListener('click', toggleFullscreen);
   var fsDoneBtn = document.getElementById('pe-fs-done');
   if (fsDoneBtn) fsDoneBtn.addEventListener('click', closeFs);
-  document.addEventListener('keydown', (e) => {
+  doc.on(document, 'keydown', (e) => {
     if (e.key === 'Escape' && isFs()) { e.preventDefault(); closeFs(); }
   });
@@ -663,5 +679,5 @@
     if (bqBtn) bqBtn.classList.toggle('is-active', !!blockquoteAncestor());
   }
-  document.addEventListener('selectionchange', () => {
+  doc.on(document, 'selectionchange', () => {
     if (document.activeElement === editor) updateToolbarState();
   });
@@ -884,5 +900,5 @@
       }
     });
-    document.addEventListener('keydown', (e) => {
+    doc.on(document, 'keydown', (e) => {
       if (!trackPicker.hidden && e.key === 'Escape') {
         e.preventDefault();
@@ -1092,7 +1108,9 @@
   }
   position();
-  window.addEventListener('resize', position);
-  if (vv) { vv.addEventListener('resize', position); vv.addEventListener('scroll', position); }
-  // has-audio-player is toggled via a body class → observe it.
-  try { new MutationObserver(position).observe(document.body, { attributes: true, attributeFilter: ['class'] }); } catch (_) {}
+  doc.on(window, 'resize', position);
+  if (vv) { doc.on(vv, 'resize', position); doc.on(vv, 'scroll', position); }
+  // has-audio-player is toggled via a body class → observe it. De observer
+  // overleeft de swap net als de listeners; init() disconnect de vorige.
+  try { _barObserver = new MutationObserver(position); _barObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] }); } catch (_) {}
 })();
+}
Index: src/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision c1df3e9e512d5945947d452659529f8866f65d9b)
+++ src/views/shell.ejs	(revision 4b5db37be989c52df56e680e3acb98ee0cd84ac0)
@@ -490,5 +490,13 @@
   window.__modBoot = true;
 
+  // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
+  // die `init` exporteert draait die bij ELKE paginawissel waarop hij actief
+  // is -- dat is het gedrag van de oude inline scripts, die per render
+  // draaiden. Een module zonder init draait alleen zijn top-level, een keer.
   var loaded = {};
+  function start(name, m) {
+    if (!m || typeof m.init !== 'function') return;
+    try { m.init(); } catch (e) { console.warn('[mod] ' + name + ' init:', e && e.message); }
+  }
   function load() {
     var names = (document.body.getAttribute('data-js') || '').trim().split(/\s+/);
@@ -497,7 +505,11 @@
       // kleine letters, cijfers en streepjes; nooit een punt of een schuine
       // streep.
-      if (!name || loaded[name] || !/^[a-z0-9-]+$/.test(name)) return;
+      if (!name || !/^[a-z0-9-]+$/.test(name)) return;
+      if (loaded[name]) { start(name, loaded[name]); return; }
       loaded[name] = 1;
-      import('/assets/js/mod/' + name + '.js').catch(function (e) {
+      import('/assets/js/mod/' + name + '.js').then(function (m) {
+        loaded[name] = m;
+        start(name, m);
+      }).catch(function (e) {
         console.warn('[mod] ' + name + ' laadde niet:', e && e.message);
       });
