Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 0dba0925244c76ab297b7130179cfd7339182667)
+++ src/config/database.js	(revision 914eb9fede1ff8c7deceec392018ef477d88dc47)
@@ -356,4 +356,28 @@
   `);
   ensureColumn('ap_interactions', 'parent_uri', 'TEXT'); // nesting (existing DBs)
+
+  // Fediverse CLIENT: accounts WE follow (outbound) + the home timeline of their posts.
+  db.exec(`
+    CREATE TABLE IF NOT EXISTS ap_following (
+      id INTEGER PRIMARY KEY AUTOINCREMENT,
+      slug TEXT NOT NULL,            -- our site that follows
+      actor_uri TEXT NOT NULL,       -- the followed account's actor id
+      handle TEXT, name TEXT, icon TEXT, url TEXT,
+      inbox TEXT,                    -- their inbox (for Create delivery / Undo)
+      follow_id TEXT,                -- the Follow activity id we sent (Accept matching)
+      status TEXT DEFAULT 'pending', -- pending | accepted
+      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+      UNIQUE(slug, actor_uri)
+    );
+    CREATE TABLE IF NOT EXISTS ap_timeline (
+      id TEXT NOT NULL,              -- the remote note's AP id
+      slug TEXT NOT NULL,            -- whose home timeline (our site)
+      author_uri TEXT, author_name TEXT, author_handle TEXT, author_icon TEXT, author_url TEXT,
+      content TEXT, url TEXT, published TEXT, media_json TEXT,
+      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+      UNIQUE(slug, id)
+    );
+    CREATE INDEX IF NOT EXISTS idx_ap_timeline_slug ON ap_timeline(slug, published);
+  `);
 }
 
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 0dba0925244c76ab297b7130179cfd7339182667)
+++ src/routes/posts.js	(revision 914eb9fede1ff8c7deceec392018ef477d88dc47)
@@ -86,5 +86,5 @@
   'tag', 'type', 'user', 'users', 'artiesten', 'leden', 'favorieten', 'feed.xml', 'atom.xml', 'sitemap.xml',
   'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets',
-  'authorize_interaction', 'fediverse',
+  'authorize_interaction', 'fediverse', 'tijdlijn',
 ]);
 
@@ -561,4 +561,37 @@
   }
   res.redirect(req.get('Referer') || `${res.locals.siteUrlBase || ''}/fediverse`);
+});
+
+// ==================== FEDIVERSE CLIENT: home timeline + following ====================
+router.get('/tijdlijn', requireSiteManager, (req, res) => {
+  const site = res.locals.site;
+  const following = site ? ActivityPubService.listFollowing(site.slug) : [];
+  const timeline = site ? ActivityPubService.getTimeline(site.slug, 60) : [];
+  renderPage(req, res, 'pages/timeline', {
+    pageTitle: 'Tijdlijn', bodyClass: 'on-special',
+    following, timeline,
+    success: req.query.success || null, error: req.query.error || null,
+  });
+});
+
+router.post('/tijdlijn/follow', requireSiteManager, async (req, res) => {
+  const site = res.locals.site;
+  const handle = (req.body.handle || '').toString();
+  let q = 'success=' + encodeURIComponent('Volgverzoek verstuurd');
+  if (site && handle.trim()) {
+    try {
+      const r = await ActivityPubService.followActor(site, handle);
+      if (r && r.error) q = 'error=' + encodeURIComponent(r.error === 'not_found' ? 'Account niet gevonden' : (r.error === 'unreachable' ? 'Server onbereikbaar' : 'Volgen mislukt'));
+      else q = 'success=' + encodeURIComponent('Je volgt nu ' + ((r && r.name) || handle));
+    } catch (e) { q = 'error=' + encodeURIComponent('Volgen mislukt'); }
+  }
+  res.redirect('/tijdlijn?' + q);
+});
+
+router.post('/tijdlijn/unfollow', requireSiteManager, async (req, res) => {
+  const site = res.locals.site;
+  const actorUri = (req.body.actor_uri || '').toString();
+  if (site && actorUri) { try { await ActivityPubService.unfollowActor(site, actorUri); } catch (e) { /* ignore */ } }
+  res.redirect('/tijdlijn?success=' + encodeURIComponent('Ontvolgd'));
 });
 
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 0dba0925244c76ab297b7130179cfd7339182667)
+++ src/services/ActivityPubService.js	(revision 914eb9fede1ff8c7deceec392018ef477d88dc47)
@@ -406,4 +406,16 @@
       iStmts().ins.run('reply', tgt.post_id, o.id || '', actorUri, ai.name, ai.handle, ai.url, ai.icon, html, o.published || null, tgt.parent_uri);
       console.log('[AP] reply', actorUri, '→', tgt.post_id);
+      return 202;
+    }
+    // Home timeline (client): a top-level post from an account we follow.
+    if (actorUri && !isLocalActor && !o.inReplyTo && o.id) {
+      let subs = []; try { subs = db.prepare('SELECT slug FROM ap_following WHERE actor_uri = ?').all(actorUri); } catch { /* table may not exist yet */ }
+      if (subs.length) {
+        const ai = actorInfo(await resolveActor(actorUri), actorUri);
+        const html = HtmlSanitizerService.sanitize(o.content || '');
+        const media = JSON.stringify((Array.isArray(o.attachment) ? o.attachment : []).filter((a) => a && a.url).map((a) => ({ url: a.url, type: a.mediaType || '' })));
+        for (const s of subs) tlStmts().ins.run(o.id, s.slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.url || null, o.published || null, media);
+        console.log('[AP] timeline +', actorUri, 'x' + subs.length);
+      }
     }
     return 202;
@@ -420,7 +432,19 @@
   }
   if (type === 'Delete') {
-    // A remote reply was deleted upstream → drop it if we stored it.
+    // A remote note was deleted upstream → drop it from replies AND the timeline.
     const oid = typeof act.object === 'string' ? act.object : (act.object && act.object.id);
-    if (oid) iStmts().delReply.run(oid);
+    if (oid) { iStmts().delReply.run(oid); try { tlStmts().del.run(oid); } catch { /* ignore */ } }
+    return 202;
+  }
+  // Accept/Reject of a Follow WE sent (client side).
+  if (type === 'Accept' && act.object) {
+    const fid = typeof act.object === 'string' ? act.object : (act.object && act.object.id);
+    if (fid) { try { fwStmts().acc.run(fid); } catch { /* ignore */ } }
+    console.log('[AP] follow accepted', actorUri);
+    return 202;
+  }
+  if (type === 'Reject' && act.object) {
+    const who = actorUri;
+    if (who && slugParam) { try { fwStmts().del.run(slugParam, who); } catch { /* ignore */ } }
     return 202;
   }
@@ -616,4 +640,78 @@
 }
 
+// ── Fediverse CLIENT: follow accounts + home timeline ─────────────
+// Resolve an @user@domain handle to its actor URL via WebFinger.
+export async function webfingerResolve(handle) {
+  const h = String(handle || '').trim().replace(/^@/, '');
+  const parts = h.split('@');
+  if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
+  const acct = `${parts[0]}@${parts[1]}`;
+  try {
+    const r = await fetch(`https://${parts[1]}/.well-known/webfinger?resource=acct:${encodeURIComponent(acct)}`,
+      { headers: { Accept: 'application/jrd+json, application/json' }, redirect: 'follow', signal: AbortSignal.timeout(8000) });
+    if (!r.ok) return null;
+    const jrd = await r.json();
+    const link = (jrd.links || []).find((l) => l.rel === 'self' && /activity\+json|ld\+json/.test(l.type || ''));
+    return link ? link.href : null;
+  } catch { return null; }
+}
+
+let _insFw, _delFw, _listFw, _accFw, _oneFw;
+function fwStmts() {
+  if (!_insFw) {
+    _insFw = db.prepare('INSERT OR REPLACE INTO ap_following (slug, actor_uri, handle, name, icon, url, inbox, follow_id, status, created_at) VALUES (?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)');
+    _delFw = db.prepare('DELETE FROM ap_following WHERE slug = ? AND actor_uri = ?');
+    _listFw = db.prepare('SELECT * FROM ap_following WHERE slug = ? ORDER BY created_at DESC');
+    _accFw = db.prepare("UPDATE ap_following SET status = 'accepted' WHERE follow_id = ?");
+    _oneFw = db.prepare('SELECT * FROM ap_following WHERE slug = ? AND actor_uri = ?');
+  }
+  return { ins: _insFw, del: _delFw, list: _listFw, acc: _accFw, one: _oneFw };
+}
+export function listFollowing(slug) { return fwStmts().list.all(slug); }
+
+let _insTl, _listTl, _delTl;
+function tlStmts() {
+  if (!_insTl) {
+    _insTl = db.prepare('INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, author_handle, author_icon, author_url, content, url, published, media_json, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)');
+    _listTl = db.prepare('SELECT * FROM ap_timeline WHERE slug = ? ORDER BY COALESCE(published, created_at) DESC LIMIT ?');
+    _delTl = db.prepare('DELETE FROM ap_timeline WHERE id = ?');
+  }
+  return { ins: _insTl, list: _listTl, del: _delTl };
+}
+export function getTimeline(slug, limit) { return tlStmts().list.all(slug, limit || 50); }
+
+// Follow a fediverse account by @handle (WebFinger → actor → signed Follow).
+export async function followActor(site, handle) {
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  if (!base || !site || !site.slug) return { error: 'config' };
+  const actorUrl = await webfingerResolve(handle);
+  if (!actorUrl) return { error: 'not_found' };
+  const actor = await fetchActor(actorUrl).catch(() => null);
+  if (!actor || !actor.id || !actor.inbox) return { error: 'unreachable' };
+  const ai = actorInfo(actor, actor.id);
+  const me = actorId(base, site.slug);
+  const keys = getOrCreateKeys(site.slug);
+  const followId = `${me}#follow-${Date.now()}`;
+  fwStmts().ins.run(site.slug, actor.id, ai.handle, ai.name, ai.icon, ai.url, actor.inbox, followId, 'pending');
+  const follow = { '@context': 'https://www.w3.org/ns/activitystreams', id: followId, type: 'Follow', actor: me, object: actor.id };
+  try { await deliver(actor.inbox, follow, `${me}#main-key`, keys.private_pem); }
+  catch (e) { console.warn('[AP] follow deliver failed:', e.message); }
+  console.log('[AP] follow', site.slug, '→', actor.id);
+  return { ok: true, name: ai.name, handle: ai.handle };
+}
+
+export async function unfollowActor(site, actorUri) {
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  const me = actorId(base, site.slug);
+  const keys = getOrCreateKeys(site.slug);
+  const row = fwStmts().one.get(site.slug, actorUri);
+  if (row && row.inbox) {
+    const undo = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${me}#unfollow-${Date.now()}`, type: 'Undo', actor: me, object: { id: row.follow_id || `${me}#follow`, type: 'Follow', actor: me, object: actorUri } };
+    try { await deliver(row.inbox, undo, `${me}#main-key`, keys.private_pem); } catch { /* best-effort */ }
+  }
+  fwStmts().del.run(site.slug, actorUri);
+  return { ok: true };
+}
+
 export default {
   getOrCreateKeys, apWants, sendAP, actorId, noteId,
@@ -622,3 +720,4 @@
   getInteractions, getInteractionById, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
   listOutbox, deliverOutboxDelete,
+  webfingerResolve, followActor, unfollowActor, listFollowing, getTimeline,
 };
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision 0dba0925244c76ab297b7130179cfd7339182667)
+++ src/services/i18n.js	(revision 914eb9fede1ff8c7deceec392018ef477d88dc47)
@@ -109,5 +109,5 @@
     'fedi.heading': 'Vanuit de fediverse', 'fedi.likes': 'sterren', 'fedi.boosts': 'boosts', 'fedi.replies': 'Reacties uit de fediverse',
     'fedi.reply': 'Reageer', 'fedi.reply_ph': 'Je antwoord aan de fediverse…', 'fedi.send': 'Versturen', 'fedi.you': 'Jij',
-    'fedi.remote_title': 'Reageer via de fediverse', 'fedi.remote_reply': 'Reageer via de fediverse', 'fedi.remote_prompt': 'Je fediverse-adres (bv. @jij@mastodon.social):', 'fedi.remote_notfound': 'Kon die post niet ophalen. Plak de volledige post-URL:', 'fedi.remote_load': 'Ophalen', 'fedi.remote_replying_to': 'Je reageert op', 'fedi.remote_as': 'Wordt verzonden als {site}.', 'fedi.remote_view_original': 'Bekijk de hele post + reacties op de bron →', 'fedi.remote_reply_short': 'via de fediverse', 'fedi.remote_ph': '@naam@server.social', 'fedi.remote_sent_title': 'Verzonden ✅', 'fedi.remote_sent': 'Je reactie is onderweg naar de fediverse en verschijnt zo bij de ontvanger.', 'fedi.remote_back': '← Terug naar je site', 'fedi.delete_confirm': 'Deze reactie verwijderen?', 'fedi.manage_title': 'Mijn fediverse-reacties', 'fedi.manage_empty': 'Je hebt nog geen reacties verstuurd.',
+    'fedi.remote_title': 'Reageer via de fediverse', 'fedi.remote_reply': 'Reageer via de fediverse', 'fedi.remote_prompt': 'Je fediverse-adres (bv. @jij@mastodon.social):', 'fedi.remote_notfound': 'Kon die post niet ophalen. Plak de volledige post-URL:', 'fedi.remote_load': 'Ophalen', 'fedi.remote_replying_to': 'Je reageert op', 'fedi.remote_as': 'Wordt verzonden als {site}.', 'fedi.remote_view_original': 'Bekijk de hele post + reacties op de bron →', 'fedi.remote_reply_short': 'via de fediverse', 'fedi.remote_ph': '@naam@server.social', 'fedi.remote_sent_title': 'Verzonden ✅', 'fedi.remote_sent': 'Je reactie is onderweg naar de fediverse en verschijnt zo bij de ontvanger.', 'fedi.remote_back': '← Terug naar je site', 'fedi.delete_confirm': 'Deze reactie verwijderen?', 'fedi.manage_title': 'Mijn fediverse-reacties', 'fedi.manage_empty': 'Je hebt nog geen reacties verstuurd.', 'tl.title': 'Tijdlijn', 'tl.lead': 'Volg accounts in de fediverse en zie hun berichten hier.', 'tl.follow_btn': 'Volgen', 'tl.following': 'Je volgt', 'tl.unfollow': 'Ontvolgen', 'tl.pending': 'in afwachting', 'tl.feed': 'Berichten', 'tl.empty': 'Nog niks — volg iemand om hun berichten hier te zien.', 'tl.view_original': 'Bekijk origineel →',
     'comments.to_start': 'om de conversatie te starten.',
     'comments.reply': 'Reageer', 'comments.delete': 'Verwijder', 'comments.cancel': 'Annuleren',
@@ -1105,5 +1105,5 @@
     'fedi.heading': 'From the fediverse', 'fedi.likes': 'favourites', 'fedi.boosts': 'boosts', 'fedi.replies': 'Replies from the fediverse',
     'fedi.reply': 'Reply', 'fedi.reply_ph': 'Your reply to the fediverse…', 'fedi.send': 'Send', 'fedi.you': 'You',
-    'fedi.remote_title': 'Reply via the fediverse', 'fedi.remote_reply': 'Reply via the fediverse', 'fedi.remote_prompt': 'Your fediverse address (e.g. @you@mastodon.social):', 'fedi.remote_notfound': 'Could not fetch that post. Paste the full post URL:', 'fedi.remote_load': 'Fetch', 'fedi.remote_replying_to': 'Replying to', 'fedi.remote_as': 'Sent as {site}.', 'fedi.remote_view_original': 'View the full post + comments on the source →', 'fedi.remote_reply_short': 'via the fediverse', 'fedi.remote_ph': '@name@server.social', 'fedi.remote_sent_title': 'Sent ✅', 'fedi.remote_sent': 'Your reply is on its way to the fediverse and will appear for the recipient shortly.', 'fedi.remote_back': '← Back to your site', 'fedi.delete_confirm': 'Delete this reply?', 'fedi.manage_title': 'My fediverse replies', 'fedi.manage_empty': 'You have not sent any replies yet.',
+    'fedi.remote_title': 'Reply via the fediverse', 'fedi.remote_reply': 'Reply via the fediverse', 'fedi.remote_prompt': 'Your fediverse address (e.g. @you@mastodon.social):', 'fedi.remote_notfound': 'Could not fetch that post. Paste the full post URL:', 'fedi.remote_load': 'Fetch', 'fedi.remote_replying_to': 'Replying to', 'fedi.remote_as': 'Sent as {site}.', 'fedi.remote_view_original': 'View the full post + comments on the source →', 'fedi.remote_reply_short': 'via the fediverse', 'fedi.remote_ph': '@name@server.social', 'fedi.remote_sent_title': 'Sent ✅', 'fedi.remote_sent': 'Your reply is on its way to the fediverse and will appear for the recipient shortly.', 'fedi.remote_back': '← Back to your site', 'fedi.delete_confirm': 'Delete this reply?', 'fedi.manage_title': 'My fediverse replies', 'fedi.manage_empty': 'You have not sent any replies yet.', 'tl.title': 'Timeline', 'tl.lead': 'Follow accounts on the fediverse and see their posts here.', 'tl.follow_btn': 'Follow', 'tl.following': 'Following', 'tl.unfollow': 'Unfollow', 'tl.pending': 'pending', 'tl.feed': 'Posts', 'tl.empty': 'Nothing yet — follow someone to see their posts here.', 'tl.view_original': 'View original →',
     'comments.to_start': 'to start the conversation.',
     'comments.reply': 'Reply', 'comments.delete': 'Delete', 'comments.cancel': 'Cancel',
@@ -2099,5 +2099,5 @@
     'fedi.heading': 'Aus dem Fediverse', 'fedi.likes': 'Favoriten', 'fedi.boosts': 'Boosts', 'fedi.replies': 'Antworten aus dem Fediverse',
     'fedi.reply': 'Antworten', 'fedi.reply_ph': 'Deine Antwort an das Fediverse…', 'fedi.send': 'Senden', 'fedi.you': 'Du',
-    'fedi.remote_title': 'Über das Fediverse antworten', 'fedi.remote_reply': 'Über das Fediverse antworten', 'fedi.remote_prompt': 'Deine Fediverse-Adresse (z.B. @du@mastodon.social):', 'fedi.remote_notfound': 'Beitrag konnte nicht geladen werden. Füge die vollständige Beitrags-URL ein:', 'fedi.remote_load': 'Laden', 'fedi.remote_replying_to': 'Antwort an', 'fedi.remote_as': 'Wird als {site} gesendet.', 'fedi.remote_view_original': 'Ganzen Beitrag + Kommentare an der Quelle ansehen →', 'fedi.remote_reply_short': 'übers Fediverse', 'fedi.remote_ph': '@name@server.social', 'fedi.remote_sent_title': 'Gesendet ✅', 'fedi.remote_sent': 'Deine Antwort ist auf dem Weg ins Fediverse und erscheint gleich beim Empfänger.', 'fedi.remote_back': '← Zurück zu deiner Seite', 'fedi.delete_confirm': 'Diese Antwort löschen?', 'fedi.manage_title': 'Meine Fediverse-Antworten', 'fedi.manage_empty': 'Du hast noch keine Antworten gesendet.',
+    'fedi.remote_title': 'Über das Fediverse antworten', 'fedi.remote_reply': 'Über das Fediverse antworten', 'fedi.remote_prompt': 'Deine Fediverse-Adresse (z.B. @du@mastodon.social):', 'fedi.remote_notfound': 'Beitrag konnte nicht geladen werden. Füge die vollständige Beitrags-URL ein:', 'fedi.remote_load': 'Laden', 'fedi.remote_replying_to': 'Antwort an', 'fedi.remote_as': 'Wird als {site} gesendet.', 'fedi.remote_view_original': 'Ganzen Beitrag + Kommentare an der Quelle ansehen →', 'fedi.remote_reply_short': 'übers Fediverse', 'fedi.remote_ph': '@name@server.social', 'fedi.remote_sent_title': 'Gesendet ✅', 'fedi.remote_sent': 'Deine Antwort ist auf dem Weg ins Fediverse und erscheint gleich beim Empfänger.', 'fedi.remote_back': '← Zurück zu deiner Seite', 'fedi.delete_confirm': 'Diese Antwort löschen?', 'fedi.manage_title': 'Meine Fediverse-Antworten', 'fedi.manage_empty': 'Du hast noch keine Antworten gesendet.', 'tl.title': 'Timeline', 'tl.lead': 'Folge Konten im Fediverse und sieh ihre Beiträge hier.', 'tl.follow_btn': 'Folgen', 'tl.following': 'Du folgst', 'tl.unfollow': 'Entfolgen', 'tl.pending': 'ausstehend', 'tl.feed': 'Beiträge', 'tl.empty': 'Noch nichts — folge jemandem, um Beiträge hier zu sehen.', 'tl.view_original': 'Original ansehen →',
     'comments.to_start': 'um das Gespräch zu starten.',
     'comments.reply': 'Antworten', 'comments.delete': 'Löschen', 'comments.cancel': 'Abbrechen',
Index: src/views/pages/admin.ejs
===================================================================
--- src/views/pages/admin.ejs	(revision 0dba0925244c76ab297b7130179cfd7339182667)
+++ src/views/pages/admin.ejs	(revision 914eb9fede1ff8c7deceec392018ef477d88dc47)
@@ -57,4 +57,5 @@
       <a href="/admin/shows" class="btn"><%= t('admin.b_agenda') %></a>
     <% } %>
+    <a href="/tijdlijn" class="btn">🌐 <%= t('tl.title') %></a>
     <a href="/admin/updates" class="btn"><%= t('admin.b_updates') %></a>
     <a href="/admin/handleiding" class="btn"><%= t('admin.b_help') %></a>
Index: src/views/pages/timeline.ejs
===================================================================
--- src/views/pages/timeline.ejs	(revision 914eb9fede1ff8c7deceec392018ef477d88dc47)
+++ src/views/pages/timeline.ejs	(revision 914eb9fede1ff8c7deceec392018ef477d88dc47)
@@ -0,0 +1,84 @@
+<div class="tl-wrap">
+  <h1 class="tl-title"><%= t('tl.title') %></h1>
+  <p class="tl-lead"><%= t('tl.lead') %></p>
+  <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
+  <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
+
+  <form method="post" action="/tijdlijn/follow" class="tl-follow">
+    <input type="text" name="handle" placeholder="@naam@server.social" autocomplete="off" spellcheck="false" required>
+    <button type="submit" class="btn btn-primary"><%= t('tl.follow_btn') %></button>
+  </form>
+
+  <% if (following && following.length) { %>
+    <h2 class="tl-sub"><%= t('tl.following') %> (<%= following.length %>)</h2>
+    <ul class="tl-foll-list">
+      <% following.forEach(function(f){ %>
+        <li class="tl-foll">
+          <span class="tl-foll-av"><% if (f.icon) { %><img src="<%= f.icon %>" alt=""><% } else { %><%= (f.name || '?').charAt(0).toUpperCase() %><% } %></span>
+          <span class="tl-foll-meta">
+            <a href="<%= f.url || f.actor_uri %>" target="_blank" rel="nofollow noopener"><%= f.name || f.handle %></a>
+            <span class="tl-foll-handle"><%= f.handle %></span>
+            <% if (f.status === 'pending') { %><span class="tl-pending"><%= t('tl.pending') %></span><% } %>
+          </span>
+          <form method="post" action="/tijdlijn/unfollow"><input type="hidden" name="actor_uri" value="<%= f.actor_uri %>"><button type="submit" class="tl-unfollow"><%= t('tl.unfollow') %></button></form>
+        </li>
+      <% }); %>
+    </ul>
+  <% } %>
+
+  <h2 class="tl-sub"><%= t('tl.feed') %></h2>
+  <% if (!timeline || !timeline.length) { %>
+    <p class="tl-empty"><%= t('tl.empty') %></p>
+  <% } else { %>
+    <ol class="tl-feed">
+      <% timeline.forEach(function(p){ %>
+        <li class="tl-item">
+          <span class="tl-avatar"><% if (p.author_icon) { %><img src="<%= p.author_icon %>" alt="" loading="lazy"><% } else { %><%= (p.author_name || '?').charAt(0).toUpperCase() %><% } %></span>
+          <div class="tl-itembody">
+            <div class="tl-meta">
+              <a class="tl-author" href="<%= p.author_url || p.author_uri %>" target="_blank" rel="nofollow noopener"><%= p.author_name %></a>
+              <span class="tl-handle"><%= p.author_handle %></span>
+              <% if (p.published || p.created_at) { %><span class="tl-time"><%= formatDateTime(p.published || p.created_at) %></span><% } %>
+            </div>
+            <div class="tl-content"><%- p.content %></div>
+            <% var media = []; try { media = JSON.parse(p.media_json || '[]'); } catch (e) { media = []; } %>
+            <% media.filter(function(m){ return !m.type || /^image\//.test(m.type); }).forEach(function(m){ %><img class="tl-media" src="<%= m.url %>" alt="" loading="lazy"><% }); %>
+            <% if (p.url) { %><p class="tl-orig"><a href="<%= p.url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a></p><% } %>
+          </div>
+        </li>
+      <% }); %>
+    </ol>
+  <% } %>
+</div>
+
+<style>
+  .tl-wrap { max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
+  .tl-title { margin: 0 0 .25rem; }
+  .tl-lead { color: var(--ink-soft, #888); margin: 0 0 1.25rem; }
+  .tl-sub { font-size: 1.1rem; margin: 1.75rem 0 .75rem; }
+  .tl-follow { display: flex; gap: .5rem; margin: 0 0 1rem; flex-wrap: wrap; }
+  .tl-follow input { flex: 1; min-width: 200px; height: 2.4rem; padding: 0 .9rem; border-radius: 999px; font: inherit;
+    border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, #000); }
+  .tl-foll-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .4rem; }
+  .tl-foll { display: flex; align-items: center; gap: .6rem; padding: .45rem .6rem; border-radius: 12px; background: color-mix(in srgb, var(--ink, #000) 4%, transparent); }
+  .tl-foll-av, .tl-avatar { flex: 0 0 36px; width: 36px; height: 36px; border-radius: 50%; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; font-weight: 700; background: color-mix(in srgb, var(--accent, #888) 20%, transparent); color: var(--accent, #555); }
+  .tl-foll-av img, .tl-avatar img { width: 100%; height: 100%; object-fit: cover; }
+  .tl-foll-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
+  .tl-foll-handle, .tl-handle { color: var(--ink-soft, #888); font-size: .82rem; }
+  .tl-pending { font-size: .72rem; color: var(--ink-soft, #999); }
+  .tl-unfollow { background: none; border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); border-radius: 8px; padding: .25rem .6rem; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; }
+  .tl-unfollow:hover { color: var(--ink, #000); }
+  .tl-empty { color: var(--ink-soft, #888); }
+  .tl-feed { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .9rem; }
+  .tl-item { display: flex; gap: .75rem; padding: .9rem 1rem; border-radius: 14px; border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent); background: color-mix(in srgb, var(--ink, #000) 3%, transparent); }
+  .tl-itembody { flex: 1; min-width: 0; }
+  .tl-meta { display: flex; align-items: baseline; gap: .4rem; flex-wrap: wrap; }
+  .tl-author { font-weight: 600; color: var(--ink, inherit); text-decoration: none; }
+  .tl-author:hover { text-decoration: underline; }
+  .tl-time { color: var(--ink-soft, #999); font-size: .8rem; margin-left: auto; }
+  .tl-content { margin: .3rem 0 0; line-height: 1.5; overflow-wrap: anywhere; }
+  .tl-content p { margin: .25rem 0; } .tl-content p:first-child { margin-top: 0; }
+  .tl-media { max-width: 100%; height: auto; border-radius: 10px; margin: .5rem 0 0; display: block; }
+  .tl-orig { margin: .5rem 0 0; font-size: .82rem; }
+  .tl-orig a { color: var(--accent, #06c); }
+</style>
