Index: src/views/pages/authorize-interaction.ejs
===================================================================
--- src/views/pages/authorize-interaction.ejs	(revision 2d66d66df4fdd568ae1d3ba78850058c55572590)
+++ src/views/pages/authorize-interaction.ejs	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
@@ -189,12 +189,14 @@
     <p class="auth-interact-or"><%= t('fedi.or_reply') %></p>
     <p class="auth-interact-where">💬 <%= t('fedi.reply_where') %></p>
-    <form method="post" action="/authorize_interaction" class="auth-interact-form">
-      <input type="hidden" name="uri" value="<%= uri %>">
-      <textarea name="text" rows="4" required placeholder="<%= t('fedi.reply_ph') %>"></textarea>
-      <div class="auth-interact-actions">
-        <button type="submit" class="btn"><%= t('fedi.send') %></button>
-        <a href="<%= uri %>" class="btn"><%= t('comments.cancel') %></a>
-      </div>
-    </form>
+    <div class="auth-interact-form">
+      <%- include('../partials/reply-editor', {
+        action: '/authorize_interaction',
+        hiddenFields: [{ name: 'uri', value: uri }],
+        placeholder: t('fedi.reply_ph'),
+        submitLabel: t('fedi.send'),
+        rows: 4,
+      }) %>
+      <div class="auth-interact-actions"><a href="<%= uri %>" class="btn"><%= t('comments.cancel') %></a></div>
+    </div>
     <p class="auth-interact-note"><%= t('fedi.remote_as', { site: siteTitle }) %></p>
     <details class="auth-report">
Index: src/views/partials/fedi-node.ejs
===================================================================
--- src/views/partials/fedi-node.ejs	(revision 2d66d66df4fdd568ae1d3ba78850058c55572590)
+++ src/views/partials/fedi-node.ejs	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
@@ -31,11 +31,11 @@
       <details class="fedi-owner-reply">
         <summary class="fedi-cact fedi-cact-reply" title="<%= t('fedi.remote_reply_short') %>" aria-label="<%= t('fedi.remote_reply_short') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg></summary>
-        <form method="post" action="<%= _base %>/posts/<%= postSlug %>/fedi-reply" class="comment-reply-form">
-          <input type="hidden" name="interaction_id" value="<%= n.id %>">
-          <textarea name="text" rows="2" required placeholder="<%= t('fedi.reply_ph') %>"></textarea>
-          <div class="comment-reply-form-actions">
-            <button type="submit" class="btn"><%= t('fedi.send') %></button>
-          </div>
-        </form>
+        <%- include('reply-editor', {
+          action: _base + '/posts/' + postSlug + '/fedi-reply',
+          hiddenFields: [{ name: 'interaction_id', value: n.id }],
+          placeholder: t('fedi.reply_ph'),
+          submitLabel: t('fedi.send'),
+          rows: 2,
+        }) %>
       </details>
       <%# Owner moderation: report to their instance + remove (tombstoned: never comes back via re-delivery or thread-crawl). %>
Index: src/views/partials/reply-editor.ejs
===================================================================
--- src/views/partials/reply-editor.ejs	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ src/views/partials/reply-editor.ejs	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
@@ -0,0 +1,54 @@
+<%#
+  Rich reply editor (shared component) — progressive enhancement.
+
+  Renders a plain <textarea name="text"> form that works without JS; the shared
+  /assets/js/reply-editor.js upgrades it to a rich contenteditable with a small
+  toolbar, a language select and (on ≤700px) a full-screen compose overlay,
+  which is the right pattern on mobile. On submit the JS fills the hidden
+  `content` field with the editor HTML and `text` with a plain-text fallback;
+  the server prefers `content` and sanitizes it (fedi-reply / authorize_interaction).
+
+  Locals:
+    action        POST target (required)
+    hiddenFields  [{ name, value }] extra hidden inputs (optional)
+    placeholder   textarea/editor placeholder (required)
+    submitLabel   submit button label (required)
+    rows          textarea rows for the no-JS fallback (default 3)
+    defaultLang   preselected reply language (default: UI lang)
+%>
+<% var _reLang = (typeof defaultLang !== 'undefined' && defaultLang) || (typeof lang !== 'undefined' ? lang : 'en'); %>
+<form method="post" action="<%= action %>" class="re-form" data-re>
+  <% (typeof hiddenFields !== 'undefined' ? hiddenFields : []).forEach(function (h) { %>
+    <input type="hidden" name="<%= h.name %>" value="<%= h.value %>">
+  <% }); %>
+  <input type="hidden" name="content" value="">
+  <%# Full-screen top bar (mobile compose). Hidden until JS + fullscreen. %>
+  <div class="re-head" hidden>
+    <button type="button" class="re-cancel" aria-label="<%= t('comments.cancel') %>">×</button>
+    <strong class="re-head-title"><%= t('re.title') %></strong>
+    <button type="submit" class="btn re-send-top"><%= submitLabel %></button>
+  </div>
+  <div class="re-toolbar" role="toolbar" aria-label="<%= t('re.title') %>" hidden>
+    <button type="button" data-cmd="bold" title="<%= t('re.bold') %>" aria-label="<%= t('re.bold') %>"><b>B</b></button>
+    <button type="button" data-cmd="italic" title="<%= t('re.italic') %>" aria-label="<%= t('re.italic') %>"><i>I</i></button>
+    <button type="button" data-cmd="link" title="<%= t('re.link') %>" aria-label="<%= t('re.link') %>">🔗</button>
+    <button type="button" data-cmd="list" title="<%= t('re.list') %>" aria-label="<%= t('re.list') %>">•≡</button>
+    <button type="button" data-cmd="quote" title="<%= t('re.quote') %>" aria-label="<%= t('re.quote') %>">❝</button>
+  </div>
+  <textarea name="text" rows="<%= typeof rows !== 'undefined' ? rows : 3 %>" required placeholder="<%= placeholder %>"></textarea>
+  <div class="re-editor" contenteditable="true" role="textbox" aria-multiline="true"
+       data-ph="<%= placeholder %>" aria-label="<%= placeholder %>" hidden></div>
+  <div class="re-foot">
+    <select name="language" class="re-lang" aria-label="<%= t('re.lang') %>" hidden>
+      <% [['en','English'],['nl','Nederlands'],['de','Deutsch'],['fr','Français'],['es','Español'],['it','Italiano'],['pt','Português'],['pl','Polski'],['ru','Русский'],['uk','Українська'],['sv','Svenska'],['da','Dansk'],['nb','Norsk'],['fi','Suomi'],['tr','Türkçe'],['ja','日本語'],['zh','中文'],['ar','العربية']].forEach(function (l) { %>
+        <option value="<%= l[0] %>" <%= l[0] === _reLang ? 'selected' : '' %>><%= l[1] %></option>
+      <% }); %>
+    </select>
+    <button type="submit" class="btn re-send"><%= submitLabel %></button>
+  </div>
+</form>
+<%# Assets once per render, even when this partial repeats per comment. %>
+<% if (!locals.__reAssets) { locals.__reAssets = 1; %>
+<link rel="stylesheet" href="/assets/css/reply-editor.css">
+<script src="/assets/js/reply-editor.js" defer></script>
+<% } %>
