Index: src/assets/css/ward.css
===================================================================
--- src/assets/css/ward.css	(revision 2086ea255ae8ea5a474fc805eda10bd71146c947)
+++ src/assets/css/ward.css	(revision 39dab6b0c3b5fd70bb2f06743430f0014384b3b7)
@@ -6,4 +6,7 @@
   --dim: #9aa7bb;
   --accent: #5a32e6;
+  --on-accent: #ffffff;
+  --bg-top: #1a2233;
+  --bg-bottom: #0d1017;
   --alarm: #ef9f27;
   --ok: #46c26a;
@@ -17,5 +20,6 @@
 body {
   margin: 0; padding: 16px calc(16px + env(safe-area-inset-right)) 32px calc(16px + env(safe-area-inset-left));
-  background: var(--bg); color: var(--ink);
+  background: linear-gradient(var(--bg-top), var(--bg-bottom)) fixed, var(--bg);
+  color: var(--ink);
   font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
 }
@@ -27,5 +31,5 @@
 .help button {
   width: 100%; padding: 18px; border: 0; border-radius: 14px;
-  background: var(--accent); color: #fff; font-size: 1.1rem; font-weight: 600;
+  background: var(--accent); color: var(--on-accent); font-size: 1.1rem; font-weight: 600;
 }
 .help button:disabled { background: #3a3f4d; color: var(--dim); }
@@ -50,5 +54,4 @@
 
 /* ── De feed ──────────────────────────────────────────────────────────── */
-.cards { display: grid; gap: 12px; }
 .post { background: var(--card); border-radius: 12px; padding: 12px 14px; border: 2px solid transparent; }
 .post .avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; }
@@ -84,5 +87,5 @@
 dialog#help-dialog menu { display: flex; gap: 8px; justify-content: flex-end; margin: 14px 0 0; padding: 0; }
 dialog#help-dialog button { border: 0; border-radius: 10px; padding: 10px 14px; background: #2a3342; color: var(--ink); font: inherit; }
-dialog#help-dialog button.primary { background: var(--accent); color: #fff; font-weight: 600; }
+dialog#help-dialog button.primary { background: var(--accent); color: var(--on-accent); font-weight: 600; }
 
 /* Voor schermlezers, niet voor het oog. */
@@ -138,5 +141,5 @@
   background: #131a25; color: var(--ink); padding: 10px 16px; font: inherit;
 }
-#thread-form button { border: 0; border-radius: 999px; padding: 10px 18px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; }
+#thread-form button { border: 0; border-radius: 999px; padding: 10px 18px; background: var(--accent); color: var(--on-accent); font: inherit; font-weight: 600; }
 
 /* ── Het zwaai-paneel ─────────────────────────────────────────────────── */
@@ -148,5 +151,5 @@
 dialog#wave-dialog h2 { margin: 0 0 12px; font-size: 1.05rem; }
 .wave-answers { display: grid; gap: 8px; }
-.wave-answers button { border: 0; border-radius: 10px; padding: 12px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; }
+.wave-answers button { border: 0; border-radius: 10px; padding: 12px; background: var(--accent); color: var(--on-accent); font: inherit; font-weight: 600; }
 #wave-cancel { margin-top: 10px; border: 0; background: none; color: var(--dim); font: inherit; }
 
@@ -170,5 +173,5 @@
 .feed-head { display: flex; align-items: center; gap: 12px; }
 .feed-head h2 { margin: 24px 0 10px; }
-#compose-open { margin-left: auto; border: 0; border-radius: 999px; padding: 8px 16px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; }
+#compose-open { margin-left: auto; border: 0; border-radius: 999px; padding: 8px 16px; background: var(--accent); color: var(--on-accent); font: inherit; font-weight: 600; }
 
 dialog#compose-dialog {
@@ -180,5 +183,5 @@
 .compose-head #compose-cancel { border: 0; background: #2a3342; color: var(--ink); border-radius: 10px; padding: 8px 12px; font-size: 1rem; }
 .compose-head #compose-vis { flex: 1; border-radius: 10px; border: 1px solid #2a3342; background: #131a25; color: var(--ink); padding: 8px; font: inherit; }
-.compose-head .primary { border: 0; border-radius: 10px; padding: 8px 16px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; }
+.compose-head .primary { border: 0; border-radius: 10px; padding: 8px 16px; background: var(--accent); color: var(--on-accent); font: inherit; font-weight: 600; }
 .compose-head .primary:disabled { background: #3a3f4d; color: var(--dim); }
 #compose-text { width: 100%; border-radius: 12px; border: 1px solid #2a3342; background: #131a25; color: var(--ink); padding: 10px; font: inherit; resize: vertical; }
@@ -192,2 +195,38 @@
 .compose-tools .tool { font-size: 1.4rem; cursor: pointer; padding: 4px 8px; border-radius: 10px; background: #131a25; }
 .compose-tools .hint { margin-left: auto; }
+
+/* ── Het kaartraster: meer kolommen naarmate er ruimte is ─────────────── */
+/* Robins wens (12-8): op desktop net als op mobiel en tablet MEERDERE
+   kolommen. auto-fill met een minimum vult zelf op: één kolom op een smalle
+   telefoon, twee op een tablet, drie of meer op een breed scherm. */
+.cards {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+  gap: 12px;
+  align-items: start;
+}
+
+/* ── De mini-speler ───────────────────────────────────────────────────── */
+#mini-player {
+  position: fixed; left: 12px; right: 12px; bottom: calc(12px + env(safe-area-inset-bottom));
+  z-index: 45; display: flex; align-items: center; gap: 10px;
+  padding: 8px 12px; border-radius: 14px;
+  background: var(--card); box-shadow: 0 6px 24px rgba(0,0,0,.4);
+  max-width: 520px; margin: 0 auto;
+}
+#mini-player #mini-toggle { border: 0; border-radius: 50%; width: 40px; height: 40px; background: var(--accent); color: var(--on-accent); font-size: 1rem; }
+#mini-player #mini-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+#mini-player #mini-close { border: 0; background: none; color: var(--dim); font-size: 1rem; }
+
+/* ── De themakiezer ───────────────────────────────────────────────────── */
+.icon-btn { border: 0; background: #2a3342; border-radius: 999px; width: 38px; height: 38px; font-size: 1.1rem; }
+dialog#theme-dialog {
+  border: 0; border-radius: 16px; padding: 18px; max-width: 320px; width: calc(100% - 48px);
+  background: var(--card); color: var(--ink); text-align: center;
+}
+dialog#theme-dialog::backdrop { background: rgba(0,0,0,.4); }
+dialog#theme-dialog h2 { margin: 0 0 14px; font-size: 1.05rem; }
+.swatches { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
+.swatch { width: 100%; aspect-ratio: 1; border: 3px solid transparent; border-radius: 50%; cursor: pointer; }
+.swatch.chosen { border-color: var(--ink); }
+#theme-close { margin-top: 14px; border: 0; background: none; color: var(--dim); font: inherit; }
Index: src/assets/js/ward-sky.js
===================================================================
--- src/assets/js/ward-sky.js	(revision 2086ea255ae8ea5a474fc805eda10bd71146c947)
+++ src/assets/js/ward-sky.js	(revision 39dab6b0c3b5fd70bb2f06743430f0014384b3b7)
@@ -99,5 +99,5 @@
 
 /** Eén canvas met alle avatars naast elkaar, zodat tekenen één drawImage is. */
-async function buildAtlas(faces) {
+async function buildAtlas(faces, accent) {
   const size = FACE * 2;                       // retina-scherp
   const cols = Math.max(1, Math.ceil(Math.sqrt(faces.length || 1)));
@@ -120,5 +120,5 @@
       g.save();
       g.beginPath(); g.arc(x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2); g.clip();
-      g.fillStyle = '#5a32e6'; g.fillRect(x, y, size, size);
+      g.fillStyle = accent || '#5a32e6'; g.fillRect(x, y, size, size);
       g.fillStyle = '#fff';
       g.font = `600 ${size * 0.45}px system-ui, sans-serif`;
@@ -176,6 +176,16 @@
     this.faces = faces;
     this.spots = orbitPlacement(faces.map((f) => f.ageMs));
-    this.atlas = await buildAtlas(faces);
+    this.atlas = await buildAtlas(faces, this._accent());
     this._resize();
+  }
+
+  /** De themakleur uit de CSS-variabele, voor de placeholder-schijfjes. */
+  _accent() {
+    return getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#5a32e6';
+  }
+
+  /** Het thema veranderde: de letters-op-schijf opnieuw in het nieuwe accent. */
+  async reskin() {
+    if (this.faces.length) this.atlas = await buildAtlas(this.faces, this._accent());
   }
 
Index: src/assets/js/ward-theme.js
===================================================================
--- src/assets/js/ward-theme.js	(revision 39dab6b0c3b5fd70bb2f06743430f0014384b3b7)
+++ src/assets/js/ward-theme.js	(revision 39dab6b0c3b5fd70bb2f06743430f0014384b3b7)
@@ -0,0 +1,63 @@
+/**
+ * De themakleur (SkyPalette): één gekozen kleur die de hele ward kleurt.
+ *
+ * De acht presets en de luminantie-regel komen LETTERLIJK uit Shaer (iOS is de
+ * bron): dezelfde kleuren, en dezelfde beslissing welke tekst erop leesbaar is
+ * (donker op licht, wit op de rest). Een handgehouden lijstje zou uit de pas
+ * gaan lopen; luminantie beslist.
+ *
+ * Opgeslagen in localStorage, per apparaat, net als in de app.
+ */
+'use strict';
+
+export const PRESETS = [
+  0x5A32E6, // paars, de vertrouwde
+  0x1E6AE1, // oceaan
+  0x0E9B8A, // zeegroen
+  0x2E7D32, // bos
+  0xF59E0B, // zon
+  0xFF6B4A, // koraal
+  0xE255A1, // roze
+  0x1F2937, // nacht
+];
+const FALLBACK = 0x5A32E6;
+const KEY = 'ward.skyColor';
+
+const hex = (rgb) => '#' + rgb.toString(16).padStart(6, '0');
+
+/** Wit of donker, wat leesbaar is op deze kleur. Zelfde som als in de app. */
+export function onSky(rgb) {
+  const r = ((rgb >> 16) & 0xff) / 255;
+  const g = ((rgb >> 8) & 0xff) / 255;
+  const b = (rgb & 0xff) / 255;
+  const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+  return lum > 0.5 ? '#1f2430' : '#ffffff';
+}
+
+/** De opgeslagen kleur, of de vertrouwde paarse als er nog niets gekozen is. */
+export function loadColor() {
+  const v = parseInt(localStorage.getItem(KEY) || '', 10);
+  return Number.isFinite(v) && v >= 0 && v <= 0xffffff ? v : FALLBACK;
+}
+
+/**
+ * De kleur toepassen: het accent dat de knoppen, de bubbels en de (+) dragen,
+ * plus een zachte tint over de donkere achtergrond zodat de hele hemel de
+ * gekozen kleur ademt. De tekst-op-accent volgt de luminantie.
+ */
+export function applyColor(rgb) {
+  const root = document.documentElement.style;
+  root.setProperty('--accent', hex(rgb));
+  root.setProperty('--on-accent', onSky(rgb));
+  // De achtergrond blijft donker (leesbaar), maar krijgt een vleugje van de
+  // kleur: een verticale gradient van de getinte nacht naar bijna zwart.
+  const r = (rgb >> 16) & 0xff, g = (rgb >> 8) & 0xff, b = rgb & 0xff;
+  const tint = (f) => `rgb(${Math.round(r * f + 20 * (1 - f))},${Math.round(g * f + 26 * (1 - f))},${Math.round(b * f + 36 * (1 - f))})`;
+  root.setProperty('--bg-top', tint(0.22));
+  root.setProperty('--bg-bottom', tint(0.06));
+}
+
+export function saveColor(rgb) {
+  localStorage.setItem(KEY, String(rgb));
+  applyColor(rgb);
+}
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();
