Index: src/assets/js/guardian.js
===================================================================
--- src/assets/js/guardian.js	(revision 82c3356e2cc0cc877397783c3e1d2c722cbdb3a6)
+++ src/assets/js/guardian.js	(revision e667d26aec08a6380a8c2e1687a3f374682f383d)
@@ -108,5 +108,5 @@
    */
   function helpState(h) {
-    var st = h.state || { open: true, pickedUpBy: [], handled: null, ageMs: null };
+    var st = h.state || { open: true, pickedUpBy: [], handled: null, oldestPickupAt: null };
     var box = el('div', 'g-help-state');
 
@@ -133,6 +133,10 @@
     // Oud, maar niet weg. Dit is het verschil tussen 'er is iemand mee bezig' en
     // 'er was ooit iemand mee bezig'.
-    if (st.ageMs != null && st.ageMs > 3600000) {
-      box.appendChild(el('div', 'g-help-age small', ago(st.ageMs)));
+    // De leeftijd wordt HIER uitgerekend, niet op de server: een verschil met
+    // `now` in het antwoord maakt dat antwoord elke milliseconde anders, en dan
+    // kan de ETag nooit gelijk zijn.
+    var oud = st.oldestPickupAt ? (Date.now() - Date.parse(st.oldestPickupAt)) : null;
+    if (oud != null && !isNaN(oud) && oud > 3600000) {
+      box.appendChild(el('div', 'g-help-age small', ago(oud)));
     }
 
@@ -894,4 +898,16 @@
   }
 
+
+  // De scrollpositie overleeft een verversing (Barts melding, 9-8). renderAll
+  // bouwt lijsten opnieuw op; staat de pagina daarbij even korter, dan springt
+  // de browser omhoog en ben je kwijt waar je was. Meten en terugzetten is
+  // genoeg -- en het is het verschil tussen een scherm dat bijwerkt en een
+  // scherm dat onder je handen wegschuift.
+  function metBehoudVanScroll(fn) {
+    var y = window.scrollY || document.documentElement.scrollTop || 0;
+    fn();
+    if (y) window.scrollTo(0, y);
+  }
+
   function renderAll() { renderHelp(); renderPending(); renderWards(); }
 
@@ -963,5 +979,5 @@
     return fetch('/guardian/api/state?site=' + encodeURIComponent(S.site))
       .then(function (r) { return r.json(); })
-      .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; renderAll(); } })
+      .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; metBehoudVanScroll(renderAll); } })
       .then(loadFeed).then(loadFollowReqs);
   }
@@ -978,17 +994,35 @@
   // opnieuw getekend.
   var pollBezig = false;
+  var laatsteState = null;   // de vorige body, om onnodig hertekenen te herkennen
+
   function longPoll() {
     if (pollBezig || document.hidden) return;
     pollBezig = true;
+    var t0 = Date.now();
     fetch('/guardian/api/state?wait=25&site=' + encodeURIComponent(S.site))
       .then(function (r) {
         pollBezig = false;
         if (r.status === 304) return null;          // stilte
-        return r.json().then(function (s) {
-          if (s && !s.error) { S = s; T = s.strings || T; renderAll(); }
+        return r.text().then(function (tekst) {
+          // ZELFDE INHOUD, NIET HERTEKENEN. De 304 zou dit al moeten afvangen,
+          // maar een enkel veld dat per verzoek verandert (er stond een levende
+          // klok in) maakt elke ETag anders, en dan tekent het paneel zichzelf
+          // stuk terwijl je scrollt. Dit is de tweede grendel, aan de kant die
+          // de gebruiker merkt.
+          if (tekst === laatsteState) return null;
+          laatsteState = tekst;
+          var s; try { s = JSON.parse(tekst); } catch (e) { return null; }
+          if (s && !s.error) { S = s; T = s.strings || T; metBehoudVanScroll(renderAll); }
           return loadFeed().then(loadFollowReqs);
         });
       })
-      .then(function () { setTimeout(longPoll, 200); })
+      .then(function () {
+        // EEN LANGE POLL DIE METEEN TERUGKEERT MAG GEEN LUS WORDEN. Wachtte de
+        // server niet (een fout, een tussenliggende proxy, iets wat we nog niet
+        // weten), dan houden we zelf de rem erop. Anders vervangt een defect de
+        // ene tik van 45 seconden door duizend per minuut.
+        var snel = Date.now() - t0 < 2000;
+        setTimeout(longPoll, snel ? 3000 : 200);
+      })
       .catch(function () {
         // Netwerk weg of server herstart: niet meteen opnieuw beuken.
Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision 82c3356e2cc0cc877397783c3e1d2c722cbdb3a6)
+++ src/routes/guardian.js	(revision e667d26aec08a6380a8c2e1687a3f374682f383d)
@@ -99,5 +99,5 @@
     // niet is, is de gevaarlijke fout -- niet andersom.
     state: Guardianship.help.withWardship(
-      helpStaat.get(h.object_uri) || { open: true, pickedUpBy: [], handled: null, ageMs: null },
+      helpStaat.get(h.object_uri) || { open: true, pickedUpBy: [], handled: null, oldestPickupAt: null },
       mijnWards.has(h.actor_uri),
     ),
Index: src/services/guardianship/help.js
===================================================================
--- src/services/guardianship/help.js	(revision 82c3356e2cc0cc877397783c3e1d2c722cbdb3a6)
+++ src/services/guardianship/help.js	(revision e667d26aec08a6380a8c2e1687a3f374682f383d)
@@ -52,7 +52,16 @@
  * zijn zonder database of scherm.
  *
- * `ageMs` is de leeftijd van de OUDSTE oppik. Daar tekent het scherm mee dat een
- * signaal oud wordt -- niets verdwijnt, maar je ziet wel dat er misschien niets
- * meer gebeurt.
+ * `oldestPickupAt` is het TIJDSTIP van de oudste oppik, niet de leeftijd. Daar
+ * tekent het scherm mee dat een signaal oud wordt -- niets verdwijnt, maar je
+ * ziet wel dat er misschien niets meer gebeurt.
+ *
+ * EEN TIJDSTIP, GEEN LEEFTIJD, en dat is geen smaak. Hier stond `ageMs`, een
+ * verschil met `now`, en dus veranderde dit antwoord elke milliseconde. Zodra
+ * het paneel een ETag kreeg (9-8) kon die daardoor nooit meer gelijk zijn: de
+ * 304 kwam nooit, de lange poll keerde meteen terug, en de browser kwam in een
+ * lus van ongeveer een seconde waarin de scrollpositie werd vermalen. Een
+ * levende klok in een antwoord maakt dat antwoord onvergelijkbaar met zichzelf.
+ *
+ * De leeftijd is een weergavedetail en wordt in de client uitgerekend.
  */
 export function helpStatus(rows, now = Date.now()) {
@@ -67,5 +76,5 @@
     handled: done ? { uri: done.guardian_uri, handle: done.guardian_handle || null, at: done.created_at } : null,
     // Alleen betekenisvol zolang er niets is afgesloten.
-    ageMs: (!done && oudste) ? Math.max(0, now - oudste) : null,
+    oldestPickupAt: (!done && oudste) ? new Date(oudste).toISOString() : null,
     // Waar het scherm op afgaat. Bij twijfel OPEN: een lege lijst, een rij die we
     // niet kunnen lezen, wat dan ook -- alles wat geen expliciete afsluiting is,
Index: src/services/guardianship/queues.js
===================================================================
--- src/services/guardianship/queues.js	(revision 82c3356e2cc0cc877397783c3e1d2c722cbdb3a6)
+++ src/services/guardianship/queues.js	(revision e667d26aec08a6380a8c2e1687a3f374682f383d)
@@ -226,5 +226,5 @@
     // niet is, is de gevaarlijke fout -- niet andersom.
     state: help.withWardship(
-      staat.get(r.object_uri) || { open: true, pickedUpBy: [], handled: null, ageMs: null },
+      staat.get(r.object_uri) || { open: true, pickedUpBy: [], handled: null, oldestPickupAt: null },
       mijn.has(r.actor_uri),
     ),
Index: test/guardian-help-state.test.js
===================================================================
--- test/guardian-help-state.test.js	(revision 82c3356e2cc0cc877397783c3e1d2c722cbdb3a6)
+++ test/guardian-help-state.test.js	(revision e667d26aec08a6380a8c2e1687a3f374682f383d)
@@ -63,5 +63,8 @@
   const s = help.helpStatus(rijen, Date.parse('2026-08-07T10:00:00.000Z'));
   assert.equal(s.open, true);
-  assert.equal(s.ageMs, 2 * 24 * 3600 * 1000, 'twee dagen oud, en dat hoort te zien te zijn');
+  // Een TIJDSTIP, geen leeftijd: een verschil met `now` maakt het antwoord elke
+  // milliseconde anders, en dan kan de ETag van het paneel nooit gelijk zijn --
+  // dat leverde een lus op waarin de browser zichzelf bleef verversen.
+  assert.equal(s.oldestPickupAt, '2026-08-05T10:00:00.000Z');
 });
 
@@ -71,5 +74,5 @@
   assert.equal(s.open, false);
   assert.equal(s.handled.handle, '@opa@opa.test');
-  assert.equal(s.ageMs, null, 'de leeftijd van een oppik zegt niets meer als het klaar is');
+  assert.equal(s.oldestPickupAt, null, 'het tijdstip van een oppik zegt niets meer als het klaar is');
 });
 
@@ -146,2 +149,16 @@
   assert.deepEqual(help.withWardship(basis, true), basis);
 });
+
+test('het antwoord verandert NIET als je het twee keer opvraagt', () => {
+  // De fout die Barts browser in een lus bracht (9-8): hier stond ageMs, een
+  // verschil met `now`. Daardoor was elk antwoord anders, kon de ETag van het
+  // paneel nooit gelijk zijn, kwam de 304 nooit, en keerde de lange poll meteen
+  // terug -- inclusief een hertekening die de scrollpositie vermaalde.
+  //
+  // Een levende klok in een antwoord maakt dat antwoord onvergelijkbaar met
+  // zichzelf. Deze toets bewaakt precies dat.
+  const rijen = [{ kind: 'pickup', guardian_uri: OMA, created_at: '2026-08-05T10:00:00.000Z' }];
+  const a = JSON.stringify(help.helpStatus(rijen, Date.parse('2026-08-05T12:00:00.000Z')));
+  const b = JSON.stringify(help.helpStatus(rijen, Date.parse('2026-08-05T12:00:00.500Z')));
+  assert.equal(a, b, 'een halve seconde later is hetzelfde antwoord');
+});
