Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 15f1cb57a2792167f63aa562ececc415c9943088)
+++ src/config/database.js	(revision 2a1044514fb6dd87ad43ab100954639ef3720d11)
@@ -696,4 +696,11 @@
   ensureColumn('ap_gated_offers', 'proposer', 'TEXT'); // who proposed (5.6): the settle-answer goes back to them
   ensureColumn('posts', 'c2s_attachments', 'TEXT'); // media a C2S Note carried (JSON [{url,mediaType,name}]); buildNote federates them
+  // 30-7: C2S posts briefly got their content media copied onto the cover,
+  // which showed the same video twice on the post page. Clear the covers that
+  // duplicate their own content; idempotent, only ever touches those.
+  try {
+    db.prepare("UPDATE posts SET cover_video_url = NULL WHERE cover_video_url LIKE '/media/reply-media/%' AND instr(content, cover_video_url) > 0").run();
+    db.prepare("UPDATE posts SET cover_image_url = NULL WHERE cover_image_url LIKE '/media/reply-media/%' AND instr(content, cover_image_url) > 0").run();
+  } catch { /* posts table absent on fresh init */ }
   ensureColumn('ap_mentions', 'wave', 'INTEGER');  // inbound guardian wave
   // FEP-633c §2.2: object hint that the author is a ward. Register-only for now;
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 15f1cb57a2792167f63aa562ececc415c9943088)
+++ src/services/ActivityPubService.js	(revision 2a1044514fb6dd87ad43ab100954639ef3720d11)
@@ -2410,12 +2410,4 @@
     return `<p><video controls playsinline preload="metadata"${poster} src="${a.url}"></video></p>`;
   }).join('');
-  // The web tile (Robins aanwijzing, 30-7): a video post's first video
-  // becomes the cover video, so the grid plays it muted-looping as its own
-  // thumbnail, exactly like an animated cover. Deliberately NO cover image
-  // next to it: the tile's image branch would win and freeze the tile. A
-  // photo post gets its first image as cover, instead of the (untitled)
-  // gradient.
-  const coverVid = media.find((a) => a.mediaType.startsWith('video/'));
-  const coverImg = media.find((a) => a.mediaType.startsWith('image/'));
   const postId = crypto.randomUUID();
   const slug = 'n-' + postId.slice(0, 8);
@@ -2428,8 +2420,10 @@
   const vis = c2sVisibility(object);
   const fanOnly = (vis === 'friends' || vis === 'direct') ? 1 : 0;
-  db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, excerpt, status, type, language, fan_only, ap_visibility, cover_image_url, cover_video_url, created_at, updated_at, published_at)
-              VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
-    .run(postId, site.id, slug, user.id, '', html + mediaHtml, '', 'published', 'post', object.language || 'nl', fanOnly, vis,
-      coverVid ? null : (coverImg ? coverImg.url : null), coverVid ? coverVid.url : null, now, now, now);
+  // Deliberately NO cover (Robins besluit, 30-7): the media lives in the
+  // content, and a cover next to it showed the same video twice on the post
+  // page. The tiles derive their picture from the content instead.
+  db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, excerpt, status, type, language, fan_only, ap_visibility, created_at, updated_at, published_at)
+              VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
+    .run(postId, site.id, slug, user.id, '', html + mediaHtml, '', 'published', 'post', object.language || 'nl', fanOnly, vis, now, now, now);
   if (media.length) { try { db.prepare('UPDATE posts SET c2s_attachments = ? WHERE id = ?').run(JSON.stringify(media), postId); } catch { /* column exists via ensureColumn */ } }
   try { db.prepare('UPDATE posts SET content_rendered = ? WHERE id = ?').run(bakePostContent(html + mediaHtml), postId); } catch { /* render fallback covers it */ }
@@ -2437,5 +2431,5 @@
   try { db.prepare('INSERT INTO posts_fts(content, title, author, post_id) VALUES (?,?,?,?)').run(HtmlSanitizerService.toPlainText(html), '', user.username || '', postId); } catch { /* FTS non-fatal */ }
   if (vis !== 'direct') {
-    deliverCreate(site, { id: postId, slug, title: '', content: html + mediaHtml, published_at: now, created_at: now, fan_only: fanOnly, ap_visibility: vis, cover_image_url: coverVid ? null : (coverImg ? coverImg.url : null), cover_video_url: coverVid ? coverVid.url : null, c2s_attachments: media.length ? JSON.stringify(media) : null }).catch(() => { /* best-effort */ });
+    deliverCreate(site, { id: postId, slug, title: '', content: html + mediaHtml, published_at: now, created_at: now, fan_only: fanOnly, ap_visibility: vis, c2s_attachments: media.length ? JSON.stringify(media) : null }).catch(() => { /* best-effort */ });
   }
   return { status: 201, id: postId, url: `${base}/ap/notes/${postId}` };
Index: src/views/partials/post-card.ejs
===================================================================
--- src/views/partials/post-card.ejs	(revision 15f1cb57a2792167f63aa562ececc415c9943088)
+++ src/views/partials/post-card.ejs	(revision 2a1044514fb6dd87ad43ab100954639ef3720d11)
@@ -16,5 +16,14 @@
 const _ext       = !!_external;
 const _src       = post.source_name || '';
-const _hasCover  = !!post.cover_image_url || !!post.cover_video_url;
+const _realCover = !!post.cover_image_url || !!post.cover_video_url;
+// Content-derived fallback (see post-tile.ejs): a C2S post's media lives in
+// its content, and the card fronts it from there.
+let _cVideo = null, _cPoster = null, _cImg = null;
+if (!_realCover && post.content) {
+  const vm = String(post.content).match(/<video[^>]*\ssrc="([^"]+)"[^>]*>/i);
+  if (vm) { _cVideo = vm[1]; const pm = vm[0].match(/poster="([^"]+)"/i); if (pm) _cPoster = pm[1]; }
+  else { const im = String(post.content).match(/<img[^>]*\ssrc="([^"]+)"/i); if (im) _cImg = im[1]; }
+}
+const _hasCover  = _realCover || !!_cVideo || !!_cImg;
 const _typeLabel = (post.type && post.type !== 'overig' && post.type !== 'post') ? post.type : '';
 const _isPinned  = !!post.pinned;
@@ -53,5 +62,5 @@
            sizes="(min-width: 768px) 120px, 100vw"
            alt="" loading="lazy" decoding="async"<% if (post.cover_video_url) { %> data-ios-mp4="<%= post.cover_video_url %>"<% } %>>
-      <% } else if (post.cover_video_url) { %><video src="<%= post.cover_video_url %>" poster="<%= thumb(post.cover_video_url, 480) %>" autoplay loop muted playsinline></video><% } %>
+      <% } else if (post.cover_video_url) { %><video src="<%= post.cover_video_url %>" poster="<%= thumb(post.cover_video_url, 480) %>" autoplay loop muted playsinline></video><% } else if (_cVideo) { %><video src="<%= _cVideo %>"<% if (_cPoster) { %> poster="<%= _cPoster %>"<% } %> autoplay loop muted playsinline preload="metadata"></video><% } else if (_cImg) { %><img src="<%= _cImg %>" alt="" loading="lazy" decoding="async"><% } %>
       <% if (post.nsfw) { %><span class="nsfw-veil"><%- include('nsfw-veil', { cw: post.content_warning }) %></span><% } %>
     </a>
Index: src/views/partials/post-tile.ejs
===================================================================
--- src/views/partials/post-tile.ejs	(revision 15f1cb57a2792167f63aa562ececc415c9943088)
+++ src/views/partials/post-tile.ejs	(revision 2a1044514fb6dd87ad43ab100954639ef3720d11)
@@ -7,4 +7,15 @@
 const _isBoost = !!post.isBoost;
 const _hasCover = !!post.cover_image_url || !!post.cover_video_url;
+// A C2S post carries its media IN the content (a cover next to it showed the
+// video twice on the post page). The tile derives its picture from the
+// content instead, like the Cirkel card: first <video> (with its poster) or
+// first <img>.
+let _cVideo = null, _cPoster = null, _cImg = null;
+if (!_hasCover && post.content) {
+  const vm = String(post.content).match(/<video[^>]*\ssrc="([^"]+)"[^>]*>/i);
+  if (vm) { _cVideo = vm[1]; const pm = vm[0].match(/poster="([^"]+)"/i); if (pm) _cPoster = pm[1]; }
+  else { const im = String(post.content).match(/<img[^>]*\ssrc="([^"]+)"/i); if (im) _cImg = im[1]; }
+}
+const _showCover = _hasCover || !!_cVideo || !!_cImg;
 const _typeLabel = (post.type && post.type !== 'post') ? post.type : '';
 const _src = post.source_name || '';   // bron-site (cirkel-feed: van welke site komt deze post)
@@ -14,10 +25,10 @@
 const _ext = !!_external;
 %>
-<a class="grid-tile<%= _hasCover ? '' : ' grid-tile-gradient' %><%= (_isPinned || _isBoost) ? ' is-pinned' : '' %><%= post.nsfw ? ' nsfw-media' : '' %>"
+<a class="grid-tile<%= _showCover ? '' : ' grid-tile-gradient' %><%= (_isPinned || _isBoost) ? ' is-pinned' : '' %><%= post.nsfw ? ' nsfw-media' : '' %>"
    href="<%= _href %>"
    style="--tile-hue: <%= _tileHue %>;"
    <% if (_ext) { %>target="_blank" rel="noopener"<% } else { %>hx-get="<%= _href %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="<%= _href %>" hx-indicator="#pcms-loading"<% } %>>
 
-  <% if (post.cover_image_url) { %><img class="grid-tile-img" src="<%= thumb(post.cover_image_url, 480) %>" alt="" loading="lazy" decoding="async"<% if (post.cover_video_url) { %> data-ios-mp4="<%= post.cover_video_url %>"<% } %>><% } else if (post.cover_video_url) { %><video class="grid-tile-img" src="<%= post.cover_video_url %>" poster="<%= thumb(post.cover_video_url, 480) %>" autoplay loop muted playsinline></video><% } %>
+  <% if (post.cover_image_url) { %><img class="grid-tile-img" src="<%= thumb(post.cover_image_url, 480) %>" alt="" loading="lazy" decoding="async"<% if (post.cover_video_url) { %> data-ios-mp4="<%= post.cover_video_url %>"<% } %>><% } else if (post.cover_video_url) { %><video class="grid-tile-img" src="<%= post.cover_video_url %>" poster="<%= thumb(post.cover_video_url, 480) %>" autoplay loop muted playsinline></video><% } else if (_cVideo) { %><video class="grid-tile-img" src="<%= _cVideo %>"<% if (_cPoster) { %> poster="<%= _cPoster %>"<% } %> autoplay loop muted playsinline preload="metadata"></video><% } else if (_cImg) { %><img class="grid-tile-img" src="<%= _cImg %>" alt="" loading="lazy" decoding="async"><% } %>
 
   <% if (_typeLabel) { %>
