Index: CHANGELOG.de.md
===================================================================
--- CHANGELOG.de.md	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
+++ CHANGELOG.de.md	(revision 5190152c90d2d397b7f87f9c900eae8d8c1e2856)
@@ -7,4 +7,9 @@
 
 ### Hinzugefügt
+- **Das Bearbeiten einer gesendeten Antwort nutzt jetzt ebenfalls den
+  Rich-Editor.** Die Bearbeiten-Formulare auf Nachrichten und der
+  Interaktionsseite öffnen denselben Editor wie neue Antworten (Formatierung
+  bleibt, Sprache anpassbar, Vollbild auf Telefonen). Anhänge der Antwort
+  überstehen eine Bearbeitung unangetastet.
 - **Medien in Antworten (reichhaltige Antworten, Phase 2).** Ziehe, füge ein
   oder wähle Bilder, Audio und Video direkt im Antwort-Editor (die Büroklammer
Index: CHANGELOG.md
===================================================================
--- CHANGELOG.md	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
+++ CHANGELOG.md	(revision 5190152c90d2d397b7f87f9c900eae8d8c1e2856)
@@ -7,4 +7,8 @@
 
 ### Added
+- **Editing a sent reply uses the rich editor too.** The edit forms on Messages
+  and the interact page open the same editor as new replies (formatting kept,
+  language adjustable, full-screen on phones). Attachments on the reply survive
+  an edit untouched.
 - **Media in replies (rich replies, phase 2).** Drop, paste or pick images,
   audio and video straight into the reply editor (the paperclip works on
Index: CHANGELOG.nl.md
===================================================================
--- CHANGELOG.nl.md	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
+++ CHANGELOG.nl.md	(revision 5190152c90d2d397b7f87f9c900eae8d8c1e2856)
@@ -7,4 +7,8 @@
 
 ### Toegevoegd
+- **Een verzonden reactie bewerken gebruikt nu ook de rijke editor.** De
+  bewerk-formulieren op Berichten en de interactiepagina openen dezelfde editor
+  als nieuwe reacties (opmaak blijft, taal aanpasbaar, volledig scherm op
+  telefoons). Bijlagen op de reactie overleven een bewerking onaangeroerd.
 - **Media in reacties (rijke reacties, fase 2).** Sleep, plak of kies
   afbeeldingen, audio en video direct in de reactie-editor (de paperclip werkt
Index: src/assets/js/reply-editor.js
===================================================================
--- src/assets/js/reply-editor.js	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
+++ src/assets/js/reply-editor.js	(revision 5190152c90d2d397b7f87f9c900eae8d8c1e2856)
@@ -36,7 +36,9 @@
     var attField = form.querySelector('input[name="attachments"]');
     var fileInput = form.querySelector('.re-file');
+    var canAttach = !!(attWrap && attField && fileInput);   // edit mode renders without media
     var attachments = [];
 
     function syncAtt() {
+      if (!canAttach) return;
       attField.value = attachments.length ? JSON.stringify(attachments) : '';
       attWrap.hidden = attachments.length === 0;
@@ -62,4 +64,5 @@
     }
     function uploadFiles(files) {
+      if (!canAttach) return;
       Array.prototype.forEach.call(files, function (file) {
         if (!/^(image|audio|video)\//.test(file.type) || attachments.length >= 4) return;
@@ -93,5 +96,5 @@
       e.preventDefault();
       var cmd = btn.getAttribute('data-cmd');
-      if (cmd === 'attach') { fileInput.click(); return; }   // no editor focus: keeps the picker usable on mobile
+      if (cmd === 'attach') { if (fileInput) fileInput.click(); return; }   // no editor focus: keeps the picker usable on mobile
       ed.focus();
       if (cmd === 'bold') document.execCommand('bold');
@@ -104,5 +107,5 @@
       }
     });
-    fileInput.addEventListener('change', function () {
+    if (fileInput) fileInput.addEventListener('change', function () {
       uploadFiles(fileInput.files);
       fileInput.value = '';
@@ -114,6 +117,6 @@
       var cd = e.clipboardData || window.clipboardData;
       if (cd.files && cd.files.length) {
-        e.preventDefault();
-        uploadFiles(cd.files);
+        e.preventDefault();            // never let the browser inline-paste a file as base64
+        uploadFiles(cd.files);         // no-op without the attach UI (edit mode)
         return;
       }
@@ -125,4 +128,5 @@
 
     // Drag/drop media onto the editor.
+    if (canAttach) {
     ed.addEventListener('dragover', function (e) {
       if (e.dataTransfer && Array.prototype.some.call(e.dataTransfer.types || [], function (t) { return t === 'Files'; })) {
@@ -139,4 +143,5 @@
       }
     });
+    }
 
     // Full-screen compose on mobile: enter on focus, leave via ×.
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
+++ src/routes/posts.js	(revision 5190152c90d2d397b7f87f9c900eae8d8c1e2856)
@@ -827,7 +827,12 @@
 router.post('/fediverse/:id/edit', requireSiteManager, async (req, res) => {
   const site = res.locals.site;
-  if (site && String(req.body.text || '').trim()) {
-    try { await ActivityPubService.deliverOutboxUpdate(site, req.params.id, req.body.text); }
-    catch (e) { console.warn('[AP] outbox edit failed:', e.message); }
+  const text = String(req.body.text || '');
+  const html = String(req.body.content || '');   // rich reply editor HTML (sanitized in deliverOutboxUpdate)
+  if (site && (text.trim() || html.trim())) {
+    try {
+      await ActivityPubService.deliverOutboxUpdate(site, req.params.id, text, {
+        html, language: String(req.body.language || ''),
+      });
+    } catch (e) { console.warn('[AP] outbox edit failed:', e.message); }
   }
   res.redirect(req.get('Referer') || `${res.locals.siteUrlBase || ''}/fediverse`);
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
+++ src/services/ActivityPubService.js	(revision 5190152c90d2d397b7f87f9c900eae8d8c1e2856)
@@ -513,5 +513,5 @@
       items.push({
         type: 'sent', outboxId: m.id, to_handle: m.to_handle, in_reply_to: m.in_reply_to,
-        content: m.content, editable: m.editable, created_at: m.created_at,
+        content: m.content, editable: m.editable, language: m.language, created_at: m.created_at,
       });
     }
@@ -2049,5 +2049,5 @@
 }
 export function listOutbox(siteSlug) {
-  return db.prepare('SELECT id, content, to_handle, in_reply_to, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')
+  return db.prepare('SELECT id, content, to_handle, in_reply_to, language, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')
     .all(siteSlug).map((r) => { const c = stripLeadingMentions(r.content); return { ...r, content: c, editable: outboxEditableText(c) }; });
 }
@@ -2077,9 +2077,12 @@
 // Edit one of our outbound replies: rewrite the stored content (mention re-added + #tags
 // re-linked) and send an Update(Note) so recipients refresh their cached copy.
-export async function deliverOutboxUpdate(site, outboxId, newText) {
+export async function deliverOutboxUpdate(site, outboxId, newText, opts = {}) {
   const row = iStmts().getO.get(outboxId);
   if (!row || row.site_slug !== site.slug) return false;
   const text = String(newText || '').trim();
-  if (!text) return false;
+  // Rich edit: same sanitize + enrichment pipeline as deliverReply.
+  const richClean = opts.html ? HtmlSanitizerService.sanitize(String(opts.html)) : '';
+  const rich = richClean && HtmlSanitizerService.toPlainText(richClean).trim() ? richClean : '';
+  if (!text && !rich) return false;
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
   if (!base) return false;
@@ -2091,7 +2094,19 @@
   const mention = row.to_actor
     ? `<a href="${escHtml(toProfile)}" class="u-url mention" data-actor="${escHtml(row.to_actor)}">${escHtml(toHandle)}</a> ` : '';
-  const mres = await resolveMentionsInText(base, escHtml(text).replace(/\r?\n/g, '<br>'));
-  const content = `<p>${mention}${linkUrls(linkHashtags(base, mres.html))}</p>`;
-  db.prepare('UPDATE ap_outbox SET content = ? WHERE id = ?').run(content, outboxId);
+  let content;
+  let mres;
+  if (rich) {
+    mres = await resolveMentionsInText(base, rich);
+    const processed = linkUrls(linkHashtags(base, mres.html));
+    if (processed.startsWith('<p>')) content = processed.replace('<p>', `<p>${mention}`);
+    else if (/^<(blockquote|ul|ol|pre|h[1-6]|div|hr)\b/i.test(processed)) content = `<p>${mention}</p>${processed}`;
+    else content = `<p>${mention}${processed}</p>`;
+  } else {
+    mres = await resolveMentionsInText(base, escHtml(text).replace(/\r?\n/g, '<br>'));
+    content = `<p>${mention}${linkUrls(linkHashtags(base, mres.html))}</p>`;
+  }
+  // Language may be updated with the edit; attachments always survive untouched.
+  const newLang = /^[a-z]{2,3}(-[A-Za-z0-9-]+)?$/.test(String(opts.language || '')) ? opts.language : null;
+  db.prepare('UPDATE ap_outbox SET content = ?, language = COALESCE(?, language) WHERE id = ?').run(content, newLang, outboxId);
   const note = buildReplyNote(base, site, iStmts().getO.get(outboxId));
   note.updated = new Date().toISOString();
Index: src/views/pages/authorize-interaction.ejs
===================================================================
--- src/views/pages/authorize-interaction.ejs	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
+++ src/views/pages/authorize-interaction.ejs	(revision 5190152c90d2d397b7f87f9c900eae8d8c1e2856)
@@ -41,8 +41,16 @@
               </form>
             </div>
-            <form method="post" action="/fediverse/<%= m.id %>/edit" class="fedi-edit-form">
-              <textarea name="text" rows="3" maxlength="2000" class="fedi-edit-ta"><%= m.editable %></textarea>
-              <div class="fedi-edit-actions"><button type="submit" class="fedi-edit-save"><%= t('fedi.save_edit') %></button></div>
-            </form>
+            <div class="fedi-edit-form">
+              <%- include('../partials/reply-editor', {
+                action: '/fediverse/' + m.id + '/edit',
+                placeholder: t('fedi.reply_ph'),
+                submitLabel: t('fedi.save_edit'),
+                rows: 3,
+                initialHtml: m.content,
+                initialText: m.editable,
+                defaultLang: m.language,
+                noAttach: true,
+              }) %>
+            </div>
           </li>
         <% }); %>
@@ -59,5 +67,5 @@
           b.classList.toggle('is-open', open);
           b.setAttribute('aria-expanded', open ? 'true' : 'false');
-          if (open) { var ta = form.querySelector('textarea'); if (ta) ta.focus(); }
+          if (open) { var ed = form.querySelector('.re-editor') || form.querySelector('textarea'); if (ed) ed.focus(); }
         });
       })();
Index: src/views/pages/messages.ejs
===================================================================
--- src/views/pages/messages.ejs	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
+++ src/views/pages/messages.ejs	(revision 5190152c90d2d397b7f87f9c900eae8d8c1e2856)
@@ -78,8 +78,16 @@
                 <details class="msg-edit">
                   <summary><%= t('fedi.edit') %></summary>
-                  <form method="post" action="/fediverse/<%= n.outboxId %>/edit" class="msg-edit-form">
-                    <textarea name="text" rows="2" required><%= n.editable %></textarea>
-                    <button type="submit" class="btn"><%= t('fedi.save_edit') %></button>
-                  </form>
+                  <div class="msg-edit-form">
+                    <%- include('../partials/reply-editor', {
+                      action: '/fediverse/' + n.outboxId + '/edit',
+                      placeholder: t('fedi.reply_ph'),
+                      submitLabel: t('fedi.save_edit'),
+                      rows: 2,
+                      initialHtml: n.content,
+                      initialText: n.editable,
+                      defaultLang: n.language,
+                      noAttach: true,
+                    }) %>
+                  </div>
                 </details>
                 <form method="post" action="/fediverse/<%= n.outboxId %>/delete" data-confirm="<%= t('fedi.delete_confirm') %>">
Index: src/views/partials/reply-editor.ejs
===================================================================
--- src/views/partials/reply-editor.ejs	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
+++ src/views/partials/reply-editor.ejs	(revision 5190152c90d2d397b7f87f9c900eae8d8c1e2856)
@@ -16,5 +16,11 @@
     rows          textarea rows for the no-JS fallback (default 3)
     defaultLang   preselected reply language (default: UI lang)
+    initialHtml   prefill for EDIT mode (our own sanitized HTML) (optional)
+    initialText   plain-text prefill for the no-JS textarea (optional)
+    noAttach      true hides the media button (edits do not touch attachments)
 %>
+<% var _reInitHtml = (typeof initialHtml !== 'undefined' && initialHtml) || ''; %>
+<% var _reInitText = (typeof initialText !== 'undefined' && initialText) || ''; %>
+<% var _reNoAttach = (typeof noAttach !== 'undefined' && noAttach); %>
 <% var _reLang = (typeof defaultLang !== 'undefined' && defaultLang) || (typeof lang !== 'undefined' ? lang : 'en'); %>
 <form method="post" action="<%= action %>" class="re-form" data-re
@@ -36,12 +42,16 @@
     <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>
-    <button type="button" data-cmd="attach" title="<%= t('re.attach') %>" aria-label="<%= t('re.attach') %>">📎</button>
-    <input type="file" class="re-file" accept="image/*,audio/*,video/*" multiple hidden>
+    <% if (!_reNoAttach) { %>
+      <button type="button" data-cmd="attach" title="<%= t('re.attach') %>" aria-label="<%= t('re.attach') %>">📎</button>
+      <input type="file" class="re-file" accept="image/*,audio/*,video/*" multiple hidden>
+    <% } %>
   </div>
-  <input type="hidden" name="attachments" value="">
-  <div class="re-attachments" hidden></div>
-  <textarea name="text" rows="<%= typeof rows !== 'undefined' ? rows : 3 %>" required placeholder="<%= placeholder %>"></textarea>
+  <% if (!_reNoAttach) { %>
+    <input type="hidden" name="attachments" value="">
+    <div class="re-attachments" hidden></div>
+  <% } %>
+  <textarea name="text" rows="<%= typeof rows !== 'undefined' ? rows : 3 %>" required placeholder="<%= placeholder %>"><%= _reInitText %></textarea>
   <div class="re-editor" contenteditable="true" role="textbox" aria-multiline="true"
-       data-ph="<%= placeholder %>" aria-label="<%= placeholder %>" hidden></div>
+       data-ph="<%= placeholder %>" aria-label="<%= placeholder %>" hidden><%- _reInitHtml %></div>
   <div class="re-foot">
     <select name="language" class="re-lang" aria-label="<%= t('re.lang') %>" hidden>
Index: test/rich-reply.test.js
===================================================================
--- test/rich-reply.test.js	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
+++ test/rich-reply.test.js	(revision 5190152c90d2d397b7f87f9c900eae8d8c1e2856)
@@ -120,2 +120,26 @@
   assert.equal(r, null);
 });
+
+test('rich edit: content replaced, language updated, attachments survive', async () => {
+  const r = await AP.deliverReply(site, {
+    postId: 'p1', postSlug: 'hallo', parent, text: 'origineel', html: '',
+    attachments: [{ url: '/media/reply-media/keep.webp', mediaType: 'image/webp', name: 'blijft' }],
+    language: 'nl',
+  });
+  const upd = await AP.deliverOutboxUpdate(site, r.id, '', { html: '<p>bewerkt met <em>nadruk</em></p>', language: 'en' });
+  assert.ok(upd && upd.ok);
+  const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get(r.id);
+  assert.match(row.content, /bewerkt met <em>nadruk<\/em>/);
+  assert.match(row.content, /^<p><a [^>]*class="u-url mention"/); // mention re-attached inline
+  assert.equal(row.language, 'en');
+  assert.equal(JSON.parse(row.attachments)[0].url, '/media/reply-media/keep.webp');
+});
+
+test('plain edit path unchanged; bogus language keeps the old one', async () => {
+  const r = await AP.deliverReply(site, { postId: 'p1', postSlug: 'hallo', parent, text: 'plain start', language: 'nl' });
+  const upd = await AP.deliverOutboxUpdate(site, r.id, 'plain bewerkt', { language: '???' });
+  assert.ok(upd && upd.ok);
+  const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get(r.id);
+  assert.match(row.content, /plain bewerkt/);
+  assert.equal(row.language, 'nl');
+});
