Index: src/views/pages/home.ejs
===================================================================
--- src/views/pages/home.ejs	(revision 2d2fd8c352950d0d5ff366c901fb7e2e0cf6d614)
+++ src/views/pages/home.ejs	(revision 7cf8144b1073d0f9d019acfcd4ee74b4e6a4dccd)
@@ -59,4 +59,20 @@
 </div>
 
+<!-- ========== LEZEN (alleen zichtbaar bij body[data-feed-view="reader"]) ==========
+     Hetzelfde stel berichten, maar heel: één per scherm, met de grenzen door
+     CSS-snapping. Geen eigen route en geen eigen shell -- de balken blijven
+     staan, want je bent nog gewoon op de feed en moet terug kunnen schakelen.
+     Leeg? Dan valt de lege staat van de tijdlijn hierboven in beeld; deze
+     sectie rendert dan niet. -->
+<% if (typeof readerItems !== 'undefined' && readerItems && readerItems.length) { %>
+<section class="feed-reader" aria-label="<%= t('switch.reader') %>">
+  <div class="read-stream" id="read-stream" data-base="<%= _base %>">
+    <% readerItems.forEach(function(it) { %>
+      <%- include('../partials/read-article', { post: it.post, entry: it.entry }) %>
+    <% }); %>
+  </div>
+</section>
+<% } %>
+
 <!-- ========== GRID (only visible when body[data-feed-view="grid"]) ========== -->
 <% if (allPosts.length > 0) { %>
Index: src/views/pages/read.ejs
===================================================================
--- src/views/pages/read.ejs	(revision 2d2fd8c352950d0d5ff366c901fb7e2e0cf6d614)
+++ 	(revision )
@@ -1,111 +1,0 @@
-<%
-// De leesweergave: berichten achter elkaar, één per beeld, en de balken gaan weg.
-//
-// Barts idee, naar bartoverkamp.nl -- maar NIET met de truc die daar staat.
-// Daar wordt bij de rand naar de andere kant gesprongen en het artikel
-// "vloeiend" in beeld gezet; dat is een naad die je verbergt. Hier groeit de
-// stroom gewoon: bij de onderrand hangt de client de oudere buur eronder, bij de
-// bovenrand de nieuwere erboven, en de browser snapt naar de berichtgrenzen.
-// Dan is er geen naad om te verbergen.
-//
-// Zonder JavaScript blijft dit één leesbaar bericht met twee gewone links. Een
-// leesweergave die alleen met script bestaat, bestaat niet voor wie het uitzet.
-var _b = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
-%>
-
-<%# De teksten die het script zelf plaatst, komen van hier -- zoals de
-    zoekteksten van de topnav (zie mod/chrome.js). Een zin die in het
-    script staat, staat in één taal, en dat is nooit de taal van de lezer. %>
-<div class="read-stream" id="read-stream" data-base="<%= _b %>"
-     data-i18n="<%= JSON.stringify({ end: t('read.end'), load_error: t('read.load_error') }) %>">
-  <%- include('../partials/read-article', {
-        post: post, entry: entry, newerPost: newerPost, olderPost: olderPost,
-      }) %>
-</div>
-
-<%# Geen eigen uitgangsknop. De balken schuiven terug zodra je omhoog scrolt
-    (en staan er sowieso bovenaan), dus de weg terug is altijd één veeg weg. Een
-    zwevende knop erbij zou een tweede uitgang zijn die hetzelfde doet. %>
-
-<noscript>
-  <nav class="read-fallback">
-    <% if (newerPost) { %><a href="<%= _b %>/read/<%= newerPost.slug %>">← <%= newerPost.title %></a><% } %>
-    <% if (olderPost) { %><a href="<%= _b %>/read/<%= olderPost.slug %>"><%= olderPost.title %> →</a><% } %>
-  </nav>
-</noscript>
-
-<style>
-/* Alles onder .on-read: dit is een nieuw scherm op een LIVE site, dus niets
-   hiervan mag daarbuiten iets aanraken. */
-
-/* De balken schuiven weg zodra je leest. Transform en niet display: een
-   schuivende balk laat zien dat hij terugkomt, een verdwijnende niet. */
-.on-read #pcms-chrome,
-.on-read .bottom-tab {
-  transition: transform .28s ease, opacity .28s ease;
-  will-change: transform;
-}
-.on-read.read-chrome-hidden #pcms-chrome { transform: translateY(-100%); opacity: 0; }
-.on-read.read-chrome-hidden .bottom-tab  { transform: translateY(100%);  opacity: 0; }
-
-/* De vrije ruimte onder de laatste post moet weg: anders eindigt het document
-   ver onder het laatste bericht en klopt "bij de rand" niet meer. */
-.on-read #pcms-main { padding-bottom: 0; }
-
-/* PROXIMITY, geen mandatory. Een bericht dat langer is dan het scherm moet je
-   rustig kunnen doorlezen; mandatory zou je bij elke pauze terugtrekken naar
-   een grens. Zo snapt hij alleen als je in de buurt van een grens bent.
-
-   OP <html>, en dat is geen smaakkwestie: snappen doet de SCROLLCONTAINER, en
-   dat is hier het document -- .read-stream heeft overflow: visible en scrollt
-   zelf niet. Op .read-stream stond de regel er wel, maar deed hij niets. */
-html:has(body.on-read) { scroll-snap-type: y proximity; }
-
-/* De browser NIET ook laten compenseren. Chrome en Firefox schuiven bij
-   invoegen bóven het beeld de scrollpositie zelf mee (scroll anchoring); samen
-   met onze eigen correctie in mod/read.js is dat twee keer, en dan springt de
-   tekst precies één bericht weg. Onze eigen correctie moet blijven, want Safari
-   (en dus iOS) kent scroll anchoring helemaal niet. */
-html:has(body.on-read), .on-read .read-stream, .on-read .read-post { overflow-anchor: none; }
-.on-read .read-post {
-  scroll-snap-align: start;
-  scroll-snap-stop: normal;
-  min-height: 100svh;
-  max-width: 46rem;
-  margin: 0 auto;
-  padding: 2rem 1.2rem 4rem;
-  box-sizing: border-box;
-}
-/* Een lijn tussen twee berichten: zonder grens is een stroom een lange brij. */
-.on-read .read-post + .read-post { border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent); }
-
-/* De titel is een link, maar mag er niet als een link uitzien: dit is een
-   leesscherm, geen index. Onderstrepen pas als je hem aanwijst. */
-.on-read .read-title a { color: inherit; text-decoration: none; }
-.on-read .read-title a:hover,
-.on-read .read-title a:focus-visible { text-decoration: underline; }
-
-/* De aankondiging onderaan: waar de tik je heen brengt. */
-.on-read .read-foot { margin-top: 2rem; }
-.on-read .read-open {
-  display: inline-block; font-size: .9rem; opacity: .75;
-  text-decoration: none; border-bottom: 1px solid currentColor; padding-bottom: 1px;
-}
-.on-read .read-open:hover, .on-read .read-open:focus-visible { opacity: 1; }
-
-.on-read .read-head { padding-bottom: .5rem; }
-.on-read .read-title { font-size: clamp(1.6rem, 4vw, 2.4rem); line-height: 1.15; margin: 0 0 .3rem; }
-.on-read .read-when { font-size: .85rem; opacity: .6; margin: 0; }
-
-/* Bezig / mislukt: de lezer hoort te merken dat er iets gebeurt, en vooral dat
-   er NIETS gebeurde als het misging. */
-.on-read .read-loading { text-align: center; padding: 1.2rem; opacity: .6; font-size: .9rem; }
-.on-read .read-end  { text-align: center; padding: 2rem 1rem 3rem; opacity: .5; font-size: .9rem; }
-
-.on-read .read-fallback { display: flex; gap: 1rem; padding: 1rem 1.2rem; }
-
-@media (prefers-reduced-motion: reduce) {
-  .on-read #pcms-chrome, .on-read .bottom-tab { transition: none; }
-  html:has(body.on-read) { scroll-snap-type: none; }
-}
-</style>
Index: src/views/partials/home-append.ejs
===================================================================
--- src/views/partials/home-append.ejs	(revision 2d2fd8c352950d0d5ff366c901fb7e2e0cf6d614)
+++ src/views/partials/home-append.ejs	(revision 7cf8144b1073d0f9d019acfcd4ee74b4e6a4dccd)
@@ -4,3 +4,6 @@
     items instead of nesting under an extra box. %>
 <div hx-swap-oob="beforeend:#grid-tiles" style="display:contents"><% posts.forEach(function(post){ %><%- include('post-tile', { post: post }) %><% }); %></div>
+<%# Lezen: dezelfde berichten OOB achteraan de leesstroom, net als de tegels
+    hierboven. Zonder dit zou "meer laden" in de leesweergave niets doen. %>
+<% if (typeof readerItems !== 'undefined' && readerItems && readerItems.length) { %><div hx-swap-oob="beforeend:#read-stream" style="display:contents"><% readerItems.forEach(function(it){ %><%- include('read-article', { post: it.post, entry: it.entry }) %><% }); %></div><% } %>
 <%- include('load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#post-list', morePath: (typeof morePath !== 'undefined' ? morePath : '/'), oob: true }) %>
Index: src/views/partials/shared-styles.ejs
===================================================================
--- src/views/partials/shared-styles.ejs	(revision 2d2fd8c352950d0d5ff366c901fb7e2e0cf6d614)
+++ src/views/partials/shared-styles.ejs	(revision 7cf8144b1073d0f9d019acfcd4ee74b4e6a4dccd)
@@ -27,6 +27,5 @@
   padding: 0.15rem;
 }
-.view-switch-btn,
-.view-switch-link {
+.view-switch-btn {
   display: inline-flex; align-items: center; gap: 0.35rem;
   padding: 0.35rem 0.85rem;
@@ -36,12 +35,9 @@
   transition: background 120ms, color 120ms;
 }
-.view-switch-btn:hover,
-.view-switch-link:hover { color: var(--ink); }
-.view-switch-link { text-decoration: none; }
+.view-switch-btn:hover { color: var(--ink); }
 /* Active = white (light background, dark text). Via aria-selected (element
    attribute, set by syncAria + on htmx:afterSettle) → htmx-robust. On non-
    feed pages syncAria sets aria-selected=false → both grey. */
-.view-switch-btn[aria-selected="true"],
-.view-switch-link[aria-selected="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); }
+.view-switch-btn[aria-selected="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); }
 
 /* Agenda = its OWN pill (separate from Timeline/Grid, so the feed-toggle is never
Index: src/views/partials/view-switcher.ejs
===================================================================
--- src/views/partials/view-switcher.ejs	(revision 2d2fd8c352950d0d5ff366c901fb7e2e0cf6d614)
+++ src/views/partials/view-switcher.ejs	(revision 7cf8144b1073d0f9d019acfcd4ee74b4e6a4dccd)
@@ -8,8 +8,6 @@
   // Pagina's zonder actieve feed-weergave → niets in de pill is 'actief' (Solo/Cirkel
   // + Tijdlijn/Grid grijs). Alleen Agenda krijgt z'n eigen is-active op de agenda.
-  var _neutral = ['on-shows','on-downloads','on-post','on-epk','on-linkbio','on-newsletter','on-archive','on-read']
+  var _neutral = ['on-shows','on-downloads','on-post','on-epk','on-linkbio','on-newsletter','on-archive']
                    .some(function(c){ return _bc.indexOf(c) >= 0; });
-  var _onRead = _bc.indexOf('on-read') >= 0;
-  var _sbR = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
 %>
 <div class="view-switcher-wrap">
@@ -47,16 +45,12 @@
         <span><%= t('switch.grid') %></span>
       </button>
-      <%# Lezen is GEEN in-place omschakeling maar een andere route (/read), dus
-          een <a> en niet een <button>. En bewust NIET de klasse view-switch-btn:
-          de gedelegeerde klik in shell.ejs pakt alles met die klasse op, zet
-          body[data-feed-view] erop en springt op niet-feedpagina's terug naar
-          home -- dat zou deze link opeten. Andere klasse, zelfde uiterlijk.
-          aria-selected staat hier vast: syncAria() raakt alleen .view-switch-btn
-          aan, dus de actieve staat op /read blijft staan na een htmx-navigatie. %>
-      <a class="view-switch-link" href="<%= _sbR %>/read" role="tab"
-         aria-selected="<%= _onRead ? 'true' : 'false' %>">
+      <%# Lezen is een gewone derde weergave: dezelfde berichten, andere vorm.
+          Dus een <button> met data-view, net als de andere twee -- dan doen de
+          gedelegeerde klik, localStorage en syncAria in shell.ejs hun werk al. %>
+      <button type="button" class="view-switch-btn" data-view="reader" role="tab"
+              aria-selected="false">
         <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
         <span><%= t('switch.reader') %></span>
-      </a>
+      </button>
     </div>
     <% if (!_neutral) { %>
Index: src/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision 2d2fd8c352950d0d5ff366c901fb7e2e0cf6d614)
+++ src/views/shell.ejs	(revision 7cf8144b1073d0f9d019acfcd4ee74b4e6a4dccd)
@@ -222,5 +222,5 @@
 
 <!-- v9 stylesheet (full palette system) -->
-<link rel="stylesheet" href="/assets/css/style.css?v=68">
+<link rel="stylesheet" href="/assets/css/style.css?v=70">
 <script>
 /* iOS safe-area, built by hand. env(safe-area-inset-top) resolves to 0 on this iOS in
@@ -435,12 +435,8 @@
   // focusscherm óók via htmx z'n styling krijgt (geen chrome, geen tab, audio op 0).
   // ALLES wat de server als page-class kan sturen moet hier staan: wat er niet in
-  // staat wordt bij een htmx-navigatie stilletjes weggefilterd. 'on-read' ontbrak,
-  // en dan kwam je via de Lezen-knop op een leesweergave waarvan ELKE regel
-  // (.on-read ...) niet aansloeg: geen schermhoge berichten, geen snappen, geen
-  // wegschuivende balken. Bij intikken van /read ging het wel goed, want dan
-  // rendert de shell de class zelf -- vandaar dat het lang onzichtbaar bleef.
+  // staat wordt bij een htmx-navigatie stilletjes weggefilterd.
   var PAGE_CLASSES = ['on-home','on-post','on-special','on-archive','on-search','on-admin','on-auth',
     'on-shows','on-downloads','on-download','on-epk','on-linkbio','on-newsletter',
-    'on-tag','on-type','on-user','on-cirkel','on-hub','on-chat','on-read'];
+    'on-tag','on-type','on-user','on-cirkel','on-hub','on-chat'];
 
   document.body.addEventListener('pcmsNav', function(ev) {
@@ -509,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 = 3;
+  var MOD_V = 4;
 
   // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
@@ -554,5 +550,5 @@
   try {
     var v = localStorage.getItem('pcms-feed-view');
-    if (v === 'timeline' || v === 'grid') body.dataset.feedView = v;
+    if (v === 'timeline' || v === 'grid' || v === 'reader') body.dataset.feedView = v;
     var c = parseInt(localStorage.getItem('pcms-grid-cols'), 10);
     if (c === 2 || c === 3 || c === 4) body.dataset.gridCols = String(c);
