Index: src/assets/js/mod/post-edit.js
===================================================================
--- src/assets/js/mod/post-edit.js	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
+++ src/assets/js/mod/post-edit.js	(revision b406d25e8f0b0b49e6c45df1a3281e46425ca139)
@@ -6,4 +6,7 @@
 
 import { pageData, esc, makeSweeper } from './lib.js';
+// Dezelfde regel die de server gebruikt -- zie shared/post-music-type.js voor
+// waarom hij daar staat en niet twee keer.
+import { afleidenUitInsluitingen } from '../shared/post-music-type.js';
 
 // De oude inline scripts draaiden bij ELKE render; een module draait zijn
@@ -791,5 +794,11 @@
     }
     updateCharCount();
-  }
+    volgMuziek();
+  }
+
+  // Wordt hieronder gevuld door het typeblok. Zolang dat er niet is (of de
+  // gebruiker het type zelf koos) gebeurt er niets -- insertChip mag daar niet
+  // op stuklopen.
+  let volgMuziek = () => {};
 
   // ── Embed insert: paste a platform URL -> [[embed:url]]-chip that becomes
@@ -972,4 +981,5 @@
     const seg = card.querySelector('.pe-typeseg');
     const panels = card.querySelectorAll('.pe-type-panel');
+    let handmatig = false;   // heeft de gebruiker het type zelf aangeklikt?
 
     function applyType(tt) {
@@ -989,7 +999,66 @@
     seg.addEventListener('click', (e) => {
       const btn = e.target.closest('.pe-typeseg-btn');
-      if (btn) applyType(btn.dataset.type);
+      if (!btn) return;
+      handmatig = true;          // jouw klik wint vanaf nu van de afleiding
+      applyType(btn.dataset.type);
     });
     applyType(typeInput.value || 'post');
+
+    // ── Het type volgt de muziek (shaer-cyg) ──────────────────────
+    //
+    // Robins regel: de post neemt de soort van zijn muziek over als hij precies
+    // EEN muzikale eenheid bevat. Dus zodra je een playlist invoegt verspringt
+    // de balk mee, en zie je wat je aan het maken bent in plaats van het zelf te
+    // moeten bijhouden.
+    //
+    // TWEE DINGEN DIE HIJ MET RUST LAAT. Klik je zelf een type aan, dan wint die
+    // keuze -- daarna wordt er niets meer voor je omgezet. En Foto en Video zijn
+    // een bewuste andere keuze over dezelfde post, dus die overschrijft hij
+    // nooit, ook niet als er muziek in staat.
+    //
+    // Bij het OPENEN van een post gebeurt er niets: de eerste afleiding wordt
+    // alleen als ijkpunt onthouden. Anders zou een oude post van type veranderen
+    // door hem te bekijken.
+    const VOLGBAAR = new Set(['post', 'album', 'playlist', 'audio']);
+    const kindVan = new Map();
+    let vorigeAfleiding = null;
+
+    const soortenGeladen = fetch('/admin/playlists/api/list', { credentials: 'same-origin' })
+      .then((r) => r.json())
+      .then((j) => {
+        if (j && j.ok && Array.isArray(j.playlists)) {
+          for (const p of j.playlists) kindVan.set(p.id, p.kind === 'playlist' ? 'playlist' : 'album');
+        }
+      })
+      // Zonder de soorten leidt de regel niets af (elke playlist is dan
+      // 'onbekend'), en dat is beter dan een gok die als keuze oogt.
+      .catch(() => {})
+      .then(() => { vorigeAfleiding = afleidenNu(); });
+
+    function afleidenNu() {
+      // Precies de tekst die straks wordt opgeslagen: chips terug naar
+      // shortcodes. Zo leidt de editor af uit wat de server ook zal zien.
+      const clone = editor.cloneNode(true);
+      serializeChips(clone);
+      const r = afleidenUitInsluitingen(clone.innerHTML, (id) => kindVan.get(id) || null);
+      return r ? r.type : null;
+    }
+
+    volgMuziek = async () => {
+      await soortenGeladen;
+      const nu = afleidenNu();
+      if (nu === vorigeAfleiding) return;    // de muziek is niet van soort veranderd
+      vorigeAfleiding = nu;
+      if (handmatig || !nu) return;
+      if (!VOLGBAAR.has(typeInput.value)) return;
+      applyType(nu);
+    };
+
+    // Ook getypte of geplakte shortcodes tellen, niet alleen de knoppen.
+    let tik = null;
+    editor.addEventListener('input', () => {
+      clearTimeout(tik);
+      tik = setTimeout(volgMuziek, 300);
+    });
 
     // Video URL → [[embed:url]] chip
Index: src/assets/js/shared/post-music-type.js
===================================================================
--- src/assets/js/shared/post-music-type.js	(revision b406d25e8f0b0b49e6c45df1a3281e46425ca139)
+++ src/assets/js/shared/post-music-type.js	(revision b406d25e8f0b0b49e6c45df1a3281e46425ca139)
@@ -0,0 +1,101 @@
+/**
+ * De regel: welk soort uitgave is een post? (shaer-cyg)
+ *
+ * WAAROM DIT ONDER assets/ STAAT. De regel wordt op twee plaatsen gebruikt: de
+ * server leidt er het type van bestaande posts mee af, en de editor laat het
+ * type er live door volgen als je muziek invoegt. Dat MOET dezelfde regel zijn.
+ * Twee kopieën zouden niet luidruchtig kapotgaan maar stil uit elkaar lopen --
+ * de editor zegt album, de server zegt playlist, en niemand die het merkt. De
+ * browser kan alleen bij /assets, de server kan overal bij; dus staat hij hier,
+ * en importeert de server hem vanuit services/music.
+ *
+ * Deze module is bewust PUUR: geen database, geen DOM, geen fetch. Wie hem
+ * gebruikt levert zelf een opzoeker voor de soort van een playlist.
+ *
+ * DE REGEL GAAT OVER IDENTITEIT, NIET OVER TELLEN (Robins herformulering, 9-8):
+ * een post neemt het type van zijn muziek over als hij precies EEN muzikale
+ * eenheid bevat. Zijn het er meer, dan is de post een post die naar muziek
+ * verwijst, en houden de collecties hun eigen identiteit.
+ *
+ *   losse track(s)                -> playlist   metadata van de post geleend
+ *   een collectie                 -> die soort  metadata van de post geleend
+ *   een collectie + losse tracks  -> album      de losse zijn BONUS-TRACKS
+ *   twee of meer collecties       -> post       NIETS geleend
+ *
+ * Waarom de lening bij de laatste vervalt: die bestaat omdat een collectie soms
+ * dun is -- geen eigen hoes, geen eigen titel. Bij twee is de post niet meer de
+ * drager van EEN identiteit, en vervalt de reden vanzelf.
+ *
+ * WIE KIEST ALBUM OF PLAYLIST: dat gebeurt wanneer de PLAYLIST wordt gemaakt.
+ * Die keuze staat in playlists.kind en wordt hier OPGEZOCHT, niet afgeleid --
+ * een post om een album is een album, ook al heet de shortcode [[playlist:...]].
+ *
+ * EN ALS WE HET NIET WETEN: een gewone post met insluitingen die de post als
+ * context hebben. Geen noodgreep maar de rustende toestand; tracks wijzen met
+ * `context` toch al terug naar hun post, dus er gaat niets verloren.
+ */
+
+// Dezelfde patronen als de renderer in AudioEmbedService: wat daar niet
+// insluit, telt hier niet mee. Anders zou een shortcode die niets oplevert wel
+// het type van de post kunnen bepalen.
+const RE_PLAYLIST = /\[\[playlist:([a-z0-9][a-z0-9-]*)\]\]/gi;
+const RE_ALBUM    = /\[\[album:([^\]]+)\]\]/g;
+const RE_TRACK    = /\[\[track:([A-Za-z0-9_-]+)\]\]/g;
+
+/**
+ * @param {string} content    de HTML/tekst van de post
+ * @param {(id: string) => ('album'|'playlist'|null)} kindVan
+ *        de gekozen soort van een playlist, of null als hij onbekend is
+ * @returns {null|{type, collectie?, collecties?, tracks, bonus, onbekend?, leentMetadata}}
+ */
+export function afleidenUitInsluitingen(content, kindVan) {
+  const c = String(content || '');
+  const zoek = typeof kindVan === 'function' ? kindVan : () => null;
+  const uniek = (re) => [...new Set([...c.matchAll(re)].map((m) => m[1].trim()))];
+
+  const playlists = uniek(RE_PLAYLIST);
+  const albums    = uniek(RE_ALBUM);
+  const tracks    = uniek(RE_TRACK);
+
+  if (!playlists.length && !albums.length && !tracks.length) return null;
+
+  // De soort van een playlist is een gegeven, geen afleiding.
+  const onbekend = [];
+  const uitPlaylists = playlists.map((id) => {
+    const kind = zoek(id);
+    if (kind !== 'album' && kind !== 'playlist') { onbekend.push(id); return null; }
+    return { soort: kind, id };
+  }).filter(Boolean);
+
+  const collecties = [
+    ...uitPlaylists,
+    ...albums.map((naam) => ({ soort: 'album', naam })),
+  ];
+
+  // Onbekende situatie -> gewone post. De insluitingen blijven staan en houden
+  // de post als context; alleen het label wordt niet verzonnen.
+  if (onbekend.length) {
+    return { type: 'post', collecties, tracks, bonus: [], onbekend, leentMetadata: false };
+  }
+
+  if (!collecties.length) {
+    // Ook EEN losse track wordt een playlist: naar buiten toe is er dan altijd
+    // een collectie om naar te wijzen. Hoe Klonkt dat toont is een aparte vraag.
+    return { type: 'playlist', collectie: null, tracks, bonus: [], leentMetadata: true };
+  }
+
+  if (collecties.length === 1) {
+    const c0 = collecties[0];
+    // Losse tracks naast een collectie zijn geen rommelrestje maar bonus-tracks,
+    // en dat maakt het geheel een album.
+    if (tracks.length) return { type: 'album', collectie: c0, tracks: [], bonus: tracks, leentMetadata: true };
+    return { type: c0.soort, collectie: c0, tracks: [], bonus: [], leentMetadata: true };
+  }
+
+  return { type: 'post', collecties, tracks, bonus: [], leentMetadata: false };
+}
+
+/** De playlist-ids die in een tekst worden ingesloten. */
+export function ingeslotenPlaylists(content) {
+  return [...new Set([...String(content || '').matchAll(RE_PLAYLIST)].map((m) => m[1].trim()))];
+}
Index: src/services/music/index.js
===================================================================
--- src/services/music/index.js	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
+++ src/services/music/index.js	(revision b406d25e8f0b0b49e6c45df1a3281e46425ca139)
@@ -15,4 +15,5 @@
 import db from '../../config/database.js';
 import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType } from '../ap-core.js';
+import { afleidenUitInsluitingen } from '../../assets/js/shared/post-music-type.js';
 
 // m.size hoort erbij voor de RSS-enclosure: die eist een lengte in bytes.
@@ -333,46 +334,19 @@
 }
 
-// ── Welk soort muzikale uitgave is deze post? (shaer-cyg) ─────────────────
-
-/**
- * Het type van een BESTAANDE post afleiden uit de muziek die erin staat.
+// ── Welk soort muzikale uitgave is deze post? (shaer-cyg) ─────────────
+
+/**
+ * Het type van een post afleiden uit de muziek die erin staat.
+ *
+ * DE REGEL ZELF staat in assets/js/shared/post-music-type.js, want de editor
+ * gebruikt hem ook -- daar volgt het type live mee terwijl je schrijft. Twee
+ * kopieen zouden stil uit elkaar lopen, dus is er er een. Hier komt alleen het
+ * stuk bij dat de server kan en de browser niet: de gekozen soort van een
+ * playlist opzoeken.
  *
  * WAARVOOR DIT WEL EN NIET IS (Robins afbakening, 9-8). Nieuwe posts krijgen
  * hun type uit de keuze: album of playlist wordt gekozen als de playlist wordt
- * gemaakt, en de post neemt dat over. Deze functie is er voor wat er al staat --
- * de posts met type=audio uit de tijd voor die keuze bestond. Daarmee is de
- * vraag "wie wint, de keuze of de afleiding?" geen vraag meer: ze komen elkaar
- * niet tegen. De afleiding draait eenmalig, de keuze draait daarna.
- *
- * DE REGEL GAAT OVER IDENTITEIT, NIET OVER TELLEN (Robins herformulering, 9-8):
- * een post neemt het type van zijn muziek over als hij precies EEN muzikale
- * eenheid bevat. Zijn het er meer, dan is de post een post die naar muziek
- * verwijst, en houden de collecties hun eigen identiteit.
- *
- *   losse track(s)                -> playlist   metadata van de post geleend
- *   een collectie                 -> die soort  metadata van de post geleend
- *   een collectie + losse tracks  -> album      de losse zijn BONUS-TRACKS
- *   twee of meer collecties       -> post       NIETS geleend
- *
- * Waarom de lening bij de laatste vervalt: die bestaat omdat een collectie soms
- * dun is -- geen eigen hoes, geen eigen titel. Bij twee is de post niet meer de
- * drager van EEN identiteit, en vervalt de reden vanzelf. Dezelfde regel zet
- * zichzelf uit.
- *
- * EEN COLLECTIE IS EEN PLAYLIST OF EEN ALBUM-INSLUITING. Robins regel noemde
- * alleen playlists, maar [[album:naam]] groepeert net zo goed tracks en is
- * letterlijk een album; hem als losse tracks tellen zou een albumpost tot
- * playlist maken. Telt hij straks anders, dan is dat hier een regel.
- *
- * WIE KIEST ALBUM OF PLAYLIST: dat gebeurt wanneer de PLAYLIST wordt gemaakt,
- * en die keuze staat al in playlists.kind. Bij een enkele insluiting neemt de
- * post dus die soort over -- een post om een album is een album, ook al heet de
- * shortcode [[playlist:...]]. De soort van een playlist wordt hier dus
- * OPGEZOCHT en niet afgeleid.
- *
- * EN ALS WE HET NIET WETEN: een gewone post met insluitingen die de post als
- * context hebben. Dat is geen noodgreep maar de rustende toestand -- tracks
- * wijzen met `context` toch al terug naar hun post, dus er gaat niets verloren
- * als het label 'post' wordt.
+ * gemaakt, en de post neemt dat over. Op de server is dit vooral voor wat er al
+ * staat -- de posts met type=audio uit de tijd voor die keuze bestond.
  *
  * @param {string} content   de HTML/tekst van de post
@@ -380,50 +354,5 @@
  */
 export function postMusicType(content, siteId) {
-  const c = String(content || '');
-  const uniek = (re) => [...new Set([...c.matchAll(re)].map((m) => m[1].trim()))];
-
-  // Dezelfde patronen als de renderer in AudioEmbedService: wat daar niet
-  // insluit, telt hier niet mee. Anders zou een shortcode die niets oplevert
-  // wel het type van de post kunnen bepalen.
-  const playlists = uniek(/\[\[playlist:([a-z0-9][a-z0-9-]*)\]\]/gi);
-  const albums    = uniek(/\[\[album:([^\]]+)\]\]/g);
-  const tracks    = uniek(/\[\[track:([A-Za-z0-9_-]+)\]\]/g);
-
-  if (!playlists.length && !albums.length && !tracks.length) return null;
-
-  // De soort van een playlist is een gegeven, geen afleiding.
-  let onbekend = [];
-  const uitPlaylists = playlists.map((id) => {
-    const kind = playlistKind(id, siteId);
-    if (!kind) { onbekend.push(id); return null; }
-    return { soort: kind, id };
-  }).filter(Boolean);
-
-  const collecties = [
-    ...uitPlaylists,
-    ...albums.map((naam) => ({ soort: 'album', naam })),
-  ];
-
-  // Onbekende situatie -> gewone post. De insluitingen blijven staan en houden
-  // de post als context; alleen het label wordt niet verzonnen.
-  if (onbekend.length) {
-    return { type: 'post', collecties, tracks, bonus: [], onbekend, leentMetadata: false };
-  }
-
-  if (!collecties.length) {
-    // Ook EEN losse track wordt een playlist: naar buiten toe is er dan altijd
-    // een collectie om naar te wijzen. Hoe Klonkt dat toont is een aparte vraag.
-    return { type: 'playlist', collectie: null, tracks, bonus: [], leentMetadata: true };
-  }
-
-  if (collecties.length === 1) {
-    const c0 = collecties[0];
-    // Losse tracks naast een collectie zijn geen rommelrestje maar bonus-tracks,
-    // en dat maakt het geheel een album.
-    if (tracks.length) return { type: 'album', collectie: c0, tracks: [], bonus: tracks, leentMetadata: true };
-    return { type: c0.soort, collectie: c0, tracks: [], bonus: [], leentMetadata: true };
-  }
-
-  return { type: 'post', collecties, tracks, bonus: [], leentMetadata: false };
+  return afleidenUitInsluitingen(content, (id) => playlistKind(id, siteId));
 }
 
