Index: CHANGELOG.de.md
===================================================================
--- CHANGELOG.de.md	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ CHANGELOG.de.md	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -7,4 +7,10 @@
 
 ### Hinzugefügt
+- **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
+  funktioniert auf Telefonen). Dateien laden auf deine eigene Seite hoch,
+  erscheinen beim Schreiben als entfernbare Chips, reisen als echte Anhänge der
+  föderierten Note mit und erscheinen in deinem Thread. Eine Antwort nur mit
+  Medien (ohne Text) geht auch.
 - **Reichhaltige Antworten (Phase 1).** Das Antworten auf Fediverse-Kommentare
   (inline im Thread und auf der Interaktionsseite) nutzt jetzt einen geteilten
Index: CHANGELOG.md
===================================================================
--- CHANGELOG.md	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ CHANGELOG.md	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -7,4 +7,9 @@
 
 ### Added
+- **Media in replies (rich replies, phase 2).** Drop, paste or pick images,
+  audio and video straight into the reply editor (the paperclip works on
+  phones). Files upload to your own site, show as removable chips while you
+  write, travel as real attachments on the federated note, and render in your
+  thread. A media-only reply (no text) works too.
 - **Rich replies (phase 1).** Replying to fediverse comments (inline in the
   thread and on the interact page) now uses a shared rich editor: bold, italic,
Index: CHANGELOG.nl.md
===================================================================
--- CHANGELOG.nl.md	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ CHANGELOG.nl.md	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -7,4 +7,10 @@
 
 ### Toegevoegd
+- **Media in reacties (rijke reacties, fase 2).** Sleep, plak of kies
+  afbeeldingen, audio en video direct in de reactie-editor (de paperclip werkt
+  op telefoons). Bestanden uploaden naar je eigen site, verschijnen als
+  verwijderbare chips tijdens het schrijven, reizen als echte attachments mee op
+  de gefedereerde note, en renderen in je thread. Een reactie met alleen media
+  (zonder tekst) kan ook.
 - **Rijke reacties (fase 1).** Reageren op fediverse-reacties (inline in de
   thread en op de interactiepagina) gebruikt nu een gedeelde rijke editor: vet,
Index: src/assets/css/reply-editor.css
===================================================================
--- src/assets/css/reply-editor.css	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ src/assets/css/reply-editor.css	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -30,4 +30,23 @@
 }
 
+/* Media attachments (chips under the editor). */
+.re-attachments { display: flex; flex-wrap: wrap; gap: .4rem; }
+.re-att {
+  display: inline-flex; align-items: center; gap: .35rem;
+  padding: .25rem .4rem; border-radius: 8px; font-size: .8rem;
+  background: color-mix(in srgb, var(--ink, #000) 7%, transparent);
+  max-width: 100%; overflow: hidden;
+}
+.re-att img { width: 44px; height: 44px; object-fit: cover; border-radius: 6px; display: block; }
+.re-att-del { border: none; background: none; color: inherit; font-size: 1rem; line-height: 1; cursor: pointer; padding: 0 .15rem; }
+.re-att-err { background: color-mix(in srgb, #c00 18%, transparent); }
+.re-editor.re-drop { border-color: var(--accent, #06c); border-style: dashed; }
+
+/* Media on a sent reply in the thread (fedi-node). */
+.re-reply-media { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .4rem; }
+.re-reply-media img { max-width: min(260px, 100%); max-height: 260px; border-radius: 10px; display: block; }
+.re-reply-media audio { max-width: 100%; }
+.re-reply-media video { max-width: min(320px, 100%); border-radius: 10px; }
+
 /* Full-screen compose (mobile). JS toggles .re-full on the form. */
 .re-head { display: flex; align-items: center; gap: .6rem; }
Index: src/assets/js/reply-editor.js
===================================================================
--- src/assets/js/reply-editor.js	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ src/assets/js/reply-editor.js	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -32,4 +32,58 @@
     if (lang) lang.hidden = false;
 
+    // ── Media attachments: picker (📎), drag/drop and paste ──────────────
+    var attWrap = form.querySelector('.re-attachments');
+    var attField = form.querySelector('input[name="attachments"]');
+    var fileInput = form.querySelector('.re-file');
+    var attachments = [];
+
+    function syncAtt() {
+      attField.value = attachments.length ? JSON.stringify(attachments) : '';
+      attWrap.hidden = attachments.length === 0;
+    }
+    function addChip(a) {
+      var chip = document.createElement('span');
+      chip.className = 're-att';
+      if (a.mediaType.indexOf('image/') === 0) {
+        var img = document.createElement('img');
+        img.src = a.url; img.alt = a.name || '';
+        chip.appendChild(img);
+      } else {
+        chip.appendChild(document.createTextNode((a.mediaType.indexOf('audio/') === 0 ? '🎵 ' : '🎬 ') + (a.name || a.mediaType)));
+      }
+      var del = document.createElement('button');
+      del.type = 'button'; del.className = 're-att-del'; del.textContent = '×';
+      del.addEventListener('click', function () {
+        attachments = attachments.filter(function (x) { return x !== a; });
+        chip.remove(); syncAtt();
+      });
+      chip.appendChild(del);
+      attWrap.appendChild(chip);
+    }
+    function uploadFiles(files) {
+      Array.prototype.forEach.call(files, function (file) {
+        if (!/^(image|audio|video)\//.test(file.type) || attachments.length >= 4) return;
+        var chip = document.createElement('span');
+        chip.className = 're-att re-att-busy';
+        chip.textContent = '⏳ ' + file.name;
+        attWrap.hidden = false;
+        attWrap.appendChild(chip);
+        var fd = new FormData();
+        fd.append('media', file);
+        fetch(form.getAttribute('data-upload'), { method: 'POST', body: fd })
+          .then(function (r) { return r.json().then(function (j) { return r.ok ? j : Promise.reject(j); }); })
+          .then(function (j) {
+            chip.remove();
+            var a = { url: j.url, mediaType: j.mediaType, name: j.name || file.name };
+            attachments.push(a); addChip(a); syncAtt();
+          })
+          .catch(function (err) {
+            chip.className = 're-att re-att-err';
+            chip.textContent = (form.getAttribute('data-upload-err') || 'Upload failed') + (err && err.error ? ': ' + err.error : '');
+            setTimeout(function () { chip.remove(); syncAtt(); }, 5000);
+          });
+      });
+    }
+
     // Toolbar commands (execCommand is deprecated-but-universal; same approach
     // as the post editor).
@@ -38,6 +92,7 @@
       if (!btn) return;
       e.preventDefault();
+      var cmd = btn.getAttribute('data-cmd');
+      if (cmd === 'attach') { fileInput.click(); return; }   // no editor focus: keeps the picker usable on mobile
       ed.focus();
-      var cmd = btn.getAttribute('data-cmd');
       if (cmd === 'bold') document.execCommand('bold');
       else if (cmd === 'italic') document.execCommand('italic');
@@ -49,12 +104,38 @@
       }
     });
+    fileInput.addEventListener('change', function () {
+      uploadFiles(fileInput.files);
+      fileInput.value = '';
+    });
 
-    // Paste as plain text (rich paste becomes messy HTML; formatting is what
-    // the toolbar is for). Media paste/drop lands in the media phase.
+    // Paste: files become attachments; text pastes as plain text (rich paste
+    // becomes messy HTML; formatting is what the toolbar is for).
     ed.addEventListener('paste', function (e) {
-      var txt = (e.clipboardData || window.clipboardData).getData('text/plain');
+      var cd = e.clipboardData || window.clipboardData;
+      if (cd.files && cd.files.length) {
+        e.preventDefault();
+        uploadFiles(cd.files);
+        return;
+      }
+      var txt = cd.getData('text/plain');
       if (!txt) return;
       e.preventDefault();
       document.execCommand('insertText', false, txt);
+    });
+
+    // Drag/drop media onto the editor.
+    ed.addEventListener('dragover', function (e) {
+      if (e.dataTransfer && Array.prototype.some.call(e.dataTransfer.types || [], function (t) { return t === 'Files'; })) {
+        e.preventDefault();
+        ed.classList.add('re-drop');
+      }
+    });
+    ed.addEventListener('dragleave', function () { ed.classList.remove('re-drop'); });
+    ed.addEventListener('drop', function (e) {
+      ed.classList.remove('re-drop');
+      if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) {
+        e.preventDefault();
+        uploadFiles(e.dataTransfer.files);
+      }
     });
 
@@ -72,10 +153,10 @@
     if (cancel) cancel.addEventListener('click', function () { setFull(false); });
 
-    // Serialize on submit; block truly empty replies.
+    // Serialize on submit; block truly empty replies (media-only is fine).
     form.addEventListener('submit', function (e) {
       var html = ed.innerHTML.trim();
       var plain = (ed.innerText || '').replace(/ /g, ' ').trim();
-      if (!plain) { e.preventDefault(); ed.focus(); return; }
-      hidden.value = html;
+      if (!plain && !attachments.length) { e.preventDefault(); ed.focus(); return; }
+      hidden.value = plain ? html : '';
       ta.value = plain;
       document.documentElement.classList.remove('re-lock');
Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ src/config/database.js	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -438,4 +438,6 @@
   // Rich replies: the reply's language (BCP47 code) → contentMap on the outgoing Note.
   ensureColumn('ap_outbox', 'language', 'TEXT');
+  // Rich replies: JSON array [{url, mediaType, name}] → `attachment` on the Note.
+  ensureColumn('ap_outbox', 'attachments', 'TEXT');
 }
 
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ src/routes/posts.js	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -32,4 +32,30 @@
 const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
 
+// Rich replies: media dropped/pasted into the reply editor. Images, audio and
+// video, stored as-is (no transcode; a reply attachment is not a track).
+const REPLY_MEDIA_DIR = path.resolve(
+  process.env.REPLY_MEDIA_PATH ||
+  path.join(__dirname, '..', '..', 'storage', 'media', 'reply-media')
+);
+fs.mkdirSync(REPLY_MEDIA_DIR, { recursive: true });
+const ALLOWED_REPLY_MEDIA_EXT = new Set([
+  '.jpg', '.jpeg', '.png', '.webp', '.gif',
+  '.mp3', '.m4a', '.ogg', '.opus', '.flac', '.wav',
+  '.mp4', '.webm', '.mov',
+]);
+const MAX_REPLY_MEDIA_BYTES = 32 * 1024 * 1024;
+const replyMediaUpload = multer({
+  storage: multer.diskStorage({
+    destination: (req, file, cb) => cb(null, REPLY_MEDIA_DIR),
+    filename: (req, file, cb) => cb(null, `${uuid()}${path.extname(file.originalname).toLowerCase()}`),
+  }),
+  limits: { fileSize: MAX_REPLY_MEDIA_BYTES },
+  fileFilter: (req, file, cb) => {
+    const ext = path.extname(file.originalname).toLowerCase();
+    if (!ALLOWED_REPLY_MEDIA_EXT.has(ext)) return cb(new Error('Media must be an image, audio or video file'));
+    cb(null, true);
+  },
+});
+
 const imageStorage = multer.diskStorage({
   destination: (req, file, cb) => cb(null, POST_IMAGES_DIR),
@@ -90,4 +116,23 @@
     } catch { /* keep the still image */ }
     res.json({ url, video, size: req.file.size, mime: req.file.mimetype });
+  });
+});
+
+// Rich replies: media for a reply (image/audio/video). Returns { url, mediaType, name }
+// exactly as the editor's attachments JSON wants it; deliverReply re-validates.
+router.post('/posts/upload-reply-media', requireSiteManager, (req, res) => {
+  replyMediaUpload.single('media')(req, res, (err) => {
+    if (err) return res.status(400).json({ error: err.message });
+    if (!req.file) return res.status(400).json({ error: 'No file' });
+    const mime = String(req.file.mimetype || '');
+    if (!/^(image|audio|video)\//.test(mime)) {
+      try { fs.unlinkSync(req.file.path); } catch { /* best effort */ }
+      return res.status(400).json({ error: 'Media must be an image, audio or video file' });
+    }
+    res.json({
+      url: '/media/reply-media/' + req.file.filename,
+      mediaType: mime,
+      name: String(req.file.originalname || '').slice(0, 120),
+    });
   });
 });
@@ -714,8 +759,10 @@
   const html = (req.body.content || '').toString();      // rich reply editor HTML (sanitized in deliverReply)
   const language = (req.body.language || '').toString();
-  if (site && uri && (text.trim() || html.trim())) {
+  let attachments = [];
+  try { attachments = JSON.parse(req.body.attachments || '[]'); } catch { /* geen media */ }
+  if (site && uri && (text.trim() || html.trim() || (Array.isArray(attachments) && attachments.length))) {
     // Resolve + deliver in the background so Send responds instantly.
     ActivityPubService.resolveRemoteNote(uri)
-      .then((parent) => parent && ActivityPubService.deliverReply(site, { postId: parent.localPostId || '', postSlug: null, parent, text, html, language }))
+      .then((parent) => parent && ActivityPubService.deliverReply(site, { postId: parent.localPostId || '', postSlug: null, parent, text, html, language, attachments }))
       .catch((e) => console.warn('[AP] remote reply failed:', e.message));
   }
@@ -1260,8 +1307,10 @@
   const text = (req.body.text || '').toString();
   const html = (req.body.content || '').toString();      // rich reply editor HTML (sanitized in deliverReply)
-  if (parent && parent.post_id === post.id && (text.trim() || html.trim())) {
+  let attachments = [];
+  try { attachments = JSON.parse(req.body.attachments || '[]'); } catch { /* geen media */ }
+  if (parent && parent.post_id === post.id && (text.trim() || html.trim() || (Array.isArray(attachments) && attachments.length))) {
     try {
       await ActivityPubService.deliverReply(site, {
-        postId: post.id, postSlug: post.slug, parent, text, html,
+        postId: post.id, postSlug: post.slug, parent, text, html, attachments,
         language: (req.body.language || '').toString(),
       });
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ src/services/ActivityPubService.js	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -227,4 +227,18 @@
   if (opts.isReply) {
     const meR = actorId(base, site.slug);
+    // Rich replies: attachments column (JSON [{url, mediaType, name}]) → AS2
+    // attachment array with absolute URLs and the matching object type.
+    let replyAtt;
+    try {
+      const list = post.attachments ? JSON.parse(post.attachments) : [];
+      if (Array.isArray(list) && list.length) {
+        replyAtt = list.map((a) => ({
+          type: a.mediaType.startsWith('image/') ? 'Image' : a.mediaType.startsWith('audio/') ? 'Audio' : 'Video',
+          mediaType: a.mediaType,
+          url: /^https?:/i.test(a.url) ? a.url : `${base}${a.url}`,
+          name: a.name || undefined,
+        }));
+      }
+    } catch { /* malformed attachments never block the Note */ }
     return {
       id: noteId(base, post.id),
@@ -235,4 +249,5 @@
       // Reply language (rich replies): the AS2 language map next to `content`.
       contentMap: post.language ? { [post.language]: post.content } : undefined,
+      attachment: replyAtt,
       url: post.post_slug ? `${base}/${encodeURIComponent(post.post_slug)}` : undefined,
       published: toISO(post.created_at),
@@ -712,5 +727,5 @@
     _listI = db.prepare('SELECT id, kind, object_uri, parent_uri, actor_uri, actor_name, actor_handle, actor_url, actor_icon, content, published, created_at, acted_boost, acted_like, visibility 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, language, created_at) VALUES (?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)');
+    _insO = db.prepare('INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, language, attachments, 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 = ?');
@@ -817,4 +832,5 @@
       noteId: baseClean ? `${baseClean}/ap/notes/${o.id}` : o.id, parent: o.in_reply_to || null,
       mine: true, outboxId: o.id, content: stripLeadingMentions(o.content), created_at: o.created_at,
+      media: (() => { try { return o.attachments ? JSON.parse(o.attachments) : []; } catch { return []; } })(),
       actor_name: siteName, actor_handle: siteHandle, actor_url: siteUrl, actor_icon: siteIcon,
       children: [],
@@ -1856,5 +1872,5 @@
 // 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, html, language }) {
+export async function deliverReply(site, { postId, postSlug, parent, text, html, language, attachments }) {
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
   // Rich replies: `html` is the reply editor's HTML (sanitized here); `text` is
@@ -1862,5 +1878,13 @@
   const richClean = html ? HtmlSanitizerService.sanitize(String(html)) : '';
   const rich = richClean && HtmlSanitizerService.toPlainText(richClean).trim() ? richClean : '';
-  if (!base || !site || !site.slug || !parent || (!String(text || '').trim() && !rich)) return null;
+  // Attachments: only OUR OWN uploads (/media/... paths, no remote URLs — the
+  // upload route is the sole producer), image/audio/video only, max 4.
+  const media = (Array.isArray(attachments) ? attachments : [])
+    .filter((a) => a && typeof a.url === 'string' && /^\/media\/[\w./-]+$/.test(a.url)
+      && /^(image|audio|video)\//.test(String(a.mediaType || '')))
+    .slice(0, 4)
+    .map((a) => ({ url: a.url, mediaType: String(a.mediaType), name: String(a.name || '').slice(0, 120) }));
+  // A media-only reply (no text) is a valid reply.
+  if (!base || !site || !site.slug || !parent || (!String(text || '').trim() && !rich && !media.length)) return null;
   const me = actorId(base, site.slug);
   const handle = parent.actor_handle || deriveHandle(parent.actor_uri);
@@ -1890,9 +1914,11 @@
   const replyLang = /^[a-z]{2,3}(-[A-Za-z0-9-]+)?$/.test(String(language || '')) ? language : null;
   // Dedup: skip if the exact same reply was already sent (double-submit guard).
-  const dup = db.prepare('SELECT 1 FROM ap_outbox WHERE site_slug = ? AND IFNULL(in_reply_to, \'\') = ? AND content = ? LIMIT 1')
-    .get(site.slug, parent.object_uri || '', content);
+  // Attachments count toward "the same": two media-only replies share content.
+  const mediaJson = media.length ? JSON.stringify(media) : null;
+  const dup = db.prepare('SELECT 1 FROM ap_outbox WHERE site_slug = ? AND IFNULL(in_reply_to, \'\') = ? AND content = ? AND IFNULL(attachments, \'\') = IFNULL(?, \'\') LIMIT 1')
+    .get(site.slug, parent.object_uri || '', content, mediaJson);
   if (dup) { console.log('[AP] outreply skipped (duplicate)'); return { duplicate: true, delivered: 0 }; }
   const id = crypto.randomUUID();
-  iStmts().insO.run(id, site.slug, postId, postSlug || null, parent.object_uri || null, parent.actor_uri || null, handle, content, replyLang);
+  iStmts().insO.run(id, site.slug, postId, postSlug || null, parent.object_uri || null, parent.actor_uri || null, handle, content, replyLang, mediaJson);
   const row = iStmts().getO.get(id);
   const note = buildReplyNote(base, site, row);
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ src/services/i18n.js	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -113,5 +113,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.follow_heading': 'Volgen via de fediverse', 'fedi.profile_follow': 'Volg via de fediverse', 'profile.since': 'Op Klonkt sinds', 'profile.free': 'Gratis', 'fedi.follow_intro': 'Je staat op het punt te volgen:', 'fedi.follow_btn': 'Volgen', 'fedi.cancel': 'Annuleren', 'fedi.followed_title': 'Volgverzoek verstuurd ✅', 'fedi.followed_done': 'Je volgverzoek is onderweg. Zodra de andere kant het accepteert, verschijnen hun berichten in je tijdlijn.', 'fedi.view_profile': 'Bekijk profiel →', 'fedi.remote_reply': 'Reageer via de fediverse', 'fedi.remote_prompt': 'Je fediverse-adres:', '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.like_short': 'Like', 'fedi.unlike_short': 'Like intrekken', 'fedi.boost_short': 'Boost', 'fedi.remote_ph': 'jouw server', 'fedi.remote_sent_title': 'Verzonden ✅', 'fedi.remote_sent': 'Je reactie is verstuurd. Hij verschijnt zo bij de originele post op de fediverse, niet op deze pagina. Bekijk hem daar:', 'fedi.reply_where': 'Je reactie verschijnt bij de originele post op de fediverse, niet op deze pagina. Via de link hierboven zie je hem daar.', 'fedi.remote_back': '← Terug naar je site', 'fedi.like_btn': 'Like deze post', 'fedi.or_reply': 'of reageer:', 'fedi.liked_title': 'Geliket', 'fedi.liked_done': 'Je like is onderweg naar de fediverse.', 'fedi.boost_btn': 'Boost deze post', 'fedi.boosted_title': 'Geboost', 'fedi.boosted_done': 'Je boost is onderweg naar de fediverse.', 'fedi.remote_interact': 'Interageer via de fediverse', 'fedi.report_open': 'Deze post rapporteren', 'fedi.report_where': 'De melding gaat naar de instance en hun moderator(s).', 'fedi.report_ph': 'Wat is er mis? (optioneel)', 'fedi.report_send': 'Rapporteren', 'fedi.reported_title': 'Gerapporteerd', 'fedi.reported_done': 'Je melding is naar de server van de gebruiker gestuurd. Hun moderators bekijken het.', 'fedi.delete_confirm': 'Deze reactie verwijderen?', 'fedi.mod_remove_confirm': 'Deze reactie uit je thread verwijderen? Hij komt niet terug, ook niet via thread-aanvulling.', 'fedi.mod_report_confirm': 'Deze reactie rapporteren bij de server van de auteur?', 'fedi.manage_title': 'Mijn fediverse-reacties', 'fedi.manage_empty': 'Je hebt nog geen reacties verstuurd.', 'fedi.goto_post': 'Naar de post', 'fedi.edit': 'Bewerken', 'fedi.save_edit': 'Opslaan', 'fedi.bm_label': 'Interageer via mijn site', 'fedi.bm_help': 'Sleep deze knop naar je bladwijzerbalk. Klik ’m daarna op elke fediverse-post (Mastodon, een andere Klonkt…) om er via jouw site op te reageren, te liken of te boosten.', 'tl.title': 'Krant', 'tl.lead': 'Volg accounts in de fediverse en zie hun berichten hier.', 'tl.follow_btn': 'Volgen', 'tl.following': 'Je volgt', 'tl.unfollow': 'Ontvolgen', 'tl.autoboost': 'Uitgelicht', 'tl.autoboost_follow': 'uitlichten in cirkel', 'tl.autoboost_hint': 'Hun nieuwe posts verschijnen doorlopend in jouw Cirkel (lokaal, geen fediverse-boost).', 'tl.pending': 'in afwachting', 'tl.unboost': 'Boost intrekken', 'tl.feed': 'Berichten', 'tl.tab_feed': 'Krant', 'tl.tab_following': 'Volgend', 'tl.tab_replies': 'Reacties', 'tl.tab_followers': 'Volgers', 'tl.followers': 'Volgers', 'tl.followers_lead': 'Wie jou volgt in de fediverse, met de laatste geslaagde bezorging. Rood = nog nooit bezorgd of laatste poging mislukt — kandidaat om op te ruimen na een check.', 'tl.empty_followers': 'Nog geen volgers.', 'tl.last_delivery': 'Laatste bezorging', 'tl.never_delivered': 'Nog nooit bezorgd', 'tl.delivery_failed': 'laatste poging mislukt', 'tl.remove_follower': 'Verwijderen', 'tl.remove_confirm': 'Deze volger verwijderen? Een actief account moet je dan opnieuw volgen.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'jij volgt', 'tl.dir_follower': 'volgt jou', 'tl.dir_mutual': 'wederzijds', 'tl.connect_empty': 'Nog geen connecties. Volg iemand hierboven om te beginnen.', 'tl.unreachable': 'Niet bereikbaar', 'tl.unreachable_lead': 'Deze volgers konden we niet bereiken (nooit bezorgd of laatste poging mislukt). Ruim ze op na een handmatige check.', 'msg.tab': 'Berichten', 'msg.title': 'Berichten', 'msg.filter_all': 'Alles', 'msg.filter_conv': 'Gesprekken', 'msg.filter_act': 'Activiteit', 'msg.filter_sent': 'Verzonden', 'msg.you': 'Jij', 'msg.sent_reply': 'reageerde via de fediverse', 'msg.and_more': 'en {n} anderen', 'msg.liked_many': 'liketen je post', 'msg.boosted_many': 'boostten je post', 'msg.private': 'privé', 'msg.private_hint': 'Alleen aan jou gericht; staat niet op de publieke postpagina.', 'msg.new': 'Nieuw sinds je laatste bezoek', 'msg.empty': 'Nog geen berichten. Reacties, vermeldingen en activiteit verschijnen hier.', 'oauth.title': 'App toegang geven', 'oauth.wants_access': 'wil verbinding maken met je Klonkt-account.', 'oauth.post_as': 'Plaatsen als', 'oauth.scope_read': 'Je berichten, reacties en meldingen lezen', 'oauth.scope_write': 'Namens jou posten, reageren, liken en volgen', 'oauth.allow': 'Toestaan', 'oauth.deny': 'Weigeren', 'oauth.foot': 'Je kunt de toegang later intrekken. Geef alleen apps toegang die je vertrouwt.', 're.title': 'Reactie schrijven', 're.bold': 'Vet', 're.italic': 'Cursief', 're.link': 'Link invoegen', 're.list': 'Opsomming', 're.quote': 'Citaat', 're.lang': 'Taal van je reactie', 'tl.empty_following': 'Je volgt nog niemand.', 'tl.empty': 'Nog niks — volg iemand om hun berichten hier te zien.', 'tl.view_original': 'Bekijk origineel →', 'tl.open_player': 'Open de speler', 'tl.paste_ph': 'Plak een fediverse-post-URL', 'tl.paste_go': 'Openen', 'tl.boosted': 'boostte dit', 'tl.read_more': 'Meer lezen', 'tl.show_less': 'Minder', 'poll.vote': 'Stem', 'poll.votes': 'stemmen', 'poll.closed': 'gesloten', 'poll.open': 'open', 'poll.aria': 'Peiling', 'poll.voter_one': 'stemmer', 'poll.voter_many': 'stemmers', 'poll.closes': 'sluit op', 'poll.multiple': 'meerkeuze', 'poll.fedi_only': 'Stemmen kan vanuit de fediverse — volg deze site en stem in je eigen app.', 'poll.voted_title': 'Stem verstuurd', 'poll.voted_done': 'Je stem is verstuurd naar de poll. De uitslag werkt bij zodra de maker die doorstuurt.',
+    'fedi.remote_title': 'Reageer via de fediverse', 'fedi.follow_heading': 'Volgen via de fediverse', 'fedi.profile_follow': 'Volg via de fediverse', 'profile.since': 'Op Klonkt sinds', 'profile.free': 'Gratis', 'fedi.follow_intro': 'Je staat op het punt te volgen:', 'fedi.follow_btn': 'Volgen', 'fedi.cancel': 'Annuleren', 'fedi.followed_title': 'Volgverzoek verstuurd ✅', 'fedi.followed_done': 'Je volgverzoek is onderweg. Zodra de andere kant het accepteert, verschijnen hun berichten in je tijdlijn.', 'fedi.view_profile': 'Bekijk profiel →', 'fedi.remote_reply': 'Reageer via de fediverse', 'fedi.remote_prompt': 'Je fediverse-adres:', '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.like_short': 'Like', 'fedi.unlike_short': 'Like intrekken', 'fedi.boost_short': 'Boost', 'fedi.remote_ph': 'jouw server', 'fedi.remote_sent_title': 'Verzonden ✅', 'fedi.remote_sent': 'Je reactie is verstuurd. Hij verschijnt zo bij de originele post op de fediverse, niet op deze pagina. Bekijk hem daar:', 'fedi.reply_where': 'Je reactie verschijnt bij de originele post op de fediverse, niet op deze pagina. Via de link hierboven zie je hem daar.', 'fedi.remote_back': '← Terug naar je site', 'fedi.like_btn': 'Like deze post', 'fedi.or_reply': 'of reageer:', 'fedi.liked_title': 'Geliket', 'fedi.liked_done': 'Je like is onderweg naar de fediverse.', 'fedi.boost_btn': 'Boost deze post', 'fedi.boosted_title': 'Geboost', 'fedi.boosted_done': 'Je boost is onderweg naar de fediverse.', 'fedi.remote_interact': 'Interageer via de fediverse', 'fedi.report_open': 'Deze post rapporteren', 'fedi.report_where': 'De melding gaat naar de instance en hun moderator(s).', 'fedi.report_ph': 'Wat is er mis? (optioneel)', 'fedi.report_send': 'Rapporteren', 'fedi.reported_title': 'Gerapporteerd', 'fedi.reported_done': 'Je melding is naar de server van de gebruiker gestuurd. Hun moderators bekijken het.', 'fedi.delete_confirm': 'Deze reactie verwijderen?', 'fedi.mod_remove_confirm': 'Deze reactie uit je thread verwijderen? Hij komt niet terug, ook niet via thread-aanvulling.', 'fedi.mod_report_confirm': 'Deze reactie rapporteren bij de server van de auteur?', 'fedi.manage_title': 'Mijn fediverse-reacties', 'fedi.manage_empty': 'Je hebt nog geen reacties verstuurd.', 'fedi.goto_post': 'Naar de post', 'fedi.edit': 'Bewerken', 'fedi.save_edit': 'Opslaan', 'fedi.bm_label': 'Interageer via mijn site', 'fedi.bm_help': 'Sleep deze knop naar je bladwijzerbalk. Klik ’m daarna op elke fediverse-post (Mastodon, een andere Klonkt…) om er via jouw site op te reageren, te liken of te boosten.', 'tl.title': 'Krant', 'tl.lead': 'Volg accounts in de fediverse en zie hun berichten hier.', 'tl.follow_btn': 'Volgen', 'tl.following': 'Je volgt', 'tl.unfollow': 'Ontvolgen', 'tl.autoboost': 'Uitgelicht', 'tl.autoboost_follow': 'uitlichten in cirkel', 'tl.autoboost_hint': 'Hun nieuwe posts verschijnen doorlopend in jouw Cirkel (lokaal, geen fediverse-boost).', 'tl.pending': 'in afwachting', 'tl.unboost': 'Boost intrekken', 'tl.feed': 'Berichten', 'tl.tab_feed': 'Krant', 'tl.tab_following': 'Volgend', 'tl.tab_replies': 'Reacties', 'tl.tab_followers': 'Volgers', 'tl.followers': 'Volgers', 'tl.followers_lead': 'Wie jou volgt in de fediverse, met de laatste geslaagde bezorging. Rood = nog nooit bezorgd of laatste poging mislukt — kandidaat om op te ruimen na een check.', 'tl.empty_followers': 'Nog geen volgers.', 'tl.last_delivery': 'Laatste bezorging', 'tl.never_delivered': 'Nog nooit bezorgd', 'tl.delivery_failed': 'laatste poging mislukt', 'tl.remove_follower': 'Verwijderen', 'tl.remove_confirm': 'Deze volger verwijderen? Een actief account moet je dan opnieuw volgen.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'jij volgt', 'tl.dir_follower': 'volgt jou', 'tl.dir_mutual': 'wederzijds', 'tl.connect_empty': 'Nog geen connecties. Volg iemand hierboven om te beginnen.', 'tl.unreachable': 'Niet bereikbaar', 'tl.unreachable_lead': 'Deze volgers konden we niet bereiken (nooit bezorgd of laatste poging mislukt). Ruim ze op na een handmatige check.', 'msg.tab': 'Berichten', 'msg.title': 'Berichten', 'msg.filter_all': 'Alles', 'msg.filter_conv': 'Gesprekken', 'msg.filter_act': 'Activiteit', 'msg.filter_sent': 'Verzonden', 'msg.you': 'Jij', 'msg.sent_reply': 'reageerde via de fediverse', 'msg.and_more': 'en {n} anderen', 'msg.liked_many': 'liketen je post', 'msg.boosted_many': 'boostten je post', 'msg.private': 'privé', 'msg.private_hint': 'Alleen aan jou gericht; staat niet op de publieke postpagina.', 'msg.new': 'Nieuw sinds je laatste bezoek', 'msg.empty': 'Nog geen berichten. Reacties, vermeldingen en activiteit verschijnen hier.', 'oauth.title': 'App toegang geven', 'oauth.wants_access': 'wil verbinding maken met je Klonkt-account.', 'oauth.post_as': 'Plaatsen als', 'oauth.scope_read': 'Je berichten, reacties en meldingen lezen', 'oauth.scope_write': 'Namens jou posten, reageren, liken en volgen', 'oauth.allow': 'Toestaan', 'oauth.deny': 'Weigeren', 'oauth.foot': 'Je kunt de toegang later intrekken. Geef alleen apps toegang die je vertrouwt.', 're.title': 'Reactie schrijven', 're.bold': 'Vet', 're.italic': 'Cursief', 're.link': 'Link invoegen', 're.list': 'Opsomming', 're.quote': 'Citaat', 're.lang': 'Taal van je reactie', 're.attach': 'Media toevoegen (afbeelding, audio, video)', 're.attach_err': 'Upload mislukt', 'tl.empty_following': 'Je volgt nog niemand.', 'tl.empty': 'Nog niks — volg iemand om hun berichten hier te zien.', 'tl.view_original': 'Bekijk origineel →', 'tl.open_player': 'Open de speler', 'tl.paste_ph': 'Plak een fediverse-post-URL', 'tl.paste_go': 'Openen', 'tl.boosted': 'boostte dit', 'tl.read_more': 'Meer lezen', 'tl.show_less': 'Minder', 'poll.vote': 'Stem', 'poll.votes': 'stemmen', 'poll.closed': 'gesloten', 'poll.open': 'open', 'poll.aria': 'Peiling', 'poll.voter_one': 'stemmer', 'poll.voter_many': 'stemmers', 'poll.closes': 'sluit op', 'poll.multiple': 'meerkeuze', 'poll.fedi_only': 'Stemmen kan vanuit de fediverse — volg deze site en stem in je eigen app.', 'poll.voted_title': 'Stem verstuurd', 'poll.voted_done': 'Je stem is verstuurd naar de poll. De uitslag werkt bij zodra de maker die doorstuurt.',
     'comments.to_start': 'om de conversatie te starten.',
     'comments.reply': 'Reageer', 'comments.delete': 'Verwijder', 'comments.cancel': 'Annuleren',
@@ -1042,5 +1042,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.follow_heading': 'Follow via the fediverse', 'fedi.profile_follow': 'Follow via the fediverse', 'profile.since': 'On Klonkt since', 'profile.free': 'Free', 'fedi.follow_intro': 'You are about to follow:', 'fedi.follow_btn': 'Follow', 'fedi.cancel': 'Cancel', 'fedi.followed_title': 'Follow request sent ✅', 'fedi.followed_done': 'Your follow request is on its way. Once accepted, their posts show up in your timeline.', 'fedi.view_profile': 'View profile →', 'fedi.remote_reply': 'Reply via the fediverse', 'fedi.remote_prompt': 'Your fediverse address:', '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.like_short': 'Like', 'fedi.unlike_short': 'Unlike', 'fedi.boost_short': 'Boost', 'fedi.remote_ph': 'your server', 'fedi.remote_sent_title': 'Sent ✅', 'fedi.remote_sent': 'Your reply has been sent. It will show up on the original post on the fediverse, not on this page. See it there:', 'fedi.reply_where': 'Your reply appears on the original post on the fediverse, not on this page. Use the link above to see it there.', 'fedi.remote_back': '← Back to your site', 'fedi.like_btn': 'Like this post', 'fedi.or_reply': 'or reply:', 'fedi.liked_title': 'Liked', 'fedi.liked_done': 'Your like is on its way to the fediverse.', 'fedi.boost_btn': 'Boost this post', 'fedi.boosted_title': 'Boosted', 'fedi.boosted_done': 'Your boost is on its way to the fediverse.', 'fedi.remote_interact': 'Interact via the fediverse', 'fedi.report_open': 'Report this post', 'fedi.report_where': 'The report goes to the instance and their moderator(s).', 'fedi.report_ph': 'What is wrong? (optional)', 'fedi.report_send': 'Report', 'fedi.reported_title': 'Reported', 'fedi.reported_done': 'Your report has been sent to their server. Their moderators will review it.', 'fedi.delete_confirm': 'Delete this reply?', 'fedi.mod_remove_confirm': 'Remove this reply from your thread? It will not come back, not even via thread-filling.', 'fedi.mod_report_confirm': 'Report this reply to its author’s server?', 'fedi.manage_title': 'My fediverse replies', 'fedi.manage_empty': 'You have not sent any replies yet.', 'fedi.goto_post': 'Go to post', 'fedi.edit': 'Edit', 'fedi.save_edit': 'Save', 'fedi.bm_label': 'Interact via my site', 'fedi.bm_help': 'Drag this button to your bookmarks bar. Then click it on any fediverse post (Mastodon, another Klonkt…) to reply, like or boost it via your own site.', 'tl.title': 'News', 'tl.lead': 'Follow accounts on the fediverse and see their posts here.', 'tl.follow_btn': 'Follow', 'tl.following': 'Following', 'tl.unfollow': 'Unfollow', 'tl.autoboost': 'Featured', 'tl.autoboost_follow': 'feature in circle', 'tl.autoboost_hint': 'Their new posts keep showing in your Circle (local, no fediverse boost).', 'tl.pending': 'pending', 'tl.unboost': 'Unboost', 'tl.feed': 'Posts', 'tl.tab_feed': 'News', 'tl.tab_following': 'Following', 'tl.tab_replies': 'Replies', 'tl.tab_followers': 'Followers', 'tl.followers': 'Followers', 'tl.followers_lead': 'Who follows you on the fediverse, with the last successful delivery. Red = never delivered or last attempt failed — a candidate to clean up after a check.', 'tl.empty_followers': 'No followers yet.', 'tl.last_delivery': 'Last delivery', 'tl.never_delivered': 'Never delivered', 'tl.delivery_failed': 'last attempt failed', 'tl.remove_follower': 'Remove', 'tl.remove_confirm': 'Remove this follower? An active account would have to follow you again.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'you follow', 'tl.dir_follower': 'follows you', 'tl.dir_mutual': 'mutual', 'tl.connect_empty': 'No connections yet. Follow someone above to get started.', 'tl.unreachable': 'Unreachable', 'tl.unreachable_lead': 'We could not reach these followers (never delivered or last attempt failed). Clean them up after a manual check.', 'msg.tab': 'Messages', 'msg.title': 'Messages', 'msg.filter_all': 'All', 'msg.filter_conv': 'Conversations', 'msg.filter_act': 'Activity', 'msg.filter_sent': 'Sent', 'msg.you': 'You', 'msg.sent_reply': 'replied via the fediverse', 'msg.and_more': 'and {n} others', 'msg.liked_many': 'liked your post', 'msg.boosted_many': 'boosted your post', 'msg.private': 'private', 'msg.private_hint': 'Addressed to you only; not shown on the public post page.', 'msg.new': 'New since your last visit', 'msg.empty': 'No messages yet. Replies, mentions and activity show up here.', 'oauth.title': 'Authorize app', 'oauth.wants_access': 'wants to connect to your Klonkt account.', 'oauth.post_as': 'Post as', 'oauth.scope_read': 'Read your posts, replies and notifications', 'oauth.scope_write': 'Post, reply, like and follow on your behalf', 'oauth.allow': 'Allow', 'oauth.deny': 'Deny', 'oauth.foot': 'You can revoke access later. Only authorize apps you trust.', 're.title': 'Write a reply', 're.bold': 'Bold', 're.italic': 'Italic', 're.link': 'Insert link', 're.list': 'Bullet list', 're.quote': 'Quote', 're.lang': 'Language of your reply', 'tl.empty_following': 'You do not follow anyone yet.', 'tl.empty': 'Nothing yet — follow someone to see their posts here.', 'tl.view_original': 'View original →', 'tl.open_player': 'Open the player', 'tl.paste_ph': 'Paste a fediverse post URL', 'tl.paste_go': 'Open', 'tl.boosted': 'boosted', 'tl.read_more': 'Read more', 'tl.show_less': 'Show less', 'poll.vote': 'Vote', 'poll.votes': 'votes', 'poll.closed': 'closed', 'poll.open': 'open', 'poll.aria': 'Poll', 'poll.voter_one': 'voter', 'poll.voter_many': 'voters', 'poll.closes': 'closes', 'poll.multiple': 'multiple choice', 'poll.fedi_only': 'Voting happens on the fediverse — follow this site and vote from your own app.', 'poll.voted_title': 'Vote sent', 'poll.voted_done': 'Your vote is on its way to the poll. The results refresh once the author sends the update.',
+    'fedi.remote_title': 'Reply via the fediverse', 'fedi.follow_heading': 'Follow via the fediverse', 'fedi.profile_follow': 'Follow via the fediverse', 'profile.since': 'On Klonkt since', 'profile.free': 'Free', 'fedi.follow_intro': 'You are about to follow:', 'fedi.follow_btn': 'Follow', 'fedi.cancel': 'Cancel', 'fedi.followed_title': 'Follow request sent ✅', 'fedi.followed_done': 'Your follow request is on its way. Once accepted, their posts show up in your timeline.', 'fedi.view_profile': 'View profile →', 'fedi.remote_reply': 'Reply via the fediverse', 'fedi.remote_prompt': 'Your fediverse address:', '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.like_short': 'Like', 'fedi.unlike_short': 'Unlike', 'fedi.boost_short': 'Boost', 'fedi.remote_ph': 'your server', 'fedi.remote_sent_title': 'Sent ✅', 'fedi.remote_sent': 'Your reply has been sent. It will show up on the original post on the fediverse, not on this page. See it there:', 'fedi.reply_where': 'Your reply appears on the original post on the fediverse, not on this page. Use the link above to see it there.', 'fedi.remote_back': '← Back to your site', 'fedi.like_btn': 'Like this post', 'fedi.or_reply': 'or reply:', 'fedi.liked_title': 'Liked', 'fedi.liked_done': 'Your like is on its way to the fediverse.', 'fedi.boost_btn': 'Boost this post', 'fedi.boosted_title': 'Boosted', 'fedi.boosted_done': 'Your boost is on its way to the fediverse.', 'fedi.remote_interact': 'Interact via the fediverse', 'fedi.report_open': 'Report this post', 'fedi.report_where': 'The report goes to the instance and their moderator(s).', 'fedi.report_ph': 'What is wrong? (optional)', 'fedi.report_send': 'Report', 'fedi.reported_title': 'Reported', 'fedi.reported_done': 'Your report has been sent to their server. Their moderators will review it.', 'fedi.delete_confirm': 'Delete this reply?', 'fedi.mod_remove_confirm': 'Remove this reply from your thread? It will not come back, not even via thread-filling.', 'fedi.mod_report_confirm': 'Report this reply to its author’s server?', 'fedi.manage_title': 'My fediverse replies', 'fedi.manage_empty': 'You have not sent any replies yet.', 'fedi.goto_post': 'Go to post', 'fedi.edit': 'Edit', 'fedi.save_edit': 'Save', 'fedi.bm_label': 'Interact via my site', 'fedi.bm_help': 'Drag this button to your bookmarks bar. Then click it on any fediverse post (Mastodon, another Klonkt…) to reply, like or boost it via your own site.', 'tl.title': 'News', 'tl.lead': 'Follow accounts on the fediverse and see their posts here.', 'tl.follow_btn': 'Follow', 'tl.following': 'Following', 'tl.unfollow': 'Unfollow', 'tl.autoboost': 'Featured', 'tl.autoboost_follow': 'feature in circle', 'tl.autoboost_hint': 'Their new posts keep showing in your Circle (local, no fediverse boost).', 'tl.pending': 'pending', 'tl.unboost': 'Unboost', 'tl.feed': 'Posts', 'tl.tab_feed': 'News', 'tl.tab_following': 'Following', 'tl.tab_replies': 'Replies', 'tl.tab_followers': 'Followers', 'tl.followers': 'Followers', 'tl.followers_lead': 'Who follows you on the fediverse, with the last successful delivery. Red = never delivered or last attempt failed — a candidate to clean up after a check.', 'tl.empty_followers': 'No followers yet.', 'tl.last_delivery': 'Last delivery', 'tl.never_delivered': 'Never delivered', 'tl.delivery_failed': 'last attempt failed', 'tl.remove_follower': 'Remove', 'tl.remove_confirm': 'Remove this follower? An active account would have to follow you again.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'you follow', 'tl.dir_follower': 'follows you', 'tl.dir_mutual': 'mutual', 'tl.connect_empty': 'No connections yet. Follow someone above to get started.', 'tl.unreachable': 'Unreachable', 'tl.unreachable_lead': 'We could not reach these followers (never delivered or last attempt failed). Clean them up after a manual check.', 'msg.tab': 'Messages', 'msg.title': 'Messages', 'msg.filter_all': 'All', 'msg.filter_conv': 'Conversations', 'msg.filter_act': 'Activity', 'msg.filter_sent': 'Sent', 'msg.you': 'You', 'msg.sent_reply': 'replied via the fediverse', 'msg.and_more': 'and {n} others', 'msg.liked_many': 'liked your post', 'msg.boosted_many': 'boosted your post', 'msg.private': 'private', 'msg.private_hint': 'Addressed to you only; not shown on the public post page.', 'msg.new': 'New since your last visit', 'msg.empty': 'No messages yet. Replies, mentions and activity show up here.', 'oauth.title': 'Authorize app', 'oauth.wants_access': 'wants to connect to your Klonkt account.', 'oauth.post_as': 'Post as', 'oauth.scope_read': 'Read your posts, replies and notifications', 'oauth.scope_write': 'Post, reply, like and follow on your behalf', 'oauth.allow': 'Allow', 'oauth.deny': 'Deny', 'oauth.foot': 'You can revoke access later. Only authorize apps you trust.', 're.title': 'Write a reply', 're.bold': 'Bold', 're.italic': 'Italic', 're.link': 'Insert link', 're.list': 'Bullet list', 're.quote': 'Quote', 're.lang': 'Language of your reply', 're.attach': 'Add media (image, audio, video)', 're.attach_err': 'Upload failed', 'tl.empty_following': 'You do not follow anyone yet.', 'tl.empty': 'Nothing yet — follow someone to see their posts here.', 'tl.view_original': 'View original →', 'tl.open_player': 'Open the player', 'tl.paste_ph': 'Paste a fediverse post URL', 'tl.paste_go': 'Open', 'tl.boosted': 'boosted', 'tl.read_more': 'Read more', 'tl.show_less': 'Show less', 'poll.vote': 'Vote', 'poll.votes': 'votes', 'poll.closed': 'closed', 'poll.open': 'open', 'poll.aria': 'Poll', 'poll.voter_one': 'voter', 'poll.voter_many': 'voters', 'poll.closes': 'closes', 'poll.multiple': 'multiple choice', 'poll.fedi_only': 'Voting happens on the fediverse — follow this site and vote from your own app.', 'poll.voted_title': 'Vote sent', 'poll.voted_done': 'Your vote is on its way to the poll. The results refresh once the author sends the update.',
     'comments.to_start': 'to start the conversation.',
     'comments.reply': 'Reply', 'comments.delete': 'Delete', 'comments.cancel': 'Cancel',
@@ -1969,5 +1969,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.follow_heading': 'Über das Fediverse folgen', 'fedi.profile_follow': 'Über das Fediverse folgen', 'profile.since': 'Auf Klonkt seit', 'profile.free': 'Kostenlos', 'fedi.follow_intro': 'Du folgst gleich:', 'fedi.follow_btn': 'Folgen', 'fedi.cancel': 'Abbrechen', 'fedi.followed_title': 'Folge-Anfrage gesendet ✅', 'fedi.followed_done': 'Deine Folge-Anfrage ist unterwegs. Sobald sie akzeptiert wird, erscheinen ihre Beiträge in deiner Timeline.', 'fedi.view_profile': 'Profil ansehen →', 'fedi.remote_reply': 'Über das Fediverse antworten', 'fedi.remote_prompt': 'Deine Fediverse-Adresse:', '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.like_short': 'Liken', 'fedi.unlike_short': 'Like zurücknehmen', 'fedi.boost_short': 'Boosten', 'fedi.remote_ph': 'dein Server', 'fedi.remote_sent_title': 'Gesendet ✅', 'fedi.remote_sent': 'Deine Antwort wurde gesendet. Sie erscheint gleich beim Originalbeitrag im Fediverse, nicht auf dieser Seite. Sieh sie dir dort an:', 'fedi.reply_where': 'Deine Antwort erscheint beim Originalbeitrag im Fediverse, nicht auf dieser Seite. Über den Link oben siehst du sie dort.', 'fedi.remote_back': '← Zurück zu deiner Seite', 'fedi.like_btn': 'Diesen Beitrag liken', 'fedi.or_reply': 'oder antworten:', 'fedi.liked_title': 'Geliked', 'fedi.liked_done': 'Dein Like ist unterwegs ins Fediverse.', 'fedi.boost_btn': 'Diesen Beitrag boosten', 'fedi.boosted_title': 'Geboostet', 'fedi.boosted_done': 'Dein Boost ist unterwegs ins Fediverse.', 'fedi.remote_interact': 'Übers Fediverse interagieren', 'fedi.report_open': 'Diesen Beitrag melden', 'fedi.report_where': 'Die Meldung geht an die Instanz und deren Moderator(en).', 'fedi.report_ph': 'Was ist das Problem? (optional)', 'fedi.report_send': 'Melden', 'fedi.reported_title': 'Gemeldet', 'fedi.reported_done': 'Deine Meldung wurde an den Server der Person gesendet. Deren Moderatoren prüfen sie.', 'fedi.delete_confirm': 'Diese Antwort löschen?', 'fedi.mod_remove_confirm': 'Diese Antwort aus deinem Thread entfernen? Sie kommt nicht zurück, auch nicht über Thread-Auffüllung.', 'fedi.mod_report_confirm': 'Diese Antwort beim Server des Autors melden?', 'fedi.manage_title': 'Meine Fediverse-Antworten', 'fedi.manage_empty': 'Du hast noch keine Antworten gesendet.', 'fedi.goto_post': 'Zur Post', 'fedi.edit': 'Bearbeiten', 'fedi.save_edit': 'Speichern', 'fedi.bm_label': 'Über meine Seite interagieren', 'fedi.bm_help': 'Zieh diesen Button in deine Lesezeichenleiste. Klick ihn dann auf einem beliebigen Fediverse-Beitrag (Mastodon, ein anderes Klonkt…), um über deine eigene Seite zu antworten, zu liken oder zu boosten.', 'tl.title': 'Zeitung', 'tl.lead': 'Folge Konten im Fediverse und sieh ihre Beiträge hier.', 'tl.follow_btn': 'Folgen', 'tl.following': 'Du folgst', 'tl.unfollow': 'Entfolgen', 'tl.autoboost': 'Hervorgehoben', 'tl.autoboost_follow': 'im Zirkel hervorheben', 'tl.autoboost_hint': 'Ihre neuen Beiträge erscheinen laufend in deinem Zirkel (lokal, kein Fediverse-Boost).', 'tl.pending': 'ausstehend', 'tl.unboost': 'Boost zurücknehmen', 'tl.feed': 'Beiträge', 'tl.tab_feed': 'Zeitung', 'tl.tab_following': 'Folge ich', 'tl.tab_replies': 'Antworten', 'tl.tab_followers': 'Follower', 'tl.followers': 'Follower', 'tl.followers_lead': 'Wer dir im Fediverse folgt, mit der letzten erfolgreichen Zustellung. Rot = nie zugestellt oder letzter Versuch fehlgeschlagen — nach einer Prüfung ein Kandidat zum Aufräumen.', 'tl.empty_followers': 'Noch keine Follower.', 'tl.last_delivery': 'Letzte Zustellung', 'tl.never_delivered': 'Nie zugestellt', 'tl.delivery_failed': 'letzter Versuch fehlgeschlagen', 'tl.remove_follower': 'Entfernen', 'tl.remove_confirm': 'Diesen Follower entfernen? Ein aktives Konto müsste dir erneut folgen.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'du folgst', 'tl.dir_follower': 'folgt dir', 'tl.dir_mutual': 'gegenseitig', 'tl.connect_empty': 'Noch keine Verbindungen. Folge oben jemandem, um zu starten.', 'tl.unreachable': 'Nicht erreichbar', 'tl.unreachable_lead': 'Diese Follower konnten wir nicht erreichen (nie zugestellt oder letzter Versuch fehlgeschlagen). Räume sie nach einer Prüfung auf.', 'msg.tab': 'Nachrichten', 'msg.title': 'Nachrichten', 'msg.filter_all': 'Alle', 'msg.filter_conv': 'Gespräche', 'msg.filter_act': 'Aktivität', 'msg.filter_sent': 'Gesendet', 'msg.you': 'Du', 'msg.sent_reply': 'hat über das Fediverse geantwortet', 'msg.and_more': 'und {n} andere', 'msg.liked_many': 'gefällt dein Beitrag', 'msg.boosted_many': 'teilten deinen Beitrag', 'msg.private': 'privat', 'msg.private_hint': 'Nur an dich gerichtet; erscheint nicht auf der öffentlichen Beitragsseite.', 'msg.new': 'Neu seit deinem letzten Besuch', 'msg.empty': 'Noch keine Nachrichten. Antworten, Erwähnungen und Aktivität erscheinen hier.', 'oauth.title': 'App autorisieren', 'oauth.wants_access': 'möchte sich mit deinem Klonkt-Konto verbinden.', 'oauth.post_as': 'Posten als', 'oauth.scope_read': 'Deine Beiträge, Antworten und Meldungen lesen', 'oauth.scope_write': 'In deinem Namen posten, antworten, liken und folgen', 'oauth.allow': 'Erlauben', 'oauth.deny': 'Ablehnen', 'oauth.foot': 'Du kannst den Zugriff später widerrufen. Autorisiere nur Apps, denen du vertraust.', 're.title': 'Antwort schreiben', 're.bold': 'Fett', 're.italic': 'Kursiv', 're.link': 'Link einfügen', 're.list': 'Aufzählung', 're.quote': 'Zitat', 're.lang': 'Sprache deiner Antwort', 'tl.empty_following': 'Du folgst noch niemandem.', 'tl.empty': 'Noch nichts — folge jemandem, um Beiträge hier zu sehen.', 'tl.view_original': 'Original ansehen →', 'tl.open_player': 'Player öffnen', 'tl.paste_ph': 'URL eines Fediverse-Beitrags einfügen', 'tl.paste_go': 'Öffnen', 'tl.boosted': 'hat geteilt', 'tl.read_more': 'Mehr lesen', 'tl.show_less': 'Weniger', 'poll.vote': 'Abstimmen', 'poll.votes': 'Stimmen', 'poll.closed': 'geschlossen', 'poll.open': 'offen', 'poll.aria': 'Umfrage', 'poll.voter_one': 'Teilnehmer', 'poll.voter_many': 'Teilnehmer', 'poll.closes': 'endet am', 'poll.multiple': 'Mehrfachauswahl', 'poll.fedi_only': 'Abstimmen geht über das Fediverse — folge dieser Seite und stimme in deiner eigenen App ab.', 'poll.voted_title': 'Stimme gesendet', 'poll.voted_done': 'Deine Stimme ist unterwegs zur Umfrage. Die Ergebnisse aktualisieren sich, sobald die Autorin oder der Autor das Update sendet.',
+    'fedi.remote_title': 'Über das Fediverse antworten', 'fedi.follow_heading': 'Über das Fediverse folgen', 'fedi.profile_follow': 'Über das Fediverse folgen', 'profile.since': 'Auf Klonkt seit', 'profile.free': 'Kostenlos', 'fedi.follow_intro': 'Du folgst gleich:', 'fedi.follow_btn': 'Folgen', 'fedi.cancel': 'Abbrechen', 'fedi.followed_title': 'Folge-Anfrage gesendet ✅', 'fedi.followed_done': 'Deine Folge-Anfrage ist unterwegs. Sobald sie akzeptiert wird, erscheinen ihre Beiträge in deiner Timeline.', 'fedi.view_profile': 'Profil ansehen →', 'fedi.remote_reply': 'Über das Fediverse antworten', 'fedi.remote_prompt': 'Deine Fediverse-Adresse:', '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.like_short': 'Liken', 'fedi.unlike_short': 'Like zurücknehmen', 'fedi.boost_short': 'Boosten', 'fedi.remote_ph': 'dein Server', 'fedi.remote_sent_title': 'Gesendet ✅', 'fedi.remote_sent': 'Deine Antwort wurde gesendet. Sie erscheint gleich beim Originalbeitrag im Fediverse, nicht auf dieser Seite. Sieh sie dir dort an:', 'fedi.reply_where': 'Deine Antwort erscheint beim Originalbeitrag im Fediverse, nicht auf dieser Seite. Über den Link oben siehst du sie dort.', 'fedi.remote_back': '← Zurück zu deiner Seite', 'fedi.like_btn': 'Diesen Beitrag liken', 'fedi.or_reply': 'oder antworten:', 'fedi.liked_title': 'Geliked', 'fedi.liked_done': 'Dein Like ist unterwegs ins Fediverse.', 'fedi.boost_btn': 'Diesen Beitrag boosten', 'fedi.boosted_title': 'Geboostet', 'fedi.boosted_done': 'Dein Boost ist unterwegs ins Fediverse.', 'fedi.remote_interact': 'Übers Fediverse interagieren', 'fedi.report_open': 'Diesen Beitrag melden', 'fedi.report_where': 'Die Meldung geht an die Instanz und deren Moderator(en).', 'fedi.report_ph': 'Was ist das Problem? (optional)', 'fedi.report_send': 'Melden', 'fedi.reported_title': 'Gemeldet', 'fedi.reported_done': 'Deine Meldung wurde an den Server der Person gesendet. Deren Moderatoren prüfen sie.', 'fedi.delete_confirm': 'Diese Antwort löschen?', 'fedi.mod_remove_confirm': 'Diese Antwort aus deinem Thread entfernen? Sie kommt nicht zurück, auch nicht über Thread-Auffüllung.', 'fedi.mod_report_confirm': 'Diese Antwort beim Server des Autors melden?', 'fedi.manage_title': 'Meine Fediverse-Antworten', 'fedi.manage_empty': 'Du hast noch keine Antworten gesendet.', 'fedi.goto_post': 'Zur Post', 'fedi.edit': 'Bearbeiten', 'fedi.save_edit': 'Speichern', 'fedi.bm_label': 'Über meine Seite interagieren', 'fedi.bm_help': 'Zieh diesen Button in deine Lesezeichenleiste. Klick ihn dann auf einem beliebigen Fediverse-Beitrag (Mastodon, ein anderes Klonkt…), um über deine eigene Seite zu antworten, zu liken oder zu boosten.', 'tl.title': 'Zeitung', 'tl.lead': 'Folge Konten im Fediverse und sieh ihre Beiträge hier.', 'tl.follow_btn': 'Folgen', 'tl.following': 'Du folgst', 'tl.unfollow': 'Entfolgen', 'tl.autoboost': 'Hervorgehoben', 'tl.autoboost_follow': 'im Zirkel hervorheben', 'tl.autoboost_hint': 'Ihre neuen Beiträge erscheinen laufend in deinem Zirkel (lokal, kein Fediverse-Boost).', 'tl.pending': 'ausstehend', 'tl.unboost': 'Boost zurücknehmen', 'tl.feed': 'Beiträge', 'tl.tab_feed': 'Zeitung', 'tl.tab_following': 'Folge ich', 'tl.tab_replies': 'Antworten', 'tl.tab_followers': 'Follower', 'tl.followers': 'Follower', 'tl.followers_lead': 'Wer dir im Fediverse folgt, mit der letzten erfolgreichen Zustellung. Rot = nie zugestellt oder letzter Versuch fehlgeschlagen — nach einer Prüfung ein Kandidat zum Aufräumen.', 'tl.empty_followers': 'Noch keine Follower.', 'tl.last_delivery': 'Letzte Zustellung', 'tl.never_delivered': 'Nie zugestellt', 'tl.delivery_failed': 'letzter Versuch fehlgeschlagen', 'tl.remove_follower': 'Entfernen', 'tl.remove_confirm': 'Diesen Follower entfernen? Ein aktives Konto müsste dir erneut folgen.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'du folgst', 'tl.dir_follower': 'folgt dir', 'tl.dir_mutual': 'gegenseitig', 'tl.connect_empty': 'Noch keine Verbindungen. Folge oben jemandem, um zu starten.', 'tl.unreachable': 'Nicht erreichbar', 'tl.unreachable_lead': 'Diese Follower konnten wir nicht erreichen (nie zugestellt oder letzter Versuch fehlgeschlagen). Räume sie nach einer Prüfung auf.', 'msg.tab': 'Nachrichten', 'msg.title': 'Nachrichten', 'msg.filter_all': 'Alle', 'msg.filter_conv': 'Gespräche', 'msg.filter_act': 'Aktivität', 'msg.filter_sent': 'Gesendet', 'msg.you': 'Du', 'msg.sent_reply': 'hat über das Fediverse geantwortet', 'msg.and_more': 'und {n} andere', 'msg.liked_many': 'gefällt dein Beitrag', 'msg.boosted_many': 'teilten deinen Beitrag', 'msg.private': 'privat', 'msg.private_hint': 'Nur an dich gerichtet; erscheint nicht auf der öffentlichen Beitragsseite.', 'msg.new': 'Neu seit deinem letzten Besuch', 'msg.empty': 'Noch keine Nachrichten. Antworten, Erwähnungen und Aktivität erscheinen hier.', 'oauth.title': 'App autorisieren', 'oauth.wants_access': 'möchte sich mit deinem Klonkt-Konto verbinden.', 'oauth.post_as': 'Posten als', 'oauth.scope_read': 'Deine Beiträge, Antworten und Meldungen lesen', 'oauth.scope_write': 'In deinem Namen posten, antworten, liken und folgen', 'oauth.allow': 'Erlauben', 'oauth.deny': 'Ablehnen', 'oauth.foot': 'Du kannst den Zugriff später widerrufen. Autorisiere nur Apps, denen du vertraust.', 're.title': 'Antwort schreiben', 're.bold': 'Fett', 're.italic': 'Kursiv', 're.link': 'Link einfügen', 're.list': 'Aufzählung', 're.quote': 'Zitat', 're.lang': 'Sprache deiner Antwort', 're.attach': 'Medien hinzufügen (Bild, Audio, Video)', 're.attach_err': 'Upload fehlgeschlagen', 'tl.empty_following': 'Du folgst noch niemandem.', 'tl.empty': 'Noch nichts — folge jemandem, um Beiträge hier zu sehen.', 'tl.view_original': 'Original ansehen →', 'tl.open_player': 'Player öffnen', 'tl.paste_ph': 'URL eines Fediverse-Beitrags einfügen', 'tl.paste_go': 'Öffnen', 'tl.boosted': 'hat geteilt', 'tl.read_more': 'Mehr lesen', 'tl.show_less': 'Weniger', 'poll.vote': 'Abstimmen', 'poll.votes': 'Stimmen', 'poll.closed': 'geschlossen', 'poll.open': 'offen', 'poll.aria': 'Umfrage', 'poll.voter_one': 'Teilnehmer', 'poll.voter_many': 'Teilnehmer', 'poll.closes': 'endet am', 'poll.multiple': 'Mehrfachauswahl', 'poll.fedi_only': 'Abstimmen geht über das Fediverse — folge dieser Seite und stimme in deiner eigenen App ab.', 'poll.voted_title': 'Stimme gesendet', 'poll.voted_done': 'Deine Stimme ist unterwegs zur Umfrage. Die Ergebnisse aktualisieren sich, sobald die Autorin oder der Autor das Update sendet.',
     'comments.to_start': 'um das Gespräch zu starten.',
     'comments.reply': 'Antworten', 'comments.delete': 'Löschen', 'comments.cancel': 'Abbrechen',
Index: src/views/partials/fedi-node.ejs
===================================================================
--- src/views/partials/fedi-node.ejs	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ src/views/partials/fedi-node.ejs	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -12,4 +12,21 @@
   </div>
   <div class="comment-content"><%- n.content %></div>
+  <%# Rich replies: media on our own sent replies (attachments on the Note).
+      Visitors see these too, and the reply editor (owner-only) may not render,
+      so make sure the stylesheet is on the page either way. %>
+  <% if (n.media && n.media.length) { %>
+    <% if (!locals.__reAssets) { locals.__reAssets = 1; %><link rel="stylesheet" href="/assets/css/reply-editor.css"><% } %>
+    <div class="re-reply-media">
+      <% n.media.forEach(function (m) { %>
+        <% if ((m.mediaType || '').indexOf('image/') === 0) { %>
+          <a href="<%= m.url %>" target="_blank" rel="noopener"><img src="<%= m.url %>" alt="<%= m.name || '' %>" loading="lazy"></a>
+        <% } else if ((m.mediaType || '').indexOf('audio/') === 0) { %>
+          <audio controls preload="none" src="<%= m.url %>"></audio>
+        <% } else { %>
+          <video controls preload="metadata" src="<%= m.url %>"></video>
+        <% } %>
+      <% }); %>
+    </div>
+  <% } %>
   <div class="comment-actions">
     <% if (n.mine && typeof canManageSite !== 'undefined' && canManageSite && n.outboxId) { %>
Index: src/views/partials/reply-editor.ejs
===================================================================
--- src/views/partials/reply-editor.ejs	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ src/views/partials/reply-editor.ejs	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -18,5 +18,6 @@
 %>
 <% var _reLang = (typeof defaultLang !== 'undefined' && defaultLang) || (typeof lang !== 'undefined' ? lang : 'en'); %>
-<form method="post" action="<%= action %>" class="re-form" data-re>
+<form method="post" action="<%= action %>" class="re-form" data-re
+      data-upload="/posts/upload-reply-media" data-upload-err="<%= t('re.attach_err') %>">
   <% (typeof hiddenFields !== 'undefined' ? hiddenFields : []).forEach(function (h) { %>
     <input type="hidden" name="<%= h.name %>" value="<%= h.value %>">
@@ -35,5 +36,9 @@
     <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>
   </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>
   <div class="re-editor" contenteditable="true" role="textbox" aria-multiline="true"
Index: test/rich-reply.test.js
===================================================================
--- test/rich-reply.test.js	(revision 33e1dbd11a4cb4638e01c26e3f3c98e352e8892e)
+++ test/rich-reply.test.js	(revision feced2ce628d8bee7974f77404806f4be6f2272b)
@@ -80,2 +80,42 @@
   assert.equal(row.language, null);
 });
+
+test('attachments: own /media/ urls stored, note carries typed absolute attachments', async () => {
+  const r = await AP.deliverReply(site, {
+    postId: 'p1', postSlug: 'hallo', parent, text: 'met media', html: '',
+    attachments: [
+      { url: '/media/reply-media/a.webp', mediaType: 'image/webp', name: 'foto' },
+      { url: '/media/reply-media/b.mp3', mediaType: 'audio/mpeg', name: 'liedje' },
+      { url: 'https://evil.example/x.png', mediaType: 'image/png', name: 'remote' },  // rejected: not ours
+      { url: '/media/reply-media/c.pdf', mediaType: 'application/pdf', name: 'doc' }, // rejected: type
+    ],
+  });
+  const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get(r.id);
+  const stored = JSON.parse(row.attachments);
+  assert.equal(stored.length, 2);
+  assert.deepEqual(stored.map((a) => a.url), ['/media/reply-media/a.webp', '/media/reply-media/b.mp3']);
+
+  const note = AP.buildReplyNote('https://klonkt.test', site, row);
+  assert.equal(note.attachment.length, 2);
+  assert.deepEqual(note.attachment.map((a) => a.type), ['Image', 'Audio']);
+  assert.equal(note.attachment[0].url, 'https://klonkt.test/media/reply-media/a.webp');
+});
+
+test('a media-only reply (no text) is delivered', async () => {
+  const r = await AP.deliverReply(site, {
+    postId: 'p1', postSlug: 'hallo', parent, text: '', html: '',
+    attachments: [{ url: '/media/reply-media/solo.webp', mediaType: 'image/webp', name: '' }],
+  });
+  assert.ok(r && r.id);
+  const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get(r.id);
+  assert.match(row.content, /^<p><a /); // just the mention paragraph
+  assert.equal(JSON.parse(row.attachments).length, 1);
+});
+
+test('only foreign/invalid attachments and no text -> rejected', async () => {
+  const r = await AP.deliverReply(site, {
+    postId: 'p1', postSlug: 'hallo', parent, text: '', html: '',
+    attachments: [{ url: 'https://evil.example/x.png', mediaType: 'image/png' }],
+  });
+  assert.equal(r, null);
+});
