Index: src/assets/js/audio-player.js
===================================================================
--- src/assets/js/audio-player.js	(revision 8bb3b098556cf72e282aaaa66c62a2122d992f8b)
+++ src/assets/js/audio-player.js	(revision 4fbe2c1f0fd6e892ba64f0b6b6b9d72393a93573)
@@ -279,4 +279,5 @@
   function preloadNext() {
     if (queue.length < 2) return;
+    if (tapeMode && currentIndex >= queue.length - 1) return;   // einde band
     const ni = (currentIndex + 1) % queue.length;
     const t = queue[ni];
@@ -361,7 +362,15 @@
     const start = chain.length ? chain[chain.length - 1].end : (buffered.length ? buffered.start(0) : 0);
     chain.push({ qIndex, start, end: chainEnd });
-    if (queue.length === 1 && ms && ms.readyState === 'open') {
+    // DE STREAM MOET DICHT ALS ER NIETS MEER KOMT, anders vuurt `ended` nooit.
+    // Bij een wachtrij van een was dat al zo. Een BANDJE heeft nu hetzelfde
+    // nodig: sinds hij niet meer rondloopt haakt de keten na het laatste nummer
+    // niets meer aan, en dan bleef de band aan het eind hangen -- de teller
+    // stilstaand op de laatste seconde, isPlaying() waar, en de spoelen
+    // draaiend. Gemeten op dev (22-8): 125,6 van 125,7 en daar bleef hij.
+    const laatsteVanDeBand = tapeMode && qIndex >= queue.length - 1;
+    if ((queue.length === 1 || laatsteVanDeBand) && ms && ms.readyState === 'open') {
       // Single-track queue: close the stream so `ended` fires (which replays
-      // it, matching the old engine's behaviour).
+      // it, matching the old engine's behaviour). Bij een bandje stopt `ended`
+      // hem juist, want next() pauzeert daar aan het eind.
       try { ms.endOfStream(); } catch (e) {}
     }
@@ -374,5 +383,11 @@
     const seg = currentSegment();
     if (!seg || chain.length - 1 - chain.indexOf(seg) >= 1) return;  // already one ahead
-    const nextIdx = (chain[chain.length - 1].qIndex + 1) % queue.length;
+    // EEN BANDJE LOOPT NIET ROND (Robins eis, 22-8). Deze modulo is precies wat
+    // een cassette eindeloos maakte: na het laatste nummer hing hij nummer een
+    // er weer achter, en omdat het een doorlopende keten is merk je dat niet
+    // eens als een trackwissel -- de band gaat gewoon door.
+    const volgendeInRij = chain[chain.length - 1].qIndex + 1;
+    if (tapeMode && volgendeInRij > queue.length - 1) return;   // einde band
+    const nextIdx = volgendeInRij % queue.length;
     const t = queue[nextIdx];
     if (!t || !t.url) return;
@@ -642,8 +657,17 @@
   function next() {
     if (!queue.length) return;
+    // Aan het eind van een bandje: stoppen. `ended` roept deze functie aan, dus
+    // zonder deze tak begint de band na het laatste nummer weer vooraan.
+    if (tapeMode && currentIndex >= queue.length - 1) { pause(); return; }
     loadTrack((currentIndex + 1) % queue.length, true);
   }
   function prev() {
     if (!queue.length) return;
+    // En aan het begin ook niet omlopen. Terugspoelen voorbij het begin levert
+    // de kop van de band op, niet het laatste nummer.
+    if (tapeMode && currentIndex === 0) {
+      try { audio.currentTime = 0; } catch (e) { /* nog niets geladen */ }
+      return;
+    }
     loadTrack(currentIndex === 0 ? queue.length - 1 : currentIndex - 1, true);
   }
@@ -1157,6 +1181,19 @@
     const duur = audio.duration || 0;
     const nu = audio.currentTime || 0;
-    if (duur && nu + d >= duur) { next(); return; }
-    if (nu + d < 0) { prev(); return; }
+    if (duur && nu + d >= duur) {
+      // Vooruit voorbij het eind. In bandmodus stopt next() aan het eind van de
+      // band; daarbuiten loopt hij door naar het volgende nummer.
+      next();
+      return;
+    }
+    if (nu + d < 0) {
+      // TERUGSPOELEN VOORBIJ HET BEGIN moet in het vorige nummer landen aan het
+      // EIND, niet aan het begin -- dat is wat terugspoelen doet. Zonder dit
+      // sprong je bij elke druk naar de kop van het vorige nummer en kwam je
+      // nooit ergens in het midden uit.
+      if (!(tapeMode && currentIndex === 0)) pendingSeek = Number.MAX_SAFE_INTEGER;
+      prev();
+      return;
+    }
     try { audio.currentTime = Math.max(0, nu + d); } catch (e) {}
   }
Index: src/assets/js/mod/tape.js
===================================================================
--- src/assets/js/mod/tape.js	(revision 8bb3b098556cf72e282aaaa66c62a2122d992f8b)
+++ src/assets/js/mod/tape.js	(revision 4fbe2c1f0fd6e892ba64f0b6b6b9d72393a93573)
@@ -109,4 +109,6 @@
   stopWinden(tape);
   let snelheid = START_SNELHEID;
+  let vorigePositie = null;
+  let stil = 0;
   tape.classList.add('is-winding');
   if (richting < 0) tape.classList.add('is-winding-back');
@@ -115,4 +117,13 @@
     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/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision 8bb3b098556cf72e282aaaa66c62a2122d992f8b)
+++ src/views/shell.ejs	(revision 4fbe2c1f0fd6e892ba64f0b6b6b9d72393a93573)
@@ -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=35"></script>
+<script src="/assets/js/audio-player.js?v=37"></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 = 53;
+  var MOD_V = 54;
 
   // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
Index: test/mixtape-ui.test.js
===================================================================
--- test/mixtape-ui.test.js	(revision 8bb3b098556cf72e282aaaa66c62a2122d992f8b)
+++ test/mixtape-ui.test.js	(revision 4fbe2c1f0fd6e892ba64f0b6b6b9d72393a93573)
@@ -166,2 +166,46 @@
   assert.doesNotMatch(mod, /p\.next\(\); else p\.prev\(\)/, 'de oude sprong per nummer');
 });
+
+test('een bandje loopt niet rond, aan geen van beide kanten', () => {
+  const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
+
+  // De keten mag na het laatste nummer niet nummer een er weer achter hangen.
+  // Dat was de stilste van de drie: omdat het EEN doorlopende tijdlijn is merk
+  // je die omloop niet eens als een trackwissel, de band gaat gewoon door.
+  assert.match(speler, /if \(tapeMode && volgendeInRij > queue\.length - 1\) return;/,
+    'de keten hoort te stoppen aan het eind van de band');
+
+  // ended roept next() aan, dus zonder deze tak begint de band opnieuw.
+  assert.match(speler, /if \(tapeMode && currentIndex >= queue\.length - 1\) \{ pause\(\); return; \}/,
+    'aan het eind stoppen in plaats van omlopen');
+
+  // En terugspoelen voorbij het begin levert de kop van de band op, niet het
+  // laatste nummer.
+  assert.match(speler, /if \(tapeMode && currentIndex === 0\) \{/,
+    'aan het begin niet naar achteren omlopen');
+
+  // Alle drie de modulo's zijn nog aanwezig voor de NIET-bandmodus: een gewone
+  // playlist hoort wel te blijven rondlopen.
+  assert.equal((speler.match(/% queue\.length/g) || []).length, 3,
+    'de omloop van een gewone wachtrij mag niet gesneuveld zijn');
+});
+
+test('terugspoelen komt in het vorige nummer uit aan het EIND', () => {
+  const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
+  // Zonder dit sprong terugspoelen naar de kop van het vorige nummer, en kwam
+  // je nooit ergens in het midden uit -- dat is geen terugspoelen maar
+  // terugspringen. Geldt voor de blob-motor; op MSE is de band een tijdlijn en
+  // gaat het vanzelf goed.
+  assert.match(speler, /pendingSeek = Number\.MAX_SAFE_INTEGER;/);
+});
+
+test('aan het eind van de band gaat de stream dicht', () => {
+  const speler = fs.readFileSync('src/assets/js/audio-player.js', 'utf8');
+  // ZONDER DIT HANGT DE BAND. Bij een MediaSource vuurt `ended` pas als de
+  // stream gesloten is. Sinds een bandje niet meer rondloopt haakt de keten na
+  // het laatste nummer niets meer aan, en dan bleef hij op de laatste seconde
+  // staan: teller stil, isPlaying() waar, spoelen draaiend. Gemeten op dev
+  // (22-8): 125,6 van 125,7 en daar bleef hij staan.
+  assert.match(speler, /const laatsteVanDeBand = tapeMode && qIndex >= queue\.length - 1;/);
+  assert.match(speler, /if \(\(queue\.length === 1 \|\| laatsteVanDeBand\) && ms && ms\.readyState === 'open'\)/);
+});
