Index: src/assets/css/style.css
===================================================================
--- src/assets/css/style.css	(revision 598f09038f721164997d5a888801e65d93c1acb2)
+++ src/assets/css/style.css	(revision e46ebaa7d0ba257f3e7916d2a4ecfa52e68617da)
@@ -2430,8 +2430,43 @@
 }
 
+/* -------- AUTO (data-feed-view="auto") --------
+   Lezen op mobiel, Tijdlijn op desktop. GEEN JavaScript: de CSS kiest, dus geen
+   resize-afhandeling en geen flikkering waarin je eerst de verkeerde weergave
+   ziet. De tijdlijn is de terugval, dus boven de grens hoeft hier niets te
+   staan -- die is vanzelf al zichtbaar.
+   De :has(.feed-reader)-voorwaarde staat er om dezelfde reden als hierboven:
+   zonder berichten is er geen leessectie, en dan moet de tijdlijn blijven staan
+   met "nog niets geschreven" erin. */
+@media (max-width: 767px) {
+    body.on-home[data-feed-view="auto"] .feed-reader { display: block; }
+    body.on-home[data-feed-view="auto"]:has(.feed-reader) .feed-timeline,
+    body.on-home[data-feed-view="auto"]:has(.feed-reader) .feed-grid,
+    body.on-home[data-feed-view="auto"]:has(.feed-reader) .pagination,
+    body.on-home[data-feed-view="auto"]:has(.feed-reader) .related-posts,
+    body.on-home[data-feed-view="auto"]:has(.feed-reader) .article-footer {
+        display: none !important;
+    }
+}
+/* In de auto-stand staan BEIDE knoppen in de HTML en laat de mediaquery er een
+   zien. Zo verspringt de naam van de knop niet terwijl je ernaar kijkt, en is er
+   geen JavaScript nodig dat op resize moet luisteren. */
+.view-switch-btn.vs-alleen-mobiel { display: none; }
+@media (max-width: 767px) {
+    .view-switch-btn.vs-alleen-mobiel { display: inline-flex; }
+    .view-switch-btn.vs-alleen-desktop { display: none; }
+}
+
 /* Snappen doet de SCROLLCONTAINER, en dat is het document -- niet .read-stream,
    die heeft overflow: visible en scrollt zelf niet. Proximity en geen mandatory:
    in een bericht dat langer is dan het scherm moet je rustig door kunnen lezen. */
-html:has(body.on-home[data-feed-view="reader"]) { scroll-snap-type: y proximity; }
+html:has(body.on-home[data-feed-view="reader"]) {
+    scroll-snap-type: y proximity;
+    /* Vloeiend, niet in een klap. Zonder dit springt een snap er in EEN frame
+       naartoe en voelt het als een hik in plaats van als meegeven -- Barts
+       melding (20-8). Geldt ook voor de terug-naar-boven-knop en voor een
+       ankersprong, en dat is precies wat je wilt.
+       Uitgezet bij prefers-reduced-motion, onderaan dit blok. */
+    scroll-behavior: smooth;
+}
 /* En de browser niet óók laten compenseren als Load more er berichten bij zet:
    Chrome en Firefox schuiven bij invoegen zelf mee (scroll anchoring), en dat
@@ -2441,7 +2476,32 @@
 
 .feed-reader .read-post {
+    /* Snap naar de BOVENkant van een bericht, zacht (proximity op html) en nooit
+       dwingend (stop: normal). Een korte post vult het scherm, zodat hij niet als
+       snipper tussen twee lijnen hangt.
+       De uitzondering staat NIET hier maar in mod/read.js: een bericht dat langer
+       is dan het scherm krijgt scroll-snap-align:none, anders trekt de browser je
+       tijdens het lezen terug naar zijn bovenrand en lijkt het of het scrollen
+       stopt. "Past het op het scherm?" is een vraag over gemeten hoogte, en die
+       kan CSS niet stellen. */
+    /* Je LANDT op de bovenkant van een bericht -- dat is wat "snap to post"
+       hoort te betekenen. (align:end probeerde ik ook; dan land je op de
+       ONDERKANT van het vorige bericht, en bij een lang bericht zie je dus zijn
+       laatste schermvol in plaats van de kop van het volgende.)
+       Wanneer die bovenkant een snappunt IS, staat in mod/read.js: alleen zolang
+       je er nog niet voorbij bent. Eenmaal in een bericht snapt er niets meer,
+       ook niet naar zijn eigen bovenrand -- anders trekt proximity je terug
+       zodra je een paar regels verder leest. */
     scroll-snap-align: start;
     scroll-snap-stop: normal;
-    min-height: 100svh;
+    /* GEEN min-height meer. Die rekte elk bericht tot een vol scherm, en bij een
+       kort bericht leverde dat een halve pagina leegte op onder "Replies and
+       reactions" -- je zat naar niets te kijken en moest verder scrollen om te
+       ontdekken dat er nog iets kwam. Robins vondst (20-8), met een
+       schermafbeelding erbij.
+       Zonder die hoogte is de stroom gewoon bericht na bericht: je ziet het
+       volgende meteen onder het vorige beginnen. Het snappen doet nog steeds
+       zijn werk aan de grens -- dat hangt aan scroll-snap-align en niet aan de
+       hoogte -- dus de streep lijnt nog altijd uit met de bovenkant van het
+       volgende bericht. */
     max-width: 46rem;
     margin: 0 auto;
@@ -2461,9 +2521,8 @@
    scrollen om te zien wat het IS. */
 .feed-reader .read-cover {
-    /* Krimpt om het beeld heen en staat gecentreerd, in plaats van de kolom vol
-       te trekken. Anders staat een klein plaatje in een veel te grote lijst. */
-    width: fit-content;
-    max-width: 100%;
-    margin: 0 auto 1.2rem;
+    /* Volle kolombreedte: de afbeelding erin neemt die breedte, dus de figure
+       hoeft niet meer om het beeld te krimpen. */
+    width: 100%;
+    margin: 0 0 1.2rem;
     border-radius: 10px;
     overflow: hidden;
@@ -2472,16 +2531,15 @@
 .feed-reader .read-cover video {
     display: block;
-    /* GEEN width:100%. Dat blies een cover van 221x165 op tot 505x378 -- ruim
-       twee keer zo groot als hij is, en dus wazig. De tijdlijn deed dat nooit:
-       daar zat een cover in een vakje van 120px, dus klein bleef klein. Met
-       auto + max-width vult een grote afbeelding de kolom en blijft een kleine
-       op ware grootte staan.
-       En GEEN object-fit:cover: samen met de hoogtegrens sneed dat de onderkant
-       eraf (553x464 kwam er als 505x400 uit). Met max-width en max-height houdt
-       de browser de verhouding zelf en past het beeld HEEL in het vak. */
-    width: auto;
+    /* Neemt de VOLLE kolombreedte, met de verhouding intact (object-fit
+       weggelaten, dus geen bijsnijden). Een cover die van zichzelf kleiner is
+       wordt netjes tot de kolom opgeschaald in plaats van als postzegeltje te
+       blijven staan -- dat was te klein. De hoogtegrens houdt een staande foto
+       nog binnen de perken zodat titel + eerste alinea boven de vouw blijven;
+       hij krimpt dan mee in plaats van bijgesneden te worden. */
+    display: block;
+    width: 100%;
     height: auto;
-    max-width: 100%;
-    max-height: 45svh;
+    max-height: 70svh;
+    object-fit: contain;
     /* Een logo met doorzichtige achtergrond zweeft anders op de paginakleur;
        dezelfde keuze als bij .post-list-cover in de tijdlijn. */
@@ -2506,6 +2564,47 @@
 .feed-reader .read-open:focus-visible { opacity: 1; }
 
+/* Terug naar boven. Rechtsonder, want daar zit de duim -- en boven de tabbalk,
+   die op mobiel 1050 is en anders overheen zou vallen. Onzichtbaar tot je ver
+   genoeg bent, zodat hij niet meteen in de weg staat bij het eerste bericht. */
+.read-top {
+    position: fixed;
+    right: max(1rem, env(safe-area-inset-right, 0));
+    bottom: calc(env(safe-area-inset-bottom, 0) + 4.75rem);
+    z-index: 1040;
+    display: grid;
+    place-items: center;
+    width: 44px;
+    height: 44px;                 /* 44px = de kleinste maat die een duim betrouwbaar raakt */
+    border-radius: 999px;
+    border: 1px solid var(--rule);
+    background: color-mix(in srgb, var(--paper) 88%, transparent);
+    backdrop-filter: blur(14px) saturate(140%);
+    -webkit-backdrop-filter: blur(14px) saturate(140%);
+    color: var(--ink);
+    box-shadow: 0 2px 12px color-mix(in srgb, var(--ink) 12%, transparent);
+    cursor: pointer;
+    opacity: 0;
+    pointer-events: none;
+    transition: opacity .18s ease, transform .18s ease;
+    transform: translateY(6px);
+}
+.read-top.is-zichtbaar { opacity: 1; pointer-events: auto; transform: none; }
+/* Buiten de leesweergave bestaat hij niet -- home.ejs rendert alle drie de
+   secties, dus zonder deze regel zweeft hij ook boven Tijdlijn en Grid. */
+body:not([data-feed-view="reader"]) .read-top,
+body:not(.on-home) .read-top { display: none; }
+/* Op desktop is er geen tabbalk, dus mag hij lager staan. */
+@media (min-width: 768px) {
+    .read-top { bottom: 1.5rem; right: 1.5rem; }
+}
 @media (prefers-reduced-motion: reduce) {
-    html:has(body.on-home[data-feed-view="reader"]) { scroll-snap-type: none; }
+    .read-top { transition: none; transform: none; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+    html:has(body.on-home[data-feed-view="reader"]) {
+        scroll-snap-type: none;
+        scroll-behavior: auto;
+    }
 }
 
Index: src/assets/js/mod/read.js
===================================================================
--- src/assets/js/mod/read.js	(revision 598f09038f721164997d5a888801e65d93c1acb2)
+++ src/assets/js/mod/read.js	(revision e46ebaa7d0ba257f3e7916d2a4ecfa52e68617da)
@@ -17,4 +17,120 @@
  * cmd/ctrl-klik hoort de browser zelf af te handelen.
  */
+
+/**
+ * Snappen alleen waar het HELPT.
+ *
+ * Elk bericht heeft scroll-snap-align:start en min-height:100svh. Voor een kort
+ * bericht is dat precies goed: het vult het scherm en de volgende klikt netjes
+ * op zijn plek. Voor een LANG bericht is het een val -- je leest naar beneden en
+ * de browser trekt je terug naar de bovenrand, waardoor het scrollen lijkt te
+ * stoppen terwijl je gewoon doorscrollt. Barts melding (20-8).
+ *
+ * "Past dit bericht op het scherm?" is een vraag over gemeten hoogte, en die kan
+ * CSS niet stellen -- vandaar hier. Past het niet, dan gaat de snap eraf en lees
+ * je ononderbroken door tot het volgende bericht wel weer een snappunt is.
+ *
+ * De marge van 4px vangt afrondingsverschillen tussen svh en de echte hoogte;
+ * zonder die speling wipt een bericht dat toevallig exact past heen en weer.
+ */
+/**
+ * Wanneer is de bovenkant van een bericht een snappunt?
+ *
+ * Alleen zolang je er nog NIET voorbij bent. Zit de bovenrand op of onder de
+ * bovenkant van het scherm, dan kom je er nog aan en mag hij vangen. Is hij
+ * eenmaal voorbij -- je leest in het bericht -- dan gaat de snap eraf.
+ *
+ * Robins formulering (20-8), en die is preciezer dan wat ik er eerst van maakte:
+ * "in de post zelf mag er nooit gesnapt worden, ook niet bovenin; enkel de
+ * onderkant mag snappen naar de volgende post, dus daar de bovenkant van".
+ * Precies dat: de onderkant van bericht N is de bovenkant van N+1, en die is een
+ * doelwit omdat je hem nadert. De bovenkant van het bericht waar je IN zit is
+ * dat niet meer, want daar ben je voorbij.
+ *
+ * Zonder deze regel trok proximity je terug naar de bovenrand zodra je een paar
+ * regels verder scrolde, en leek het of het scrollen vastliep.
+ *
+ * De 4px is speling voor afronding: tijdens het snappen zelf loopt top naar 0 en
+ * mag hij niet halverwege afhaken.
+ */
+const SPELING = 4;
+
+/**
+ * En snappen doet alleen mee op de weg NAAR BENEDEN (Robin, 20-8).
+ *
+ * Omhoog scroll je om iets terug te zoeken, en dan is elke vangst een hindernis:
+ * je wilt zelf bepalen waar je stopt. Omlaag lees je door, en dan helpt de
+ * grens juist. Dus bij omhoog gaat de snap er overal af.
+ *
+ * De drempel van 2px is er tegen richtingsruis: tijdens een vloeiende snap
+ * schommelt scrollY een fractie, en zonder speling zou de richting dan heen en
+ * weer klappen -- precies midden in de beweging die net soepel moest zijn.
+ */
+let laatsteY = 0;
+let omlaag = true;
+
+/**
+ * Hoe DICHT bij de grens hij vangt: de onderste VANGZONE pixels van een bericht.
+ *
+ * In pixels, en bewust niet inhoudelijk. Ik probeerde het aan de voet van het
+ * bericht te hangen ("ben je de reacties voorbij"), maar bij een KORT bericht
+ * gaat dat mis: min-height rekt zo'n bericht tot een vol scherm, dus de voet
+ * staat middenin met lege ruimte eronder en de zone begint op een willekeurige
+ * plek. Robin ving dat (20-8) voordat het uitgerold stond.
+ *
+ * Eerst stond dit op 20px en dat was te krap om ooit te vangen. Honderdvijftig
+ * is ruwweg het laatste stukje van een bericht: kom je daarbinnen tot stilstand,
+ * dan trekt hij de streep recht naar de bovenkant van het volgende. Stop je
+ * eerder, dan blijf je gewoon staan.
+ *
+ * Eén getal, dus makkelijk bij te stellen als het onder een duim anders voelt.
+ */
+const VANGZONE = 150;
+
+function ijkEen(art) {
+  const top = art.getBoundingClientRect().top;
+  // Omhoog: nooit. Omlaag: alleen een grens die je NADERT en die binnen de zone
+  // ligt. De ondergrens blijft krap (SPELING): eenmaal voorbij wordt er niet
+  // teruggetrokken, ook niet over een paar pixels.
+  const wil = (!omlaag || top < -SPELING || top > VANGZONE) ? 'none' : '';
+  // Alleen aanraken als het echt verandert: elke stijlwijziging tijdens een
+  // vloeiende scroll is een kans op een hik, zeker op iOS.
+  if (art.style.scrollSnapAlign !== wil) art.style.scrollSnapAlign = wil;
+}
+
+function ijkSnappen() {
+  const y = window.scrollY;
+  if (Math.abs(y - laatsteY) > 2) omlaag = y > laatsteY;
+  laatsteY = y;
+  document.querySelectorAll('.feed-reader .read-post').forEach(ijkEen);
+}
+
+/**
+ * Terug naar boven, en bewust NIET window.scrollTo(0).
+ *
+ * In een stroom wil je terug naar het BEGIN VAN DIT BERICHT als je halverwege een
+ * lang stuk zit, en pas daarna naar de kop van de pagina. Twee keer drukken doet
+ * dus twee verschillende dingen -- dat scheelt op mobiel een halve minuut vegen.
+ */
+function naarBoven() {
+  const zacht = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+  const gedrag = zacht ? 'smooth' : 'auto';
+  const posts = [...document.querySelectorAll('.feed-reader .read-post')];
+  const huidig = posts.find((a) => {
+    const r = a.getBoundingClientRect();
+    return r.top <= 8 && r.bottom > 8;
+  });
+  // Sta je al bovenaan dit bericht (of bij het eerste), dan naar de paginakop.
+  if (huidig && huidig.getBoundingClientRect().top < -8) {
+    huidig.scrollIntoView({ behavior: gedrag, block: 'start' });
+    return;
+  }
+  window.scrollTo({ top: 0, behavior: gedrag });
+}
+
+/** De knop verschijnt pas als er iets ONDER je ligt om naar terug te keren. */
+function toonKnop(knop) {
+  knop.classList.toggle('is-zichtbaar', window.scrollY > window.innerHeight * 0.6);
+}
 
 let tapX = 0, tapY = 0;
@@ -37,4 +153,7 @@
 }
 
+let knop = null;
+let opScroll = null;
+
 export function init() {
   const s = document.getElementById('read-stream');
@@ -47,4 +166,31 @@
   s.addEventListener('pointerdown', onPointerDown, { passive: true });
   s.addEventListener('click', onTap);
+
+  // De knop staat in de HTML (read-top.ejs), zodat hij er ook is zonder deze
+  // module -- dan doet hij niets, maar hij springt niet in beeld bij het laden.
+  knop = document.getElementById('read-top');
+  if (knop) {
+    knop.onclick = naarBoven;
+    toonKnop(knop);
+  }
+
+  // Eén luisteraar, en losmaken bij een volgende init(): deze module draait bij
+  // ELKE paginawissel en anders stapelen ze op.
+  if (opScroll) {
+    window.removeEventListener('scroll', opScroll);
+    window.removeEventListener('resize', opScroll);
+  }
+  // Eén keer per frame, niet per scroll-event: scroll vuurt tientallen keren per
+  // seconde en we raken hier de stijl van elk bericht aan.
+  let gepland = false;
+  opScroll = () => {
+    if (knop) toonKnop(knop);
+    if (gepland) return;
+    gepland = true;
+    requestAnimationFrame(() => { gepland = false; ijkSnappen(); });
+  };
+  window.addEventListener('scroll', opScroll, { passive: true });
+  window.addEventListener('resize', opScroll, { passive: true });
+  ijkSnappen();   // ook meteen bij binnenkomst, niet pas bij de eerste scroll
 }
 
Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 598f09038f721164997d5a888801e65d93c1acb2)
+++ src/config/database.js	(revision e46ebaa7d0ba257f3e7916d2a4ecfa52e68617da)
@@ -200,4 +200,13 @@
   ensureColumn('sites', 'feed_view_default', "TEXT DEFAULT 'grid'"); // timeline | grid
   ensureColumn('sites', 'feed_view_switch',  'INTEGER DEFAULT 1');       // show switcher
+  // Welke TWEEDE weergave deze site aanbiedt naast Grid.
+  //   'reader'    Lezen, hele berichten, een per scherm
+  //   'timeline'  de chronologische lijst
+  //   'auto'      Lezen op mobiel, Tijdlijn op desktop (grens 768px)
+  // Lezen verving Tijdlijn, maar de tijdlijn is nooit weggehaald -- home.ejs
+  // rendert alle drie de secties en de CSS kiest. Dit maakt er weer een keuze
+  // van in plaats van een besluit voor iedereen. 'auto' kost geen JavaScript:
+  // het is een mediaquery, dus geen resize-afhandeling en geen flikkering.
+  ensureColumn('sites', 'feed_alt_view', "TEXT DEFAULT 'reader'");   // reader | timeline | auto
   ensureColumn('sites', 'show_search',     'INTEGER DEFAULT 1');
   ensureColumn('sites', 'show_archive_link', 'INTEGER DEFAULT 1');
Index: src/routes/admin-sites.js
===================================================================
--- src/routes/admin-sites.js	(revision 598f09038f721164997d5a888801e65d93c1acb2)
+++ src/routes/admin-sites.js	(revision e46ebaa7d0ba257f3e7916d2a4ecfa52e68617da)
@@ -327,5 +327,10 @@
 
   const f = req.body;
-  const feedViewDef = f.feed_view_default === 'grid' ? 'grid' : 'timeline';
+  // Twee vragen, en ze hingen scheef: dit pad schreef 'timeline' terwijl het
+  // AANMAAKpad 'reader' schreef, voor precies dezelfde keuze. Elke site die ooit
+  // is opgeslagen droeg dus 'timeline', en de client vertaalde dat stil terug.
+  // Nu betekent de waarde weer wat er staat.
+  const feedAlt = ['timeline', 'auto'].includes(f.feed_alt_view) ? f.feed_alt_view : 'reader';
+  const feedViewDef = f.feed_view_default === 'grid' ? 'grid' : feedAlt;
   const profileLinksJson = buildProfileLinks(f);
 
@@ -366,5 +371,5 @@
       enable_audio_player = ?,
       approve_followers = ?,
-      feed_view_default = ?, feed_view_switch = ?,
+      feed_view_default = ?, feed_view_switch = ?, feed_alt_view = ?,
       show_search = ?, show_archive_link = ?,
       custom_css = ?, custom_head_html = ?, custom_foot_html = ?,
@@ -390,4 +395,5 @@
     feedViewDef,
     f.feed_view_switch ? 1 : 0,
+    feedAlt,
     f.show_search ? 1 : 0,
     f.show_archive_link ? 1 : 0,
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision 598f09038f721164997d5a888801e65d93c1acb2)
+++ src/services/i18n.js	(revision e46ebaa7d0ba257f3e7916d2a4ecfa52e68617da)
@@ -33,5 +33,6 @@
     'switch.solo': 'Solo',
     'switch.circle': 'Cirkels',
-    'switch.grid': 'Grid', 'switch.reader': 'Lezen',
+    'switch.grid': 'Grid', 'switch.reader': 'Lezen', 'switch.timeline': 'Tijdlijn', 'read.to_top': 'Terug naar boven',
+    'asite.feed_alt': 'Tweede weergave', 'asite.feed_alt_reader': 'Lezen', 'asite.feed_alt_timeline': 'Tijdlijn', 'asite.feed_alt_auto': 'Lezen op mobiel, Tijdlijn op desktop',
     'switch.reader_solo_only': 'Lezen kan alleen in Solo — in de cirkel staan berichten van anderen',
     'postnav.newer': 'Nieuwer',
@@ -1048,5 +1049,6 @@
     'switch.solo': 'Solo',
     'switch.circle': 'Circles',
-    'switch.grid': 'Grid', 'switch.reader': 'Reader',
+    'switch.grid': 'Grid', 'switch.reader': 'Reader', 'switch.timeline': 'Timeline', 'read.to_top': 'Back to top',
+    'asite.feed_alt': 'Second view', 'asite.feed_alt_reader': 'Reader', 'asite.feed_alt_timeline': 'Timeline', 'asite.feed_alt_auto': 'Reader on mobile, Timeline on desktop',
     'switch.reader_solo_only': 'Reader is Solo only — a circle shows other people\u2019s posts',
     'postnav.newer': 'Newer',
@@ -2054,5 +2056,6 @@
     'switch.solo': 'Solo',
     'switch.circle': 'Zirkel',
-    'switch.grid': 'Raster', 'switch.reader': 'Lesen',
+    'switch.grid': 'Raster', 'switch.reader': 'Lesen', 'switch.timeline': 'Zeitleiste', 'read.to_top': 'Nach oben',
+    'asite.feed_alt': 'Zweite Ansicht', 'asite.feed_alt_reader': 'Lesen', 'asite.feed_alt_timeline': 'Zeitleiste', 'asite.feed_alt_auto': 'Lesen mobil, Zeitleiste am Desktop',
     'switch.reader_solo_only': 'Lesen gibt es nur in Solo — im Zirkel stehen Beitr\u00e4ge anderer',
     'postnav.newer': 'Neuer',
Index: src/views/pages/admin-site-edit.ejs
===================================================================
--- src/views/pages/admin-site-edit.ejs	(revision 598f09038f721164997d5a888801e65d93c1acb2)
+++ src/views/pages/admin-site-edit.ejs	(revision e46ebaa7d0ba257f3e7916d2a4ecfa52e68617da)
@@ -196,8 +196,19 @@
     <fieldset>
       <legend><%= t('asite.feed_view') %></legend>
+      <%# Twee vragen, en ze zijn niet hetzelfde: WELKE tweede weergave je site
+          aanbiedt naast Grid, en waarin hij opent. %>
+      <label>
+        <span><%= t('asite.feed_alt') %></span>
+        <select name="feed_alt_view">
+          <% var _a = ['timeline','auto'].indexOf(site.feed_alt_view) >= 0 ? site.feed_alt_view : 'reader'; %>
+          <option value="reader"   <%= _a === 'reader'   ? 'selected' : '' %>><%= t('asite.feed_alt_reader') %></option>
+          <option value="timeline" <%= _a === 'timeline' ? 'selected' : '' %>><%= t('asite.feed_alt_timeline') %></option>
+          <option value="auto"     <%= _a === 'auto'     ? 'selected' : '' %>><%= t('asite.feed_alt_auto') %></option>
+        </select>
+      </label>
       <label>
         <span><%= t('asite.feed_default') %></span>
         <select name="feed_view_default">
-          <option value="reader" <%= site.feed_view_default !== 'grid' ? 'selected' : '' %>><%= t('asite.feed_reader') %></option>
+          <option value="reader" <%= site.feed_view_default !== 'grid' ? 'selected' : '' %>><%= t('asite.feed_alt_' + _a) %></option>
           <option value="grid"     <%= site.feed_view_default === 'grid' ? 'selected' : '' %>><%= t('asite.feed_grid') %></option>
         </select>
Index: src/views/pages/home.ejs
===================================================================
--- src/views/pages/home.ejs	(revision 598f09038f721164997d5a888801e65d93c1acb2)
+++ src/views/pages/home.ejs	(revision e46ebaa7d0ba257f3e7916d2a4ecfa52e68617da)
@@ -67,4 +67,10 @@
 <% if (typeof readerItems !== 'undefined' && readerItems && readerItems.length) { %>
 <section class="feed-reader" aria-label="<%= t('switch.reader') %>">
+  <%# Terug naar boven. Staat IN de HTML en niet in JavaScript, zodat hij er ook
+      is als de module niet laadt -- dan doet hij niets, maar hij springt ook niet
+      opeens in beeld. Verschijnt pas als je ver genoeg bent (mod/read.js). %>
+  <button type="button" id="read-top" class="read-top" aria-label="<%= t('read.to_top') %>" title="<%= t('read.to_top') %>">
+    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
+  </button>
   <div class="read-stream" id="read-stream" data-base="<%= _base %>">
     <% readerItems.forEach(function(it) { %>
Index: src/views/partials/view-switcher.ejs
===================================================================
--- src/views/partials/view-switcher.ejs	(revision 598f09038f721164997d5a888801e65d93c1acb2)
+++ src/views/partials/view-switcher.ejs	(revision e46ebaa7d0ba257f3e7916d2a4ecfa52e68617da)
@@ -23,4 +23,22 @@
   var _leesUit = _bc.indexOf('on-cirkel') >= 0;
   var _gridStandaard = (typeof site !== 'undefined' && site && site.feed_view_default === 'grid');
+  // Welke tweede weergave biedt deze site aan? Bij 'auto' tekenen we ZE ALLEBEI
+  // en laat de CSS er per viewport een zien -- Lezen op mobiel, Tijdlijn op
+  // desktop. Dat scheelt een resize-afhandeling en een knop die van naam
+  // verspringt terwijl je kijkt.
+  var _alt = (typeof site !== 'undefined' && site && site.feed_alt_view) || 'reader';
+  if (['reader', 'timeline', 'auto'].indexOf(_alt) < 0) _alt = 'reader';
+  var _tweede = _alt === 'auto' ? ['reader', 'timeline'] : [_alt];
+  // De tijdlijn WERKT wel in de cirkel -- dat is er zelfs de natuurlijke vorm.
+  // Alleen Lezen kan daar niet, want dat zijn berichten van anderen.
+  var _dood = function (v) { return v === 'reader' && _leesUit; };
+  // Staat er GEEN levende tweede knop, dan is Grid het enige dat overblijft en
+  // hoort die aan te staan. Bij een tijdlijn-site is dat in de cirkel niet zo:
+  // daar werkt de tijdlijn gewoon.
+  var _alleenGrid = _tweede.every(_dood);
+  var _icoon = {
+    reader: '<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"/>',
+    timeline: '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
+  };
 %>
 <div class="view-switcher-wrap">
@@ -49,16 +67,18 @@
     <div class="view-switcher" role="tablist" aria-label="Weergave">
 
-      <%# Lezen is de standaardweergave sinds Tijdlijn weg is: hele berichten,
-          één per scherm. De chronologische lijst bestaat nog wel als opmaak
-          (.feed-timeline, ook op de cirkel en het archief), maar is geen KEUZE
-          meer -- daar was Lezen de betere vorm van. %>
-      <button type="button" class="view-switch-btn" data-view="reader" role="tab"
-              <%- _leesUit ? 'disabled aria-disabled="true" tabindex="-1" title="' + t('switch.reader_solo_only') + '"' : '' %>
-              aria-selected="<%= (!_leesUit && !_neutral && !_gridStandaard) ? 'true' : '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>
+      <%# De tweede weergave naast Grid. Welke dat is, staat in de instellingen
+          van de site (feed_alt_view). Bij 'auto' staan ze er allebei en kiest
+          een mediaquery; vandaar de klasse vs-alleen-*. %>
+      <% _tweede.forEach(function (v) { %>
+      <button type="button" class="view-switch-btn<%= _alt === 'auto' ? ' vs-alleen-' + (v === 'reader' ? 'mobiel' : 'desktop') : '' %>"
+              data-view="<%= v %>" role="tab"
+              <%- _dood(v) ? 'disabled aria-disabled="true" tabindex="-1" title="' + t('switch.reader_solo_only') + '"' : '' %>
+              aria-selected="<%= (!_dood(v) && !_neutral && !_gridStandaard) ? 'true' : '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"><%- _icoon[v] %></svg>
+        <span><%= t('switch.' + v) %></span>
       </button>
+      <% }); %>
       <button type="button" class="view-switch-btn" data-view="grid" role="tab"
-              aria-selected="<%= (!_neutral && (_leesUit || _gridStandaard)) ? 'true' : 'false' %>">
+              aria-selected="<%= (!_neutral && (_alleenGrid || _gridStandaard)) ? 'true' : '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"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
         <span><%= t('switch.grid') %></span>
Index: src/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision 598f09038f721164997d5a888801e65d93c1acb2)
+++ src/views/shell.ejs	(revision e46ebaa7d0ba257f3e7916d2a4ecfa52e68617da)
@@ -222,5 +222,5 @@
 
 <!-- v9 stylesheet (full palette system) -->
-<link rel="stylesheet" href="/assets/css/style.css?v=72">
+<link rel="stylesheet" href="/assets/css/style.css?v=77">
 <script>
 /* iOS safe-area, built by hand. env(safe-area-inset-top) resolves to 0 on this iOS in
@@ -345,5 +345,5 @@
 </head>
 
-<body class="<%= bodyClass || 'on-home' %> has-bottom-tab" data-js="chrome<%= (typeof pageJs !== 'undefined' && pageJs) ? ' ' + pageJs : '' %>" data-feed-view="<%- _e(safeSite.feed_view_default === 'grid' ? 'grid' : 'reader') %>" data-grid-cols="3" data-site-base="<%- _e((typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '') %>">
+<body class="<%= bodyClass || 'on-home' %> has-bottom-tab" data-js="chrome<%= (typeof pageJs !== 'undefined' && pageJs) ? ' ' + pageJs : '' %>" data-feed-view="<%- _e(safeSite.feed_view_default === 'grid' ? 'grid' : (["timeline","auto"].indexOf(safeSite.feed_alt_view) >= 0 ? safeSite.feed_alt_view : "reader")) %>" data-feed-alt="<%- _e(["timeline","auto"].indexOf(safeSite.feed_alt_view) >= 0 ? safeSite.feed_alt_view : "reader") %>" data-grid-cols="3" data-site-base="<%- _e((typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '') %>">
 
 <% if (typeof isViewer !== 'undefined' && isViewer) { %>
@@ -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 = 4;
+  var MOD_V = 13;
 
   // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
@@ -559,4 +559,5 @@
   // 'pcms-feed-view' en wordt hier nergens geschreven, zodat je thuiskomt in de
   // weergave die je achterliet.
+  var CIRKEL_SLEUTEL = 'pcms-cirkel-view';
   function opCirkel() { return body.classList.contains('on-cirkel'); }
 
@@ -565,14 +566,37 @@
     try {
       if (opCirkel()) {
-        body.dataset.feedView = 'grid';   // de enige mogelijkheid daar
+        // Lezen kan hier niet -- dat zijn berichten van ANDEREN, van hun servers,
+        // en "het hele stuk" hebben wij niet in handen. De knop staat er wel maar
+        // dood. Een TIJDLIJN kan hier juist bij uitstek: dat is de natuurlijke
+        // vorm van een cirkel.
+        //
+        // Beschikbaar, niet verplicht. Dit dwong eerst 'timeline' af zodra de
+        // site geen Lezen-site was, en dan kon je in de cirkel geen Grid meer
+        // kiezen. Nu is Grid de landing en is Tijdlijn een keuze ernaast.
+        var altC = body.dataset.feedAlt || 'reader';
+        var tijdlijnHier = altC === 'timeline'
+          || (altC === 'auto' && window.matchMedia('(min-width: 768px)').matches);
+        var magC = tijdlijnHier ? ['grid', 'timeline'] : ['grid'];
+        // Een EIGEN geheugen, en niet dat van solo. Twee redenen: hier kun je
+        // alleen uit Grid en Tijdlijn kiezen, dus een solo-keuze 'reader' zegt
+        // hier niets; en andersom mag een tuimeling hier je thuisweergave niet
+        // veranderen -- dan kom je thuis in iets anders dan je achterliet.
+        // (Dit geheugen stond er eerder al, verdween toen de cirkel nog maar een
+        // mogelijkheid had, en is nu weer nodig omdat Tijdlijn een echte keuze is.)
+        var vC = null;
+        try { vC = localStorage.getItem(CIRKEL_SLEUTEL); } catch (e) { /* geen opslag */ }
+        body.dataset.feedView = magC.indexOf(vC) >= 0 ? vC : 'grid';
         return;
       }
       var v = localStorage.getItem('pcms-feed-view');
-      // 'timeline' bestaat niet meer als keuze, maar staat nog in de localStorage
-      // van iedereen die hier eerder was. Zonder deze vertaling zou zo iemand een
-      // weergave houden die nergens meer een knop heeft: de pil helemaal grijs, en
-      // een feed die niet is wat hij koos. Lezen is waar Tijdlijn in opging.
-      if (v === 'timeline') v = 'reader';
-      if (v === 'grid' || v === 'reader') body.dataset.feedView = v;
+      // 'timeline' is sinds 20-8 weer een geldige keuze, maar alleen op een site
+      // die hem AANBIEDT. Wie de tijdlijn koos op de ene site en daarna een
+      // Lezen-site bezoekt, moet daar niet op een weergave landen die er geen
+      // knop heeft: dan staat de pil grijs en is de feed niet wat hij koos.
+      // Vandaar de toets tegen wat deze site biedt, in plaats van een vaste
+      // vertaling naar 'reader'.
+      var alt = body.dataset.feedAlt || 'reader';
+      var mag = alt === 'auto' ? ['grid', 'reader', 'timeline'] : ['grid', alt];
+      if (mag.indexOf(v) >= 0) body.dataset.feedView = v;
     } catch (e) { /* geen opslag: de server-standaard blijft staan */ }
   }
@@ -647,5 +671,8 @@
       // veranderen: die moet je terugvinden zoals je hem achterliet.
       try {
-        if (!opCirkel()) localStorage.setItem('pcms-feed-view', view);
+        // De cirkel heeft zijn eigen geheugen, zodat een keuze daar je
+        // thuisweergave niet aanraakt en andersom.
+        if (opCirkel()) localStorage.setItem(CIRKEL_SLEUTEL, view);
+        else localStorage.setItem('pcms-feed-view', view);
       } catch(_) {}
       syncAria();
Index: test/feed-alt-view.test.js
===================================================================
--- test/feed-alt-view.test.js	(revision e46ebaa7d0ba257f3e7916d2a4ecfa52e68617da)
+++ test/feed-alt-view.test.js	(revision e46ebaa7d0ba257f3e7916d2a4ecfa52e68617da)
@@ -0,0 +1,104 @@
+// De tweede weergave van een site: Lezen, Tijdlijn, of allebei (mobiel/desktop).
+//
+// Tijdlijn was geen KEUZE meer sinds Lezen hem verving, maar de renderkant is
+// nooit weggehaald: home.ejs stuurt alle drie de secties mee en de CSS kiest op
+// body[data-feed-view]. Dit maakt er weer een instelling van.
+//
+// Deze tests gaan door de ECHTE routes heen, en niet langs de SQL met een
+// handgemaakte rij. Reden: bij het bouwen zette ik een waarde in de INSERT
+// zonder de kolom (te weinig vraagtekens) en een kolom in de UPDATE zonder de
+// waarde -- waarbij alles erna een plek opschuift en show_search de waarde van
+// feed_alt_view kreeg. Geen van beide had een test langs de zijkant gevangen.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://test.example';
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'baas', 'b@t.nl', 'x', 'god');
+
+const express = (await import('express')).default;
+const router = (await import('../src/routes/admin-sites.js')).default;
+const app = express();
+app.use(express.urlencoded({ extended: true }));
+app.use((req, _res, next) => { req.session = { user: { id: 'u1', role: 'god' } }; next(); });
+app.use('/admin/sites', router);
+
+const server = app.listen(0);
+server.unref();   // houdt de suite niet in leven als een test halverwege stopt
+const poort = server.address().port;
+// MET EEN TIJDSLIMIET, en dat is geen voorzorg maar een gemeten noodzaak: gaat er
+// in de route iets mis (een SQL-fout door een scheve parameterlijst), dan vangt
+// express dat in een async-handler niet af en komt er NOOIT een antwoord. Zonder
+// limiet hangt de hele suite dan op 124 in plaats van te falen -- precies wat er
+// gebeurde toen ik de controleproef deed. Een test die op de fout die hij moet
+// vangen blijft hangen, meldt niets.
+const post = async (pad, velden) => {
+  try {
+    return await fetch(`http://127.0.0.1:${poort}${pad}`, {
+      method: 'POST', redirect: 'manual',
+      headers: { 'content-type': 'application/x-www-form-urlencoded' },
+      body: new URLSearchParams(velden).toString(),
+      signal: AbortSignal.timeout(5000),
+    });
+  } catch (e) {
+    assert.fail(`de route antwoordde niet op ${pad} (${e.name}) -- vrijwel altijd een `
+      + 'worp in de handler, en bij dit formulier meestal een kolommenlijst die niet '
+      + 'meer bij de waarden past');
+  }
+};
+const siteVan = (slug) => db.prepare('SELECT * FROM sites WHERE slug = ?').get(slug);
+
+test('de kolom bestaat en staat standaard op Lezen', async () => {
+  const r = await post('/admin/sites/create', { slug: 'proef', title: 'Proef' });
+  assert.ok(r.status === 302 || r.status === 200, 'aanmaken mag niet stranden, kreeg ' + r.status);
+  const s = siteVan('proef');
+  assert.ok(s, 'de site hoort aangemaakt te zijn');
+  assert.equal(s.feed_alt_view, 'reader', 'een verse site biedt Lezen aan');
+});
+
+test('opslaan zet de tweede weergave, en raakt zijn buren niet', async () => {
+  // show_search is de kolom die in de UPDATE direct NA feed_alt_view komt. Zette
+  // je de kolom er wel bij en de waarde niet, dan kwam de checkbox-waarde in
+  // feed_alt_view terecht en schoof al het andere op. Daarom staat hij hier.
+  await post('/admin/sites/proef/save', {
+    title: 'Proef', feed_alt_view: 'timeline', feed_view_default: 'reader',
+    feed_view_switch: '1', show_search: '1', show_archive_link: '1',
+  });
+  const s = siteVan('proef');
+  assert.equal(s.feed_alt_view, 'timeline');
+  assert.equal(s.show_search, 1, 'de buurkolom mag niet meeschuiven');
+  assert.equal(s.show_archive_link, 1, 'en die erachter ook niet');
+  assert.equal(s.feed_view_switch, 1);
+});
+
+test('de standaardweergave volgt de tweede weergave, niet een vaste naam', () => {
+  // Hier hing het scheef: het aanmaakpad schreef 'reader' en het opslaanpad
+  // 'timeline', voor precies dezelfde keuze. De waarde betekende dus niet wat
+  // er stond, en de client vertaalde dat stil terug.
+  const s = siteVan('proef');
+  assert.equal(s.feed_view_default, 'timeline',
+    'kiest de site Tijdlijn, dan opent hij daar ook in');
+});
+
+test('Grid als standaard blijft Grid, wat de tweede weergave ook is', async () => {
+  await post('/admin/sites/proef/save', { title: 'Proef', feed_alt_view: 'auto', feed_view_default: 'grid' });
+  const s = siteVan('proef');
+  assert.equal(s.feed_alt_view, 'auto');
+  assert.equal(s.feed_view_default, 'grid');
+});
+
+test('onzin in het formulier valt terug op Lezen', async () => {
+  await post('/admin/sites/proef/save', { title: 'Proef', feed_alt_view: 'iets-anders', feed_view_default: 'reader' });
+  assert.equal(siteVan('proef').feed_alt_view, 'reader');
+});
+
+// closeAllConnections EERST: fetch houdt de verbinding open (keep-alive), en dan
+// wacht server.close() tot in de eeuwigheid. Bij een geslaagde run valt dat niet
+// op; valt er een test om, dan hangt de hele suite. Zo overkwam het me bij de
+// controleproef -- de test die de fout moest aantonen, hing erop.
+test.after(() => { server.closeAllConnections?.(); server.close(); });
