Index: src/routes/search.js
===================================================================
--- src/routes/search.js	(revision e579a9844cdcc6898157a46486534dc0b81b928f)
+++ src/routes/search.js	(revision 097da22557ca814590ad9affc1e7910636320b0a)
@@ -1,19 +1,15 @@
 /**
- * GET /search?q=...
+ * GET /search?q=...          -> volledige resultatenpagina
+ * GET /search/suggest?q=...  -> compacte JSON voor live resultaten in de overlay
  *
- * Doorzoekt de huidige site op twee dingen:
- *   1. Posts via de posts_fts virtuele tabel (FTS5) — published only.
+ * Doorzoekt de huidige site op:
+ *   1. Posts via posts_fts (FTS5, prefix-matching) — published only.
  *   2. Nummers (audio_tracks) op titel / artiest / album.
+ *   3. Evenementen (shows) op plaats / locatie / land / notitie — als de agenda aan staat.
+ *   4. Pagina's (Agenda / Downloads / Links / Perskit / Archief) op naam — alleen
+ *      de beschikbare.
  *
- * Verbeteringen t.o.v. de oude versie:
- *   - Prefix-matching: elk woord wordt als prefix-term gezocht ("astr"* vindt
- *     "astra"), met AND tussen de woorden — typen-terwijl-je-zoekt werkt nu.
- *   - Nummers zijn doorzoekbaar en direct afspeelbaar in de resultatenlijst,
- *     met een link naar de post/album/playlist waarin het nummer voorkomt.
- *
- * FTS5-randgevallen:
- *   - Lege / whitespace-only query: form zonder resultaten.
- *   - User-input wordt getokeniseerd op niet-letter/cijfer en elk token tussen
- *     dubbele quotes + `*` gezet → geen operator-soup, geen syntax-errors.
+ * FTS5: user-input wordt getokeniseerd op niet-letter/cijfer en elk token tussen
+ * dubbele quotes + `*` gezet → prefix-match, geen operator-soup/syntax-errors.
  */
 
@@ -22,10 +18,9 @@
 import { renderPage } from '../middleware/render.js';
 import { audioUrl } from '../services/AudioStreamService.js';
+import { getSetting } from '../services/SettingsService.js';
+import { premiumUnlocked } from '../services/PatreonService.js';
 
 const router = express.Router();
 
-// Bouw een veilige FTS5-prefix-query: tokeniseer op alles wat geen letter/cijfer
-// is, en maak van elk token een prefix-term. Spatie = impliciete AND.
-// Bv. 'rock astr' -> '"rock"* "astr"*'. Geeft null als er geen bruikbaar token is.
 function buildFtsQuery(q) {
   const terms = q.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
@@ -34,19 +29,15 @@
 }
 
-// Escape LIKE-wildcards in user-input zodat % en _ letterlijk matchen.
 function likeArg(q) {
   return '%' + q.replace(/[%_\\]/g, '\\$&') + '%';
 }
 
-// Maak de FTS-snippet leesbaar: shortcodes ([[...]]) en markdown-tekens eruit.
-// De <mark>…</mark>-highlights van FTS blijven staan. Valt terug op de excerpt
-// (geëscaped) als er na het schoonmaken niets leesbaars overblijft.
 function cleanSnippet(html, excerpt) {
   const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
   const s = (html || '')
-    .replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')   // markdown-afbeeldingen
-    .replace(/\[\[[^\]]*?\]\]/g, ' ')         // shortcodes (incl. <mark> erin)
-    .replace(/\[\[|\]\]/g, ' ')               // losse haakjes door truncatie
-    .replace(/[#>*_`~]+/g, ' ')               // markdown-opmaaktekens
+    .replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
+    .replace(/\[\[[^\]]*?\]\]/g, ' ')
+    .replace(/\[\[|\]\]/g, ' ')
+    .replace(/[#>*_`~]+/g, ' ')
     .replace(/\s+/g, ' ')
     .trim();
@@ -55,120 +46,135 @@
 }
 
+// ── De kern: alle bronnen doorzoeken voor één site. `lim` begrenst per groep
+//    (klein voor de live-suggesties, ruim voor de volle pagina). ──────────────
+function searchSite(req, res, rawQ, lim) {
+  const site = res.locals.site;
+  const isHub = res.locals.tenancy === 'hub';
+  const base = res.locals.siteUrlBase || '';
+  const urlFor = (slug) => (isHub ? `/user/${site.slug}/${slug}` : `/${slug}`);
+  const t = res.locals.t || ((k) => k);
+
+  const out = { results: [], tracks: [], events: [], pages: [], queryError: null };
+  if (!site || !rawQ) return out;
+  const like = likeArg(rawQ);
+
+  // 1. Posts (FTS5)
+  const ftsQuery = buildFtsQuery(rawQ);
+  if (ftsQuery) {
+    try {
+      out.results = db.prepare(`
+        SELECT p.slug, p.title, p.excerpt, p.published_at, u.username AS author_username,
+               snippet(posts_fts, 0, '<mark>', '</mark>', '…', 18) AS snippet, bm25(posts_fts) AS score
+        FROM posts_fts
+        JOIN posts p ON p.id = posts_fts.post_id
+        JOIN users u ON u.id = p.author_id
+        WHERE posts_fts MATCH ? AND p.site_id = ? AND p.status = 'published'
+        ORDER BY score ASC LIMIT ?
+      `).all(ftsQuery, site.id, lim.posts);
+      out.results = out.results.map((r) => ({ ...r, snippet: cleanSnippet(r.snippet, r.excerpt) }));
+    } catch (err) { out.queryError = err.message; }
+  }
+
+  // 2. Nummers
+  try {
+    const trackRows = db.prepare(`
+      SELECT t.id, t.title, t.artist, t.album, t.cover_url, t.play_count, m.filename
+      FROM audio_tracks t LEFT JOIN media m ON m.id = t.media_id
+      WHERE t.site_id = @site
+        AND ( t.title LIKE @like ESCAPE '\\' OR t.artist LIKE @like ESCAPE '\\' OR t.album LIKE @like ESCAPE '\\' )
+      ORDER BY t.play_count DESC, t.title ASC LIMIT @lim
+    `).all({ site: site.id, like, lim: lim.tracks });
+    const playable = trackRows.filter((t) => t.filename);
+    let posts = [];
+    if (playable.length) {
+      posts = db.prepare("SELECT slug, content FROM posts WHERE site_id = ? AND status = 'published' ORDER BY published_at DESC").all(site.id);
+    }
+    const postUrlForTrack = (tr) => {
+      let hit = posts.find((p) => p.content && p.content.includes('[[track:' + tr.id + ']]'));
+      if (!hit && tr.album) hit = posts.find((p) => p.content && p.content.includes('[[album:' + tr.album + ']]'));
+      if (!hit) {
+        const plids = db.prepare('SELECT playlist_id FROM playlist_tracks WHERE track_id = ?').all(tr.id).map((r) => r.playlist_id);
+        if (plids.length) hit = posts.find((p) => p.content && plids.some((pl) => p.content.includes('[[playlist:' + pl + ']]')));
+      }
+      return hit ? urlFor(hit.slug) : null;
+    };
+    out.tracks = playable.map((tr) => ({
+      id: tr.id, title: tr.title || 'Untitled', artist: tr.artist || '', album: tr.album || '',
+      cover: tr.cover_url || '', url: audioUrl(tr.filename), postUrl: postUrlForTrack(tr),
+    }));
+  } catch (err) { if (!out.queryError) out.queryError = err.message; }
+
+  // 3. Evenementen (agenda) — alleen als de agenda publiek aan staat.
+  if (premiumUnlocked() && getSetting('agenda_enabled') === '1') {
+    try {
+      out.events = db.prepare(`
+        SELECT date, time, city, venue, country FROM shows
+        WHERE site_id = @site
+          AND ( city LIKE @like ESCAPE '\\' OR venue LIKE @like ESCAPE '\\'
+             OR country LIKE @like ESCAPE '\\' OR notes LIKE @like ESCAPE '\\' OR date LIKE @like ESCAPE '\\' )
+        ORDER BY date ASC LIMIT @lim
+      `).all({ site: site.id, like, lim: lim.events }).map((e) => ({
+        date: e.date, time: e.time || '',
+        where: [e.venue, e.city, e.country].filter(Boolean).join(', '),
+        url: base + '/shows',
+      }));
+    } catch (err) { if (!out.queryError) out.queryError = err.message; }
+  }
+
+  // 4. Pagina's — curated, alleen de beschikbare; match op de (vertaalde) naam.
+  const ql = rawQ.toLowerCase();
+  const candidates = [
+    { key: 'search.page_agenda', url: base + '/shows', on: premiumUnlocked() && getSetting('agenda_enabled') === '1' },
+    { key: 'search.page_downloads', url: base + '/downloads', on: premiumUnlocked() },
+    { key: 'search.page_links', url: base + '/links', on: premiumUnlocked() },
+    { key: 'search.page_perskit', url: base + '/pers', on: premiumUnlocked() },
+    { key: 'search.page_archive', url: urlFor('archive'), on: !site || site.show_archive_link === undefined || site.show_archive_link },
+  ];
+  out.pages = candidates
+    .filter((c) => c.on)
+    .map((c) => ({ label: t(c.key), url: c.url }))
+    .filter((c) => c.label.toLowerCase().includes(ql))
+    .slice(0, lim.pages);
+
+  return out;
+}
+
+// ── Volledige resultatenpagina ───────────────────────────────────────────────
 router.get('/', (req, res) => {
   const site = res.locals.site;
+  if (!site) return res.status(404).send('No site');
   const rawQ = (req.query.q || '').toString().trim();
-  const isHub = res.locals.tenancy === 'hub';
-  const urlFor = (slug) => (isHub ? `/user/${site.slug}/${slug}` : `/${slug}`);
 
-  if (!site) return res.status(404).send('No site');
-
-  // Lege query — toon het formulier zonder resultaten.
   if (!rawQ) {
     return renderPage(req, res, 'pages/search', {
-      pageTitle: 'Zoeken',
-      bodyClass: 'on-special',
-      query: '',
-      results: [],
-      tracks: [],
-      total: 0,
+      pageTitle: 'Zoeken', bodyClass: 'on-special', query: '',
+      results: [], tracks: [], events: [], pages: [], total: 0,
     });
   }
 
-  let results = [];
-  let tracks = [];
-  let queryError = null;
+  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;
+  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,
+  });
+});
 
-  // ── Posts (FTS5, prefix) ───────────────────────────────────────────
-  const ftsQuery = buildFtsQuery(rawQ);
-  if (ftsQuery) {
-    try {
-      results = db.prepare(`
-        SELECT
-          p.slug,
-          p.title,
-          p.excerpt,
-          p.published_at,
-          u.username AS author_username,
-          snippet(posts_fts, 0, '<mark>', '</mark>', '…', 18) AS snippet,
-          bm25(posts_fts) AS score
-        FROM posts_fts
-        JOIN posts p ON p.id = posts_fts.post_id
-        JOIN users u ON u.id = p.author_id
-        WHERE posts_fts MATCH ?
-          AND p.site_id = ?
-          AND p.status = 'published'
-        ORDER BY score ASC
-        LIMIT 50
-      `).all(ftsQuery, site.id);
-      results = results.map((r) => ({ ...r, snippet: cleanSnippet(r.snippet, r.excerpt) }));
-    } catch (err) {
-      queryError = err.message;
-    }
-  }
+// ── Live suggesties (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: [] });
 
-  // ── Nummers (audio_tracks: titel / artiest / album) ────────────────
-  try {
-    const like = likeArg(rawQ);
-    const trackRows = db.prepare(`
-      SELECT t.id, t.title, t.artist, t.album, t.cover_url, t.play_count, m.filename
-      FROM audio_tracks t
-      LEFT JOIN media m ON m.id = t.media_id
-      WHERE t.site_id = @site
-        AND ( t.title  LIKE @like ESCAPE '\\'
-           OR t.artist LIKE @like ESCAPE '\\'
-           OR t.album  LIKE @like ESCAPE '\\' )
-      ORDER BY t.play_count DESC, t.title ASC
-      LIMIT 25
-    `).all({ site: site.id, like });
-
-    // Eén keer alle published posts van de site ophalen om per nummer de
-    // post/album/playlist-pagina te vinden waarin 'ie voorkomt (in-memory match).
-    const playable = trackRows.filter((t) => t.filename);
-    let posts = [];
-    if (playable.length) {
-      posts = db.prepare(`
-        SELECT slug, content FROM posts
-        WHERE site_id = ? AND status = 'published'
-        ORDER BY published_at DESC
-      `).all(site.id);
-    }
-    const postUrlForTrack = (t) => {
-      let hit = posts.find((p) => p.content && p.content.includes('[[track:' + t.id + ']]'));
-      if (!hit && t.album) {
-        hit = posts.find((p) => p.content && p.content.includes('[[album:' + t.album + ']]'));
-      }
-      if (!hit) {
-        const plids = db.prepare('SELECT playlist_id FROM playlist_tracks WHERE track_id = ?')
-          .all(t.id).map((r) => r.playlist_id);
-        if (plids.length) {
-          hit = posts.find((p) => p.content && plids.some((pl) => p.content.includes('[[playlist:' + pl + ']]')));
-        }
-      }
-      return hit ? urlFor(hit.slug) : null;
-    };
-
-    tracks = playable.map((t) => ({
-      id: t.id,
-      title: t.title || 'Untitled',
-      artist: t.artist || '',
-      album: t.album || '',
-      cover: t.cover_url || '',
-      url: audioUrl(t.filename),
-      postUrl: postUrlForTrack(t),
-    }));
-  } catch (err) {
-    if (!queryError) queryError = err.message;
-  }
-
-  const total = results.length + tracks.length;
-
-  renderPage(req, res, 'pages/search', {
-    pageTitle: `Zoeken: ${rawQ}`,
-    bodyClass: 'on-special',
-    query: rawQ,
-    results,
-    tracks,
-    total,
-    queryError,
+  const isHub = res.locals.tenancy === 'hub';
+  const urlFor = (slug) => (isHub ? `/user/${site.slug}/${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,
   });
 });
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision e579a9844cdcc6898157a46486534dc0b81b928f)
+++ src/services/i18n.js	(revision 097da22557ca814590ad9affc1e7910636320b0a)
@@ -109,4 +109,7 @@
     'search.section_tracks': 'Nummers', 'search.section_posts': 'Posts',
     'search.empty': 'Niets gevonden.', 'search.in_post': 'in post →',
+    'search.section_events': 'Evenementen', 'search.section_pages': 'Pagina’s',
+    'search.page_agenda': 'Agenda', 'search.page_downloads': 'Downloads', 'search.page_links': 'Links', 'search.page_perskit': 'Perskit', 'search.page_archive': 'Archief',
+    'search.suggest_all': 'Alle resultaten →', 'search.suggest_empty': 'Geen resultaten', 'search.suggest_typing': 'Typ om te zoeken…',
     'like.login_title': 'Log in om deze post te liken',
     // === Beheer-sub-pagina's ===
@@ -1072,4 +1075,7 @@
     'search.section_tracks': 'Tracks', 'search.section_posts': 'Posts',
     'search.empty': 'Nothing found.', 'search.in_post': 'in post →',
+    'search.section_events': 'Events', 'search.section_pages': 'Pages',
+    'search.page_agenda': 'Agenda', 'search.page_downloads': 'Downloads', 'search.page_links': 'Links', 'search.page_perskit': 'Press kit', 'search.page_archive': 'Archive',
+    'search.suggest_all': 'All results →', 'search.suggest_empty': 'No results', 'search.suggest_typing': 'Type to search…',
     'like.login_title': 'Log in to like this post',
     // === Admin sub-pages ===
@@ -2034,4 +2040,7 @@
     'search.section_tracks': 'Titel', 'search.section_posts': 'Beiträge',
     'search.empty': 'Nichts gefunden.', 'search.in_post': 'im Beitrag →',
+    'search.section_events': 'Veranstaltungen', 'search.section_pages': 'Seiten',
+    'search.page_agenda': 'Termine', 'search.page_downloads': 'Downloads', 'search.page_links': 'Links', 'search.page_perskit': 'Pressekit', 'search.page_archive': 'Archiv',
+    'search.suggest_all': 'Alle Ergebnisse →', 'search.suggest_empty': 'Keine Ergebnisse', 'search.suggest_typing': 'Zum Suchen tippen…',
     'like.login_title': 'Zum Liken anmelden',
     // === Verwaltungs-Unterseiten ===
Index: src/views/pages/search.ejs
===================================================================
--- src/views/pages/search.ejs	(revision e579a9844cdcc6898157a46486534dc0b81b928f)
+++ src/views/pages/search.ejs	(revision 097da22557ca814590ad9affc1e7910636320b0a)
@@ -1,4 +1,6 @@
 <% 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>
@@ -89,5 +91,26 @@
   <% } %>
 
-  <% if (query && !queryError && !results.length && !_tracks.length) { %>
+  <% 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>
   <% } %>
@@ -211,3 +234,10 @@
   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>
Index: src/views/partials/topnav.ejs
===================================================================
--- src/views/partials/topnav.ejs	(revision e579a9844cdcc6898157a46486534dc0b81b928f)
+++ src/views/partials/topnav.ejs	(revision 097da22557ca814590ad9affc1e7910636320b0a)
@@ -183,15 +183,16 @@
 
 <!-- Search overlay (shared by mobile bottom-tab + desktop top-nav) -->
-<div id="search-overlay" class="search-overlay" hidden>
+<div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data-action="<%= _siteUrlBase %>/search">
   <div class="container">
     <form class="search-overlay-form" method="get" action="<%= _siteUrlBase %>/search">
-      <input type="search" name="q" placeholder="Zoeken in posts…" autocomplete="off">
-      <button type="submit" class="nav-btn" aria-label="Zoeken">
+      <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="Sluiten">
+      <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>
@@ -215,5 +216,5 @@
   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; }
+  function closeSearch() { var o = overlay(); if (o) { o.hidden = true; var b = document.getElementById('search-suggest'); if (b) b.innerHTML = ''; } }
 
   document.body.addEventListener('click', function(e) {
@@ -228,5 +229,51 @@
 
   document.addEventListener('keydown', function(e) {
-    if (e.key === 'Escape') { var o = overlay(); if (o && !o.hidden) o.hidden = true; }
+    if (e.key === 'Escape') { var o = overlay(); if (o && !o.hidden) closeSearch(); }
+  });
+
+  // ── Live resultaten tijdens typen ───────────────────────────────
+  var _ST = {
+    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') %>'
+  };
+  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;
+  }
+  var _sTimer;
+  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();
+    clearTimeout(_sTimer);
+    if (q.length < 2) { box.innerHTML = ''; return; }
+    _sTimer = setTimeout(function() {
+      var ov = document.getElementById('search-overlay');
+      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) renderSuggest(box, d, q, ov); })
+        .catch(function(){});
+    }, 200);
+  });
+  // Klik op een suggestie → overlay sluiten (de link/boost doet de navigatie).
+  document.addEventListener('click', function(e) {
+    if (e.target.closest && e.target.closest('#search-suggest a')) { var o = overlay(); if (o) o.hidden = true; }
   });
 })();
@@ -499,3 +546,15 @@
   display: inline-flex; align-items: center; justify-content: center;
 }
+/* Live-resultaten dropdown */
+.search-suggest { margin-top: .6rem; max-height: 60vh; overflow-y: auto; 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; }
 </style>
