Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 92f6976f791f1d27ee577caf5bd62c219b25420b)
+++ src/config/database.js	(revision b7d4458e21f8eef0c969874d5d571662936d8702)
@@ -85,4 +85,5 @@
   ensureColumn('posts', 'fan_only', 'INTEGER DEFAULT 0');  // fan-only preview (premium #3)
   ensureColumn('posts', 'nsfw',     'INTEGER DEFAULT 0');  // sensitive content → blur + click-to-reveal; fediverse sensitive
+  ensureColumn('posts', 'content_warning', 'TEXT');        // custom CW label (empty = default "Gevoelige inhoud")
   ensureColumn('posts', 'type',    "TEXT DEFAULT 'post'");  // post | foto | video | audio
 
@@ -416,4 +417,6 @@
   ensureColumn('ap_timeline', 'boosted', 'INTEGER DEFAULT 0');
   ensureColumn('ap_timeline', 'liked', 'INTEGER DEFAULT 0'); // a feed post you liked (⭐) → toggle
+  ensureColumn('ap_timeline', 'nsfw', 'INTEGER DEFAULT 0');  // remote sensitive post → blur in the Cirkel
+  ensureColumn('ap_timeline', 'cw', 'TEXT');                 // remote content-warning text
 }
 
Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 92f6976f791f1d27ee577caf5bd62c219b25420b)
+++ src/routes/activitypub.js	(revision b7d4458e21f8eef0c969874d5d571662936d8702)
@@ -67,5 +67,5 @@
   if (!site) return res.status(404).end();
   const posts = db.prepare(
-    `SELECT id, slug, title, content, cover_image_url, nsfw, published_at, created_at
+    `SELECT id, slug, title, content, cover_image_url, nsfw, content_warning, published_at, created_at
      FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)
      ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20`
@@ -90,5 +90,5 @@
   // rank 1 last) → Mastodon flips it back to pin-rank ascending on the profile.
   const posts = db.prepare(
-    `SELECT id, slug, title, content, cover_image_url, nsfw, published_at, created_at
+    `SELECT id, slug, title, content, cover_image_url, nsfw, content_warning, published_at, created_at
      FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)
        AND pinned IS NOT NULL AND pinned > 0
Index: src/routes/circle.js
===================================================================
--- src/routes/circle.js	(revision 92f6976f791f1d27ee577caf5bd62c219b25420b)
+++ src/routes/circle.js	(revision b7d4458e21f8eef0c969874d5d571662936d8702)
@@ -53,4 +53,6 @@
       pinned: 0,
       isBoost: !!r.boosted, // a post YOU boosted → render in the pinned style with a Boost badge
+      nsfw: r.nsfw ? 1 : 0, // remote sensitive post → blur in the Cirkel (post-card/tile)
+      content_warning: r.cw || '',
       status: 'published',
       source_name: name,
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 92f6976f791f1d27ee577caf5bd62c219b25420b)
+++ src/routes/posts.js	(revision b7d4458e21f8eef0c969874d5d571662936d8702)
@@ -177,4 +177,5 @@
   const fanOnly = req.body.fan_only ? 1 : 0;
   const nsfw = req.body.nsfw ? 1 : 0;
+  const cw = (req.body.content_warning || '').trim().slice(0, 200);
 
   // Content arrives as user-authored HTML from the WYSIWYG editor — sanitize
@@ -214,7 +215,7 @@
     INSERT INTO posts (
       id, site_id, slug, author_id, title, content, excerpt,
-      status, cover_image_url, pinned, tags, type, noindex, fan_only, nsfw, publish_at,
+      status, cover_image_url, pinned, tags, type, noindex, fan_only, nsfw, content_warning, publish_at,
       created_at, updated_at, published_at
-    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
   `).run(
     postId, site.id, finalSlug, req.session.user.id,
@@ -222,5 +223,5 @@
     finalStatus, cover_image_url || null, parsePinnedRank(pinned),
     JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
-    finalType, noindex ? 1 : 0, fanOnly, nsfw, publishAt,
+    finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, publishAt,
     now, now, publishedAt
   );
@@ -239,5 +240,5 @@
         id: postId, slug: finalSlug, title: title || finalSlug,
         content: cleanContent, cover_image_url: cover_image_url || null,
-        published_at: publishedAt, created_at: now, fan_only: fanOnly, nsfw,
+        published_at: publishedAt, created_at: now, fan_only: fanOnly, nsfw, content_warning: cw,
       }).catch(() => { /* best-effort */ });
     }
@@ -298,4 +299,5 @@
   const fanOnly = req.body.fan_only ? 1 : 0;
   const nsfw = req.body.nsfw ? 1 : 0;
+  const cw = (req.body.content_warning || '').trim().slice(0, 200);
   const newSlug = req.body.slug;
   const action = req.body.action || 'save';
@@ -336,5 +338,5 @@
       title = ?, content = ?, excerpt = ?, status = ?,
       cover_image_url = ?, pinned = ?, tags = ?,
-      type = ?, noindex = ?, fan_only = ?, nsfw = ?, publish_at = ?,
+      type = ?, noindex = ?, fan_only = ?, nsfw = ?, content_warning = ?, publish_at = ?,
       slug = ?, published_at = ?, updated_at = ?
     WHERE id = ?
@@ -343,5 +345,5 @@
     cover_image_url || null, parsePinnedRank(pinned),
     JSON.stringify((tags || '').split(',').map(t => t.trim()).filter(Boolean)),
-    finalType, noindex ? 1 : 0, fanOnly, nsfw, publishAt,
+    finalType, noindex ? 1 : 0, fanOnly, nsfw, cw, publishAt,
     finalSlug, publishedAt, now, post.id
   );
@@ -364,5 +366,5 @@
       id: post.id, slug: finalSlug, title: title || finalSlug,
       content: cleanContent, cover_image_url: cover_image_url || null,
-      published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly, nsfw,
+      published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly, nsfw, content_warning: cw,
     };
     if (post.status !== 'published') ActivityPubService.deliverCreate(site, apPost).catch(() => { /* best-effort */ });
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 92f6976f791f1d27ee577caf5bd62c219b25420b)
+++ src/services/ActivityPubService.js	(revision b7d4458e21f8eef0c969874d5d571662936d8702)
@@ -250,5 +250,5 @@
     sensitive: !!post.nsfw,
   };
-  if (post.nsfw) note.summary = 'Gevoelige inhoud';
+  if (post.nsfw) note.summary = post.content_warning || 'Gevoelige inhoud';
   if (attachment.length) note.attachment = attachment;
   // Playable-audio posts suppress the cover attachment (player card). Still expose
@@ -708,5 +708,5 @@
         // the timeline).
         for (const s of subs) {
-          tlStmts().ins.run(o.id, s.slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.url || null, o.published || null, media);
+          tlStmts().ins.run(o.id, s.slug, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.url || null, o.published || null, media, o.sensitive ? 1 : 0, o.summary || null);
         }
         console.log('[AP] timeline +', actorUri, 'x' + subs.length);
@@ -775,5 +775,5 @@
   if (!site) return;
   const recent = db.prepare(
-    `SELECT id, slug, title, content, cover_image_url, nsfw, published_at, created_at
+    `SELECT id, slug, title, content, cover_image_url, nsfw, content_warning, published_at, created_at
      FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)
      ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20`
@@ -1011,4 +1011,6 @@
     url: note.url || url,
     content: HtmlSanitizerService.sanitize(rawHtml),       // full, sanitized
+    sensitive: !!note.sensitive,                            // remote CW → blur in the Cirkel
+    cw: note.summary || '',
     images,
     threadInboxes,                                          // every ancestor author's inbox
@@ -1083,5 +1085,5 @@
 function tlStmts() {
   if (!_insTl) {
-    _insTl = db.prepare('INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, author_handle, author_icon, author_url, content, url, published, media_json, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)');
+    _insTl = db.prepare('INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, author_handle, author_icon, author_url, content, url, published, media_json, nsfw, cw, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)');
     _listTl = db.prepare('SELECT * FROM ap_timeline WHERE slug = ? ORDER BY COALESCE(published, created_at) DESC LIMIT ?');
     _delTl = db.prepare('DELETE FROM ap_timeline WHERE id = ?');
@@ -1102,5 +1104,5 @@
     if (!_cirkelPosts) _cirkelPosts = db.prepare(`
       SELECT t.id, t.author_uri, t.author_name, t.author_handle, t.author_icon, t.author_url,
-             t.content, t.url, t.published, t.media_json, t.boosted
+             t.content, t.url, t.published, t.media_json, t.boosted, t.nsfw, t.cw
       FROM ap_timeline t
       LEFT JOIN ap_following f ON f.slug = t.slug AND f.actor_uri = t.author_uri
@@ -1142,5 +1144,5 @@
     tlStmts().ins.run(id, slug, note.actor_uri || '', note.actor_name || '', note.actor_handle || '',
       note.actor_icon || '', note.actor_url || '', note.content || '', note.url || null,
-      new Date().toISOString(), media);
+      new Date().toISOString(), media, note.sensitive ? 1 : 0, note.cw || null);
   } catch { /* ignore */ }
   markBoosted(slug, id);
Index: src/services/Scheduler.js
===================================================================
--- src/services/Scheduler.js	(revision 92f6976f791f1d27ee577caf5bd62c219b25420b)
+++ src/services/Scheduler.js	(revision b7d4458e21f8eef0c969874d5d571662936d8702)
@@ -15,5 +15,5 @@
   try {
     const due = db.prepare(`
-      SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.fan_only, p.nsfw,
+      SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.fan_only, p.nsfw, p.content_warning,
              p.published_at, p.publish_at, p.created_at, u.username
       FROM posts p JOIN users u ON u.id = p.author_id
@@ -39,5 +39,5 @@
             id: p.id, slug: p.slug, title: p.title || p.slug,
             content: p.content, cover_image_url: p.cover_image_url || null,
-            published_at: p.published_at || p.publish_at, created_at: p.created_at, fan_only: p.fan_only, nsfw: p.nsfw,
+            published_at: p.published_at || p.publish_at, created_at: p.created_at, fan_only: p.fan_only, nsfw: p.nsfw, content_warning: p.content_warning,
           }).catch(() => { /* best-effort */ });
         }
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision 92f6976f791f1d27ee577caf5bd62c219b25420b)
+++ src/services/i18n.js	(revision b7d4458e21f8eef0c969874d5d571662936d8702)
@@ -771,5 +771,5 @@
     'pedit.pin_top': 'bovenaan',
     'pedit.pin_nth_suffix': 'e van boven',
-    'pedit.noindex_label': 'noindex (verberg voor zoekmachines)', 'pedit.nsfw_label': 'NSFW / gevoelige inhoud', 'post.nsfw_warning': 'Gevoelige inhoud', 'post.nsfw_show': 'Tonen',
+    'pedit.noindex_label': 'noindex (verberg voor zoekmachines)', 'pedit.nsfw_label': 'NSFW / gevoelige inhoud', 'pedit.nsfw_cw_ph': 'Waarschuwingstekst (optioneel, standaard: Gevoelige inhoud)', 'post.nsfw_warning': 'Gevoelige inhoud', 'post.nsfw_show': 'Tonen',
     'pedit.fan_only_label': 'Alleen voor ingelogde fans',
     'pedit.schedule_label': 'Publicatie inplannen',
@@ -1782,5 +1782,5 @@
     'pedit.pin_top': 'at the top',
     'pedit.pin_nth_suffix': 'th from top',
-    'pedit.noindex_label': 'noindex (hide from search engines)', 'pedit.nsfw_label': 'NSFW / sensitive content', 'post.nsfw_warning': 'Sensitive content', 'post.nsfw_show': 'Show',
+    'pedit.noindex_label': 'noindex (hide from search engines)', 'pedit.nsfw_label': 'NSFW / sensitive content', 'pedit.nsfw_cw_ph': 'Warning text (optional, default: Sensitive content)', 'post.nsfw_warning': 'Sensitive content', 'post.nsfw_show': 'Show',
     'pedit.fan_only_label': 'Logged-in fans only',
     'pedit.schedule_label': 'Schedule publication',
@@ -2793,5 +2793,5 @@
     'pedit.pin_top': 'ganz oben',
     'pedit.pin_nth_suffix': '. von oben',
-    'pedit.noindex_label': 'noindex (vor Suchmaschinen verbergen)', 'pedit.nsfw_label': 'NSFW / sensibler Inhalt', 'post.nsfw_warning': 'Sensibler Inhalt', 'post.nsfw_show': 'Anzeigen',
+    'pedit.noindex_label': 'noindex (vor Suchmaschinen verbergen)', 'pedit.nsfw_label': 'NSFW / sensibler Inhalt', 'pedit.nsfw_cw_ph': 'Warntext (optional, Standard: Sensibler Inhalt)', 'post.nsfw_warning': 'Sensibler Inhalt', 'post.nsfw_show': 'Anzeigen',
     'pedit.fan_only_label': 'Nur für angemeldete Fans',
     'pedit.schedule_label': 'Veröffentlichung planen',
Index: src/views/pages/post-edit.ejs
===================================================================
--- src/views/pages/post-edit.ejs	(revision 92f6976f791f1d27ee577caf5bd62c219b25420b)
+++ src/views/pages/post-edit.ejs	(revision b7d4458e21f8eef0c969874d5d571662936d8702)
@@ -242,4 +242,7 @@
           <span>🔞 <%= t('pedit.nsfw_label') %></span>
         </label>
+        <input type="text" name="content_warning" value="<%= post.content_warning || '' %>" maxlength="200"
+               placeholder="<%= t('pedit.nsfw_cw_ph') %>"
+               style="width:100%;box-sizing:border-box;margin:6px 0 2px;font-size:13px;padding:7px 9px">
         <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
           <label class="pe-checkbox">
Index: src/views/pages/post.ejs
===================================================================
--- src/views/pages/post.ejs	(revision 92f6976f791f1d27ee577caf5bd62c219b25420b)
+++ src/views/pages/post.ejs	(revision b7d4458e21f8eef0c969874d5d571662936d8702)
@@ -13,5 +13,5 @@
     <div class="nsfw-banner">
       <span class="nsfw-banner-icon">🔞</span>
-      <span class="nsfw-banner-text"><%= t('post.nsfw_warning') %></span>
+      <span class="nsfw-banner-text"><%= post.content_warning || t('post.nsfw_warning') %></span>
       <button type="button" class="nsfw-banner-btn nsfw-reveal"><%= t('post.nsfw_show') %></button>
     </div>
Index: src/views/partials/post-card.ejs
===================================================================
--- src/views/partials/post-card.ejs	(revision 92f6976f791f1d27ee577caf5bd62c219b25420b)
+++ src/views/partials/post-card.ejs	(revision b7d4458e21f8eef0c969874d5d571662936d8702)
@@ -50,5 +50,5 @@
        aria-label="<%= post.title || '(zonder titel)' %>" tabindex="-1">
       <img src="<%= post.cover_image_url %>" alt="" loading="lazy" decoding="async">
-      <% if (post.nsfw) { %><span class="nsfw-veil"><span class="nsfw-veil-label">🔞 <%= t('post.nsfw_warning') %></span><span class="nsfw-veil-btn"><%= t('post.nsfw_show') %></span></span><% } %>
+      <% if (post.nsfw) { %><span class="nsfw-veil"><span class="nsfw-veil-label">🔞 <%= post.content_warning || t('post.nsfw_warning') %></span><span class="nsfw-veil-btn"><%= t('post.nsfw_show') %></span></span><% } %>
     </a>
   <% } %>
Index: src/views/partials/post-tile.ejs
===================================================================
--- src/views/partials/post-tile.ejs	(revision 92f6976f791f1d27ee577caf5bd62c219b25420b)
+++ src/views/partials/post-tile.ejs	(revision b7d4458e21f8eef0c969874d5d571662936d8702)
@@ -37,4 +37,4 @@
     <% if (_src) { %><span class="grid-tile-gradient-src">van <%= _src %></span><% } %>
   <% } %>
-  <% if (post.nsfw) { %><div class="nsfw-veil"><span class="nsfw-veil-label">🔞 <%= t('post.nsfw_warning') %></span><span class="nsfw-veil-btn"><%= t('post.nsfw_show') %></span></div><% } %>
+  <% if (post.nsfw) { %><div class="nsfw-veil"><span class="nsfw-veil-label">🔞 <%= post.content_warning || t('post.nsfw_warning') %></span><span class="nsfw-veil-btn"><%= t('post.nsfw_show') %></span></div><% } %>
 </a>
