Index: src/assets/js/mod/chrome.js
===================================================================
--- src/assets/js/mod/chrome.js	(revision 86cfa1a2d9b65efe6ad3db8bd0411bedd61b0f82)
+++ src/assets/js/mod/chrome.js	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
@@ -119,7 +119,26 @@
     try { localStorage.setItem('pcms-theme', next); } catch (e) {}
   }
+  // ── Het zoeken: de balk bovenaan, het vak met resultaten eronder ─────
+  // Twee plekken, één vorm (Robins ontwerp, 30-9): de overlay die op elke
+  // pagina kan openen, en de pagina /search?q=... zelf. Alles hieronder werkt
+  // op elk .search-surface, zodat de twee zich ook hetzelfde gedragen.
   function overlay() { return document.getElementById('search-overlay'); }
-  function openSearch() { var o = overlay(); if (o) { o.hidden = false; var i = o.querySelector('input'); if (i) i.focus(); } }
-  function closeSearch() { var o = overlay(); if (o) { o.hidden = true; var b = document.getElementById('search-suggest'); if (b) b.innerHTML = ''; } }
+  function openSearch() {
+    var o = overlay(); if (!o) return;
+    var inp = o.querySelector('input[name="q"]');
+    var box = o.querySelector('.search-box-inner');
+    // Een verse start: tekst en resultaten van de vorige keer horen niet te
+    // blijven staan als je hem opnieuw opent.
+    if (inp) inp.value = '';
+    if (box) box.innerHTML = '';
+    o.hidden = false;
+    document.documentElement.classList.add('search-open');
+    if (inp) inp.focus();
+  }
+  function closeSearch() {
+    var o = overlay(); if (!o) return;
+    o.hidden = true;
+    document.documentElement.classList.remove('search-open');
+  }
 
   document.body.addEventListener('click', function(e) {
@@ -137,84 +156,74 @@
   });
 
-  // ── Live results while typing ───────────────────────────────────
-  // De teksten komen van de overlay zelf, elke keer opnieuw: bij een
-  // htmx-navigatie wordt die vervangen en kan de taal gewisseld zijn.
-  function _st() {
-    var o = document.getElementById('search-overlay');
-    try { return JSON.parse((o && o.getAttribute('data-i18n')) || '{}'); } catch (e) { return {}; }
-  }
   function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }
-  function renderSuggest(box, d, q, ov) {
-    var html = '';
-    function grp(title, items, fmt) {
-      if (!items || !items.length) return;
-      html += '<div class="ss-group"><div class="ss-title">' + esc(title) + '</div>' + items.map(fmt).join('') + '</div>';
-    }
-    grp(_st().posts, d.posts, function(p){ return '<a class="ss-item" href="' + esc(p.url) + '">' + esc(p.title) + '</a>'; });
-    grp(_st().tracks, d.tracks, function(tk){ var sub = tk.artist ? ' <span class="ss-sub">' + esc(tk.artist) + '</span>' : ''; return tk.url ? '<a class="ss-item" href="' + esc(tk.url) + '">' + esc(tk.title) + sub + '</a>' : '<span class="ss-item ss-noclick">' + esc(tk.title) + sub + '</span>'; });
-    grp(_st().events, d.events, function(ev){ return '<a class="ss-item" href="' + esc(ev.url) + '">' + esc(ev.where || ev.when) + ' <span class="ss-sub">' + esc(ev.when) + '</span></a>'; });
-    grp(_st().pages, d.pages, function(pg){ return '<a class="ss-item" href="' + esc(pg.url) + '">' + esc(pg.label) + '</a>'; });
-    var hasAny = (d.posts && d.posts.length) || (d.tracks && d.tracks.length) || (d.events && d.events.length) || (d.pages && d.pages.length);
-    if (!hasAny) { box.innerHTML = '<div class="ss-empty">' + esc(_st().none) + '</div>'; return; }
-    var action = (ov && ov.getAttribute('data-action')) || '/search';
-    html += '<a class="ss-all" href="' + esc(action) + '?q=' + encodeURIComponent(q) + '">' + esc(_st().all) + '</a>';
-    box.innerHTML = html;
-  }
-  // Een ADRES is geen zoekterm (shaer-utpi). Dezelfde regel als lookupUri in
-  // routes/search.js: een schema is verplicht, anders wordt zoeken naar
-  // "soundfabrics.nl" een netwerkverzoek. De server beslist daarna nog eens,
-  // met de rechten erbij; dit scheelt alleen een vraag die toch nee oplevert.
-  function lookupUri(q) {
-    if (!/^https?:\/\//i.test(q) || /\s/.test(q)) return null;
-    try { var u = new URL(q); return u.hostname.indexOf('.') < 0 ? null : u.href; } catch (e) { return null; }
-  }
-  function allLink(ov, q) {
-    var action = (ov && ov.getAttribute('data-action')) || '/search';
-    return '<a class="ss-all" href="' + esc(action) + '?q=' + encodeURIComponent(q) + '">' + esc(_st().all) + '</a>';
-  }
-  function fetchSuggest(box, q, ov, seq) {
-    var url = (ov && ov.getAttribute('data-suggest')) || '/search/suggest';
-    fetch(url + '?q=' + encodeURIComponent(q))
-      .then(function(r){ return r.ok ? r.json() : null; })
-      .then(function(d){ if (d && seq === _sSeq) renderSuggest(box, d, q, ov); })
-      .catch(function(){});
-  }
-  // Elke toetsaanslag krijgt een volgnummer. Een ophaling van een andere
-  // server duurt soms seconden; zonder nummer overschrijft een traag antwoord
-  // op een oude vraag wat je inmiddels hebt getypt.
-  var _sTimer, _sSeq = 0;
+  function texts(surface) { try { return JSON.parse(surface.getAttribute('data-i18n') || '{}'); } catch (e) { return {}; } }
+
+  // Iets dat van een andere server moet komen: een adres of een handle. Dezelfde
+  // regels als lookupUri en lookupHandle in routes/search.js. Hier alleen om te
+  // weten of er meteen een wachtregel moet staan: de server beslist daarna
+  // zelf, met de rechten erbij.
+  function isRemoteLookup(q) {
+    if (/^https?:\/\//i.test(q) && !/\s/.test(q)) {
+      try { return new URL(q).hostname.indexOf('.') >= 0; } catch (e) { return false; }
+    }
+    return /^@?[a-z0-9_.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,}$/i.test(q);
+  }
+
+  // Elke zoekopdracht krijgt een volgnummer per vlak. Een ophaling van een
+  // andere server duurt soms seconden; zonder nummer overschrijft een traag
+  // antwoord op een oude vraag wat je inmiddels hebt getypt.
+  function runSearch(surface, now) {
+    if (!surface) return;
+    var inp = surface.querySelector('input[name="q"]');
+    var box = surface.querySelector('.search-box-inner');
+    if (!inp || !box) return;
+    var q = inp.value.trim();
+    var seq = (surface.__seq = (surface.__seq || 0) + 1);
+    clearTimeout(surface.__timer);
+    if (q.length < 2) { box.innerHTML = ''; return; }
+    var remote = isRemoteLookup(q);
+    if (remote) box.innerHTML = '<div class="sr-loading">' + esc(texts(surface).looking) + '</div>';
+    // Op de pagina beweegt het adres mee, zodat wat je ziet te delen blijft.
+    if (surface.classList.contains('is-page') && history.replaceState) {
+      history.replaceState(history.state, '', (surface.getAttribute('data-action') || '/search') + '?q=' + encodeURIComponent(q));
+    }
+    surface.__timer = setTimeout(function() {
+      var url = surface.getAttribute('data-results') || '/search/results';
+      fetch(url + '?q=' + encodeURIComponent(q), { credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } })
+        .then(function(r) { return r.status === 200 ? r.text() : ''; })
+        .then(function(html) {
+          if (seq !== surface.__seq) return;
+          box.innerHTML = html;
+          // De links naar een post zijn htmx-links; wat via innerHTML binnenkomt
+          // kent htmx nog niet.
+          if (window.htmx && window.htmx.process) window.htmx.process(box);
+        })
+        .catch(function() {});
+    }, now ? 0 : (remote ? 300 : 200));
+  }
   document.addEventListener('input', function(e) {
-    var inp = e.target.closest && e.target.closest('#search-overlay input[name="q"]');
-    if (!inp) return;
-    var box = document.getElementById('search-suggest');
-    if (!box) return;
-    var q = inp.value.trim();
-    var seq = ++_sSeq;
-    clearTimeout(_sTimer);
-    if (q.length < 2) { box.innerHTML = ''; return; }
-    var ov = document.getElementById('search-overlay');
-    var address = lookupUri(q);
-    if (!address) { _sTimer = setTimeout(function() { fetchSuggest(box, q, ov, seq); }, 200); return; }
-    // Een geplakt adres: de post zelf, niet de tekst van de link doorzocht.
-    box.innerHTML = '<div class="ss-remote-loading">' + esc(_st().looking) + '</div>';
-    _sTimer = setTimeout(function() {
-      var url = (ov && ov.getAttribute('data-remote')) || '/search/remote';
-      fetch(url + '?q=' + encodeURIComponent(address), { credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } })
-        .then(function(r) {
-          if (seq !== _sSeq) return;
-          // 204: geen recht om op te halen (of toch geen adres). Dan is het
-          // gewoon een zoekterm en krijg je de gewone suggesties.
-          if (r.status === 204 || !r.ok) { fetchSuggest(box, q, ov, seq); return; }
-          return r.text().then(function(html) { if (seq === _sSeq) box.innerHTML = html + allLink(ov, q); });
-        })
-        .catch(function() { if (seq === _sSeq) fetchSuggest(box, q, ov, seq); });
-    }, 300);
-  });
-  // Een klik op een suggestie sluit de overlay, want de link gaat ergens heen.
-  // Behalve wat in een nieuw tabblad opent (het origineel, een profiel): dan
-  // blijf je hier, en hoort de kaart er nog te staan als je terugkomt.
+    var inp = e.target.closest && e.target.closest('.search-surface input[name="q"]');
+    if (inp) runSearch(inp.closest('.search-surface'), false);
+  });
+  // Enter blijft waar hij is: het vak IS de resultaten. Zonder JS gaat het
+  // formulier naar /search, dat er precies zo uitziet.
+  document.addEventListener('submit', function(e) {
+    var f = e.target.closest && e.target.closest('.search-bar-form');
+    if (!f) return;
+    e.preventDefault();
+    runSearch(f.closest('.search-surface'), true);
+  });
+  // Sluiten op de pagina is teruggaan, als je van deze site kwam; anders volgt
+  // de link naar het begin van de site.
   document.addEventListener('click', function(e) {
-    var a = e.target.closest && e.target.closest('#search-suggest a');
-    if (a && a.getAttribute('target') !== '_blank') { var o = overlay(); if (o) o.hidden = true; }
+    var leave = e.target.closest && e.target.closest('[data-search-leave]');
+    if (!leave) return;
+    if (document.referrer.indexOf(location.origin + '/') === 0 && history.length > 1) { e.preventDefault(); history.back(); }
+  });
+  // Een klik op een resultaat in de overlay sluit hem, want de link gaat ergens
+  // heen. Behalve wat in een nieuw tabblad opent: dan blijf je hier.
+  document.addEventListener('click', function(e) {
+    var a = e.target.closest && e.target.closest('#search-overlay .search-box a');
+    if (a && a.getAttribute('target') !== '_blank') closeSearch();
   });
 })();
Index: src/middleware/render.js
===================================================================
--- src/middleware/render.js	(revision 86cfa1a2d9b65efe6ad3db8bd0411bedd61b0f82)
+++ src/middleware/render.js	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
@@ -108,5 +108,5 @@
 };
 
-const formatDate = (iso) => {
+export const formatDate = (iso) => {
   const d = parseStamp(iso);
   return d ? d.toLocaleDateString('nl-NL', { timeZone: siteTimezone(), day: 'numeric', month: 'long', year: 'numeric' }) : '';
Index: src/routes/search.js
===================================================================
--- src/routes/search.js	(revision 86cfa1a2d9b65efe6ad3db8bd0411bedd61b0f82)
+++ src/routes/search.js	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
@@ -21,5 +21,5 @@
 import express from 'express';
 import db from '../config/database.js';
-import { renderPage } from '../middleware/render.js';
+import { renderPage, formatDate } from '../middleware/render.js';
 import { audioUrl } from '../services/AudioStreamService.js';
 import { getSetting } from '../services/SettingsService.js';
@@ -86,4 +86,20 @@
 
 /**
+ * Is dit een HANDLE (@naam@server) en geen zoekterm? Dan de genormaliseerde
+ * handle, anders null.
+ *
+ * Met of zonder @ vooraan, zoals Mastodon het ook aanneemt. De server moet een
+ * echte domeinnaam zijn (met een punt en een extensie): "@robin" is een naam om
+ * naar te zoeken, geen adres. Een spatie maakt er een zoekopdracht van.
+ *
+ * WebFinger zelf is openbaar en gaat niet ondertekend; het profiel erachter
+ * wel, via resolveRemoteActor, en alleen namens een ingelogde beheerder.
+ */
+export function lookupHandle(q) {
+  const m = /^@?([a-z0-9_.-]+)@([a-z0-9-]+(?:\.[a-z0-9-]+)*\.[a-z]{2,})$/i.exec(String(q || '').trim());
+  return m ? `@${m[1]}@${m[2].toLowerCase()}` : null;
+}
+
+/**
  * Mag deze bezoeker een adres laten OPHALEN?
  *
@@ -114,8 +130,12 @@
  * een anonieme GET, en dan lijkt een bestaande post te ontbreken.
  */
-async function lookUpRemote(remoteUri, site) {
+async function lookUpRemote(remoteUri, site, { handle = false } = {}) {
   let remote = null;
   let remoteKind = null;
-  try { remote = await ActivityPubService.resolveRemoteNote(remoteUri, { asSlug: site.slug }); } catch { /* onbereikbaar */ }
+  // Een handle wijst altijd naar een persoon: geen post-poging, die zou alleen
+  // een verzoek kosten dat niets kan opleveren.
+  if (!handle) {
+    try { remote = await ActivityPubService.resolveRemoteNote(remoteUri, { asSlug: site.slug }); } catch { /* onbereikbaar */ }
+  }
   if (remote) remoteKind = 'note';
   if (!remote) {
@@ -226,63 +246,72 @@
 }
 
-// ── Full results page ────────────────────────────────────────────────────────
-router.get('/', async (req, res) => {
-  const site = res.locals.site;
-  if (!site) return res.status(404).send('No site');
-  const rawQ = (req.query.q || '').toString().trim();
-
-  if (!rawQ) {
-    return renderPage(req, res, 'pages/search', {
-      pageTitle: 'Zoeken', bodyClass: 'on-special', query: '',
-      results: [], tracks: [], events: [], pages: [], total: 0,
-    });
-  }
-
-  // Een adres wordt OPGEHAALD, niet doorzocht (shaer-utpi). Eerst als post,
-  // dan als profiel: een URL die geen van beide oplevert is gewoon een
-  // zoekterm die toevallig op een link lijkt, en dan blijft de rest staan.
-  const remoteUri = lookupUri(rawQ);
+// ── Wat er bij een zoekopdracht hoort ────────────────────────────────────────
+//
+// EEN verzameling voor beide ingangen: de pagina /search?q=... en het live vak
+// onder de zoekbalk (/search/results). Ze tonen sinds Robins ontwerp van 30-9
+// hetzelfde, dus ze horen ook hetzelfde op te halen.
+//
+// Een adres wordt OPGEHAALD, niet doorzocht (shaer-utpi). Eerst als post, dan
+// als profiel; ondertekend, en alleen namens een ingelogde beheerder.
+async function gather(req, res, rawQ) {
+  const site = res.locals.site;
+  // Een adres (https://...) of een handle (@naam@server): allebei iets dat van
+  // een andere server moet komen, dus allebei achter dezelfde rechtengrens.
+  const uri = lookupUri(rawQ);
+  const handle = uri ? null : lookupHandle(rawQ);
+  const remoteUri = uri || handle;
   const mayLookup = remoteUri ? mayLookUp(req, res) : false;
   const { remote, remoteKind, reacted: remoteReacted } = (remoteUri && mayLookup)
-    ? await lookUpRemote(remoteUri, site)
+    ? await lookUpRemote(remoteUri, site, { handle: !!handle })
     : { remote: null, remoteKind: null, reacted: { liked: false, boosted: false } };
-
   const r = searchSite(req, res, rawQ, { posts: 50, tracks: 25, events: 25, pages: 8 });
-  const total = r.results.length + r.tracks.length + r.events.length + r.pages.length;
+  return {
+    query: rawQ,
+    results: r.results, tracks: r.tracks, events: r.events, pages: r.pages,
+    total: r.results.length + r.tracks.length + r.events.length + r.pages.length,
+    queryError: r.queryError,
+    remoteUri, remote, remoteKind, mayLookup, remoteReacted,
+    siteTitle: site.title || '',
+  };
+}
+
+// ── De pagina: het zoekvlak, open, met de resultaten erin ────────────────────
+router.get('/', async (req, res) => {
+  const site = res.locals.site;
+  if (!site) return res.status(404).send('No site');
+  const rawQ = (req.query.q || '').toString().trim().slice(0, 2048);
+  const data = rawQ ? await gather(req, res, rawQ) : { query: '' };
   renderPage(req, res, 'pages/search', {
-    pageTitle: `Zoeken: ${rawQ}`, bodyClass: 'on-special', query: rawQ,
-    results: r.results, tracks: r.tracks, events: r.events, pages: r.pages,
-    total, queryError: r.queryError,
-    remoteUri, remote, remoteKind, mayLookup,
-    remoteReacted,
-    siteTitle: site.title || '',
+    pageTitle: rawQ ? `Zoeken: ${rawQ}` : 'Zoeken', bodyClass: 'on-special',
+    ...data,
   });
 });
 
-// ── De live-preview van een adres (HTML-fragment) ─────────────────────────────
+// ── Het live vak onder de zoekbalk (HTML-fragment) ───────────────────────────
 //
-// Plak je een link in de zoekbalk, dan haalt de uitklaplijst de post zelf op
-// in plaats van te zoeken naar de tekst van de link (shaer-utpi).
+// Dezelfde render als de pagina (partials/search-results.ejs), zodat het vak
+// en de pagina er per constructie hetzelfde uitzien. Dit verving twee routes:
+// /suggest (JSON, dat de browser zelf tot een lijstje bouwde, anders dan de
+// pagina) en /remote (alleen het adres).
 //
-// Dezelfde grens als de volle pagina, en hier weegt hij zwaarder: deze route
-// wordt bij elke toetsaanslag geraakt, dus zonder die grens is hij een
-// haalservice die iedereen deze server op adressen naar keuze laat afsturen.
-// Mag je niet ophalen, of is het geen adres, dan 204: de zoekbalk valt dan
-// terug op de gewone suggesties. Geen 403, want voor een bezoeker is een
-// geplakte link gewoon een zoekterm en geen geweigerde handeling.
+// De rechtengrens voor het ophalen zit in gather en is dezelfde als voor de
+// pagina; hier weegt hij zwaarder, want dit wordt bij elke toetsaanslag
+// geraakt. Een bezoeker die een link plakt krijgt de gewone resultaten en de
+// zin waarom er verder niets staat, en er gaat geen verzoek uit.
 const VIEWS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'views');
 
-router.get('/remote', async (req, res) => {
-  const site = res.locals.site;
-  const remoteUri = lookupUri(String(req.query.q || '').slice(0, 2048));
-  if (!site || !remoteUri || !mayLookUp(req, res)) return res.status(204).end();
-  const { remote, remoteKind, reacted } = await lookUpRemote(remoteUri, site);
-  // Dezelfde taal als de pagina eromheen, anders staat er een Engelse kaart
+router.get('/results', async (req, res) => {
+  const site = res.locals.site;
+  const rawQ = String(req.query.q || '').trim().slice(0, 2048);
+  if (!site || rawQ.length < 2) return res.status(204).end();
+  const data = await gather(req, res, rawQ);
+  // Dezelfde taal als de pagina eromheen, anders staat er een Engelse lijst
   // in een Nederlandse zoekbalk.
   const lang = resolveLang(req, { userLang: req.session?.user?.lang, defaultLang: getSetting('default_lang') });
-  const html = await ejs.renderFile(path.join(VIEWS_DIR, 'partials', 'remote-preview.ejs'), {
+  const html = await ejs.renderFile(path.join(VIEWS_DIR, 'partials', 'search-results.ejs'), {
+    ...data,
     t: (k, vars) => i18nT(lang, k, vars),
-    remote, remoteKind, remoteUri, reacted,
-    siteTitle: site.title || '',
+    formatDate,
+    siteUrlBase: res.locals.siteUrlBase || '',
   }, { async: false });
   // Wat deze bezoeker ziet hangt van zijn rechten en zijn reacties af: niet
@@ -292,20 +321,3 @@
 });
 
-// ── Live suggestions (JSON) ──────────────────────────────────────────────────
-router.get('/suggest', (req, res) => {
-  const site = res.locals.site;
-  if (!site) return res.json({ posts: [], tracks: [], events: [], pages: [] });
-  const rawQ = (req.query.q || '').toString().trim().slice(0, 100);
-  if (rawQ.length < 2) return res.json({ posts: [], tracks: [], events: [], pages: [] });
-
-  const urlFor = (slug) => `/${slug}`;
-  const r = searchSite(req, res, rawQ, { posts: 5, tracks: 4, events: 3, pages: 4 });
-  res.json({
-    posts: r.results.map((p) => ({ title: p.title || '(zonder titel)', url: urlFor(p.slug) })),
-    tracks: r.tracks.map((tr) => ({ title: tr.title, artist: tr.artist, url: tr.postUrl })),
-    events: r.events.map((e) => ({ when: [e.date, e.time].filter(Boolean).join(' '), where: e.where, url: e.url })),
-    pages: r.pages,
-  });
-});
-
 export default router;
Index: src/views/pages/search.ejs
===================================================================
--- src/views/pages/search.ejs	(revision 86cfa1a2d9b65efe6ad3db8bd0411bedd61b0f82)
+++ src/views/pages/search.ejs	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
@@ -1,265 +1,9 @@
-<% const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : ''; %>
-<% const _tracks = (typeof tracks !== 'undefined' && tracks) ? tracks : []; %>
-<% const _events = (typeof events !== 'undefined' && events) ? events : []; %>
-<% const _pages = (typeof pages !== 'undefined' && pages) ? pages : []; %>
-<div class="container search-page">
-  <h1><%= t('nav.search') %></h1>
+<%#
+  /search?q=... is geen eigen pagina meer met een eigen uiterlijk (Robins
+  ontwerp, 30-9): het is het zoekvlak, open, met de resultaten al op de server
+  gerenderd. Een gedeelde link en Enter zonder JavaScript komen zo op precies
+  hetzelfde beeld uit als de zoekbalk op elke andere pagina.
 
-  <form method="get" action="<%= _base %>/search" class="search-page-form">
-    <input
-      type="search"
-      name="q"
-      value="<%= query %>"
-      placeholder="<%= t('search.placeholder') %>"
-      autocomplete="off"
-      autofocus
-      aria-label="<%= t('nav.search') %>">
-    <button type="submit" class="btn btn-primary"><%= t('search.button') %></button>
-  </form>
-
-  <% if (queryError) { %>
-    <p class="search-error"><%= t('search.error') %></p>
-  <% } %>
-
-  <% if (query && !queryError) { %>
-    <p class="search-meta">
-      <%= t(total === 1 ? 'search.results_one' : 'search.results_other', { n: total, q: query }) %>
-    </p>
-  <% } %>
-
-  <%# Een geplakt adres is geen zoekterm maar een aanwijzing (shaer-utpi): het
-      object van de andere server staat bovenaan, met dezelfde knoppen als op
-      /authorize_interaction -- dezelfde partial, dus één render en één plek
-      waar een poort of een sanitizer kan wonen. %>
-  <% if (typeof remoteUri !== 'undefined' && remoteUri) { %>
-    <h2 class="search-section-title"><%= t('search.section_fediverse') %></h2>
-    <div class="auth-interact search-remote">
-      <% if (!mayLookup) { %>
-        <%# Ophalen is een uitgaand verzoek namens deze server; dat doen we niet
-            op verzoek van een willekeurige bezoeker. %>
-        <p class="auth-interact-note"><%= t('search.fedi_signin') %></p>
-      <% } else if (remote && remoteKind === 'note') { %>
-        <%- include('../partials/remote-object', { target: remote, uri: remoteUri, reacted: remoteReacted, siteTitle: siteTitle, cancelUrl: _base + '/search?q=' + encodeURIComponent(query) }) %>
-      <% } else if (remote && remoteKind === 'actor') { %>
-        <%- include('../partials/remote-actor', { actor: remote, siteTitle: siteTitle }) %>
-      <% } else { %>
-        <p class="auth-interact-note"><%= t('fedi.remote_notfound') %></p>
-      <% } %>
-    </div>
-    <%- include('../partials/fedi-interact-styles') %>
-  <% } %>
-
-  <% if (_tracks.length) { %>
-    <h2 class="search-section-title"><%= t('search.section_tracks') %></h2>
-    <% var _inPost = t('search.in_post'); /* t() vóór de loop ophalen: function(t) overschaduwt 'm */ %>
-    <div class="search-tracks">
-      <% _tracks.forEach(function(t) {
-           const tj = JSON.stringify({ id: t.id, url: t.url, title: t.title, artist: t.artist || '', cover: t.cover || '' })
-             .replace(/&/g, '&amp;').replace(/'/g, '&#39;').replace(/</g, '&lt;'); %>
-        <div class="post-audio-track search-track" id="track-<%= t.id %>"
-             data-pcms-track-id="<%= t.id %>" data-pcms-track-url="<%= t.url %>"
-             data-pcms-track='<%- tj %>'>
-          <button type="button" class="pat-play" aria-label="Speel <%= t.title %>">
-            <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 4l12 8-12 8z"/></svg>
-          </button>
-          <% if (t.cover) { %>
-            <span class="search-track-cover" style="background-image:url('<%= t.cover %>')" aria-hidden="true"></span>
-          <% } %>
-          <div class="pat-info">
-            <div class="pat-title"><%= t.title %></div>
-            <% if (t.artist || t.album) { %>
-              <div class="pat-artist">
-                <%= t.artist %><% if (t.artist && t.album) { %> &middot; <% } %><%= t.album %>
-              </div>
-            <% } %>
-          </div>
-          <% if (t.postUrl) { %>
-            <a class="search-track-link"
-               href="<%= t.postUrl %>"
-               hx-get="<%= t.postUrl %>?partial=1"
-               hx-target="#pcms-main"
-               hx-swap="innerHTML"
-               hx-push-url="<%= t.postUrl %>"
-               aria-label="<%= _inPost %>"><%= _inPost %></a>
-          <% } %>
-        </div>
-      <% }); %>
-    </div>
-  <% } %>
-
-  <% if (results && results.length) { %>
-    <% if (_tracks.length) { %><h2 class="search-section-title"><%= t('search.section_posts') %></h2><% } %>
-    <ol class="search-results">
-      <% results.forEach(function(r) { %>
-        <li class="search-result">
-          <h3 class="search-result-title">
-            <a href="<%= _base %>/<%= r.slug %>"
-               hx-get="<%= _base %>/<%= r.slug %>?partial=1"
-               hx-target="#pcms-main"
-               hx-swap="innerHTML"
-               hx-push-url="<%= _base %>/<%= r.slug %>"
-               hx-indicator="#pcms-loading">
-              <%= r.title || '(zonder titel)' %>
-            </a>
-          </h3>
-          <p class="search-result-meta">
-            <%= r.author_username %>
-            <% if (r.published_at) { %> &middot; <%= formatDate(r.published_at) %><% } %>
-          </p>
-          <p class="search-result-snippet"><%- r.snippet %></p>
-        </li>
-      <% }); %>
-    </ol>
-  <% } %>
-
-  <% if (_events.length) { %>
-    <h2 class="search-section-title"><%= t('search.section_events') %></h2>
-    <ol class="search-results">
-      <% _events.forEach(function(ev) { %>
-        <li class="search-result">
-          <h3 class="search-result-title"><a href="<%= ev.url %>"><%= ev.where || '—' %></a></h3>
-          <p class="search-result-meta"><%= [ev.date, ev.time].filter(Boolean).join(' ') %></p>
-        </li>
-      <% }); %>
-    </ol>
-  <% } %>
-
-  <% if (_pages.length) { %>
-    <h2 class="search-section-title"><%= t('search.section_pages') %></h2>
-    <ul class="search-pages">
-      <% _pages.forEach(function(pg) { %>
-        <li><a class="search-page-link" href="<%= pg.url %>"><%= pg.label %> <span aria-hidden="true">→</span></a></li>
-      <% }); %>
-    </ul>
-  <% } %>
-
-  <% if (query && !queryError && !results.length && !_tracks.length && !_events.length && !_pages.length) { %>
-    <p class="search-empty"><%= t('search.empty') %></p>
-  <% } %>
-</div>
-
-<style>
-.search-page { max-width: 720px; margin: 3rem auto; padding: 0 1rem; }
-.search-page h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: 0 0 1.5rem; }
-
-.search-page-form {
-  display: flex;
-  gap: 0.5rem;
-  margin-bottom: 1.5rem;
-}
-.search-page-form input {
-  flex: 1;
-  padding: 0.6rem 0.9rem;
-  border: 1px solid var(--rule);
-  border-radius: 5px;
-  background: var(--paper-2);
-  color: var(--ink);
-  font-size: 1rem;
-}
-.search-page-form input:focus {
-  outline: none;
-  border-color: var(--accent);
-}
-
-.search-meta {
-  color: var(--ink-muted, var(--ink-soft));
-  font-size: 0.9rem;
-  margin: 0 0 1rem;
-}
-.search-section-title {
-  font-family: var(--font-display, serif);
-  font-size: 1.1rem;
-  margin: 1.75rem 0 0.75rem;
-  color: var(--ink-soft);
-  text-transform: uppercase;
-  letter-spacing: 0.05em;
-}
-.search-error {
-  background: rgba(200, 60, 60, 0.15);
-  color: #c33;
-  padding: 0.75rem 1rem;
-  border-radius: 6px;
-  border: 1px solid rgba(200, 60, 60, 0.3);
-}
-.search-empty {
-  color: var(--ink-muted, var(--ink-soft));
-  text-align: center;
-  margin: 3rem 0;
-}
-
-/* Nummer-resultaten */
-.search-tracks { display: flex; flex-direction: column; gap: 0.25rem; }
-.search-track.post-audio-track {
-  display: flex;
-  align-items: center;
-  gap: 0.75rem;
-  padding: 0.5rem 0.6rem;
-  border-radius: 8px;
-}
-.search-track .search-track-cover {
-  width: 38px; height: 38px;
-  flex: 0 0 38px;
-  border-radius: 5px;
-  background-size: cover;
-  background-position: center;
-  background-color: var(--paper-2);
-}
-.search-track .pat-info { flex: 1; min-width: 0; }
-.search-track .pat-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.search-track .pat-artist { font-size: 0.82rem; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.search-track-link {
-  flex: 0 0 auto;
-  font-size: 0.82rem;
-  color: var(--accent);
-  text-decoration: none;
-  white-space: nowrap;
-}
-.search-track-link:hover { text-decoration: underline; }
-
-.search-results {
-  list-style: none;
-  margin: 0;
-  padding: 0;
-}
-.search-result {
-  padding: 1rem 0;
-  border-bottom: 1px solid var(--rule);
-}
-.search-result:last-child { border-bottom: 0; }
-
-.search-result-title {
-  font-family: var(--font-display, serif);
-  font-size: 1.25rem;
-  margin: 0 0 0.25rem;
-}
-.search-result-title a {
-  color: var(--ink);
-  text-decoration: none;
-}
-.search-result-title a:hover { color: var(--accent); }
-
-.search-result-meta {
-  margin: 0 0 0.5rem;
-  color: var(--ink-muted, var(--ink-soft));
-  font-size: 0.85rem;
-}
-
-.search-result-snippet {
-  margin: 0;
-  color: var(--ink-soft);
-  line-height: 1.5;
-}
-.search-result-snippet mark {
-  background: var(--accent);
-  color: white;
-  padding: 0.05em 0.25em;
-  border-radius: 3px;
-}
-.search-pages { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
-.search-page-link {
-  display: inline-flex; align-items: center; gap: 0.3rem;
-  padding: 0.45rem 0.8rem; border: 1px solid var(--rule); border-radius: 999px;
-  color: var(--ink); text-decoration: none; font-size: 0.9rem; background: var(--paper-2);
-}
-.search-page-link:hover { border-color: var(--accent); color: var(--accent); }
-</style>
+  De opmaak staat in partials/search-styles.ejs, die de topnav al meebrengt.
+%>
+<%- include('../partials/search-surface', Object.assign({}, locals, { mode: 'page' })) %>
Index: src/views/partials/fedi-interact-styles.ejs
===================================================================
--- src/views/partials/fedi-interact-styles.ejs	(revision 86cfa1a2d9b65efe6ad3db8bd0411bedd61b0f82)
+++ src/views/partials/fedi-interact-styles.ejs	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
@@ -159,16 +159,8 @@
     border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); background: var(--paper, #fff); color: var(--ink, #000); }
 
-  /* De live-preview: open-link onder de kaart, en de kaart zonder rand-marge. */
+  /* De compacte kaart in het zoekvak: de link naar de volle pagina eronder. */
   .rp-open { display: block; padding: 10px 4px 2px; text-align: right; font-weight: 600; font-size: .88rem;
     color: var(--accent, #06c); text-decoration: none; }
   .rp-open:hover { text-decoration: underline; }
-  .ss-remote { padding: 4px 0 6px; }
-  .ss-remote-note { padding: .8rem .6rem; color: var(--ink-soft, var(--ink-muted)); font-size: .9rem; }
-  .ss-remote-loading { display: flex; align-items: center; gap: 10px; padding: .9rem .6rem; color: var(--ink-soft, var(--ink-muted)); font-size: .9rem; }
-  .ss-remote-loading::before { content: ''; width: 16px; height: 16px; border-radius: 50%; flex: 0 0 16px;
-    border: 2px solid color-mix(in srgb, var(--accent, #06c) 30%, transparent); border-top-color: var(--accent, #06c);
-    animation: ss-spin .8s linear infinite; }
-  @keyframes ss-spin { to { transform: rotate(360deg); } }
-  @media (prefers-reduced-motion: reduce) { .ss-remote-loading::before { animation: none; } }
 
   /* Smal scherm: alleen de tekens, gelijk verdeeld over de breedte. De woorden
Index: src/views/partials/remote-preview.ejs
===================================================================
--- src/views/partials/remote-preview.ejs	(revision 86cfa1a2d9b65efe6ad3db8bd0411bedd61b0f82)
+++ 	(revision )
@@ -1,28 +1,0 @@
-<%#
-  De live-preview van een geplakt adres, onder de zoekbalk (shaer-utpi).
-
-  Een los fragment, geen pagina: de zoekbalk haalt dit op terwijl je plakt en
-  zet het in de uitklaplijst. Daarom:
-
-    - GEEN SCRIPT. Wat via innerHTML binnenkomt draait niet, en onder de
-      strenge CSP zou het ook niet mogen. Het gedrag (waarderen en boosten
-      zonder de pagina te verlaten) zit in chrome.js, dat op elke pagina staat.
-    - DE OPMAAK REIST MEE. De overlay staat op elke pagina en die heeft de
-      opmaak van de kaart niet vanzelf; style-src staat inline toe.
-    - COMPACT. Antwoorden en melden gebeuren op de volle pagina: een editor in
-      een uitklaplijst is op een telefoon geen plek om te schrijven.
-
-  Verwacht: remote, remoteKind ('note' | 'actor' | null), remoteUri, reacted,
-  siteTitle.
-%>
-<%- include('./fedi-interact-styles') %>
-<div class="ss-remote">
-  <div class="ss-title"><%= t('search.section_fediverse') %></div>
-  <% if (remote && remoteKind === 'note') { %>
-    <%- include('./remote-object', { target: remote, uri: remoteUri, reacted: reacted, siteTitle: siteTitle, compact: true }) %>
-  <% } else if (remote && remoteKind === 'actor') { %>
-    <%- include('./remote-actor', { actor: remote, siteTitle: siteTitle, compact: true }) %>
-  <% } else { %>
-    <div class="ss-remote-note"><%= t('fedi.remote_notfound') %></div>
-  <% } %>
-</div>
Index: src/views/partials/search-results.ejs
===================================================================
--- src/views/partials/search-results.ejs	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
+++ src/views/partials/search-results.ejs	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
@@ -0,0 +1,138 @@
+<%#
+  Wat er onder de zoekbalk staat. EEN render voor beide ingangen: het live
+  resultatenvak (via /search/results) en de pagina /search?q=... zelf. Zo zien
+  ze er per constructie hetzelfde uit; twee renders lopen gegarandeerd uit de
+  pas.
+
+  Verwacht: query, results (posts), tracks, events, pages, queryError,
+  remoteUri, remote, remoteKind, mayLookup, remoteReacted, siteTitle,
+  siteUrlBase, formatDate, t.
+
+  GEEN SCRIPT hierin: het live vak zet dit via innerHTML neer, en daar draait
+  niets. Gedrag (afspelen, waarderen, htmx-links) luistert op document.
+%>
+<%
+  const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
+  const _results = (typeof results !== 'undefined' && results) ? results : [];
+  const _tracks = (typeof tracks !== 'undefined' && tracks) ? tracks : [];
+  const _events = (typeof events !== 'undefined' && events) ? events : [];
+  const _pages = (typeof pages !== 'undefined' && pages) ? pages : [];
+  const _remote = (typeof remoteUri !== 'undefined' && remoteUri) ? remoteUri : '';
+  // Alles via locals: een veld dat een aanroeper vergeet mag de render niet
+  // laten omvallen (een kale verwijzing naar iets ongedefinieerds gooit).
+  const _queryError = !!locals.queryError;
+  const _mayLookup = !!locals.mayLookup;
+  const _found = locals.remote || null;
+  const _kind = locals.remoteKind || null;
+  const _reacted = locals.remoteReacted || { liked: false, boosted: false };
+  const _siteTitle = locals.siteTitle || '';
+  const _inPost = t('search.in_post');
+  const _any = _results.length || _tracks.length || _events.length || _pages.length;
+%>
+<% if (locals.query) { %>
+<div class="sr">
+  <% if (_remote) { %>
+    <%# Een geplakt adres is geen zoekterm maar een aanwijzing (shaer-utpi).
+        Ophalen doen we alleen namens een ingelogde beheerder van de site. %>
+    <section class="sr-section sr-fediverse">
+      <h2 class="sr-title"><%= t('search.section_fediverse') %></h2>
+      <% if (!_mayLookup) { %>
+        <p class="sr-note"><%= t('search.fedi_signin') %></p>
+      <% } else if (_found && _kind === 'note') { %>
+        <%- include('./remote-object', { target: _found, uri: _remote, reacted: _reacted, siteTitle: _siteTitle, compact: true }) %>
+      <% } else if (_found && _kind === 'actor') { %>
+        <%- include('./remote-actor', { actor: _found, siteTitle: _siteTitle, compact: true }) %>
+      <% } else { %>
+        <p class="sr-note"><%= t('fedi.remote_notfound') %></p>
+      <% } %>
+      <% if (_found) { %><%- include('./fedi-interact-styles') %><% } %>
+    </section>
+  <% } %>
+
+  <% if (_queryError) { %>
+    <p class="sr-note"><%= t('search.error') %></p>
+  <% } %>
+
+  <% if (_tracks.length) { %>
+    <section class="sr-section">
+      <h2 class="sr-title"><%= t('search.section_tracks') %></h2>
+      <div class="sr-tracks">
+        <% _tracks.forEach(function (tr) {
+             const tj = JSON.stringify({ id: tr.id, url: tr.url, title: tr.title, artist: tr.artist || '', cover: tr.cover || '' })
+               .replace(/&/g, '&amp;').replace(/'/g, '&#39;').replace(/</g, '&lt;'); %>
+          <div class="post-audio-track sr-track" id="track-<%= tr.id %>"
+               data-pcms-track-id="<%= tr.id %>" data-pcms-track-url="<%= tr.url %>"
+               data-pcms-track='<%- tj %>'>
+            <button type="button" class="pat-play" aria-label="<%= tr.title %>">
+              <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 4l12 8-12 8z"/></svg>
+            </button>
+            <% if (tr.cover) { %>
+              <span class="sr-track-cover" style="background-image:url('<%= tr.cover %>')" aria-hidden="true"></span>
+            <% } %>
+            <div class="pat-info">
+              <div class="pat-title"><%= tr.title %></div>
+              <% if (tr.artist || tr.album) { %>
+                <div class="pat-artist"><%= tr.artist %><% if (tr.artist && tr.album) { %> &middot; <% } %><%= tr.album %></div>
+              <% } %>
+            </div>
+            <% if (tr.postUrl) { %>
+              <a class="sr-track-link" href="<%= tr.postUrl %>"
+                 hx-get="<%= tr.postUrl %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="<%= tr.postUrl %>"><%= _inPost %></a>
+            <% } %>
+          </div>
+        <% }); %>
+      </div>
+    </section>
+  <% } %>
+
+  <% if (_results.length) { %>
+    <section class="sr-section">
+      <h2 class="sr-title"><%= t('search.section_posts') %></h2>
+      <ol class="sr-list">
+        <% _results.forEach(function (r) { %>
+          <li>
+            <a class="sr-item" href="<%= _base %>/<%= r.slug %>"
+               hx-get="<%= _base %>/<%= r.slug %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
+               hx-push-url="<%= _base %>/<%= r.slug %>" hx-indicator="#pcms-loading">
+              <span class="sr-item-title"><%= r.title || '(zonder titel)' %></span>
+              <span class="sr-item-meta"><%= r.author_username %><% if (r.published_at) { %> &middot; <%= formatDate(r.published_at) %><% } %></span>
+              <% if (r.snippet) { %><span class="sr-item-snippet"><%- r.snippet %></span><% } %>
+            </a>
+          </li>
+        <% }); %>
+      </ol>
+    </section>
+  <% } %>
+
+  <% if (_events.length) { %>
+    <section class="sr-section">
+      <h2 class="sr-title"><%= t('search.section_events') %></h2>
+      <ol class="sr-list">
+        <% _events.forEach(function (ev) { %>
+          <li>
+            <a class="sr-item" href="<%= ev.url %>">
+              <span class="sr-item-title"><%= ev.where || ev.date %></span>
+              <span class="sr-item-meta"><%= [ev.date, ev.time].filter(Boolean).join(' ') %></span>
+            </a>
+          </li>
+        <% }); %>
+      </ol>
+    </section>
+  <% } %>
+
+  <% if (_pages.length) { %>
+    <section class="sr-section">
+      <h2 class="sr-title"><%= t('search.section_pages') %></h2>
+      <ul class="sr-list">
+        <% _pages.forEach(function (pg) { %>
+          <li><a class="sr-item sr-item-row" href="<%= pg.url %>"><span class="sr-item-title"><%= pg.label %></span><span aria-hidden="true">→</span></a></li>
+        <% }); %>
+      </ul>
+    </section>
+  <% } %>
+
+  <% if (!_queryError && !_any && !_remote) { %>
+    <p class="sr-empty"><%= t('search.empty') %></p>
+  <% } %>
+</div>
+<% } %>
Index: src/views/partials/search-styles.ejs
===================================================================
--- src/views/partials/search-styles.ejs	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
+++ src/views/partials/search-styles.ejs	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
@@ -0,0 +1,70 @@
+<%#
+  De opmaak van het zoeken: het vlak (balk + vak) en wat erin staat. Staat in
+  de topnav, dus op elke pagina: de overlay kan overal open, en de pagina
+  /search gebruikt precies dezelfde regels.
+%>
+<style>
+  /* ── Het vlak: de balk bovenaan, het vak neemt de rest ───────────────────
+     Boven de tabbalk (1050) en de mini-speler (1000), onder een open blad
+     (1100). De vorige overlay lag op 200 en schoof op een telefoon onder de
+     tabbalk, zodat het onderste stuk resultaten wegviel. */
+  .search-surface { position: fixed; inset: 0; z-index: 1060; display: flex; flex-direction: column;
+    background: var(--paper); color: var(--ink); }
+  .search-surface[hidden] { display: none; }
+  .search-bar { flex: 0 0 auto; padding: .75rem 0; padding-top: calc(.75rem + env(safe-area-inset-top, 0px));
+    border-bottom: 1px solid var(--rule);
+    background: color-mix(in srgb, var(--paper) 94%, var(--ink) 6%); }
+  .search-bar-form { display: flex; align-items: center; gap: .5rem; max-width: 760px; margin: 0 auto; padding: 0 1rem; box-sizing: border-box; }
+  .search-bar-form input { flex: 1 1 auto; min-width: 0; min-height: 44px; padding: .7rem 1rem; box-sizing: border-box;
+    border: 1px solid var(--rule); border-radius: 12px; background: var(--paper); color: var(--ink);
+    font-family: var(--font-ui, inherit); font-size: 1rem; }
+  .search-bar-form input:focus { outline: 2px solid var(--accent); outline-offset: 0; border-color: transparent; }
+  .search-bar-btn { flex: 0 0 44px; width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center;
+    border: 0; border-radius: 999px; background: transparent; color: var(--ink-soft, var(--ink)); cursor: pointer;
+    text-decoration: none; -webkit-tap-highlight-color: transparent; }
+  .search-bar-btn:hover { background: color-mix(in srgb, var(--ink) 7%, transparent); color: var(--ink); }
+  .search-bar-btn svg { width: 22px; height: 22px; }
+  /* De goot aan beide kanten houdt de kolom op dezelfde lijn als de balk: een
+     scrollbalk alleen rechts schuift de resultaten anders een stukje naar links. */
+  .search-box { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
+    scrollbar-gutter: stable both-edges; }
+  .search-box-inner { max-width: 760px; margin: 0 auto; padding: .5rem 1rem calc(2rem + env(safe-area-inset-bottom, 0px)); box-sizing: border-box; }
+  .search-box-inner:empty { display: none; }
+  /* Zolang het vlak open is, scrolt de pagina eronder niet mee. */
+  html.search-open, html.search-open body { overflow: hidden; }
+
+  /* ── Wat er in het vak staat ─────────────────────────────────────────── */
+  .sr-section { margin: 1rem 0 0; }
+  .sr-title { margin: 0 0 .35rem; padding: 0 .25rem; font: 700 .72rem/1.4 var(--font-ui, inherit);
+    text-transform: uppercase; letter-spacing: .06em; color: var(--ink-soft, var(--ink-muted)); }
+  .sr-list { list-style: none; margin: 0; padding: 0; }
+  .sr-item { display: flex; flex-direction: column; gap: .1rem; min-height: 44px; padding: .6rem .6rem; border-radius: 10px;
+    color: var(--ink); text-decoration: none; box-sizing: border-box; }
+  .sr-item:hover, .sr-item:focus-visible { background: var(--paper-2); }
+  .sr-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
+  .sr-item-row { flex-direction: row; align-items: center; justify-content: space-between; }
+  .sr-item-title { font-weight: 600; overflow-wrap: anywhere; }
+  .sr-item:hover .sr-item-title { color: var(--accent); }
+  .sr-item-meta { font-size: .82rem; color: var(--ink-soft, var(--ink-muted)); }
+  .sr-item-snippet { font-size: .9rem; line-height: 1.5; color: var(--ink-soft); overflow: hidden;
+    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
+  .sr-item-snippet mark { background: color-mix(in srgb, var(--accent) 22%, transparent); color: inherit; padding: 0 .15em; border-radius: 3px; }
+  .sr-tracks { display: flex; flex-direction: column; gap: .15rem; }
+  .sr-track.post-audio-track { display: flex; align-items: center; gap: .75rem; min-height: 44px; padding: .45rem .6rem; border-radius: 10px; }
+  .sr-track-cover { flex: 0 0 38px; width: 38px; height: 38px; border-radius: 6px; background: var(--paper-2) center / cover; }
+  .sr-track .pat-info { flex: 1; min-width: 0; }
+  .sr-track .pat-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+  .sr-track .pat-artist { font-size: .82rem; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+  .sr-track-link { flex: 0 0 auto; font-size: .82rem; color: var(--accent); text-decoration: none; white-space: nowrap; }
+  .sr-track-link:hover { text-decoration: underline; }
+  .sr-note { margin: .25rem 0; padding: .7rem .8rem; border-radius: 10px; font-size: .9rem; line-height: 1.5;
+    background: var(--paper-2); color: var(--ink-soft, var(--ink-muted)); }
+  .sr-empty { margin: 3rem 0; text-align: center; color: var(--ink-soft, var(--ink-muted)); }
+  .sr-loading { display: flex; align-items: center; gap: 10px; padding: 1rem .6rem; color: var(--ink-soft, var(--ink-muted)); font-size: .9rem; }
+  .sr-loading::before { content: ''; flex: 0 0 16px; width: 16px; height: 16px; border-radius: 50%;
+    border: 2px solid color-mix(in srgb, var(--accent) 30%, transparent); border-top-color: var(--accent);
+    animation: sr-spin .8s linear infinite; }
+  @keyframes sr-spin { to { transform: rotate(360deg); } }
+  @media (prefers-reduced-motion: reduce) { .sr-loading::before { animation: none; } }
+  .sr-section .rp-card { margin-top: .25rem; }
+</style>
Index: src/views/partials/search-surface.ejs
===================================================================
--- src/views/partials/search-surface.ejs	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
+++ src/views/partials/search-surface.ejs	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
@@ -0,0 +1,50 @@
+<%#
+  Het zoeken, in twee vormen en verder niets: de zoekbalk bovenaan, en
+  daaronder het vak met resultaten dat alle ruimte krijgt (Robins ontwerp,
+  30-9). Er is geen aparte resultatenpagina meer die er anders uitziet.
+
+  EEN PARTIAL, TWEE INGANGEN:
+    mode 'overlay'  in de topnav, op elke pagina; verborgen tot je op zoeken
+                    tikt. Draagt de id's waar chrome.js hem aan herkent.
+    mode 'page'     /search?q=...: dezelfde vorm, open, met de resultaten al
+                    op de server gerenderd. Een gedeelde link en Enter zonder
+                    JavaScript komen zo op precies hetzelfde beeld uit.
+
+  Verwacht: mode, t, siteUrlBase. Voor 'page' ook query en de resultaten
+  (zie search-results.ejs).
+%>
+<%
+  const _page = locals.mode === 'page';
+  const _base = locals.siteUrlBase || '';
+  const _q = _page ? (locals.query || '') : '';
+%>
+<div class="search-surface<%= _page ? ' is-page' : '' %>"<% if (!_page) { %> id="search-overlay" hidden<% } %>
+     role="search"
+     data-results="<%= _base %>/search/results" data-action="<%= _base %>/search"
+     data-i18n="<%= JSON.stringify({ looking: t('search.fedi_looking') }) %>">
+  <div class="search-bar">
+    <form class="search-bar-form" method="get" action="<%= _base %>/search">
+      <input type="search" name="q" value="<%= _q %>" placeholder="<%= t('search.placeholder') %>"
+             autocomplete="off" aria-label="<%= t('nav.search') %>"<%= (_page && !_q) ? ' autofocus' : '' %>>
+      <button type="submit" class="search-bar-btn" aria-label="<%= t('search.button') %>">
+        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
+      </button>
+      <% if (_page) { %>
+        <%# Op de pagina is "sluiten" teruggaan waar je vandaan kwam. Zonder JS
+            brengt de link je naar het begin van de site. %>
+        <a class="search-bar-btn" href="<%= _base || '/' %>" data-search-leave aria-label="<%= t('imed.cancel') %>">
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
+        </a>
+      <% } else { %>
+        <button type="button" class="search-bar-btn" id="search-close" aria-label="<%= t('imed.cancel') %>">
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
+        </button>
+      <% } %>
+    </form>
+  </div>
+  <div class="search-box" data-lenis-prevent>
+    <div class="search-box-inner"<% if (!_page) { %> id="search-suggest"<% } %> aria-live="polite">
+      <% if (_page) { %><%- include('./search-results', locals) %><% } %>
+    </div>
+  </div>
+</div>
Index: src/views/partials/topnav.ejs
===================================================================
--- src/views/partials/topnav.ejs	(revision 86cfa1a2d9b65efe6ad3db8bd0411bedd61b0f82)
+++ src/views/partials/topnav.ejs	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
@@ -175,22 +175,8 @@
 </header>
 
-<!-- Search overlay (shared by mobile bottom-tab + desktop top-nav) -->
-<%# De teksten staan op het element dat ze gebruikt, niet in het script: een
-    module is een statisch bestand en kan geen vertaling interpoleren. %>
-<div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data-remote="<%= _siteUrlBase %>/search/remote" data-action="<%= _siteUrlBase %>/search"
-     data-i18n="<%= JSON.stringify({ posts: t('search.section_posts'), tracks: t('search.section_tracks'), events: t('search.section_events'), pages: t('search.section_pages'), all: t('search.suggest_all'), none: t('search.suggest_empty'), looking: t('search.fedi_looking') }) %>">
-  <div class="container">
-    <form class="search-overlay-form" method="get" action="<%= _siteUrlBase %>/search">
-      <input type="search" name="q" placeholder="<%= t('search.placeholder') %>" autocomplete="off">
-      <button type="submit" class="nav-btn" aria-label="<%= t('search.button') %>">
-        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
-      </button>
-      <button type="button" class="nav-btn" id="search-close" aria-label="<%= t('imed.cancel') %>">
-        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
-      </button>
-    </form>
-    <div id="search-suggest" class="search-suggest" data-lenis-prevent></div>
-  </div>
-</div>
+<!-- Het zoeken (gedeeld door de tabbalk op mobiel en de topnav op desktop) -->
+<%# Balk + vak, dezelfde partial als de pagina /search: zie search-surface.ejs. %>
+<%- include('./search-surface', { mode: 'overlay', t: t, siteUrlBase: _siteUrlBase }) %>
+<%- include('./search-styles') %>
 
 <%# Het script van de topnav staat in assets/js/mod/chrome.js (shaer-bqr). %>
@@ -441,41 +427,5 @@
 }
 
-/* ── Search overlay ── */
-.search-overlay {
-  position: fixed; top: 0; left: 0; right: 0;
-  background: color-mix(in srgb, var(--paper) 96%, transparent);
-  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
-  border-bottom: 1px solid var(--rule);
-  padding: 1rem 0; z-index: 200;
-  padding-top: calc(1rem + env(safe-area-inset-top, 0));
-  box-shadow: 0 8px 24px color-mix(in srgb, var(--ink) 8%, transparent);
-}
-.search-overlay-form { display: flex; gap: .5rem; align-items: center; }
-.search-overlay-form input {
-  flex: 1; padding: .85rem 1rem;
-  min-height: 44px;
-  border: 1px solid var(--rule); border-radius: 8px;
-  font-family: var(--font-ui); font-size: 1rem;
-  background: var(--paper); color: var(--ink);
-}
-.search-overlay-form input:focus {
-  outline: 2px solid var(--accent); outline-offset: 0;
-}
-.search-overlay-form .nav-btn {
-  width: 44px; height: 44px;
-  display: inline-flex; align-items: center; justify-content: center;
-}
-/* Live-resultaten dropdown */
-.search-suggest { margin-top: .6rem; max-height: 60vh; max-height: 70dvh; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; display: flex; flex-direction: column; gap: .25rem; }
-.search-suggest:empty { display: none; }
-.ss-group { padding: .25rem 0; }
-.ss-title { font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-soft, var(--ink-muted)); padding: .35rem .25rem .2rem; }
-.ss-item { display: block; padding: .55rem .6rem; border-radius: 8px; color: var(--ink); text-decoration: none; font-size: .95rem; }
-.ss-item:hover { background: var(--paper-2); color: var(--accent); }
-.ss-item.ss-noclick { opacity: .7; cursor: default; }
-.ss-sub { color: var(--ink-soft, var(--ink-muted)); font-size: .85em; }
-.ss-empty { padding: .8rem .6rem; color: var(--ink-soft, var(--ink-muted)); font-size: .9rem; }
-.ss-all { display: block; margin-top: .35rem; padding: .55rem .6rem; border-top: 1px solid var(--rule); color: var(--accent); font-weight: 600; text-decoration: none; font-size: .9rem; }
-.ss-all:hover { text-decoration: underline; }
+/* Het zoeken heeft zijn opmaak in partials/search-styles.ejs. */
 /* Notification bell + unread badge */
 .nav-notif { position: relative; }
Index: src/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision 86cfa1a2d9b65efe6ad3db8bd0411bedd61b0f82)
+++ src/views/shell.ejs	(revision fd8faf581c665d69b41dd6f5262d74bf432c0e1b)
@@ -130,5 +130,5 @@
 // ontwikkeling een jaar gecachet, dus zonder bump bereikt een reparatie
 // iedereen behalve wie de fout al heeft.
-const MOD_V = 65;
+const MOD_V = 66;
 %><!DOCTYPE html>
 <html lang="<%- _e(lang) %>" data-palette="<%- _e((typeof palette !== 'undefined' && palette) ? palette : (safeSite.palette || 'klonkt')) %>">
