Index: src/assets/js/ward.js
===================================================================
--- src/assets/js/ward.js	(revision 2086ea255ae8ea5a474fc805eda10bd71146c947)
+++ src/assets/js/ward.js	(revision 39dab6b0c3b5fd70bb2f06743430f0014384b3b7)
@@ -8,6 +8,10 @@
  */
 import { SkyRenderer } from '/ward/sky.js';
+import { PRESETS, loadColor, saveColor, applyColor } from '/ward/theme.js';
 
 const state = JSON.parse(document.getElementById('ward-state').textContent);
+
+// De opgeslagen themakleur meteen toepassen, voor het eerste frame.
+applyColor(loadColor());
 const $ = (id) => document.getElementById(id);
 
@@ -421,4 +425,7 @@
     } catch { /* zonder hemel blijft het scherm gewoon werken */ }
   })();
+
+  // De hemel skint zijn placeholder-schijfjes bij als de themakleur wisselt.
+  window.addEventListener('ward-accent', () => sky.reskin());
 }
 
@@ -515,5 +522,73 @@
 }
 
-/** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. */
+/**
+ * De themakiezer (SkyPalette): acht presets, gekozen kleur kleurt alles.
+ * Een rij swatches in een dialoog; de keuze staat meteen en blijft bewaard.
+ */
+function wireTheme() {
+  const dlg = document.getElementById('theme-dialog');
+  const row = document.getElementById('theme-swatches');
+  const current = loadColor();
+  row.replaceChildren();
+  PRESETS.forEach((rgb) => {
+    const b = el('button', 'swatch');
+    b.type = 'button';
+    b.style.background = '#' + rgb.toString(16).padStart(6, '0');
+    b.setAttribute('aria-label', 'kleur ' + rgb.toString(16));
+    if (rgb === current) b.classList.add('chosen');
+    b.addEventListener('click', () => {
+      saveColor(rgb);
+      row.querySelectorAll('.swatch').forEach((s) => s.classList.remove('chosen'));
+      b.classList.add('chosen');
+      // De hemel tekent de placeholder-schijfjes in het accent; even opnieuw.
+      window.dispatchEvent(new CustomEvent('ward-accent'));
+    });
+    row.append(b);
+  });
+  document.getElementById('theme-open').addEventListener('click', () => dlg.showModal());
+  document.getElementById('theme-close').addEventListener('click', () => dlg.close());
+}
+
+/**
+ * De mini-speler: audio blijft doorspelen terwijl je scrolt of van stand
+ * wisselt.
+ *
+ * Een feed-kaart rendert een gewone <audio>, maar de feed wordt bij elke
+ * verversing opnieuw getekend en dan sterft dat element -- de muziek stopt
+ * midden in een nummer. Daarom is er ÉÉN blijvende speler buiten de feed; een
+ * audiotegel geeft alleen zijn bron door en pauzeert zichzelf. Zo overleeft
+ * het geluid een herteken en een wissel naar de hemel, precies als in de app.
+ */
+function wireMiniPlayer() {
+  const bar = document.getElementById('mini-player');
+  const audio = document.getElementById('mini-audio');
+  const title = document.getElementById('mini-title');
+  const toggle = document.getElementById('mini-toggle');
+
+  // Vangen in de CAPTURE-fase: zodra een feed-audio wil spelen, neemt de
+  // gedeelde speler het over. Delegatie op document, want de kaarten komen en
+  // gaan.
+  document.addEventListener('play', (e) => {
+    const t = e.target;
+    if (!(t instanceof HTMLAudioElement) || t === audio) return;
+    e.preventDefault();
+    t.pause();
+    const card = t.closest('.post');
+    title.textContent = card?.querySelector('.name')?.textContent || 'Audio';
+    if (audio.src !== new URL(t.currentSrc || t.src, location.href).href) {
+      audio.src = t.currentSrc || t.src;
+    }
+    audio.play().catch(() => {});
+    bar.hidden = false;
+  }, true);
+
+  toggle.addEventListener('click', () => { audio.paused ? audio.play() : audio.pause(); });
+  audio.addEventListener('play', () => { toggle.textContent = '⏸'; });
+  audio.addEventListener('pause', () => { toggle.textContent = '▶'; });
+  audio.addEventListener('ended', () => { bar.hidden = true; });
+  document.getElementById('mini-close').addEventListener('click', () => { audio.pause(); bar.hidden = true; });
+}
+
+/** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. *//** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. */
 async function loadChecks() {
   if (!state.guardians.length) return;
@@ -542,4 +617,6 @@
 wireSky();
 wireComposer();
+wireTheme();
+wireMiniPlayer();
 loadFeed();
 renderOffers();
