Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ src/config/database.js	(revision 8f2f97ce2ca4540506ee22910e99a093826a9555)
@@ -47,6 +47,4 @@
   // Site-level moderation toggle. 'trust' = auto-approve, 'moderate' = pending until reviewed.
   ensureColumn('sites', 'comments_moderation_mode', "TEXT DEFAULT 'moderate'");
-  // Per-site Prutter toggle: when off, DM endpoints/UI are hidden for that site.
-  ensureColumn('sites', 'enable_prutter', 'INTEGER DEFAULT 1');
   // Cirkels: mag deze site in cirkels van anderen verschijnen (surfacing opt-out).
   ensureColumn('sites', 'allow_circle', 'INTEGER DEFAULT 1');
Index: src/routes/admin-sites.js
===================================================================
--- src/routes/admin-sites.js	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ src/routes/admin-sites.js	(revision 8f2f97ce2ca4540506ee22910e99a093826a9555)
@@ -98,5 +98,5 @@
 const RESERVED_SITE_SLUGS = new Set([
   'auth', 'admin', 'login', 'register', 'logout', 'archive', 'search',
-  'account', 'sites', 'comments', 'posts', 'media', 'audio', 'prutter',
+  'account', 'sites', 'comments', 'posts', 'media', 'audio',
   'forum', 'tag', 'user', 'users', 'artiesten', 'leden', 'feed.xml', 'atom.xml', 'sitemap.xml',
   'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets',
@@ -119,5 +119,4 @@
     require_login_to_comment: 1,
     enable_audio_player: 1,
-    enable_prutter: 1,
     comments_moderation_mode: 'moderate',
     feed_view_default: 'grid',
@@ -300,5 +299,5 @@
       profile_links = ?,
       is_public = ?, robots_index = ?, require_login_to_comment = ?,
-      enable_audio_player = ?, enable_prutter = ?,
+      enable_audio_player = ?,
       comments_moderation_mode = ?,
       feed_view_default = ?, feed_view_switch = ?,
@@ -322,5 +321,4 @@
     f.require_login_to_comment ? 1 : 0,
     f.enable_audio_player ? 1 : 0,
-    f.enable_prutter ? 1 : 0,
     moderationMode,
     feedViewDef,
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ src/routes/posts.js	(revision 8f2f97ce2ca4540506ee22910e99a093826a9555)
@@ -81,5 +81,5 @@
   'auth', 'admin', 'login', 'register', 'logout',
   'archive', 'search', 'account', 'sites', 'comments',
-  'posts', 'media', 'audio', 'prutter', 'forum',
+  'posts', 'media', 'audio', 'forum',
   'tag', 'type', 'user', 'users', 'artiesten', 'leden', 'favorieten', 'feed.xml', 'atom.xml', 'sitemap.xml',
   'manifest.webmanifest', 'sw.js', 'favicon.ico', 'favicon.svg', 'assets',
Index: src/routes/prutter.js
===================================================================
--- src/routes/prutter.js	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ 	(revision )
@@ -1,156 +1,0 @@
-/**
- * Prutter — Direct Messaging routes.
- *
- * GET  /prutter                   -> inbox (list of your conversations on THIS site)
- * GET  /prutter/new?to=<username> -> start (or resume) a conversation, redirects to /prutter/:id
- * GET  /prutter/:id               -> conversation view (messages + send form)
- * POST /prutter/:id/send          -> send a message (HTMX-friendly response)
- *
- * Scoping: conversations are per-site (PrutterService.getOrCreateConversation
- * uses res.locals.site.id). Robin's quote: "Prutter = DM (per .com domain)".
- *
- * Per-site toggle: site.enable_prutter == 0 -> 404 the whole feature.
- *
- * No anonymous DMs — always requires login.
- */
-
-import express from 'express';
-import db from '../config/database.js';
-import { renderPage } from '../middleware/render.js';
-import { requireAuth, isViewer } from '../middleware/auth.js';
-import { getTenancy } from '../services/SettingsService.js';
-import { premiumUnlocked } from '../services/PatreonService.js';
-import PermissionsService from '../services/PermissionsService.js';
-
-const router = express.Router();
-
-const MAX_MESSAGE_LEN = 2000;
-
-function siteAllowsDM(req, res) {
-  const site = res.locals.site;
-  if (!site) return false;
-  return site.enable_prutter !== 0;
-}
-
-// Middleware: Prutter = Hub-feature achter de premium-laag, plus login.
-//  - alleen in Hub-modus (DM tussen artiesten/leden van het collectief)
-//  - vergrendeld als de premium-laag aan staat maar Patreon niet gekoppeld is
-//    (premium uit = niet gegate → huidige gedrag, demo's blijven werken)
-//  - geen anonieme DMs
-function requirePrutter(req, res, next) {
-  if (!siteAllowsDM(req, res)) return res.status(404).send('Prutter not enabled on this site');
-  if (getTenancy() !== 'hub') return res.status(404).send('Prutter is alleen beschikbaar in Hub-modus');
-  if (!premiumUnlocked()) {
-    return res.status(403).send('Prutter is een premium-functie — koppel Patreon in Beheer → Instellingen.');
-  }
-  return requireAuth(req, res, next);
-}
-
-// ==================== INBOX ====================
-router.get('/', requirePrutter, (req, res) => {
-  const prutter = req.app.locals.prutter;
-  const conversations = prutter.getUserConversations(req.session.user.id);
-
-  // Filter to only conversations on THIS site (Prutter scope is per-site)
-  const siteId = res.locals.site.id;
-  const scoped = conversations.filter(c => c.site_id === siteId);
-
-  renderPage(req, res, 'pages/prutter-inbox', {
-    pageTitle: 'Prutter',
-    bodyClass: 'on-special',
-    conversations: scoped,
-  });
-});
-
-// ==================== START / RESUME CONVERSATION ====================
-router.get('/new', requirePrutter, (req, res) => {
-  // Een gesprek starten is een schrijf-actie (INSERT) — een kijker mag dat niet.
-  // De globale guard pakt dit niet omdat het een GET is, dus expliciet blokkeren.
-  if (isViewer(req.session.user)) {
-    res.status(403);
-    return renderPage(req, res, 'pages/viewer-blocked', { pageTitle: 'Kijker-modus', bodyClass: 'on-special' });
-  }
-  const targetUsername = (req.query.to || '').toString().trim();
-  if (!targetUsername) {
-    return res.redirect(`${res.locals.siteUrlBase || ''}/prutter`);
-  }
-  const target = db.prepare('SELECT id, username FROM users WHERE username = ?').get(targetUsername);
-  if (!target) return res.status(404).send('User not found');
-  if (target.id === req.session.user.id) {
-    return res.redirect(`${res.locals.siteUrlBase || ''}/prutter`);
-  }
-
-  const prutter = req.app.locals.prutter;
-  const conv = prutter.getOrCreateConversation(req.session.user.id, target.id, res.locals.site.id);
-  res.redirect(`${res.locals.siteUrlBase || ''}/prutter/${conv.id}`);
-});
-
-// ==================== CONVERSATION VIEW ====================
-router.get('/:id', requirePrutter, (req, res) => {
-  const prutter = req.app.locals.prutter;
-  const conv = db.prepare('SELECT * FROM conversations WHERE id = ?').get(req.params.id);
-  if (!conv) return res.status(404).send('Conversation not found');
-
-  // Auth: must be a participant
-  const me = req.session.user.id;
-  if (conv.user_a_id !== me && conv.user_b_id !== me) return res.status(403).send('Not a participant');
-
-  // Scope: this conversation must belong to the resolved site
-  if (conv.site_id !== res.locals.site.id) return res.status(404).send('Conversation not on this site');
-
-  // Other party
-  const otherId = conv.user_a_id === me ? conv.user_b_id : conv.user_a_id;
-  const other = db.prepare('SELECT id, username, avatar_url FROM users WHERE id = ?').get(otherId);
-
-  // Messages (oldest first for natural reading order)
-  const messages = prutter.getMessages(conv.id, 200, 0).reverse();
-
-  // Mark inbound messages as read — sla over voor kijkers (markAsRead is een
-  // UPDATE; een GET valt buiten de globale guard, dus hier expliciet skippen).
-  if (!isViewer(req.session.user)) prutter.markAsRead(conv.id, me);
-
-  renderPage(req, res, 'pages/prutter-conversation', {
-    pageTitle: 'Prutter — ' + (other?.username || ''),
-    // on-chat → full-height chat-view (geen artiest-profielkop, alleen de thread
-    // scrollt). Zie chrome.ejs (_headerless) + de page-CSS hieronder.
-    bodyClass: 'on-special on-chat',
-    conversation: conv,
-    other,
-    messages,
-  });
-});
-
-// ==================== SEND MESSAGE ====================
-router.post('/:id/send', requirePrutter, (req, res) => {
-  const prutter = req.app.locals.prutter;
-  const conv = db.prepare('SELECT * FROM conversations WHERE id = ?').get(req.params.id);
-  if (!conv) return res.status(404).send('Not found');
-
-  const me = req.session.user.id;
-  if (conv.user_a_id !== me && conv.user_b_id !== me) return res.status(403).send('Not a participant');
-  if (conv.site_id !== res.locals.site.id) return res.status(404).send('Wrong site');
-
-  const content = (req.body.content || '').toString().trim();
-  if (!content) return res.status(400).send('Empty');
-  if (content.length > MAX_MESSAGE_LEN) return res.status(413).send('Too long');
-
-  const message = prutter.sendMessage(conv.id, me, content);
-
-  // HTMX request → return the single rendered message HTML, appended to the thread
-  if (req.headers['hx-request']) {
-    return res.send(
-      `<li class="prutter-msg prutter-msg--mine" data-msg-id="${message.id}">` +
-      `<div class="prutter-msg-bubble">${escapeHtml(content)}</div>` +
-      `</li>`
-    );
-  }
-
-  res.redirect(`${res.locals.siteUrlBase || ''}/prutter/${conv.id}`);
-});
-
-function escapeHtml(s) {
-  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
-          .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
-}
-
-export default router;
Index: src/server.js
===================================================================
--- src/server.js	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ src/server.js	(revision 8f2f97ce2ca4540506ee22910e99a093826a9555)
@@ -19,7 +19,4 @@
 import { SqliteSessionStore } from './services/SqliteSessionStore.js';
 import { ensurePrimarySite } from './services/ensurePrimarySite.js';
-import PrutterService from './services/PrutterService.js';
-import { WebSocketServer } from 'ws';
-
 import { resolveSite, loadAudioTracks, loadTheme } from './middleware/site.js';
 import { isViewer } from './middleware/auth.js';
@@ -36,5 +33,4 @@
 import adminSettingsRoutes from './routes/admin-settings.js';
 import adminSeoRoutes from './routes/admin-seo.js';
-import prutterRoutes from './routes/prutter.js';
 import audioRoutes from './routes/audio.js';
 import searchRoutes from './routes/search.js';
@@ -206,8 +202,4 @@
 })();
 
-// Singleton PrutterService — routes get it via req.app.locals.prutter.
-const prutter = new PrutterService(db);
-app.locals.prutter = prutter;
-
 // Cirkels-federatie: publieke, site-agnostische endpoints (/.klonkt/*).
 // Vóór resolveSite/theme — ze hebben geen site-context nodig.
@@ -274,5 +266,4 @@
 app.use('/admin/epk', adminEpkRoutes);
 app.use('/admin', adminRoutes);
-app.use('/prutter', prutterRoutes);
 app.use('/audio', audioRoutes);
 app.use('/search', searchRoutes);
@@ -422,55 +413,4 @@
 });
 
-// ==================== WebSocket: Prutter real-time ====================
-// Authenticate via the existing session cookie. We reuse sessionMiddleware
-// during the HTTP upgrade so req.session is populated; if no user, abort.
-const wss = new WebSocketServer({ noServer: true });
-
-server.on('upgrade', (req, socket, head) => {
-  if (req.url !== '/ws/prutter') {
-    socket.destroy();
-    return;
-  }
-  // Run session middleware on the upgrade request.
-  // (Express's middleware accepts (req, res, next); we pass a stub res.)
-  const stubRes = { setHeader: () => {}, getHeader: () => undefined, on: () => {}, end: () => {} };
-  sessionMiddleware(req, stubRes, () => {
-    if (!req.session?.user) {
-      socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
-      socket.destroy();
-      return;
-    }
-    // Kijker-accounts zijn alleen-lezen: weiger de WS-upgrade. De HTTP-guard
-    // dekt geen WS, dus dit is de plek om schrijven via een (toekomstige)
-    // message-handler te voorkomen.
-    if (isViewer(req.session.user)) {
-      socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
-      socket.destroy();
-      return;
-    }
-    wss.handleUpgrade(req, socket, head, (ws) => {
-      ws.userId = req.session.user.id;
-      wss.emit('connection', ws, req);
-    });
-  });
-});
-
-wss.on('connection', (ws) => {
-  prutter.addConnection(ws.userId, ws);
-  ws.on('close', () => prutter.removeConnection(ws.userId, ws));
-  ws.on('error', () => prutter.removeConnection(ws.userId, ws));
-  // Optional: ping every 30s to keep connections alive through proxies
-  ws.isAlive = true;
-  ws.on('pong', () => { ws.isAlive = true; });
-});
-const wsPing = setInterval(() => {
-  for (const ws of wss.clients) {
-    if (ws.isAlive === false) { ws.terminate(); continue; }
-    ws.isAlive = false;
-    try { ws.ping(); } catch {}
-  }
-}, 30000);
-if (wsPing.unref) wsPing.unref();
-
 server.listen(PORT, () => {
   console.log('');
@@ -483,5 +423,4 @@
   console.log(`   ✓ Auth:     wachtwoord (beheer) + Google (luisteraars) / logout`);
   console.log(`   ✓ Posts:    create / edit / view / archive`);
-  console.log(`   ✓ Realtime: WebSocket server ready (Prutter)`);
   console.log('');
   console.log(`   Mode: ${isDev ? 'development' : 'PRODUCTION'}`);
Index: src/services/PrutterService.js
===================================================================
--- src/services/PrutterService.js	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ 	(revision )
@@ -1,196 +1,0 @@
-/**
- * PrutterService — Real-time Direct Messaging
- * 
- * Features:
- * - Per-conversation (user A ↔ user B)
- * - Optional site-specific (conversations tied to a community)
- * - WebSocket real-time notifications
- * - Message history in SQLite
- * - Unread message tracking
- */
-
-import { v4 as uuid } from 'uuid';
-
-class PrutterService {
-  constructor(db) {
-    this.db = db;
-    this.wsConnections = new Map(); // userId → Set<WebSocket>
-  }
-
-  /**
-   * Get or create conversation
-   */
-  getOrCreateConversation(userA, userB, siteId = null) {
-    if (!userA || !userB) throw new Error('Both users required');
-    
-    // Normalize order: always smaller ID first
-    const [u1, u2] = userA < userB ? [userA, userB] : [userB, userA];
-
-    const existing = this.db.prepare(`
-      SELECT * FROM conversations
-      WHERE (user_a_id = ? AND user_b_id = ? AND site_id IS ?)
-      LIMIT 1
-    `).get(u1, u2, siteId);
-
-    if (existing) {
-      return existing;
-    }
-
-    const convId = uuid();
-    this.db.prepare(`
-      INSERT INTO conversations (id, user_a_id, user_b_id, site_id)
-      VALUES (?, ?, ?, ?)
-    `).run(convId, u1, u2, siteId);
-
-    return { id: convId, user_a_id: u1, user_b_id: u2, site_id: siteId };
-  }
-
-  /**
-   * Send message
-   */
-  sendMessage(conversationId, authorId, content) {
-    if (!conversationId || !authorId || !content) {
-      throw new Error('Missing required fields');
-    }
-
-    const msgId = uuid();
-    const now = new Date().toISOString();
-
-    this.db.prepare(`
-      INSERT INTO messages (id, conversation_id, author_id, content, created_at)
-      VALUES (?, ?, ?, ?, ?)
-    `).run(msgId, conversationId, authorId, content, now);
-
-    // Update last_message_at on conversation
-    this.db.prepare(`
-      UPDATE conversations SET last_message_at = ? WHERE id = ?
-    `).run(now, conversationId);
-
-    // Fetch full message for response
-    const message = this.db.prepare(`
-      SELECT m.*, u.username, u.avatar_url
-      FROM messages m
-      JOIN users u ON m.author_id = u.id
-      WHERE m.id = ?
-    `).get(msgId);
-
-    // Notify recipient via WebSocket (if online)
-    const conv = this.db.prepare('SELECT * FROM conversations WHERE id = ?').get(conversationId);
-    const recipientId = conv.user_a_id === authorId ? conv.user_b_id : conv.user_a_id;
-    
-    this.notifyUser(recipientId, {
-      type: 'new_message',
-      conversationId,
-      message
-    });
-
-    return message;
-  }
-
-  /**
-   * Get conversation messages
-   */
-  getMessages(conversationId, limit = 50, offset = 0) {
-    return this.db.prepare(`
-      SELECT m.*, u.username, u.avatar_url
-      FROM messages m
-      JOIN users u ON m.author_id = u.id
-      WHERE m.conversation_id = ?
-      ORDER BY m.created_at DESC
-      LIMIT ? OFFSET ?
-    `).all(conversationId, limit, offset);
-  }
-
-  /**
-   * Get user's conversations (list)
-   */
-  getUserConversations(userId) {
-    return this.db.prepare(`
-      SELECT c.*,
-             CASE 
-               WHEN c.user_a_id = ? THEN u2.id
-               ELSE u1.id
-             END as other_user_id,
-             CASE 
-               WHEN c.user_a_id = ? THEN u2.username
-               ELSE u1.username
-             END as other_username,
-             CASE 
-               WHEN c.user_a_id = ? THEN u2.avatar_url
-               ELSE u1.avatar_url
-             END as other_avatar,
-             (SELECT COUNT(*) FROM messages m 
-              WHERE m.conversation_id = c.id 
-              AND m.author_id != ? 
-              AND m.read_at IS NULL) as unread_count,
-             (SELECT content FROM messages m 
-              WHERE m.conversation_id = c.id 
-              ORDER BY m.created_at DESC LIMIT 1) as last_message_preview
-      FROM conversations c
-      JOIN users u1 ON c.user_a_id = u1.id
-      JOIN users u2 ON c.user_b_id = u2.id
-      WHERE c.user_a_id = ? OR c.user_b_id = ?
-      ORDER BY c.last_message_at DESC
-    `).all(userId, userId, userId, userId, userId, userId);
-  }
-
-  /**
-   * Mark conversation messages as read
-   */
-  markAsRead(conversationId, userId) {
-    const now = new Date().toISOString();
-    this.db.prepare(`
-      UPDATE messages
-      SET read_at = ?
-      WHERE conversation_id = ? AND author_id != ? AND read_at IS NULL
-    `).run(now, conversationId, userId);
-  }
-
-  /**
-   * WebSocket connection management
-   */
-  addConnection(userId, ws) {
-    if (!this.wsConnections.has(userId)) {
-      this.wsConnections.set(userId, new Set());
-    }
-    this.wsConnections.get(userId).add(ws);
-  }
-
-  removeConnection(userId, ws) {
-    const conns = this.wsConnections.get(userId);
-    if (conns) {
-      conns.delete(ws);
-      if (conns.size === 0) {
-        this.wsConnections.delete(userId);
-      }
-    }
-  }
-
-  /**
-   * Notify user via WebSocket (if online)
-   */
-  notifyUser(userId, message) {
-    const conns = this.wsConnections.get(userId);
-    if (!conns) return;
-
-    const data = JSON.stringify(message);
-    for (const ws of conns) {
-      if (ws.readyState === 1) { // OPEN
-        ws.send(data);
-      }
-    }
-  }
-
-  /**
-   * Broadcast to all users in conversation (except sender)
-   */
-  broadcastToConversation(conversationId, senderUserId, message) {
-    const conv = this.db.prepare('SELECT * FROM conversations WHERE id = ?').get(conversationId);
-    if (!conv) return;
-
-    const otherUserId = conv.user_a_id === senderUserId ? conv.user_b_id : conv.user_a_id;
-    this.notifyUser(otherUserId, message);
-  }
-}
-
-export default PrutterService;
Index: src/views/pages/admin-site-edit.ejs
===================================================================
--- src/views/pages/admin-site-edit.ejs	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ src/views/pages/admin-site-edit.ejs	(revision 8f2f97ce2ca4540506ee22910e99a093826a9555)
@@ -163,11 +163,4 @@
         <span><%= t('asite.enable_audio') %></span>
       </label>
-      <%# Prutter is een Hub-only premium-functie — toggle alleen tonen in Hub-modus. %>
-      <% if (typeof tenancy !== 'undefined' && tenancy === 'hub') { %>
-      <label class="cb">
-        <input type="checkbox" name="enable_prutter" value="1" <%= site.enable_prutter ? 'checked' : '' %>>
-        <span><%= t('asite.enable_prutter') %> <small class="form-hint-inline"><%= t('asite.enable_prutter_hint') %></small></span>
-      </label>
-      <% } %>
     </fieldset>
 
Index: src/views/pages/prutter-conversation.ejs
===================================================================
--- src/views/pages/prutter-conversation.ejs	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ 	(revision )
@@ -1,257 +1,0 @@
-<div class="prutter-conv-page" data-conversation-id="<%= conversation.id %>" data-me="<%= user.id %>">
-  <header class="prutter-conv-header">
-    <a class="prutter-back" href="<%= siteUrlBase %>/prutter" aria-label="<%= t('prcv.back_aria') %>" title="<%= t('prcv.inbox') %>">
-      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
-    </a>
-    <div class="prutter-conv-avatar">
-      <% if (other && other.avatar_url) { %>
-        <img src="<%= other.avatar_url %>" alt="">
-      <% } else { %>
-        <span><%= (other && other.username || '?').charAt(0).toUpperCase() %></span>
-      <% } %>
-    </div>
-    <div class="prutter-conv-id">
-      <span class="prutter-conv-name"><%= other ? other.username : t('prcv.unknown') %></span>
-      <% if (other) { %>
-        <a class="prutter-conv-sub" href="/users/<%= encodeURIComponent(other.username) %>"><%= t('prcv.view_profile') %></a>
-      <% } %>
-    </div>
-  </header>
-
-  <ol class="prutter-thread" id="prutter-thread">
-    <% messages.forEach(function(m) { %>
-      <li class="prutter-msg <%= m.author_id === user.id ? 'prutter-msg--mine' : 'prutter-msg--theirs' %>" data-msg-id="<%= m.id %>">
-        <div class="prutter-msg-bubble"><%= m.content %></div>
-        <div class="prutter-msg-time" title="<%= m.created_at %>"><%= formatDateTime(m.created_at) %></div>
-      </li>
-    <% }); %>
-  </ol>
-
-  <form class="prutter-composer" id="prutter-composer"
-        method="post"
-        action="<%= siteUrlBase %>/prutter/<%= conversation.id %>/send"
-        hx-post="<%= siteUrlBase %>/prutter/<%= conversation.id %>/send"
-        hx-target="#prutter-thread"
-        hx-swap="beforeend">
-    <textarea name="content" rows="1" maxlength="2000" placeholder="<%= t('prcv.placeholder') %>" required></textarea>
-    <button type="submit" class="btn btn-primary"><%= t('prcv.send') %></button>
-  </form>
-</div>
-
-<script>
-(function() {
-  // Auto-scroll to bottom on load and after appends.
-  var thread = document.getElementById('prutter-thread');
-  function scrollToBottom() { thread.scrollTop = thread.scrollHeight; }
-  scrollToBottom();
-  window.__prutterScroll = scrollToBottom;
-
-  // Live updates via WebSocket.
-  // Browser auto-resolves protocol/host; same origin as the page.
-  try {
-    var page = document.querySelector('.prutter-conv-page');
-    var convId = page.dataset.conversationId;
-    var me = page.dataset.me;
-    var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
-    var ws = new WebSocket(proto + '//' + location.host + '/ws/prutter');
-
-    ws.addEventListener('message', function(ev) {
-      try {
-        var data = JSON.parse(ev.data);
-        if (data.type !== 'new_message' || data.conversationId !== convId) return;
-        var m = data.message;
-        // Don't double-render our own messages (already added via HTMX response)
-        if (m.author_id === me) return;
-
-        var li = document.createElement('li');
-        li.className = 'prutter-msg prutter-msg--theirs';
-        li.dataset.msgId = m.id;
-        var bubble = document.createElement('div');
-        bubble.className = 'prutter-msg-bubble';
-        bubble.textContent = m.content;  // text content = automatic escaping
-        var time = document.createElement('div');
-        time.className = 'prutter-msg-time';
-        time.textContent = new Date(m.created_at).toLocaleString();
-        li.appendChild(bubble);
-        li.appendChild(time);
-        thread.appendChild(li);
-        scrollToBottom();
-      } catch (e) { /* ignore malformed */ }
-    });
-  } catch (e) {
-    console.warn('Prutter WS unavailable:', e);
-  }
-
-  // ── Composer: invoer-gedrag + auto-groei + wissen na versturen ──────────
-  var form = document.getElementById('prutter-composer');
-  var ta = form ? form.querySelector('textarea') : null;
-  if (form && ta) {
-    var MAXH = function () { return Math.round(window.innerHeight * 0.4); };
-    function autoGrow() {
-      ta.style.height = 'auto';
-      ta.style.height = Math.min(ta.scrollHeight, MAXH()) + 'px';
-    }
-    function isTouch() {
-      return window.matchMedia && window.matchMedia('(pointer: coarse)').matches;
-    }
-    function submit() {
-      if (!ta.value.trim()) return;
-      if (form.requestSubmit) form.requestSubmit();
-      else form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
-    }
-
-    ta.addEventListener('input', autoGrow);
-    ta.addEventListener('keydown', function (e) {
-      if (e.key !== 'Enter' || e.isComposing) return;   // IME-invoer met rust laten
-      // Touch (mobiel/tablet): Enter = nieuwe regel, versturen via de Stuur-knop.
-      if (isTouch()) return;
-      // Desktop: Shift+Enter = nieuwe regel, gewone Enter = versturen.
-      if (e.shiftKey) return;
-      e.preventDefault();
-      submit();
-    });
-
-    // Wissen + terug naar één regel ná een geslaagde verzending.
-    form.addEventListener('htmx:afterRequest', function (e) {
-      if (e.detail && e.detail.successful) {
-        ta.value = '';
-        autoGrow();
-        scrollToBottom();
-        ta.focus();
-      }
-    });
-
-    autoGrow();
-  }
-})();
-</script>
-
-<style>
-/* De chat is position:fixed (los uit de flow) → de site-footer (met copyright,
-   versie én de "Installeer app"-knop) schemerde erdoorheen in het lege
-   berichtgebied. In de chat hoort die footer niet thuis: verbergen. */
-body:has(.prutter-conv-page) .site-footer { display: none; }
-
-/* ── Full-height chat-layout (mobile-first) ──────────────────────────────
-   De pagina is FIXED gepind tussen de topnav (desktop) en de onderste balken
-   (bottom-tab + audiospeler), zodat ALLEEN de thread scrollt — niet de hele
-   pagina. De offsets volgen de body-classes has-bottom-tab/has-audio-player,
-   dus de composer valt nooit achter een balk. */
-.prutter-conv-page {
-  position: fixed;
-  left: 0; right: 0;
-  top: 0;                 /* mobiel: masthead is verborgen → vanaf de top */
-  bottom: 0;
-  max-width: 760px; margin: 0 auto;
-  display: flex; flex-direction: column;
-  padding: 0.5rem 0.75rem;
-  z-index: 1;
-}
-/* Desktop: de sticky masthead staat erboven → begin eronder. */
-@media (min-width: 768px) {
-  .prutter-conv-page { top: 64px; padding: 0.75rem 1rem; }
-}
-/* Onderste vrije ruimte = de balken die de viewport-bodem overlappen. */
-@media (max-width: 767px) {
-  body.has-bottom-tab .prutter-conv-page { bottom: calc(56px + env(safe-area-inset-bottom, 0)); }
-}
-body.has-audio-player .prutter-conv-page { bottom: calc(76px + env(safe-area-inset-bottom, 0)); }
-@media (max-width: 767px) {
-  body.has-bottom-tab.has-audio-player .prutter-conv-page { bottom: calc(56px + 76px + env(safe-area-inset-bottom, 0)); }
-}
-
-/* Compacte chat-kop: terug-pijl + avatar + naam op één regel. */
-.prutter-conv-header {
-  flex: 0 0 auto;
-  display: flex; align-items: center; gap: 0.65rem;
-  padding-bottom: 0.6rem; margin-bottom: 0.5rem;
-  border-bottom: 1px solid var(--rule);
-}
-.prutter-back {
-  flex: 0 0 auto;
-  display: inline-flex; align-items: center; justify-content: center;
-  width: 40px; height: 40px; border-radius: 8px;
-  color: var(--ink); text-decoration: none;
-}
-.prutter-back:hover { color: var(--accent); background: var(--paper-2); }
-.prutter-back svg { width: 20px; height: 20px; }
-.prutter-conv-avatar {
-  flex: 0 0 auto;
-  width: 40px; height: 40px; border-radius: 50%;
-  overflow: hidden; background: var(--paper-2);
-  display: inline-flex; align-items: center; justify-content: center;
-  color: var(--accent); font-weight: 700;
-}
-.prutter-conv-avatar img { width: 100%; height: 100%; object-fit: cover; }
-.prutter-conv-id { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
-.prutter-conv-name {
-  font-family: var(--font-display, serif); font-size: 1.15rem; color: var(--ink);
-  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
-}
-.prutter-conv-sub { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); text-decoration: none; }
-.prutter-conv-sub:hover { color: var(--accent); }
-
-/* De ENIGE scroller. min-height:0 is cruciaal in een flex-kolom, anders
-   groeit de lijst i.p.v. te scrollen. */
-.prutter-thread {
-  flex: 1 1 auto; min-height: 0;
-  list-style: none; margin: 0;
-  display: flex; flex-direction: column; gap: 0.4rem;
-  padding: 0.25rem 0.1rem 0.5rem;
-  overflow-y: auto; overscroll-behavior: contain;
-  -webkit-overflow-scrolling: touch;
-}
-
-.prutter-msg { display: flex; flex-direction: column; max-width: 82%; }
-.prutter-msg-bubble {
-  padding: 0.55rem 0.85rem;
-  border-radius: 14px;
-  white-space: pre-wrap;
-  word-wrap: break-word;
-  line-height: 1.4;
-  font-size: 0.95rem;
-}
-.prutter-msg-time {
-  font-size: 0.7rem;
-  color: var(--ink-muted, var(--ink-soft));
-  margin-top: 0.15rem;
-}
-
-.prutter-msg--theirs { align-self: flex-start; }
-.prutter-msg--theirs .prutter-msg-bubble {
-  background: var(--paper);
-  border: 1px solid var(--rule);
-  border-bottom-left-radius: 4px;
-}
-.prutter-msg--theirs .prutter-msg-time { padding-left: 0.5rem; }
-
-.prutter-msg--mine { align-self: flex-end; align-items: flex-end; }
-.prutter-msg--mine .prutter-msg-bubble {
-  background: var(--accent);
-  color: white;
-  border-bottom-right-radius: 4px;
-}
-.prutter-msg--mine .prutter-msg-time { padding-right: 0.5rem; }
-
-.prutter-composer {
-  flex: 0 0 auto;
-  display: flex; gap: 0.5rem; align-items: flex-end;
-  padding-top: 0.6rem; margin-top: 0.25rem;
-  border-top: 1px solid var(--rule);
-}
-.prutter-composer textarea {
-  flex: 1;
-  padding: 0.65rem 0.85rem;
-  border: 1px solid var(--rule);
-  border-radius: 12px;
-  background: var(--paper);
-  color: var(--ink);
-  font-family: inherit;
-  font-size: 1rem;            /* ≥16px → iOS zoomt niet in bij focus */
-  line-height: 1.35;
-  resize: none;
-  min-height: 44px; max-height: 40vh;
-  overflow-y: auto;   /* scrollt intern zodra de auto-groei de max raakt */
-}
-.prutter-composer .btn { flex: 0 0 auto; min-height: 44px; }
-</style>
Index: src/views/pages/prutter-inbox.ejs
===================================================================
--- src/views/pages/prutter-inbox.ejs	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ 	(revision )
@@ -1,111 +1,0 @@
-<div class="container prutter-inbox-page">
-  <header class="prutter-header">
-    <h1>Prutter</h1>
-    <p class="prutter-tagline"><%= t('prin.tagline') %></p>
-  </header>
-
-  <% if (!conversations.length) { %>
-    <p class="prutter-empty">
-      <%= t('prin.empty') %>
-    </p>
-  <% } else { %>
-    <ol class="prutter-conv-list">
-      <% conversations.forEach(function(c) { %>
-        <li class="prutter-conv-row">
-          <a class="prutter-conv-link" href="<%= siteUrlBase %>/prutter/<%= c.id %>">
-            <div class="prutter-conv-avatar">
-              <% if (c.other_avatar) { %>
-                <img src="<%= c.other_avatar %>" alt="">
-              <% } else { %>
-                <span><%= (c.other_username || '?').charAt(0).toUpperCase() %></span>
-              <% } %>
-            </div>
-            <div class="prutter-conv-body">
-              <div class="prutter-conv-meta">
-                <strong class="prutter-conv-name"><%= c.other_username %></strong>
-                <% if (c.last_message_at) { %>
-                  <span class="prutter-conv-time"><%= formatDateTime(c.last_message_at) %></span>
-                <% } %>
-              </div>
-              <% if (c.last_message_preview) { %>
-                <div class="prutter-conv-preview"><%= c.last_message_preview %></div>
-              <% } else { %>
-                <div class="prutter-conv-preview muted"><%= t('prin.empty_conv') %></div>
-              <% } %>
-            </div>
-            <% if (c.unread_count > 0) { %>
-              <span class="prutter-unread-badge"><%= c.unread_count %></span>
-            <% } %>
-          </a>
-        </li>
-      <% }); %>
-    </ol>
-  <% } %>
-</div>
-
-<style>
-/* Geen site-footer in Prutter (ook niet op de inbox) — net als de chat. */
-body:has(.prutter-inbox-page) .site-footer { display: none; }
-
-.prutter-inbox-page { max-width: 720px; margin: 3rem auto; padding: 0 1rem; }
-.prutter-header h1 {
-  font-family: var(--font-display, serif);
-  font-size: 2rem; margin: 0 0 0.25rem;
-}
-.prutter-tagline { color: var(--ink-muted, var(--ink-soft)); margin: 0 0 1.5rem; }
-.prutter-empty {
-  text-align: center; padding: 2rem; color: var(--ink-muted, var(--ink-soft));
-  background: var(--paper-2); border: 1px dashed var(--rule); border-radius: 8px;
-}
-
-.prutter-conv-list { list-style: none; padding: 0; margin: 0; }
-.prutter-conv-row { border-bottom: 1px solid var(--rule); }
-.prutter-conv-row:last-child { border-bottom: 0; }
-.prutter-conv-link {
-  display: flex; gap: 0.85rem; align-items: center;
-  padding: 0.85rem 0;
-  text-decoration: none; color: var(--ink);
-  transition: background 100ms;
-}
-.prutter-conv-link:hover { background: var(--paper-2); }
-
-.prutter-conv-avatar {
-  flex-shrink: 0;
-  width: 44px; height: 44px;
-  border-radius: 50%;
-  overflow: hidden;
-  background: var(--paper-2);
-  border: 1px solid var(--rule);
-  display: inline-flex; align-items: center; justify-content: center;
-}
-.prutter-conv-avatar img { width: 100%; height: 100%; object-fit: cover; }
-.prutter-conv-avatar span {
-  font-family: var(--font-display, serif);
-  font-weight: 700; color: var(--accent);
-}
-
-.prutter-conv-body { flex: 1; min-width: 0; }
-.prutter-conv-meta {
-  display: flex; justify-content: space-between; gap: 0.5rem;
-  margin-bottom: 0.15rem;
-}
-.prutter-conv-name { font-weight: 600; }
-.prutter-conv-time {
-  color: var(--ink-muted, var(--ink-soft));
-  font-size: 0.8rem; flex-shrink: 0;
-}
-.prutter-conv-preview {
-  color: var(--ink-soft);
-  font-size: 0.9rem;
-  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
-}
-.prutter-conv-preview.muted { font-style: italic; color: var(--ink-muted, var(--ink-soft)); }
-
-.prutter-unread-badge {
-  background: var(--accent); color: white;
-  border-radius: 10px;
-  padding: 0.1rem 0.5rem;
-  font-size: 0.75rem;
-  font-weight: 600;
-}
-</style>
Index: src/views/pages/user.ejs
===================================================================
--- src/views/pages/user.ejs	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ src/views/pages/user.ejs	(revision 8f2f97ce2ca4540506ee22910e99a093826a9555)
@@ -27,9 +27,4 @@
         <% } %>
       </p>
-      <% if (user && user.id !== author.id && site && site.enable_prutter && (typeof tenancy !== 'undefined' && tenancy === 'hub') && (typeof premiumUnlocked === 'undefined' || premiumUnlocked)) { %>
-        <p class="user-actions">
-          <a href="<%= siteUrlBase %>/prutter/new?to=<%= encodeURIComponent(author.username) %>" class="btn btn-primary">💬 <%= t('pusr.send_dm') %></a>
-        </p>
-      <% } %>
     </div>
   </header>
Index: src/views/partials/bottom-tab.ejs
===================================================================
--- src/views/partials/bottom-tab.ejs	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ src/views/partials/bottom-tab.ejs	(revision 8f2f97ce2ca4540506ee22910e99a093826a9555)
@@ -13,5 +13,4 @@
 const _canPost     = !!(user && (typeof canMutate === 'undefined' || canMutate)
                         && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site));
-const _hasPrutter  = !!(user && site && site.enable_prutter && (typeof tenancy !== 'undefined' && tenancy === 'hub') && (typeof premiumUnlocked === 'undefined' || premiumUnlocked));
 const _ownProfile  = user ? ('/users/' + user.username) : null;
 const _isHub       = (typeof tenancy !== 'undefined' && tenancy === 'hub');
@@ -32,5 +31,4 @@
 else if (_p.indexOf('/search') === 0 || _p.indexOf('/tag/') === 0 || _p.indexOf('/type/') === 0) _active = 'search';
 else if (_p === '/posts/new' || /^\/posts\/[^/]+\/edit$/.test(_p)) _active = 'create';
-else if (_p.indexOf('/prutter') === 0) _active = 'prutter';
 else if (_p.indexOf('/account') === 0 || (_ownProfile && _p.indexOf(_ownProfile) === 0)) _active = 'profile';
 else if (_p.indexOf('/auth/login') === 0) _active = 'login';
@@ -80,18 +78,4 @@
   <% } %>
 
-  <!-- Prutter (DMs) -->
-  <% if (_hasPrutter) { %>
-    <a class="bottom-tab-item<%= _active === 'prutter' ? ' is-active' : '' %>"
-       href="<%= _siteUrlBase %>/prutter"
-       aria-label="Prutter berichten" <%= _active === 'prutter' ? 'aria-current="page"' : '' %>>
-      <svg class="bottom-tab-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
-        <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
-      </svg>
-      <span class="bottom-tab-label">Prutter</span>
-      <% if (typeof unreadDmCount !== 'undefined' && unreadDmCount > 0) { %>
-        <span class="bottom-tab-badge" aria-label="<%= unreadDmCount %> ongelezen"><%= unreadDmCount > 99 ? '99+' : unreadDmCount %></span>
-      <% } %>
-    </a>
-  <% } %>
 
   <!-- Profiel / Inloggen -->
Index: src/views/partials/topnav.ejs
===================================================================
--- src/views/partials/topnav.ejs	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ src/views/partials/topnav.ejs	(revision 8f2f97ce2ca4540506ee22910e99a093826a9555)
@@ -15,5 +15,4 @@
 const _isAdmin     = typeof bodyClass !== 'undefined' && bodyClass.indexOf('on-admin') >= 0;
 const _hasSearch   = !site || site.show_search === undefined || site.show_search;
-const _hasPrutter  = user && site && site.enable_prutter && (typeof tenancy !== 'undefined' && tenancy === 'hub') && (typeof premiumUnlocked === 'undefined' || premiumUnlocked);
 const _canPost     = user && (typeof canMutate === 'undefined' || canMutate)
                      && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site);
@@ -98,10 +97,4 @@
           </div>
         </details>
-      <% } %>
-
-      <% if (_hasPrutter) { %>
-        <a class="nav-btn" href="<%= _siteUrlBase %>/prutter" aria-label="Prutter (DMs)" title="Prutter">
-          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
-        </a>
       <% } %>
 
Index: src/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision 24cdcc61abea38cc9e4d1996d3736d1c14c5d142)
+++ src/views/shell.ejs	(revision 8f2f97ce2ca4540506ee22910e99a093826a9555)
@@ -33,5 +33,5 @@
 // Listing pages (search/tag/type/archive) shouldn't be indexed (dupe content)
 if (currentPath) {
-  if (/^\/(?:search|tag|type|archive|users|prutter|account|admin)(?:$|\/)/.test(currentPath)) {
+  if (/^\/(?:search|tag|type|archive|users|account|admin)(?:$|\/)/.test(currentPath)) {
     _shouldIndex = false;
   }
