Index: src/assets/css/style.css
===================================================================
--- src/assets/css/style.css	(revision 47a0d299cfdbd087a1c792a46e9dbe273e3de27f)
+++ src/assets/css/style.css	(revision 55bc7f95d6ee2d94bdaba55d6ca87265e217dbc4)
@@ -4434,2 +4434,19 @@
 .post-fediverse .fedi-text p:first-child { margin-top: 0; }
 .post-fediverse .fedi-text a { color: var(--accent, #06c); }
+.post-fediverse .fedi-ours {
+  margin: .6rem 0 0; padding: .55rem .75rem; border-radius: 12px;
+  border-left: 3px solid var(--accent, #888);
+  background: color-mix(in srgb, var(--accent, #888) 8%, transparent);
+  font-size: .95rem;
+}
+.post-fediverse .fedi-ours-label { font-weight: 600; color: var(--accent, #555); margin-right: .25rem; }
+.post-fediverse .fedi-ours-body p { margin: 0; display: inline; }
+.post-fediverse .fedi-replybox { margin-top: .55rem; }
+.post-fediverse .fedi-replybox summary { cursor: pointer; font-size: .85rem; color: var(--ink-soft, #888); width: fit-content; }
+.post-fediverse .fedi-replybox form { display: flex; flex-direction: column; gap: .5rem; margin-top: .5rem; }
+.post-fediverse .fedi-replybox textarea {
+  width: 100%; box-sizing: border-box; resize: vertical; padding: .55rem .7rem;
+  border-radius: 10px; border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent);
+  background: var(--paper, #fff); color: var(--ink, #000); font: inherit;
+}
+.post-fediverse .fedi-replybox button { align-self: flex-end; }
Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 47a0d299cfdbd087a1c792a46e9dbe273e3de27f)
+++ src/config/database.js	(revision 55bc7f95d6ee2d94bdaba55d6ca87265e217dbc4)
@@ -332,4 +332,16 @@
     );
     CREATE INDEX IF NOT EXISTS idx_ap_inter_post ON ap_interactions(post_id, kind);
+    CREATE TABLE IF NOT EXISTS ap_outbox (
+      id TEXT PRIMARY KEY,            -- note path segment (uuid) → /ap/notes/<id>
+      site_slug TEXT NOT NULL,
+      post_id TEXT NOT NULL,
+      post_slug TEXT,
+      in_reply_to TEXT,               -- remote status uri we reply to
+      to_actor TEXT,                  -- remote actor uri (mentioned)
+      to_handle TEXT,
+      content TEXT NOT NULL,          -- sanitized HTML of our reply
+      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+    );
+    CREATE INDEX IF NOT EXISTS idx_ap_outbox_post ON ap_outbox(post_id);
   `);
 }
Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 47a0d299cfdbd087a1c792a46e9dbe273e3de27f)
+++ src/routes/activitypub.js	(revision 55bc7f95d6ee2d94bdaba55d6ca87265e217dbc4)
@@ -74,5 +74,10 @@
     "SELECT * FROM posts WHERE id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)"
   ).get(req.params.id);
-  if (!post) return res.status(404).end();
+  if (!post) {
+    // Could be one of OUR outbound replies (ap_outbox), not a post.
+    const note = AP.getOutboxNote(baseUrl(req), req.params.id);
+    if (note) return AP.sendAP(res, { '@context': 'https://www.w3.org/ns/activitystreams', ...note });
+    return res.status(404).end();
+  }
   const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(post.site_id);
   if (!site) return res.status(404).end();
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 47a0d299cfdbd087a1c792a46e9dbe273e3de27f)
+++ src/routes/posts.js	(revision 55bc7f95d6ee2d94bdaba55d6ca87265e217dbc4)
@@ -7,5 +7,5 @@
 import ejs from 'ejs';
 import db from '../config/database.js';
-import { requireAuth } from '../middleware/auth.js';
+import { requireAuth, requireSiteManager } from '../middleware/auth.js';
 import { renderPage } from '../middleware/render.js';
 import { recordPageview, recordPostView } from '../services/StatsService.js';
@@ -749,6 +749,8 @@
 
   // Inbound fediverse activity (replies/likes/boosts) for this post.
-  let fediverse = { replies: [], likeCount: 0, announceCount: 0, total: 0 };
+  let fediverse = { replies: [], outReplies: [], likeCount: 0, announceCount: 0, total: 0 };
   try { fediverse = ActivityPubService.getInteractions(post.id); } catch { /* non-fatal */ }
+  // Owner/admin of this site may reply back to a fediverse interaction.
+  const canManageSite = !!(req.session?.user && PermissionsService.canAdminSite(req.session.user, site));
 
   renderPage(req, res, 'pages/post', {
@@ -760,4 +762,5 @@
     totalComments,
     fediverse,
+    canManageSite,
     likeCount,
     likedByMe,
@@ -769,4 +772,20 @@
 });
 
+// ── Reply back to a fediverse interaction (site owner/admin only) ──
+router.post('/posts/:slug/fedi-reply', requireSiteManager, async (req, res) => {
+  const site = res.locals.site;
+  if (!site) return res.status(404).send('Site required');
+  const post = db.prepare('SELECT id, slug FROM posts WHERE site_id = ? AND slug = ?').get(site.id, req.params.slug);
+  if (!post) return res.status(404).send('Not found');
+  const parent = ActivityPubService.getInteractionById(req.body.interaction_id);
+  const text = (req.body.text || '').toString();
+  if (parent && parent.post_id === post.id && text.trim()) {
+    try {
+      await ActivityPubService.deliverReply(site, { postId: post.id, postSlug: post.slug, parent, text });
+    } catch (e) { console.warn('[AP] reply send failed:', e.message); }
+  }
+  res.redirect(`${res.locals.siteUrlBase || ''}/${post.slug}#fediverse`);
+});
+
 export default router;
 export { postNeighbors };
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 47a0d299cfdbd087a1c792a46e9dbe273e3de27f)
+++ src/services/ActivityPubService.js	(revision 55bc7f95d6ee2d94bdaba55d6ca87265e217dbc4)
@@ -191,6 +191,6 @@
 export function followerCount(slug) { return fStmts().cnt.get(slug).n; }
 
-// ── inbound interactions store (replies / likes / boosts), lazy stmts ──
-let _insI, _delLA, _delReply, _listI;
+// ── inbound interactions store (replies / likes / boosts) + our outbound replies ──
+let _insI, _delLA, _delReply, _listI, _getI, _insO, _listO, _getO;
 function iStmts() {
   if (!_insI) {
@@ -198,8 +198,14 @@
     _delLA = db.prepare('DELETE FROM ap_interactions WHERE kind = ? AND post_id = ? AND actor_uri = ?');
     _delReply = db.prepare("DELETE FROM ap_interactions WHERE kind = 'reply' AND object_uri = ?");
-    _listI = db.prepare('SELECT kind, actor_uri, actor_name, actor_handle, actor_url, actor_icon, content, published, created_at FROM ap_interactions WHERE post_id = ? ORDER BY created_at ASC');
-  }
-  return { ins: _insI, delLA: _delLA, delReply: _delReply, list: _listI };
-}
+    _listI = db.prepare('SELECT id, kind, object_uri, actor_uri, actor_name, actor_handle, actor_url, actor_icon, content, published, created_at FROM ap_interactions WHERE post_id = ? ORDER BY created_at ASC');
+    _getI = db.prepare('SELECT * FROM ap_interactions WHERE id = ?');
+    _insO = db.prepare('INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, created_at) VALUES (?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)');
+    _listO = db.prepare('SELECT * FROM ap_outbox WHERE post_id = ? ORDER BY created_at ASC');
+    _getO = db.prepare('SELECT * FROM ap_outbox WHERE id = ?');
+  }
+  return { ins: _insI, delLA: _delLA, delReply: _delReply, list: _listI, getI: _getI, insO: _insO, listO: _listO, getO: _getO };
+}
+
+export function getInteractionById(id) { return iStmts().getI.get(id); }
 
 const localPostExists = (id) => { try { return !!db.prepare('SELECT 1 FROM posts WHERE id = ?').get(id); } catch { return false; } };
@@ -226,12 +232,18 @@
 }
 
-// Stored, view-ready summary of a post's inbound fediverse activity.
+// Stored, view-ready summary of a post's inbound fediverse activity + our replies.
 export function getInteractions(postId) {
-  const rows = iStmts().list.all(postId);
+  const s = iStmts();
+  const rows = s.list.all(postId);
+  const outReplies = s.listO.all(postId).map((o) => ({
+    id: o.id, content: o.content, in_reply_to: o.in_reply_to, to_handle: o.to_handle,
+    created_at: o.created_at, mine: true,
+  }));
   return {
     replies: rows.filter((r) => r.kind === 'reply'),
+    outReplies,
     likeCount: rows.filter((r) => r.kind === 'like').length,
     announceCount: rows.filter((r) => r.kind === 'announce').length,
-    total: rows.length,
+    total: rows.length + outReplies.length,
   };
 }
@@ -399,8 +411,74 @@
 }
 
+// ── outbound replies (Klonkt → fediverse) ─────────────────────────
+const escHtml = (s) => String(s || '').replace(/[<>&]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c]));
+const toISO = (v) => { if (!v) return new Date().toISOString(); const s = String(v); const d = new Date(/[TZ]/.test(s) ? s : s.replace(' ', 'T') + 'Z'); return isNaN(d) ? new Date().toISOString() : d.toISOString(); };
+
+// Build one of OUR outbound reply Notes from an ap_outbox row.
+export function buildReplyNote(base, site, row) {
+  const me = actorId(base, site.slug);
+  return {
+    id: noteId(base, row.id),
+    type: 'Note',
+    attributedTo: me,
+    inReplyTo: row.in_reply_to || undefined,
+    content: row.content,
+    url: row.post_slug ? `${base}/${encodeURIComponent(row.post_slug)}` : undefined,
+    published: toISO(row.created_at),
+    to: row.to_actor ? [row.to_actor] : [PUBLIC],
+    cc: [PUBLIC, `${me}/followers`],
+    tag: row.to_actor ? [{ type: 'Mention', href: row.to_actor, name: row.to_handle }] : [],
+  };
+}
+
+// Resolve one of our outbound reply Notes by id (for /ap/notes/:id fallback).
+export function getOutboxNote(base, id) {
+  const row = iStmts().getO.get(id);
+  if (!row) return null;
+  const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(row.site_slug);
+  if (!site) return null;
+  return buildReplyNote(base, site, row);
+}
+
+// Send a reply FROM this site to a remote actor (in reply to their inbound reply).
+// `parent` = an ap_interactions row (actor_uri, actor_url, actor_handle, object_uri).
+export async function deliverReply(site, { postId, postSlug, parent, text }) {
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  if (!base || !site || !site.slug || !parent || !String(text || '').trim()) return null;
+  const me = actorId(base, site.slug);
+  const handle = parent.actor_handle || deriveHandle(parent.actor_uri);
+  const body = escHtml(String(text).trim()).replace(/\r?\n/g, '<br>');
+  const mention = parent.actor_uri
+    ? `<a href="${escHtml(parent.actor_url || parent.actor_uri)}" class="u-url mention">${escHtml(handle)}</a> ` : '';
+  const content = `<p>${mention}${body}</p>`;
+  const id = crypto.randomUUID();
+  iStmts().insO.run(id, site.slug, postId, postSlug || null, parent.object_uri || null, parent.actor_uri || null, handle, content);
+  const row = iStmts().getO.get(id);
+  const note = buildReplyNote(base, site, row);
+  const create = {
+    '@context': 'https://www.w3.org/ns/activitystreams',
+    id: note.id + '#create', type: 'Create', actor: me,
+    published: note.published, to: note.to, cc: note.cc, object: note,
+  };
+  const keys = getOrCreateKeys(site.slug);
+  const keyId = `${me}#main-key`;
+  const inboxes = new Set();
+  if (parent.actor_uri) {
+    const a = await fetchActor(parent.actor_uri).catch(() => null);
+    if (a) inboxes.add((a.endpoints && a.endpoints.sharedInbox) || a.inbox);
+  }
+  for (const f of fStmts().list.all(site.slug)) inboxes.add(f.shared_inbox || f.inbox);
+  let delivered = 0;
+  for (const inbox of [...inboxes].filter(Boolean)) {
+    try { const st = await deliver(inbox, create, keyId, keys.private_pem); if (st >= 200 && st < 300) delivered++; } catch { /* best-effort */ }
+  }
+  console.log('[AP] outreply', site.slug, '→', parent.actor_uri, 'delivered', delivered);
+  return { id, content, delivered };
+}
+
 export default {
   getOrCreateKeys, apWants, sendAP, actorId, noteId,
   buildActor, buildNote, buildCreate, buildOutbox, buildFollowers,
   followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete,
-  getInteractions,
+  getInteractions, getInteractionById, buildReplyNote, getOutboxNote, deliverReply,
 };
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision 47a0d299cfdbd087a1c792a46e9dbe273e3de27f)
+++ src/services/i18n.js	(revision 55bc7f95d6ee2d94bdaba55d6ca87265e217dbc4)
@@ -108,4 +108,5 @@
     'comments.empty': 'Nog geen reacties.',
     '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:',
     'comments.to_start': 'om de conversatie te starten.',
     'comments.reply': 'Reageer', 'comments.delete': 'Verwijder', 'comments.cancel': 'Annuleren',
@@ -1101,4 +1102,5 @@
     'comments.empty': 'No comments yet.',
     '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:',
     'comments.to_start': 'to start the conversation.',
     'comments.reply': 'Reply', 'comments.delete': 'Delete', 'comments.cancel': 'Cancel',
@@ -2092,4 +2094,5 @@
     'comments.empty': 'Noch keine Kommentare.',
     '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:',
     'comments.to_start': 'um das Gespräch zu starten.',
     'comments.reply': 'Antworten', 'comments.delete': 'Löschen', 'comments.cancel': 'Abbrechen',
Index: src/views/pages/post.ejs
===================================================================
--- src/views/pages/post.ejs	(revision 47a0d299cfdbd087a1c792a46e9dbe273e3de27f)
+++ src/views/pages/post.ejs	(revision 55bc7f95d6ee2d94bdaba55d6ca87265e217dbc4)
@@ -96,4 +96,18 @@
                 </div>
                 <div class="fedi-text"><%- r.content %></div>
+                <% var mine = (typeof fediverse.outReplies !== 'undefined' ? fediverse.outReplies : []).filter(function(o){ return o.in_reply_to && o.in_reply_to === r.object_uri; }); %>
+                <% mine.forEach(function(o){ %>
+                  <div class="fedi-ours"><span class="fedi-ours-label"><%= t('fedi.you') %></span> <span class="fedi-ours-body"><%- o.content %></span></div>
+                <% }); %>
+                <% if (typeof canManageSite !== 'undefined' && canManageSite) { %>
+                  <details class="fedi-replybox">
+                    <summary><%= t('fedi.reply') %></summary>
+                    <form method="post" action="<%= _base %>/posts/<%= post.slug %>/fedi-reply">
+                      <input type="hidden" name="interaction_id" value="<%= r.id %>">
+                      <textarea name="text" rows="2" required placeholder="<%= t('fedi.reply_ph') %>"></textarea>
+                      <button type="submit" class="btn btn-primary"><%= t('fedi.send') %></button>
+                    </form>
+                  </details>
+                <% } %>
               </div>
             </li>
Index: src/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision 47a0d299cfdbd087a1c792a46e9dbe273e3de27f)
+++ src/views/shell.ejs	(revision 55bc7f95d6ee2d94bdaba55d6ca87265e217dbc4)
@@ -175,5 +175,5 @@
 
 <!-- v9 stylesheet (full palette system) -->
-<link rel="stylesheet" href="/assets/css/style.css?v=36">
+<link rel="stylesheet" href="/assets/css/style.css?v=37">
 
 <!-- Audio player styles: loaded on every page so the mini-player works
