Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision c45c1873a0dd747921d35135cbd7286e98789835)
+++ src/services/ActivityPubService.js	(revision 7b0703526cef42176d0c46db5f65acae0b4064cf)
@@ -352,4 +352,10 @@
   for (const a of openAudio) attachment.push(a); // fedi_open tracks → native Audio players
 
+  // Inline @user@host mentions: the Mention tag objects + the mentioned actor URIs. Only
+  // present when the content was already mention-linked (deliverCreate/Update resolve them
+  // at send time); a plain buildNote (outbox/notes) yields none.
+  const _mentionTags = mentionTags(body);
+  const _mentionCc = _mentionTags.map((t) => t.href);
+
   const note = {
     id,
@@ -362,6 +368,8 @@
     // but not addressed to Public, so Mastodon shows it only to them and can't boost it).
     to: post.fan_only ? [`${aId}/followers`] : [PUBLIC],
-    cc: post.fan_only ? [] : [`${aId}/followers`],
-    tag: buildHashtagList(base, post.tags, body),
+    // Mentioned actors (from inline @user@host links the caller resolved) are addressed in cc
+    // so Mastodon notifies them; empty unless the content was mention-linked (delivery time).
+    cc: [...new Set([...(post.fan_only ? [] : [`${aId}/followers`]), ..._mentionCc])],
+    tag: [...buildHashtagList(base, post.tags, body), ..._mentionTags],
     replies: `${id}/replies`,
     // NSFW → Mastodon-style content warning: sensitive (blurs media) + a summary/spoiler
@@ -1138,10 +1146,14 @@
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
   if (!base || !site || !site.slug) return;
+  // Resolve inline @user@host mentions → link them in the note + collect their inboxes, so a
+  // mentioned person is notified even if they don't follow us (Mastodon-standard mention).
+  const mres = await resolveMentionsInText(base, post.content || '');
+  const post2 = mres.inboxes.length ? { ...post, content: mres.html } : post;
   const followers = fStmts().list.all(site.slug);
-  if (!followers.length) return;
-  const inboxes = [...new Set(followers.map((f) => f.shared_inbox || f.inbox).filter(Boolean))];
+  const inboxes = [...new Set([...followers.map((f) => f.shared_inbox || f.inbox), ...mres.inboxes].filter(Boolean))];
+  if (!inboxes.length) return; // no followers and no one mentioned
   const keys = getOrCreateKeys(site.slug);
   const keyId = `${actorId(base, site.slug)}#main-key`;
-  const create = buildCreate(base, site, post);
+  const create = buildCreate(base, site, post2);
   for (const inbox of inboxes) deliverWithRetry(site.slug, inbox, create, keyId, keys.private_pem);
 }
@@ -1195,15 +1207,17 @@
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
   if (!base || !site || !site.slug || !post || !post.id) return;
+  const mres = await resolveMentionsInText(base, post.content || ''); // link mentions + collect inboxes
+  const post2 = mres.inboxes.length ? { ...post, content: mres.html } : post;
   const followers = fStmts().list.all(site.slug);
-  if (!followers.length) return;
-  const inboxes = [...new Set(followers.map((f) => f.shared_inbox || f.inbox).filter(Boolean))];
+  const inboxes = [...new Set([...followers.map((f) => f.shared_inbox || f.inbox), ...mres.inboxes].filter(Boolean))];
+  if (!inboxes.length) return;
   const keys = getOrCreateKeys(site.slug);
   const me = actorId(base, site.slug);
-  const note = buildNote(base, site, post);
+  const note = buildNote(base, site, post2);
   note.updated = new Date().toISOString();
   const update = {
     '@context': AP_CONTEXT,
     id: `${noteId(base, post.id)}#update-${Date.now()}-${rid()}`,
-    type: 'Update', actor: me, to: [PUBLIC], cc: [`${me}/followers`],
+    type: 'Update', actor: me, to: [PUBLIC], cc: note.cc,
     object: note,
   };
