Index: src/assets/css/style.css
===================================================================
--- src/assets/css/style.css	(revision 4fbe2c1f0fd6e892ba64f0b6b6b9d72393a93573)
+++ src/assets/css/style.css	(revision c0e463b1f870f3f742bb65ca7ee345c081ff782b)
@@ -5235,2 +5235,26 @@
     background: color-mix(in srgb, var(--accent) 12%, transparent);
 }
+
+
+/* ── De speler in bandmodus ────────────────────────────────────────────
+   Zelfde speler, ander gedrag: op een cassette kies je geen nummer. De lijst
+   blijft staan zodat je ziet wat erop staat, maar zonder de opmaak die belooft
+   dat je erin kunt prikken. */
+.audio-sheet-queue-list.is-tape .audio-sheet-queue-item {
+    cursor: default;
+    pointer-events: none;
+}
+.audio-sheet-queue-list.is-tape .audio-sheet-queue-item:hover {
+    background: none;
+}
+/* Het lopende nummer blijft wel gemarkeerd -- dat is de enige plek waar je op
+   een bandje leest waar je bent. */
+.audio-sheet-queue-list.is-tape .audio-sheet-queue-item.is-current {
+    opacity: 1;
+}
+/* Spoelknoppen mogen niet als "volgende" voelen: vasthouden hoort geen tekst te
+   selecteren of de pagina te laten zoomen. */
+.audio-btn.is-wind {
+    touch-action: manipulation;
+    user-select: none;
+}
Index: src/assets/js/audio-player.js
===================================================================
--- src/assets/js/audio-player.js	(revision 4fbe2c1f0fd6e892ba64f0b6b6b9d72393a93573)
+++ src/assets/js/audio-player.js	(revision c0e463b1f870f3f742bb65ca7ee345c081ff782b)
@@ -27,4 +27,7 @@
     vol:   '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 9v6h4l5 5V4L7 9H3zM16 8a5 5 0 010 8M19 5a9 9 0 010 14" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>',
     mute:  '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 9v6h4l5 5V4L7 9H3zM17 9l5 5M22 9l-5 5" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>',
+    // Dubbele driehoeken: het teken voor spoelen, niet voor overslaan.
+    rew:   '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11 12l9-7v14zM2 12l9-7v14z" fill="currentColor"/></svg>',
+    ff:    '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13 12L4 5v14zM22 12l-9-7v14z" fill="currentColor"/></svg>',
     musicNote: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 17V5l12-2v12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="6" cy="17" r="3" fill="currentColor"/><circle cx="18" cy="15" r="3" fill="currentColor"/></svg>',
   };
@@ -128,4 +131,8 @@
   const prevBtn = $('audio-prev');
   const nextBtn = $('audio-next');
+  // De vier knoppen die in bandmodus van betekenis veranderen: vorige/volgende
+  // worden terugspoelen/vooruitspoelen. Ze staan hier bij elkaar zodat het
+  // omzetten op EEN plek gebeurt.
+  const SPOELKNOPPEN = ['audio-prev', 'audio-next', 'audio-sheet-prev', 'audio-sheet-next'];
   const muteBtn = $('audio-mute');
   const volumeSlider = $('audio-volume');
@@ -491,10 +498,38 @@
   }
 
+  // De lengte van de hele band, uit de bekende trackduren. Null zodra er van
+  // een nummer geen duur bekend is: een som met gaten is een verzonnen getal,
+  // en dan valt de teller liever terug op wat hij wel zeker weet.
+  function bandDuur() {
+    if (!queue.length) return null;
+    let som = 0;
+    for (const t of queue) {
+      const d = Number(t && t.duration) || 0;
+      if (d <= 0) return null;
+      som += d;
+    }
+    return som;
+  }
+
+  /** Hoeveel band ligt er voor nummer `i`. */
+  function bandOffset(i) {
+    let som = 0;
+    for (let n = 0; n < i && n < queue.length; n++) som += Number(queue[n].duration) || 0;
+    return som;
+  }
+
   function displayTimes() {
-    // Een bandje heeft een teller, geen nummerpositie: hij telt door over de
-    // kant heen. Dat is letterlijk de ketentijdlijn, dus hier juist NIET
-    // terugrekenen naar trackcoordinaten.
-    if (tapeMode && useMse() && chain.length) {
-      return { cur: audio.currentTime || 0, dur: chain[chain.length - 1].end };
+    // EEN BANDJE HEEFT EEN TELLER, geen nummerpositie. Hij telt door over de
+    // kant heen.
+    //
+    // Uit de trackduren en niet uit de keten, en dat verschil is zichtbaar: de
+    // keten bevat alleen wat gebufferd is, dus het totaal groeide mee tijdens
+    // het luisteren (2:05 met een nummer geladen, 4:07 met drie). En sprong je
+    // naar nummer drie, dan begon de keten daar opnieuw op nul en stond de
+    // teller weer aan het begin van de band.
+    if (tapeMode) {
+      const totaal = bandDuur();
+      if (totaal) return { cur: bandOffset(currentIndex) + (trackTijd().cur || 0), dur: totaal };
+      if (useMse() && chain.length) return { cur: audio.currentTime || 0, dur: chain[chain.length - 1].end };
     }
     return trackTijd();
@@ -621,4 +656,5 @@
     albumName = (opts && opts.albumName) || '';
     tapeMode = !!(opts && opts.asTape);
+    zetKnopStanden();
     if (!queue.length) return;
     loadTrack(typeof startIdx === 'number' ? Math.max(0, Math.min(startIdx, queue.length - 1)) : 0, true);
@@ -681,4 +717,5 @@
     albumName = '';
     tapeMode = false;
+    zetKnopStanden();
     loadSeq++;  // cancel any in-flight load
     dropPreload();
@@ -700,4 +737,9 @@
       sheetQueueList.appendChild(li);
     });
+    // OP EEN BANDJE KIES JE NIET. De lijst blijft staan -- je mag zien wat
+    // erop staat -- maar zonder klik en zonder de opmaak die belooft dat het
+    // kan. Dat is hetzelfde onderscheid als bij de cassette in de post.
+    sheetQueueList.classList.toggle('is-tape', tapeMode);
+    if (tapeMode) return;
     sheetQueueList.querySelectorAll('.audio-sheet-queue-item').forEach((li) => {
       li.addEventListener('click', () => {
@@ -883,9 +925,41 @@
   // ============================================================
   playBtn.addEventListener('click', togglePlay);
-  prevBtn.addEventListener('click', prev);
-  nextBtn.addEventListener('click', next);
+  /**
+   * De vier knoppen in de juiste stand zetten. In bandmodus zijn het
+   * spoelknoppen: ander teken, ander woord, en vasthouden spoelt door.
+   */
+  function zetKnopStanden() {
+    for (const id of SPOELKNOPPEN) {
+      const b = $(id);
+      if (!b) continue;
+      const vooruit = id.endsWith('next');
+      b.innerHTML = tapeMode ? (vooruit ? SVG.ff : SVG.rew) : (vooruit ? SVG.next : SVG.prev);
+      const label = tapeMode ? (vooruit ? 'Vooruitspoelen' : 'Terugspoelen') : (vooruit ? 'Volgende' : 'Vorige');
+      b.setAttribute('aria-label', label);
+      b.setAttribute('title', label);
+      b.classList.toggle('is-wind', tapeMode);
+    }
+  }
+
+  // Een KLIK: een nummer verder, of in bandmodus een stukje spoelen. Dat laatste
+  // is er voor toetsenbord en schermlezer, want vasthouden is met een
+  // spatiebalk geen gebaar.
+  const TIK_SPOEL_S = 5;
+  prevBtn.addEventListener('click', () => (tapeMode ? seekBy(-TIK_SPOEL_S) : prev()));
+  nextBtn.addEventListener('click', () => (tapeMode ? seekBy(TIK_SPOEL_S) : next()));
   sheetPlay.addEventListener('click', togglePlay);
-  sheetPrev.addEventListener('click', prev);
-  sheetNext.addEventListener('click', next);
+  sheetPrev.addEventListener('click', () => (tapeMode ? seekBy(-TIK_SPOEL_S) : prev()));
+  sheetNext.addEventListener('click', () => (tapeMode ? seekBy(TIK_SPOEL_S) : next()));
+
+  // VASTHOUDEN spoelt door. Op de knop zelf indrukken, maar loslaten op
+  // document: glijdt je vinger van de knop af, dan hoort de band te stoppen en
+  // niet door te blijven spoelen.
+  for (const id of SPOELKNOPPEN) {
+    const b = $(id);
+    if (!b) continue;
+    b.addEventListener('pointerdown', () => { if (tapeMode) startWind(id.endsWith('next') ? 1 : -1); });
+  }
+  document.addEventListener('pointerup', stopWind);
+  document.addEventListener('pointercancel', stopWind);
 
   // ============================================================
@@ -1199,4 +1273,43 @@
   }
 
+  /**
+   * SPOELEN, met de knop ingedrukt.
+   *
+   * Stond eerst in assets/js/mod/tape.js, want daar zat de cassette. Nu de
+   * speler zelf ook spoelknoppen krijgt zou dezelfde lus op twee plekken staan,
+   * en dan lopen ze uit elkaar zodra iemand aan de versnelling draait. Hier is
+   * de enige plek; de cassette op de pagina roept deze aan.
+   */
+  const WIND_START = 4;        // maal de normale snelheid bij het indrukken
+  const WIND_MAX = 16;
+  const WIND_VERSNELLING = 1.35;
+  const WIND_STAP_MS = 120;
+  let winder = null;
+
+  function stopWind() {
+    if (winder) { clearInterval(winder.timer); winder = null; }
+  }
+
+  function startWind(richting) {
+    stopWind();
+    const r = richting < 0 ? -1 : 1;
+    let snelheid = WIND_START;
+    let vorige = null;
+    let stil = 0;
+    const timer = setInterval(() => {
+      snelheid = Math.min(WIND_MAX, snelheid * WIND_VERSNELLING);
+      seekBy(r * snelheid * (WIND_STAP_MS / 1000));
+      // AAN DE KOP EN DE STAART STOPPEN. De positie wordt afgeklemd, dus daar
+      // gebeurt niets meer -- maar zonder deze controle blijft de knop malen en
+      // draaien de spoelen door alsof er nog band is.
+      const nu = displayTimes().cur;
+      if (vorige !== null && Math.abs(nu - vorige) < 0.05) {
+        if (++stil >= 2) { stopWind(); return; }
+      } else stil = 0;
+      vorige = nu;
+    }, WIND_STAP_MS);
+    winder = { timer, richting: r };
+  }
+
   /** Waar staat de teller, over het hele bandje. */
   function tapeTijden() { return displayTimes(); }
@@ -1204,5 +1317,7 @@
   window.pcmsAudioPlayer = {
     setQueue, play, pause, next, prev, close, openSheet, closeSheet, seekBy, tapeTijden,
+    startWind, stopWind,
     isTape: () => tapeMode,
+    isWinding: () => !!winder,
     isPlaying: () => isPlaying,
     currentTrack: () => queue[currentIndex] || null,
@@ -1252,4 +1367,5 @@
     albumName = s.albumName || '';
     tapeMode = !!s.tapeMode;
+    zetKnopStanden();
     pendingSeek = s.time || 0;
     const idx = Math.max(0, Math.min(s.currentIndex || 0, queue.length - 1));
Index: src/assets/js/mod/tape.js
===================================================================
--- src/assets/js/mod/tape.js	(revision 4fbe2c1f0fd6e892ba64f0b6b6b9d72393a93573)
+++ src/assets/js/mod/tape.js	(revision c0e463b1f870f3f742bb65ca7ee345c081ff782b)
@@ -14,4 +14,8 @@
  * die tijdlijn. Vandaar seekBy() en niet next()/prev() -- springen per nummer
  * is een playlistgebaar, en dan is dit een lijst met een cassetteplaatje.
+ *
+ * DE SPOELLUS STAAT IN DE SPELER, niet hier: sinds de speler onderin dezelfde
+ * spoelknoppen heeft zou dezelfde versnelling op twee plekken staan. Deze
+ * module bedient hem en tekent het jasje.
  *
  * WAAROM ER GEPOLLD WORDT. audio-player.js zendt geen gebeurtenissen uit -- het
@@ -56,4 +60,9 @@
   tape.classList.toggle('is-playing', draait);
   tape.classList.toggle('is-loaded', i >= 0);
+  // Spoelt de SPELER (bijvoorbeeld via zijn eigen knoppen onderin), dan hoort
+  // deze cassette dat ook te laten zien.
+  const windt = i >= 0 && !!p && p.isWinding && p.isWinding();
+  if (!windt) tape.classList.remove('is-winding', 'is-winding-back');
+  else tape.classList.add('is-winding');
 
   const knop = tape.querySelector('[data-tape-play]');
@@ -91,14 +100,9 @@
  * schermlezer, want vasthouden is met een spatiebalk geen gebaar.
  */
-const START_SNELHEID = 4;    // maal de normale snelheid bij het indrukken
-const MAX_SNELHEID = 16;
-const VERSNELLING = 1.35;    // per stap
-const STAP_MS = 120;
 const TIK_STAP_S = 5;        // een losse klik
 
-let winder = null;
-
 function stopWinden(tape) {
-  if (winder) { clearInterval(winder.timer); winder = null; }
+  const p = speler();
+  if (p && p.stopWind) p.stopWind();
   if (tape) tape.classList.remove('is-winding', 'is-winding-back');
 }
@@ -106,26 +110,12 @@
 function startWinden(tape, richting) {
   const p = speler();
-  if (!p || huidigeIndex(tape) < 0) return;
-  stopWinden(tape);
-  let snelheid = START_SNELHEID;
-  let vorigePositie = null;
-  let stil = 0;
+  if (!p || !p.startWind || huidigeIndex(tape) < 0) return;
+  // DE LUS STAAT IN DE SPELER. Hij stond hier, en toen kreeg de speler zelf ook
+  // spoelknoppen -- twee kopieen van dezelfde versnelling die uit elkaar lopen
+  // zodra iemand aan een getal draait. Deze module zet alleen nog het jasje:
+  // welke spoelen draaien en hoe hard.
+  p.startWind(richting);
   tape.classList.add('is-winding');
   if (richting < 0) tape.classList.add('is-winding-back');
-  const timer = setInterval(() => {
-    snelheid = Math.min(MAX_SNELHEID, snelheid * VERSNELLING);
-    p.seekBy(richting * snelheid * (STAP_MS / 1000));
-    tekenBand(tape);
-    // AAN HET EIND VAN DE BAND STOPPEN MET SPOELEN. De speler klemt de positie
-    // af, dus daar gebeurt niets meer -- maar de spoelen bleven wel doordraaien
-    // en de knop bleef oplichten, alsof er nog band was. Beweegt de teller twee
-    // rondes niet, dan zijn we bij de kop of de staart.
-    const nu = p.tapeTijden ? p.tapeTijden().cur : null;
-    if (nu !== null && vorigePositie !== null && Math.abs(nu - vorigePositie) < 0.05) {
-      if (++stil >= 2) { stopWinden(tape); return; }
-    } else stil = 0;
-    vorigePositie = nu;
-  }, STAP_MS);
-  winder = { timer, tape };
 }
 
Index: src/services/AudioEmbedService.js
===================================================================
--- src/services/AudioEmbedService.js	(revision 4fbe2c1f0fd6e892ba64f0b6b6b9d72393a93573)
+++ src/services/AudioEmbedService.js	(revision c0e463b1f870f3f742bb65ca7ee345c081ff782b)
@@ -562,4 +562,9 @@
         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)
Index: src/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision 4fbe2c1f0fd6e892ba64f0b6b6b9d72393a93573)
+++ src/views/shell.ejs	(revision c0e463b1f870f3f742bb65ca7ee345c081ff782b)
@@ -222,5 +222,5 @@
 
 <!-- v9 stylesheet (full palette system) -->
-<link rel="stylesheet" href="/assets/css/style.css?v=107">
+<link rel="stylesheet" href="/assets/css/style.css?v=108">
 <script>
 /* iOS safe-area, built by hand. env(safe-area-inset-top) resolves to 0 on this iOS in
@@ -406,5 +406,5 @@
      ?v=N — cache-buster: bump bij elke audio-player.js wijziging zodat
      Cloudflare (max-age=1y) niet de oude versie blijft serveren. -->
-<script src="/assets/js/audio-player.js?v=37"></script>
+<script src="/assets/js/audio-player.js?v=38"></script>
 <!-- Eigen custom media-embeds (YouTube/SoundCloud/Spotify) via de echte
      player-API's + gedeelde mutual-exclusion registry met de site-speler. -->
@@ -505,5 +505,5 @@
   // Eén nummer voor de hele map. Te vaak bumpen kost één download; te weinig
   // bumpen kost een bugfix die nooit aankomt.
-  var MOD_V = 54;
+  var MOD_V = 55;
 
   // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
Index: test/mixtape-ui.test.js
===================================================================
--- test/mixtape-ui.test.js	(revision 4fbe2c1f0fd6e892ba64f0b6b6b9d72393a93573)
+++ test/mixtape-ui.test.js	(revision c0e463b1f870f3f742bb65ca7ee345c081ff782b)
@@ -210,2 +210,37 @@
   assert.match(speler, /if \(\(queue\.length === 1 \|\| laatsteVanDeBand\) && ms && ms\.readyState === 'open'\)/);
 });
+
+test('de speler-UI schakelt mee in bandmodus (shaer-tmyn)', () => {
+  const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
+
+  // 1. De tracklijst blijft staan maar is geen keuzelijst meer.
+  assert.match(speler, /sheetQueueList\.classList\.toggle\('is-tape', tapeMode\);\s*\n\s*if \(tapeMode\) return;/,
+    'zonder deze return kun je op een cassette alsnog naar nummer zeven springen');
+
+  // 2. Vorige/volgende worden terugspoelen/vooruitspoelen, op EEN plek geregeld.
+  assert.match(speler, /function zetKnopStanden\(\)/);
+  assert.match(speler, /'Vooruitspoelen' : 'Terugspoelen'/);
+  // en vasthouden spoelt door
+  assert.match(speler, /addEventListener\('pointerdown', \(\) => \{ if \(tapeMode\) startWind/);
+
+  // 3. De spoellus staat in de speler, niet meer in de module -- anders lopen
+  //    twee kopieen van dezelfde versnelling uit elkaar.
+  assert.match(speler, /function startWind\(richting\)/);
+  const mod = fs.readFileSync('src/assets/js/mod/tape.js', 'utf8');
+  assert.doesNotMatch(mod, /VERSNELLING/, 'de versnelling hoort niet meer in de module te staan');
+  assert.match(mod, /p\.startWind\(richting\)/, 'de cassette bedient de gedeelde lus');
+});
+
+test('de bandteller komt uit de trackduren, niet uit de buffer', () => {
+  const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
+  assert.match(speler, /function bandDuur\(\)/);
+  assert.match(speler, /function bandOffset\(i\)/);
+  // Een som met gaten is een verzonnen getal: ontbreekt er een duur, dan valt
+  // hij terug in plaats van te gokken.
+  assert.match(speler, /if \(d <= 0\) return null;/);
+
+  // En de duur moet mee in de wachtrij, anders kan de speler het niet weten.
+  const embed = fs.readFileSync('src/services/AudioEmbedService.js', 'utf8');
+  assert.match(embed, /duration: Number\(t\.duration\) \|\| 0,/,
+    'zonder dit las de teller de lengte van wat toevallig gebufferd was');
+});
