Index: src/services/AudioEmbedService.js
===================================================================
--- src/services/AudioEmbedService.js	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/services/AudioEmbedService.js	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -11,48 +11,13 @@
  */
 
-// Dezelfde lijst soorten als de server en de editor gebruiken. Zie
-// assets/js/shared/post-music-type.js: die module is puur, dus hij mag hier.
-import { SOORTEN } from '../assets/js/shared/post-music-type.js';
-
-// "Open in" icons (brand-colored via CSS .pat-link--).
-const OPEN_IN_SVG = {
-  spotify: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2a10 10 0 100 20 10 10 0 000-20zm4.6 14.42a.62.62 0 01-.86.21c-2.35-1.44-5.3-1.76-8.79-.96a.62.62 0 11-.28-1.21c3.8-.87 7.07-.5 9.71 1.11.3.18.39.57.22.85zm1.23-2.73a.78.78 0 01-1.07.26c-2.69-1.66-6.79-2.14-9.97-1.17a.78.78 0 11-.45-1.49c3.63-1.1 8.15-.56 11.24 1.33.36.22.48.7.25 1.07zm.1-2.85C14.66 8.95 9.4 8.78 6.3 9.72a.93.93 0 11-.54-1.79c3.56-1.08 9.37-.87 13.07 1.33a.94.94 0 01-.96 1.61z"/></svg>',
-  youtube: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M23 7.1a3 3 0 00-2.1-2.12C19.04 4.5 12 4.5 12 4.5s-7.04 0-8.9.48A3 3 0 001 7.1 31.2 31.2 0 00.5 12 31.2 31.2 0 001 16.9a3 3 0 002.1 2.12c1.86.48 8.9.48 8.9.48s7.04 0 8.9-.48A3 3 0 0023 16.9 31.2 31.2 0 0023.5 12 31.2 31.2 0 0023 7.1zM9.75 15.5v-7l6 3.5-6 3.5z"/></svg>',
-  soundcloud: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 14v4M7.5 11v7M11 9v9"/><path d="M14.5 9.5V18h4a3 3 0 100-6 4 4 0 00-4-2.5z"/></svg>',
-};
-
 class AudioEmbedService {
-  // Small "open in" links for a track (Spotify/YouTube/SoundCloud). The hrefs
-  // are already validated server-side (https + correct host only). Returns ''
-  // when no links exist. Placed next to the play button (outside the button →
-  // no conflict with playback).
-  static openInLinks(t) {
-    if (!t) return '';
-    const out = [];
-    const add = (url, key, label) => {
-      if (!url) return;
-      out.push(`<a class="pat-link pat-link--${key}" href="${this.escape(url)}" target="_blank" rel="noopener noreferrer" title="Open in ${label}" aria-label="Open in ${label}">${OPEN_IN_SVG[key]}</a>`);
-    };
-    add(t.link_spotify, 'spotify', 'Spotify');
-    add(t.link_youtube, 'youtube', 'YouTube');
-    add(t.link_soundcloud, 'soundcloud', 'SoundCloud');
-    return out.length ? `<span class="pat-links">${out.join('')}</span>` : '';
-  }
-
   static detectProvider(url) {
     if (!url || typeof url !== 'string') return null;
     url = url.trim();
 
-    // Only embed http(s) URLs. The provider regexes below are NOT anchored,
-    // so without this check e.g. `javascript:alert(1)//youtu.be/x` would match
-    // and land as an embed URL (stored XSS via an [[embed:...]] shortcode —
-    // that text never passes through the HTML sanitizer because it lives in a
-    // text node). The scheme guard excludes javascript:/data:/vbscript: etc.
-    if (!/^https?:\/\//i.test(url)) return null;
-
     // Spotify
     if (/open\.spotify\.com\/(track|album|playlist|episode|show)\/([A-Za-z0-9]+)/i.test(url)) {
       const match = url.match(/\/(track|album|playlist|episode|show)\/([A-Za-z0-9]+)/i);
-      return { provider: 'spotify', type: match[1], id: match[2], url };
+      return { provider: 'spotify', type: match[1], id: match[2] };
     }
 
@@ -72,35 +37,8 @@
     }
 
-    // YouTube — a video id is always exactly 11 characters (aligns with the
-    // client-side ytId() in embed-player.js, which also expects {11}).
-    //
-    // A link may carry a video, a playlist, or both, and until now we kept only
-    // the video and threw `list=` away -- so a link to an album played its first
-    // song and stopped. The ref now keeps whichever is there, in the same three
-    // shapes the Klonkt hub uses, so one ref travels between the two unchanged:
-    //
-    //   "<video>"           one video
-    //   "<video>?list=<L>"  that video, and on through the list
-    //   "list:<L>"          the whole playlist (YouTube's `videoseries`)
-    //
-    // `list` may sit before or after `v=` and is often entity-encoded (&amp;)
-    // in a baked href, hence the scan over the whole URL rather than a fixed
-    // order. A list id is 10-60 chars: longer and looser than a video id.
-    if (/(?:youtube(?:-nocookie)?\.com\/(?:watch\?|playlist\?|embed\/|shorts\/|live\/)|youtu\.be\/)/i.test(url)) {
-      const vm = url.match(/(?:[?&](?:amp;)?v=|youtu\.be\/|\/embed\/|\/shorts\/|\/live\/)([A-Za-z0-9_-]{11})(?![A-Za-z0-9_-])/i);
-      const lm = url.match(/[?&](?:amp;)?list=([A-Za-z0-9_-]{10,60})/i);
-      // `videoseries` is a marker, not a video: a bare playlist embed URL reads
-      // /embed/videoseries?list=..., and taking that for an id gives a dead
-      // frame. It is EXACTLY eleven characters, so no length rule catches it --
-      // it has to be named. (Measured, not assumed: it slipped through a
-      // boundary check that looked like it covered this.)
-      const id = vm && vm[1] !== 'videoseries' ? vm[1] : null;
-      const list = lm ? lm[1] : null;
-      if (id || list) {
-        const ref = id ? (list ? `${id}?list=${list}` : id) : `list:${list}`;
-        // `id` stays exactly what it was for every caller that only wants a
-        // video; `list` and `ref` are additions.
-        return { provider: 'youtube', id, list, ref, url };
-      }
+    // YouTube
+    if (/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([A-Za-z0-9_-]{6,20})/i.test(url)) {
+      const match = url.match(/(?:v=|youtu\.be\/|embed\/)([A-Za-z0-9_-]{6,20})/i);
+      return { provider: 'youtube', id: match[1] };
     }
 
@@ -108,64 +46,22 @@
     if (/vimeo\.com\/(?:video\/)?(\d+)/i.test(url)) {
       const match = url.match(/\d+/);
-      return { provider: 'vimeo', id: match[0], url };
+      return { provider: 'vimeo', id: match[0] };
     }
 
     return null;
-  }
-
-  // Direct media files (video/audio) hosted anywhere → a native <video>/<audio>
-  // player. Kept OUT of detectProvider() on purpose: the timeline/cover callers
-  // switch on provider slugs (youtube/spotify/…) and a bare file has none, so
-  // overloading detectProvider would suppress e.g. a PeerTube fallback. Only
-  // autoembed() and [[embed:…]] use this.
-  static MEDIA_FILE_EXT = {
-    video: ['mp4', 'webm', 'm4v', 'mov', 'ogv'],
-    audio: ['mp3', 'ogg', 'oga', 'wav', 'm4a', 'flac', 'opus', 'aac'],
-  };
-
-  static detectMediaFile(url) {
-    if (!url || typeof url !== 'string') return null;
-    if (!/^https?:\/\//i.test(url)) return null;
-    let pathname;
-    try { pathname = new URL(url).pathname.toLowerCase(); } catch { return null; }
-    const ext = (pathname.match(/\.([a-z0-9]+)$/) || [])[1];
-    if (!ext) return null;
-    if (this.MEDIA_FILE_EXT.video.includes(ext)) return { kind: 'video', url };
-    if (this.MEDIA_FILE_EXT.audio.includes(ext)) return { kind: 'audio', url };
-    return null;
-  }
-
-  static mediaFileEmbed(url) {
-    const m = this.detectMediaFile(url);
-    if (!m) return null;
-    const src = this.escape(m.url);
-    if (m.kind === 'video') {
-      return `<figure class="folio-embed folio-embed--video"><video src="${src}" controls preload="metadata" playsinline></video></figure>`;
-    }
-    return `<figure class="folio-embed folio-embed--audio"><audio src="${src}" controls preload="metadata"></audio></figure>`;
   }
 
   static generateIframe(provider, config) {
     switch (provider) {
-      // Custom players (client-side via embed-player.js + the real platform APIs).
-      // We render a placeholder with data attributes instead of the bare platform
-      // iframe, so the embed appears in OUR brand style.
-      case 'youtube':
-        // The ref carries the list when there is one; `id` alone would drop it
-        // and play a single song out of an album.
-        return this.embedPlaceholder('youtube', config.ref || config.id, 'video',
-          config.url || (config.id ? `https://youtu.be/${config.id}`
-                                   : `https://www.youtube.com/playlist?list=${config.list}`));
-      case 'soundcloud':
-        return this.embedPlaceholder('soundcloud', config.url, 'track', config.url);
       case 'spotify':
-        return this.embedPlaceholder('spotify', `spotify:${config.type}:${config.id}`,
-          config.type, config.url || `https://open.spotify.com/${config.type}/${config.id}`);
-      // No JS API (Bandcamp/Apple) or low priority (Vimeo): remain as iframes;
-      // mutual exclusion for these runs via the blur fallback.
+        return this.spotifyIframe(config);
       case 'bandcamp':
         return this.bandcampIframe(config);
+      case 'soundcloud':
+        return this.soundcloudIframe(config);
       case 'applemusic':
         return this.applemusicIframe(config);
+      case 'youtube':
+        return this.youtubeIframe(config);
       case 'vimeo':
         return this.vimeoIframe(config);
@@ -173,19 +69,4 @@
         return null;
     }
-  }
-
-  /**
-   * Placeholder for a custom player. embed-player.js picks up
-   * .folio-embed[data-embed-provider] and builds the card + player client-side.
-   * ALL values go through escape() — post.content_html is executed unescaped.
-   */
-  static embedPlaceholder(provider, ref, type, url) {
-    const attrs = [
-      `data-embed-provider="${this.escape(provider)}"`,
-      `data-embed-ref="${this.escape(ref)}"`,
-      type ? `data-embed-type="${this.escape(type)}"` : '',
-      `data-embed-url="${this.escape(url)}"`,
-    ].filter(Boolean).join(' ');
-    return `<div class="folio-embed folio-embed--${this.escape(provider)} pcms-embed pcms-embed-card pcms-embed-loading" ${attrs}></div>`;
   }
 
@@ -243,31 +124,11 @@
 
   static applemusicIframe({ url }) {
-    // Een album of nummer heeft een NUMMER als id, een afspeellijst niet: die
-    // heet `pl.u-LdbqzVvI3go5g`. Met alleen [0-9]+ viel elke playlist hier af
-    // en gaf deze functie null -- waarna de shortcode zelf op de pagina kwam.
-    // Barts melding (17-8): het concept "The Mixtape" toonde in preview
-    // letterlijk [[embed:https://music.apple.com/nl/playlist/...]].
-    //
-    // Bewust krap: geen slash, vraagteken of hekje in het id, want wat hier
-    // gevangen wordt gaat rechtstreeks achter https://embed.music.apple.com/ aan.
-    const match = url.match(
-      /music\.apple\.com\/([a-z]{2}\/(album|playlist|song)\/[^/?#]+\/(?:[0-9]+|pl\.[A-Za-z0-9_-]+))/i,
-    );
+    const match = url.match(/music\.apple\.com\/([a-z]{2}\/(?:album|playlist|song)\/[^/?#]+\/[0-9]+)/i);
     if (!match) return null;
     const src = `https://embed.music.apple.com/${match[1]}`;
-    // De hoogte hangt af van WAT je insluit, en dat stond hier op een vaste
-    // 175px -- de maat van een LOS NUMMER. Een album of afspeellijst is 450px,
-    // dus daarvan zag je ongeveer een derde, met `overflow:hidden` eroverheen
-    // zodat de rest ook niet te bereiken viel. Barts melding (20-8) over
-    // boiert.eu/the-mixtape.
-    //
-    // Nagemeten en niet overgenomen: de embed-pagina van die lijst
-    // (pl.u-LdbqzVvI3go5g) geeft zijn <main> EN zijn <body> allebei precies
-    // 450px. Dat is ook de hoogte in Apple's eigen insluitcode.
-    const hoogte = String(match[2]).toLowerCase() === 'song' ? 175 : 450;
     return `
       <figure class="folio-embed folio-embed--applemusic">
         <iframe src="${this.escape(src)}"
-                style="width:100%;height:${hoogte}px;border:0;overflow:hidden;border-radius:8px;"
+                style="width:100%;height:175px;border:0;overflow:hidden;border-radius:8px;"
                 loading="lazy"
                 allow="autoplay; clipboard-write; encrypted-media"
@@ -277,23 +138,6 @@
   }
 
-  /**
-   * The plain provider iframe. Takes the same ref shapes as the placeholder:
-   * "<video>", "<video>?list=<L>" and "list:<L>" -- a bare playlist embeds as
-   * `videoseries`. Kept in step with the placeholder path on purpose: this is
-   * the fallback, and a fallback that silently drops the playlist is the worst
-   * kind, because it looks like it worked.
-   */
-  static youtubeIframe({ id, ref }) {
-    const r = ref || id || '';
-    const base = 'https://www.youtube-nocookie.com/embed/';
-    let src;
-    if (r.startsWith('list:')) {
-      src = `${base}videoseries?list=${encodeURIComponent(r.slice(5))}`;
-    } else if (r.includes('?list=')) {
-      const [v, l] = r.split('?list=');
-      src = `${base}${encodeURIComponent(v)}?list=${encodeURIComponent(l)}`;
-    } else {
-      src = base + encodeURIComponent(r);
-    }
+  static youtubeIframe({ id }) {
+    const src = `https://www.youtube-nocookie.com/embed/${id}`;
     return `
       <figure class="folio-embed folio-embed--youtube">
@@ -340,37 +184,7 @@
           return iframe || match;
         }
-        // Bare media file (…/clip.webm, …/song.mp3) → native player.
-        const media = this.mediaFileEmbed(url);
-        if (media) return media;
         return match;
       }
     );
-  }
-
-  /**
-   * Replace [[embed:<url>]] shortcodes with the platform iframe (YouTube, Spotify,
-   * SoundCloud, Apple Music, Bandcamp, Vimeo). The editor button inserts this
-   * shortcode; bare URL lines also embed automatically via autoembed().
-   * Unsupported/invalid URLs get a clean inline notice.
-   */
-  static embedMediaShortcodes(html) {
-    if (!html) return html;
-    return html.replace(/\[\[embed:([^\]]+)\]\]/gi, (match, rawUrl) => {
-      const url = rawUrl.trim().replace(/&amp;/g, '&');
-      const detected = this.detectProvider(url);
-      if (!detected) {
-        // Bare media file (…/clip.webm, …/song.mp3) → native player.
-        const media = this.mediaFileEmbed(url);
-        if (media) return media;
-        return `<div class="post-embed-missing"><em>Embed: niet-ondersteunde of ongeldige URL.</em></div>`;
-      }
-      // HERKEND maar niet te bouwen is geen reden om de shortcode zelf te
-      // tonen. Dat deed het wel, en dan leest een bezoeker "[[embed:https://...]]"
-      // op de pagina en denkt hij dat er iets stuk is. Onherkend gaf hierboven
-      // al een nette melding; herkend-maar-mislukt hoort dezelfde te geven,
-      // want voor de lezer is het hetzelfde geval.
-      return this.generateIframe(detected.provider, detected)
-        || `<div class="post-embed-missing"><em>Embed: niet-ondersteunde of ongeldige URL.</em></div>`;
-    });
   }
 
@@ -384,42 +198,17 @@
     return html.replace(/\[\[track:([A-Za-z0-9_-]+)\]\]/g, (match, id) => {
       const t = trackLookup(id);
-      if (!t) return match;
-      const titleH0 = this.escape(t.title || 'Untitled');
-      const artistH0 = this.escape(t.artist || '');
-      const creditBits0 = [this.escape(t.credit || ''), this.escape(t.license || '')].filter(Boolean).join(' · ');
-      // Link-only track (no audio file): no play button, but info + open-in links.
-      if (!t.url) {
-        const coverH0 = this.escape(t.cover || '');
-        const leader0 = coverH0
-          ? `<span class="pat-noplay pat-noplay--cover" style="background-image:url('${coverH0}')" aria-hidden="true"></span>`
-          : `<span class="pat-noplay" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg></span>`;
-        return `<div class="post-audio-track post-audio-track--static" id="track-${id}">
-  ${leader0}
-  <div class="pat-info">
-    <div class="pat-title">${titleH0}</div>
-    ${artistH0 ? `<div class="pat-artist">${artistH0}</div>` : ''}
-    ${creditBits0 ? `<div class="pat-credit">${creditBits0}</div>` : ''}
-  </div>
-  ${this.openInLinks(t)}
-</div>`;
-      }
+      if (!t || !t.url) return match;
       const trackJson = JSON.stringify({
-        id,
         url: t.url,
         title: t.title || 'Untitled',
         artist: t.artist || '',
         cover: t.cover || '',
-        credit: t.credit || '',
-        license: t.license || '',
       });
       const titleH = this.escape(t.title || 'Untitled');
       const artistH = this.escape(t.artist || '');
       const urlH = this.escape(t.url);
-      // Visible owner/license line below the track.
-      const creditBits = [this.escape(t.credit || ''), this.escape(t.license || '')].filter(Boolean).join(' · ');
       const dataAttr = trackJson
         .replace(/&/g, '&amp;').replace(/'/g, '&#39;').replace(/</g, '&lt;');
-      // id="track-<id>" = anchor so the mini-player can scroll to this element.
-      return `<div class="post-audio-track" id="track-${id}" data-pcms-track-id="${id}" data-pcms-track-url="${urlH}" data-pcms-track='${dataAttr}'>
+      return `<div class="post-audio-track" data-pcms-track-url="${urlH}" data-pcms-track='${dataAttr}'>
   <button type="button" class="pat-play" aria-label="Play ${titleH}">
     <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 4l12 8-12 8z"/></svg>
@@ -428,7 +217,5 @@
     <div class="pat-title">${titleH}</div>
     ${artistH ? `<div class="pat-artist">${artistH}</div>` : ''}
-    ${creditBits ? `<div class="pat-credit">${creditBits}</div>` : ''}
   </div>
-  ${this.openInLinks(t)}
 </div>`;
     });
@@ -451,7 +238,5 @@
       // Stable DOM id for this rendering — used as data-pcms-album-id on tracks
       const albumDomId = 'album-' + Math.random().toString(36).slice(2, 10);
-      // Only playable tracks (with url) in the queue; link-only tracks appear
-      // in the list but not in the playback JSON.
-      const albumJson = JSON.stringify(album.tracks.filter((t) => t.url))
+      const albumJson = JSON.stringify(album.tracks)
         .replace(/&/g, '&amp;').replace(/'/g, '&#39;').replace(/</g, '&lt;');
       const titleH = this.escape(album.title || name);
@@ -462,17 +247,6 @@
         const tTitle = this.escape(t.title || ('Track ' + (i + 1)));
         const tArtist = this.escape(t.artist || '');
-        // Link-only track: no play button, but track number + info + open-in links.
-        if (!t.url) {
-          return `    <li class="post-audio-track post-audio-track--static"${t.id ? ` id="track-${t.id}"` : ''}>
-      <span class="pat-track-num">${i + 1}.</span>
-      <div class="pat-info">
-        <div class="pat-title">${tTitle}</div>
-        ${tArtist && tArtist !== artistH ? `<div class="pat-artist">${tArtist}</div>` : ''}
-      </div>
-      ${this.openInLinks(t)}
-    </li>`;
-        }
         const tUrl = this.escape(t.url);
-        return `    <li class="post-audio-track"${t.id ? ` id="track-${t.id}" data-pcms-track-id="${t.id}"` : ''} data-pcms-track-url="${tUrl}" data-pcms-album-id="${albumDomId}">
+        return `    <li class="post-audio-track" data-pcms-track-url="${tUrl}" data-pcms-album-id="${albumDomId}">
       <button type="button" class="pat-play" aria-label="Play ${tTitle}">
         <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 4l12 8-12 8z"/></svg>
@@ -483,5 +257,4 @@
         ${tArtist && tArtist !== artistH ? `<div class="pat-artist">${tArtist}</div>` : ''}
       </div>
-      ${this.openInLinks(t)}
     </li>`;
       }).join('\n');
@@ -542,10 +315,6 @@
 
       const albumDomId = 'album-' + id;
-      // De soort zoals hij is opgeslagen. Stond hier als ternair met twee
-      // uitkomsten, en dan draagt een mixtape het jasje en het woord van een
-      // album -- dezelfde vorm die op vier andere plekken al misging.
-      const kind = SOORTEN.includes(pl.kind) ? pl.kind : 'album';
-      const KIND_LABEL = { album: '💿 Album', playlist: '📃 Playlist', mixtape: '📼 Mixtape' };
-      const kindLabel = KIND_LABEL[kind] || KIND_LABEL.album;
+      const kind = (pl.kind === 'playlist') ? 'playlist' : 'album';
+      const kindLabel = kind === 'playlist' ? '📃 Playlist' : '💿 Album';
       const titleH  = this.escape(pl.title || 'Naamloos');
       const artistH = this.escape(pl.artist || '');
@@ -554,17 +323,9 @@
       // Audio-player.js reads data-pcms-album for queue. Same shape as
       // embedAlbumShortcodes — keep both in sync.
-      // Only playable tracks in the queue; link-only tracks appear in the list
-      // but not in the playback JSON.
-      const tracksData = pl.tracks.filter(t => t.url).map(t => ({
-        id:     t.id,
+      const tracksData = pl.tracks.map(t => ({
         url:    t.url,
         title:  t.title,
         artist: t.artist || pl.artist || '',
         cover:  t.cover  || pl.cover  || '',
-        // De duur gaat mee omdat een BANDJE een lengte heeft. Zonder dit kan de
-        // speler alleen de gebufferde keten optellen, en dan las de teller
-        // 2:05 met een nummer geladen en 4:07 met drie -- een totaal dat
-        // meegroeit terwijl je luistert.
-        duration: Number(t.duration) || 0,
       }));
       const albumJson = JSON.stringify(tracksData)
@@ -583,5 +344,5 @@
       }
       const metaLine = this.escape(metaParts.join(' · '));
-      const firstUrl = this.escape((pl.tracks.find(t => t.url) || {}).url || '');
+      const firstUrl = this.escape(pl.tracks[0].url);
 
       // Track items — playlist-kind shows per-track cover thumbs, album-kind shows numbers
@@ -602,20 +363,6 @@
           : `<span class="pat-num">${i + 1}</span>`;
 
-        // Link-only track: no clickable play row (static div), but open-in links.
-        if (!t.url) {
-          return `    <li class="post-album-track-compact post-album-track-compact--static"${t.id ? ` id="track-${t.id}"` : ''}>
-      <div class="pat-row pat-static">
-        ${leader}
-        <span class="pat-meta">
-          <span class="pat-title">${tTitleH}</span>
-          ${showArtist ? `<span class="pat-artist">${tArtistH}</span>` : ''}
-        </span>
-        ${durHtml}
-      </div>
-      ${this.openInLinks(t)}
-    </li>`;
-        }
         const trackBase = String(t.url).split('?')[0];
-        return `    <li class="post-album-track-compact"${t.id ? ` id="track-${t.id}" data-pcms-track-id="${t.id}"` : ''}>
+        return `    <li class="post-album-track-compact">
       <button type="button" class="pat-row"
               data-pcms-track-url="${tUrl}"
@@ -630,19 +377,6 @@
         ${durHtml}
       </button>
-      ${this.openInLinks(t)}
     </li>`;
       }).join('\n');
-
-      // HET BANDJE HEEFT ZIJN EIGEN VORM. Een album toont een genummerde lijst
-      // waar je in kunt prikken; een cassette is juist het tegenovergestelde --
-      // je hoort wat er komt, in de volgorde waarin het is opgenomen. Alles
-      // hierboven (de wachtrij, de metaregel, de duur) is gedeeld; alleen de
-      // opmaak splitst hier.
-      if (kind === 'mixtape') {
-        return this.renderTape({
-          domId: albumDomId, id, titleH, artistH, coverH, metaLine, firstUrl,
-          albumJson, tracks: pl.tracks, isAdmin,
-        });
-      }
 
       return `<div class="post-album" id="${albumDomId}"
@@ -663,5 +397,5 @@
             data-pcms-track-url="${firstUrl}"
             data-pcms-album-id="${albumDomId}"
-            aria-label="Speel ${kind}">
+            aria-label="Speel ${kind === 'playlist' ? 'playlist' : 'album'}">
       ${coverH
         ? `<span class="post-album-cover" style="background-image:url('${coverH}')"></span>`
@@ -688,109 +422,4 @@
 
   /**
-   * Het bandje (Robins idee, 21-8).
-   *
-   * WAT HET ANDERS MAAKT DAN EEN ALBUM, en dat is de hele reden dat dit een
-   * eigen vorm heeft: bij een album prik je in een genummerde lijst en spring
-   * je naar nummer zeven. Op een cassette kan dat niet. Je spoelt vooruit of
-   * terug, en wat er komt hoor je in de volgorde waarin het is opgenomen. De
-   * lijst staat er dus wel -- je mag zien wat erop staat -- maar hij is geen
-   * knoppenrij.
-   *
-   * DE SPELER IS DE BESTAANDE SPELER. De knop hieronder draagt exact dezelfde
-   * data-attributen als de albumhoes (data-pcms-track-url + data-pcms-album-id),
-   * dus audio-player.js pakt hem op zonder dat hier iets nieuws bij komt. Vooruit
-   * en terug lopen via window.pcmsAudioPlayer.next()/prev(), en dat is meteen de
-   * reden dat spoelen per NUMMER gaat en niet per seconde: die speler denkt in
-   * een wachtrij, en een tweede speler ernaast bouwen om een band na te doen zou
-   * twee dingen tegelijk laten afspelen.
-   */
-  static renderTape({ domId, id, titleH, artistH, coverH, metaLine, firstUrl, albumJson, tracks, isAdmin }) {
-    // De nummers als tekst, niet als knoppen. Bewust geen data-pcms-track-url:
-    // een aanklikbaar nummer is precies wat een bandje niet heeft.
-    const lijst = tracks.map((t, i) => {
-      const tTitle = this.escape(t.title || ('Track ' + (i + 1)));
-      const dur = t.duration > 0
-        ? `${Math.floor(t.duration / 60)}:${String(t.duration % 60).padStart(2, '0')}`
-        : '—:—';
-      return `      <li class="tape-track" data-tape-index="${i}"><span class="tape-track-title">${tTitle}</span><span class="tape-track-dur">${dur}</span></li>`;
-    }).join('\n');
-
-    const spoel = (richting, label, pad) => `    <button type="button" class="tape-btn tape-btn--${richting}" data-tape-go="${richting}" aria-label="${label}">
-      <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">${pad}</svg>
-    </button>`;
-
-    return `<div class="post-tape" id="${domId}"
-     data-pcms-album='${albumJson}'
-     data-pcms-album-title="${titleH}"
-     data-pcms-album-kind="mixtape"
-     data-pcms-playlist-id="${this.escape(id)}">
-${isAdmin ? `  <div class="post-album-actions" role="group" aria-label="Mixtape beheren">
-    <a class="post-album-action" href="/admin/playlists?edit=${this.escape(id)}" title="Bewerk mixtape" aria-label="Bewerk mixtape">
-      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
-    </a>
-  </div>
-` : ''}  <div class="tape-shell">
-    <svg class="tape-svg" viewBox="0 0 314 200" role="img" aria-label="Cassette" preserveAspectRatio="xMidYMid meet">
-      <defs>
-        <clipPath id="tapewin-${domId}"><rect x="72" y="96" width="170" height="62" rx="6"/></clipPath>
-      </defs>
-      <!-- de behuizing -->
-      <rect class="tape-body" x="3" y="3" width="308" height="194" rx="9"/>
-      <rect class="tape-body-inner" x="12" y="12" width="290" height="176" rx="6"/>
-      <!-- het labelvlak: hier komt de HTML-tekst overheen te staan -->
-      <rect class="tape-labelplate" x="24" y="22" width="266" height="62" rx="3"/>
-      <!-- het venster waardoor je de band ziet lopen -->
-      <rect class="tape-glass" x="72" y="96" width="170" height="62" rx="6"/>
-      <g clip-path="url(#tapewin-${domId})">
-        <!-- de bandpakketten om de spoelen; links loopt vol terwijl rechts leegloopt -->
-        <circle class="tape-pack tape-pack--left" cx="112" cy="127" r="30"/>
-        <circle class="tape-pack tape-pack--right" cx="202" cy="127" r="22"/>
-        <rect class="tape-ribbon" x="112" y="150" width="90" height="3"/>
-      </g>
-      <!-- de spoelen zelf: deze twee draaien -->
-      <g class="tape-reel tape-reel--left" style="transform-origin:112px 127px">
-        <circle class="tape-hub" cx="112" cy="127" r="15"/>
-        ${[0, 60, 120, 180, 240, 300].map((a) => `<rect class="tape-tooth" x="109.5" y="112" width="5" height="9" rx="1" transform="rotate(${a} 112 127)"/>`).join('')}
-      </g>
-      <g class="tape-reel tape-reel--right" style="transform-origin:202px 127px">
-        <circle class="tape-hub" cx="202" cy="127" r="15"/>
-        ${[0, 60, 120, 180, 240, 300].map((a) => `<rect class="tape-tooth" x="199.5" y="112" width="5" height="9" rx="1" transform="rotate(${a} 202 127)"/>`).join('')}
-      </g>
-      <!-- de schroefjes in de hoeken, en de openingen voor de kop onderin -->
-      <circle class="tape-screw" cx="22" cy="22" r="3.5"/>
-      <circle class="tape-screw" cx="292" cy="22" r="3.5"/>
-      <circle class="tape-screw" cx="22" cy="178" r="3.5"/>
-      <circle class="tape-screw" cx="292" cy="178" r="3.5"/>
-      <rect class="tape-slot" x="128" y="168" width="26" height="14" rx="2"/>
-      <rect class="tape-slot" x="160" y="168" width="26" height="14" rx="2"/>
-      <rect class="tape-slot tape-slot--capstan" x="98" y="170" width="10" height="12" rx="2"/>
-      <rect class="tape-slot tape-slot--capstan" x="206" y="170" width="10" height="12" rx="2"/>
-    </svg>
-    <div class="tape-label">
-      <p class="tape-kind">Mixtape</p>
-      <h3 class="tape-title">${titleH}</h3>
-      ${artistH ? `<p class="tape-artist">${artistH}</p>` : ''}
-      <p class="tape-meta">${metaLine}</p>
-    </div>
-  </div>
-  <div class="tape-controls" role="group" aria-label="Bandje bedienen">
-${spoel('back', 'Terugspoelen', '<path d="M11 12l9-7v14zM2 12l9-7v14z"/>')}
-    <button type="button" class="tape-btn tape-btn--play"
-            data-pcms-track-url="${firstUrl}"
-            data-pcms-album-id="${domId}"
-            data-tape-play
-            aria-label="Afspelen">
-      <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 4l12 8-12 8z"/></svg>
-    </button>
-${spoel('fwd', 'Vooruitspoelen', '<path d="M13 12L4 5v14zM22 12l-9-7v14z"/>')}
-  </div>
-  <p class="tape-now" data-tape-now aria-live="polite"></p>
-  <ol class="tape-tracks">
-${lijst}
-  </ol>
-</div>`;
-  }
-
-  /**
    * Human-readable label for a provider slug. Used by external-link buttons.
    */
@@ -834,5 +463,5 @@
    * Replace [[link:url]] or [[link:url|Custom Label]] shortcodes with a
    * branded "Open in <Platform>" anchor (no iframe). Opens in new tab.
-   * Per Robin's v9: "External link, click = open platform (target _blank)".
+   * Per Robin's v9: "Externe link, klik = open platform (target _blank)".
    */
   static embedExternalLinkShortcodes(html) {
