Index: package.json
===================================================================
--- package.json	(revision c83206430014ce7861a00faf49d21b26ce8f3ad4)
+++ package.json	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
@@ -12,5 +12,6 @@
     "migrate": "node src/db/migrate.js",
     "reset-admin": "node scripts/reset-admin.mjs",
-    "backfill:durations": "node scripts/backfill-durations.mjs"
+    "backfill:durations": "node scripts/backfill-durations.mjs",
+    "backfill:post-types": "node scripts/backfill-post-types.mjs"
   },
   "dependencies": {
Index: scripts/backfill-post-types.mjs
===================================================================
--- scripts/backfill-post-types.mjs	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
+++ scripts/backfill-post-types.mjs	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
@@ -0,0 +1,64 @@
+#!/usr/bin/env node
+/**
+ * Zet bestaande type=audio posts om naar album of playlist. (shaer-cyg)
+ *
+ * Audio is geen keuze meer: alles wat muziek is landt op album of playlist. Wat
+ * er al staat moet daar dus heen, en dat is een migratie en geen hernoeming --
+ * welke van de twee het wordt hangt af van wat er IN de post zit. Die afleiding
+ * staat in music/postMusicType en wordt hier alleen toegepast.
+ *
+ *   node scripts/backfill-post-types.mjs            # laat zien wat er zou gebeuren
+ *   node scripts/backfill-post-types.mjs --doen     # en voert het uit
+ *
+ * Idempotent: kijkt alleen naar type='audio', dus een tweede run doet niets.
+ * Posts waar de afleiding geen album of playlist van maakt (twee collecties,
+ * een onvindbare playlist, of helemaal geen muziek meer) blijven met rust
+ * gelaten en worden apart gemeld -- die verdienen een blik en geen gok.
+ */
+import db from '../src/config/database.js';
+import { postMusicType } from '../src/services/music/index.js';
+
+const DOEN = process.argv.includes('--doen');
+
+const posts = db.prepare(`
+  SELECT p.id, p.site_id, p.slug, p.title, p.content, s.slug AS site_slug
+  FROM posts p JOIN sites s ON s.id = p.site_id
+  WHERE p.type = 'audio'
+  ORDER BY p.created_at
+`).all();
+
+console.log(`[backfill-post-types] ${posts.length} post(s) met type=audio${DOEN ? '' : '  (proefdraai — geef --doen om te schrijven)'}`);
+
+const update = db.prepare('UPDATE posts SET type = ? WHERE id = ?');
+const telling = { album: 0, playlist: 0 };
+const overgeslagen = [];
+
+for (const p of posts) {
+  const r = postMusicType(p.content, p.site_id);
+  const naam = `${p.site_slug}/${p.slug}`;
+
+  if (!r || (r.type !== 'album' && r.type !== 'playlist')) {
+    const reden = !r ? 'geen muziek meer in de post'
+      : r.onbekend?.length ? `playlist niet gevonden: ${r.onbekend.join(', ')}`
+      : `${r.collecties?.length ?? 0} collecties — de post blijft een post`;
+    overgeslagen.push({ naam, reden });
+    continue;
+  }
+
+  telling[r.type]++;
+  const bonus = r.bonus?.length ? `, ${r.bonus.length} bonus-track(s)` : '';
+  console.log(`  ${r.type.padEnd(8)} ${naam}${bonus}`);
+  if (DOEN) update.run(r.type, p.id);
+}
+
+console.log(`\n  album:    ${telling.album}`);
+console.log(`  playlist: ${telling.playlist}`);
+
+if (overgeslagen.length) {
+  console.log(`\n  overgeslagen (${overgeslagen.length}) — deze blijven type=audio:`);
+  for (const o of overgeslagen) console.log(`    ${o.naam}: ${o.reden}`);
+}
+
+if (!DOEN && (telling.album || telling.playlist)) {
+  console.log('\nNiets geschreven. Draai opnieuw met --doen als dit klopt.');
+}
Index: src/assets/js/mod/post-edit.js
===================================================================
--- src/assets/js/mod/post-edit.js	(revision c83206430014ce7861a00faf49d21b26ce8f3ad4)
+++ src/assets/js/mod/post-edit.js	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
@@ -980,5 +980,10 @@
         b.setAttribute('aria-checked', on ? 'true' : 'false');
       });
-      panels.forEach(p => { p.hidden = (p.dataset.panel !== tt); });
+      // data-panel mag meerdere types noemen: Album en Playlist delen het
+      // muziekpaneel, want het verschil zit in de playlist en niet in de upload.
+      panels.forEach(p => {
+        const voor = String(p.dataset.panel || '').trim().split(/\s+/);
+        p.hidden = !voor.includes(tt);
+      });
     }
     seg.addEventListener('click', (e) => {
Index: src/config/post-types.js
===================================================================
--- src/config/post-types.js	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
+++ src/config/post-types.js	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
@@ -0,0 +1,24 @@
+/**
+ * Welke posttypes bestaan er. (shaer-cyg)
+ *
+ * Dit stond op drie plaatsen los van elkaar: twee keer in routes/posts.js (het
+ * opslaan) en een keer in routes/types.js (de /type/-pagina's). Zolang die
+ * lijsten hetzelfde waren viel dat niet op, maar het is precies het soort naad
+ * dat stil faalt: kent de opslag 'playlist' niet, dan wordt de post zonder
+ * melding een gewone post, en dan is de keuze verdwenen in plaats van geweigerd.
+ *
+ * AUDIO STAAT ER NOG WEL IN, MAAR IS GEEN KEUZE MEER. Alles wat muziek is landt
+ * voortaan op album of playlist (Robins besluit, 8-8). Bestaande posts hebben
+ * type=audio nog wel, dus /type/audio moet blijven werken en het opslaan van
+ * zo'n post mag zijn type niet stilzwijgend weggooien -- tot de backfill hem
+ * heeft omgezet. Daarna kan audio hier weg.
+ */
+
+/** Types die een gebruiker in de editor kan kiezen. */
+export const KEUZE_TYPES = ['post', 'foto', 'video', 'album', 'playlist'];
+
+/** Alles wat in de kolom posts.type mag staan, inclusief wat er historisch is. */
+export const POST_TYPES = new Set([...KEUZE_TYPES, 'audio']);
+
+/** De types die muziek dragen -- ze delen hetzelfde paneel en dezelfde afleiding. */
+export const MUZIEK_TYPES = new Set(['album', 'playlist', 'audio']);
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision c83206430014ce7861a00faf49d21b26ce8f3ad4)
+++ src/routes/posts.js	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
@@ -6,4 +6,5 @@
 import ejs from 'ejs';
 import db from '../config/database.js';
+import { POST_TYPES, KEUZE_TYPES } from '../config/post-types.js';
 import { requireAuth, requireSiteManager, isViewer } from '../middleware/auth.js';
 import { renderPage } from '../middleware/render.js';
@@ -265,4 +266,5 @@
     },
     isNew: true,
+    keuzeTypes: KEUZE_TYPES,
     pageTitle: 'New post',
     bodyClass: 'on-special',
@@ -359,6 +361,5 @@
   finalSlug = uniqueSlug(site.id, finalSlug);
 
-  const validTypes = new Set(['post', 'foto', 'video', 'audio']);
-  const finalType = validTypes.has(type) ? type : 'post';
+  const finalType = POST_TYPES.has(type) ? type : 'post';
   const pollJson = parsePollForm(req.body);   // AS2 Question definition, or null
   const postId = uuid();
@@ -454,4 +455,5 @@
     post,
     isNew: false,
+    keuzeTypes: KEUZE_TYPES,
     pollLocked,
     fediOpenAudio: postAudioFediOpen(site.id, post.content),
@@ -486,6 +488,5 @@
   const newSlug = req.body.slug;
   const action = req.body.action || 'save';
-  const validTypes = new Set(['post', 'foto', 'video', 'audio']);
-  const finalType = validTypes.has(type) ? type : (post.type || 'post');
+  const finalType = POST_TYPES.has(type) ? type : (post.type || 'post');
 
   // A poll that has already received votes is frozen (you can still edit the surrounding
Index: src/routes/types.js
===================================================================
--- src/routes/types.js	(revision c83206430014ce7861a00faf49d21b26ce8f3ad4)
+++ src/routes/types.js	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
@@ -8,8 +8,10 @@
 import express from 'express';
 import db from '../config/database.js';
+import { POST_TYPES } from '../config/post-types.js';
 import { renderPage } from '../middleware/render.js';
 
 const router = express.Router();
-const VALID_TYPES = new Set(['post', 'foto', 'video', 'audio']);
+// Een lijst, drie gebruikers: zie config/post-types.js.
+const VALID_TYPES = POST_TYPES;
 
 router.get('/:type', (req, res) => {
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision c83206430014ce7861a00faf49d21b26ce8f3ad4)
+++ src/services/i18n.js	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
@@ -706,4 +706,6 @@
     'pedit.type_video': 'Video',
     'pedit.type_audio': 'Audio',
+    'pedit.type_album': 'Album',
+    'pedit.type_playlist': 'Playlist',
     'pedit.s_type': 'Wat voor post?',
     'pedit.audio_up_drop': 'Sleep audio hierheen of klik om te kiezen',
@@ -1650,4 +1652,6 @@
     'pedit.type_video': 'Video',
     'pedit.type_audio': 'Audio',
+    'pedit.type_album': 'Album',
+    'pedit.type_playlist': 'Playlist',
     'pedit.s_type': 'What kind of post?',
     'pedit.audio_up_drop': 'Drop audio here or click to choose',
@@ -2594,4 +2598,6 @@
     'pedit.type_video': 'Video',
     'pedit.type_audio': 'Audio',
+    'pedit.type_album': 'Album',
+    'pedit.type_playlist': 'Playlist',
     'pedit.s_type': 'Was für ein Beitrag?',
     'pedit.audio_up_drop': 'Audio hierher ziehen oder klicken',
Index: src/views/pages/post-edit.ejs
===================================================================
--- src/views/pages/post-edit.ejs	(revision c83206430014ce7861a00faf49d21b26ce8f3ad4)
+++ src/views/pages/post-edit.ejs	(revision d7e72b84b3578aaadd6519dbe240f683eec33591)
@@ -24,10 +24,16 @@
         Foto → cover/insert hint. %>
     <% var ptype = post.type || 'post'; %>
-    <% var TYPE_ICONS = { post: '📝', foto: '📷', video: '🎬', audio: '🎵' }; %>
+    <% var TYPE_ICONS = { post: '📝', foto: '📷', video: '🎬', album: '💿', playlist: '📃', audio: '🎵' }; %>
+    <%# Audio is geen keuze meer (shaer-cyg): alles wat muziek is landt op Album
+        of Playlist. De knop blijft alleen staan zolang deze post nog audio IS,
+        zodat een oude post niet stilzwijgend van type verandert bij het openen. %>
+    <% var TYPES = (typeof keuzeTypes !== 'undefined' && keuzeTypes.length)
+                     ? keuzeTypes.slice() : ['post','foto','video','album','playlist']; %>
+    <% if (ptype === 'audio' && TYPES.indexOf('audio') < 0) TYPES.push('audio'); %>
     <section class="pe-card pe-type-card">
       <div class="pe-section-title"><%= t('pedit.s_type') %></div>
       <input type="hidden" name="type" id="pe-type-input" value="<%= ptype %>">
       <div class="pe-typeseg" role="radiogroup" aria-label="<%= t('pedit.s_type') %>">
-        <% ['post','foto','video','audio'].forEach(function (tt) { %>
+        <% TYPES.forEach(function (tt) { %>
           <button type="button" class="pe-typeseg-btn<%= ptype === tt ? ' is-active' : '' %>"
                   data-type="<%= tt %>" role="radio" aria-checked="<%= ptype === tt ? 'true' : 'false' %>">
@@ -38,6 +44,8 @@
       </div>
 
-      <%# Audio: inline upload (transcodes + drops [[track]] into the post) %>
-      <div class="pe-type-panel" data-panel="audio" hidden>
+      <%# Muziek: inline upload (transcodes + drops [[track]] into the post).
+          Album en Playlist delen dit paneel -- het verschil zit in de playlist,
+          niet in wat je hier uploadt. Audio staat er alleen nog voor oude posts. %>
+      <div class="pe-type-panel" data-panel="album playlist audio" hidden>
         <div class="pe-audio-up" id="pe-audio-drop" tabindex="0" role="button" aria-label="<%= t('pedit.audio_up_drop') %>">
           <input type="file" id="pe-audio-file" accept="audio/*,.mp3,.m4a,.ogg,.opus,.flac,.wav" multiple hidden>
