Index: src/views/pages/404.ejs
===================================================================
--- src/views/pages/404.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,22 +1,0 @@
-<section class="nf">
-  <div class="nf-code">404</div>
-  <h1 class="nf-title"><%= t('e404.title') %></h1>
-  <p class="nf-sub"><%= t('e404.sub') %></p>
-  <div class="nf-actions">
-    <a class="nf-btn nf-btn-primary" href="<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/">← <%= t('e404.home') %></a>
-    <% if (typeof site !== 'undefined' && site && site.show_archive_link !== 0) { %>
-      <a class="nf-btn" href="<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/archive"><%= t('e404.archive') %></a>
-    <% } %>
-  </div>
-</section>
-
-<style>
-  .nf { max-width: 540px; margin: 0 auto; padding: 12vh 20px 64px; text-align: center; }
-  .nf-code { font-size: clamp(72px, 22vw, 140px); font-weight: 800; line-height: .9; color: var(--accent, #6b8f71); opacity: .9; letter-spacing: -.03em; }
-  .nf-title { font-size: clamp(22px, 6vw, 32px); margin: .4rem 0 .6rem; }
-  .nf-sub { opacity: .8; line-height: 1.6; margin: 0 auto 1.6rem; max-width: 36ch; }
-  .nf-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
-  .nf-btn { display: inline-block; padding: 12px 20px; border-radius: 999px; text-decoration: none; font-weight: 600; font-size: 15px; border: 1px solid rgba(128,128,128,.35); color: inherit; }
-  .nf-btn-primary { background: var(--accent, #6b8f71); border-color: var(--accent, #6b8f71); color: #fff; }
-  @media (max-width: 420px) { .nf-actions { flex-direction: column; } .nf-btn { width: 100%; } }
-</style>
Index: src/views/pages/account.ejs
===================================================================
--- src/views/pages/account.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/account.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -2,8 +2,8 @@
 
   <header class="ax-header">
-    <a href="/" class="ax-back" aria-label="<%= t('acct.back_home') %>">←</a>
+    <a href="/" class="ax-back" aria-label="Terug naar home">←</a>
     <div>
-      <h1><%= t('acct.title') %></h1>
-      <p class="ax-tagline"><%= t('acct.subtitle') %></p>
+      <h1>Account</h1>
+      <p class="ax-tagline">Profiel en wachtwoord.</p>
     </div>
   </header>
@@ -12,165 +12,80 @@
   <% if (error)   { %><div class="ax-flash ax-flash-err"><%= error %></div><% } %>
 
-  <% if (isViewer) { %>
-    <div class="ax-viewer-note" role="note">
-      <span class="ax-viewer-ico" aria-hidden="true">👁️</span>
-      <div>
-        <strong><%= t('acct.viewer_mode') %></strong>
-        <span><%= t('acct.viewer_note') %></span>
-      </div>
-    </div>
-  <% } %>
-
   <%# ── PROFILE ────────────────────────────────────────────── %>
   <section class="ax-card">
-    <div class="ax-card-title"><%= t('acct.profile') %></div>
+    <div class="ax-card-title">Profiel</div>
 
     <div class="ax-profile-id">
-      <span class="ax-profile-avatar ax-profile-avatar-static">
-        <% var _av = (typeof siteAvatar !== 'undefined' ? siteAvatar : null); %>
-        <% if (_av) { %>
-          <img src="<%= avatar(_av, 128) %>" alt="">
+      <div class="ax-profile-avatar">
+        <% if (account.avatar_url) { %>
+          <img src="<%= account.avatar_url %>" alt="">
         <% } else { %>
           <span><%= account.username.charAt(0).toUpperCase() %></span>
         <% } %>
-      </span>
+      </div>
       <div class="ax-profile-meta">
         <div class="ax-profile-name"><%= account.username %></div>
         <div class="ax-profile-role"><%= account.role %> · <%= account.email %></div>
         <% if (account.created_at) { %>
-          <div class="ax-profile-joined"><%= t('acct.member_since') %> <%= formatDate(account.created_at) %></div>
+          <div class="ax-profile-joined">Lid sinds <%= formatDate(account.created_at) %></div>
         <% } %>
-        <% if (canMutate) { %>
-          <div class="ax-profile-joined"><%- t('acct.photo_site_hint') %></div>
-        <% } %>
       </div>
     </div>
 
-    <% if (canMutate) { %>
-      <form action="/account/profile" method="post" class="ax-form ax-form-bio">
-        <label class="ax-field">
-          <span><%= t('acct.username') %></span>
-          <input type="text" name="username" value="<%= account.username %>" minlength="2" maxlength="30" pattern="[A-Za-z0-9_\-]{2,30}" autocomplete="username">
-        </label>
-        <label class="ax-field">
-          <span><%= t('acct.email') %></span>
-          <input type="email" name="email" value="<%= account.email || '' %>" maxlength="254" autocomplete="email" placeholder="<%= t('acct.email_ph') %>">
-        </label>
-        <label class="ax-field">
-          <span><%= t('acct.bio') %></span>
-          <textarea name="bio" rows="3" maxlength="500" placeholder="<%= t('acct.bio_ph') %>"><%= account.bio || '' %></textarea>
-        </label>
-        <div class="ax-form-actions">
-          <button type="submit" class="ax-btn ax-btn-primary"><%= t('acct.save') %></button>
-        </div>
-      </form>
-    <% } else { %>
-      <div class="ax-form ax-form-bio">
-        <label class="ax-field">
-          <span><%= t('acct.bio') %></span>
-          <textarea rows="3" disabled placeholder="<%= t('acct.bio_empty') %>"><%= account.bio || '' %></textarea>
-        </label>
-      </div>
-    <% } %>
-  </section>
-
-  <%# ── PERSONAL LANGUAGE ──────────────────────────────────── %>
-  <% if (canMutate) { %>
-  <section class="ax-card">
-    <div class="ax-card-title"><%= t('acct.lang_label') %></div>
-    <form action="/account/lang" method="post" class="ax-form">
-      <label class="ax-field">
-        <span><%= t('acct.lang_label') %> <small><%= t('acct.lang_hint') %></small></span>
-        <select name="lang">
-          <% langs.forEach(function(l){ var sel = (account && account.lang) ? (account.lang === l.code) : l.active; %>
-            <option value="<%= l.code %>"<%= sel ? ' selected' : '' %>><%= l.name %></option>
-          <% }); %>
-        </select>
+    <form action="/account/avatar" method="post" enctype="multipart/form-data" class="ax-form">
+      <label class="ax-field ax-file">
+        <span>Avatar uploaden <small>(jpg/png/webp/gif, max 5 MB)</small></span>
+        <span class="ax-file-control">
+          <span class="ax-btn ax-btn-secondary ax-file-btn">📷 Kies bestand</span>
+          <span class="ax-file-name" data-empty>Geen bestand gekozen</span>
+          <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp,image/gif" required>
+        </span>
       </label>
       <div class="ax-form-actions">
-        <button type="submit" class="ax-btn ax-btn-primary"><%= t('acct.save') %></button>
+        <button type="submit" class="ax-btn ax-btn-primary">⬆ Uploaden</button>
+        <% if (account.avatar_url) { %>
+          <%# Separate <form> block kept inline since it submits to a different endpoint. %>
+          <button type="button" class="ax-btn" onclick="document.getElementById('avatar-remove-form').submit()">Avatar verwijderen</button>
+        <% } %>
+      </div>
+    </form>
+    <% if (account.avatar_url) { %>
+      <form action="/account/avatar/remove" method="post" id="avatar-remove-form" hidden></form>
+    <% } %>
+
+    <form action="/account/profile" method="post" class="ax-form ax-form-bio">
+      <label class="ax-field">
+        <span>Bio</span>
+        <textarea name="bio" rows="3" maxlength="500" placeholder="Een korte regel over jezelf"><%= account.bio || '' %></textarea>
+      </label>
+      <div class="ax-form-actions">
+        <button type="submit" class="ax-btn ax-btn-primary">💾 Bio opslaan</button>
       </div>
     </form>
   </section>
-  <% } %>
-
-  <%# ── SITE (owner may update their site name) ─────────── %>
-  <% if (typeof editableSite !== 'undefined' && editableSite && canMutate) { %>
+
+  <%# ── PASSWORD ──────────────────────────────────────────── %>
   <section class="ax-card">
-    <div class="ax-card-title"><%= t('acct.site') %></div>
-    <form action="/account/site" method="post" class="ax-form">
-      <label class="ax-field">
-        <span><%= t('acct.site_name') %> <small><%= t('acct.site_name_hint') %></small></span>
-        <input type="text" name="site_title" value="<%= editableSite.title || '' %>" maxlength="200" required>
-      </label>
-      <label class="ax-field">
-        <span><%= t('acct.tagline') %> <small><%= t('acct.tagline_hint') %></small></span>
-        <input type="text" name="site_tagline" value="<%= editableSite.tagline || '' %>" maxlength="200">
-      </label>
-      <div class="ax-form-actions">
-        <button type="submit" class="ax-btn ax-btn-primary"><%= t('acct.site_save') %></button>
-      </div>
-    </form>
-  </section>
-  <% } %>
-
-  <%# ── PASSWORD ──────────────────────────────────────────── %>
-  <%# In viewer mode there is no password section (nothing to change). %>
-  <%# ── CONNECTED APPS (OAuth C2S) ─────────────────────────── %>
-  <section class="ax-card">
-    <div class="ax-card-title"><%= t('acct.oauth_apps') %></div>
-    <p class="ax-tagline" style="margin:0 0 .6rem"><%= t('acct.oauth_hint') %></p>
-    <% if (!authorizations || !authorizations.length) { %>
-      <p class="ax-oauth-empty"><%= t('acct.oauth_none') %></p>
-    <% } else { %>
-      <ul class="ax-oauth-list">
-        <% authorizations.forEach(function(a){ %>
-          <li class="ax-oauth-item">
-            <div class="ax-oauth-info">
-              <strong><%= a.client_name || t('acct.oauth_unknown_app') %></strong>
-              <span class="ax-oauth-meta">@<%= a.site_slug %> · <%= a.scope || 'c2s' %> · <%= t('acct.oauth_last_used') %> <%= a.last_used_at ? formatDate(a.last_used_at) : t('acct.oauth_never') %></span>
-            </div>
-            <form action="/account/oauth/revoke" method="post">
-              <input type="hidden" name="token_hash" value="<%= a.token_hash %>">
-              <button type="submit" class="ax-btn ax-btn-danger"><%= t('acct.oauth_revoke') %></button>
-            </form>
-          </li>
-        <% }); %>
-      </ul>
-    <% } %>
-  </section>
-
-  <% if (canMutate) { %>
-  <% if (hasPassword) { %>
-  <section class="ax-card">
-    <div class="ax-card-title"><%= t('acct.password_change') %></div>
+    <div class="ax-card-title">Wachtwoord wijzigen</div>
     <form action="/account/password" method="post" class="ax-form">
       <label class="ax-field">
-        <span><%= t('acct.password_current') %></span>
+        <span>Huidig wachtwoord</span>
         <input type="password" name="current" required autocomplete="current-password">
       </label>
       <div class="ax-row ax-row-2">
         <label class="ax-field">
-          <span><%= t('acct.password_new') %> <small><%= t('acct.password_min') %></small></span>
+          <span>Nieuw wachtwoord <small>(min 8 tekens)</small></span>
           <input type="password" name="new_password" required minlength="8" autocomplete="new-password">
         </label>
         <label class="ax-field">
-          <span><%= t('acct.password_confirm') %></span>
+          <span>Bevestig nieuw wachtwoord</span>
           <input type="password" name="confirm" required minlength="8" autocomplete="new-password">
         </label>
       </div>
       <div class="ax-form-actions">
-        <button type="submit" class="ax-btn ax-btn-primary"><%= t('acct.password_change') %></button>
+        <button type="submit" class="ax-btn ax-btn-primary">🔒 Wachtwoord wijzigen</button>
       </div>
     </form>
   </section>
-
-  <% } else { %>
-  <section class="ax-card">
-    <div class="ax-card-title"><%= t('acct.login') %></div>
-    <p class="ax-tagline" style="margin:0"><%= t('acct.login_google_only', { email: account.email }) %></p>
-  </section>
-  <% } %>
-  <% } %>
 
 </div>
@@ -193,19 +108,4 @@
 .ax-flash-ok  { background: rgba(40,160,90,.15); color: #2a9d5e; border: 1px solid rgba(40,160,90,.3); }
 .ax-flash-err { background: rgba(200,60,60,.15); color: #c33; border: 1px solid rgba(200,60,60,.3); }
-
-/* ─── Kijker-modus notice ──────────────────────────────────────── */
-.ax-viewer-note {
-  display: flex; align-items: flex-start; gap: 0.75rem;
-  padding: 0.85rem 1rem; margin-bottom: 1rem;
-  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--rule));
-  border-left: 3px solid var(--accent);
-  border-radius: 10px;
-  background: color-mix(in srgb, var(--accent) 8%, var(--paper));
-}
-.ax-viewer-ico { font-size: 1.1rem; line-height: 1.4; }
-.ax-viewer-note strong { display: block; font-size: 0.92rem; }
-.ax-viewer-note span { color: var(--ink-muted, var(--ink-soft)); font-size: 0.85rem; }
-.ax-profile-avatar-static { flex-shrink: 0; }
-.ax-form-bio textarea:disabled { opacity: 0.7; cursor: default; }
 
 /* ─── Cards ────────────────────────────────────────────────────── */
@@ -299,6 +199,6 @@
 .ax-btn:hover { border-color: var(--accent); }
 .ax-btn-secondary { background: var(--paper-2); }
-.ax-btn-primary, a.ax-btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
-.ax-btn-primary:hover, a.ax-btn-primary:hover { opacity: 0.92; border-color: var(--accent); color: #fff; }
+.ax-btn-primary { background: var(--accent); color: white; border-color: var(--accent); }
+.ax-btn-primary:hover { opacity: 0.92; border-color: var(--accent); }
 
 /* ─── Profile ID row ──────────────────────────────────────────── */
@@ -309,11 +209,6 @@
   margin-bottom: 1rem;
 }
-.ax-avatar-form { margin: 0; flex-shrink: 0; }
-.ax-avatar-wrap {
-  position: relative; display: block;
-  width: 84px; height: 84px; cursor: pointer;
-}
 .ax-profile-avatar {
-  width: 84px; height: 84px;
+  width: 64px; height: 64px;
   border-radius: 50%;
   overflow: hidden;
@@ -322,26 +217,8 @@
   display: inline-flex; align-items: center; justify-content: center;
   font-family: var(--font-display, serif);
-  font-size: 1.7rem; font-weight: 600;
-  border: 1px solid var(--rule);
-  transition: border-color 120ms;
-}
-.ax-avatar-wrap:hover .ax-profile-avatar { border-color: var(--accent); }
+  font-size: 1.6rem; font-weight: 600;
+  flex-shrink: 0;
+}
 .ax-profile-avatar img { width: 100%; height: 100%; object-fit: cover; }
-.ax-avatar-edit {
-  position: absolute; right: -2px; bottom: -2px;
-  width: 28px; height: 28px; border-radius: 50%;
-  background: var(--accent); color: #fff;
-  display: flex; align-items: center; justify-content: center;
-  border: 3px solid var(--paper);
-  transition: transform 120ms;
-}
-.ax-avatar-wrap:hover .ax-avatar-edit { transform: scale(1.08); }
-.ax-avatar-remove { margin-top: 0.45rem; }
-.ax-linkbtn {
-  background: none; border: 0; padding: 0; cursor: pointer;
-  color: var(--ink-muted, var(--ink-soft)); font-size: 0.8rem; text-decoration: underline;
-  font-family: var(--font-ui, system-ui), sans-serif;
-}
-.ax-linkbtn:hover { color: #c33; }
 .ax-profile-meta { min-width: 0; }
 .ax-profile-name { font-weight: 600; font-size: 1.1rem; }
@@ -355,13 +232,3 @@
   .ax-profile-id { flex-direction: column; align-items: flex-start; gap: 0.75rem; text-align: left; }
 }
-
-.ax-oauth-empty { margin: 0; color: var(--ink-soft, #888); }
-.ax-oauth-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .6rem; }
-.ax-oauth-item { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .7rem .85rem; border-radius: 12px; background: var(--paper-2, rgba(0,0,0,.04)); }
-.ax-oauth-info { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
-.ax-oauth-info strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.ax-oauth-meta { font-size: .8rem; color: var(--ink-soft, #888); }
-@media (max-width: 480px) {
-  .ax-oauth-item { flex-direction: column; align-items: stretch; }
-}
 </style>
Index: src/views/pages/admin-audio.ejs
===================================================================
--- src/views/pages/admin-audio.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/admin-audio.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,14 +1,9 @@
-<%# De gedeelde ax-*-primitieven. MOET boven het eigen style-blok van deze
-    pagina staan: wat hieronder blijft staan wijkt bewust af en hoort dus
-    later te komen, anders wint de partial ervan. %>
-<%- include('../partials/admin-styles') %>
 <div class="container ax-page">
 
-  <%- include('../partials/admin-back') %>
-  <%- include('../partials/media-tabs', { active: 'audio', audioOn: true }) %>
   <header class="ax-header">
+    <a href="/admin" class="ax-back" aria-label="Terug naar admin">←</a>
     <div>
-      <h1><%= t('aaud.title') %></h1>
-      <p class="ax-tagline"><%= t('aaud.tagline_pre') %> <code>[[track:&lt;id&gt;]]</code> <%= t('aaud.tagline_post') %></p>
+      <h1>Audio tracks</h1>
+      <p class="ax-tagline">Site-level MP3s. Gebruik <code>[[track:&lt;id&gt;]]</code> in een post om een play-knop in te voegen.</p>
     </div>
   </header>
@@ -19,5 +14,5 @@
   <%# ── UPLOAD ──────────────────────────────────────────────── %>
   <section class="ax-card">
-    <div class="ax-card-title"><%= t('aaud.upload') %></div>
+    <div class="ax-card-title">Upload</div>
 
     <%# Bulk drag-drop zone. Multiple files at once, transcoded sequentially.
@@ -30,17 +25,17 @@
       <div class="ax-row ax-row-2">
         <label class="ax-field">
-          <span><%= t('aaud.artist') %> <small><%= t('aaud.applied_all') %></small></span>
-          <input type="text" id="batch-artist" placeholder="<%= t('aaud.optional') %>">
+          <span>Artiest <small>(toegepast op alle bestanden)</small></span>
+          <input type="text" id="batch-artist" placeholder="Optioneel">
         </label>
         <label class="ax-field">
-          <span><%= t('aaud.album') %> <small><%= t('aaud.applied_all') %></small></span>
-          <input type="text" id="batch-album" placeholder="<%= t('aaud.optional') %>">
+          <span>Album <small>(toegepast op alle bestanden)</small></span>
+          <input type="text" id="batch-album" placeholder="Optioneel">
         </label>
       </div>
       <label class="ax-field ax-file">
-        <span><%= t('aaud.cover') %> <small><%= t('aaud.cover_hint') %></small></span>
+        <span>Cover <small>(optioneel, toegepast op alle bestanden — jpg/png/webp/gif, max 5 MB)</small></span>
         <span class="ax-file-control">
-          <span class="ax-btn ax-btn-secondary ax-file-btn">🖼 <%= t('aaud.choose_cover') %></span>
-          <span class="ax-file-name" data-empty><%= t('aaud.no_file') %></span>
+          <span class="ax-btn ax-btn-secondary ax-file-btn">🖼 Kies cover</span>
+          <span class="ax-file-name" data-empty>Geen bestand gekozen</span>
           <input type="file" id="batch-cover" accept="image/jpeg,image/png,image/webp,image/gif">
         </span>
@@ -50,6 +45,6 @@
       <label class="ax-dropzone" id="audio-dropzone" tabindex="0">
         <span class="ax-dropzone-icon">🎵</span>
-        <strong class="ax-dropzone-title"><%= t('aaud.drag_here') %></strong>
-        <span class="ax-dropzone-sub"><%= t('aaud.or_click') %><br><small>mp3, m4a, ogg, opus, flac · max <%= maxBytesMb %> MB · wav max <%= maxWavMb %> MB</small></span>
+        <strong class="ax-dropzone-title">Sleep audio hierheen</strong>
+        <span class="ax-dropzone-sub">of klik om bestanden te kiezen<br><small>mp3, m4a, ogg, opus, flac, wav · max <%= maxBytesMb %> MB per bestand</small></span>
         <input type="file" id="audio-files" accept="audio/*" multiple>
       </label>
@@ -59,6 +54,6 @@
 
       <div class="ax-form-actions" id="upload-actions" hidden>
-        <button type="button" class="ax-btn ax-btn-primary" id="start-upload-btn">⬆ <%= t('aaud.start_upload') %></button>
-        <button type="button" class="ax-btn" id="clear-queue-btn"><%= t('aaud.clear_list') %></button>
+        <button type="button" class="ax-btn ax-btn-primary" id="start-upload-btn">⬆ Start upload</button>
+        <button type="button" class="ax-btn" id="clear-queue-btn">Wis lijst</button>
       </div>
     </div>
@@ -67,10 +62,6 @@
   <%# ── TRACK LIST ─────────────────────────────────────────── %>
   <section class="ax-card">
-    <div class="ax-card-title" style="display:flex;align-items:center;gap:.5rem">
-      <%= t('aaud.tracks') %> <span class="ax-count"><%= tracks.length %></span>
-      <button type="button" class="ax-btn ax-btn-secondary" id="add-link-track-btn"
-              style="margin-left:auto;font-size:.85rem" title="<%= t('aaud.add_link_track_title') %>">
-        + <%= t('aaud.add_link_track') %>
-      </button>
+    <div class="ax-card-title">
+      Tracks <span class="ax-count"><%= tracks.length %></span>
     </div>
 
@@ -78,18 +69,7 @@
       <div class="ax-empty">
         <div class="ax-empty-icon">♫</div>
-        <p><%= t('aaud.no_tracks') %></p>
+        <p>Nog geen tracks. Upload er een hierboven.</p>
       </div>
     <% } else { %>
-      <%
-        // Pre-fetch loop labels — `t` is shadowed by the loop var below.
-        var _aaudUntitled = t('aaud.untitled');
-        var _aaudPlay     = t('aaud.play');
-        var _aaudEdit     = t('aaud.edit');
-        var _aaudDlOn     = t('aaud.dl_on');
-        var _aaudDlOff    = t('aaud.dl_off');
-        var _aaudDelete   = t('aaud.delete');
-        var _aaudCopy     = t('aaud.copy_click');
-        var _aaudDelConfirm = t('aaud.delete_confirm');
-      %>
       <ul class="ax-list">
         <% tracks.forEach(function(t) { %>
@@ -102,5 +82,5 @@
 
             <div class="ax-track-meta">
-              <div class="ax-track-title" data-cell="title"><%= t.title || _aaudUntitled %></div>
+              <div class="ax-track-title" data-cell="title"><%= t.title || '(zonder titel)' %></div>
               <div class="ax-track-sub">
                 <span data-cell="artist"><%= t.artist || '—' %></span>
@@ -108,5 +88,5 @@
                 <span class="ax-track-sep">·</span><span><%= t.size ? Math.round(t.size/1024) + ' KB' : '—' %></span>
               </div>
-              <code class="ax-embed" data-copy="[[track:<%= t.id %>]]" title="<%= _aaudCopy %>">[[track:<%= t.id %>]]</code>
+              <code class="ax-embed" data-copy="[[track:<%= t.id %>]]" title="Klik om te kopiëren">[[track:<%= t.id %>]]</code>
             </div>
 
@@ -116,16 +96,11 @@
                         data-stream-url="<%= t.stream_url %>"
                         data-track-id="<%= t.id %>"
-                        aria-label="<%= _aaudPlay %>" title="<%= _aaudPlay %>">
+                        aria-label="Afspelen" title="Afspelen">
                   <span class="ax-track-play-icon" aria-hidden="true">▶</span>
                 </button>
               <% } %>
-              <button type="button" class="ax-icon-btn" data-track-edit data-id="<%= t.id %>" aria-label="<%= _aaudEdit %>" title="<%= _aaudEdit %>">✎</button>
-              <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
-                <button type="button" class="ax-icon-btn" data-track-dl data-id="<%= t.id %>" data-on="<%= t.downloadable ? '1' : '0' %>"
-                        aria-label="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>" title="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>"
-                        style="<%= t.downloadable ? 'color:var(--accent,#6b8f71)' : 'opacity:.5' %>">⬇</button>
-              <% } %>
-              <form action="/admin/audio/<%= t.id %>/delete" method="post" data-confirm="<%= _aaudDelConfirm %>" class="ax-track-delete">
-                <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _aaudDelete %>" title="<%= _aaudDelete %>">🗑</button>
+              <button type="button" class="ax-icon-btn" data-track-edit data-id="<%= t.id %>" aria-label="Bewerken" title="Bewerken">✎</button>
+              <form action="/admin/audio/<%= t.id %>/delete" method="post" onsubmit="return confirm('Track verwijderen?')" class="ax-track-delete">
+                <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="Verwijderen" title="Verwijderen">🗑</button>
               </form>
             </div>
@@ -136,13 +111,4 @@
   </section>
 
-  <% if ((typeof premiumUnlocked === 'undefined' || premiumUnlocked) && typeof embedUrl !== 'undefined') { %>
-    <section class="ax-section" style="margin-top:1.5rem">
-      <h2 style="margin:0 0 .4rem">🔊 <%= t('aaud.embed_player') %></h2>
-      <p style="opacity:.75;font-size:.9rem;margin:0 0 .6rem"><%= t('aaud.embed_hint') %></p>
-      <textarea readonly rows="3" data-selectall style="width:100%;font-family:monospace;font-size:12px;padding:10px;border-radius:8px;border:1px solid var(--rule);background:var(--paper-2);color:inherit;">&lt;iframe src="<%= embedUrl %>" width="100%" height="420" frameborder="0" loading="lazy" style="border-radius:12px"&gt;&lt;/iframe&gt;</textarea>
-      <p style="margin:.6rem 0 0"><a class="btn" href="<%= embedUrl %>" target="_blank">▶ <%= t('aaud.preview_player') %></a></p>
-    </section>
-  <% } %>
-
 </div>
 
@@ -153,4 +119,57 @@
   margin: 1.5rem auto 4rem;
   padding: 0 1rem;
+}
+.ax-header {
+  display: flex; align-items: flex-start; gap: 0.75rem;
+  margin-bottom: 1.5rem;
+}
+.ax-header h1 {
+  font-family: var(--font-display, serif);
+  font-size: 1.75rem;
+  margin: 0 0 0.25rem;
+}
+.ax-tagline {
+  color: var(--ink-muted, var(--ink-soft));
+  margin: 0;
+  font-size: 0.9rem;
+  line-height: 1.5;
+}
+.ax-tagline code {
+  background: var(--paper-2);
+  padding: 0.1em 0.4em;
+  border-radius: 4px;
+  font-size: 0.9em;
+}
+.ax-back {
+  display: inline-flex; align-items: center; justify-content: center;
+  width: 40px; height: 40px;
+  border: 1px solid var(--rule);
+  border-radius: 8px;
+  background: var(--paper);
+  color: var(--ink);
+  text-decoration: none;
+  font-size: 1.1rem;
+  flex-shrink: 0;
+  transition: border-color 120ms;
+}
+.ax-back:hover { border-color: var(--accent); }
+
+/* ─── Flash messages ───────────────────────────────────────────── */
+.ax-flash {
+  padding: 0.75rem 1rem;
+  border-radius: 8px;
+  margin-bottom: 1rem;
+  font-size: 0.9rem;
+}
+.ax-flash-ok  { background: rgba(40,160,90,.15); color: #2a9d5e; border: 1px solid rgba(40,160,90,.3); }
+.ax-flash-err { background: rgba(200,60,60,.15); color: #c33;    border: 1px solid rgba(200,60,60,.3); }
+
+/* ─── Card sections ────────────────────────────────────────────── */
+.ax-card {
+  background: var(--paper);
+  border: 1px solid var(--rule);
+  border-radius: 12px;
+  padding: 1.25rem;
+  margin-bottom: 1rem;
 }
 .ax-card-title {
@@ -254,4 +273,43 @@
 .ax-form-actions[hidden] { display: none; }
 
+/* ─── Buttons ──────────────────────────────────────────────────── */
+.ax-btn {
+  display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
+  padding: 0.55rem 1rem;
+  border: 1px solid var(--rule);
+  background: var(--paper-2);
+  color: var(--ink);
+  font-family: var(--font-ui, system-ui), sans-serif;
+  font-size: 0.9rem; font-weight: 500;
+  text-decoration: none;
+  border-radius: 6px;
+  cursor: pointer;
+  min-height: 40px;
+  transition: background 120ms, border-color 120ms;
+  -webkit-tap-highlight-color: transparent;
+}
+.ax-btn:hover { border-color: var(--accent); }
+.ax-btn-secondary { background: var(--paper-2); }
+.ax-btn-primary {
+  background: var(--accent); color: white;
+  border-color: var(--accent);
+}
+.ax-btn-primary:hover { opacity: 0.92; border-color: var(--accent); }
+
+/* Tiny icon-only button for row actions */
+.ax-icon-btn {
+  display: inline-flex; align-items: center; justify-content: center;
+  width: 40px; height: 40px;
+  border: 1px solid var(--rule);
+  background: var(--paper-2);
+  color: var(--ink);
+  border-radius: 8px;
+  cursor: pointer;
+  font-size: 1rem;
+  transition: background 120ms, border-color 120ms, color 120ms;
+}
+.ax-icon-btn:hover { border-color: var(--accent); }
+.ax-icon-btn-danger:hover { color: #dc2626; border-color: #dc2626; }
+
 .ax-track-play.is-playing {
   border-color: var(--accent);
@@ -260,9 +318,108 @@
 }
 .ax-track-play-icon { font-size: 0.85rem; line-height: 1; }
+
+/* ─── Empty state ──────────────────────────────────────────────── */
+.ax-empty {
+  text-align: center;
+  padding: 2rem 1rem;
+  color: var(--ink-muted, var(--ink-soft));
+}
+.ax-empty-icon {
+  font-size: 2.5rem;
+  font-family: var(--font-display, serif);
+  color: var(--ink-soft);
+  opacity: 0.4;
+  margin-bottom: 0.5rem;
+}
+.ax-empty p { margin: 0; }
+
+/* ─── Track list (card per row) ────────────────────────────────── */
+.ax-list {
+  list-style: none; padding: 0; margin: 0;
+  display: flex; flex-direction: column;
+  gap: 0.6rem;
+}
+.ax-track {
+  display: grid;
+  grid-template-columns: 56px 1fr auto;
+  gap: 0.85rem;
+  align-items: center;
+  padding: 0.75rem;
+  background: var(--paper-2);
+  border: 1px solid var(--rule);
+  border-radius: 10px;
+  transition: border-color 120ms;
+}
+.ax-track:hover { border-color: var(--accent); }
+.ax-track-cover {
+  width: 56px; height: 56px;
+  border-radius: 6px;
+  object-fit: cover;
+  background: var(--paper);
+  display: flex; align-items: center; justify-content: center;
+  font-family: var(--font-display, serif);
+  font-size: 1.5rem;
+  color: var(--accent);
+  flex-shrink: 0;
+}
 .ax-track-cover-empty { /* span variant */ }
+.ax-track-meta {
+  min-width: 0;  /* allow text-overflow on long titles */
+  display: flex; flex-direction: column; gap: 0.2rem;
+}
+.ax-track-title {
+  font-weight: 600;
+  font-size: 0.95rem;
+  color: var(--ink);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.ax-track-sub {
+  font-size: 0.8rem;
+  color: var(--ink-muted, var(--ink-soft));
+  display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center;
+}
+.ax-track-sep { opacity: 0.5; }
+.ax-embed {
+  display: inline-block;
+  background: var(--paper);
+  padding: 0.15rem 0.5rem;
+  border-radius: 4px;
+  font-family: var(--font-mono, ui-monospace, monospace);
+  font-size: 0.75rem;
+  color: var(--ink-soft);
+  cursor: pointer;
+  user-select: all;
+  margin-top: 0.15rem;
+  word-break: break-all;
+  align-self: flex-start;
+  transition: background 120ms;
+}
+.ax-embed:hover { background: var(--rule); }
+.ax-embed.is-copied { background: rgba(40,160,90,0.2); color: #2a9d5e; }
+
+.ax-track-actions {
+  display: flex; gap: 0.4rem;
+  flex-shrink: 0;
+}
 .ax-track-delete { display: inline; }
 
 /* ─── Mobile tweaks ───────────────────────────────────────────── */
 @media (max-width: 480px) {
+  .ax-track {
+    grid-template-columns: 48px 1fr;
+    grid-template-areas: "cover meta" "actions actions";
+    gap: 0.6rem;
+  }
+  .ax-track-cover { grid-area: cover; width: 48px; height: 48px; }
+  .ax-track-meta  { grid-area: meta; }
+  .ax-track-actions {
+    grid-area: actions;
+    justify-content: flex-end;
+    border-top: 1px solid var(--rule);
+    padding-top: 0.5rem;
+    margin-top: 0.25rem;
+  }
 }
 
@@ -386,4 +543,382 @@
 <%- include('../partials/track-editor', { csrfToken: (typeof csrfToken !== 'undefined' ? csrfToken : '') }) %>
 
-<%# Het script staat in assets/js/mod/admin-audio.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
-<%- include('../partials/page-data', { pageData: { change_failed: t('aaud.change_failed'), copied: t('aaud.copied'), create_failed: t('aaud.create_failed'), dl_off: t('aaud.dl_off'), dl_on: t('aaud.dl_on'), editor_not_loaded: t('aaud.editor_not_loaded'), err_unexpected: t('aaud.err_unexpected'), failed: t('aaud.failed'), new_track: t('aaud.new_track'), no_file: t('aaud.no_file'), pause: t('aaud.pause'), play: t('aaud.play'), st_done: t('aaud.st_done'), st_error: t('aaud.st_error'), st_queued: t('aaud.st_queued'), st_transcoding: t('aaud.st_transcoding'), st_uploading: t('aaud.st_uploading'), untitled: t('aaud.untitled') } }) %>
+<script>
+(function() {
+
+  // ── Live filename display for custom file inputs ──────────────
+  document.querySelectorAll('.ax-file-control input[type="file"]').forEach(input => {
+    const nameEl = input.parentElement.querySelector('.ax-file-name');
+    if (!nameEl) return;
+    input.addEventListener('change', () => {
+      if (input.files && input.files[0]) {
+        nameEl.textContent = input.files[0].name;
+        nameEl.removeAttribute('data-empty');
+      } else {
+        nameEl.textContent = 'Geen bestand gekozen';
+        nameEl.setAttribute('data-empty', '');
+      }
+    });
+  });
+
+  // ── Inline track preview (routed through the global mini-player) ──
+  // Each .ax-track-play button is a thin wrapper around the global
+  // window.pcmsAudioPlayer.setQueue([...]) call. Visual state (▶ / ⏸ /
+  // .is-playing) is synced from the player's own audio element so it
+  // stays accurate even when the user uses the mini-player's controls.
+  (function setupTrackPreview() {
+    const buttons = document.querySelectorAll('.ax-track-play');
+    if (!buttons.length) return;
+
+    function setIcon(btn, playing) {
+      const icon = btn.querySelector('.ax-track-play-icon');
+      if (icon) icon.textContent = playing ? '⏸' : '▶';
+      btn.classList.toggle('is-playing', playing);
+      btn.setAttribute('aria-label', playing ? 'Pauzeren' : 'Afspelen');
+    }
+
+    // Resync ALL preview buttons against the current audio element state.
+    // Called on every play/pause/ended event so admins always see the right
+    // icon — including the case where they hit pause on the mini-player
+    // bar instead of the row's button.
+    function resyncAll() {
+      const audio = document.getElementById('audio-element');
+      const playing = audio && !audio.paused && !audio.ended;
+      const currentSrc = audio ? audio.src : '';
+      buttons.forEach(b => {
+        const isThisOne = playing && currentSrc.endsWith(b.dataset.streamUrl);
+        setIcon(b, isThisOne);
+      });
+    }
+
+    buttons.forEach(btn => {
+      btn.addEventListener('click', e => {
+        e.preventDefault();
+        const player = window.pcmsAudioPlayer;
+        if (!player) {
+          console.warn('[admin-audio] miniplayer not available');
+          return;
+        }
+        const url = btn.dataset.streamUrl;
+        if (!url) return;
+
+        // Build track metadata from the row's DOM so the mini-player shows
+        // useful info (title/artist/album/cover) without an extra API call.
+        const row = btn.closest('.ax-track');
+        const titleEl  = row && row.querySelector('[data-cell="title"]');
+        const artistEl = row && row.querySelector('[data-cell="artist"]');
+        const albumEl  = row && row.querySelector('[data-cell="album"]');
+        const coverImg = row && row.querySelector('img[data-cover-thumb]');
+        const track = {
+          url,
+          title:  titleEl  ? titleEl.textContent.trim()  : 'Track',
+          artist: artistEl ? artistEl.textContent.trim() : '',
+          album:  albumEl  ? albumEl.textContent.trim()  : '',
+          cover:  coverImg ? coverImg.src                : '',
+        };
+
+        // If this exact track is already playing, toggle pause/play instead
+        // of restarting from zero.
+        const audio = document.getElementById('audio-element');
+        if (audio && audio.src.endsWith(url)) {
+          if (audio.paused) player.play();
+          else              player.pause();
+          return;
+        }
+
+        player.setQueue([track], 0);
+      });
+    });
+
+    // Hook the global audio element's events to keep the row buttons synced.
+    // We attach lazily after the player has built its DOM. The script in
+    // shell.ejs runs at page-load so #audio-element exists by the time
+    // this IIFE fires (script tag is below the body content).
+    const audio = document.getElementById('audio-element');
+    if (audio) {
+      ['play', 'pause', 'ended', 'loadstart', 'emptied'].forEach(ev => {
+        audio.addEventListener(ev, resyncAll);
+      });
+      // Initial state on page load (e.g. user navigated back while a track
+      // was already playing — buttons should reflect that).
+      resyncAll();
+    }
+  })();
+
+
+  // ── Bulk upload (drag-drop + sequential transcoding) ─────────
+  // Files dropped or picked are queued (not uploaded immediately) so the
+  // user can review the list, set shared metadata, then hit "Start upload".
+  // The loop POSTs one file at a time to /admin/audio/upload with
+  // Accept: application/json so the server returns structured per-file
+  // results instead of redirecting.
+  const dropzone   = document.getElementById('audio-dropzone');
+  const fileInput  = document.getElementById('audio-files');
+  const queueEl    = document.getElementById('upload-queue');
+  const actionsEl  = document.getElementById('upload-actions');
+  const startBtn   = document.getElementById('start-upload-btn');
+  const clearBtn   = document.getElementById('clear-queue-btn');
+  const artistInput= document.getElementById('batch-artist');
+  const albumInput = document.getElementById('batch-album');
+  const coverInput = document.getElementById('batch-cover');
+
+  /** @type {Array<{file: File, el: HTMLElement, status: string}>} */
+  const queue = [];
+
+  function fmtBytes(n) {
+    if (n < 1024) return n + ' B';
+    if (n < 1024 * 1024) return (n / 1024).toFixed(0) + ' KB';
+    return (n / 1024 / 1024).toFixed(1) + ' MB';
+  }
+
+  function setItemStatus(item, status, label) {
+    const labels = {
+      queued:      'Wachten',
+      uploading:   'Uploaden…',
+      transcoding: 'Converteren…',
+      done:        '✓ Klaar',
+      error:       '✗ Fout',
+    };
+    item.status = status;
+    const badge = item.el.querySelector('.ax-queue-status');
+    badge.className = 'ax-queue-status ax-queue-status--' + status;
+    badge.textContent = label || labels[status] || status;
+    // Restyle the row
+    item.el.classList.remove(
+      'ax-queue-item--active', 'ax-queue-item--done', 'ax-queue-item--error'
+    );
+    if (status === 'uploading' || status === 'transcoding') item.el.classList.add('ax-queue-item--active');
+    else if (status === 'done')  item.el.classList.add('ax-queue-item--done');
+    else if (status === 'error') item.el.classList.add('ax-queue-item--error');
+  }
+
+  function addFiles(files) {
+    let added = 0;
+    for (const file of files) {
+      if (!file.type.startsWith('audio/') &&
+          !/\.(mp3|m4a|ogg|opus|flac|wav|webm|aac|oga|mp4)$/i.test(file.name)) {
+        // Silently skip non-audio drops; keeps the UX uncluttered.
+        continue;
+      }
+      const li = document.createElement('li');
+      li.className = 'ax-queue-item';
+      li.innerHTML =
+        '<div class="ax-queue-name"></div>' +
+        '<span class="ax-queue-status ax-queue-status--queued">Wachten</span>';
+      // Use textContent to avoid HTML-injection if a filename contains markup.
+      li.querySelector('.ax-queue-name').textContent = file.name;
+      // Append size hint inline
+      const size = document.createElement('span');
+      size.className = 'ax-queue-size';
+      size.textContent = fmtBytes(file.size);
+      li.querySelector('.ax-queue-name').appendChild(size);
+      queueEl.appendChild(li);
+      queue.push({ file, el: li, status: 'queued' });
+      added++;
+    }
+    if (added) {
+      queueEl.hidden = false;
+      actionsEl.hidden = false;
+    }
+  }
+
+  // ── Drop-zone events ─────────────────────────────────────────
+  // Page-level guard: a dropped file outside the zone would otherwise
+  // make the browser navigate to it (e.g. opening the audio inline), which
+  // discards typed metadata. We swallow drops anywhere unless the dropzone
+  // explicitly handles them.
+  ['dragover', 'drop'].forEach(ev => {
+    window.addEventListener(ev, e => {
+      // Allow drops INSIDE the dropzone — its own listener handles those.
+      if (dropzone.contains(e.target)) return;
+      e.preventDefault();
+    });
+  });
+
+  ['dragenter', 'dragover'].forEach(ev => {
+    dropzone.addEventListener(ev, e => {
+      e.preventDefault();
+      dropzone.classList.add('is-dragover');
+    });
+  });
+  ['dragleave', 'drop'].forEach(ev => {
+    dropzone.addEventListener(ev, e => {
+      e.preventDefault();
+      dropzone.classList.remove('is-dragover');
+    });
+  });
+  dropzone.addEventListener('drop', e => {
+    if (e.dataTransfer && e.dataTransfer.files) addFiles(e.dataTransfer.files);
+  });
+  fileInput.addEventListener('change', () => {
+    addFiles(fileInput.files);
+    // Reset so the same file can be picked again later if user wants
+    fileInput.value = '';
+  });
+
+  // ── Clear queue button ───────────────────────────────────────
+  clearBtn.addEventListener('click', () => {
+    // Only remove items that aren't currently uploading (anything queued
+    // or already finished). An in-progress upload finishes, then its row
+    // would also disappear once we re-render — but we keep it simple and
+    // just refuse to clear during an active run.
+    if (startBtn.disabled) return;
+    queue.length = 0;
+    queueEl.innerHTML = '';
+    queueEl.hidden = true;
+    actionsEl.hidden = true;
+  });
+
+  // ── Sequential upload loop ───────────────────────────────────
+  startBtn.addEventListener('click', async () => {
+    if (startBtn.disabled) return;
+    startBtn.disabled = true;
+    clearBtn.disabled = true;
+    dropzone.style.pointerEvents = 'none';
+    dropzone.style.opacity = '0.5';
+
+    const sharedArtist = artistInput.value.trim();
+    const sharedAlbum  = albumInput.value.trim();
+    const sharedCover  = coverInput.files && coverInput.files[0];
+
+    // Process queued items one at a time. We iterate via index so that
+    // if more files get dropped during the run they ALSO get processed
+    // (queue.push above mutates the same array we're iterating).
+    for (let i = 0; i < queue.length; i++) {
+      const item = queue[i];
+      if (item.status !== 'queued') continue;
+      try {
+        await uploadOne(item, sharedArtist, sharedAlbum, sharedCover);
+      } catch (err) {
+        console.error('upload failed for', item.file.name, err);
+        setItemStatus(item, 'error', '✗ ' + (err.message || 'Mislukt'));
+      }
+    }
+
+    startBtn.disabled = false;
+    clearBtn.disabled = false;
+    dropzone.style.pointerEvents = '';
+    dropzone.style.opacity = '';
+
+    // Reload the page so the new tracks appear in the list below.
+    // Could also fetch them and inject, but a full reload is simpler and
+    // ensures position indexes / album-grouping are correct.
+    const anyDone = queue.some(q => q.status === 'done');
+    if (anyDone) {
+      setTimeout(() => location.reload(), 700);
+    }
+  });
+
+  async function uploadOne(item, sharedArtist, sharedAlbum, sharedCover) {
+    setItemStatus(item, 'uploading');
+
+    const fd = new FormData();
+    fd.append('audio', item.file);
+    if (sharedArtist) fd.append('artist', sharedArtist);
+    if (sharedAlbum)  fd.append('album',  sharedAlbum);
+    if (sharedCover)  fd.append('cover',  sharedCover);
+    // Title is intentionally omitted — server uses filename fallback.
+
+    // We can't reliably distinguish "still uploading bytes" from
+    // "uploading done, ffmpeg running" without progress events, but the
+    // status flips to "Converteren…" once the request is past upload phase.
+    // We approximate this by waiting until the response arrives — by then
+    // both phases are complete on the server side. For a smoother feel we
+    // briefly show "transcoding" near the end of the request lifecycle.
+    const transcodeHint = setTimeout(() => {
+      if (item.status === 'uploading') setItemStatus(item, 'transcoding');
+    }, 1500);
+
+    try {
+      const res = await fetch('/admin/audio/upload', {
+        method: 'POST',
+        headers: { 'Accept': 'application/json' },
+        body: fd,
+        credentials: 'same-origin',
+      });
+      clearTimeout(transcodeHint);
+
+      // Server responds with JSON for our Accept header. If it didn't
+      // (e.g. session expired and got an HTML login page), surface that.
+      let data;
+      try { data = await res.json(); }
+      catch (_) { throw new Error('Onverwacht serverantwoord (' + res.status + ')'); }
+
+      if (!res.ok || !data.ok) {
+        throw new Error(data.error || ('HTTP ' + res.status));
+      }
+
+      setItemStatus(item, 'done', '✓ ' + (data.title || 'Klaar'));
+    } catch (err) {
+      clearTimeout(transcodeHint);
+      throw err;
+    }
+  }
+
+  // ── Click-to-copy embed codes ─────────────────────────────────
+  document.querySelectorAll('[data-copy]').forEach(el => {
+    el.addEventListener('click', async () => {
+      const text = el.dataset.copy;
+      try {
+        await navigator.clipboard.writeText(text);
+        el.classList.add('is-copied');
+        const original = el.textContent;
+        el.textContent = '✓ gekopieerd';
+        setTimeout(() => {
+          el.classList.remove('is-copied');
+          el.textContent = original;
+        }, 1200);
+      } catch (_) { /* fall through — selection still works */ }
+    });
+  });
+
+  // ── Wire all "Edit" buttons to the track-editor modal ─────────
+  // After save we patch the row in-place rather than reloading,
+  // so the user keeps their scroll position on long lists.
+  document.querySelectorAll('[data-track-edit]').forEach(btn => {
+    btn.addEventListener('click', () => {
+      if (typeof window.openTrackEditor !== 'function') {
+        alert('Track editor niet geladen');
+        return;
+      }
+      const id = btn.dataset.id;
+      window.openTrackEditor({
+        id,
+        onSaved: (track) => {
+          const row = document.querySelector('li[data-track-id="' + id + '"]');
+          if (!row) return;
+          // Update visible cells
+          const titleEl  = row.querySelector('[data-cell="title"]');
+          const artistEl = row.querySelector('[data-cell="artist"]');
+          const albumEl  = row.querySelector('[data-cell="album"]');
+          if (titleEl)  titleEl.textContent  = track.title  || '(zonder titel)';
+          if (artistEl) artistEl.textContent = track.artist || '—';
+          if (albumEl)  {
+            albumEl.textContent = track.album || '';
+            if (track.album) albumEl.removeAttribute('hidden');
+            else albumEl.setAttribute('hidden', '');
+          }
+          // Update cover thumb (replace element if type changed)
+          const oldThumb = row.querySelector('[data-cover-thumb]');
+          if (oldThumb) {
+            const parent = oldThumb.parentElement;
+            if (track.cover_url) {
+              const img = document.createElement('img');
+              img.className = 'ax-track-cover';
+              img.src = track.cover_url;
+              img.alt = '';
+              img.dataset.coverThumb = '';
+              parent.replaceChild(img, oldThumb);
+            } else {
+              const sp = document.createElement('span');
+              sp.className = 'ax-track-cover ax-track-cover-empty';
+              sp.textContent = '♫';
+              sp.dataset.coverThumb = '';
+              parent.replaceChild(sp, oldThumb);
+            }
+          }
+        },
+      });
+    });
+  });
+})();
+</script>
Index: src/views/pages/admin-comments.ejs
===================================================================
--- src/views/pages/admin-comments.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
+++ src/views/pages/admin-comments.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -0,0 +1,107 @@
+<div class="container admin-comments-page">
+  <p><a href="/admin" class="btn">&larr; Admin</a></p>
+  <h1>Comment moderation</h1>
+
+  <p class="muted">
+    Mode for this site: <strong><%= moderationMode %></strong>
+    <% if (moderationMode === 'trust') { %>
+      &mdash; comments are auto-approved. Switch to <em>moderate</em> in
+      <a href="/admin/sites/<%= site.slug %>/edit">site settings</a> to queue them.
+    <% } else { %>
+      &mdash; new comments require approval before showing up on posts.
+    <% } %>
+  </p>
+
+  <% if (success) { %><div class="audio-flash audio-flash--ok"><%= success %></div><% } %>
+  <% if (error)   { %><div class="audio-flash audio-flash--err"><%= error %></div><% } %>
+
+  <h2>Pending (<%= pending.length %>)</h2>
+  <% if (!pending.length) { %>
+    <p class="muted">Nothing waiting.</p>
+  <% } else { %>
+    <ol class="moderation-list">
+      <% pending.forEach(function(c) { %>
+        <li class="moderation-item">
+          <div class="moderation-meta">
+            <strong><%= c.author_username %></strong>
+            <% if (c.parent_comment_id) { %><span class="muted">(reply)</span><% } %>
+            on <a href="/<%= c.post_slug %>"><%= c.post_title || c.post_slug %></a>
+            <span class="muted">&middot; <%= formatDateTime(c.created_at) %></span>
+          </div>
+          <blockquote class="moderation-content"><%= c.content %></blockquote>
+          <div class="moderation-actions">
+            <form method="post" action="/admin/comments/<%= c.id %>/approve" style="display:inline">
+              <button type="submit" class="btn btn-primary">Approve</button>
+            </form>
+            <form method="post" action="/admin/comments/<%= c.id %>/reject" style="display:inline">
+              <button type="submit" class="btn btn-danger">Reject</button>
+            </form>
+          </div>
+        </li>
+      <% }); %>
+    </ol>
+  <% } %>
+
+  <h2>Recent decisions</h2>
+  <% if (!recent.length) { %>
+    <p class="muted">Nothing yet.</p>
+  <% } else { %>
+    <table class="admin-table">
+      <thead>
+        <tr><th>Author</th><th>Post</th><th>Status</th><th>Date</th></tr>
+      </thead>
+      <tbody>
+        <% recent.forEach(function(c) { %>
+          <tr class="status-<%= c.status %>">
+            <td><%= c.author_username %></td>
+            <td><a href="/<%= c.post_slug %>"><%= c.post_title || c.post_slug %></a></td>
+            <td>
+              <span class="status-pill status-pill--<%= c.status %>"><%= c.status %></span>
+            </td>
+            <td><%= formatDateTime(c.created_at) %></td>
+          </tr>
+        <% }); %>
+      </tbody>
+    </table>
+  <% } %>
+</div>
+
+<style>
+.admin-comments-page { max-width: 900px; margin: 3rem auto; padding: 0 1rem; }
+.admin-comments-page h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: 0 0 1rem; }
+.admin-comments-page h2 { font-family: var(--font-display, serif); font-size: 1.25rem; margin: 1.5rem 0 0.75rem; }
+.muted { color: var(--ink-muted, var(--ink-soft)); }
+.muted a { color: var(--accent); }
+
+.moderation-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 1rem; }
+.moderation-item {
+  background: var(--paper-2);
+  border: 1px solid var(--rule);
+  border-radius: 8px;
+  padding: 1rem;
+}
+.moderation-meta { font-size: 0.85rem; margin-bottom: 0.5rem; color: var(--ink-soft); }
+.moderation-meta a { color: var(--accent); }
+.moderation-content {
+  margin: 0 0 0.75rem;
+  padding: 0.5rem 0.75rem;
+  border-left: 3px solid var(--accent);
+  background: var(--paper);
+  white-space: pre-wrap;
+  word-wrap: break-word;
+  color: var(--ink);
+}
+.moderation-actions { display: flex; gap: 0.5rem; }
+
+.status-pill {
+  display: inline-block; padding: 0.1rem 0.5rem; border-radius: 10px;
+  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em;
+}
+.status-pill--approved { background: rgba(40, 160, 90, 0.15); color: #2a9d5e; }
+.status-pill--rejected { background: rgba(200, 60, 60, 0.15); color: #c33; }
+.status-pill--pending  { background: var(--paper); color: var(--ink-muted, var(--ink-soft)); }
+
+.audio-flash { padding: 0.75rem 1rem; border-radius: 6px; margin: 1rem 0; font-size: 0.9rem; }
+.audio-flash--ok { background: rgba(40, 160, 90, 0.15); color: #2a9d5e; border: 1px solid rgba(40, 160, 90, 0.3); }
+.audio-flash--err { background: rgba(200, 60, 60, 0.15); color: #c33; border: 1px solid rgba(200, 60, 60, 0.3); }
+</style>
Index: src/views/pages/admin-epk.ejs
===================================================================
--- src/views/pages/admin-epk.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,92 +1,0 @@
-<div class="container admin-page">
-  <h1><%= t('aepk.title') %></h1>
-  <p><a href="/admin" class="btn">&larr; <%= t('aepk.back_admin') %></a> <a href="/pers" class="btn" target="_blank" rel="noopener">📰 <%= t('aepk.view_epk') %></a></p>
-
-  <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
-
-  <section class="set-card">
-    <h2><%= t('aepk.text_heading') %></h2>
-    <p class="set-help">
-      <%= t('aepk.text_help') %>
-    </p>
-
-    <form method="post" action="/admin/epk" class="set-form">
-      <label class="set-field">
-        <span><%= t('aepk.bio_label') %></span>
-        <textarea name="epk_bio" rows="5" maxlength="1000"
-                  placeholder="<%= t('aepk.bio_ph') %>"><%= epkBio %></textarea>
-      </label>
-      <label class="set-field">
-        <span><%= t('aepk.contact_label') %></span>
-        <input type="text" name="epk_contact" maxlength="300"
-               placeholder="<%= t('aepk.contact_ph') %>"
-               value="<%= epkContact %>">
-      </label>
-
-      <div class="set-field">
-        <span><%= t('aepk.tracks_label') %> <small><%= t('aepk.tracks_hint', { n: maxEpkTracks }) %></small></span>
-        <% if (!allTracks.length) { %>
-          <p class="set-help" style="margin:0"><%= t('aepk.no_tracks') %></p>
-        <% } else { %>
-          <% var _untitled = t('aepk.untitled'); %>
-          <div class="epk-track-pick" data-max="<%= maxEpkTracks %>">
-            <% allTracks.forEach(function(t){ %>
-              <label class="epk-pick-item">
-                <input type="checkbox" name="epk_tracks" value="<%= t.id %>" <%= chosenTracks.indexOf(t.id) >= 0 ? 'checked' : '' %>>
-                <span class="epk-box" aria-hidden="true">✓</span>
-                <span class="epk-pick-title"><%= t.title || _untitled %></span>
-                <% if (t.artist) { %><span class="epk-pick-artist"><%= t.artist %></span><% } %>
-              </label>
-            <% }); %>
-          </div>
-        <% } %>
-      </div>
-
-      <button type="submit" class="btn btn-primary"><%= t('aepk.save') %></button>
-    </form>
-  </section>
-</div>
-
-<style>
-  .admin-page .set-card { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 12px; padding: 1.25rem; margin-top: 1rem; }
-  .admin-page .set-card h2 { font-family: var(--font-display, serif); font-size: 1.25rem; margin: 0 0 0.5rem; }
-  .admin-page .set-help { font-size: 0.85rem; color: var(--ink-soft, var(--ink-muted)); line-height: 1.5; margin: 0 0 1rem; max-width: 60ch; }
-  .admin-page .set-form { display: flex; flex-direction: column; gap: 1rem; max-width: 520px; }
-  .admin-page .set-field { display: flex; flex-direction: column; gap: 0.3rem; }
-  .admin-page .set-field > span { font-size: 0.8rem; font-weight: 600; color: var(--ink-soft, var(--ink-muted)); }
-  .admin-page .set-field input,
-  .admin-page .set-field textarea {
-    width: 100%; box-sizing: border-box;
-    padding: 0.6rem 0.75rem;
-    border: 1px solid var(--rule); border-radius: 6px;
-    background: var(--paper); color: var(--ink);
-    font-family: var(--font-ui, system-ui), sans-serif; font-size: 0.95rem;
-    -webkit-appearance: none; appearance: none;
-  }
-  .admin-page .set-field textarea { resize: vertical; line-height: 1.5; }
-  .admin-page .set-field input:focus,
-  .admin-page .set-field textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
-  .admin-page .set-form .btn { align-self: flex-start; }
-  .admin-page .epk-track-pick { display: flex; flex-direction: column; gap: 2px; max-height: 300px; overflow-y: auto; border: 1px solid var(--rule); border-radius: 8px; padding: 4px; }
-  .admin-page .epk-pick-item { display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 0.6rem; border-radius: 6px; cursor: pointer; border: 1px solid transparent; }
-  .admin-page .epk-pick-item:hover { background: var(--paper); }
-  /* Native checkbox verbergen; we tekenen een eigen, duidelijk vakje (.epk-box). */
-  .admin-page .epk-pick-item input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }
-  .admin-page .epk-box {
-    flex: 0 0 auto; width: 20px; height: 20px;
-    border: 2px solid var(--rule); border-radius: 5px;
-    display: grid; place-items: center;
-    color: transparent; font-size: 13px; font-weight: 900; line-height: 1;
-  }
-  .admin-page .epk-pick-item:has(input:checked) .epk-box { background: var(--accent); border-color: var(--accent); color: #fff; }
-  /* Aangevinkte rij ook getint. */
-  .admin-page .epk-pick-item:has(input:checked) {
-    background: color-mix(in srgb, var(--accent) 14%, transparent);
-    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
-  }
-  .admin-page .epk-pick-item:has(input:disabled) { opacity: .45; cursor: not-allowed; }
-  .admin-page .epk-pick-title { font-weight: 600; }
-  .admin-page .epk-pick-artist { color: var(--ink-muted, var(--ink-soft)); font-size: 0.85rem; }
-  .admin-page .epk-pick-item input:disabled ~ .epk-pick-title { opacity: 0.45; }
-</style>
-<%# Het script van deze pagina staat in assets/js/mod/admin-epk.js (shaer-bqr). %>
Index: src/views/pages/admin-help.ejs
===================================================================
--- src/views/pages/admin-help.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,152 +1,0 @@
-<div class="container admin-page hb">
-  <p><a href="/admin" class="btn">&larr; <%= t('ahelp.back') %></a></p>
-  <h1><%= t('ahelp.title') %></h1>
-  <p class="hb-intro"><%= t('ahelp.intro') %></p>
-
-  <div class="hb-search">
-    <input type="search" id="hb-q" placeholder="<%= t('ahelp.search_placeholder') %>" autocomplete="off" aria-label="<%= t('ahelp.search_aria') %>">
-  </div>
-  <p class="hb-count" id="hb-count" aria-live="polite"></p>
-
-  <div class="hb-list">
-
-    <section class="hb-item">
-      <h2>✍️ <%= t('ahelp.s_newpost_h') %></h2>
-      <p><%- t('ahelp.s_newpost_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>📝 <%= t('ahelp.s_excerpt_h') %></h2>
-      <p><%- t('ahelp.s_excerpt_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>📌 <%= t('ahelp.s_pin_h') %></h2>
-      <p><%- t('ahelp.s_pin_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>🗓️ <%= t('ahelp.s_schedule_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
-      <p><%- t('ahelp.s_schedule_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>🖼️ <%= t('ahelp.s_images_h') %></h2>
-      <p><%- t('ahelp.s_images_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>🎵 <%= t('ahelp.s_audio_h') %></h2>
-      <p><%- t('ahelp.s_audio_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>© <%= t('ahelp.s_credit_h') %></h2>
-      <p><%- t('ahelp.s_credit_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>⬇ <%= t('ahelp.s_downloads_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
-      <p><%- t('ahelp.s_downloads_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>📃 <%= t('ahelp.s_albums_h') %></h2>
-      <p><%- t('ahelp.s_albums_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>📅 <%= t('ahelp.s_agenda_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
-      <p><%- t('ahelp.s_agenda_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>📰 <%= t('ahelp.s_presskit_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
-      <p><%- t('ahelp.s_presskit_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>🔗 <%= t('ahelp.s_circles_h') %></h2>
-      <p><%- t('ahelp.s_circles_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>🌐 <%= t('ahelp.s_fedi_h') %></h2>
-      <p><%- t('ahelp.s_fedi_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>📊 <%= t('ahelp.s_stats_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
-      <p><%- t('ahelp.s_stats_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>✉️ <%= t('ahelp.s_newsletter_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
-      <p><%- t('ahelp.s_newsletter_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>🔗 <%= t('ahelp.s_linkbio_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
-      <p><%- t('ahelp.s_linkbio_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>▶️ <%= t('ahelp.s_embed_h') %> <small>(<%= t('ahelp.premium') %>)</small></h2>
-      <p><%- t('ahelp.s_embed_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>🎨 <%= t('ahelp.s_appearance_h') %></h2>
-      <p><%- t('ahelp.s_appearance_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>👥 <%= t('ahelp.s_tenancy_h') %></h2>
-      <p><%- t('ahelp.s_tenancy_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>⭐ <%= t('ahelp.s_premium_h') %></h2>
-      <p><%- t('ahelp.s_premium_b') %></p>
-    </section>
-
-
-    <section class="hb-item">
-      <h2>🔑 <%= t('ahelp.s_password_h') %></h2>
-      <p><%- t('ahelp.s_password_b') %></p>
-    </section>
-
-    <section class="hb-item">
-      <h2>🔄 <%= t('ahelp.s_updates_h') %></h2>
-      <p><%- t('ahelp.s_updates_b') %></p>
-    </section>
-
-  </div>
-
-  <p class="hb-empty" id="hb-empty" hidden><%= t('ahelp.empty') %></p>
-</div>
-
-<style>
-  .hb { max-width: 760px; }
-  .hb h1 { margin-bottom: .25rem; }
-  .hb-intro { color: var(--ink-muted, var(--ink-soft)); margin: 0 0 1rem; }
-  .hb-search input {
-    width: 100%; box-sizing: border-box;
-    padding: 0.7rem 0.9rem; font-size: 1rem;
-    border: 1px solid var(--rule); border-radius: 999px;
-    background: var(--paper); color: var(--ink);
-    -webkit-appearance: none; appearance: none;
-  }
-  .hb-search input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
-  .hb-count { color: var(--ink-muted, var(--ink-soft)); font-size: .82rem; margin: .5rem 0 1rem; min-height: 1em; }
-  .hb-list { display: flex; flex-direction: column; gap: .75rem; }
-  .hb-item { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 12px; padding: 1rem 1.1rem; }
-  .hb-item h2 { font-family: var(--font-display, serif); font-size: 1.15rem; margin: 0 0 .4rem; }
-  .hb-item h2 small { font-weight: 400; font-size: .72rem; color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); border-radius: 999px; padding: .05rem .45rem; margin-left: .35rem; vertical-align: middle; }
-  .hb-item p { margin: 0; line-height: 1.55; color: var(--ink); }
-  .hb-item code { background: var(--paper); padding: .08rem .35rem; border-radius: 4px; font-size: .88em; }
-  .hb-item.hb-hidden { display: none; }
-  .hb-item mark { background: color-mix(in srgb, var(--accent) 35%, transparent); color: inherit; border-radius: 2px; }
-  .hb-empty { color: var(--ink-muted, var(--ink-soft)); padding: 1rem; text-align: center; }
-</style>
-<%# Het script van deze pagina staat in assets/js/mod/admin-help.js (shaer-bqr). %>
Index: src/views/pages/admin-listeners.ejs
===================================================================
--- src/views/pages/admin-listeners.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,53 +1,0 @@
-<%# Beheer -> Media -> Luisteraars (shaer-0nh).
-
-    Deze pagina had zijn eigen <style>-blok met eigen klassen (.lis-*) en week
-    daarmee af van de vier tabs ernaast -- andere breedte, andere kop, andere
-    rij. Nu op dezelfde ax-*-romp; wat overblijft is een lijst zoals audio en
-    playlists er ook een hebben. %>
-<%- include('../partials/admin-styles') %>
-<div class="container ax-page">
-
-  <%- include('../partials/admin-back') %>
-  <%- include('../partials/media-tabs', { active: 'listeners', audioOn: audioOn }) %>
-  <header class="ax-header">
-    <div>
-      <h1><%= t('alis.lis_title') %></h1>
-      <%# Waarom dit een eigen tab is en niet een lijstje bij de volgers: dit
-          zijn accounts die aan de BIBLIOTHEEK hangen. Ze krijgen de muziek en
-          met opzet niet de gewone posts, en dat verschil hoort zichtbaar te
-          zijn -- ook voor wie later moet begrijpen waarom ze niet in de
-          volgerslijst staan. %>
-      <p class="ax-tagline"><%= t('alis.lis_intro') %></p>
-    </div>
-  </header>
-
-  <% if (!luisteraars.length) { %>
-    <div class="ax-empty">
-      <div class="ax-empty-icon">&#127911;</div>
-      <p><%= t('alis.lis_empty') %></p>
-    </div>
-  <% } else { %>
-    <p class="ax-tagline" style="margin-bottom:.75rem;"><strong><%= luisteraars.length %></strong> <%= t('alis.lis_count') %></p>
-    <ul class="ax-list">
-      <% luisteraars.forEach(function (l) { %>
-        <li class="ax-track">
-          <div class="ax-track-cover">
-            <% if (l.icon) { %><img src="<%= l.icon %>" alt="" loading="lazy" decoding="async"><% } else { %>&#9835;<% } %>
-          </div>
-          <div class="ax-track-meta">
-            <div class="ax-track-title">
-              <%= l.name || l.handle || l.actor_uri %>
-              <% if (l.last_error_at) { %><span class="ax-pill"><%= t('alis.lis_error') %></span><% } %>
-            </div>
-            <div class="ax-track-sub">
-              <% if (l.handle) { %><span><%= l.handle %></span><span class="ax-track-sep">&middot;</span><% } %>
-              <span><%= t('alis.lis_since') %> <%= formatDate(l.created_at) %></span>
-              <span class="ax-track-sep">&middot;</span>
-              <span><%= t('alis.lis_last') %>: <%= l.last_delivery_at ? formatDate(l.last_delivery_at) : t('alis.lis_never') %></span>
-            </div>
-          </div>
-        </li>
-      <% }); %>
-    </ul>
-  <% } %>
-</div>
Index: src/views/pages/admin-media.ejs
===================================================================
--- src/views/pages/admin-media.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,57 +1,0 @@
-<%# Beheer -> Media: de afbeeldingenbibliotheek met opruiming.
-    De audio-helft zit onder /admin/audio, als eigen tab.
-
-    Deze pagina stond volledig op inline styles en viel daarmee buiten het
-    ax-*-systeem waar audio en playlists al op zaten. Nu op dezelfde romp:
-    zelfde breedte, zelfde kop, tabs op dezelfde plek. %>
-<%- include('../partials/admin-styles') %>
-<div class="container ax-page">
-
-  <%- include('../partials/admin-back') %>
-  <%- include('../partials/media-tabs', { active: 'images', audioOn: audioOn }) %>
-  <header class="ax-header">
-    <div>
-      <h1><%= t('admin.t_media') %></h1>
-      <p class="ax-tagline">
-        <%= items.length %> <%= t('admin.media_count') %><% if (unusedCount) { %> &middot; <strong><%= unusedCount %></strong> <%= t('admin.media_unused') %><% } %>
-      </p>
-    </div>
-  </header>
-
-  <% if (success) { %><div class="ax-flash ax-flash-ok"><%= success %></div><% } %>
-
-  <% if (unusedCount) { %>
-  <div class="ax-actions-bar">
-    <button type="button" id="media-cleanup" class="ax-btn ax-btn-danger">&#128465; <%= t('admin.media_cleanup') %> (<%= unusedCount %>)</button>
-  </div>
-  <% } %>
-
-  <% if (!items.length) { %>
-    <div class="ax-empty">
-      <div class="ax-empty-icon">&#128247;</div>
-      <p><%= t('admin.media_empty') %></p>
-    </div>
-  <% } else { %>
-  <div class="ax-grid">
-    <% items.forEach(function(it){ %>
-      <div class="ax-tile media-card" data-file="<%= it.file %>">
-        <div class="ax-tile-media" style="aspect-ratio:1;">
-          <img src="<%= thumb(it.url, 320) %>" alt="" loading="lazy" decoding="async" style="object-fit:cover;height:100%;">
-          <% if (it.hasVideo) { %><span class="ax-tile-badge ax-tile-badge-r" title="animated cover">&#127916;</span><% } %>
-          <% if (!it.usedCount) { %><span class="ax-tile-badge"><%= t('admin.media_unused') %></span><% } %>
-        </div>
-        <div class="ax-tile-body">
-          <div class="ax-tile-meta"><%= it.kb %> KB &middot; <%= it.usedCount %>&times;</div>
-          <div class="ax-tile-actions">
-            <button type="button" class="ax-btn ax-btn-sm" data-copy="<%= it.url %>"><%= t('admin.media_copy') %></button>
-            <button type="button" class="ax-btn ax-btn-sm ax-btn-danger" data-del="<%= it.file %>">&#128465;</button>
-          </div>
-        </div>
-      </div>
-    <% }) %>
-  </div>
-  <% } %>
-</div>
-
-<%# Het script staat in assets/js/mod/admin-media.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
-<%- include('../partials/page-data', { pageData: { copy: t('admin.media_copy'), delC: t('admin.media_del_confirm'), cleanC: t('admin.media_cleanup_confirm') } }) %>
Index: src/views/pages/admin-migrate.ejs
===================================================================
--- src/views/pages/admin-migrate.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,329 +1,0 @@
-<div class="container mig-wrap">
-  <h1><%= t('mig.title') %></h1>
-  <p class="mig-lead"><%= t('mig.lead') %></p>
-
-  <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
-  <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
-
-  <%# ── Het stappenplan ────────────────────────────────────────── %>
-  <%# Bestaat omdat de VOLGORDE niet vanzelfsprekend is en een fout erin je
-      halverwege vastzet. Twee dingen die niemand raadt:
-        - de ophaalknop werkt PAS nadat de verhuizing is aangekondigd; de bron
-          geeft niets af aan een instantie die niet in zijn movedTo staat;
-        - met een zip kan het juist andersom, want een zip vraagt niemand iets.
-      En stap 5 staat erbij omdat hij bestaat, niet omdat er een knop voor is. %>
-  <section class="mig-card mig-plan">
-    <h2><%= t('mig.plan_title') %></h2>
-    <p class="form-hint"><%= t('mig.plan_hint') %></p>
-    <%# "hier" hangt af van WELKE kant je bekijkt. Deze pagina draait op allebei
-        de instanties, en een vaste tekst is dus op de helft van de schermen
-        onzin. Heeft deze site een moved_to, dan is dit de vertrekkende kant. %>
-    <%# Stap 1, 3 en 4 horen op de NIEUWE kant; stap 2 en 5 op de oude. Vanaf
-        welke instantie je kijkt bepaalt dus welk van de twee "hier" is. %>
-    <% var _hier = stap.isOud ? t('mig.plan_new')  : t('mig.plan_here');
-       var _daar = stap.isOud ? t('mig.plan_here') : t('mig.plan_old'); %>
-    <% if (stap.isOud) { %>
-      <p class="form-hint mig-warn"><%= t('mig.plan_from_old') %></p>
-    <% } %>
-    <ol class="mig-steps">
-      <li class="<%= stap.geclaimd ? 'is-done' : '' %>">
-        <strong><%= t('mig.plan_1') %></strong>
-        <span class="mig-waar"><%= _hier %></span>
-        <em><%= t('mig.plan_1_why') %></em>
-      </li>
-      <li class="<%= stap.aangekondigd ? 'is-done' : '' %>">
-        <strong><%= t('mig.plan_2') %></strong>
-        <span class="mig-waar"><%= _daar %></span>
-        <em><%= t('mig.plan_2_why') %></em>
-      </li>
-      <li class="<%= stap.opgehaald ? 'is-done' : '' %>">
-        <strong><%= t('mig.plan_3') %></strong>
-        <span class="mig-waar"><%= _hier %></span>
-        <em><%= t('mig.plan_3_why') %></em>
-      </li>
-      <li>
-        <strong><%= t('mig.plan_4') %></strong>
-        <span class="mig-waar"><%= _hier %></span>
-      </li>
-      <li class="mig-step-later">
-        <strong><%= t('mig.plan_5') %></strong>
-        <span class="mig-waar"><%= _daar %></span>
-        <em><%= t('mig.plan_5_why') %></em>
-      </li>
-    </ol>
-  </section>
-
-  <%# ── Je oude account claimen (FEP-7628 alsoKnownAs) ─────────── %>
-  <%# Bewust als EERSTE: zonder deze claim weigert de oude instantie de Move
-      met no_backreference, en kan de ophaalknop hieronder ook niets. Dit stond
-      op de site-bewerkpagina tussen de kleuren; daar zoekt niemand het als hij
-      aan het verhuizen is. %>
-  <section class="mig-card">
-    <h2><%= t('mig.alias_title') %></h2>
-    <p class="form-hint"><%= t('mig.alias_hint') %></p>
-    <form method="post" action="/admin/migrate/aliases">
-      <label for="mig-alias"><%= t('mig.alias_label') %></label>
-      <textarea id="mig-alias" name="ap_aliases" rows="3" class="mig-input"
-        placeholder="@jij@mastodon.social&#10;@jij@oude-klonkt.nl"><%= aliassen || '' %></textarea>
-      <p class="form-hint"><%= t('mig.alias_note') %></p>
-      <button type="submit" class="btn"><%= t('mig.alias_btn') %></button>
-    </form>
-  </section>
-
-  <%# ── Exporteren ─────────────────────────────────────────────── %>
-  <section class="mig-card">
-    <h2><%= t('mig.export_title') %></h2>
-    <p class="form-hint"><%= t('mig.export_hint') %></p>
-
-    <% if (telling) { %>
-      <ul class="mig-counts">
-        <li><strong><%= telling.posts %></strong> <%= t('mig.c_posts') %></li>
-        <li><strong><%= telling.replies %></strong> <%= t('mig.c_replies') %></li>
-        <li><strong><%= telling.media %></strong> <%= t('mig.c_media') %></li>
-        <%# Muziek APART tellen, niet onder "media". Dit ontbrak, en juist daardoor
-            kon een export van 140 nummers er als 4,5 MB uitzien zonder dat iemand
-            het merkte: de teller zei "39 mediabestanden" en zweeg over de rest. %>
-        <% if (telling.tracks) { %>
-          <%# Wat er ECHT meegaat, niet hoeveel rijen er in de database staan.
-              "8 nummers" met daaronder "van 3 ontbreekt het bestand" telt niet
-              op; 5 plus die waarschuwing wel. %>
-          <li><strong><%= telling.tracks - (telling.audioMissing || 0) %></strong> <%= t('mig.c_tracks') %></li>
-        <% } %>
-        <% if (telling.playlists) { %>
-          <li><strong><%= telling.playlists %></strong> <%= t('mig.c_playlists') %></li>
-        <% } %>
-        <% if (typeof telling.following === 'number') { %>
-          <li><strong><%= telling.following %></strong> <%= t('mig.c_following') %></li>
-        <% } %>
-        <li><strong><%= mb(telling.bytes) %></strong> <%= t('mig.c_size') %></li>
-      </ul>
-
-      <% if (telling.ontbrekend) { %>
-        <%# Niets stils: media die op schijf ontbreekt wordt geteld en gemeld, want
-            een archief dat stilzwijgend minder bevat is erger dan een waarschuwing. %>
-        <p class="alert alert-error"><%= t('mig.missing', { n: telling.ontbrekend }) %></p>
-      <% } %>
-
-      <% if (telling.audioMissing) { %>
-        <%# Een eigen zin voor muziek. "N mediaverwijzingen" zei niets over de
-            vraag die er toe doet: gaat mijn bibliotheek mee. %>
-        <p class="alert alert-error"><%= t('mig.audio_missing', { n: telling.audioMissing }) %></p>
-      <% } %>
-
-
-      <% if (telling.groot) { %>
-        <p class="alert alert-error"><%= t('mig.too_big') %></p>
-      <% } else { %>
-        <p><a class="btn btn-primary" href="/admin/migrate/export"><%= t('mig.export_btn') %></a></p>
-      <% } %>
-    <% } else { %>
-      <p class="form-hint"><%= t('mig.export_none') %></p>
-    <% } %>
-  </section>
-
-  <%# ── Rechtstreeks ophalen (FEP-1580) ────────────────────────── %>
-  <section class="mig-card">
-    <h2><%= t('mig.pull_title') %></h2>
-    <p class="form-hint"><%= t('mig.pull_hint') %></p>
-
-    <% if (haalVerslag) { %>
-      <div class="mig-report<%= haalVerslag.error ? '' : ' is-done' %>">
-        <% if (haalVerslag.error) { %>
-          <h3><%= t('mig.pull_failed') %></h3>
-          <%# Elke weigering krijgt een eigen zin. "Er ging iets mis" laat je
-              raden welke van de twee kanten van de afspraak ontbreekt, en dat
-              is precies wat je moet weten om het op te lossen. %>
-          <p><%= t('mig.e_' + haalVerslag.error) !== 'mig.e_' + haalVerslag.error
-                   ? t('mig.e_' + haalVerslag.error) : (haalVerslag.melding || haalVerslag.error) %></p>
-          <% if (haalVerslag.error === 'not_moved_here' && haalVerslag.movedTo) { %>
-            <p class="form-hint"><%= t('mig.e_points_at') %> <code><%= haalVerslag.movedTo %></code></p>
-          <% } %>
-        <% } else { %>
-          <h3><%= t('mig.pull_done') %></h3>
-          <ul class="mig-counts">
-            <li><strong><%= haalVerslag.posts %></strong> <%= t('mig.r_imported') %></li>
-            <% if (haalVerslag.overgeslagen) { %><li><strong><%= haalVerslag.overgeslagen %></strong> <%= t('mig.r_skipped') %></li><% } %>
-            <% if (haalVerslag.media) { %><li><strong><%= haalVerslag.media %></strong> <%= t('mig.r_media') %></li><% } %>
-            <% if (haalVerslag.blocks) { %><li><strong><%= haalVerslag.blocks %></strong> <%= t('mig.r_blocks') %></li><% } %>
-            <% if (haalVerslag.tracksBinnen) { %><li><strong><%= haalVerslag.tracksBinnen %></strong> <%= t('mig.c_tracks') %></li><% } %>
-            <%# Aangevuld telt apart van nieuw. Een tweede ronde die "0 erbij"
-                meldt terwijl hij drie nummers compleet maakte, leest als niets
-                gedaan. %>
-            <% if (haalVerslag.tracksBijgewerkt) { %><li><strong><%= haalVerslag.tracksBijgewerkt %></strong> <%= t('mig.r_tracks_updated') %></li><% } %>
-            <% if (haalVerslag.postsBijgewerkt) { %><li><strong><%= haalVerslag.postsBijgewerkt %></strong> <%= t('mig.r_posts_updated') %></li><% } %>
-            <% if (haalVerslag.linksBijgetrokken) { %><li><strong><%= haalVerslag.linksBijgetrokken %></strong> <%= t('mig.r_links_fixed') %></li><% } %>
-            <% if (haalVerslag.playlistsBinnen) { %><li><strong><%= haalVerslag.playlistsBinnen %></strong> <%= t('mig.c_playlists') %></li><% } %>
-          </ul>
-          <% if (haalVerslag.tracksMislukt) { %>
-            <p class="alert alert-error"><%= t('mig.r_tracks_missing', { n: haalVerslag.tracksMislukt }) %></p>
-          <% } %>
-        <% } %>
-        <% if (haalVerslag.waarschuwingen && haalVerslag.waarschuwingen.length) { %>
-          <ul class="mig-warnlist">
-            <% haalVerslag.waarschuwingen.slice(0, 6).forEach(function (w) { %><li><%= w %></li><% }); %>
-          </ul>
-        <% } %>
-      </div>
-    <% } %>
-
-    <form method="post" action="/admin/migrate/pull">
-      <label for="mig-bron"><%= t('mig.pull_source') %></label>
-      <input type="url" id="mig-bron" name="bron" value="<%= bron || '' %>"
-             placeholder="@jij@oude-klonkt.nl" class="mig-input">
-      <p class="form-hint"><%= t('mig.pull_source_hint') %></p>
-      <button type="submit" class="btn"><%= t('mig.pull_btn') %></button>
-    </form>
-
-    <% if (mig && (mig.total || mig.moves)) { %>
-      <p class="form-hint mig-state">
-        <%= t('mig.state', { n: mig.total }) %>
-        <%= mig.complete ? t('mig.state_done') : t('mig.state_busy') %>
-      </p>
-    <% } %>
-  </section>
-
-  <%# ── Importeren ─────────────────────────────────────────────── %>
-  <section class="mig-card">
-    <h2><%= t('mig.import_title') %></h2>
-    <p class="form-hint"><%= t('mig.import_hint') %></p>
-
-    <% if (verslag) { %>
-      <div class="mig-report<%= verslag.echt ? ' is-done' : '' %>">
-        <h3><%= verslag.echt ? t('mig.r_done') : t('mig.r_dry') %></h3>
-        <p class="form-hint"><%= verslag.bestand %> &middot; <%= mb(verslag.bytes) %></p>
-        <ul class="mig-counts">
-          <li><strong><%= verslag.posts %></strong> <%= verslag.echt ? t('mig.r_imported') : t('mig.r_would') %></li>
-          <li><strong><%= verslag.overgeslagen %></strong> <%= t('mig.r_skipped') %></li>
-          <% if (verslag.overschreven) { %><li><strong><%= verslag.overschreven %></strong> <%= t('mig.r_overwritten') %></li><% } %>
-          <% if (verslag.mediaMissing) { %><li><strong><%= verslag.mediaMissing %></strong> <%= t('mig.r_media_missing') %></li><% } %>
-          <% if (verslag.media) { %><li><strong><%= verslag.media %></strong> <%= t('mig.r_media') %></li><% } %>
-          <%# Ook hier: muziek apart. Zonder deze twee regels zie je aan de
-              ontvangende kant net zo min of je bibliotheek is aangekomen. %>
-          <% if (verslag.tracks) { %><li><strong><%= verslag.tracks %></strong> <%= t('mig.c_tracks') %></li><% } %>
-          <% if (verslag.playlists) { %><li><strong><%= verslag.playlists %></strong> <%= t('mig.c_playlists') %></li><% } %>
-          <% if (verslag.linksBijgetrokken) { %><li><strong><%= verslag.linksBijgetrokken %></strong> <%= t('mig.r_links_fixed') %></li><% } %>
-        </ul>
-        <% if (verslag.tracksMissing) { %>
-          <p class="alert alert-error"><%= t('mig.r_tracks_missing', { n: verslag.tracksMissing }) %></p>
-        <% } %>
-        <% if (verslag.waarschuwingen && verslag.waarschuwingen.length) { %>
-          <ul class="mig-warnlist">
-            <% verslag.waarschuwingen.slice(0, 6).forEach(function (w) { %><li><%= w %></li><% }); %>
-          </ul>
-        <% } %>
-        <% if (verslag.idsBehouden === false) { %>
-          <%# Dit is de belangrijkste regel van het hele verslag: komt het archief
-              van een ANDER adres, dan krijgen de posts nieuwe id's en zijn het
-              nieuwe objecten voor de fediverse. %>
-          <p class="alert alert-error"><%= t('mig.r_new_ids') %></p>
-        <% } %>
-
-        <% if (!verslag.echt && verslag.posts > 0) { %>
-          <form method="post" action="/admin/migrate/import" enctype="multipart/form-data" class="mig-confirm">
-            <p class="form-hint"><%= t('mig.r_confirm_hint') %></p>
-            <label for="mig-file2"><%= t('mig.file_label') %></label>
-            <input type="file" id="mig-file2" name="archief" accept=".zip,application/zip" required>
-            <input type="hidden" name="echt" value="1">
-            <% if (verslag.overschrijf) { %><input type="hidden" name="overschrijf" value="1"><% } %>
-            <button type="submit" class="btn btn-primary"><%= t('mig.r_confirm_btn') %></button>
-          </form>
-        <% } %>
-      </div>
-    <% } %>
-
-    <form method="post" action="/admin/migrate/import" enctype="multipart/form-data">
-      <label for="mig-file"><%= t('mig.file_label') %></label>
-      <input type="file" id="mig-file" name="archief" accept=".zip,application/zip" required>
-
-      <label class="mig-check">
-        <input type="checkbox" name="overschrijf" value="1">
-        <%= t('mig.overwrite_label') %>
-      </label>
-      <p class="form-hint mig-warn"><%= t('mig.overwrite_hint') %></p>
-
-      <button type="submit" class="btn"><%= t('mig.check_btn') %></button>
-      <p class="form-hint"><%= t('mig.check_hint') %></p>
-    </form>
-  </section>
-
-  <%# ── De verhuizing aankondigen (FEP-7628 Move) ──────────────── %>
-  <%# ONDERAAN, en met een rode knop. Dit is de enige knop op deze pagina die
-      de fediverse in gaat en niet terug te draaien is: je volgers verhuizen
-      mee en dit account gaat op slot. Alles hierboven kun je overdoen. %>
-  <section class="mig-card mig-card-danger">
-    <h2><%= t('mig.move_title') %></h2>
-    <p class="form-hint"><%= t('mig.move_hint') %></p>
-    <% if (movedTo) { %>
-      <p class="alert alert-error"><%= t('mig.move_done') %> <code><%= movedTo %></code></p>
-    <% } else { %>
-      <form method="post" action="/admin/sites/<%= site.slug %>/move"
-            onsubmit="return confirm('<%= t('mig.move_confirm') %>');">
-        <input type="hidden" name="next" value="/admin/migrate">
-        <label for="mig-move"><%= t('mig.move_label') %></label>
-        <input type="text" id="mig-move" name="move_target" class="mig-input"
-               placeholder="@nieuw@andere-server.nl" required>
-        <p class="form-hint mig-warn"><%= t('mig.move_warn') %></p>
-        <button type="submit" class="btn btn-danger"><%= t('mig.move_btn') %></button>
-      </form>
-    <% } %>
-  </section>
-
-  <%# ── Wie je volgt ────────────────────────────────────────────── %>
-  <%# Stond op Connect, in een <details> zonder een greintje opmaak: labels als
-      losse tekst naast hun invoerveld, een textarea van vijf regels breed. Hier
-      krijgt hij de kaartopmaak van deze pagina, en hij hoort hier ook: je
-      volglijst meenemen is een migratie-optie. %>
-  <section class="mig-card">
-    <h2><%= t('mig.follows_title') %></h2>
-    <p class="form-hint"><%= t('tl.move_hint') %></p>
-    <p><a class="btn" href="/news/following.csv" download>&#11015; <%= t('tl.move_export') %></a></p>
-
-    <form method="post" action="/news/following/import" enctype="multipart/form-data">
-      <input type="hidden" name="next" value="/admin/migrate">
-      <label for="mig-volg-bestand"><%= t('tl.move_import_file') %></label>
-      <input type="file" id="mig-volg-bestand" name="csvfile" accept=".csv,text/csv,text/plain">
-
-      <label for="mig-volg-csv"><%= t('tl.move_import_lbl') %></label>
-      <textarea id="mig-volg-csv" name="csv" rows="5" spellcheck="false" class="mig-input"
-                placeholder="Account address,Show boosts,Notify on new posts,Languages,Featured"></textarea>
-      <p class="form-hint"><%= t('mig.follows_note') %></p>
-
-      <button type="submit" class="btn btn-primary"><%= t('tl.move_import') %></button>
-    </form>
-  </section>
-</div>
-
-<style>
-  .mig-wrap { max-width: 46rem; }
-  .mig-lead { color: var(--sub); margin-bottom: 1.5rem; }
-  .mig-card { border: 1px solid var(--rule); border-radius: 12px; padding: 1.25rem 1.4rem; margin-bottom: 1.25rem; }
-  .mig-card h2 { margin: 0 0 .35rem; font-size: 1.1rem; }
-  .mig-counts { list-style: none; padding: 0; margin: .9rem 0; display: flex; flex-wrap: wrap; gap: .4rem 1.4rem; }
-  .mig-counts li { color: var(--sub); font-size: .94rem; }
-  .mig-counts strong { color: var(--ink); font-size: 1.05rem; }
-  .mig-card label { display: block; margin: .9rem 0 .35rem; font-weight: 600; font-size: .92rem; }
-  .mig-card input[type="file"] { display: block; margin-bottom: .6rem; }
-  .mig-check { display: flex; align-items: center; gap: .5rem; font-weight: 400 !important; }
-  .mig-input { width: 100%; max-width: 34rem; padding: .5rem .6rem; border: 1px solid var(--rule);
-               border-radius: 8px; background: transparent; color: var(--ink); font-size: .95rem; }
-  .mig-state { margin-top: .9rem; }
-  .mig-report code { word-break: break-all; font-size: .88rem; }
-  .mig-warn { color: var(--accent); }
-  .mig-plan { background: color-mix(in srgb, var(--ink, #000) 3%, transparent); }
-  .mig-steps { margin: 1rem 0 0; padding-left: 1.4rem; display: flex; flex-direction: column; gap: .7rem; }
-  .mig-steps li { color: var(--ink); }
-  .mig-steps li em { display: block; color: var(--sub); font-style: normal; font-size: .9rem; margin-top: .15rem; }
-  .mig-steps li.is-done > strong { text-decoration: line-through; opacity: .6; }
-  .mig-steps li.is-done::marker { content: '✓ '; }
-  .mig-steps li.mig-step-later { opacity: .8; }
-  .mig-waar { font-size: .8rem; padding: .05rem .4rem; border: 1px solid var(--rule); border-radius: 999px;
-              color: var(--sub); margin-left: .4rem; white-space: nowrap; }
-  .mig-card-danger { border-color: color-mix(in srgb, var(--accent, #c33) 45%, var(--rule)); }
-  textarea.mig-input { font-family: inherit; resize: vertical; }
-  .mig-report { border-left: 3px solid var(--accent); padding: .8rem 1rem; margin-bottom: 1.2rem;
-                background: color-mix(in srgb, var(--ink, #000) 4%, transparent); border-radius: 0 8px 8px 0; }
-  .mig-report.is-done { border-left-color: #3fa843; }
-  .mig-report h3 { margin: 0 0 .3rem; font-size: 1rem; }
-  .mig-warnlist { margin: .5rem 0; padding-left: 1.2rem; color: var(--accent); font-size: .9rem; }
-  .mig-confirm { margin-top: .9rem; padding-top: .9rem; border-top: 1px solid var(--rule); }
-</style>
Index: src/views/pages/admin-newsletter.ejs
===================================================================
--- src/views/pages/admin-newsletter.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,68 +1,0 @@
-<%
-  var c = (typeof nlCounts !== 'undefined') ? nlCounts : { confirmed:0, pending:0, unsub:0 };
-  var msg = (typeof nlMsg !== 'undefined') ? nlMsg : '';
-  var msgKind = (typeof nlMsgKind !== 'undefined') ? nlMsgKind : '';
-  var hist = (typeof nlHistory !== 'undefined') ? nlHistory : [];
-  var smtp = (typeof nlSmtp !== 'undefined') ? nlSmtp : false;
-%>
-<section class="anl">
-  <h1><%= t('anews.title') %></h1>
-
-  <% if (msg) { %><div class="anl-msg anl-<%= msgKind %>"><%= msg %></div><% } %>
-
-  <div class="anl-stats">
-    <div class="anl-stat"><span class="anl-n"><%= c.confirmed %></span><span class="anl-l"><%= t('anews.confirmed') %></span></div>
-    <div class="anl-stat"><span class="anl-n"><%= c.pending %></span><span class="anl-l"><%= t('anews.pending') %></span></div>
-    <div class="anl-stat"><span class="anl-n"><%= c.unsub %></span><span class="anl-l"><%= t('anews.unsub') %></span></div>
-  </div>
-
-  <% if (!smtp) { %>
-    <div class="anl-msg anl-warn"><%= t('anews.smtp_warn_1') %> (<code>SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM</code> <%= t('anews.smtp_warn_2') %> <code>.env</code>).</div>
-  <% } %>
-
-  <p class="anl-share"><%= t('anews.share') %> <a href="<%= nlSubscribeUrl %>"><%= nlSubscribeUrl %></a></p>
-
-  <% var _sendConfirm = t('anews.send_confirm', { n: c.confirmed }); %>
-  <form method="POST" action="<%= siteUrlBase %>/admin/newsletter/send" class="anl-form" data-confirm="<%= _sendConfirm %>">
-    <label><%= t('anews.subject') %>
-      <input type="text" name="subject" required maxlength="200" value="<%= (typeof nlSubject!=='undefined')?nlSubject:'' %>" placeholder="<%= t('anews.subject_ph') %>">
-    </label>
-    <label><%= t('anews.body') %>
-      <textarea name="body" rows="10" required placeholder="<%= t('anews.body_ph') %>"><%= (typeof nlBody!=='undefined')?nlBody:'' %></textarea>
-    </label>
-    <button type="submit" class="anl-send" <%= smtp ? '' : 'disabled' %>><%= t('anews.send_btn', { n: c.confirmed }) %></button>
-  </form>
-
-  <% if (hist.length) { %>
-    <h2 class="anl-h2"><%= t('anews.sent_heading') %></h2>
-    <% var _recipients = t('anews.recipients'); %>
-    <ul class="anl-hist">
-      <% hist.forEach(function(h){ %>
-        <li><span class="anl-hist-subj"><%= h.subject %></span><span class="anl-hist-meta"><%= h.recipient_count %> <%= _recipients %> · <%= formatDateTime ? formatDateTime(h.sent_at) : h.sent_at %></span></li>
-      <% }); %>
-    </ul>
-  <% } %>
-</section>
-
-<style>
-  .anl { max-width: 720px; margin: 0 auto; padding: 24px 18px 64px; }
-  .anl h1 { margin: 0 0 18px; }
-  .anl-msg { padding: 11px 14px; border-radius: 10px; margin-bottom: 16px; font-size: 14px; }
-  .anl-ok { background: rgba(60,160,90,.15); }
-  .anl-bad { background: rgba(200,60,60,.15); }
-  .anl-warn { background: rgba(210,160,40,.15); }
-  .anl-stats { display: flex; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
-  .anl-stat { flex: 1 1 100px; border: 1px solid rgba(128,128,128,.2); border-radius: 12px; padding: 14px; text-align: center; }
-  .anl-n { display: block; font-size: 28px; font-weight: 700; }
-  .anl-l { font-size: 12px; opacity: .65; text-transform: uppercase; letter-spacing: .06em; }
-  .anl-share { font-size: 13px; opacity: .8; margin: 0 0 20px; }
-  .anl-form { display: flex; flex-direction: column; gap: 14px; }
-  .anl-form label { display: flex; flex-direction: column; gap: 6px; font-weight: 600; font-size: 13.5px; }
-  .anl-form input, .anl-form textarea { padding: 11px 13px; border-radius: 10px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; font: inherit; font-weight: 400; }
-  .anl-send { align-self: flex-start; padding: 11px 20px; border-radius: 10px; border: none; background: var(--accent, #6b8f71); color: #fff; font-weight: 600; cursor: pointer; }
-  .anl-send:disabled { opacity: .5; cursor: not-allowed; }
-  .anl-h2 { margin: 30px 0 12px; font-size: 13px; text-transform: uppercase; letter-spacing: .08em; opacity: .6; }
-  .anl-hist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
-  .anl-hist li { display: flex; justify-content: space-between; gap: 12px; padding: 10px 12px; border: 1px solid rgba(128,128,128,.18); border-radius: 10px; flex-wrap: wrap; }
-  .anl-hist-meta { opacity: .6; font-size: 12.5px; }
-</style>
Index: src/views/pages/admin-paid.ejs
===================================================================
--- src/views/pages/admin-paid.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,74 +1,0 @@
-<div class="container" style="max-width:640px;margin:1.5rem auto 3rem;padding:0 1rem">
-  <h1 style="margin:0 0 .3rem"><%= t('apaid.t') %></h1>
-  <p style="color:var(--ink-soft,#888);margin:0 0 1.2rem">
-    <%= t('apaid.intro') %>
-  </p>
-
-  <% if (saved) { %><p style="color:var(--accent);font-weight:600"><%= t('apaid.saved') %></p><% } %>
-  <% if (error) { %><p style="color:#c0392b"><%= error %></p><% } %>
-  <% if (!secretReady) { %>
-    <p style="color:#c0392b"><%= t('apaid.nokey') %></p>
-  <% } %>
-
-  <p style="margin:.2rem 0 1.2rem">
-    <%= t('apaid.status') %>
-    <% if (status.connected) { %>
-      <strong style="color:var(--accent)"><%= t('apaid.connected') %></strong> (<%= t('apaid.campaign') %> <%= status.campaignId %>)
-    <% } else if (status.configured) { %>
-      <strong><%= t('apaid.configured') %></strong>
-    <% } else { %>
-      <strong><%= t('apaid.notyet') %></strong>
-    <% } %>
-  </p>
-
-  <div style="border:1px solid var(--border,#333);border-radius:10px;padding:.9rem 1rem;margin:0 0 1.3rem;background:color-mix(in srgb,var(--accent,#6b8f71) 8%,transparent)">
-    <p style="margin:0 0 .4rem;font-weight:600"><%= t('apaid.redirect_h') %></p>
-    <p style="margin:0 0 .6rem;color:var(--ink-soft,#888);font-size:.9rem">
-      <%= t('apaid.redirect_p') %>
-    </p>
-    <div style="display:flex;gap:.5rem;align-items:center">
-      <input id="pd-redirect" type="text" readonly value="<%= redirectUri %>" style="flex:1;min-width:0;font-family:monospace;font-size:.9rem">
-      <button type="button" id="pd-copy" class="btn" data-copied="<%= t('apaid.copied') %>"><%= t('apaid.copy') %></button>
-    </div>
-  </div>
-
-  <form method="post" action="/admin/paid" style="display:flex;flex-direction:column;gap:.9rem">
-    <label><%= t('apaid.client_id') %>
-      <input type="text" name="client_id" value="<%= status.clientId || '' %>" autocomplete="off" style="width:100%">
-    </label>
-    <label><%= t('apaid.client_secret') %>
-      <input type="password" name="client_secret" placeholder="<%= status.hasSecret ? t('apaid.unchanged') : '' %>" autocomplete="off" style="width:100%">
-      <small style="color:var(--ink-soft,#888)"><%= t('apaid.keep') %></small>
-    </label>
-    <label><%= t('apaid.campaign_id') %>
-      <input type="text" name="campaign_id" value="<%= status.campaignId || '' %>" autocomplete="off" style="width:100%">
-    </label>
-    <label><%= t('apaid.public_page') %>
-      <input type="url" name="patreon_url" value="<%= status.patreonUrl || '' %>" placeholder="https://www.patreon.com/jouwnaam" autocomplete="off" style="width:100%">
-      <small style="color:var(--ink-soft,#888)"><%= t('apaid.public_help') %></small>
-    </label>
-    <label><%= t('apaid.access') %>
-      <input type="password" name="access_token" placeholder="<%= t('apaid.unchanged') %>" autocomplete="off" style="width:100%">
-    </label>
-    <label><%= t('apaid.refresh') %>
-      <input type="password" name="refresh_token" placeholder="<%= t('apaid.unchanged') %>" autocomplete="off" style="width:100%">
-      <small style="color:var(--ink-soft,#888)"><%= t('apaid.token_help') %></small>
-    </label>
-    <label><%= t('apaid.min_eur') %>
-      <input type="text" name="default_min_eur" value="<%= status.defaultMinCents ? (status.defaultMinCents/100).toFixed(2) : '' %>" inputmode="decimal" style="width:120px">
-    </label>
-    <div style="display:flex;gap:.6rem;align-items:center">
-      <button type="submit" class="btn btn-primary"><%= t('apaid.save') %></button>
-    </div>
-  </form>
-
-  <% if (status.configured || status.connected) { %>
-    <form method="post" action="/admin/paid/disconnect" data-confirm="<%= t('apaid.disconnect_confirm') %>" style="margin-top:1rem">
-      <button type="submit" class="btn btn-danger"><%= t('apaid.disconnect') %></button>
-    </form>
-  <% } %>
-
-  <p style="margin-top:1.5rem"><a href="/admin">&larr; <%= t('admin.back') %></a></p>
-</div>
-
-<%# Het script van deze pagina staat in assets/js/mod/admin-paid.js (shaer-bqr). %>
Index: src/views/pages/admin-playlists.ejs
===================================================================
--- src/views/pages/admin-playlists.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/admin-playlists.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -5,17 +5,13 @@
 const _csrf = (typeof csrfToken !== 'undefined' && csrfToken) || '';
 %>
-<%# De gedeelde ax-*-primitieven. MOET boven het eigen style-blok van deze
-    pagina staan: wat hieronder blijft staan wijkt bewust af en hoort dus
-    later te komen, anders wint de partial ervan. %>
-<%- include('../partials/admin-styles') %>
 <div class="container ax-page">
 
-  <%- include('../partials/admin-back') %>
-  <%- include('../partials/media-tabs', { active: 'playlists', audioOn: true }) %>
   <header class="ax-header">
+    <a href="/admin" class="ax-back" aria-label="Terug naar admin">←</a>
     <div>
-      <h1><%= t('apl.title') %></h1>
+      <h1>Playlists</h1>
       <p class="ax-tagline">
-        <%= t('apl.tagline_pre') %> <code>[[playlist:&lt;id&gt;]]</code><%= t('apl.tagline_post') %>
+        Canonieke playlists. Bewerk een playlist hier en de wijzigingen werken
+        door in álle posts die hem gebruiken via <code>[[playlist:&lt;id&gt;]]</code>.
       </p>
     </div>
@@ -25,5 +21,5 @@
   <div class="ax-actions-bar">
     <button type="button" class="ax-btn ax-btn-primary" id="pl-new-btn">
-      ＋ <%= t('apl.new_playlist') %>
+      ＋ Nieuwe playlist
     </button>
   </div>
@@ -33,29 +29,14 @@
       <div class="ax-empty">
         <div class="ax-empty-icon">📃</div>
-        <p><%= t('apl.none') %></p>
-        <p class="ax-empty-sub"><%= t('apl.none_sub') %></p>
+        <p>Nog geen playlists.</p>
+        <p class="ax-empty-sub">Maak er een via de knop hierboven, of via de 📃 knop in de post-editor.</p>
       </div>
     </div>
   <% } else { %>
-    <%
-      // Pre-fetch loop labels (defensive — keep `t` calls out of the loop body).
-      var _aplPlaylist = t('apl.pill_playlist');
-      var _aplAlbum    = t('apl.pill_album');
-      var _aplMixtape  = t('apl.pill_mixtape');
-      // Een soort met een eigen teken en een eigen woord. De ternair die hier
-      // stond kende er maar twee, en toonde een mixtape dus als album.
-      var _aplTeken = { album: '💿', playlist: '📃', mixtape: '📼' };
-      var _aplWoord = { album: _aplAlbum, playlist: _aplPlaylist, mixtape: _aplMixtape };
-      var _aplTrack    = t('apl.track');
-      var _aplTracks   = t('apl.tracks');
-      var _aplCopy     = t('apl.copy_click');
-      var _aplEdit     = t('apl.edit');
-      var _aplDelete   = t('apl.delete');
-    %>
     <ul class="ax-list">
       <% playlists.forEach(function(p) { %>
         <li class="ax-track" data-id="<%= p.id %>">
           <div class="ax-track-cover" <%- p.cover ? `style="background-image:url('${p.cover}'); background-size:cover; background-position:center"` : '' %>>
-            <% if (!p.cover) { %><span><%= _aplTeken[p.kind] || _aplTeken.album %></span><% } %>
+            <% if (!p.cover) { %><span><%= p.kind === 'playlist' ? '📃' : '💿' %></span><% } %>
           </div>
 
@@ -63,17 +44,17 @@
             <div class="ax-track-title">
               <%= p.title %>
-              <span class="ax-pill"><%= (_aplTeken[p.kind] || _aplTeken.album) + ' ' + (_aplWoord[p.kind] || _aplAlbum) %></span>
+              <span class="ax-pill"><%= p.kind === 'playlist' ? '📃 playlist' : '💿 album' %></span>
             </div>
             <div class="ax-track-sub">
-              <span><%= p.track_count %> <%= p.track_count === 1 ? _aplTrack : _aplTracks %></span>
+              <span><%= p.track_count %> <%= p.track_count === 1 ? 'track' : 'tracks' %></span>
               <% if (p.artist) { %><span class="ax-track-sep">·</span><span><%= p.artist %></span><% } %>
               <% if (p.year) { %><span class="ax-track-sep">·</span><span><%= p.year %></span><% } %>
             </div>
-            <code class="ax-embed" data-copy="[[playlist:<%= p.id %>]]" title="<%= _aplCopy %>">[[playlist:<%= p.id %>]]</code>
+            <code class="ax-embed" data-copy="[[playlist:<%= p.id %>]]" title="Klik om te kopiëren">[[playlist:<%= p.id %>]]</code>
           </div>
 
           <div class="ax-track-actions">
-            <button type="button" class="ax-icon-btn" data-pl-edit data-id="<%= p.id %>" aria-label="<%= _aplEdit %>" title="<%= _aplEdit %>">✎</button>
-            <button type="button" class="ax-icon-btn ax-icon-btn-danger" data-pl-delete data-id="<%= p.id %>" data-title="<%= p.title %>" aria-label="<%= _aplDelete %>" title="<%= _aplDelete %>">🗑</button>
+            <button type="button" class="ax-icon-btn" data-pl-edit data-id="<%= p.id %>" aria-label="Bewerken" title="Bewerken">✎</button>
+            <button type="button" class="ax-icon-btn ax-icon-btn-danger" data-pl-delete data-id="<%= p.id %>" data-title="<%= p.title %>" aria-label="Verwijderen" title="Verwijderen">🗑</button>
           </div>
         </li>
@@ -92,10 +73,210 @@
 
 .ax-page { max-width: 880px; margin: 1.5rem auto 4rem; padding: 0 1rem; }
+.ax-header { display: flex; align-items: flex-start; gap: 0.75rem; margin-bottom: 1.5rem; }
+.ax-header h1 { font-family: var(--font-display, serif); font-size: 1.75rem; margin: 0 0 0.25rem; }
+.ax-tagline { color: var(--ink-muted, var(--ink-soft)); margin: 0; font-size: 0.9rem; line-height: 1.5; }
+.ax-tagline code { background: var(--paper-2); padding: 0.1em 0.4em; border-radius: 4px; font-size: 0.9em; }
+.ax-back {
+  display: inline-flex; align-items: center; justify-content: center;
+  width: 40px; height: 40px;
+  border: 1px solid var(--rule); border-radius: 8px;
+  background: var(--paper); color: var(--ink); text-decoration: none;
+  font-size: 1.1rem; flex-shrink: 0; transition: border-color 120ms;
+}
+.ax-back:hover { border-color: var(--accent); }
+
+.ax-actions-bar {
+  display: flex;
+  margin-bottom: 1rem;
+}
+
+.ax-card {
+  background: var(--paper);
+  border: 1px solid var(--rule);
+  border-radius: 12px;
+  padding: 1.25rem;
+  margin-bottom: 1rem;
+}
+
+/* ─── Buttons (same primitives as admin-audio) ─────────────────── */
+.ax-btn {
+  display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
+  padding: 0.6rem 1.1rem;
+  border: 1px solid var(--rule); background: var(--paper-2);
+  color: var(--ink);
+  font-family: var(--font-ui, system-ui), sans-serif;
+  font-size: 0.95rem; font-weight: 500;
+  text-decoration: none; border-radius: 8px; cursor: pointer;
+  min-height: 44px;
+  transition: background 120ms, border-color 120ms;
+  -webkit-tap-highlight-color: transparent;
+}
+.ax-btn:hover { border-color: var(--accent); }
+.ax-btn-primary { background: var(--accent); color: white; border-color: var(--accent); }
+.ax-btn-primary:hover { opacity: 0.92; }
+
+.ax-icon-btn {
+  display: inline-flex; align-items: center; justify-content: center;
+  width: 40px; height: 40px;
+  border: 1px solid var(--rule); border-radius: 8px;
+  background: var(--paper-2); color: var(--ink);
+  cursor: pointer; font-size: 1rem;
+  transition: background 120ms, border-color 120ms, color 120ms;
+}
+.ax-icon-btn:hover { border-color: var(--accent); }
+.ax-icon-btn-danger:hover { color: #dc2626; border-color: #dc2626; }
+
+/* ─── Empty state ──────────────────────────────────────────────── */
+.ax-empty { text-align: center; padding: 2rem 1rem; color: var(--ink-muted, var(--ink-soft)); }
+.ax-empty-icon { font-size: 2.5rem; margin-bottom: 0.5rem; opacity: 0.5; }
+.ax-empty p { margin: 0; }
+.ax-empty-sub { font-size: 0.85rem; margin-top: 0.5rem !important; }
+
+/* ─── List items (same as audio tracks) ───────────────────────── */
+.ax-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.6rem; }
+.ax-track {
+  display: grid;
+  grid-template-columns: 64px 1fr auto;
+  gap: 0.85rem; align-items: center;
+  padding: 0.85rem;
+  background: var(--paper);
+  border: 1px solid var(--rule);
+  border-radius: 12px;
+  transition: border-color 120ms;
+}
+.ax-track:hover { border-color: var(--accent); }
+.ax-track-cover {
+  width: 64px; height: 64px; border-radius: 8px;
+  background: var(--paper-2);
+  display: flex; align-items: center; justify-content: center;
+  font-family: var(--font-display, serif);
+  font-size: 1.5rem; color: var(--accent);
+  flex-shrink: 0;
+}
+.ax-track-meta { min-width: 0; display: flex; flex-direction: column; gap: 0.25rem; }
+.ax-track-title {
+  font-family: var(--font-display, serif);
+  font-weight: 600; font-size: 1.05rem; color: var(--ink);
+  display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
+}
+.ax-pill {
+  font-family: var(--font-ui);
+  font-size: 0.7rem; font-weight: 500;
+  color: var(--ink-soft);
+  background: var(--paper-2);
+  padding: 0.15em 0.55em; border-radius: 999px;
+  letter-spacing: 0;
+}
+.ax-track-sub {
+  font-size: 0.85rem;
+  color: var(--ink-muted, var(--ink-soft));
+  display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center;
+}
+.ax-track-sep { opacity: 0.5; }
+.ax-embed {
+  display: inline-block;
+  background: var(--paper-2);
+  padding: 0.15rem 0.5rem;
+  border-radius: 4px;
+  font-family: var(--font-mono, ui-monospace, monospace);
+  font-size: 0.75rem;
+  color: var(--ink-soft);
+  cursor: pointer; user-select: all;
+  margin-top: 0.15rem;
+  word-break: break-all;
+  align-self: flex-start;
+  transition: background 120ms;
+}
+.ax-embed:hover { background: var(--rule); }
+.ax-embed.is-copied { background: rgba(40,160,90,0.2); color: #2a9d5e; }
+
+.ax-track-actions { display: flex; gap: 0.4rem; flex-shrink: 0; }
 
 /* ─── Mobile ──────────────────────────────────────────────────── */
 @media (max-width: 480px) {
+  .ax-track {
+    grid-template-columns: 56px 1fr;
+    grid-template-areas: "cover meta" "actions actions";
+    gap: 0.6rem;
+  }
+  .ax-track-cover { grid-area: cover; width: 56px; height: 56px; }
+  .ax-track-meta  { grid-area: meta; }
+  .ax-track-actions {
+    grid-area: actions;
+    justify-content: flex-end;
+    border-top: 1px solid var(--rule);
+    padding-top: 0.5rem;
+    margin-top: 0.25rem;
+  }
 }
 </style>
 
-<%# Het script staat in assets/js/mod/admin-playlists.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
-<%- include('../partials/page-data', { pageData: { csrf: _csrf, copied: t('apl.copied'), delConfirm: t('apl.delete_confirm'), delFailed: t('apl.delete_failed') } }) %>
+<script>
+(function() {
+  const csrf = '<%= _csrf %>';
+
+  document.getElementById('pl-new-btn')?.addEventListener('click', () => {
+    if (typeof window.openPlaylistEditor === 'function') {
+      window.openPlaylistEditor({ mode: 'create', onSaved: () => location.reload() });
+    }
+  });
+
+  // Click-to-copy on shortcodes
+  document.querySelectorAll('[data-copy]').forEach(el => {
+    el.addEventListener('click', async () => {
+      try {
+        await navigator.clipboard.writeText(el.dataset.copy);
+        el.classList.add('is-copied');
+        const original = el.textContent;
+        el.textContent = '✓ gekopieerd';
+        setTimeout(() => { el.classList.remove('is-copied'); el.textContent = original; }, 1200);
+      } catch (_) { /* fall back to selection */ }
+    });
+  });
+
+  document.querySelectorAll('[data-pl-edit]').forEach(btn => {
+    btn.addEventListener('click', () => {
+      if (typeof window.openPlaylistEditor === 'function') {
+        window.openPlaylistEditor({ mode: 'edit', id: btn.dataset.id, onSaved: () => location.reload() });
+      }
+    });
+  });
+
+  document.querySelectorAll('[data-pl-delete]').forEach(btn => {
+    btn.addEventListener('click', async () => {
+      const id = btn.dataset.id;
+      const title = btn.dataset.title || id;
+      if (!confirm(`Playlist "${title}" verwijderen? Posts die deze playlist embedden tonen vanaf nu een placeholder.`)) return;
+      try {
+        const r = await fetch(`/admin/playlists/api/${encodeURIComponent(id)}/delete`, {
+          method: 'POST',
+          headers: { 'X-CSRF-Token': csrf },
+          credentials: 'same-origin',
+        });
+        const j = await r.json();
+        if (j.ok) location.reload();
+        else alert('Verwijderen mislukt: ' + (j.error || ''));
+      } catch (err) {
+        alert('Verwijderen mislukt: ' + err.message);
+      }
+    });
+  });
+
+  // P52 — deep-link from playlist embed (?edit=<id>) auto-opens the editor.
+  // openPlaylistEditor is defined synchronously by the included partial, so
+  // it's available by the time this IIFE runs.
+  (function deepLinkEdit() {
+    const params = new URLSearchParams(location.search);
+    const editId = params.get('edit');
+    if (!editId) return;
+    if (typeof window.openPlaylistEditor !== 'function') return;
+    // Strip the query param immediately so reload after save doesn't re-open.
+    history.replaceState({}, '', location.pathname);
+    window.openPlaylistEditor({
+      mode: 'edit',
+      id: editId,
+      onSaved: () => location.reload(),
+    });
+  })();
+})();
+</script>
Index: src/views/pages/admin-push.ejs
===================================================================
--- src/views/pages/admin-push.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,61 +1,0 @@
-<div class="container" style="max-width:640px;margin:1.5rem auto 3rem;padding:0 1rem">
-  <h1 style="margin:0 0 .3rem"><%= t('push.t') %></h1>
-  <p style="color:var(--ink-soft,#888);margin:0 0 1.2rem">
-    <%= t('push.intro') %>
-  </p>
-
-  <% if (!vapidKey) { %>
-    <p style="color:#c0392b"><%= t('push.unavailable') %></p>
-  <% } else { %>
-
-  <div id="np-unsupported" hidden style="color:#c0392b;margin:0 0 1rem">
-    <%= t('push.unsupported') %>
-  </div>
-  <div id="np-ios-hint" hidden style="border:1px solid var(--border,#333);border-radius:10px;padding:.8rem 1rem;margin:0 0 1rem;font-size:.92rem;color:var(--ink-soft,#888)">
-    <%= t('push.ios_hint') %>
-  </div>
-
-  <section style="border:1px solid var(--border,#333);border-radius:12px;padding:1rem 1.1rem;margin:0 0 1.2rem">
-    <p style="margin:0 0 .6rem"><strong><%= t('push.this_device') %></strong> <span id="np-state"><%= t('push.checking') %></span></p>
-    <div style="display:flex;gap:.6rem;flex-wrap:wrap">
-      <button type="button" id="np-on" class="btn btn-primary" hidden><%= t('push.enable') %></button>
-      <button type="button" id="np-off" class="btn" hidden><%= t('push.disable') %></button>
-      <button type="button" id="np-test" class="btn" hidden><%= t('push.test') %></button>
-    </div>
-    <fieldset id="np-alerts" hidden style="border:none;padding:0;margin:1rem 0 0">
-      <legend style="font-weight:600;margin-bottom:.4rem"><%= t('push.what') %></legend>
-      <label style="display:block"><input type="checkbox" data-alert="follow"> <%= t('push.a_follow') %></label>
-      <label style="display:block"><input type="checkbox" data-alert="reply"> <%= t('push.a_reply') %></label>
-      <label style="display:block"><input type="checkbox" data-alert="like"> <%= t('push.a_like') %></label>
-      <label style="display:block"><input type="checkbox" data-alert="boost"> <%= t('push.a_boost') %></label>
-      <label style="display:block"><input type="checkbox" data-alert="dm"> <%= t('push.a_dm') %></label>
-      <p id="np-saved" hidden style="color:var(--accent);margin:.4rem 0 0"><%= t('push.saved') %></p>
-    </fieldset>
-  </section>
-
-  <% if (subscriptions.length) { %>
-    <h2 style="font-size:1.05rem;margin:0 0 .5rem"><%= t('push.devices') %></h2>
-    <ul style="list-style:none;padding:0;margin:0 0 1.2rem">
-      <% subscriptions.forEach(function (s) { %>
-        <li data-endpoint="<%= s.endpoint %>" style="display:flex;justify-content:space-between;gap:.8rem;align-items:center;padding:.45rem 0;border-bottom:1px solid var(--border,#2a2a2a)">
-          <span style="min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><%= s.ua_label || t('push.device') %> <small style="color:var(--ink-soft,#888)"><%= t('push.since') %> <%= String(s.created_at || '').slice(0, 10) %></small></span>
-          <button type="button" class="btn np-remove" data-endpoint="<%= s.endpoint %>"><%= t('push.remove') %></button>
-        </li>
-      <% }); %>
-    </ul>
-  <% } %>
-
-  <% } %>
-
-  <p style="margin-top:1.5rem"><a href="/admin">&larr; <%= t('admin.back') %></a></p>
-</div>
-
-<% if (vapidKey) { %>
-<script id="np-data" type="application/json"><%- JSON.stringify({
-  vapid: vapidKey,
-  alerts: defaultAlerts,
-  saved: subscriptions.reduce(function (m, s) { try { m[s.endpoint] = JSON.parse(s.alert_types || '{}'); } catch (e) { m[s.endpoint] = {}; } return m; }, {}),
-  i18n: { on: t('push.state_on'), off: t('push.state_off'), denied: t('push.state_denied'), unknown: t('push.state_unknown'), unsupported: t('push.state_unsupported'), failed: t('push.enable_failed') },
-}).replace(/</g, '\\u003c') %></script>
-<%# Het script van deze pagina staat in assets/js/mod/admin-push.js (shaer-bqr). %>
-<% } %>
Index: src/views/pages/admin-seo.ejs
===================================================================
--- src/views/pages/admin-seo.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,229 +1,0 @@
-<div class="container admin-site-edit-page">
-  <p><a href="/admin" class="btn">&larr; <%= t('aseo.back') %></a></p>
-  <h1><%= t('aseo.title') %></h1>
-  <p class="admin-tagline"><%= t('aseo.tagline_pre') %> <strong><%= site.title %></strong> <%= t('aseo.tagline_post') %></p>
-
-  <% if (typeof success !== 'undefined' && success) { %><div class="audio-flash audio-flash--ok"><%= success %></div><% } %>
-  <% if (typeof error !== 'undefined' && error) { %><div class="audio-flash audio-flash--err"><%= error %></div><% } %>
-
-  <form method="post" action="/admin/seo" class="site-form">
-
-    <fieldset>
-      <legend><%= t('aseo.index_legend') %></legend>
-      <label class="cb">
-        <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>>
-        <span><%= t('aseo.index_label') %> <small class="form-hint-inline">— <%= t('aseo.index_hint_pre') %> <code>noindex</code> <%= t('aseo.index_hint_post') %></small></span>
-      </label>
-    </fieldset>
-
-    <fieldset>
-      <legend><%= t('aseo.title_legend') %></legend>
-      <label>
-        <span><%= t('aseo.title_template') %> <small class="form-hint-inline">— <%= t('aseo.title_template_hint_pre') %> <code>{title}</code> <%= t('aseo.title_template_hint_and') %> <code>{site}</code></small></span>
-        <input type="text" name="title_template" value="<%= site.title_template || '{title} — {site}' %>" maxlength="200" placeholder="{title} — {site}">
-      </label>
-      <label>
-        <span><%= t('aseo.default_desc') %> <small class="form-hint-inline">— <%= t('aseo.default_desc_hint') %></small></span>
-        <textarea name="default_description" rows="2" maxlength="500" placeholder="<%= t('aseo.default_desc_ph') %>"><%= site.default_description || '' %></textarea>
-      </label>
-      <label>
-        <span><%= t('aseo.canonical') %> <small class="form-hint-inline">— <%= t('aseo.canonical_hint') %></small></span>
-        <input type="text" name="canonical" value="<%= site.canonical || '' %>" placeholder="https://yourdomain.com" maxlength="200">
-      </label>
-      <label>
-        <span><%= t('aseo.author') %> <small class="form-hint-inline">— <%= t('aseo.author_hint') %></small></span>
-        <input type="text" name="author" value="<%= site.author || '' %>" maxlength="120" placeholder="<%= t('aseo.author_ph') %>">
-      </label>
-    </fieldset>
-
-    <fieldset>
-      <legend><%= t('aseo.social_legend') %></legend>
-      <label>
-        <span><%= t('aseo.og_image') %> <small class="form-hint-inline">— <%= t('aseo.og_image_hint') %></small></span>
-        <input type="text" name="og_image_default" value="<%= site.og_image_default || '' %>" placeholder="https://yourdomain.com/share.jpg" maxlength="500">
-      </label>
-      <label>
-        <span><%= t('aseo.og_theme') %> <small class="form-hint-inline">— <%= t('aseo.og_theme_hint') %></small></span>
-        <select name="og_theme">
-          <option value="" <%= !site.og_theme ? 'selected' : '' %>><%= t('aseo.og_theme_auto') %></option>
-          <option value="light" <%= site.og_theme === 'light' ? 'selected' : '' %>><%= t('aseo.og_theme_light') %></option>
-          <option value="dark" <%= site.og_theme === 'dark' ? 'selected' : '' %>><%= t('aseo.og_theme_dark') %></option>
-        </select>
-      </label>
-      <label>
-        <span><%= t('aseo.og_locale') %> <small class="form-hint-inline">— <%= t('aseo.og_locale_hint_pre') %> <code>nl_NL</code> <%= t('aseo.og_locale_hint_or') %> <code>en_US</code></small></span>
-        <input type="text" name="og_locale" value="<%= site.og_locale || '' %>" placeholder="nl_NL" maxlength="32">
-      </label>
-      <label>
-        <span><%= t('aseo.twitter') %> <small class="form-hint-inline">— <%= t('aseo.twitter_hint') %></small></span>
-        <input type="text" name="twitter" value="<%= site.twitter || '' %>" placeholder="@jouwnaam" maxlength="64">
-      </label>
-      <label>
-        <span><%= t('aseo.fb_app') %> <small class="form-hint-inline">— <%= t('aseo.fb_app_hint') %></small></span>
-        <input type="text" name="facebook_app_id" value="<%= site.facebook_app_id || '' %>" maxlength="64">
-      </label>
-    </fieldset>
-
-    <fieldset>
-      <legend><%= t('aseo.publisher_legend') %></legend>
-      <label>
-        <span><%= t('aseo.type') %></span>
-        <select name="schema_type">
-          <option value="Person" <%= site.schema_type === 'Organization' ? '' : 'selected' %>><%= t('aseo.type_person') %></option>
-          <option value="Organization" <%= site.schema_type === 'Organization' ? 'selected' : '' %>><%= t('aseo.type_org') %></option>
-        </select>
-      </label>
-      <label>
-        <span><%= t('aseo.publisher_name') %> <small class="form-hint-inline">— <%= t('aseo.publisher_name_hint') %></small></span>
-        <input type="text" name="publisher_name" value="<%= site.publisher_name || '' %>" maxlength="200">
-      </label>
-      <label>
-        <span><%= t('aseo.publisher_url') %></span>
-        <input type="text" name="publisher_url" value="<%= site.publisher_url || '' %>" placeholder="https://yourdomain.com" maxlength="200">
-      </label>
-      <label>
-        <span><%= t('aseo.publisher_logo') %></span>
-        <input type="text" name="publisher_logo" value="<%= site.publisher_logo || '' %>" placeholder="https://yourdomain.com/logo.png" maxlength="500">
-      </label>
-    </fieldset>
-
-    <%# ── MUSICBRAINZ ─────────────────────────────────────────────
-        Direct onder Uitgever, en dat is geen willekeur: sameAs IS een
-        schema.org-eigenschap, en dat blok hierboven is waar ons schema.org
-        vandaan komt. Een koppeling hier voedt twee uitgangen tegelijk -- de
-        JSON-LD van de pagina en het fediverse-profiel (shaer-mbz). %>
-    <fieldset id="mb-veld">
-      <legend><%= t('aseo.mb_legend') %></legend>
-      <p class="form-hint"><%= t('aseo.mb_hint') %></p>
-
-      <input type="hidden" name="mb_artist_id" id="mb-id" value="<%= site.mb_artist_id || '' %>">
-      <input type="hidden" name="mb_artist_name" id="mb-naam" value="<%= site.mb_artist_name || '' %>">
-
-      <p class="mb-huidig" id="mb-huidig" <%= site.mb_artist_id ? '' : 'hidden' %>>
-        <%= t('aseo.mb_linked') %>
-        <strong id="mb-huidig-naam"><%= site.mb_artist_name || site.mb_artist_id || '' %></strong>
-        <a id="mb-huidig-link" target="_blank" rel="noopener"
-           href="https://musicbrainz.org/artist/<%= site.mb_artist_id || '' %>"><%= t('aseo.mb_open') %></a>
-        <button type="button" class="btn" id="mb-wis"><%= t('aseo.mb_unlink') %></button>
-      </p>
-      <%# De terug-weg. Een koppeling van onze kant is een BEWERING -- iedereen
-          kan een id typen. Pas als de MusicBrainz-pagina terugwijst is het een
-          paar, en dat staat hier met zoveel woorden zodat "eenzijdig" niet als
-          "klaar" leest. %>
-      <p class="mb-terugweg" id="mb-terugweg" hidden></p>
-
-      <label>
-        <span><%= t('aseo.mb_search_label') %> <small class="form-hint-inline">— <%= t('aseo.mb_search_hint') %></small></span>
-        <div class="mb-zoek">
-          <input type="text" id="mb-q" value="<%= site.publisher_name || site.title || '' %>"
-                 autocomplete="off" placeholder="<%= t('aseo.mb_placeholder') %>">
-          <button type="button" class="btn" id="mb-zoek-btn"><%= t('aseo.mb_search') %></button>
-        </div>
-      </label>
-      <div id="mb-uit" class="mb-uit" hidden></div>
-    </fieldset>
-
-    <fieldset>
-      <legend><%= t('aseo.verify_legend') %></legend>
-      <label>
-        <span><%= t('aseo.verify_google') %></span>
-        <input type="text" name="google_verification" value="<%= site.google_verification || '' %>" maxlength="200">
-      </label>
-      <label>
-        <span><%= t('aseo.verify_bing') %> <small class="form-hint-inline">— <%= t('aseo.verify_bing_hint') %></small></span>
-        <input type="text" name="bing_verification" value="<%= site.bing_verification || '' %>" maxlength="200">
-      </label>
-      <label>
-        <span><%= t('aseo.verify_pinterest') %> <small class="form-hint-inline">— <%= t('aseo.verify_pinterest_hint') %></small></span>
-        <input type="text" name="pinterest_verification" value="<%= site.pinterest_verification || '' %>" maxlength="200">
-      </label>
-      <label>
-        <span><%= t('aseo.verify_yandex') %></span>
-        <input type="text" name="yandex_verification" value="<%= site.yandex_verification || '' %>" maxlength="200">
-      </label>
-    </fieldset>
-
-    <button type="submit" class="btn btn-primary"><%= t('aseo.save') %></button>
-  </form>
-</div>
-
-<style>
-/* ── MusicBrainz-kandidaten: een kaartje per artiest ──────────────
-   Meerdere regels en geen opsomming op een rij: dit is een KEUZE, en de
-   toelichting die twee gelijknamige bands uit elkaar houdt hoort niet als
-   bijzin achter de naam weg te vallen. */
-.mb-uit { margin-top: 0.5rem; }
-.mb-lijst { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; }
-.mb-kandidaat {
-  display: flex; flex-direction: column; gap: 0.2rem;
-  border: 1px solid var(--rule); border-radius: 8px;
-  padding: 0.7rem 0.85rem; background: var(--paper);
-  min-width: 0;
-}
-.mb-naam { font-weight: 600; font-size: 0.95rem; color: var(--ink); }
-.mb-toelichting { font-size: 0.85rem; color: var(--ink); }
-.mb-feiten { font-size: 0.8rem; color: var(--ink-muted); }
-.mb-voet { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.35rem; flex-wrap: wrap; }
-.mb-open { font-size: 0.8rem; }
-/* Een zwakke treffer als zwak tonen; bij een goede is een getal ruis. */
-.mb-zwak { font-size: 0.75rem; color: var(--ink-muted); }
-.mb-kies { margin-left: auto; }
-.mb-huidig { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; font-size: 0.9rem; }
-.mb-zoek { display: flex; gap: 0.5rem; min-width: 0; }
-.mb-zoek input { flex: 1 1 auto; min-width: 0; }
-/* De terug-weg: eenzijdig is geen fout, maar het mag ook niet als klaar lezen. */
-.mb-terugweg { font-size: 0.82rem; margin: 0.2rem 0 0; }
-.mb-terugweg.is-ok { color: var(--ok, #2e7d32); }
-.mb-terugweg.is-eenzijdig { color: var(--ink-muted); }
-
-.admin-site-edit-page { max-width: 760px; margin: 3rem auto; padding: 0 1rem; }
-.admin-site-edit-page h1 { font-family: var(--font-display, serif); font-size: 1.8rem; margin: 0 0 0.4rem; }
-.admin-site-edit-page .admin-tagline { color: var(--ink-muted); margin: 0 0 1.75rem; }
-
-.site-form { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }
-/* <fieldset> does not shrink because the UA sets min-inline-size:min-content → mobile overflow.
-   min-width:0 forces it to shrink. */
-.site-form fieldset { min-width: 0; }
-.site-form input, .site-form textarea, .site-form select { min-width: 0; }
-.site-form fieldset {
-  border: 1px solid var(--rule);
-  border-radius: 8px;
-  padding: 1rem 1.25rem;
-  background: var(--paper-2);
-  display: flex;
-  flex-direction: column;
-  gap: 0.85rem;
-}
-.site-form fieldset legend { font-family: var(--font-display, serif); font-size: 1rem; padding: 0 0.4rem; }
-.site-form label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.85rem; color: var(--ink-muted, var(--ink-soft)); }
-.site-form label.cb { flex-direction: row; align-items: flex-start; gap: 0.55rem; font-size: 0.9rem; color: var(--ink); }
-.site-form label.cb input { margin-top: 0.15rem; }
-.site-form label > span { line-height: 1.35; }
-.form-hint-inline { font-weight: 400; color: var(--ink-muted); font-size: 0.82em; }
-.site-form code { background: var(--paper); padding: 0.05rem 0.35rem; border-radius: 3px; font-size: 0.85em; }
-.site-form input[type="text"], .site-form textarea, .site-form select {
-  padding: 0.55rem 0.85rem;
-  border: 1px solid var(--rule);
-  border-radius: 5px;
-  background: var(--paper);
-  color: var(--ink);
-  font-family: inherit;
-  font-size: 0.95rem;
-  width: 100%;
-  box-sizing: border-box;
-}
-.site-form textarea { resize: vertical; min-height: 2.6rem; }
-.site-form select { max-width: 320px; }
-.site-form input:focus, .site-form textarea:focus, .site-form select:focus {
-  outline: none; border-color: var(--accent);
-}
-.seo-pointer p { margin: 0; font-size: 0.9rem; color: var(--ink-muted); }
-.seo-pointer a { color: var(--accent); font-weight: 600; }
-.site-form > button[type="submit"] { align-self: flex-start; }
-</style>
-
-<%# Servertekst naar mod/admin-seo.js. KORTE sleutels, zoals overal in dit
-    project -- de module leest T.mb_busy en niet T.aseo_mb_busy. Zonder dit
-    blok geeft pageData() een leeg object en valt alles terug op de
-    ingebakken Nederlandse tekst, ook voor een Engelse of Duitse lezer. %>
-<%- include('../partials/page-data', { pageData: { mb_busy: t('aseo.mb_busy'), mb_fail: t('aseo.mb_fail'), mb_none: t('aseo.mb_none'), mb_open: t('aseo.mb_open'), mb_pick: t('aseo.mb_pick'), mb_verified: t('aseo.mb_verified'), mb_unverified: t('aseo.mb_unverified'), mb_checking: t('aseo.mb_checking') } }) %>
Index: src/views/pages/admin-settings.ejs
===================================================================
--- src/views/pages/admin-settings.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,218 +1,0 @@
-<div class="container admin-page">
-  <h1><%= t('aset.title') %></h1>
-  <p><a href="/admin" class="btn">&larr; <%= t('aset.back_admin') %></a></p>
-
-  <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
-  <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
-
-  <%# ── Mode: Solo or Cirkels (ActivityPub) — the primary, top-of-page choice ── %>
-  <section class="set-card set-card--mode" style="margin-top:1rem">
-    <h2><%= t('aset.mode') %></h2>
-    <p class="set-help"><%= t('aset.mode_help') %></p>
-    <form method="post" action="/admin/settings/ap" class="set-form" id="mode-form"
-          data-was-cirkels="<%= (typeof apEnabledSetting === 'undefined' || apEnabledSetting) ? '1' : '0' %>"
-          data-confirm="<%= t('aset.mode_18plus_confirm') %>">
-      <label class="set-opt">
-        <input type="radio" name="mode" value="solo" <%= (typeof apEnabledSetting !== 'undefined' && !apEnabledSetting) ? 'checked' : '' %>>
-        <span><strong><%= t('aset.mode_solo') %></strong><small><%= t('aset.mode_solo_help') %></small></span>
-      </label>
-      <label class="set-opt">
-        <input type="radio" name="mode" value="cirkels" <%= (typeof apEnabledSetting === 'undefined' || apEnabledSetting) ? 'checked' : '' %>>
-        <span><strong><%= t('aset.mode_cirkels') %></strong><small><%= t('aset.mode_cirkels_help') %></small></span>
-      </label>
-      <div style="display:flex;gap:.6rem;align-items:flex-start;margin:.7rem 0 .2rem;padding:.7rem .85rem;border-radius:10px;border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);background:color-mix(in srgb,var(--accent) 7%,transparent)">
-        <span style="font-size:1.05rem;line-height:1.4;flex:0 0 auto" aria-hidden="true">🔞</span>
-        <span style="font-size:.85rem;line-height:1.5;color:var(--ink)"><%= t('aset.mode_18plus') %></span>
-      </div>
-      <div class="set-actions"><button type="submit" class="btn btn-primary"><%= t('aset.save') %></button></div>
-    </form>
-    <%# Het script van deze pagina staat in assets/js/mod/admin-settings.js (shaer-bqr). %>
-  </section>
-
-  <%# ── Default language for visitors ── %>
-  <section class="set-card" style="margin-top:1rem">
-    <h2><%= t('aset.default_lang') %></h2>
-    <p class="set-help"><%= t('aset.default_lang_hint') %></p>
-    <form method="post" action="/admin/settings" class="set-form">
-      <label class="set-field">
-        <span><%= t('aset.default_lang') %></span>
-        <select name="default_lang">
-          <option value=""<%= !defaultLang ? ' selected' : '' %>><%= t('aset.default_lang_auto') %></option>
-          <% langs.forEach(function(l){ %>
-            <option value="<%= l.code %>"<%= defaultLang === l.code ? ' selected' : '' %>><%= l.name %></option>
-          <% }); %>
-        </select>
-      </label>
-      <button type="submit" class="btn btn-primary"><%= t('aset.save') %></button>
-    </form>
-  </section>
-
-  <%# ── Timezone ── %>
-  <section class="set-card" style="margin-top:1rem">
-    <h2><%= t('aset.timezone') %></h2>
-    <p class="set-help"><%= t('aset.timezone_hint') %></p>
-    <form method="post" action="/admin/settings" class="set-form">
-      <label class="set-field">
-        <span><%= t('aset.timezone') %></span>
-        <select name="timezone">
-          <option value=""<%= !timezone ? ' selected' : '' %>><%= t('aset.timezone_auto') %></option>
-          <%
-            var _tz = ['Europe/Amsterdam','Europe/Brussels','Europe/Berlin','Europe/Paris','Europe/London','Europe/Madrid','Europe/Rome','Europe/Lisbon','Europe/Warsaw','Europe/Athens','UTC','America/New_York','America/Chicago','America/Los_Angeles','America/Sao_Paulo','Asia/Tokyo','Asia/Dubai','Australia/Sydney'];
-            if (timezone && _tz.indexOf(timezone) < 0) _tz.unshift(timezone);
-          %>
-          <% _tz.forEach(function(z){ %><option value="<%= z %>"<%= timezone === z ? ' selected' : '' %>><%= z %></option><% }); %>
-        </select>
-      </label>
-      <button type="submit" class="btn btn-primary"><%= t('aset.save') %></button>
-    </form>
-  </section>
-
-  <%# Mode-specific config — Circle settings below the mode card. (Hub removed.) %>
-
-  <%# Premium (Patreon) — only visible when the self-hoster enables the premium layer
-      (env KLONKT_PREMIUM_ENABLED=on). Hidden by default; nothing gated. %>
-  <% if (premium && premium.enabled) { %>
-  <section class="set-card" style="margin-top:1rem">
-    <h2><%= t('aset.premium') %></h2>
-    <p class="set-help"><%= t('aset.premium_help_1') %> <strong><%= t('aset.premium_lifetime') %></strong> <%= t('aset.premium_help_2') %></p>
-    <% if (premium.premium) { %>
-      <p class="set-help"><strong>✓ <%= t('aset.premium_active') %></strong>
-        <% if (premium.supportCents) { %><%= t('aset.lifetime_support') %> $<%= (premium.supportCents/100).toFixed(2) %>.<% } %></p>
-      <a href="/admin/patreon/disconnect" class="btn"><%= t('aset.patreon_disconnect') %></a>
-    <% } else if (premium.connected) { %>
-      <p class="set-help"><%= t('aset.patreon_no_lifetime') %><% if (premium.supportCents) { %>
-        (<%= t('aset.now') %> $<%= (premium.supportCents/100).toFixed(2) %>)<% } %>. <%= t('aset.patreon_support_again') %></p>
-      <a href="/admin/patreon/connect" class="btn btn-primary"><%= t('aset.patreon_reconnect') %></a>
-    <% } else { %>
-      <p class="set-help"><%= t('aset.patreon_not_connected') %></p>
-      <a href="/admin/patreon/connect" class="btn btn-primary"><%= t('aset.patreon_connect') %></a>
-    <% } %>
-  </section>
-  <% } %>
-
-  <%# Newsletter signup in the footer (premium). %>
-  <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
-  <section class="set-card" style="margin-top:1rem">
-    <h2><%= t('aset.newsletter') %></h2>
-    <p class="set-help"><%= t('aset.newsletter_help_1') %> <strong><%= t('aset.newsletter_footer_field') %></strong> <%= t('aset.newsletter_help_2') %> <code><%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/nieuwsbrief</code>.)</p>
-    <form method="post" action="/admin/settings/footer" class="set-form">
-      <label class="set-toggle" style="display:flex;align-items:center;gap:.6rem;cursor:pointer">
-        <input type="checkbox" name="footer_newsletter" value="1" <%= footerNewsletter ? 'checked' : '' %>>
-        <span><%= t('aset.newsletter_show_footer') %></span>
-      </label>
-      <div class="set-actions"><button type="submit" class="btn btn-primary"><%= t('aset.save') %></button></div>
-    </form>
-  </section>
-  <% } %>
-
-  <%# E-mail / SMTP — required for sending newsletters, notify-mails and password resets. %>
-  <section class="set-card" style="margin-top:1rem">
-    <h2><%= t('aset.email_smtp') %></h2>
-    <p class="set-help"><%= t('aset.smtp_help_1') %> <strong><%= t('aset.smtp_help_newsletter') %></strong><%= t('aset.smtp_help_2') %> <strong><%= t('aset.smtp_help_notify') %></strong><%= t('aset.smtp_help_3') %>
-      <% if (smtp && smtp.configured) { %><br><strong><%= t('aset.status_set') %>&nbsp;✓</strong><% if (smtp.fromEnv) { %> <small>(<%= t('aset.via_env') %>)</small><% } %><% } else { %><br><%= t('aset.smtp_not_set') %><% } %></p>
-
-    <form method="post" action="/admin/settings/smtp" class="set-form">
-      <label><span><%= t('aset.smtp_host') %></span>
-        <input type="text" name="smtp_host" value="<%= smtp ? smtp.host : '' %>" placeholder="smtp.jouwprovider.nl" autocomplete="off">
-      </label>
-      <label><span><%= t('aset.smtp_port') %> <small style="font-weight:400">— <%= t('aset.smtp_port_hint') %></small></span>
-        <input type="text" name="smtp_port" value="<%= (smtp && smtp.port) ? smtp.port : '' %>" placeholder="587" autocomplete="off" inputmode="numeric">
-      </label>
-      <label><span><%= t('aset.smtp_user') %></span>
-        <input type="text" name="smtp_user" value="<%= smtp ? smtp.user : '' %>" placeholder="jij@jouwprovider.nl" autocomplete="off">
-      </label>
-      <label><span><%= t('aset.smtp_pass') %> <% if (smtp && smtp.passSet) { %><small style="font-weight:400">— <%= t('aset.smtp_pass_set_hint') %></small><% } %></span>
-        <input type="password" name="smtp_pass" placeholder="<%= (smtp && smtp.passSet) ? t('aset.smtp_pass_ph_set') : t('aset.smtp_pass_ph') %>" autocomplete="off">
-      </label>
-      <label><span><%= t('aset.smtp_from') %> <small style="font-weight:400">— <%= t('aset.smtp_from_hint') %></small></span>
-        <input type="text" name="smtp_from" value="<%= smtp ? smtp.from : '' %>" placeholder="<%= t('aset.smtp_from_ph') %>" autocomplete="off">
-      </label>
-      <div class="set-actions">
-        <button type="submit" class="btn btn-primary"><%= t('aset.smtp_save') %></button>
-        <% if (smtp && (smtp.host || smtp.passSet)) { %>
-          <button type="submit" name="clear" value="1" class="btn" formnovalidate><%= t('aset.clear') %></button>
-        <% } %>
-      </div>
-    </form>
-
-    <% if (smtp && smtp.configured) { %>
-      <form method="post" action="/admin/settings/smtp/test" class="set-form" style="margin-top:.8rem">
-        <label><span><%= t('aset.test_mail_to') %></span>
-          <input type="email" name="to" placeholder="jij@email.nl" autocomplete="off">
-        </label>
-        <div class="set-actions"><button type="submit" class="btn">✉ <%= t('aset.send_test_mail') %></button></div>
-      </form>
-    <% } %>
-  </section>
-
-</div>
-
-<style>
-.admin-page { max-width: 700px; margin: 3rem auto; padding: 0 1rem; }
-.admin-page h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: 0 0 0.25rem; }
-.admin-tagline { color: var(--ink-muted); margin: 0 0 2rem; }
-.admin-tagline a { color: var(--accent); }
-.set-card { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 12px; padding: 1.25rem; }
-/* Mode-specific card (Hub/Circle): accent border on the left so it visually
-   belongs to the selected mode, directly below the Mode card. */
-.set-card--mode { border-left: 3px solid var(--accent); }
-.set-card h2 { font-family: var(--font-display, serif); font-size: 1.25rem; margin: 0 0 0.5rem; }
-.set-help { color: var(--ink-muted); font-size: 0.9rem; margin: 0 0 1.25rem; }
-/* Google setup guide (collapsible) */
-.set-google-guide { margin: 0 0 1rem; border: 1px solid var(--rule); border-radius: 8px; background: var(--paper); }
-.set-google-guide > summary { cursor: pointer; padding: 0.6rem 0.85rem; font-size: 0.9rem; font-weight: 600; color: var(--accent); list-style: none; }
-.set-google-guide > summary::-webkit-details-marker { display: none; }
-.set-google-guide > summary::before { content: "▸ "; }
-.set-google-guide[open] > summary::before { content: "▾ "; }
-.set-google-steps { margin: 0; padding: 0 1rem 0.6rem 2rem; font-size: 0.88rem; color: var(--ink); line-height: 1.5; }
-.set-google-steps li { margin: 0.35rem 0; }
-.set-google-guide code { background: var(--paper-2); padding: 0.05rem 0.35rem; border-radius: 4px; font-size: 0.85em; }
-.set-google-guide .set-help { padding: 0 0.85rem; }
-.set-form { display: flex; flex-direction: column; gap: 1rem; max-width: 460px; }
-/* Bare <label>s inside a set-form inherit the same neat stacked style as .set-field:
-   label text on top, input full-width. (.set-toggle = checkbox row, excluded.) */
-.set-form > label:not(.set-toggle):not(.set-opt) { display: flex; flex-direction: column; gap: 0.3rem; }
-.set-form > label:not(.set-toggle):not(.set-opt) > span { font-size: 0.8rem; font-weight: 600; color: var(--ink-soft, var(--ink-muted)); }
-.set-form > label:not(.set-toggle):not(.set-opt) > span small { font-weight: 400; }
-.set-form > label:not(.set-toggle):not(.set-opt) input {
-  width: 100%; box-sizing: border-box; padding: 0.55rem 0.7rem;
-  border: 1px solid var(--rule); border-radius: 6px; background: var(--paper); color: var(--ink);
-  font: inherit; font-size: 0.95rem;
-}
-.set-form > label:not(.set-toggle):not(.set-opt) input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
-.set-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.25rem; }
-.set-opt { display: flex; gap: 0.75rem; align-items: flex-start; padding: 0.85rem 1rem; border: 1px solid var(--rule); border-radius: 8px; cursor: pointer; background: var(--paper); }
-.set-opt:has(input:checked) { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
-.set-opt input { margin-top: 0.25rem; flex-shrink: 0; }
-.set-opt span { display: flex; flex-direction: column; gap: 0.2rem; }
-.set-opt small { color: var(--ink-muted); font-size: 0.82rem; }
-.set-opt code { background: var(--paper-2); padding: 0.05rem 0.35rem; border-radius: 3px; font-size: 0.85em; }
-.set-form .btn { align-self: flex-start; }
-.set-field { display: flex; flex-direction: column; gap: 0.3rem; }
-.set-field > span { font-size: 0.8rem; font-weight: 600; color: var(--ink-soft, var(--ink-muted)); }
-.set-field input, .set-field textarea {
-  width: 100%; box-sizing: border-box; padding: 0.55rem 0.7rem;
-  border: 1px solid var(--rule); border-radius: 6px; background: var(--paper); color: var(--ink);
-  font: inherit; font-size: 0.95rem;
-}
-.set-field textarea { resize: vertical; }
-.set-field input:focus, .set-field textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
-/* Selects styled the same as inputs instead of the bare native dropdown:
-   custom chevron, accent focus ring, full width. */
-.set-form select {
-  appearance: none; -webkit-appearance: none;
-  width: 100%; box-sizing: border-box;
-  padding: 0.55rem 2.2rem 0.55rem 0.7rem;
-  border: 1px solid var(--rule); border-radius: 6px;
-  background-color: var(--paper); color: var(--ink);
-  font: inherit; font-size: 0.95rem; cursor: pointer;
-  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
-  background-repeat: no-repeat; background-position: right 0.7rem center;
-  transition: border-color 0.15s ease, box-shadow 0.15s ease;
-}
-.set-form select:hover { border-color: var(--ink-soft, #888); }
-.set-form select:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
-.set-form select option { background: var(--paper); color: var(--ink); }
-.alert { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; }
-.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
-</style>
Index: src/views/pages/admin-shows.ejs
===================================================================
--- src/views/pages/admin-shows.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,80 +1,0 @@
-<%
-  var ss = (typeof shows !== 'undefined') ? shows : [];
-  var msg = (typeof msg !== 'undefined') ? msg : '';
-  var smtp = (typeof smtp !== 'undefined') ? smtp : false;
-  var nc = (typeof notifyCount !== 'undefined') ? notifyCount : 0;
-%>
-<section class="ash">
-  <p><a href="/admin" class="btn">&larr; <%= t('ashow.back_admin') %></a></p>
-  <h1><%= t('ashow.title') %></h1>
-
-  <% if (msg) { %><div class="ash-msg ash-<%= (typeof msgKind!=='undefined')?msgKind:'' %>"><%= msg %></div><% } %>
-
-  <% var agOn = (typeof agendaEnabled !== 'undefined') && agendaEnabled; %>
-  <form method="POST" action="<%= siteUrlBase %>/admin/shows/toggle" style="margin:0 0 14px">
-    <label style="display:flex;align-items:center;gap:10px;cursor:pointer;font-weight:600">
-      <input type="checkbox" name="enabled" value="1" <%= agOn ? 'checked' : '' %> data-autosubmit style="width:18px;height:18px;accent-color:var(--accent)">
-      <span><%= t('ashow.show_toggle') %> <small style="font-weight:400;opacity:.7"><%= t('ashow.show_toggle_hint') %></small></span>
-    </label>
-    <noscript><button type="submit" class="btn"><%= t('ashow.save') %></button></noscript>
-  </form>
-  <% if (!agOn) { %><div class="ash-msg"><%= t('ashow.off_notice_1') %> <strong><%= t('ashow.off') %></strong> <%= t('ashow.off_notice_2') %></div><% } %>
-
-  <p class="ash-note"><strong><%= nc %></strong> <%= t('ashow.subscribers') %>
-    <% if (!smtp) { %><br><%= t('ashow.smtp_warn') %><% } %></p>
-
-  <form method="POST" action="<%= siteUrlBase %>/admin/shows" class="ash-form">
-    <div class="ash-grid">
-      <label><%= t('ashow.f_date') %><input type="date" name="date" required></label>
-      <label><%= t('ashow.f_time') %><input type="time" name="time"></label>
-      <label><%= t('ashow.f_city') %><input type="text" name="city" required placeholder="Amsterdam"></label>
-      <label><%= t('ashow.f_country') %><input type="text" name="country" placeholder="NL"></label>
-      <label class="ash-wide"><%= t('ashow.f_venue') %><input type="text" name="venue" placeholder="Paradiso"></label>
-      <label class="ash-wide"><%= t('ashow.f_ticket') %><input type="url" name="ticket_url" placeholder="https://..."></label>
-      <label class="ash-wide"><%= t('ashow.f_notes') %><input type="text" name="notes" placeholder="<%= t('ashow.f_notes_ph') %>"></label>
-    </div>
-    <label class="ash-check"><input type="checkbox" name="notify" value="1" <%= smtp ? '' : 'disabled' %>> <%= t('ashow.notify_label') %><%= smtp ? '' : ' ' + t('ashow.smtp_required') %></label>
-    <button type="submit" class="ash-add"><%= t('ashow.add_event') %></button>
-  </form>
-
-  <% if (ss.length) { %>
-    <% var _delConfirm = t('ashow.del_confirm'); %>
-    <ul class="ash-list">
-      <% ss.forEach(function(s){ %>
-        <li class="ash-item">
-          <span class="ash-when"><%= s.date %><% if (s.time) { %> <%= s.time %><% } %></span>
-          <span class="ash-where"><%= s.city %><% if (s.venue) { %> — <%= s.venue %><% } %></span>
-          <form method="POST" action="<%= siteUrlBase %>/admin/shows/<%= s.id %>/delete" data-confirm="<%= _delConfirm %>" style="margin:0">
-            <button type="submit" class="ash-del">🗑</button>
-          </form>
-        </li>
-      <% }); %>
-    </ul>
-  <% } else { %>
-    <p class="ash-empty"><%= t('ashow.empty') %></p>
-  <% } %>
-</section>
-
-<style>
-  .ash { max-width: 680px; margin: 0 auto; padding: 24px 18px 64px; }
-  .ash h1 { margin: .4rem 0 14px; }
-  .ash-msg { padding: 10px 14px; border-radius: 10px; margin-bottom: 14px; }
-  .ash-ok { background: rgba(60,160,90,.15); } .ash-bad { background: rgba(200,60,60,.15); }
-  .ash-note { font-size: 13px; opacity: .8; margin: 0 0 18px; }
-  .ash-form { border: 1px solid rgba(128,128,128,.2); border-radius: 14px; padding: 16px; margin-bottom: 24px; }
-  /* Mobile-first: single column (two 1fr columns with non-shrinkable date/text inputs
-     combined to ~374px > phone width). Two columns only from 520px onwards. */
-  .ash-grid { display: grid; grid-template-columns: 1fr; gap: 10px; }
-  @media (min-width: 520px) { .ash-grid { grid-template-columns: 1fr 1fr; } }
-  .ash-grid label { display: flex; flex-direction: column; gap: 4px; font-size: 12.5px; font-weight: 600; min-width: 0; }
-  .ash-grid .ash-wide { grid-column: 1 / -1; }
-  .ash-grid input { min-width: 0; padding: 9px 11px; border-radius: 8px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; font: inherit; font-weight: 400; }
-  .ash-check { display: flex; align-items: center; gap: 8px; margin: 12px 0; font-size: 13.5px; }
-  .ash-add { padding: 10px 18px; border-radius: 10px; border: none; background: var(--accent,#6b8f71); color: #fff; font-weight: 600; cursor: pointer; }
-  .ash-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
-  .ash-item { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border: 1px solid rgba(128,128,128,.18); border-radius: 10px; }
-  .ash-when { font-variant-numeric: tabular-nums; font-weight: 600; flex: 0 0 auto; }
-  .ash-where { flex: 1 1 auto; opacity: .85; }
-  .ash-del { background: none; border: none; cursor: pointer; font-size: 16px; opacity: .6; }
-  .ash-empty { opacity: .7; }
-</style>
Index: src/views/pages/admin-site-edit.ejs
===================================================================
--- src/views/pages/admin-site-edit.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/admin-site-edit.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,5 +1,5 @@
 <div class="container admin-site-edit-page">
-  <p><a href="/admin" class="btn">&larr; <%= t('asite.back_admin') %></a></p>
-  <h1><%= isNew ? t('asite.title_new') : t('asite.title_edit') + ': ' + site.title %></h1>
+  <p><a href="/admin/sites" class="btn">&larr; Sites</a></p>
+  <h1><%= isNew ? 'New site' : 'Edit: ' + site.title %></h1>
 
   <% if (typeof success !== 'undefined' && success) { %><div class="audio-flash audio-flash--ok"><%= success %></div><% } %>
@@ -9,32 +9,28 @@
 
     <fieldset>
-      <legend><%= t('asite.identity') %></legend>
-      <label>
-        <span><%= t('asite.slug') %> <% if (!isNew) { %><em><%= t('asite.slug_fixed') %></em><% } %></span>
-        <span class="slug-url">
-          <span class="slug-url-host" id="slug-host"
-                data-prefix="/">website.com/</span><input
-                type="text" name="slug" id="slug-input" class="slug-url-input" value="<%= site.slug %>" required <% if (!isNew) { %>readonly<% } %>
-                pattern="[a-z0-9_-]{2,40}" placeholder="<%= t('asite.slug_placeholder') %>"
-                autocapitalize="none" autocorrect="off" spellcheck="false">
-        </span>
-      </label>
-      <label>
-        <span><%= t('asite.field_title') %> <small class="form-hint-inline"><%= t('asite.field_title_hint') %></small></span>
+      <legend>Identity</legend>
+      <label>
+        <span>Slug <% if (!isNew) { %><em>(immutable)</em><% } %></span>
+        <input type="text" name="slug" value="<%= site.slug %>" required <% if (!isNew) { %>readonly<% } %>
+               pattern="[a-z0-9_-]{2,40}"
+               placeholder="bedrijf1">
+      </label>
+      <label>
+        <span>Title <small class="form-hint-inline">— shown in the header and as display name</small></span>
         <input type="text" name="title" value="<%= site.title || '' %>" required maxlength="200">
       </label>
       <label>
-        <span><%= t('asite.tagline') %> <small class="form-hint-inline"><%= t('asite.tagline_hint') %></small></span>
+        <span>Tagline <small class="form-hint-inline">— short one-liner</small></span>
         <input type="text" name="tagline" value="<%= site.tagline || '' %>" maxlength="200">
       </label>
       <label>
-        <span><%= t('asite.bio') %> <small class="form-hint-inline"><%= t('asite.bio_hint') %></small></span>
+        <span>Bio / description <small class="form-hint-inline">— shown in the profile header and used for SEO</small></span>
         <textarea name="description" rows="3" maxlength="500"><%= site.description || '' %></textarea>
       </label>
       <label>
-        <span><%= t('asite.profile_photo') %></span>
+        <span>Profile photo</span>
         <div class="photo-picker" id="photo-picker">
           <div class="photo-thumb" id="photo-thumb" <%= site.profile_photo ? '' : 'data-empty' %>>
-            <img id="photo-preview" src="<%= avatar(site.profile_photo || '', 128) %>" alt="" <%= site.profile_photo ? '' : 'hidden' %>>
+            <img id="photo-preview" src="<%= site.profile_photo || '' %>" alt="" <%= site.profile_photo ? '' : 'hidden' %>>
             <span class="photo-empty" aria-hidden="true">
               <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
@@ -42,8 +38,8 @@
           </div>
           <div class="photo-controls">
-            <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder="<%= t('asite.photo_url_placeholder') %>">
+            <input type="text" name="profile_photo" id="photo-url" value="<%= site.profile_photo || '' %>" placeholder="/media/avatars/foo.jpg of https://…">
             <div class="photo-actions">
-              <button type="button" class="btn" id="photo-upload-trigger"><%= t('asite.photo_upload') %></button>
-              <button type="button" class="btn" id="photo-clear" <%= site.profile_photo ? '' : 'hidden' %>><%= t('asite.photo_clear') %></button>
+              <button type="button" class="btn" id="photo-upload-trigger">📷 Upload</button>
+              <button type="button" class="btn" id="photo-clear" <%= site.profile_photo ? '' : 'hidden' %>>Verwijder</button>
               <small id="photo-status" class="form-hint photo-status"></small>
             </div>
@@ -53,15 +49,15 @@
       </label>
       <label>
-        <span><%= t('asite.language') %></span>
+        <span>Language (ISO code)</span>
         <input type="text" name="language" value="<%= site.language || 'nl' %>" maxlength="8">
       </label>
       <label class="cb">
         <input type="checkbox" name="profile_enabled" value="1" <%= site.profile_enabled ? 'checked' : '' %>>
-        <span><%= t('asite.profile_enabled') %></span>
+        <span>Show profile header below the nav</span>
       </label>
     </fieldset>
 
     <fieldset>
-      <legend><%= t('asite.appearance') %></legend>
+      <legend>Appearance</legend>
 
       <%# ── Curated accent picker ───────────────────────────────────────
@@ -70,5 +66,5 @@
           still what's stored in the DB — we just constrain WHICH hex. %>
       <fieldset class="accent-grid">
-        <legend><%= t('asite.accent_color') %></legend>
+        <legend>Accent color</legend>
         <% const _curAccent = (site.accent || '#c2410c').toLowerCase(); %>
         <% accents.forEach(function(a) { %>
@@ -87,25 +83,25 @@
           visitors with a localStorage override, still win over this default. %>
       <fieldset class="theme-default">
-        <legend><%= t('asite.theme_default') %></legend>
+        <legend>Default theme for new visitors</legend>
         <% const td = site.theme_override || ''; %>
         <label class="cb">
           <input type="radio" name="theme_override" value=""      <%= td === ''      ? 'checked' : '' %>>
-          <span><%= t('asite.theme_auto') %></span>
+          <span>Auto (volg device-voorkeur)</span>
         </label>
         <label class="cb">
           <input type="radio" name="theme_override" value="light" <%= td === 'light' ? 'checked' : '' %>>
-          <span><%= t('asite.theme_light') %></span>
+          <span>Light</span>
         </label>
         <label class="cb">
           <input type="radio" name="theme_override" value="dark"  <%= td === 'dark'  ? 'checked' : '' %>>
-          <span><%= t('asite.theme_dark') %></span>
+          <span>Dark</span>
         </label>
       </fieldset>
 
       <fieldset class="palette-grid">
-        <legend><%= t('asite.palette') %></legend>
+        <legend>Palette</legend>
         <% palettes.forEach(function(p) { %>
           <label class="palette-swatch">
-            <input type="radio" name="palette" value="<%= p.key %>" <%= (site.palette || 'klonkt') === p.key ? 'checked' : '' %>>
+            <input type="radio" name="palette" value="<%= p.key %>" <%= (site.palette || 'sage') === p.key ? 'checked' : '' %>>
             <span class="palette-preview" aria-hidden="true">
               <span class="pp-half pp-light" style="background: <%= p.light.paper %>; color: <%= p.light.ink %>;">
@@ -124,33 +120,38 @@
     </fieldset>
 
-    <%# Live preview: selecting an accent / theme / palette re-themes this page
-        instantly (reverts on reload; persists on save). %>
-    <%# Het script van deze pagina staat in assets/js/mod/admin-site-edit.js (shaer-bqr). %>
-
     <fieldset>
-      <legend><%= t('asite.behavior') %></legend>
+      <legend>Behaviour</legend>
       <label class="cb">
         <input type="checkbox" name="is_public" value="1" <%= site.is_public ? 'checked' : '' %>>
-        <span><%= t('asite.is_public') %></span>
+        <span>Public site (uncheck for closed circle)</span>
       </label>
       <label class="cb">
         <input type="checkbox" name="robots_index" value="1" <%= site.robots_index ? 'checked' : '' %>>
-        <span><%= t('asite.robots_index') %></span>
-      </label>
-      <% if (typeof audioEnabled === 'undefined' || audioEnabled) { %>
+        <span>Search engines may index (sitemap.xml is hidden if off)</span>
+      </label>
+      <label class="cb">
+        <input type="checkbox" name="require_login_to_comment" value="1" <%= site.require_login_to_comment ? 'checked' : '' %>>
+        <span>Require login to comment</span>
+      </label>
+      <label>
+        <span>Comment moderation</span>
+        <select name="comments_moderation_mode">
+          <option value="trust"    <%= (site.comments_moderation_mode || 'trust') === 'trust'    ? 'selected' : '' %>>Trust mode (auto-approve)</option>
+          <option value="moderate" <%= site.comments_moderation_mode === 'moderate' ? 'selected' : '' %>>Moderate (queue for review)</option>
+        </select>
+      </label>
       <label class="cb">
         <input type="checkbox" name="enable_audio_player" value="1" <%= site.enable_audio_player ? 'checked' : '' %>>
-        <span><%= t('asite.enable_audio') %></span>
-      </label>
-      <% } %>
-      <label class="cb">
-        <input type="checkbox" name="approve_followers" value="1" <%= site.approve_followers ? 'checked' : '' %>>
-        <span><%= t('asite.approve_followers') %></span>
+        <span>Enable audio player + embeds</span>
+      </label>
+      <label class="cb">
+        <input type="checkbox" name="enable_prutter" value="1" <%= site.enable_prutter ? 'checked' : '' %>>
+        <span>Enable Prutter (direct messaging)</span>
       </label>
     </fieldset>
 
     <fieldset class="profile-links-fieldset">
-      <legend><%= t('asite.links') %></legend>
-      <p class="form-hint"><%= t('asite.links_hint') %></p>
+      <legend>Social / streaming links</legend>
+      <p class="form-hint">Shown as branded icons on the profile header. Add as many as you like.</p>
 
       <div id="profile-links-rows">
@@ -180,84 +181,179 @@
       </template>
 
-      <button type="button" id="profile-link-add" class="btn"><%= t('asite.link_add') %></button>
-    </fieldset>
-
-    <%# Aliassen en verhuizen staan sinds 14-8 op /admin/migrate, bij elkaar met
-        exporteren, importeren en ophalen (Robin: alle migratie-opties op de
-        migrate-pagina). Ze hoorden hier ook niet thuis: dit scherm gaat over hoe
-        je site eruitziet, en een verhuizing is geen uiterlijk. Alleen de
-        wegwijzer blijft, want wie ze hier zocht moet ze kunnen vinden. %>
+      <button type="button" id="profile-link-add" class="btn">+ Add link</button>
+    </fieldset>
+
     <fieldset>
-      <legend><%= t('asite.aliases') %></legend>
-      <p class="form-hint"><%= t('asite.moved_to_migrate') %></p>
-      <p><a href="/admin/migrate" class="btn"><%= t('admin.b_migrate') %></a></p>
+      <legend>Feed view</legend>
+      <label>
+        <span>Default view for the homepage</span>
+        <select name="feed_view_default">
+          <option value="timeline" <%= (site.feed_view_default || 'timeline') === 'timeline' ? 'selected' : '' %>>Timeline (chronological list)</option>
+          <option value="grid"     <%= site.feed_view_default === 'grid' ? 'selected' : '' %>>Grid (cards)</option>
+        </select>
+      </label>
+      <label class="cb">
+        <input type="checkbox" name="feed_view_switch" value="1" <%= site.feed_view_switch ? 'checked' : '' %>>
+        <span>Show timeline ↔ grid switcher above the feed</span>
+      </label>
+      <label class="cb">
+        <input type="checkbox" name="show_search" value="1" <%= site.show_search ? 'checked' : '' %>>
+        <span>Show search button in nav</span>
+      </label>
+      <label class="cb">
+        <input type="checkbox" name="show_archive_link" value="1" <%= site.show_archive_link ? 'checked' : '' %>>
+        <span>Show archive link in nav</span>
+      </label>
     </fieldset>
 
     <fieldset>
-      <legend><%= t('asite.feed_view') %></legend>
-      <%# Twee vragen, en ze zijn niet hetzelfde: WELKE tweede weergave je site
-          aanbiedt naast Grid, en waarin hij opent. %>
-      <label>
-        <span><%= t('asite.feed_alt') %></span>
-        <select name="feed_alt_view">
-          <% var _a = ['timeline','auto'].indexOf(site.feed_alt_view) >= 0 ? site.feed_alt_view : 'reader'; %>
-          <option value="reader"   <%= _a === 'reader'   ? 'selected' : '' %>><%= t('asite.feed_alt_reader') %></option>
-          <option value="timeline" <%= _a === 'timeline' ? 'selected' : '' %>><%= t('asite.feed_alt_timeline') %></option>
-          <option value="auto"     <%= _a === 'auto'     ? 'selected' : '' %>><%= t('asite.feed_alt_auto') %></option>
-        </select>
-      </label>
-      <label>
-        <span><%= t('asite.feed_default') %></span>
-        <select name="feed_view_default">
-          <option value="reader" <%= site.feed_view_default !== 'grid' ? 'selected' : '' %>><%= t('asite.feed_alt_' + _a) %></option>
-          <option value="grid"     <%= site.feed_view_default === 'grid' ? 'selected' : '' %>><%= t('asite.feed_grid') %></option>
-        </select>
-      </label>
-      <label class="cb">
-        <input type="checkbox" name="feed_view_switch" value="1" <%= site.feed_view_switch ? 'checked' : '' %>>
-        <span><%= t('asite.feed_switch') %></span>
-      </label>
-      <%# Alleen van toepassing op Lezen: geeft elk bericht daar een eigen scherm. %>
-      <label class="cb">
-        <input type="checkbox" name="reader_full_page" value="1" <%= site.reader_full_page ? 'checked' : '' %>>
-        <span><%= t('asite.reader_full_page') %></span>
-      </label>
-      <label class="cb">
-        <input type="checkbox" name="show_search" value="1" <%= site.show_search ? 'checked' : '' %>>
-        <span><%= t('asite.show_search') %></span>
-      </label>
-      <label class="cb">
-        <input type="checkbox" name="show_archive_link" value="1" <%= site.show_archive_link ? 'checked' : '' %>>
-        <span><%= t('asite.show_archive') %></span>
-      </label>
-    </fieldset>
-
-    <fieldset class="seo-pointer">
-      <legend><%= t('asite.seo') %></legend>
-      <p><%= t('asite.seo_pointer') %> <a href="/admin/seo"><%= t('asite.seo_link') %></a>.</p>
+      <legend>SEO &amp; social</legend>
+      <label>
+        <span>Title template (use {title} and {site})</span>
+        <input type="text" name="title_template" value="<%= site.title_template || '{title} — {site}' %>" maxlength="200">
+      </label>
+      <label>
+        <span>Canonical base URL (for production HTTPS)</span>
+        <input type="text" name="canonical" value="<%= site.canonical || '' %>" placeholder="https://your-domain.com">
+      </label>
+      <label>
+        <span>Twitter handle (with @)</span>
+        <input type="text" name="twitter" value="<%= site.twitter || '' %>" placeholder="@yourname" maxlength="64">
+      </label>
+      <details>
+        <summary>Search-engine verification</summary>
+        <label>
+          <span>Google site verification</span>
+          <input type="text" name="google_verification"    value="<%= site.google_verification    || '' %>" maxlength="200">
+        </label>
+        <label>
+          <span>Bing (msvalidate.01)</span>
+          <input type="text" name="bing_verification"      value="<%= site.bing_verification      || '' %>" maxlength="200">
+        </label>
+        <label>
+          <span>Pinterest (p:domain_verify)</span>
+          <input type="text" name="pinterest_verification" value="<%= site.pinterest_verification || '' %>" maxlength="200">
+        </label>
+        <label>
+          <span>Yandex</span>
+          <input type="text" name="yandex_verification"    value="<%= site.yandex_verification    || '' %>" maxlength="200">
+        </label>
+      </details>
     </fieldset>
 
     <fieldset>
-      <legend><%= t('asite.custom_legend') %> <em><%= t('asite.optional') %></em></legend>
-      <label>
-        <span><%= t('asite.custom_css') %></span>
+      <legend>Custom CSS &amp; HTML <em>(optional)</em></legend>
+      <label>
+        <span>Custom CSS (injected as &lt;style&gt; in &lt;head&gt;)</span>
         <textarea name="custom_css" rows="3" placeholder=".foo { color: red; }"><%= site.custom_css || '' %></textarea>
       </label>
       <label>
-        <span><%= t('asite.custom_head') %></span>
+        <span>Custom &lt;head&gt; HTML (analytics, extra metas)</span>
         <textarea name="custom_head_html" rows="3" placeholder="<meta name=&quot;...&quot;>"><%= site.custom_head_html || '' %></textarea>
       </label>
       <label>
-        <span><%= t('asite.custom_foot') %></span>
+        <span>Custom footer HTML</span>
         <textarea name="custom_foot_html" rows="3"><%= site.custom_foot_html || '' %></textarea>
       </label>
     </fieldset>
 
-    <button type="submit" class="btn btn-primary"><%= isNew ? t('asite.submit_create') : t('asite.submit_save') %></button>
+    <button type="submit" class="btn btn-primary"><%= isNew ? 'Create site' : 'Save changes' %></button>
   </form>
-
 </div>
 
-
+<script>
+(function() {
+  // Profile-links repeater: add row from <template>, remove on click.
+  var rows = document.getElementById('profile-links-rows');
+  var tpl  = document.getElementById('profile-link-template');
+  var add  = document.getElementById('profile-link-add');
+  if (!rows || !tpl || !add) return;
+
+  add.addEventListener('click', function() {
+    var clone = tpl.content.cloneNode(true);
+    rows.appendChild(clone);
+  });
+  rows.addEventListener('click', function(e) {
+    if (e.target && e.target.classList.contains('pl-remove')) {
+      var row = e.target.closest('.profile-link-row');
+      if (row) row.remove();
+    }
+  });
+})();
+
+// P63 — Profile photo picker: upload via /admin/sites/upload-photo,
+// then write the returned URL into the visible input. Live thumb preview.
+(function() {
+  var picker  = document.getElementById('photo-picker');
+  if (!picker) return;
+  var thumb   = document.getElementById('photo-thumb');
+  var preview = document.getElementById('photo-preview');
+  var urlEl   = document.getElementById('photo-url');
+  var trigger = document.getElementById('photo-upload-trigger');
+  var clear   = document.getElementById('photo-clear');
+  var field   = document.getElementById('photo-upload-field');
+  var status  = document.getElementById('photo-status');
+
+  function showPreview(url) {
+    if (url) {
+      preview.src = url;
+      preview.hidden = false;
+      thumb.removeAttribute('data-empty');
+      clear.hidden = false;
+    } else {
+      preview.src = '';
+      preview.hidden = true;
+      thumb.setAttribute('data-empty', '');
+      clear.hidden = true;
+    }
+  }
+
+  if (urlEl) {
+    urlEl.addEventListener('input', function() {
+      showPreview(urlEl.value.trim());
+    });
+  }
+
+  if (trigger && field) {
+    trigger.addEventListener('click', function() { field.click(); });
+    field.addEventListener('change', async function() {
+      var file = field.files && field.files[0];
+      if (!file) return;
+      status.classList.remove('is-error');
+      status.textContent = 'Uploaden…';
+      try {
+        var fd = new FormData();
+        fd.append('photo', file);
+        var r = await fetch('/admin/sites/upload-photo', {
+          method: 'POST',
+          body: fd,
+          credentials: 'same-origin',
+        });
+        var j = await r.json();
+        if (!r.ok || !j.ok) throw new Error(j.error || ('Upload mislukt (' + r.status + ')'));
+        urlEl.value = j.url;
+        showPreview(j.url);
+        status.textContent = 'Geüpload ✓';
+        setTimeout(function() { status.textContent = ''; }, 2000);
+      } catch (e) {
+        status.classList.add('is-error');
+        status.textContent = 'Mislukt: ' + e.message;
+      } finally {
+        field.value = '';
+      }
+    });
+  }
+
+  if (clear) {
+    clear.addEventListener('click', function() {
+      urlEl.value = '';
+      showPreview('');
+      status.textContent = '';
+      // Note: doesn't delete the file from disk — saving the form with empty
+      // URL leaves the file orphaned on the server. Acceptable for now.
+    });
+  }
+})();
+</script>
 
 <style>
@@ -265,11 +361,5 @@
 .admin-site-edit-page h1 { font-family: var(--font-display, serif); font-size: 1.8rem; margin: 0 0 1.5rem; }
 
-.site-form { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }
-/* Mobile: a <fieldset> has a browser-default min-inline-size:min-content and
-   therefore does NOT shrink → the entire form became wider than the screen. min-width:0
-   forces it to shrink; form fields get max-width:100% + border-box so they
-   never overflow their fieldset. */
-.site-form fieldset { min-width: 0; }
-.site-form input, .site-form select, .site-form textarea { min-width: 0; max-width: 100%; box-sizing: border-box; }
+.site-form { display: flex; flex-direction: column; gap: 1.5rem; }
 .site-form fieldset {
   border: 1px solid var(--rule);
@@ -310,58 +400,4 @@
 .site-form input[readonly] { opacity: 0.6; cursor: not-allowed; background: var(--paper-2); }
 
-/* Slug as URL field: dimmed host prefix + the slug itself in the accent colour. */
-.slug-url {
-  display: flex; align-items: stretch;
-  border: 1px solid var(--rule); border-radius: 5px;
-  background: var(--paper); overflow: hidden;
-  transition: border-color 120ms;
-}
-.slug-url:focus-within { border-color: var(--accent); }
-.slug-url-host {
-  display: inline-flex; align-items: center;
-  padding-left: 0.85rem;
-  color: var(--ink-muted, var(--ink-soft));
-  white-space: nowrap; user-select: none;
-  font-size: 0.95rem;
-}
-.site-form .slug-url-input {
-  flex: 1 1 auto; min-width: 0;
-  border: none !important; border-radius: 0 !important;
-  background: transparent !important;
-  color: var(--accent) !important; font-weight: 700;
-  padding: 0.55rem 0.85rem 0.55rem 0.1rem;
-}
-.site-form .slug-url-input:focus { outline: none; }
-.site-form .slug-url-input[readonly] {
-  opacity: 1; cursor: default; background: transparent !important;
-}
-
-/* Selects: same look as the inputs, with a custom chevron instead of the bare
-   native arrow. The open option list is still rendered by the browser. */
-.site-form select {
-  appearance: none;
-  -webkit-appearance: none;
-  padding: 0.55rem 2.2rem 0.55rem 0.85rem;
-  border: 1px solid var(--rule);
-  border-radius: 5px;
-  background-color: var(--paper);
-  color: var(--ink);
-  font-family: inherit;
-  font-size: 0.95rem;
-  cursor: pointer;
-  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
-  background-repeat: no-repeat;
-  background-position: right 0.7rem center;
-  transition: border-color 0.15s ease, box-shadow 0.15s ease;
-}
-.site-form select:hover { border-color: var(--ink-soft, #888); }
-.site-form select:focus,
-.site-form input[type="text"]:focus,
-.site-form textarea:focus {
-  outline: none;
-  border-color: var(--accent, #c2410c);
-  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #c2410c) 22%, transparent);
-}
-
 /* Reuse the palette-swatch styles from account.ejs */
 .palette-swatch {
@@ -474,16 +510,8 @@
   .photo-controls { gap: .55rem; }
 }
-/* Mobile-first: platform selector on its own line (full width), URL input + remove
-   button below. Prevents horizontal overflow on phones (fixed 160px column +
-   non-shrinkable input together ~447px > screen width). */
 .profile-link-row {
-  display: grid; grid-template-columns: 1fr auto; gap: 0.5rem; margin-bottom: 0.5rem;
-}
-.profile-link-row select { padding: 0.45rem 0.6rem; grid-column: 1 / -1; }
-.profile-link-row input { min-width: 0; }
-@media (min-width: 520px) {
-  .profile-link-row { grid-template-columns: 160px 1fr auto; }
-  .profile-link-row select { grid-column: auto; }
-}
+  display: grid; grid-template-columns: 160px 1fr auto; gap: 0.5rem; margin-bottom: 0.5rem;
+}
+.profile-link-row select { padding: 0.45rem 0.6rem; }
 .profile-links-fieldset details > label { margin-top: 0.4rem; }
 </style>
Index: src/views/pages/admin-sites.ejs
===================================================================
--- src/views/pages/admin-sites.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/admin-sites.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,9 +1,9 @@
 <div class="container ax-page">
 
-  <p><a href="/admin" class="btn">&larr; <%= t('asit2.back') %></a></p>
   <header class="ax-header">
+    <a href="/admin" class="ax-back" aria-label="Terug naar admin">←</a>
     <div>
-      <h1><%= t('asit2.title') %></h1>
-      <p class="ax-tagline"><%= t('asit2.tagline') %></p>
+      <h1>Sites</h1>
+      <p class="ax-tagline">Beheer alle sites op deze installatie.</p>
     </div>
   </header>
@@ -13,5 +13,5 @@
 
   <div class="ax-actions-bar">
-    <a href="/admin/sites/new" class="ax-btn ax-btn-primary">＋ <%= t('asit2.new_site') %></a>
+    <a href="/admin/sites/new" class="ax-btn ax-btn-primary">＋ Nieuwe site</a>
   </div>
 
@@ -20,30 +20,9 @@
       <div class="ax-empty">
         <div class="ax-empty-icon">🌐</div>
-        <p><%= t('asit2.empty') %></p>
-        <p class="ax-empty-sub"><%= t('asit2.empty_sub') %></p>
+        <p>Nog geen sites.</p>
+        <p class="ax-empty-sub">Maak er een via de knop hierboven.</p>
       </div>
     </div>
   <% } else { %>
-    <%
-      var _s_primary    = t('asit2.pill_primary');
-      var _s_primary_t  = t('asit2.pill_primary_title');
-      var _s_public     = t('asit2.pill_public');
-      var _s_public_t   = t('asit2.pill_public_title');
-      var _s_private    = t('asit2.pill_private');
-      var _s_private_t  = t('asit2.pill_private_title');
-      var _s_noindex    = t('asit2.pill_noindex');
-      var _s_noindex_t  = t('asit2.pill_noindex_title');
-      var _s_by         = t('asit2.by');
-      var _s_t_posts    = t('asit2.t_posts');
-      var _s_l_posts    = t('asit2.l_posts');
-      var _s_t_created  = t('asit2.t_created');
-      var _s_l_created  = t('asit2.l_created');
-      var _s_makeprim   = t('asit2.make_primary');
-      var _s_makeprim_t = t('asit2.make_primary_title');
-      var _s_makeprim_c = t('asit2.make_primary_confirm');
-      var _s_edit       = t('asit2.edit');
-      var _s_del        = t('asit2.delete');
-      var _s_del_c      = t('asit2.delete_confirm');
-    %>
     <ul class="ax-list">
       <% sites.forEach(function(s) { %>
@@ -56,15 +35,12 @@
             <div class="ax-site-meta">
               <div class="ax-site-name">
-                <a href="/user/<%= s.slug %>"><%= s.title %></a>
-                <% if (s.is_primary) { %>
-                  <span class="ax-pill ax-pill-primary" title="<%= _s_primary_t %>">★ <%= _s_primary %></span>
-                <% } %>
+                <a href="/sites/<%= s.slug %>"><%= s.title %></a>
                 <% if (s.is_public) { %>
-                  <span class="ax-pill ax-pill-ok" title="<%= _s_public_t %>">✓ <%= _s_public %></span>
+                  <span class="ax-pill ax-pill-ok" title="Publiek zichtbaar">✓ public</span>
                 <% } else { %>
-                  <span class="ax-pill" title="<%= _s_private_t %>">🔒 <%= _s_private %></span>
+                  <span class="ax-pill" title="Niet publiek">🔒 private</span>
                 <% } %>
                 <% if (!s.robots_index) { %>
-                  <span class="ax-pill" title="<%= _s_noindex_t %>">🚫🤖 <%= _s_noindex %></span>
+                  <span class="ax-pill" title="Niet geïndexeerd door zoekmachines">🚫🤖 noindex</span>
                 <% } %>
               </div>
@@ -73,5 +49,5 @@
                 <% if (s.owner_username) { %>
                   <span class="ax-track-sep">·</span>
-                  <span><%= _s_by %> <%= s.owner_username %></span>
+                  <span>door <%= s.owner_username %></span>
                 <% } %>
               </div>
@@ -80,25 +56,19 @@
 
           <div class="ax-site-stats">
-            <span class="ax-stat" title="<%= _s_t_posts %>">
+            <span class="ax-stat" title="Aantal posts">
               <span class="ax-stat-num"><%= s.post_count %></span>
-              <span class="ax-stat-label"><%= _s_l_posts %></span>
+              <span class="ax-stat-label">posts</span>
             </span>
-            <span class="ax-stat" title="<%= _s_t_created %>">
+            <span class="ax-stat" title="Aangemaakt op">
               <span class="ax-stat-num ax-stat-date"><%= formatDate(s.created_at) %></span>
-              <span class="ax-stat-label"><%= _s_l_created %></span>
+              <span class="ax-stat-label">created</span>
             </span>
           </div>
 
           <div class="ax-site-controls">
-            <% if (!s.is_primary && (typeof canMutate === 'undefined' || canMutate)) { %>
-              <form method="post" action="/admin/sites/<%= s.slug %>/make-primary" style="margin:0;display:inline"
-                    data-confirm="<%= _s_makeprim_c %>">
-                <button type="submit" class="ax-icon-btn" aria-label="<%= _s_makeprim %>" title="<%= _s_makeprim_t %>">★</button>
-              </form>
-            <% } %>
-            <a href="/admin/sites/<%= s.slug %>/edit" class="ax-icon-btn" aria-label="<%= _s_edit %>" title="<%= _s_edit %>">✎</a>
+            <a href="/admin/sites/<%= s.slug %>/edit" class="ax-icon-btn" aria-label="Bewerken" title="Bewerken">✎</a>
             <form method="post" action="/admin/sites/<%= s.slug %>/delete" class="ax-delete-form"
-                  data-confirm="<%= _s_del_c %>">
-              <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _s_del %>" title="<%= _s_del %>">🗑</button>
+                  onsubmit="return confirm('Site verwijderen? Lukt alleen als er geen posts zijn.')">
+              <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="Verwijderen" title="Verwijderen">🗑</button>
             </form>
           </div>
@@ -228,9 +198,4 @@
   background: rgba(40,160,90,0.12);
 }
-.ax-pill-primary {
-  color: var(--accent);
-  background: color-mix(in srgb, var(--accent) 14%, transparent);
-  font-weight: 600;
-}
 
 /* ─── Stats (2 mini-columns for sites) ────────────────────────── */
Index: src/views/pages/admin-stats.ejs
===================================================================
--- src/views/pages/admin-stats.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,177 +1,0 @@
-<div class="container stats-page">
-  <p><a href="/admin" class="btn">&larr; <%= t('nav.admin') %></a></p>
-  <h1><%= t('astat.title') %></h1>
-  <p class="stats-note"><%= t('astat.intro') %></p>
-
-  <% if (typeof myIp !== 'undefined' && myIp) { %>
-  <form method="post" action="/admin/stats/exclude-ip" class="stats-note" style="margin:.2rem 0 1.2rem;display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;">
-    <span><%= t('astat.your_ip') %> <code><%= myIp %></code> —
-      <% if (ipExcluded) { %><strong><%= t('astat.ip_not_counted') %></strong><% } else { %><%= t('astat.ip_counted') %><% } %></span>
-    <button type="submit" class="btn" style="padding:3px 10px;font-size:.85rem;"><%= ipExcluded ? t('astat.ip_count') : t('astat.ip_exclude') %></button>
-  </form>
-  <% } %>
-
-  <% var s = stats; var _max = Math.max(1, ...s.series.map(function(d){ return d.pageviews; })); %>
-
-  <div class="stats-cards">
-    <div class="stat-card"><div class="stat-num"><%= s.totals.visitors %></div><div class="stat-label"><%= t('astat.visitor_days', { n: s.days }) %></div></div>
-    <div class="stat-card"><div class="stat-num"><%= s.totals.pageviews %></div><div class="stat-label"><%= t('astat.pageviews_days', { n: s.days }) %></div></div>
-    <div class="stat-card"><div class="stat-num"><%= s.totals.plays %></div><div class="stat-label"><%= t('astat.plays_total') %></div></div>
-    <div class="stat-card"><div class="stat-num"><%= s.totals.postViews %></div><div class="stat-label"><%= t('astat.postviews_total') %></div></div>
-  </div>
-
-  <% if (typeof s.allTime !== 'undefined') { %>
-    <p class="stats-note"><%= t('astat.alltime_pre') %> <strong><%= s.allTime.pageviews %></strong> <%= t('astat.alltime_mid') %> · <strong><%= s.allTime.visitorDays %></strong> <%= t('astat.alltime_post') %></p>
-  <% } %>
-
-  <details class="stats-help">
-    <summary><%= t('astat.help_summary') %></summary>
-    <div class="stats-help-body">
-      <dl class="stats-help-defs">
-        <div><dt><%= t('astat.help_vd_term') %></dt><dd><%= t('astat.help_vd_a') %> <em><%= t('astat.help_vd_em') %></em><%= t('astat.help_vd_b') %></dd></div>
-        <div><dt><%= t('astat.help_pv_term') %></dt><dd><%= t('astat.help_pv') %></dd></div>
-        <div><dt><%= t('astat.help_plays_term') %></dt><dd><%= t('astat.help_plays') %></dd></div>
-        <div><dt><%= t('astat.help_postviews_term') %></dt><dd><%= t('astat.help_postviews') %></dd></div>
-      </dl>
-      <p class="stats-help-note"><%= t('astat.help_footer') %></p>
-    </div>
-  </details>
-
-  <div class="stats-period">
-    <%= t('astat.period') %>
-    <% [7,14,30,90].forEach(function(n){ %>
-      <a href="/admin/stats?days=<%= n %>" class="<%= s.days === n ? 'is-on' : '' %>"><%= n %>d</a>
-    <% }); %>
-  </div>
-
-  <section class="stats-chart-wrap">
-    <div class="stats-chart-head">
-      <h2><%= t('astat.last_n_days', { n: s.days }) %></h2>
-      <div class="stats-legend">
-        <span class="lg lg-vis"><%= t('astat.lg_visitor_days') %></span>
-        <span class="lg lg-pv"><%= t('astat.lg_pageviews') %></span>
-      </div>
-    </div>
-    <div class="stats-chart">
-      <% var _step = s.days <= 14 ? 1 : (s.days <= 31 ? 4 : 10);
-         s.series.forEach(function(d, i){
-           var pvPct = Math.round(d.pageviews / _max * 100);
-           var visPct = Math.round(d.visitors / _max * 100);
-      %>
-        <div class="sc-col" title="<%= t('astat.bar_title', { day: d.day, pv: d.pageviews, vd: d.visitors }) %>">
-          <div class="sc-bar">
-            <span class="sc-pv" style="height: <%= pvPct %>%"></span>
-            <span class="sc-vis" style="height: <%= visPct %>%"></span>
-          </div>
-          <%# Unlabeled columns get an NBSP so every .sc-day keeps its text line: an empty
-              span collapses to 0 height and made the 30/90-day columns jump around. %>
-          <span class="sc-day"><%= (i % _step === 0) ? d.day.slice(8) : ' ' %></span>
-        </div>
-      <% }); %>
-    </div>
-  </section>
-
-  <div class="stats-tops">
-    <section class="stats-top">
-      <h2><%= t('astat.top_posts') %></h2>
-      <% if (!s.topPosts.length || s.topPosts[0].views === 0) { %>
-        <p class="stats-empty"><%= t('astat.no_views') %></p>
-      <% } else { %>
-        <ol>
-          <% s.topPosts.forEach(function(p){ %>
-            <li><a href="/<%= p.slug %>"><%= p.title %></a><span class="stats-count"><%= p.views %></span></li>
-          <% }); %>
-        </ol>
-      <% } %>
-    </section>
-    <section class="stats-top">
-      <h2><%= t('astat.most_played') %></h2>
-      <% if (!s.topTracks.length || s.topTracks[0].plays === 0) { %>
-        <p class="stats-empty"><%= t('astat.no_plays') %></p>
-      <% } else { %>
-        <ol>
-          <% s.topTracks.forEach(function(t){ %>
-            <li><span><%= t.title %></span><span class="stats-count"><%= t.plays %></span></li>
-          <% }); %>
-        </ol>
-      <% } %>
-    </section>
-  </div>
-
-  <% if (typeof s.referrers !== 'undefined' && s.referrers.length) { %>
-    <section class="stats-top" style="margin-top:1.2rem">
-      <h2>🌐 <%= t('astat.sources') %></h2>
-      <ol>
-        <% s.referrers.forEach(function(r){ %>
-          <li><span><%= r.host %></span><span class="stats-count"><%= r.n %></span></li>
-        <% }); %>
-      </ol>
-    </section>
-  <% } %>
-
-  <% if (typeof linkClicks !== 'undefined' && linkClicks.length) { %>
-    <section class="stats-top" style="margin-top:1.2rem">
-      <h2>🔗 <%= t('astat.linkbio_clicks') %></h2>
-      <ol>
-        <% linkClicks.forEach(function(l){ %>
-          <li><a href="<%= l.url %>" target="_blank" rel="noopener"><%= l.url.replace(/^https?:\/\//,'').slice(0,48) %></a><span class="stats-count"><%= l.clicks %></span></li>
-        <% }); %>
-      </ol>
-    </section>
-  <% } %>
-</div>
-
-<style>
-.stats-page { max-width: 820px; margin: 2.5rem auto; padding: 0 1rem; }
-.stats-page h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: .5rem 0 .25rem; }
-.stats-note { color: var(--ink-muted); font-size: .85rem; margin: 0 0 1.5rem; max-width: 60ch; }
-.stats-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: .75rem; margin-bottom: 2rem; }
-.stat-card { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 10px; padding: 1rem 1.1rem; }
-.stat-num { font-family: var(--font-display, serif); font-size: 1.9rem; line-height: 1; color: var(--accent); }
-.stat-label { color: var(--ink-muted); font-size: .82rem; margin-top: .35rem; }
-
-.stats-chart-wrap { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 12px; padding: 1.1rem 1.2rem 1rem; margin-bottom: 2rem; }
-.stats-chart-head { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: .5rem; }
-.stats-chart-head h2 { font-size: 1.05rem; margin: 0; }
-.stats-legend { display: flex; gap: .9rem; font-size: .78rem; color: var(--ink-muted); }
-.lg::before { content: ""; display: inline-block; width: .7rem; height: .7rem; border-radius: 2px; margin-right: .3rem; vertical-align: -1px; }
-.lg-vis::before { background: var(--accent); }
-.lg-pv::before { background: color-mix(in srgb, var(--accent) 28%, transparent); }
-.stats-chart { display: flex; align-items: flex-end; gap: 2px; height: 160px; margin-top: 1rem; overflow: hidden; }
-.sc-col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; height: 100%; justify-content: flex-end; }
-.sc-bar { position: relative; width: 70%; max-width: 26px; min-width: 2px; height: 140px; display: flex; align-items: flex-end; }
-.sc-day { white-space: nowrap; }
-.sc-pv, .sc-vis { position: absolute; bottom: 0; left: 0; width: 100%; border-radius: 3px 3px 0 0; min-height: 2px; }
-.sc-pv { background: color-mix(in srgb, var(--accent) 28%, transparent); }
-.sc-vis { background: var(--accent); }
-.sc-day { font-size: .68rem; color: var(--ink-muted); margin-top: .35rem; }
-
-.stats-tops { display: grid; grid-template-columns: 1fr 1fr; gap: 1.2rem; }
-@media (max-width: 560px) { .stats-tops { grid-template-columns: 1fr; } }
-.stats-top { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 12px; padding: 1rem 1.2rem; }
-.stats-top h2 { font-size: 1.05rem; margin: 0 0 .6rem; }
-.stats-top ol { margin: 0; padding-left: 1.2rem; }
-.stats-top li { padding: .35rem 0; display: flex; justify-content: space-between; gap: .5rem; align-items: baseline; }
-.stats-top a { color: var(--ink); text-decoration: none; }
-.stats-top a:hover { color: var(--accent); }
-.stats-count { color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums; }
-.stats-empty { color: var(--ink-muted); font-size: .88rem; margin: 0; }
-.stats-period { margin: .2rem 0 1.2rem; font-size: .9rem; color: var(--ink-muted); }
-.stats-period a { display: inline-block; margin-left: .4rem; padding: .2rem .6rem; border: 1px solid var(--rule); border-radius: 999px; text-decoration: none; color: var(--ink); }
-.stats-period a.is-on { background: var(--accent); color: #fff; border-color: var(--accent); }
-.stats-help { border: 1px solid var(--rule); border-radius: 12px; background: var(--paper-2); margin: 0 0 1.25rem; }
-.stats-help summary { cursor: pointer; padding: .7rem 1rem; font-weight: 600; font-size: .9rem; list-style: none; display: flex; align-items: center; gap: .5rem; }
-.stats-help summary::-webkit-details-marker { display: none; }
-.stats-help summary::before { content: '\203A'; font-size: 1.15rem; line-height: 1; color: var(--ink-soft); transition: transform .15s; }
-.stats-help[open] summary::before { transform: rotate(90deg); }
-.stats-help-body { padding: 0 1rem 1rem; }
-.stats-help-defs { margin: 0; }
-.stats-help-defs > div { display: grid; grid-template-columns: 130px 1fr; gap: .2rem 1.1rem; align-items: baseline;
-  padding: .55rem 0; border-top: 1px solid color-mix(in srgb, var(--rule) 55%, transparent); }
-.stats-help-defs dt { font-weight: 700; font-size: .85rem; color: var(--ink); }
-.stats-help-defs dd { margin: 0; font-size: .85rem; line-height: 1.5; color: var(--ink-soft); }
-.stats-help-defs dd em { font-style: italic; color: var(--ink); }
-.stats-help-note { margin: .85rem 0 0; padding-top: .7rem; border-top: 1px solid var(--rule);
-  font-size: .8rem; line-height: 1.55; color: var(--ink-muted); }
-@media (max-width: 480px) { .stats-help-defs > div { grid-template-columns: 1fr; gap: .1rem; } }
-</style>
Index: src/views/pages/admin-updates.ejs
===================================================================
--- src/views/pages/admin-updates.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,95 +1,0 @@
-<div class="container admin-page">
-  <h1><%= t('aupd.title') %></h1>
-  <p><a href="/admin" class="btn">&larr; <%= t('aupd.back_admin') %></a></p>
-
-  <% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
-  <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
-
-  <section class="set-card">
-    <h2><%= t('aupd.version_heading') %></h2>
-
-    <div class="upd-row">
-      <span class="upd-label"><%= t('aupd.app_version') %></span>
-      <span class="upd-val"><%= appVersion || '—' %></span>
-    </div>
-    <div class="upd-row">
-      <span class="upd-label"><%= t('aupd.current') %></span>
-      <span class="upd-val">
-        <% if (currentSha) { %><code><%= currentSha %></code> <span class="upd-desc"><%= currentDesc %></span>
-        <% } else { %><em><%= t('aupd.current_unknown') %></em><% } %>
-      </span>
-    </div>
-    <div class="upd-row">
-      <span class="upd-label"><%= t('aupd.latest') %></span>
-      <span class="upd-val">
-        <% if (latestSha) { %><code><%= latestSha %></code> <span class="upd-desc"><%= latestDesc %></span>
-        <% } else { %><em><%= t('aupd.latest_failed') %></em><% } %>
-      </span>
-    </div>
-
-    <div class="upd-status">
-      <% if (!canCheck) { %>
-        <span class="upd-badge warn">&#9888; <%= t('aupd.no_source') %></span>
-      <% } else if (upToDate) { %>
-        <span class="upd-badge ok">&#10003; <%= t('aupd.uptodate') %></span>
-      <% } else { %>
-        <span class="upd-badge new">&#8635; <%= t('aupd.update_available') %><% if (behind) { %> (<%= behind === '1' ? t('aupd.behind_one', { n: behind }) : t('aupd.behind_many', { n: behind }) %>)<% } %></span>
-      <% } %>
-    </div>
-
-    <% if (canCheck && canSelfUpdate) { %>
-    <form method="post" action="/admin/updates/run" class="set-form"
-          data-confirm="<%= t('aupd.run_confirm') %>">
-      <button type="submit" class="btn btn-primary">
-        <%= upToDate ? '↻ ' + t('aupd.redeploy') : '⬆ ' + t('aupd.update_now') %>
-      </button>
-    </form>
-    <% } else if (typeof manualCommand !== 'undefined' && manualCommand && !upToDate) { %>
-    <p class="set-help" style="margin:.2rem 0 .45rem"><%= t('aupd.manual_hint') %></p>
-    <pre class="upd-cmd"><code><%= manualCommand %></code></pre>
-    <% } %>
-
-    <p class="set-help" style="margin-top:1rem">
-      <%= t('aupd.help') %>
-    </p>
-  </section>
-
-  <% if (changes && changes.length) { %>
-  <section class="set-card" style="margin-top:1rem">
-    <h2><%= t('aupd.changes_heading') %></h2>
-    <ul class="upd-changes">
-      <% changes.forEach(function (c) { %>
-        <li><span class="upd-ch-date"><%= c.date %></span> <span class="upd-ch-msg"><%= c.msg %></span></li>
-      <% }) %>
-    </ul>
-  </section>
-  <% } %>
-</div>
-
-<style>
-.admin-page { max-width: 700px; margin: 3rem auto; padding: 0 1rem; }
-.admin-page h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: 0 0 .25rem; }
-.set-card { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 12px; padding: 1.25rem; }
-.set-card h2 { font-family: var(--font-display, serif); font-size: 1.25rem; margin: 0 0 1rem; }
-.set-help { color: var(--ink-muted); font-size: .9rem; }
-.upd-row { display: flex; gap: 1rem; padding: .55rem 0; border-bottom: 1px solid var(--rule); align-items: baseline; }
-.upd-label { width: 90px; flex-shrink: 0; font-size: .8rem; font-weight: 600; color: var(--ink-muted); }
-.upd-val { font-size: .9rem; }
-.upd-val code { background: var(--paper); padding: .1rem .4rem; border-radius: 4px; font-size: .85em; }
-.upd-desc { color: var(--ink-muted); }
-.upd-status { margin: 1.1rem 0; }
-.upd-cmd { background: var(--paper); border: 1px solid var(--rule); border-radius: 6px; padding: .6rem .8rem; margin: 0; overflow-x: auto; }
-.upd-cmd code { font-size: .9em; }
-.upd-badge { font-size: .8rem; font-weight: 700; padding: .25rem .7rem; border-radius: 20px; }
-.upd-badge.ok { background: #d1fae5; color: #065f46; }
-.upd-badge.new { background: #fef3c7; color: #92400e; }
-.upd-badge.warn { background: #fee2e2; color: #991b1b; }
-.alert { padding: .75rem 1rem; border-radius: 6px; margin-bottom: 1rem; }
-.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
-.alert-error { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
-.upd-changes { list-style: none; padding: 0; margin: 0; }
-.upd-changes li { display: flex; gap: .7rem; padding: .45rem 0; border-bottom: 1px solid var(--rule); font-size: .9rem; align-items: baseline; }
-.upd-changes li:last-child { border-bottom: 0; }
-.upd-ch-date { color: var(--ink-muted); flex-shrink: 0; font-size: .8rem; font-variant-numeric: tabular-nums; }
-.upd-ch-msg { word-break: break-word; }
-</style>
Index: src/views/pages/admin-users.ejs
===================================================================
--- src/views/pages/admin-users.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/admin-users.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,9 +1,9 @@
 <div class="container ax-page">
 
-  <p><a href="/admin" class="btn">&larr; <%= t('ausr.back') %></a></p>
   <header class="ax-header">
+    <a href="/admin" class="ax-back" aria-label="Terug naar admin">←</a>
     <div>
-      <h1><%= t('ausr.title') %></h1>
-      <p class="ax-tagline"><%= t('ausr.tagline_a') %> <strong><%= t('ausr.role_kijker') %></strong> <%= t('ausr.tagline_b') %></p>
+      <h1>Users</h1>
+      <p class="ax-tagline">Beheer gebruikers, rollen, en verwijderingen.</p>
     </div>
   </header>
@@ -16,24 +16,8 @@
       <div class="ax-empty">
         <div class="ax-empty-icon">👤</div>
-        <p><%= t('ausr.empty') %></p>
+        <p>Geen gebruikers.</p>
       </div>
     </div>
   <% } else { %>
-    <%
-      var _u_self      = t('ausr.you');
-      var _u_t_sites   = t('ausr.t_sites');
-      var _u_t_posts   = t('ausr.t_posts');
-      var _u_t_joined  = t('ausr.t_joined');
-      var _u_l_sites   = t('ausr.l_sites');
-      var _u_l_posts   = t('ausr.l_posts');
-      var _u_l_joined  = t('ausr.l_joined');
-      var _u_newklonkt = t('ausr.new_klonkt');
-      var _u_rol       = t('ausr.role_label');
-      var _u_r_kijker  = t('ausr.role_kijker');
-      var _u_r_member  = t('ausr.role_member');
-      var _u_r_admin   = t('ausr.role_admin');
-      var _u_r_god     = t('ausr.role_god');
-      var _u_del       = t('ausr.delete');
-    %>
     <ul class="ax-list">
       <% users.forEach(function(u) { %>
@@ -42,5 +26,5 @@
           <div class="ax-user-id">
             <% if (u.avatar_url) { %>
-              <img class="ax-avatar" src="<%= avatar(u.avatar_url, 128) %>" alt="">
+              <img class="ax-avatar" src="<%= u.avatar_url %>" alt="">
             <% } else { %>
               <span class="ax-avatar ax-avatar-empty"><%= u.username.charAt(0).toUpperCase() %></span>
@@ -49,5 +33,5 @@
               <div class="ax-user-name">
                 <a href="/users/<%= encodeURIComponent(u.username) %>"><%= u.username %></a>
-                <% if (u.id === user.id) { %><span class="ax-pill"><%= _u_self %></span><% } %>
+                <% if (u.id === user.id) { %><span class="ax-pill">jij</span><% } %>
               </div>
               <div class="ax-user-email"><%= u.email %></div>
@@ -56,15 +40,15 @@
 
           <div class="ax-user-stats">
-            <span class="ax-stat" title="<%= _u_t_sites %>">
+            <span class="ax-stat" title="Sites">
               <span class="ax-stat-num"><%= u.site_count %></span>
-              <span class="ax-stat-label"><%= _u_l_sites %></span>
+              <span class="ax-stat-label">sites</span>
             </span>
-            <span class="ax-stat" title="<%= _u_t_posts %>">
+            <span class="ax-stat" title="Posts">
               <span class="ax-stat-num"><%= u.post_count %></span>
-              <span class="ax-stat-label"><%= _u_l_posts %></span>
+              <span class="ax-stat-label">posts</span>
             </span>
-            <span class="ax-stat" title="<%= _u_t_joined %>">
+            <span class="ax-stat" title="Geregistreerd op">
               <span class="ax-stat-num ax-stat-date"><%= formatDate(u.created_at) %></span>
-              <span class="ax-stat-label"><%= _u_l_joined %></span>
+              <span class="ax-stat-label">joined</span>
             </span>
           </div>
@@ -73,25 +57,18 @@
             <form method="post" action="/admin/users/<%= u.id %>/role" class="ax-role-form">
               <label class="ax-role">
-                <span><%= _u_rol %></span>
-                <select name="role" data-autosubmit <% if (u.id === user.id || !canMutate) { %>disabled<% } %>>
-                  <option value="kijker" <%= u.role === 'kijker' ? 'selected' : '' %>><%= _u_r_kijker %></option>
-                  <option value="member" <%= u.role === 'member' ? 'selected' : '' %>><%= _u_r_member %></option>
-                  <option value="admin"  <%= u.role === 'admin'  ? 'selected' : '' %>><%= _u_r_admin %></option>
-                  <option value="god"    <%= u.role === 'god'    ? 'selected' : '' %>><%= _u_r_god %></option>
+                <span>Rol</span>
+                <select name="role" onchange="this.form.submit()" <% if (u.id === user.id) { %>disabled<% } %>>
+                  <option value="member" <%= u.role === 'member' ? 'selected' : '' %>>member</option>
+                  <option value="admin"  <%= u.role === 'admin'  ? 'selected' : '' %>>admin</option>
+                  <option value="god"    <%= u.role === 'god'    ? 'selected' : '' %>>god</option>
                 </select>
               </label>
             </form>
 
-            <% if (u.id !== user.id && canMutate) { %>
-              <% var _warn = (u.post_count + u.site_count > 0)
-                   ? (t('ausr.del_warn', { n: u.post_count }) + ' ')
-                   : ''; %>
+            <% if (u.id !== user.id && (u.post_count + u.site_count === 0)) { %>
               <form method="post" action="/admin/users/<%= u.id %>/delete" class="ax-delete-form"
-                    data-confirm="<%= t('ausr.del_confirm', { name: u.username }) %> <%= _warn %><%= t('ausr.del_undo') %>">
-                <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _u_del %>" title="<%= _u_del %>">🗑</button>
+                    onsubmit="return confirm('Verwijder gebruiker <%= u.username %>?')">
+                <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="Verwijderen" title="Verwijderen">🗑</button>
               </form>
-            <% } else { %>
-              <%# Empty placeholder so the role column aligns on every row. %>
-              <span class="ax-icon-btn-spacer" aria-hidden="true"></span>
             <% } %>
           </div>
@@ -215,5 +192,5 @@
 /* ─── Role select + delete ────────────────────────────────────── */
 .ax-user-controls {
-  display: flex; gap: 0.5rem; align-items: flex-end;
+  display: flex; gap: 0.5rem; align-items: center;
   flex-shrink: 0;
 }
@@ -231,7 +208,4 @@
 }
 .ax-role select {
-  width: 112px;
-  height: 40px;
-  box-sizing: border-box;
   padding: 0.4rem 1.75rem 0.4rem 0.6rem;
   border: 1px solid var(--rule);
@@ -248,7 +222,4 @@
 }
 .ax-role select:disabled { cursor: not-allowed; opacity: 0.5; }
-.ax-ro { align-items: center; }
-.ax-ro input { width: 18px; height: 18px; cursor: pointer; margin-top: 0.15rem; accent-color: var(--accent); }
-.ax-ro input:disabled { cursor: not-allowed; opacity: 0.5; }
 .ax-delete-form { margin: 0; }
 .ax-icon-btn {
@@ -262,5 +233,4 @@
 .ax-icon-btn:hover { border-color: var(--accent); }
 .ax-icon-btn-danger:hover { color: #dc2626; border-color: #dc2626; }
-.ax-icon-btn-spacer { display: inline-block; width: 40px; height: 40px; flex-shrink: 0; }
 
 /* ─── Mobile: stack everything vertically ─────────────────────── */
Index: src/views/pages/admin-videos.ejs
===================================================================
--- src/views/pages/admin-videos.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,53 +1,0 @@
-<%# Beheer -> Media -> Video's (Robins opdracht, 30-7): de video's die via C2S
-    binnenkwamen (Shaers composer, de reddingsboei), met gebruik en opruiming.
-
-    De voorvertoning is de video zelf met preload="metadata", zodat de browser
-    het eerste beeld als miniatuur toont tot echte poster-generatie landt
-    (bead: ffmpeg ontbreekt).
-
-    Zelfde romp als de andere mediapagina's; stond hiervoor op inline styles. %>
-<%- include('../partials/admin-styles') %>
-<div class="container ax-page">
-
-  <%- include('../partials/admin-back') %>
-  <%- include('../partials/media-tabs', { active: 'videos', audioOn: audioOn }) %>
-  <header class="ax-header">
-    <div>
-      <h1><%= t('admin.t_media') %></h1>
-      <p class="ax-tagline"><%= items.length %> <%= t('admin.videos_count') %></p>
-    </div>
-  </header>
-
-  <% if (success) { %><div class="ax-flash ax-flash-ok"><%= success %></div><% } %>
-
-  <% if (!items.length) { %>
-    <div class="ax-empty">
-      <div class="ax-empty-icon">&#127916;</div>
-      <p><%= t('admin.videos_empty') %></p>
-    </div>
-  <% } else { %>
-  <div class="ax-grid" style="--ax-tile:220px;">
-    <% items.forEach(function(it){ %>
-      <div class="ax-tile media-card" data-file="<%= it.file %>">
-        <div class="ax-tile-media" style="aspect-ratio:16/10;background:#000;">
-          <video src="<%= it.url %>" preload="metadata" controls playsinline style="height:100%;object-fit:contain;"></video>
-          <% if (!it.usedCount) { %><span class="ax-tile-badge"><%= t('admin.media_unused') %></span><% } %>
-        </div>
-        <div class="ax-tile-body">
-          <div class="ax-tile-meta" style="word-break:break-all;"><%= it.file %></div>
-          <div class="ax-tile-meta"><%= it.kb %> KB &middot; <%= it.usedCount %>&times;</div>
-          <div class="ax-tile-actions">
-            <button type="button" class="ax-btn ax-btn-sm" data-copy="<%= it.url %>"><%= t('admin.media_copy') %></button>
-            <% if (!it.usedCount) { %>
-            <button type="button" class="ax-btn ax-btn-sm ax-btn-danger" data-del="<%= it.file %>">&#128465;</button>
-            <% } %>
-          </div>
-        </div>
-      </div>
-    <% }) %>
-  </div>
-  <% } %>
-</div>
-
-<%# Het script staat in assets/js/mod/admin-videos.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
-<%- include('../partials/page-data', { pageData: { copy: t('admin.media_copy'), delC: t('admin.videos_del_confirm') } }) %>
Index: src/views/pages/admin.ejs
===================================================================
--- src/views/pages/admin.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/admin.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,77 +1,74 @@
 <div class="container admin-page">
-  <h1><%= t('admin.title') %></h1>
-  <%
-    var _premOn = (typeof premiumEnabled !== 'undefined' && premiumEnabled);
-    var _premActive = _premOn && (typeof isPremium !== 'undefined' && isPremium);
-    var _premCls = _premActive ? 'on' : (_premOn ? 'off' : 'free');
-    var _premLbl = _premActive ? ('★ ' + t('adash.prem_active'))
-                 : (_premOn ? ('⚠ ' + t('adash.prem_unlinked'))
-                            : t('adash.prem_layeroff'));
-  %>
-  <a class="prem-badge <%= _premCls %>" href="/admin/settings" title="<%= t('admin.b_settings') %>"><%= _premLbl %></a>
-  <style>
-    .prem-badge{display:inline-flex;align-items:center;gap:.35rem;font-size:.78rem;font-weight:700;letter-spacing:.02em;padding:.3rem .85rem;border-radius:999px;text-decoration:none;border:1px solid transparent;margin:.1rem 0 .3rem}
-    .prem-badge.on{background:rgba(232,176,75,.16);color:var(--accent);border-color:rgba(232,176,75,.45)}
-    .prem-badge.off{background:rgba(239,68,68,.12);color:#ef9a9a;border-color:rgba(239,68,68,.35)}
-    .prem-badge.free{background:var(--paper-2);color:var(--ink-muted);border-color:var(--rule)}
-    .prem-badge:hover{filter:brightness(1.08)}
-  </style>
-  <% if (typeof circlesOn !== 'undefined' && circlesOn) { %>
-    <p class="admin-tagline"><%= t('admin.tagline_cirkels') %></p>
-  <% } else { %>
-    <p class="admin-tagline"><%= t('admin.tagline_solo') %></p>
-  <% } %>
+  <h1>Admin</h1>
+  <p class="admin-tagline">God-level overview. <em>(Stub — full dashboard later.)</em></p>
 
-  <nav class="admin-quick-links" aria-label="<%= t('nav.admin') %>">
-      <a href="/posts/new" class="btn"><%= t('admin.b_newpost') %></a>
-      <a href="/admin/media" class="btn"><%= t('admin.b_media') %></a>
-      <% if (primarySite) { %>
-        <a href="/admin/sites/<%= primarySite.slug %>/edit" class="btn"><%= t('admin.b_look') %></a>
-      <% } else { %>
-        <a href="/admin/sites/new" class="btn"><%= t('admin.b_makesite') %></a>
-      <% } %>
-      <% if (primarySite) { %><a href="/admin/seo" class="btn"><%= t('admin.b_seo') %></a><% } %>
-      <% if (primarySite) { %><a href="/admin/migrate" class="btn"><%= t('admin.b_migrate') %></a><% } %>
-      <a href="/admin/settings" class="btn"><%= t('admin.b_settings') %></a>
-    <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
-      <a href="/admin/stats" class="btn"><%= t('admin.b_stats') %></a>
-      <a href="/admin/paid" class="btn"><%= t('admin.b_paid') %></a>
-      <a href="/admin/newsletter" class="btn"><%= t('admin.b_newsletter') %></a>
-      <% if (primarySite) { %><a href="/pers" class="btn" target="_blank"><%= t('admin.b_perskit') %></a><% } %>
-      <% if (primarySite) { %><a href="/downloads" class="btn" target="_blank"><%= t('admin.b_downloads') %></a><% } %>
-      <% if (primarySite) { %><a href="/links" class="btn" target="_blank"><%= t('admin.b_linkbio') %></a><% } %>
-      <a href="/admin/shows" class="btn"><%= t('admin.b_agenda') %></a>
-    <% } %>
-    <%# Fediverse pages (News / Notifications / My replies / Blocking) are reached via the
-        "Fediverse" nav button + the bell + the tabbed section — not duplicated here. %>
-    <a href="/admin/push" class="btn"><%= t('admin.b_push') %></a>
-    <a href="/admin/updates" class="btn"><%= t('admin.b_updates') %></a>
-    <a href="/admin/handleiding" class="btn"><%= t('admin.b_help') %></a>
+  <nav class="admin-quick-links" aria-label="Beheer-snelkoppelingen">
+    <a href="/admin/sites" class="btn">🌐 Sites</a>
+    <a href="/admin/users" class="btn">👥 Users</a>
+    <a href="/admin/audio" class="btn">🎵 Audio tracks</a>
+    <a href="/admin/playlists" class="btn">📃 Playlists</a>
+    <a href="/admin/comments" class="btn">💬 Comments</a>
   </nav>
 
   <div class="admin-stats">
-    <div class="stat-card"><div class="stat-num"><%= stats.posts %></div><div class="stat-label"><%= t('admin.st_posts') %></div></div>
-    <div class="stat-card"><div class="stat-num"><%= stats.published %></div><div class="stat-label"><%= t('admin.st_published') %></div></div>
+    <div class="stat-card">
+      <div class="stat-num"><%= stats.users %></div>
+      <div class="stat-label">Users</div>
+    </div>
+    <div class="stat-card">
+      <div class="stat-num"><%= stats.sites %></div>
+      <div class="stat-label">Sites</div>
+    </div>
+    <div class="stat-card">
+      <div class="stat-num"><%= stats.posts %></div>
+      <div class="stat-label">Posts</div>
+    </div>
+    <div class="stat-card">
+      <div class="stat-num"><%= stats.published %></div>
+      <div class="stat-label">Published</div>
+    </div>
   </div>
 
-  <% if (typeof posts !== 'undefined' && posts && posts.length) { %>
-    <% var _drafts = posts.filter(function(p){ return p.isDraft; }).length; %>
-    <h2><%= t('admin.sec_posts') %><% if (_drafts) { %> <span class="pl-draftcount"><%= _drafts %> <%= t('admin.draft').toLowerCase() %><%= _drafts === 1 ? '' : 's' %></span><% } %></h2>
-    <ul class="post-admin-list">
-      <% posts.forEach(function(p) { %>
-        <li class="pal-row<%= p.isDraft ? ' is-draft' : '' %>">
-          <a class="pal-title" href="<%= p.editUrl %>">
-            <% if (p.isDraft) { %><span class="pal-badge"><%= t('admin.draft') %></span><% } %>
-            <span class="pal-name"><%= p.title || '(zonder titel)' %></span>
-          </a>
-          <span class="pal-actions">
-            <a class="pal-link" href="<%= p.editUrl %>"><%= t('admin.edit') %></a>
-            <% if (!p.isDraft) { %><a class="pal-link" href="<%= p.viewUrl %>"><%= t('admin.view') %></a><% } %>
-          </span>
-        </li>
-      <% }); %>
-    </ul>
+  <% if (sites && sites.length) { %>
+    <h2>Sites</h2>
+    <table class="admin-table">
+      <thead>
+        <tr><th>Slug</th><th>Title</th><th>Owner</th><th>Created</th></tr>
+      </thead>
+      <tbody>
+        <% sites.forEach(function(s) { %>
+          <tr>
+            <td><code><%= s.slug %></code></td>
+            <td><%= s.title %></td>
+            <td><%= s.owner_username || '—' %></td>
+            <td><%= formatDate(s.created_at) %></td>
+          </tr>
+        <% }); %>
+      </tbody>
+    </table>
   <% } %>
 
+  <% if (users && users.length) { %>
+    <h2>Users</h2>
+    <table class="admin-table">
+      <thead>
+        <tr><th>Username</th><th>Email</th><th>Role</th><th>Joined</th></tr>
+      </thead>
+      <tbody>
+        <% users.forEach(function(u) { %>
+          <tr>
+            <td><%= u.username %></td>
+            <td><%= u.email %></td>
+            <td><span class="role-pill role-<%= u.role %>"><%= u.role %></span></td>
+            <td><%= formatDate(u.created_at) %></td>
+          </tr>
+        <% }); %>
+      </tbody>
+    </table>
+  <% } %>
+
+  <p class="admin-todo">
+    <strong>Coming later:</strong> create/delete sites, manage users, theme picker, audio toggles, Prutter settings.
+  </p>
 </div>
 
@@ -82,18 +79,4 @@
 .admin-tagline { color: var(--ink-muted); margin: 0 0 2rem; }
 .admin-tagline em { font-style: normal; opacity: 0.7; }
-
-/* Posts/concepten-lijst */
-.pl-draftcount { font-family: var(--font-ui, system-ui); font-size: 0.8rem; font-weight: 700; color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); padding: 0.1rem 0.55rem; border-radius: 99px; vertical-align: middle; }
-.post-admin-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
-.pal-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.7rem 0.9rem; border: 1px solid var(--rule); border-radius: 10px; background: var(--paper-2); transition: border-color 120ms; }
-.pal-row:hover { border-color: var(--accent); }
-.pal-row.is-draft { border-left: 3px solid var(--accent); }
-.pal-title { display: inline-flex; align-items: center; gap: 0.6rem; min-width: 0; color: var(--ink); text-decoration: none; font-weight: 600; }
-.pal-title:hover { color: var(--accent); }
-.pal-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.pal-badge { flex-shrink: 0; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); padding: 0.1rem 0.5rem; border-radius: 99px; }
-.pal-actions { display: flex; gap: 0.75rem; flex-shrink: 0; }
-.pal-link { color: var(--ink-muted); font-size: 0.85rem; text-decoration: none; white-space: nowrap; }
-.pal-link:hover { color: var(--accent); text-decoration: underline; }
 
 /* Quick-links grid — mobile-first.
Index: src/views/pages/archive.ejs
===================================================================
--- src/views/pages/archive.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/archive.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,19 +1,32 @@
-<%# Archief — altijd de chronologische jaar/maand-lijst. Het archief negeert
-    bewust de timeline/grid-keuze (geen grid-weergave): de view-switcher is hier
-    grijs en springt bij klik terug naar de feed. Zie de CSS in style.css. %>
+<%
+// Archive — chronological feed of all posts.
+//
+// Renders both a timeline view (year/month grouped link list) AND a grid
+// view (flat tiles) — same toggle pattern as home.ejs. The view-switcher
+// at the top lets the user pick. Body class is `on-archive`; the inline
+// <style> below adds an `.on-archive` clause to the existing home-only
+// feed toggle in style.css (cleaner than editing the global CSS).
+//
+// Flatten posts for grid mode: skip the year/month grouping (it doesn't
+// translate well to a tile grid). Newest-first order is preserved by the
+// route's SQL ORDER BY.
+const _flatPosts = [];
+Object.keys(grouped).sort((a, b) => b - a).forEach(function(y) {
+  Object.keys(grouped[y]).forEach(function(m) {
+    grouped[y][m].forEach(function(p) { _flatPosts.push(p); });
+  });
+});
+%>
 
-<!-- ========== TIJDLIJN (jaar/maand gegroepeerd) ========== -->
+<%# View switcher used to be rendered here; now lives in shell.ejs above
+    #pcms-main, visible on every non-admin page. %>
+
+<!-- ========== TIMELINE (year/month grouped) ========== -->
 <div class="container archive-page feed-timeline">
-  <% var _archBase = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : ''; %>
-  <p class="archive-back-row">
-    <a href="<%= _archBase %>/" class="btn archive-back"
-       hx-get="<%= _archBase %>/?partial=1" hx-target="#pcms-main"
-       hx-push-url="<%= _archBase %>/" hx-indicator="#pcms-loading">&larr; <%= t('parch.back') %></a>
-  </p>
-  <h1><%= t('parch.title') %></h1>
-  <p class="archive-meta"><%= totalPosts === 1 ? t('parch.count_one', { n: totalPosts }) : t('parch.count_many', { n: totalPosts }) %></p>
+  <h1>Archive</h1>
+  <p class="archive-meta"><%= totalPosts %> post<%= totalPosts !== 1 ? 's' : '' %></p>
 
   <% if (totalPosts === 0) { %>
-    <p class="empty-state"><%= t('parch.empty') %></p>
+    <p class="empty-state">No posts yet.</p>
   <% } %>
 
@@ -42,7 +55,17 @@
 </div>
 
+<!-- ========== GRID (only visible when body[data-feed-view="grid"]) ========== -->
+<% if (_flatPosts.length > 0) { %>
+<section class="feed-grid container" aria-label="Grid-weergave">
+  <div class="grid-tiles">
+    <% _flatPosts.forEach(function(post) { %>
+      <%- include('../partials/post-tile', { post: post }) %>
+    <% }); %>
+  </div>
+</section>
+<% } %>
+
 <style>
 .archive-page { max-width: 700px; margin: 2rem auto; padding: 0 1rem; }
-.archive-back-row { margin: 0 0 1rem; }
 .archive-page h1 { font-family: var(--font-display, serif); margin: 0 0 0.25rem; }
 .archive-meta { color: var(--ink-muted); margin-bottom: 2rem; }
Index: src/views/pages/auth-login.ejs
===================================================================
--- src/views/pages/auth-login.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/auth-login.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,30 +1,23 @@
-<%
-  // The login page shows the admin username/password form. (Public listener
-  // login via Google was removed — interaction now runs via the fediverse.)
-  var _admin = (typeof adminLogin !== 'undefined' && adminLogin);
-%>
 <div class="container auth-page">
-  <h1><%= _admin ? t('auth.admin_login_title') : t('nav.login') %></h1>
+  <h1>Login</h1>
   <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
   <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
-
-  <% if (_admin) { %>
-    <%# Hidden admin login (password). %>
-    <form method="post" action="/auth/login" class="auth-form">
-      <% if (typeof next !== 'undefined' && next) { %>
-        <input type="hidden" name="next" value="<%= next %>">
-      <% } %>
-      <label>
-        <span><%= t('auth.username_or_email') %></span>
-        <input type="text" name="username" value="<%= username %>" required autofocus autocomplete="username">
-      </label>
-      <label>
-        <span><%= t('auth.password') %></span>
-        <input type="password" name="password" required autocomplete="current-password">
-      </label>
-      <button type="submit" class="btn btn-primary"><%= t('nav.login') %></button>
-      <p class="auth-link auth-link-muted"><a href="/auth/reset-request"><%= t('auth.forgot') %></a></p>
-    </form>
-  <% } %>
+  <form method="post" action="/auth/login" class="auth-form">
+    <% if (typeof next !== 'undefined' && next) { %>
+      <input type="hidden" name="next" value="<%= next %>">
+    <% } %>
+    <label>
+      <span>Username or email</span>
+      <input type="text" name="username" value="<%= username %>" required autofocus autocomplete="username">
+    </label>
+    <label>
+      <span>Password</span>
+      <input type="password" name="password" required autocomplete="current-password">
+    </label>
+    <button type="submit" class="btn btn-primary">Login</button>
+    <p class="auth-link">No account?
+      <a href="/auth/register<%= (typeof next !== 'undefined' && next) ? '?next=' + encodeURIComponent(next) : '' %>">Register</a>
+    </p>
+    <p class="auth-link auth-link-muted"><a href="/auth/reset-request">Forgot password?</a></p>
+  </form>
 </div>
-
Index: src/views/pages/auth-register.ejs
===================================================================
--- src/views/pages/auth-register.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/auth-register.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,14 +1,4 @@
 <div class="container auth-page">
-  <h1><%= t('setup.title') %></h1>
-  <p class="auth-intro"><%= t('setup.intro') %></p>
-
-  <div class="setup-lang" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:0 0 20px">
-    <span style="opacity:.7;font-size:.9rem"><%= t('setup.lang_label') %>:</span>
-    <% (typeof langs !== 'undefined' ? langs : []).forEach(function(L){ %>
-      <a href="/lang/<%= L.code %>?r=/auth/register"
-         style="padding:5px 13px;border-radius:8px;text-decoration:none;font-weight:600;font-size:.9rem;border:1px solid <%= L.active ? 'var(--accent)' : 'var(--rule, #2b3140)' %>;color:<%= L.active ? 'var(--accent)' : 'inherit' %>"><%= L.name %></a>
-    <% }) %>
-  </div>
-
+  <h1>Register</h1>
   <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
   <form method="post" action="/auth/register" class="auth-form">
@@ -17,27 +7,19 @@
     <% } %>
     <label>
-      <span><%= t('setup.f_sitename') %></span>
-      <input type="text" name="siteName" value="<%= typeof siteName !== 'undefined' ? siteName : '' %>" required autofocus maxlength="80" placeholder="<%= t('setup.sitename_ph') %>">
+      <span>Username (3-32 chars, letters/numbers/_-)</span>
+      <input type="text" name="username" value="<%= username %>" required autofocus autocomplete="username" pattern="[a-zA-Z0-9_-]{3,32}">
     </label>
     <label>
-      <span><%= t('auth.f_username') %></span>
-      <input type="text" id="setup-username" name="username" value="<%= username %>" required autocomplete="username" pattern="[a-zA-Z0-9_-]{3,32}">
-      <small class="setup-handle-hint"><%= t('setup.username_note') %> <code id="setup-handle">@username@…</code></small>
-    </label>
-    <label>
-      <span><%= t('auth.f_email') %></span>
+      <span>Email</span>
       <input type="email" name="email" value="<%= email %>" required autocomplete="email">
     </label>
     <label>
-      <span><%= t('auth.f_password') %></span>
+      <span>Password (min 8 chars)</span>
       <input type="password" name="password" required minlength="8" autocomplete="new-password">
     </label>
-    <button type="submit" class="btn btn-primary"><%= t('setup.submit') %></button>
+    <button type="submit" class="btn btn-primary">Create account</button>
+    <p class="auth-link">Already registered?
+      <a href="/auth/login<%= (typeof next !== 'undefined' && next) ? '?next=' + encodeURIComponent(next) : '' %>">Login</a>
+    </p>
   </form>
 </div>
-
-<style>
-  .setup-handle-hint { display: block; margin-top: .35rem; font-size: .82rem; line-height: 1.4; color: var(--ink-soft, #888); }
-  .setup-handle-hint code { color: var(--accent); font-weight: 600; background: color-mix(in srgb, var(--accent) 12%, transparent); padding: .05rem .35rem; border-radius: 6px; white-space: nowrap; }
-</style>
-<%# Het script van deze pagina staat in assets/js/mod/auth-register.js (shaer-bqr). %>
Index: src/views/pages/auth-reset-request.ejs
===================================================================
--- src/views/pages/auth-reset-request.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/auth-reset-request.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,12 +1,12 @@
 <div class="container auth-page">
-  <h1><%= t('arrq.title') %></h1>
+  <h1>Reset password</h1>
 
   <% if (sent) { %>
     <div class="alert alert-success">
-      <%= t('arrq.sent') %>
+      If an account exists for that email, a reset link has been sent.
     </div>
     <% if (devResetUrl) { %>
       <div class="alert alert-info">
-        <%- t('arrq.no_mailserver') %>
+        <strong>Dev mode:</strong> reset link below (no email server configured).
         <p style="margin: 0.5rem 0 0; word-break: break-all;">
           <a href="<%= devResetUrl %>"><%= devResetUrl %></a>
@@ -14,10 +14,7 @@
       </div>
     <% } %>
-    <% if (typeof mailer !== 'undefined' && !mailer) { %>
-      <p class="auth-link-muted"><%- t('arrq.no_mail_cli') %></p>
-    <% } %>
-    <p class="auth-link"><a href="/auth/login"><%= t('arrq.back_login') %></a></p>
+    <p class="auth-link"><a href="/auth/login">&larr; Back to login</a></p>
   <% } else { %>
-    <p class="auth-tagline"><%= t('arrq.tagline') %></p>
+    <p class="auth-tagline">Enter your email and we'll send a reset link.</p>
 
     <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
@@ -25,9 +22,9 @@
     <form method="post" action="/auth/reset-request" class="auth-form">
       <label>
-        <span><%= t('arrq.email') %></span>
+        <span>Email</span>
         <input type="email" name="email" required autofocus autocomplete="email">
       </label>
-      <button type="submit" class="btn btn-primary"><%= t('arrq.submit') %></button>
-      <p class="auth-link"><a href="/auth/login"><%= t('arrq.back_login') %></a></p>
+      <button type="submit" class="btn btn-primary">Send reset link</button>
+      <p class="auth-link"><a href="/auth/login">&larr; Back to login</a></p>
     </form>
   <% } %>
Index: src/views/pages/auth-reset.ejs
===================================================================
--- src/views/pages/auth-reset.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/auth-reset.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,15 +1,14 @@
 <div class="container auth-page">
-  <h1><%= t('arst.title') %></h1>
+  <h1>Reset password</h1>
 
   <% if (!token) { %>
     <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
     <p class="auth-link">
-      <a href="/auth/reset-request"><%= t('arst.request_new') %></a>
+      <a href="/auth/reset-request">Request a new reset link</a>
       &middot;
-      <a href="/auth/login"><%= t('arst.back_login') %></a>
+      <a href="/auth/login">Back to login</a>
     </p>
   <% } else { %>
-    <p class="auth-tagline"><%- t('arst.set_for', { username: '<strong>' + username + '</strong>' }) %></p>
-    <%# arst.set_for contains {username}; rendered with raw output because username is wrapped in a strong tag %>
+    <p class="auth-tagline">Set a new password for <strong><%= username %></strong>.</p>
 
     <% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
@@ -17,12 +16,12 @@
     <form method="post" action="/auth/reset/<%= token %>" class="auth-form">
       <label>
-        <span><%= t('arst.new_pw') %></span>
+        <span>New password (min 8 chars)</span>
         <input type="password" name="new_password" required minlength="8" autofocus autocomplete="new-password">
       </label>
       <label>
-        <span><%= t('arst.confirm_pw') %></span>
+        <span>Confirm new password</span>
         <input type="password" name="confirm" required minlength="8" autocomplete="new-password">
       </label>
-      <button type="submit" class="btn btn-primary"><%= t('arst.submit') %></button>
+      <button type="submit" class="btn btn-primary">Set password</button>
     </form>
   <% } %>
Index: src/views/pages/authorize-interaction.ejs
===================================================================
--- src/views/pages/authorize-interaction.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,314 +1,0 @@
-<div class="auth-interact">
-  <% if (typeof manage !== 'undefined' && manage) { %>
-    <%- include('../partials/fedi-tabs', { active: 'reacties' }) %>
-    <h1 class="auth-interact-title"><%= t('fedi.manage_title') %></h1>
-
-    <%# Interact bookmarklet — drag to bookmarks bar, then click on any fediverse post. %>
-    <div class="fedi-bm">
-      <a class="fedi-bm-btn" id="fedi-bm-btn" href="#" draggable="true" title="<%= t('fedi.bm_help') %>">
-        <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
-        <%= t('fedi.bm_label') %>
-      </a>
-      <p class="fedi-bm-help"><%= t('fedi.bm_help') %></p>
-    </div>
-    <%# Het script van deze pagina staat in assets/js/mod/authorize-interaction.js (shaer-bqr). %>
-    <% if (!manage.length) { %>
-      <p class="auth-interact-note"><%= t('fedi.manage_empty') %></p>
-    <% } else { %>
-      <ul class="fedi-manage">
-        <% manage.forEach(function(m){ %>
-          <li class="fedi-manage-item">
-            <div class="fedi-manage-head">
-              <span class="fedi-manage-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg></span>
-              <% if (m.to_handle) { %><span class="fedi-manage-to"><%= m.to_handle %></span><% } %>
-              <% if (m.created_at && typeof formatDateTime === 'function') { %><span class="fedi-manage-time"><%= formatDateTime(m.created_at) %></span><% } %>
-            </div>
-            <div class="fedi-manage-content"><%- m.content %></div>
-            <div class="fedi-manage-foot">
-              <button type="button" class="fedi-edit-btn" aria-expanded="false"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg> <%= t('fedi.edit') %></button>
-              <% if (m.in_reply_to) { %>
-              <a class="fedi-goto-btn" href="<%= m.in_reply_to %>" target="_blank" rel="nofollow noopener"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M7 7h10v10"/></svg> <%= t('fedi.goto_post') %></a>
-              <% } %>
-              <form method="post" action="/fediverse/<%= m.id %>/delete" class="fedi-del-form" data-confirm="<%= t('fedi.delete_confirm') %>">
-                <button type="submit" class="fedi-del-btn"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg> <%= t('comments.delete') %></button>
-              </form>
-            </div>
-            <div class="fedi-edit-form">
-              <%- include('../partials/reply-editor', {
-                action: '/fediverse/' + m.id + '/edit',
-                placeholder: t('fedi.reply_ph'),
-                submitLabel: t('fedi.save_edit'),
-                rows: 3,
-                initialHtml: m.content,
-                initialText: m.editable,
-                defaultLang: m.language,
-                noAttach: true,
-              }) %>
-            </div>
-          </li>
-        <% }); %>
-      </ul>
-      
-    <% } %>
-    <p class="auth-interact-note"><a href="/"><%= t('fedi.remote_back') %></a></p>
-  <% } else if (typeof liked !== 'undefined' && liked) { %>
-    <h1 class="auth-interact-title">★ <%= t('fedi.liked_title') %></h1>
-    <p class="auth-interact-note"><%= t('fedi.liked_done') %></p>
-    <p class="auth-interact-actions">
-      <% if (uri) { %><a class="btn" href="<%= uri %>" rel="nofollow noopener"><%= t('fedi.remote_view_original') %></a><% } %>
-      <a class="btn btn-primary" href="/"><%= t('fedi.remote_back') %></a>
-    </p>
-  <% } else if (typeof boosted !== 'undefined' && boosted) { %>
-    <h1 class="auth-interact-title">🔁 <%= t('fedi.boosted_title') %></h1>
-    <p class="auth-interact-note"><%= t('fedi.boosted_done') %></p>
-    <p class="auth-interact-actions">
-      <% if (uri) { %><a class="btn" href="<%= uri %>" rel="nofollow noopener"><%= t('fedi.remote_view_original') %></a><% } %>
-      <a class="btn btn-primary" href="/"><%= t('fedi.remote_back') %></a>
-    </p>
-  <% } else if (typeof followed !== 'undefined' && followed) { %>
-    <h1 class="auth-interact-title"><%= t('fedi.followed_title') %></h1>
-    <p class="auth-interact-note"><%= t('fedi.followed_done') %></p>
-    <p class="auth-interact-actions">
-      <% if (uri) { %><a class="btn" href="<%= uri %>" rel="nofollow noopener"><%= t('fedi.view_profile') %></a><% } %>
-      <a class="btn btn-primary" href="/"><%= t('fedi.remote_back') %></a>
-    </p>
-  <% } else if (typeof sent !== 'undefined' && sent) { %>
-    <h1 class="auth-interact-title"><%= t('fedi.remote_sent_title') %></h1>
-    <p class="auth-interact-note"><%= t('fedi.remote_sent') %></p>
-    <p class="auth-interact-actions">
-      <% if (uri) { %><a class="btn btn-primary" href="<%= uri %>" rel="nofollow noopener"><%= t('fedi.remote_view_original') %></a><% } %>
-      <a class="btn" href="/"><%= t('fedi.remote_back') %></a>
-    </p>
-  <% } else if (typeof reported !== 'undefined' && reported) { %>
-    <h1 class="auth-interact-title">🚩 <%= t('fedi.reported_title') %></h1>
-    <p class="auth-interact-note"><%= t('fedi.reported_done') %></p>
-    <p class="auth-interact-actions">
-      <a class="btn" href="/"><%= t('fedi.remote_back') %></a>
-    </p>
-  <% } else if (typeof voted !== 'undefined' && voted) { %>
-    <h1 class="auth-interact-title">📊 <%= t('poll.voted_title') %></h1>
-    <p class="auth-interact-note"><%= t('poll.voted_done') %></p>
-    <p class="auth-interact-actions">
-      <% if (uri) { %><a class="btn btn-primary" href="<%= uri %>" rel="nofollow noopener"><%= t('fedi.remote_view_original') %></a><% } %>
-      <a class="btn" href="/"><%= t('fedi.remote_back') %></a>
-    </p>
-  <% } else if (typeof followTarget !== 'undefined' && followTarget) { %>
-  <h1 class="auth-interact-title"><%= t('fedi.follow_heading') %></h1>
-  <p class="auth-interact-meta">
-    <%= t('fedi.follow_intro') %>
-    <a href="<%= followTarget.actor_url %>" rel="nofollow noopener" target="_blank"><strong><%= followTarget.actor_name %></strong></a>
-    <span class="fedi-handle"><%= followTarget.actor_handle %></span>
-  </p>
-  <form method="post" action="/authorize_interaction/follow" class="auth-interact-form">
-    <input type="hidden" name="uri" value="<%= followTarget.actor_uri %>">
-    <div class="auth-interact-actions">
-      <button type="submit" class="btn btn-primary"><%= t('fedi.follow_btn') %></button>
-      <a href="/" class="btn"><%= t('comments.cancel') %></a>
-    </div>
-  </form>
-  <p class="auth-interact-note"><%= t('fedi.remote_as', { site: siteTitle }) %></p>
-  <% } else { %>
-  <h1 class="auth-interact-title"><%= t('fedi.remote_title') %></h1>
-
-  <% if (!target) { %>
-    <p class="auth-interact-note"><%= t('fedi.remote_notfound') %></p>
-    <form method="get" action="/authorize_interaction" class="auth-interact-form">
-      <input type="url" name="uri" placeholder="https://…/een-post" value="<%= uri || '' %>" required>
-      <button type="submit" class="btn btn-primary"><%= t('fedi.remote_load') %></button>
-    </form>
-  <% } else { %>
-    <p class="auth-interact-meta">
-      <%= t('fedi.remote_replying_to') %>
-      <a href="<%= target.actor_url %>" rel="nofollow noopener" target="_blank"><strong><%= target.actor_name %></strong></a>
-      <span class="fedi-handle"><%= target.actor_handle %></span>
-    </p>
-    <blockquote class="auth-interact-preview">
-      <% if (target.content) { %><div class="auth-interact-content"><%- target.content %></div>
-      <% } else if (target.preview) { %><%= target.preview %><% } %>
-      <% (target.images || []).forEach(function(im){ %>
-        <img class="auth-interact-img" src="<%= im %>" alt="" loading="lazy">
-      <% }); %>
-    </blockquote>
-    <% if (target.url || uri) { %>
-      <a class="auth-view-src" href="<%= target.url || uri %>" target="_blank" rel="nofollow noopener">
-        <svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M7 7h10v10"/></svg>
-        <span><%= t('fedi.remote_view_original') %></span>
-      </a>
-    <% } %>
-    <% if (target.poll) { var _pl = target.poll; var _tot = _pl.options.reduce(function(s,o){return s+(o.count||0);},0); var _vn = (_pl.voters!=null?_pl.voters:_tot); %>
-      <% if (!_pl.closed) { %>
-        <form method="post" action="/authorize_interaction/vote" class="auth-poll">
-          <input type="hidden" name="uri" value="<%= uri %>">
-          <% _pl.options.forEach(function(o){ %>
-            <label class="auth-poll-choice"><input type="<%= _pl.multiple ? 'checkbox' : 'radio' %>" name="choice" value="<%= o.name %>"><span><%= o.name %></span></label>
-          <% }); %>
-          <div class="auth-interact-actions">
-            <button type="submit" class="btn btn-primary"><%= t('poll.vote') %></button>
-          </div>
-          <p class="auth-poll-foot"><%= _vn %> <%= _vn === 1 ? t('poll.voter_one') : t('poll.voter_many') %> · <%= t('poll.open') %><% if (_pl.endTime) { %> · <%= t('poll.closes') %> <%= new Date(_pl.endTime).toLocaleString() %><% } %><% if (_pl.multiple) { %> · <%= t('poll.multiple') %><% } %></p>
-        </form>
-      <% } else { %>
-        <div class="auth-poll">
-          <% _pl.options.forEach(function(o){ var _pct = _tot ? Math.round((o.count||0)*100/_tot) : 0; %>
-            <div class="auth-poll-res"><span class="auth-poll-fill" style="width:<%= _pct %>%"></span><span class="auth-poll-name"><%= o.name %></span><span class="auth-poll-pct"><%= _pct %>%</span></div>
-          <% }); %>
-          <p class="auth-poll-foot"><%= _vn %> <%= _vn === 1 ? t('poll.voter_one') : t('poll.voter_many') %> · <%= t('poll.closed') %><% if (_pl.multiple) { %> · <%= t('poll.multiple') %><% } %></p>
-        </div>
-      <% } %>
-    <% } %>
-    <% var _liked = (typeof reacted !== 'undefined' && reacted.liked); var _boosted = (typeof reacted !== 'undefined' && reacted.boosted); %>
-    <div class="auth-interact-react">
-      <form method="post" action="/authorize_interaction/like" class="fedi-react-form">
-        <input type="hidden" name="uri" value="<%= uri %>">
-        <button type="submit" class="fedi-bigact fedi-bigact-like<%= _liked ? ' is-on' : '' %>" data-on="<%= t('fedi.unlike_short') %>" data-off="<%= t('fedi.like_short') %>">
-          <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.96l-5.8 3.06 1.1-6.46-4.69-4.58 6.49-.95z"/></svg>
-          <span class="fedi-bigact-label"><%= _liked ? t('fedi.unlike_short') : t('fedi.like_short') %></span>
-        </button>
-      </form>
-      <form method="post" action="/authorize_interaction/boost" class="fedi-react-form">
-        <input type="hidden" name="uri" value="<%= uri %>">
-        <button type="submit" class="fedi-bigact fedi-bigact-boost<%= _boosted ? ' is-on' : '' %>" data-on="<%= t('tl.unboost') %>" data-off="<%= t('fedi.boost_short') %>">
-          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
-          <span class="fedi-bigact-label"><%= _boosted ? t('tl.unboost') : t('fedi.boost_short') %></span>
-        </button>
-      </form>
-    </div>
-    <% if (locals.mayReply !== false) { %>
-    <p class="auth-interact-or"><%= t('fedi.or_reply') %></p>
-    <p class="auth-interact-where">💬 <%= t('fedi.reply_where') %></p>
-    <div class="auth-interact-form">
-      <%- include('../partials/reply-editor', {
-        action: '/authorize_interaction',
-        hiddenFields: [{ name: 'uri', value: uri }],
-        placeholder: t('fedi.reply_ph'),
-        submitLabel: t('fedi.send'),
-        rows: 4,
-        participants: (target && target.actor_uri) ? [{
-          uri: target.actor_uri,
-          url: target.actor_url || target.actor_uri,
-          handle: target.actor_handle || '',
-        }] : [],
-      }) %>
-      <div class="auth-interact-actions"><a href="<%= uri %>" class="btn"><%= t('comments.cancel') %></a></div>
-    </div>
-    <% } %>
-    <p class="auth-interact-note"><%= t('fedi.remote_as', { site: siteTitle }) %></p>
-    <details class="auth-report">
-      <summary>🚩 <%= t('fedi.report_open') %></summary>
-      <form method="post" action="/authorize_interaction/report" class="auth-interact-form">
-        <input type="hidden" name="uri" value="<%= uri %>">
-        <input type="hidden" name="actor_uri" value="<%= target.actor_uri %>">
-        <p class="auth-report-note"><%= t('fedi.report_where') %></p>
-        <textarea name="reason" rows="3" maxlength="3000" placeholder="<%= t('fedi.report_ph') %>"></textarea>
-        <div class="auth-interact-actions">
-          <button type="submit" class="btn btn-danger"><%= t('fedi.report_send') %></button>
-        </div>
-      </form>
-    </details>
-  <% } %>
-  <% } %>
-</div>
-
-<style>
-  .auth-interact { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
-  .auth-interact-title { margin: 0 0 1rem; }
-  .auth-interact-meta { color: var(--ink-soft, #888); margin: 0 0 .75rem; }
-  .auth-interact-preview {
-    margin: 0 0 1rem; padding: .75rem 1rem; border-left: 3px solid var(--accent, #888);
-    background: color-mix(in srgb, var(--ink, #000) 4%, transparent); border-radius: 0 12px 12px 0;
-    color: var(--ink, inherit);
-  }
-  .auth-interact-form { display: flex; flex-direction: column; gap: .75rem; }
-  .auth-interact-form textarea, .auth-interact-form input[type=url] {
-    width: 100%; box-sizing: border-box; padding: .6rem .8rem; font: inherit; resize: vertical;
-    border-radius: 12px; border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent);
-    background: var(--paper, #fff); color: var(--ink, #000);
-  }
-  .auth-interact-actions { display: flex; gap: .5rem; align-items: center; }
-  .auth-interact-note { color: var(--ink-soft, #999); font-size: .85rem; margin-top: 1rem; }
-  .auth-interact-where { margin: .25rem 0 .85rem; padding: .6rem .8rem; font-size: .85rem; line-height: 1.5; border-radius: 10px; background: color-mix(in srgb, var(--accent, #888) 12%, transparent); color: var(--ink, inherit); }
-  .auth-interact-content { line-height: 1.55; }
-  .auth-interact-content p { margin: .35rem 0; }
-  .auth-interact-content p:first-child { margin-top: 0; }
-  .auth-interact-img { max-width: 100%; height: auto; border-radius: 10px; margin: .5rem 0 0; display: block; }
-  .auth-interact-orig { margin: .75rem 0 0; font-size: .85rem; }
-  .auth-interact-orig a { color: var(--accent, #06c); }
-  .auth-view-src { display: inline-flex; align-items: center; gap: .5rem; margin: 0 0 1.1rem; padding: .6rem 1rem;
-    border: 1px solid color-mix(in srgb, var(--accent, #888) 45%, transparent); border-radius: 12px;
-    background: color-mix(in srgb, var(--accent, #888) 10%, transparent); color: var(--accent, #06c);
-    text-decoration: none; font-weight: 600; font-size: .92rem; transition: background .15s, border-color .15s; }
-  .auth-view-src:hover { background: color-mix(in srgb, var(--accent, #888) 18%, transparent); border-color: var(--accent, #888); }
-  .auth-interact-react { display: flex; gap: .6rem; flex-wrap: wrap; margin: 0 0 .25rem; }
-  .auth-interact-react form { margin: 0; }
-  .fedi-bigact { display: inline-flex; align-items: center; gap: .5rem; padding: .55rem 1.1rem; border-radius: 999px;
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent);
-    background: color-mix(in srgb, var(--ink, #000) 4%, transparent);
-    color: var(--ink, inherit); font: inherit; font-weight: 600; font-size: .92rem; cursor: pointer; white-space: nowrap;
-    transition: background .12s, border-color .12s; }
-  .fedi-bigact svg { width: 17px; height: 17px; flex: 0 0 17px; }
-  .fedi-bigact-like svg { color: #e8b04b; }
-  .fedi-bigact-like:hover, .fedi-bigact-like.is-on { background: color-mix(in srgb, #e8b04b 16%, transparent); border-color: color-mix(in srgb, #e8b04b 55%, transparent); }
-  .fedi-bigact-boost svg { color: #2fa85a; }
-  .fedi-bigact-boost:hover, .fedi-bigact-boost.is-on { background: color-mix(in srgb, #2fa85a 16%, transparent); border-color: color-mix(in srgb, #2fa85a 55%, transparent); }
-  .fedi-manage { list-style: none; padding: 0; margin: 1.25rem 0 1rem; display: flex; flex-direction: column; gap: .7rem; }
-  .fedi-manage-item { padding: .9rem 1rem; border-radius: 14px; background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent); border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent); transition: border-color .15s ease, box-shadow .15s ease; }
-  .fedi-manage-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 28%, transparent); box-shadow: 0 2px 12px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
-  .fedi-manage-head { display: flex; align-items: center; gap: .5rem; margin: 0 0 .5rem; }
-  .fedi-manage-icon { flex: 0 0 26px; width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
-    background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); }
-  .fedi-manage-icon svg { width: 14px; height: 14px; }
-  .fedi-manage-to { font-weight: 600; color: var(--ink, inherit); font-size: .9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
-  .fedi-manage-time { color: var(--ink-soft, #999); font-size: .76rem; margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
-  .fedi-manage-content { line-height: 1.55; overflow-wrap: anywhere; padding-left: calc(26px + .5rem); }
-  .fedi-manage-content p { margin: .15rem 0; }
-  .fedi-manage-content p:first-child { margin-top: 0; }
-  .fedi-manage-content p:last-child { margin-bottom: 0; }
-  .fedi-manage-foot { display: flex; align-items: center; justify-content: flex-start; flex-wrap: wrap; gap: .5rem; margin-top: .65rem; }
-  .fedi-goto-btn { display: inline-flex; align-items: center; gap: .35rem; border: 1px solid color-mix(in srgb, var(--accent, #888) 45%, transparent); background: color-mix(in srgb, var(--accent, #888) 12%, transparent); color: var(--accent, #06c); border-radius: 999px; padding: .3rem .8rem; font-size: .82rem; text-decoration: none; transition: background .15s ease; }
-  .fedi-goto-btn:hover { background: color-mix(in srgb, var(--accent, #888) 22%, transparent); }
-  .fedi-del-btn { display: inline-flex; align-items: center; gap: .35rem; border: 1px solid color-mix(in srgb, #d9534f 45%, transparent); background: color-mix(in srgb, #d9534f 12%, transparent); color: #e06b66; border-radius: 999px; padding: .3rem .8rem; font: inherit; font-size: .82rem; cursor: pointer; transition: background .15s ease; }
-  .fedi-del-btn:hover { background: color-mix(in srgb, #d9534f 24%, transparent); }
-  .fedi-bm { margin: 0 0 1.5rem; padding: 1rem; border-radius: 14px;
-    border: 1px dashed color-mix(in srgb, var(--accent, #888) 45%, transparent);
-    background: color-mix(in srgb, var(--accent, #888) 7%, transparent); }
-  .fedi-bm-btn { display: inline-flex; align-items: center; gap: .45rem; padding: .5rem 1rem; border-radius: 999px;
-    background: var(--accent, #06c); color: #fff; font: inherit; font-weight: 600; font-size: .9rem; text-decoration: none;
-    cursor: grab; user-select: none; box-shadow: 0 2px 8px color-mix(in srgb, var(--accent, #000) 30%, transparent); }
-  .fedi-bm-btn:active { cursor: grabbing; }
-  .fedi-bm-btn.nudge { animation: fedi-bm-nudge .6s ease; }
-  @keyframes fedi-bm-nudge { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } }
-  .fedi-bm-help { color: var(--ink-soft, #888); font-size: .82rem; line-height: 1.5; margin: .6rem 0 0; }
-  .fedi-edit-btn { display: inline-flex; align-items: center; gap: .35rem; border: 1px solid color-mix(in srgb, var(--ink, #888) 28%, transparent);
-    background: color-mix(in srgb, var(--ink, #888) 8%, transparent); color: var(--ink-soft, #aaa); border-radius: 999px; padding: .3rem .8rem;
-    font: inherit; font-size: .82rem; cursor: pointer; transition: background .15s ease, color .15s ease; }
-  .fedi-edit-btn:hover { background: color-mix(in srgb, var(--ink, #888) 16%, transparent); }
-  .fedi-edit-btn.is-open { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
-  .fedi-del-form { margin-left: auto; }
-  .fedi-edit-form { display: none; margin: .55rem 0 0; }
-  .fedi-edit-form.is-open { display: block; }
-  .fedi-edit-ta { width: 100%; box-sizing: border-box; min-height: 70px; resize: vertical; padding: .55rem .7rem;
-    border: 1.5px solid var(--rule, #333); border-radius: 10px; background: var(--paper-2, #14141a); color: var(--ink, #eee); font: inherit; font-size: .9rem; }
-  .fedi-edit-ta:focus { outline: none; border-color: var(--accent); }
-  .fedi-edit-actions { display: flex; justify-content: flex-end; margin-top: .5rem; }
-  .fedi-edit-save { padding: .4rem 1rem; border: 0; border-radius: 999px; background: var(--accent); color: var(--paper, #fff);
-    font: inherit; font-weight: 600; font-size: .82rem; cursor: pointer; }
-  /* Poll ballot / results on the interact page */
-  .auth-poll { display: flex; flex-direction: column; gap: 8px; margin: 0 0 1rem; }
-  .auth-poll-choice { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; cursor: pointer;
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent); }
-  .auth-poll-choice:hover { border-color: var(--accent); }
-  .auth-poll-choice input { accent-color: var(--accent); }
-  .auth-poll-res { position: relative; padding: 9px 12px; border-radius: 10px; overflow: hidden; display: flex; align-items: center; gap: 8px;
-    background: color-mix(in srgb, var(--ink, #000) 6%, transparent); }
-  .auth-poll-fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent) 22%, transparent); z-index: 0; }
-  .auth-poll-name { position: relative; z-index: 1; flex: 1; font-size: .95rem; }
-  .auth-poll-pct { position: relative; z-index: 1; font-variant-numeric: tabular-nums; font-weight: 500; }
-  .auth-poll-foot { font-size: .82rem; color: var(--ink-soft, #888); margin: .1rem 0 0; }
-  .auth-report { margin: .6rem 0 0; }
-  .auth-report summary { cursor: pointer; font-size: .85rem; color: var(--ink-soft, #888); }
-  .auth-report summary:hover, .auth-report[open] summary { color: #c0392b; }
-  .auth-report[open] summary { margin-bottom: .5rem; }
-  .auth-report-note { font-size: .82rem; color: var(--ink-muted, #999); margin: 0 0 .5rem; }
-</style>
-
-
Index: src/views/pages/blocks.ejs
===================================================================
--- src/views/pages/blocks.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,42 +1,0 @@
-<div class="bl-wrap">
-  <%- include('../partials/fedi-tabs', { active: 'blokkeren' }) %>
-  <h1 class="bl-title"><%= t('blk.title') %></h1>
-  <p class="bl-lead"><%= t('blk.lead') %></p>
-  <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
-  <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
-
-  <form method="post" action="/blocking/add" class="bl-form">
-    <input type="text" name="target" placeholder="@naam@server.social  •  server.social" autocomplete="off" spellcheck="false" required>
-    <button type="submit" class="btn btn-primary"><%= t('blk.block_btn') %></button>
-  </form>
-
-  <% if (!blocks || !blocks.length) { %>
-    <p class="bl-empty"><%= t('blk.empty') %></p>
-  <% } else { %>
-    <ul class="bl-list">
-      <% blocks.forEach(function(b){ %>
-        <li class="bl-item">
-          <span class="bl-kind"><%= b.kind === 'domain' ? '🌐' : '👤' %></span>
-          <span class="bl-label"><%= b.label || b.target %></span>
-          <form method="post" action="/blocking/remove"><input type="hidden" name="target" value="<%= b.target %>"><button type="submit" class="bl-unblock"><%= t('blk.unblock') %></button></form>
-        </li>
-      <% }); %>
-    </ul>
-  <% } %>
-</div>
-
-<style>
-  .bl-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
-  .bl-title { margin: 0 0 .25rem; }
-  .bl-lead { color: var(--ink-soft, #888); margin: 0 0 1.25rem; }
-  .bl-form { display: flex; gap: .5rem; margin: 0 0 1.25rem; flex-wrap: wrap; }
-  .bl-form input { flex: 1; min-width: 220px; height: 2.4rem; padding: 0 .9rem; border-radius: 999px; font: inherit;
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, #000); }
-  .bl-empty { color: var(--ink-soft, #888); }
-  .bl-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .5rem; }
-  .bl-item { display: flex; align-items: center; gap: .6rem; padding: .55rem .8rem; border-radius: 12px; background: color-mix(in srgb, var(--ink, #000) 4%, transparent); border: 1px solid color-mix(in srgb, var(--ink, #000) 8%, transparent); }
-  .bl-kind { font-size: 1.05rem; }
-  .bl-label { flex: 1; min-width: 0; overflow-wrap: anywhere; }
-  .bl-unblock { background: none; border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); border-radius: 8px; padding: .25rem .6rem; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; }
-  .bl-unblock:hover { color: var(--ink, #000); }
-</style>
Index: src/views/pages/changelog.ejs
===================================================================
--- src/views/pages/changelog.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,38 +1,0 @@
-<div class="container changelog-page">
-  <p><a href="<%= (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '' %>/" class="btn">&larr; <%= t('chlog.back') %></a></p>
-  <h1><%= t('chlog.title') %></h1>
-  <p class="cl-meta">
-    <%= t('chlog.app_version') %> <strong>v<%= appVersion %></strong>
-    <% if (typeof user !== 'undefined' && user && user.role === 'god') { %>
-      <span class="cl-sep">·</span> <a href="/admin/updates"><%= t('chlog.manage_updates') %></a>
-    <% } %>
-  </p>
-  <div class="changelog-body"><%- changelogHtml %></div>
-</div>
-
-<style>
-.changelog-page { max-width: 760px; margin: 3rem auto; padding: 0 1rem; }
-.changelog-page h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: 0 0 0.4rem; }
-.cl-meta { color: var(--ink-muted); font-size: 0.9rem; margin: 0 0 1.75rem; }
-.cl-meta a { color: var(--accent); }
-.cl-sep { opacity: 0.5; margin: 0 0.2rem; }
-.changelog-body { color: var(--ink); line-height: 1.65; }
-.changelog-body h2 {
-  font-family: var(--font-display, serif); font-size: 1.3rem;
-  margin: 2rem 0 0.5rem; padding-top: 1.25rem; border-top: 1px solid var(--rule);
-}
-.changelog-body h2:first-of-type { border-top: none; padding-top: 0; margin-top: 1rem; }
-.changelog-body h3 { font-size: 1rem; margin: 1.1rem 0 0.4rem; color: var(--ink-soft, var(--ink-muted)); }
-.changelog-body ul { margin: 0.3rem 0 0.9rem; padding-left: 1.3rem; }
-.changelog-body li { margin: 0.25rem 0; }
-.changelog-body blockquote {
-  margin: 1rem 0; padding: 0.6rem 0.9rem;
-  border-left: 3px solid var(--accent); background: var(--paper-2);
-  border-radius: 0 8px 8px 0; font-size: 0.9rem; color: var(--ink-muted);
-}
-.changelog-body code {
-  background: var(--paper-2); padding: 0.05rem 0.35rem;
-  border-radius: 4px; font-size: 0.88em;
-}
-.changelog-body a { color: var(--accent); }
-</style>
Index: src/views/pages/circle-feed.ejs
===================================================================
--- src/views/pages/circle-feed.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,135 +1,0 @@
-<div class="container cirkel-hero">
-  <span class="cirkel-badge" aria-hidden="true">
-    <svg viewBox="0 0 100 100" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
-      <defs>
-        <marker id="cf-a" viewBox="0 0 10 10" refX="6.5" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M1.5 1.5 L8.5 5 L1.5 8.5 Z" fill="currentColor" stroke="none"/></marker>
-      </defs>
-      <line x1="28.95" y1="43.5" x2="63.05" y2="26.5" stroke-width="2.6" marker-start="url(#cf-a)" marker-end="url(#cf-a)"/>
-      <line x1="72" y1="32" x2="72" y2="66" stroke-width="2.6" marker-end="url(#cf-a)"/>
-      <line x1="28.8" y1="52.7" x2="63.2" y2="71.3" stroke-width="2.6" marker-end="url(#cf-a)"/>
-      <circle cx="20" cy="48" r="7" stroke-width="2.4"/>
-      <circle cx="72" cy="22" r="7" stroke-width="2.4"/>
-      <circle cx="72" cy="76" r="7" stroke-width="2.4"/>
-    </svg>
-  </span>
-  <h1><%= t('cfeed.title') %></h1>
-  <p class="cirkel-sub"><%= t('cfeed.sub') %></p>
-  <% if (typeof sites !== 'undefined' && sites && sites.length) { %>
-    <% if (sites.length <= 5) { %>
-    <div class="cirkel-members">
-      <% sites.forEach(function(s){ %>
-        <% if (s.url) { %><a class="cirkel-member" href="<%= s.url %>" target="_blank" rel="noopener" title="<%= s.name %>"><% } else { %><span class="cirkel-member" title="<%= s.name %>"><% } %>
-          <span class="cirkel-avatar"<% if (s.avatar) { %> style="background-image:url('<%= s.avatar %>');background-color:transparent"<% } %>><% if (!s.avatar) { %><%= (s.name || '?').charAt(0).toUpperCase() %><% } %></span>
-          <span class="cirkel-member-name"><%= s.name %></span>
-        <% if (s.url) { %></a><% } else { %></span><% } %>
-      <% }); %>
-    </div>
-    <% } else { %>
-      <%# More than 5: the count pill becomes a button that opens a modal listing every
-          member (avatar + name + link). CSS-only via a hidden checkbox — no JS, so it's
-          CSP- (no inline handler) and htmx-safe. Checkbox must precede .cirkel-modal so
-          the `:checked ~` sibling selector matches. %>
-      <input type="checkbox" id="cirkel-more-cb" class="cirkel-more-cb" hidden>
-      <label for="cirkel-more-cb" class="cirkel-count cirkel-count-btn" role="button" tabindex="0"><%= t('cfeed.count', { n: sites.length }) %></label>
-      <div class="cirkel-modal" role="dialog" aria-modal="true" aria-label="<%= t('cfeed.title') %>">
-        <label for="cirkel-more-cb" class="cirkel-modal-backdrop" aria-label="<%= t('cfeed.close') %>"></label>
-        <div class="cirkel-modal-card">
-          <label for="cirkel-more-cb" class="cirkel-modal-x" aria-label="<%= t('cfeed.close') %>" role="button" tabindex="0">&times;</label>
-          <h2 class="cirkel-modal-h"><%= t('cfeed.count', { n: sites.length }) %></h2>
-          <ul class="cirkel-modal-list">
-            <% sites.forEach(function(s){ %>
-              <li>
-                <% if (s.url) { %><a class="cirkel-modal-link" href="<%= s.url %>" target="_blank" rel="noopener"><% } else { %><span class="cirkel-modal-link"><% } %>
-                  <span class="cirkel-avatar"<% if (s.avatar) { %> style="background-image:url('<%= s.avatar %>');background-color:transparent"<% } %>><% if (!s.avatar) { %><%= (s.name || '?').charAt(0).toUpperCase() %><% } %></span>
-                  <span class="cirkel-member-name"><%= s.name %></span>
-                  <% if (s.url) { %><span class="cirkel-modal-go" aria-hidden="true">↗</span><% } %>
-                <% if (s.url) { %></a><% } else { %></span><% } %>
-              </li>
-            <% }); %>
-          </ul>
-        </div>
-      </div>
-    <% } %>
-  <% } %>
-</div>
-
-<% if (!posts.length) { %>
-  <div class="container">
-    <p class="cirkel-empty"><%= t('cfeed.empty') %></p>
-  </div>
-<% } else { %>
-
-  <%# Same view as the home page: timeline (post-card) + grid (post-tile),
-      switched by the view-switcher via body[data-feed-view]. The remote posts
-      are shaped to the local post format in circle.js (with external_url,
-      so cards link to the source site). %>
-
-  <!-- ===== TIMELINE (standaard) ===== -->
-  <div class="container feed-timeline">
-    <ul class="post-list feed-timeline-list" id="post-list">
-      <% posts.forEach(function(post){ %>
-        <%- include('../partials/post-card', { post: post }) %>
-      <% }); %>
-    </ul>
-  </div>
-
-  <!-- ===== GRID (body[data-feed-view="grid"]) ===== -->
-  <section class="feed-grid container" aria-label="<%= t('cfeed.grid_view') %>">
-    <div class="grid-tiles" id="grid-tiles">
-      <% posts.forEach(function(post){ %>
-        <%- include('../partials/post-tile', { post: post }) %>
-      <% }); %>
-    </div>
-  </section>
-
-  <% if (typeof hasMore !== 'undefined') { %>
-  <div class="container feed-more-wrap">
-    <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#post-list', morePath: '/cirkel' }) %>
-  </div>
-  <% } %>
-
-<% } %>
-
-<style>
-.cirkel-hero { text-align: center; max-width: 640px; margin: 2.5rem auto 2rem; }
-.cirkel-badge { display: block; width: 100px; height: 100px; margin: 0 auto .7rem; color: var(--accent);
-  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent) 65%, transparent)) drop-shadow(0 0 1px var(--accent)); }
-.cirkel-badge svg { width: 100%; height: 100%; }
-.cirkel-hero h1 { font-family: var(--font-display, serif); font-size: 2.4rem; margin: 0 0 .3rem; }
-.cirkel-sub { color: var(--ink-muted); margin: 0 0 1.25rem; font-size: 1.02rem; }
-.cirkel-members { display: flex; flex-wrap: wrap; gap: .55rem; justify-content: center; }
-.cirkel-member { display: inline-flex; align-items: center; gap: .45rem;
-  padding: .28rem .8rem .28rem .3rem; border: 1px solid var(--rule); border-radius: 999px;
-  text-decoration: none; color: var(--ink); background: var(--paper-2);
-  transition: border-color .15s, transform .15s; }
-.cirkel-member:hover { border-color: var(--accent); transform: translateY(-1px); }
-.cirkel-avatar { width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0;
-  background-size: cover; background-position: center; background-color: var(--accent);
-  color: #fff; display: inline-flex; align-items: center; justify-content: center;
-  font-weight: 700; font-size: .82rem; }
-.cirkel-member-name { font-size: .9rem; font-weight: 600; }
-.cirkel-count { display: inline-block; color: var(--ink-muted); font-size: .92rem; font-weight: 600;
-  padding: .35rem 1rem; border: 1px solid var(--rule); border-radius: 999px; background: var(--paper-2); }
-.cirkel-count-btn { cursor: pointer; user-select: none; transition: border-color .15s, color .15s, transform .15s; }
-.cirkel-count-btn:hover { border-color: var(--accent); color: var(--ink); transform: translateY(-1px); }
-.cirkel-empty { color: var(--ink-muted); text-align: center; margin: 2rem auto; }
-.cirkel-empty a { color: var(--accent); }
-/* CSS-only "more members" modal (toggled by the hidden checkbox, no JS) */
-.cirkel-more-cb { position: absolute; width: 0; height: 0; opacity: 0; pointer-events: none; }
-.cirkel-modal { display: none; position: fixed; inset: 0; z-index: 1000; align-items: center; justify-content: center; padding: 1rem; }
-.cirkel-more-cb:checked ~ .cirkel-modal { display: flex; }
-.cirkel-modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,.45); cursor: pointer; }
-.cirkel-modal-card { position: relative; background: var(--paper); color: var(--ink); border-radius: 18px;
-  padding: 1.4rem 1.2rem; width: min(420px, 92vw); max-height: 78vh; overflow-y: auto; text-align: left;
-  box-shadow: 0 20px 60px rgba(0,0,0,.32); }
-.cirkel-modal-x { position: absolute; top: .55rem; right: .65rem; width: 34px; height: 34px; border-radius: 50%;
-  display: inline-flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.45rem;
-  line-height: 1; color: var(--ink-muted); background: var(--paper-2); }
-.cirkel-modal-x:hover { color: var(--ink); }
-.cirkel-modal-h { font-family: var(--font-display, serif); font-size: 1.3rem; margin: .1rem 0 1rem; text-align: center; }
-.cirkel-modal-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .25rem; }
-.cirkel-modal-link { display: flex; align-items: center; gap: .6rem; padding: .45rem .5rem; border-radius: 12px;
-  text-decoration: none; color: var(--ink); transition: background .15s; }
-.cirkel-modal-link:hover { background: var(--paper-2); }
-.cirkel-modal-go { margin-left: auto; color: var(--accent); font-weight: 700; }
-</style>
Index: src/views/pages/connect.ejs
===================================================================
--- src/views/pages/connect.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,198 +1,0 @@
-<div class="tl-wrap">
-  <%- include('../partials/fedi-tabs', { active: 'connect' }) %>
-  <h1 class="tl-title"><%= t('tl.connect') %></h1>
-  <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
-  <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
-
-  <%# FEP-633c §2 + §3.6: who guards this account, and how available each of
-      them is. Connect is the "who am I connected to" page, and a guardian is
-      the one connection a ward should never have to hunt for. Availability in
-      buddy-list language on the responsibility axis; the labels are UI, the
-      wire keeps the spec terms. Never public: this page is the owner's. %>
-  <% if (typeof myGuardians !== 'undefined' && myGuardians.length) { %>
-    <section class="conn-guardians">
-      <h2 class="conn-gtitle">&#128737; <%= t('msg.guardians_label') %></h2>
-      <ul class="conn-glist">
-        <% myGuardians.forEach(function(g){
-             var _st = g.availability === 'away' ? 'away' : (g.availability === 'dormant' ? 'dormant' : 'active');
-             var _lbl = _st === 'away' ? t('msg.g_away', { date: g.awayUntil ? formatDate(g.awayUntil) : '?' })
-                      : (_st === 'dormant' ? t('msg.g_offline') : t('msg.g_available')); %>
-          <li class="conn-guardian">
-            <span class="conn-gdot conn-gdot--<%= _st %>" aria-hidden="true"></span>
-            <span class="conn-ghandle"><%= g.handle %></span>
-            <span class="conn-gstate"><%= _lbl %></span>
-          </li>
-        <% }); %>
-      </ul>
-    </section>
-  <% } %>
-
-  <% var _moved = (typeof movedTo !== 'undefined' && movedTo) ? movedTo : null; %>
-  <% if (_moved) { %>
-    <%# De uitgaande kant staat op slot (FEP-7628). Dat hoort te blijken voordat
-        je op een knop drukt, niet daarna uit een foutmelding. Lezen blijft wel
-        gewoon werken, dus de lijst hieronder staat er nog. %>
-    <div class="alert alert-error tl-moved">
-      <strong><%= t('tl.moved_title') %></strong>
-      <p><%= t('tl.moved_lead') %> <a href="<%= _moved %>"><%= _moved %></a></p>
-      <p class="form-hint"><%= t('tl.moved_hint') %></p>
-    </div>
-  <% } else { %>
-  <form method="post" action="/news/follow" class="tl-follow">
-    <input type="text" name="handle" placeholder="@naam@server.social of site.com" autocomplete="off" spellcheck="false" required>
-    <button type="submit" class="btn btn-primary"><%= t('tl.follow_btn') %></button>
-    <label class="tl-follow-ab" title="<%= t('tl.autoboost_hint') %>"><input type="checkbox" name="auto_boost" value="1"> &#10024; <%= t('tl.autoboost_follow') %></label>
-  </form>
-  <% } %>
-
-  <%# Je volglijst meenemen staat sinds 14-8 op /admin/migrate, bij de rest van
-      de verhuizing (Robin: alle migratie-opties op de migrate-pagina). Hier
-      stond hij bovendien zonder enige opmaak: labels als losse tekst naast hun
-      invoerveld. Op die pagina erft hij de kaartopmaak. %>
-
-  <%# De eigenaarspoort (Robins wens, 18-8): volgverzoeken die wachten op
-      een beslissing van de eigenaar. Alleen buiten voogdij — een ward ziet
-      dit nooit, want daar beslissen de guardians. De poort zelf zit hier
-      ook: aan/uit op de plek waar de verzoeken staan. %>
-  <% if (!(myGuardians && myGuardians.length)) { %>
-    <form method="post" action="/connect/approve-followers" class="tl-foll-ab" title="<%= t('tl.approve_toggle_hint') %>">
-      <label><input type="checkbox" name="on" value="1" <%= (typeof approveFollowers !== 'undefined' && approveFollowers) ? 'checked' : '' %> data-autosubmit> &#128737; <%= t('tl.approve_toggle') %></label>
-    </form>
-  <% } %>
-  <% var folreqs = (typeof followRequests !== 'undefined' && followRequests) || []; %>
-  <% if (folreqs.length) { %>
-    <h2 class="conn-h"><%= t('tl.folreq_title') %></h2>
-    <p class="conn-unreach-lead"><%= t('tl.folreq_sub') %></p>
-    <ul class="tl-foll-list">
-      <% folreqs.forEach(function (f) { var disp = f.follower_name || f.follower_handle || f.follower_uri; %>
-        <li class="tl-foll">
-          <span class="tl-foll-av"><% if (f.follower_icon) { %><img src="<%= avatar(f.follower_icon, 96) %>" alt=""><% } else { %><%= String(disp || '?').replace(/^@/, '').charAt(0).toUpperCase() %><% } %></span>
-          <span class="tl-foll-meta">
-            <a href="<%= f.follower_uri %>" target="_blank" rel="nofollow noopener"><%= disp %></a>
-            <% if (f.follower_handle && f.follower_name) { %><span class="conn-sub"><%= f.follower_handle %></span><% } %>
-          </span>
-          <form method="post" action="/follow-requests/approve"><input type="hidden" name="id" value="<%= f.id %>"><button type="submit" class="btn btn-primary"><%= t('tl.folreq_accept') %></button></form>
-          <form method="post" action="/follow-requests/deny"><input type="hidden" name="id" value="<%= f.id %>"><button type="submit" class="tl-unfollow"><%= t('tl.folreq_deny') %></button></form>
-        </li>
-      <% }); %>
-    </ul>
-  <% } %>
-
-  <%
-    var conns = connections || [];
-    var reachable   = conns.filter(function (c) { return !c.unreachable; });
-    var unreachable = conns.filter(function (c) { return c.unreachable; });
-    // Display fields for one connection: name/handle (fallback: derive from actor_uri).
-    function connView(c) {
-      var host = '', user = '';
-      try { var u = new URL(c.actor_uri); host = u.hostname; user = (u.pathname.split('/').filter(Boolean).pop() || ''); } catch (e) {}
-      var handle = c.handle || (user ? ('@' + user + '@' + host) : c.actor_uri);
-      var display = c.name || handle;
-      return { handle: handle, display: display,
-               initial: String(display || '?').replace(/^@/, '').charAt(0).toUpperCase(),
-               profileUrl: c.url || c.actor_uri };
-    }
-    function arrow(dir) { return dir === 'mutual' ? '↔' : (dir === 'following' ? '→' : '←'); }
-  %>
-
-  <% if (reachable.length) { %>
-    <ul class="tl-foll-list">
-      <% reachable.forEach(function (c) { var v = connView(c); %>
-        <li class="tl-foll">
-          <span class="tl-foll-av"><% if (c.icon) { %><img src="<%= avatar(c.icon, 96) %>" alt=""><% } else { %><%= v.initial %><% } %></span>
-          <span class="tl-foll-meta">
-            <a href="<%= v.profileUrl %>" target="_blank" rel="nofollow noopener"><%= v.display %></a>
-            <span class="conn-sub">
-              <span class="conn-dir conn-dir--<%= c.direction %>"><%= arrow(c.direction) %> <%= t('tl.dir_' + c.direction) %></span>
-              <% if (c.follower && c.last_delivery_at) { %><span class="conn-deliv">&middot; <%= t('tl.last_delivery') %>: <%= formatDateTime(c.last_delivery_at) %></span><% } %>
-            </span>
-          </span>
-          <% if (c.following) { %>
-            <form method="post" action="/news/autoboost" class="tl-foll-ab" title="<%= t('tl.autoboost_hint') %>">
-              <input type="hidden" name="actor_uri" value="<%= c.actor_uri %>">
-              <label><input type="checkbox" name="auto_boost" value="1" <%= c.auto_boost ? 'checked' : '' %> data-autosubmit> &#10024; <%= t('tl.autoboost') %></label>
-            </form>
-            <form method="post" action="/news/unfollow"><input type="hidden" name="actor_uri" value="<%= c.actor_uri %>"><button type="submit" class="tl-unfollow"><%= t('tl.unfollow') %></button></form>
-          <% } %>
-        </li>
-      <% }); %>
-    </ul>
-  <% } else { %>
-    <p class="tl-empty"><%= t('tl.connect_empty') %></p>
-  <% } %>
-
-  <% if (unreachable.length) { %>
-    <details class="conn-unreach">
-      <summary>&#9888; <%= t('tl.unreachable') %> (<%= unreachable.length %>)</summary>
-      <p class="conn-unreach-lead"><%= t('tl.unreachable_lead') %></p>
-      <ul class="tl-foll-list">
-        <% unreachable.forEach(function (c) { var v = connView(c); %>
-          <li class="tl-foll is-stale">
-            <span class="tl-foll-av"><% if (c.icon) { %><img src="<%= avatar(c.icon, 96) %>" alt=""><% } else { %><%= v.initial %><% } %></span>
-            <span class="tl-foll-meta">
-              <a href="<%= v.profileUrl %>" target="_blank" rel="nofollow noopener"><%= v.display %></a>
-              <span class="conn-sub">
-                <span class="conn-dir conn-dir--<%= c.direction %>"><%= arrow(c.direction) %> <%= t('tl.dir_' + c.direction) %></span>
-                <% if (c.last_delivery_at) { %><span class="conn-deliv">&middot; <%= t('tl.last_delivery') %>: <%= formatDateTime(c.last_delivery_at) %></span><% } else { %><span class="conn-never">&middot; <%= t('tl.never_delivered') %></span><% } %>
-                <% if (c.last_error_at) { %><span class="conn-err">&middot; <%= t('tl.delivery_failed') %>: <%= formatDateTime(c.last_error_at) %></span><% } %>
-              </span>
-            </span>
-            <% if (c.follower_id) { %>
-              <form method="post" action="/followers/<%= c.follower_id %>/remove" onsubmit="return confirm('<%= t('tl.remove_confirm') %>')">
-                <button type="submit" class="tl-unfollow"><%= t('tl.remove_follower') %></button>
-              </form>
-            <% } %>
-          </li>
-        <% }); %>
-      </ul>
-    </details>
-  <% } %>
-</div>
-
-<style>
-  /* Guardians (FEP-633c 3.6): the buddy-list dot on the responsibility axis.
-     Green available, yellow declared away with an end, grey observed dormant
-     (one answer restores). Set apart from the follow list on purpose: these
-     are not people you chose to follow, they are the ones looking after you. */
-  .conn-guardians { margin: 0 0 1.25rem; padding: .8rem 1rem; border-radius: 12px;
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent);
-    background: color-mix(in srgb, var(--ink, #000) 2.5%, transparent);
-    border-left: 3px solid #35a2ff; }
-  .conn-gtitle { margin: 0 0 .5rem; font-size: .95rem; }
-  .conn-glist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .35rem; }
-  .conn-guardian { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
-  .conn-gdot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
-  .conn-gdot--active { background: #4caf7d; }
-  .conn-gdot--away { background: #e0a83a; }
-  .conn-gdot--dormant { background: #8a93a3; }
-  .conn-ghandle { font-weight: 600; }
-  .conn-gstate { color: var(--ink-soft, #888); font-size: .82rem; }
-  .tl-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
-  .tl-title { margin: 0 0 .75rem; }
-  .tl-empty { color: var(--ink-soft, #888); }
-  .tl-follow { display: flex; gap: .5rem; margin: 0 0 1.25rem; flex-wrap: wrap; align-items: center; }
-  .tl-follow input[type="text"] { flex: 1; min-width: 200px; height: 2.4rem; padding: 0 .9rem; border-radius: 999px; font: inherit;
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, #000); }
-  .tl-follow-ab { font-size: .85rem; color: var(--ink-soft, #888); display: inline-flex; align-items: center; gap: .3rem; cursor: pointer; }
-  .tl-foll-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .4rem; }
-  .tl-foll { display: flex; align-items: center; gap: .6rem; padding: .45rem .6rem; border-radius: 12px; background: color-mix(in srgb, var(--ink, #000) 4%, transparent); }
-  .tl-foll.is-stale { background: color-mix(in srgb, #c0392b 10%, transparent); }
-  .tl-foll-av { flex: 0 0 36px; width: 36px; height: 36px; border-radius: 50%; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; font-weight: 700; background: color-mix(in srgb, var(--accent, #888) 20%, transparent); color: var(--accent, #555); }
-  .tl-foll-av img { width: 100%; height: 100%; object-fit: cover; background: #fff; }
-  .tl-foll-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.3; }
-  .tl-foll-meta a { color: var(--ink, inherit); text-decoration: none; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-  .tl-foll-meta a:hover { text-decoration: underline; }
-  .conn-sub { font-size: .78rem; color: var(--ink-soft, #888); display: inline-flex; flex-wrap: wrap; gap: .35rem; align-items: baseline; }
-  .conn-dir { font-weight: 600; }
-  .conn-dir--mutual   { color: var(--accent, #06c); }
-  .conn-dir--following { color: color-mix(in srgb, var(--ink, #000) 62%, transparent); }
-  .conn-dir--follower  { color: color-mix(in srgb, var(--ink, #000) 62%, transparent); }
-  .conn-never, .conn-err { color: #c0392b; }
-  .tl-foll-ab { margin: 0; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; }
-  .tl-foll-ab label { display: inline-flex; align-items: center; gap: .25rem; cursor: pointer; }
-  .tl-unfollow { background: none; border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); border-radius: 8px; padding: .25rem .6rem; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; white-space: nowrap; }
-  .tl-unfollow:hover { color: var(--ink, #000); }
-  .conn-unreach { margin: 1.25rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); padding-top: .75rem; }
-  .conn-unreach summary { cursor: pointer; font-weight: 600; color: #c0392b; }
-  .conn-unreach-lead { font-size: .8rem; color: var(--ink-soft, #888); margin: .5rem 0 .75rem; }
-</style>
Index: src/views/pages/download.ejs
===================================================================
--- src/views/pages/download.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,56 +1,0 @@
-<%
-  var _ready_h1     = t('dl.ready_title');
-  var _ready_sub    = t('dl.ready_sub');
-  var _manual       = t('dl.manual');
-  var _capture_sub  = t('dl.capture_sub');
-  var _email_ph     = t('dl.email_ph');
-  var _download_btn = t('dl.download_btn');
-%>
-<%
-  var tr = (typeof dlTrack !== 'undefined') ? dlTrack : {};
-  var st = (typeof dlState !== 'undefined') ? dlState : 'form';
-  var fileUrl = siteUrlBase + '/download/' + tr.id + '/bestand';
-%>
-<section class="dl">
-  <div class="dl-card">
-    <div class="dl-head">
-      <% if (tr.cover_url) { %><span class="dl-cover" style="background-image:url('<%= tr.cover_url %>')"></span><% } else { %><span class="dl-cover dl-cover-empty">♪</span><% } %>
-      <div>
-        <div class="dl-title"><%= tr.title %></div>
-        <% if (tr.artist) { %><div class="dl-artist"><%= tr.artist %></div><% } %>
-      </div>
-    </div>
-
-    <% if (st === 'ready') { %>
-      <h1 class="dl-h1"><%= _ready_h1 %></h1>
-      <p class="dl-sub"><%= _ready_sub %></p>
-      <p><a class="dl-go" href="<%= fileUrl %>"><%= _manual %></a></p>
-      <%# Het script staat in assets/js/mod/download.js; de servergegevens gaan via partials/page-data.ejs (shaer-bqr). %>
-    <% } else { %>
-      <h1 class="dl-h1"><%= _download_btn %> <%= tr.title %></h1>
-      <p class="dl-sub"><%= _capture_sub %></p>
-      <% if (typeof dlError !== 'undefined' && dlError) { %><p class="dl-err"><%= dlError %></p><% } %>
-      <form method="POST" action="<%= siteUrlBase %>/download/<%= tr.id %>" class="dl-form">
-        <input type="email" name="email" required placeholder="<%= _email_ph %>" value="<%= (typeof dlPrefill!=='undefined')?dlPrefill:'' %>" autocomplete="email">
-        <button type="submit" class="dl-go">⬇ <%= _download_btn %></button>
-      </form>
-    <% } %>
-  </div>
-</section>
-
-<style>
-  .dl { max-width: 520px; margin: 0 auto; padding: 48px 18px; }
-  .dl-card { border: 1px solid rgba(128,128,128,.2); border-radius: 18px; padding: 28px; }
-  .dl-head { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; }
-  .dl-cover { width: 56px; height: 56px; border-radius: 10px; background-size: cover; background-position: center; display: grid; place-items: center; flex: 0 0 auto; }
-  .dl-cover-empty { background: var(--accent,#6b8f71); color: #fff; opacity: .85; }
-  .dl-title { font-weight: 700; font-size: 17px; }
-  .dl-artist { opacity: .65; font-size: 13px; }
-  .dl-h1 { font-size: clamp(22px,4.5vw,30px); margin: 0 0 8px; }
-  .dl-sub { opacity: .85; line-height: 1.55; margin: 0 0 18px; }
-  .dl-err { color: #c43c3c; margin: 0 0 12px; }
-  .dl-form { display: flex; gap: 10px; flex-wrap: wrap; }
-  .dl-form input { flex: 1 1 200px; padding: 12px 14px; border-radius: 10px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; font-size: 15px; }
-  .dl-go { padding: 12px 20px; border-radius: 10px; border: none; background: var(--accent,#6b8f71); color: #fff; font-weight: 600; font-size: 15px; cursor: pointer; text-decoration: none; display: inline-block; }
-</style>
-<%- include('../partials/page-data', { pageData: { fileUrl: fileUrl } }) %>
Index: src/views/pages/downloads.ejs
===================================================================
--- src/views/pages/downloads.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,44 +1,0 @@
-<%
-  var ts = (typeof dlTracks !== 'undefined') ? dlTracks : [];
-  // NB: de loop hieronder gebruikt 't' als track-variabele, die de vertaal-t()
-  // overschaduwt. Daarom het knoplabel hier al ophalen.
-  var _dlBtn = t('downloads.btn');
-%>
-<article class="container post-page">
-  <%- include('../partials/post-nav', { newerPost: (typeof newerPost !== 'undefined' ? newerPost : null), olderPost: (typeof olderPost !== 'undefined' ? olderPost : null) }) %>
-  <section class="dls">
-  <h1 class="dls-h1"><%= t('downloads.title') %></h1>
-  <p class="dls-sub"><%= t('downloads.sub') %></p>
-
-  <% if (!ts.length) { %>
-    <p class="dls-empty"><%= t('downloads.empty') %></p>
-  <% } else { %>
-    <ul class="dls-list">
-      <% ts.forEach(function(t){ %>
-        <li class="dls-item">
-          <% if (t.cover_url) { %><span class="dls-cover" style="background-image:url('<%= t.cover_url %>')"></span><% } else { %><span class="dls-cover dls-cover-empty">♪</span><% } %>
-          <span class="dls-meta">
-            <span class="dls-title"><%= t.title %></span>
-            <% if (t.artist) { %><span class="dls-artist"><%= t.artist %></span><% } %>
-          </span>
-          <a class="dls-btn" href="<%= siteUrlBase %>/download/<%= t.id %>"><%= _dlBtn %></a>
-        </li>
-      <% }); %>
-    </ul>
-  <% } %>
-  </section>
-</article>
-
-<style>
-  .dls { padding: 0 0 64px; }
-  .dls-h1 { font-size: clamp(26px,5vw,38px); margin: 0 0 8px; }
-  .dls-sub { opacity: .8; margin: 0 0 24px; }
-  .dls-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
-  .dls-item { display: flex; align-items: center; gap: 14px; padding: 10px 12px; border: 1px solid rgba(128,128,128,.2); border-radius: 12px; }
-  .dls-cover { flex: 0 0 auto; width: 48px; height: 48px; border-radius: 8px; background-size: cover; background-position: center; display: grid; place-items: center; }
-  .dls-cover-empty { background: var(--accent,#6b8f71); color: #fff; opacity: .85; }
-  .dls-meta { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
-  .dls-title { font-weight: 600; }
-  .dls-artist { font-size: 12.5px; opacity: .65; }
-  .dls-btn { flex: 0 0 auto; padding: 9px 15px; border-radius: 999px; background: var(--accent,#6b8f71); color: #fff; text-decoration: none; font-weight: 600; font-size: 13.5px; }
-</style>
Index: src/views/pages/embed-player.ejs
===================================================================
--- src/views/pages/embed-player.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,104 +1,0 @@
-<!DOCTYPE html>
-<html lang="nl">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title><%= site.title %> — speler</title>
-<style>
-  :root { --ac: <%= (site && site.accent) ? site.accent : '#6b8f71' %>; }
-  * { box-sizing: border-box; }
-  html, body { margin: 0; }
-  body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; background: #14161a; color: #f2f3f5; }
-  .ep { display: flex; flex-direction: column; height: 100vh; }
-  .ep-now { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,.08); }
-  .ep-play { flex: 0 0 auto; width: 44px; height: 44px; border-radius: 50%; border: none; background: var(--ac); color: #fff; font-size: 18px; cursor: pointer; display: grid; place-items: center; }
-  .ep-meta { flex: 1 1 auto; min-width: 0; }
-  .ep-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-  .ep-artist { font-size: 12px; opacity: .6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-  .ep-bar { height: 4px; background: rgba(255,255,255,.12); cursor: pointer; }
-  .ep-fill { height: 100%; width: 0; background: var(--ac); }
-  .ep-list { flex: 1 1 auto; overflow-y: auto; margin: 0; padding: 0; list-style: none; }
-  .ep-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,.05); }
-  .ep-item:hover { background: rgba(255,255,255,.05); }
-  .ep-item.on { color: var(--ac); }
-  .ep-num { width: 20px; text-align: right; opacity: .5; font-size: 13px; font-variant-numeric: tabular-nums; }
-  .ep-it-title { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 14px; }
-  .ep-dur { opacity: .5; font-size: 12px; font-variant-numeric: tabular-nums; }
-  .ep-foot { padding: 8px 14px; font-size: 11px; opacity: .5; text-align: center; border-top: 1px solid rgba(255,255,255,.08); }
-  .ep-foot a { color: inherit; }
-  .ep-empty { padding: 24px; text-align: center; opacity: .6; }
-  /* Slim, themed scrollbar for the track list (instead of the chunky default). */
-  .ep-list { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.28) transparent; }
-  .ep-list::-webkit-scrollbar { width: 8px; }
-  .ep-list::-webkit-scrollbar-track { background: transparent; }
-  .ep-list::-webkit-scrollbar-thumb { background-color: rgba(255,255,255,.22); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; }
-  .ep-list::-webkit-scrollbar-thumb:hover { background-color: color-mix(in srgb, var(--ac) 70%, #fff); }
-</style>
-</head>
-<body>
-<div class="ep">
-  <div class="ep-now">
-    <button class="ep-play" id="ep-play" aria-label="Afspelen">▶</button>
-    <div class="ep-meta">
-      <div class="ep-title" id="ep-title"><%= site.title %></div>
-      <div class="ep-artist" id="ep-artist">Selecteer een nummer</div>
-    </div>
-  </div>
-  <div class="ep-bar" id="ep-bar"><div class="ep-fill" id="ep-fill"></div></div>
-  <% if (!embedTracks.length) { %>
-    <div class="ep-empty">Geen nummers beschikbaar.</div>
-  <% } else { %>
-    <ul class="ep-list" id="ep-list">
-      <% embedTracks.forEach(function(t, i){ %>
-        <li class="ep-item" data-i="<%= i %>">
-          <span class="ep-num"><%= i+1 %></span>
-          <span class="ep-it-title"><%= t.title %><% if (t.artist) { %> — <span style="opacity:.6"><%= t.artist %></span><% } %></span>
-          <span class="ep-dur" data-dur="<%= t.duration || 0 %>"></span>
-        </li>
-      <% }); %>
-    </ul>
-  <% } %>
-  <div class="ep-foot">via <a href="<%= siteUrlBase %>/" target="_blank" rel="noopener"><%= site.title %></a> · Klonkt</div>
-</div>
-<audio id="ep-audio" preload="none" playsinline></audio>
-<script>
-(function(){
-  var TRACKS = <%- JSON.stringify(embedTracks) %>;
-  var audio = document.getElementById('ep-audio');
-  var playBtn = document.getElementById('ep-play');
-  var titleEl = document.getElementById('ep-title');
-  var artistEl = document.getElementById('ep-artist');
-  var fill = document.getElementById('ep-fill');
-  var bar = document.getElementById('ep-bar');
-  var list = document.getElementById('ep-list');
-  var cur = -1;
-  function fmt(s){ s = parseInt(s,10)||0; var m=Math.floor(s/60), x=s%60; return m+':'+String(x).padStart(2,'0'); }
-  // Vul de duur-labels.
-  document.querySelectorAll('.ep-dur').forEach(function(el){ var d=parseInt(el.getAttribute('data-dur'),10)||0; if(d) el.textContent=fmt(d); });
-  function mark(){ document.querySelectorAll('.ep-item').forEach(function(li){ li.classList.toggle('on', parseInt(li.getAttribute('data-i'),10)===cur); }); }
-  function load(i, play){
-    if(i<0||i>=TRACKS.length) return;
-    cur=i; var t=TRACKS[i];
-    titleEl.textContent=t.title||'Naamloos';
-    artistEl.textContent=t.artist||'';
-    audio.src=t.url;
-    mark();
-    if(play){ audio.play().catch(function(){}); }
-  }
-  playBtn.addEventListener('click', function(){
-    if(cur<0){ load(0, true); return; }
-    if(audio.paused) audio.play().catch(function(){}); else audio.pause();
-  });
-  if(list) list.addEventListener('click', function(e){
-    var li=e.target.closest('.ep-item'); if(!li) return;
-    load(parseInt(li.getAttribute('data-i'),10), true);
-  });
-  audio.addEventListener('play', function(){ playBtn.textContent='⏸'; });
-  audio.addEventListener('pause', function(){ playBtn.textContent='▶'; });
-  audio.addEventListener('timeupdate', function(){ if(audio.duration) fill.style.width=(audio.currentTime/audio.duration*100)+'%'; });
-  audio.addEventListener('ended', function(){ if(cur+1<TRACKS.length) load(cur+1, true); else { playBtn.textContent='▶'; fill.style.width='0'; } });
-  bar.addEventListener('click', function(e){ if(!audio.duration) return; var r=bar.getBoundingClientRect(); audio.currentTime=(e.clientX-r.left)/r.width*audio.duration; });
-})();
-</script>
-</body>
-</html>
Index: src/views/pages/epk.ejs
===================================================================
--- src/views/pages/epk.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,94 +1,0 @@
-<%
-  // Duur mm:ss
-  function fmtDur(s){ s = parseInt(s,10)||0; var m=Math.floor(s/60), x=s%60; return m+':'+String(x).padStart(2,'0'); }
-  var _accent = (site && site.accent) ? site.accent : '';
-  var _photo = (site && site.profile_photo) ? site.profile_photo : '';
-  var _isMail = epkContact && epkContact.indexOf('@') !== -1 && epkContact.indexOf(' ') === -1;
-  var _isUrl = epkContact && /^https?:\/\//i.test(epkContact);
-%>
-<section class="epk" <%= _accent ? ('style="--epk-accent:' + _accent + '"') : '' %>>
-  <header class="epk-hero">
-    <% if (_photo) { %><div class="epk-photo"><img src="<%= avatar(_photo, 320) %>" alt="<%= site.title %>"></div><% } %>
-    <div class="epk-hero-text">
-      <div class="epk-kicker"><%= t('epk.kicker') %><% if (typeof user !== 'undefined' && user && user.role === 'god') { %> <a class="epk-edit-link" href="/admin/epk">✎ <%= t('epk.edit') %></a><% } %></div>
-      <h1 class="epk-title"><%= site.title %></h1>
-      <% if (epkBio) { %><p class="epk-bio"><%= epkBio %></p><% } %>
-      <div class="epk-actions">
-        <% if (_isMail) { %>
-          <a class="epk-btn epk-btn-primary" href="mailto:<%= epkContact %>">✉ <%= t('epk.contact_booking') %></a>
-        <% } else if (_isUrl) { %>
-          <a class="epk-btn epk-btn-primary" href="<%= epkContact %>" target="_blank" rel="noopener"><%= t('epk.contact_booking') %></a>
-        <% } %>
-        <a class="epk-btn" href="<%= siteUrlBase %>/"><%= t('epk.view_site') %></a>
-        <% if (_photo) { %><a class="epk-btn" href="<%= _photo %>" download>⬇ <%= t('epk.press_photo') %></a><% } %>
-      </div>
-    </div>
-  </header>
-
-  <% if (epkTracks && epkTracks.length) { %>
-    <div class="epk-block">
-      <h2 class="epk-h2"><%= t('epk.most_played') %></h2>
-      <ul class="epk-tracks">
-        <% epkTracks.forEach(function(t, i){ %>
-          <li class="epk-track">
-            <span class="epk-rank"><%= i + 1 %></span>
-            <% if (t.cover_url) { %><span class="epk-cover" style="background-image:url('<%= t.cover_url %>')"></span><% } else { %><span class="epk-cover epk-cover-empty">♪</span><% } %>
-            <span class="epk-track-meta">
-              <span class="epk-track-title"><%= t.title %></span>
-              <% if (t.artist) { %><span class="epk-track-artist"><%= t.artist %></span><% } %>
-            </span>
-            <% if (t.duration) { %><span class="epk-track-dur"><%= fmtDur(t.duration) %></span><% } %>
-          </li>
-        <% }); %>
-      </ul>
-    </div>
-  <% } %>
-
-  <% if (epkPosts && epkPosts.length) { %>
-    <div class="epk-block">
-      <h2 class="epk-h2"><%= t('epk.recent') %></h2>
-      <ul class="epk-posts">
-        <% epkPosts.forEach(function(p){ %>
-          <li><a href="<%= siteUrlBase %>/<%= p.slug %>"><%= p.title %></a></li>
-        <% }); %>
-      </ul>
-    </div>
-  <% } %>
-
-  <footer class="epk-foot">
-    <% if (_isMail) { %><a href="mailto:<%= epkContact %>"><%= epkContact %></a> · <% } %>
-    <a href="<%= siteUrlBase %>/"><%= site.title %></a>
-  </footer>
-</section>
-
-<style>
-  .epk { max-width: 860px; margin: 0 auto; padding: 28px 18px 64px; --epk-accent: var(--accent, #6b8f71); }
-  .epk-hero { display: flex; gap: 26px; align-items: center; flex-wrap: wrap; margin-bottom: 40px; }
-  .epk-photo { flex: 0 0 auto; }
-  .epk-photo img { width: 190px; height: 190px; object-fit: cover; border-radius: 18px; box-shadow: 0 8px 30px rgba(0,0,0,.18); }
-  .epk-hero-text { flex: 1 1 280px; min-width: 240px; }
-  .epk-kicker { text-transform: uppercase; letter-spacing: .14em; font-size: 12px; font-weight: 700; color: var(--epk-accent); margin-bottom: 6px; }
-  .epk-edit-link { text-transform: none; letter-spacing: normal; font-weight: 600; font-size: 12px; margin-left: 8px; padding: 2px 8px; border: 1px solid rgba(128,128,128,.4); border-radius: 999px; color: inherit; opacity: .8; text-decoration: none; }
-  .epk-edit-link:hover { opacity: 1; border-color: var(--epk-accent); color: var(--epk-accent); }
-  .epk-title { font-size: clamp(30px, 6vw, 52px); line-height: 1.05; margin: 0 0 12px; }
-  .epk-bio { font-size: 16px; line-height: 1.6; opacity: .9; margin: 0 0 18px; max-width: 52ch; white-space: pre-line; }
-  .epk-actions { display: flex; gap: 10px; flex-wrap: wrap; }
-  .epk-btn { display: inline-block; padding: 9px 15px; border-radius: 999px; border: 1px solid rgba(128,128,128,.35); text-decoration: none; font-size: 13.5px; font-weight: 600; color: inherit; }
-  .epk-btn-primary { background: var(--epk-accent); border-color: var(--epk-accent); color: #fff; }
-  .epk-block { margin: 34px 0; }
-  .epk-h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .1em; opacity: .6; margin: 0 0 14px; }
-  .epk-tracks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
-  .epk-track { display: flex; align-items: center; gap: 12px; padding: 8px 10px; border: 1px solid rgba(128,128,128,.18); border-radius: 12px; }
-  .epk-rank { flex: 0 0 auto; width: 20px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; opacity: .45; }
-  .epk-cover { flex: 0 0 auto; width: 42px; height: 42px; border-radius: 8px; background-size: cover; background-position: center; display: grid; place-items: center; }
-  .epk-cover-empty { background: var(--epk-accent); color: #fff; opacity: .85; }
-  .epk-track-meta { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
-  .epk-track-title { font-weight: 600; }
-  .epk-track-artist { font-size: 12.5px; opacity: .65; }
-  .epk-track-dur { font-variant-numeric: tabular-nums; opacity: .6; font-size: 13px; }
-  .epk-posts { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
-  .epk-posts a { text-decoration: none; color: inherit; border-bottom: 1px solid transparent; }
-  .epk-posts a:hover { border-bottom-color: var(--epk-accent); }
-  .epk-foot { margin-top: 44px; padding-top: 18px; border-top: 1px solid rgba(128,128,128,.2); font-size: 13px; opacity: .7; }
-  .epk-foot a { color: inherit; }
-</style>
Index: src/views/pages/fan-gate.ejs
===================================================================
--- src/views/pages/fan-gate.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,60 +1,0 @@
-<div class="container fg-navwrap">
-  <%# Same post navigation (Newer/Older) as on a regular post, so a visitor
-      on the fan gate can still browse rather than getting stuck. Same
-      dimensions as .post-page (720px / padding 0 1rem) for pixel-perfect alignment. %>
-  <%- include('../partials/post-nav', { newerPost: (typeof newerPost !== 'undefined' ? newerPost : null), olderPost: (typeof olderPost !== 'undefined' ? olderPost : null) }) %>
-</div>
-
-<section class="fg">
-  <div class="fg-card">
-    <div class="fg-lock">🔒</div>
-    <h1 class="fg-h1"><%= t('fgate.title') %></h1>
-    <% if (typeof fgTitle !== 'undefined' && fgTitle) { %><p class="fg-which">"<%= fgTitle %>"</p><% } %>
-    <p class="fg-sub"><%= t('fgate.sub') %></p>
-
-    <%# De hoofdweg: bewijs bij je EIGEN server wie je bent (OpenWebAuth,
-        FEP-61cf). Dit is de vraag die fan_only altijd al stelde -- volg je deze
-        site? -- in plaats van "heb je hier een account", wat juist de mensen
-        buitensloot voor wie de poort openstond. Geen account hier, geen
-        wachtwoord hier, geen cookie van een derde. %>
-    <form class="fg-owa" method="post" action="/owa/login">
-      <input type="hidden" name="next" value="<%= fgNext || '/' %>">
-      <label class="fg-label" for="fg-handle"><%= t('fgate.owa_label') %></label>
-      <div class="fg-row">
-        <input class="fg-input" id="fg-handle" name="handle" type="text" inputmode="email"
-               autocomplete="username" spellcheck="false" placeholder="@jij@jouwserver.nl" required>
-        <button class="fg-btn" type="submit"><%= t('fgate.owa_go') %></button>
-      </div>
-      <p class="fg-hint"><%= t('fgate.owa_hint') %></p>
-      <% if (typeof owaError !== 'undefined' && owaError) { %>
-        <p class="fg-error"><%= t('fgate.owa_failed') %></p>
-      <% } %>
-    </form>
-
-    <%# De lokale inlog blijft, maar als tweede: die is voor wie hier echt een
-        account heeft, en dat is de uitzondering. %>
-    <p class="fg-alt"><a href="/auth/login?next=<%= encodeURIComponent(fgNext || '/') %>"><%= t('fgate.login') %></a></p>
-  </div>
-</section>
-
-<style>
-  /* EXACT same class combination (.container + override) and values as
-     .post-page in post.ejs, so the post-nav aligns identically in the cascade. */
-  .fg-navwrap { max-width: 720px; margin: 0.5rem auto 2rem; padding: 0 1rem; }
-  .fg { max-width: 480px; margin: 0 auto; padding: 56px 18px; text-align: center; }
-  .fg-card { border: 1px solid rgba(128,128,128,.2); border-radius: 18px; padding: 36px 28px; }
-  .fg-lock { font-size: 40px; margin-bottom: 8px; }
-  .fg-h1 { font-size: 26px; margin: 0 0 6px; }
-  .fg-which { font-style: italic; opacity: .8; margin: 0 0 12px; }
-  .fg-sub { opacity: .85; line-height: 1.6; margin: 0 0 20px; }
-  .fg-btn { display: inline-block; padding: 12px 22px; border-radius: 10px; background: var(--accent,#6b8f71); color: #fff; text-decoration: none; font-weight: 600; border: 0; font-family: inherit; font-size: 1rem; cursor: pointer; }
-  .fg-owa { margin: 0 0 18px; text-align: left; }
-  .fg-label { display: block; font-size: .85rem; opacity: .8; margin-bottom: 6px; }
-  .fg-row { display: flex; gap: 8px; flex-wrap: wrap; }
-  .fg-input { flex: 1 1 12rem; min-width: 0; padding: 12px 14px; border-radius: 10px;
-              border: 1px solid rgba(128,128,128,.35); background: transparent; color: inherit;
-              font-family: inherit; font-size: 1rem; }
-  .fg-hint { font-size: .8rem; opacity: .65; margin: 8px 0 0; line-height: 1.5; }
-  .fg-error { font-size: .85rem; margin: 8px 0 0; color: #d66; }
-  .fg-alt { margin: 0; font-size: .85rem; opacity: .7; }
-</style>
Index: src/views/pages/fedi-notifications.ejs
===================================================================
--- src/views/pages/fedi-notifications.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,86 +1,0 @@
-<div class="nt-wrap">
-  <%- include('../partials/fedi-tabs', { active: 'meldingen' }) %>
-  <h1 class="nt-title"><%= t('notif.title') %></h1>
-  <% if (!items || !items.length) { %>
-    <p class="nt-empty"><%= t('notif.empty') %></p>
-  <% } else { %>
-    <ul class="nt-list">
-      <% items.forEach(function(n){
-           var _ic = n.type === 'follow' ? 'follow' : (n.type === 'like' ? 'like' : (n.type === 'announce' ? 'boost' : (n.type === 'report' ? 'report' : (n.type === 'mention' ? 'mention' : 'reply'))));
-      %>
-        <li class="nt-item nt-<%= _ic %>">
-          <span class="nt-icon" aria-hidden="true">
-            <% if (_ic === 'like') { %><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.96l-5.8 3.06 1.1-6.46-4.69-4.58 6.49-.95z"/></svg>
-            <% } else if (_ic === 'boost') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
-            <% } else if (_ic === 'follow') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
-            <% } else if (_ic === 'report') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>
-            <% } else if (_ic === 'mention') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"/></svg>
-            <% } else { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
-          <div class="nt-body">
-            <div class="nt-line">
-              <a class="nt-who" href="<%= n.url %>" target="_blank" rel="nofollow noopener"><%= n.name || n.handle %></a>
-              <% if (n.handle && n.name) { %><span class="nt-handle"><%= n.handle %></span><% } %>
-              <% if (n.created_at) { %><span class="nt-time"><%= formatDateTime(n.created_at) %></span><% } %>
-            </div>
-            <div class="nt-what">
-              <% if (n.type === 'follow') { %><%= t('notif.followed') %>
-              <% } else if (n.type === 'like') { %><%= t('notif.liked') %>
-              <% } else if (n.type === 'announce') { %><%= t('notif.boosted') %>
-              <% } else if (n.type === 'report') { %><%= t('notif.reported') %>
-              <% } else if (n.type === 'mention') { %><%= t('notif.mentioned') %>
-              <% } else { %><%= t('notif.replied') %><% } %>
-              <% if (n.post_slug) { %><a class="nt-post" href="/<%= n.post_slug %>"><%= n.post_title || n.post_slug %></a><% } %>
-              <% if (n.type === 'mention' && n.note_url) { %><a class="nt-post" href="<%= n.note_url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
-            </div>
-            <% if (n.type === 'report') { %>
-              <% if (n.content) { %><div class="nt-content"><%= n.content %></div><% } else { %><div class="nt-content nt-noreason"><%= t('notif.report_noreason') %></div><% } %>
-              <% if (n.objects && n.objects.length) { %>
-                <div class="nt-report-about">
-                  <% n.objects.forEach(function (o) { %>
-                    <a class="nt-post" href="/<%= o.slug %>"><%= t('notif.report_about') %>: <%= o.title %></a>
-                  <% }); %>
-                </div>
-              <% } %>
-            <% } else if ((n.type === 'reply' || n.type === 'mention') && n.content) { %><div class="nt-content"><%- n.content %></div><% } %>
-          </div>
-        </li>
-      <% }); %>
-    </ul>
-  <% } %>
-</div>
-
-<style>
-  .nt-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
-  .fedi-back { margin: 0 0 1.25rem; }
-  .nt-title { margin: 0 0 1.25rem; }
-  .nt-empty { color: var(--ink-soft, #888); }
-  .nt-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .6rem; }
-  .nt-item { display: flex; gap: .85rem; padding: .9rem 1rem; border-radius: 14px; align-items: flex-start;
-    background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent);
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent);
-    transition: border-color .15s ease, box-shadow .15s ease; }
-  .nt-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 30%, transparent);
-    box-shadow: 0 2px 12px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
-  .nt-icon { flex: 0 0 38px; width: 38px; height: 38px; border-radius: 50%;
-    display: inline-flex; align-items: center; justify-content: center; }
-  .nt-icon svg { width: 18px; height: 18px; }
-  .nt-report-about { margin-top: .35rem; display: flex; flex-direction: column; gap: .15rem; }
-  .nt-noreason { font-style: italic; opacity: .7; }
-  .nt-like .nt-icon { background: color-mix(in srgb, #e8b04b 16%, transparent); color: #e8b04b; }
-  .nt-boost .nt-icon { background: color-mix(in srgb, #2fa85a 16%, transparent); color: #2fa85a; }
-  .nt-follow .nt-icon, .nt-reply .nt-icon { background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); }
-  .nt-report .nt-icon { background: color-mix(in srgb, #c0392b 16%, transparent); color: #c0392b; }
-  .nt-mention .nt-icon { background: color-mix(in srgb, #8e6cf0 16%, transparent); color: #8e6cf0; }
-  .nt-body { flex: 1; min-width: 0; }
-  .nt-line { display: flex; align-items: baseline; gap: .4rem; }
-  .nt-who { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-  .nt-who:hover { text-decoration: underline; }
-  .nt-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
-  .nt-time { color: var(--ink-soft, #999); font-size: .76rem; margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
-  .nt-what { color: var(--ink-soft, #aaa); margin-top: .12rem; line-height: 1.4; }
-  .nt-post { color: var(--accent, #06c); font-weight: 600; }
-  .nt-content { margin: .45rem 0 0; padding: .55rem .75rem; border-radius: 10px; line-height: 1.5;
-    background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, inherit); overflow-wrap: anywhere; }
-  .nt-content p { margin: .2rem 0; } .nt-content p:first-child { margin-top: 0; } .nt-content p:last-child { margin-bottom: 0; }
-</style>
Index: src/views/pages/followers.ejs
===================================================================
--- src/views/pages/followers.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,58 +1,0 @@
-<div class="tl-wrap">
-  <%- include('../partials/fedi-tabs', { active: 'followers' }) %>
-  <h1 class="tl-title"><%= t('tl.followers') %><% if (followers && followers.length) { %> (<%= followers.length %>)<% } %></h1>
-  <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
-  <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
-
-  <p class="tl-lead"><%= t('tl.followers_lead') %></p>
-
-  <% if (followers && followers.length) { %>
-    <ul class="tl-foll-list">
-      <% followers.forEach(function(f){
-           var host='', user='';
-           try { var u = new URL(f.actor_uri); host = u.hostname; user = (u.pathname.split('/').filter(Boolean).pop() || ''); } catch (e) {}
-           var handle = user ? ('@' + user + '@' + host) : f.actor_uri;
-           var stale = !f.last_delivery_at || (f.last_error_at && (!f.last_delivery_at || f.last_error_at > f.last_delivery_at));
-      %>
-        <li class="tl-foll<%= stale ? ' is-stale' : '' %>">
-          <span class="tl-foll-av"><%= (user || host || '?').charAt(0).toUpperCase() %></span>
-          <span class="tl-foll-meta">
-            <a href="<%= f.actor_uri %>" target="_blank" rel="nofollow noopener"><%= handle %></a>
-            <span class="tl-foll-deliv">
-              <% if (f.last_delivery_at) { %>
-                <%= t('tl.last_delivery') %>: <%= formatDateTime(f.last_delivery_at) %>
-              <% } else { %>
-                <span class="tl-foll-never"><%= t('tl.never_delivered') %></span>
-              <% } %>
-              <% if (f.last_error_at) { %><span class="tl-foll-err">· <%= t('tl.delivery_failed') %>: <%= formatDateTime(f.last_error_at) %></span><% } %>
-            </span>
-          </span>
-          <form method="post" action="/followers/<%= f.id %>/remove" onsubmit="return confirm('<%= t('tl.remove_confirm') %>')">
-            <button type="submit" class="tl-unfollow"><%= t('tl.remove_follower') %></button>
-          </form>
-        </li>
-      <% }); %>
-    </ul>
-  <% } else { %>
-    <p class="tl-empty"><%= t('tl.empty_followers') %></p>
-  <% } %>
-</div>
-
-<style>
-  .tl-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
-  .tl-title { margin: 0 0 .25rem; }
-  .tl-lead { color: var(--ink-soft, #888); margin: 0 0 1.25rem; font-size: .9rem; line-height: 1.5; }
-  .tl-foll-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .4rem; }
-  .tl-foll { display: flex; align-items: center; gap: .6rem; padding: .45rem .6rem; border-radius: 12px; background: color-mix(in srgb, var(--ink, #000) 4%, transparent); }
-  .tl-foll.is-stale { background: color-mix(in srgb, #c0392b 10%, transparent); }
-  .tl-foll-av { flex: 0 0 36px; width: 36px; height: 36px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-weight: 700; background: color-mix(in srgb, var(--accent, #888) 20%, transparent); color: var(--accent, #555); }
-  .tl-foll-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.3; }
-  .tl-foll-meta a { color: var(--ink, inherit); text-decoration: none; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-  .tl-foll-meta a:hover { text-decoration: underline; }
-  .tl-foll-deliv { color: var(--ink-soft, #888); font-size: .78rem; }
-  .tl-foll-never { color: #c0392b; font-weight: 600; }
-  .tl-foll-err { color: #c0392b; }
-  .tl-unfollow { background: none; border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); border-radius: 8px; padding: .25rem .6rem; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; white-space: nowrap; }
-  .tl-unfollow:hover { color: #c0392b; border-color: #c0392b; }
-  .tl-empty { color: var(--ink-soft, #888); }
-</style>
Index: src/views/pages/following.ejs
===================================================================
--- src/views/pages/following.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,81 +1,0 @@
-<div class="tl-wrap">
-  <%- include('../partials/fedi-tabs', { active: 'following' }) %>
-  <h1 class="tl-title"><%= t('tl.tab_following') %></h1>
-  <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
-  <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
-
-  <div class="tl-explain">
-    <p><span class="tl-explain-tag">✨ <%= t('tl.autoboost') %></span> — <%= t('tl.autoboost_hint') %></p>
-  </div>
-
-  <form method="post" action="/news/follow" class="tl-follow">
-    <input type="text" name="handle" placeholder="@naam@server.social of site.com" autocomplete="off" spellcheck="false" required>
-    <button type="submit" class="btn btn-primary"><%= t('tl.follow_btn') %></button>
-    <label class="tl-follow-ab" title="<%= t('tl.autoboost_hint') %>"><input type="checkbox" name="auto_boost" value="1"> ✨ <%= t('tl.autoboost_follow') %></label>
-  </form>
-
-  <details class="tl-move">
-    <summary><%= t('tl.move_title') %></summary>
-    <p class="tl-move-hint"><%= t('tl.move_hint') %></p>
-    <p><a class="btn" href="/news/following.csv" download>⬇ <%= t('tl.move_export') %></a></p>
-    <form method="post" action="/news/following/import">
-      <label class="tl-move-lbl" for="tl-move-csv"><%= t('tl.move_import_lbl') %></label>
-      <textarea id="tl-move-csv" name="csv" rows="5" spellcheck="false"
-                placeholder="Account address,Show boosts,Notify on new posts,Languages,Featured"></textarea>
-      <button type="submit" class="btn btn-primary"><%= t('tl.move_import') %></button>
-    </form>
-  </details>
-
-  <% if (following && following.length) { %>
-    <h2 class="tl-sub"><%= t('tl.following') %> (<%= following.length %>)</h2>
-    <ul class="tl-foll-list">
-      <% following.forEach(function(f){ %>
-        <li class="tl-foll">
-          <span class="tl-foll-av"><% if (f.icon) { %><img src="<%= avatar(f.icon, 96) %>" alt=""><% } else { %><%= (f.name || '?').charAt(0).toUpperCase() %><% } %></span>
-          <span class="tl-foll-meta">
-            <a href="<%= f.url || f.actor_uri %>" target="_blank" rel="nofollow noopener"><%= f.name || f.handle %></a>
-            <span class="tl-foll-handle"><%= f.handle %></span>
-            <% if (f.status === 'pending') { %><span class="tl-pending"><%= t('tl.pending') %></span><% } %>
-          </span>
-          <form method="post" action="/news/autoboost" class="tl-foll-ab" title="<%= t('tl.autoboost_hint') %>">
-            <input type="hidden" name="actor_uri" value="<%= f.actor_uri %>">
-            <label><input type="checkbox" name="auto_boost" value="1" <%= f.auto_boost ? 'checked' : '' %> data-autosubmit> ✨ <%= t('tl.autoboost') %></label>
-          </form>
-          <form method="post" action="/news/unfollow"><input type="hidden" name="actor_uri" value="<%= f.actor_uri %>"><button type="submit" class="tl-unfollow"><%= t('tl.unfollow') %></button></form>
-        </li>
-      <% }); %>
-    </ul>
-  <% } else { %>
-    <p class="tl-empty"><%= t('tl.empty_following') %></p>
-  <% } %>
-</div>
-
-<style>
-  .tl-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
-  .tl-title { margin: 0 0 .25rem; }
-  .tl-lead { color: var(--ink-soft, #888); margin: 0 0 1.25rem; }
-  .tl-explain { margin: 0 0 1.25rem; padding: .85rem 1rem; border-radius: 12px;
-    background: color-mix(in srgb, var(--accent, #888) 8%, transparent);
-    border: 1px solid color-mix(in srgb, var(--accent, #888) 22%, transparent); }
-  .tl-explain p { margin: .3rem 0; font-size: .85rem; color: var(--ink-soft, #888); line-height: 1.5; }
-  .tl-explain-tag { font-weight: 600; color: var(--ink, inherit); white-space: nowrap; }
-  .tl-sub { font-size: 1.1rem; margin: 1.5rem 0 .75rem; }
-  .tl-follow { display: flex; gap: .5rem; margin: 0 0 1rem; flex-wrap: wrap; align-items: center; }
-  .tl-follow input[type="text"] { flex: 1; min-width: 200px; height: 2.4rem; padding: 0 .9rem; border-radius: 999px; font: inherit;
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, #000); }
-  .tl-follow-ab { font-size: .85rem; color: var(--ink-soft, #888); display: inline-flex; align-items: center; gap: .3rem; cursor: pointer; }
-  .tl-foll-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .4rem; }
-  .tl-foll { display: flex; align-items: center; gap: .6rem; padding: .45rem .6rem; border-radius: 12px; background: color-mix(in srgb, var(--ink, #000) 4%, transparent); }
-  .tl-foll-av { flex: 0 0 36px; width: 36px; height: 36px; border-radius: 50%; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; font-weight: 700; background: color-mix(in srgb, var(--accent, #888) 20%, transparent); color: var(--accent, #555); }
-  .tl-foll-av img { width: 100%; height: 100%; object-fit: cover; background: #fff; }
-  .tl-foll-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
-  .tl-foll-meta a { color: var(--ink, inherit); text-decoration: none; font-weight: 600; }
-  .tl-foll-meta a:hover { text-decoration: underline; }
-  .tl-foll-handle { color: var(--ink-soft, #888); font-size: .82rem; }
-  .tl-pending { font-size: .72rem; color: var(--ink-soft, #999); }
-  .tl-foll-ab { margin: 0; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; }
-  .tl-foll-ab label { display: inline-flex; align-items: center; gap: .25rem; cursor: pointer; }
-  .tl-unfollow { background: none; border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); border-radius: 8px; padding: .25rem .6rem; font-size: .8rem; color: var(--ink-soft, #888); cursor: pointer; }
-  .tl-unfollow:hover { color: var(--ink, #000); }
-  .tl-empty { color: var(--ink-soft, #888); }
-</style>
Index: src/views/pages/guardian.ejs
===================================================================
--- src/views/pages/guardian.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,107 +1,0 @@
-<!DOCTYPE html>
-<html lang="<%= lang %>">
-<head>
-  <meta charset="utf-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <title><%= t('guardian.app_name') %></title>
-  <link rel="manifest" href="/guardian/manifest.webmanifest">
-  <link rel="icon" href="/guardian/icon.svg" type="image/svg+xml">
-  <meta name="theme-color" content="#ff6b35">
-  <link rel="stylesheet" href="/guardian/app.css">
-</head>
-<body>
-  <header class="g-head">
-    <span class="g-buoy">🛟</span>
-    <div class="g-head-txt">
-      <h1><%= t('guardian.app_name') %></h1>
-      <p class="g-sub"><%= t('guardian.tagline') %></p>
-    </div>
-    <% if (sites.length > 1) { %>
-    <select id="site-picker" aria-label="<%= t('guardian.acting_as') %>">
-      <% for (const s of sites) { %>
-      <option value="<%= s.slug %>" <%= s.slug === state.site ? 'selected' : '' %>>@<%= s.slug %></option>
-      <% } %>
-    </select>
-    <% } else { %>
-    <span class="g-me" title="<%= t('guardian.acting_as') %>">@<%= state.site %></span>
-    <% } %>
-</header>
-
-  <main>
-    <!-- 1. Hulpverzoeken, uitgelicht over alle wards heen. Dat is waar dit voor
-            bestaat, en het moet opvallen zonder dat je eerst een kind opent.
-            De volledige geschiedenis per kind staat in dat kind zijn paneel. -->
-    <section id="help-section">
-      <div class="g-sec-head">
-        <h2><%= t('guardian.help_title') %></h2>
-        <span id="help-count" class="g-badge" hidden></span>
-      </div>
-      <p class="g-sec-sub"><%= t('guardian.help_sub') %></p>
-      <div id="help-list" class="g-list"></div>
-      <p id="help-empty" class="g-empty"><%= t('guardian.help_empty') %></p>
-    </section>
-
-    <!-- 2. Nieuwe ward adopteren. -->
-    <section id="adopt-section">
-      <div class="g-sec-head"><h2><%= t('guardian.adopt_title') %></h2></div>
-      <p class="g-sec-sub"><%= t('guardian.adopt_sub') %></p>
-      <form id="adopt-form" autocomplete="off">
-        <input id="adopt-handle" type="text" inputmode="email" autocomplete="off"
-               placeholder="@kind@server.eu" aria-label="<%= t('guardian.adopt_label') %>">
-        <button type="submit" id="adopt-btn"><%= t('guardian.adopt_btn') %></button>
-      </form>
-      <p id="adopt-msg" class="g-msg" hidden></p>
-    </section>
-
-    <!-- 3. Verzonden aanvragen (nog niet beantwoord). -->
-    <section id="pending-section" hidden>
-      <div class="g-sec-head"><h2><%= t('guardian.pending_title') %></h2></div>
-      <p class="g-sec-sub"><%= t('guardian.pending_sub') %></p>
-      <div id="pending-list" class="g-list"></div>
-    </section>
-
-    <!-- 4. Mijn wards. Elk kind is een regel die opent naar een paneel met
-            alles over dat kind: instellingen, volgverzoeken, hulpvragen en
-            recente berichten. Een guardian denkt per kind, niet per functie. -->
-    <section id="wards-section">
-      <div class="g-sec-head"><h2><%= t('guardian.wards_title') %></h2></div>
-      <div id="wards-list" class="g-list"></div>
-      <p id="wards-empty" class="g-empty"><%= t('guardian.wards_empty') %></p>
-    </section>
-
-    <!-- 4b. Even afwezig (FEP-633c 3.6.1): de verantwoordelijke handeling.
-            Wie zich afmeldt telt niet mee in de besluiten, en een enkel
-            antwoord brengt je meteen terug. Alleen zichtbaar met wards. -->
-    <section id="away-section" hidden>
-      <div class="g-sec-head"><h2><%= t('guardian.away_title') %></h2></div>
-      <p class="g-sec-sub"><%= t('guardian.away_sub') %></p>
-      <div class="g-away-row">
-        <button id="away-week" class="quiet small"><%= t('guardian.away_week') %></button>
-        <button id="away-month" class="quiet small"><%= t('guardian.away_month') %></button>
-      </div>
-      <p id="away-msg" class="g-msg" hidden></p>
-    </section>
-
-    <!-- 5. Meldingen. -->
-    <section id="push-section">
-      <div class="g-sec-head"><h2><%= t('guardian.push_title') %></h2></div>
-      <p class="g-sec-sub"><%= t('guardian.push_sub') %></p>
-      <button id="push-toggle" class="quiet" data-on-label="<%= t('guardian.push_off') %>"
-              data-off-label="<%= t('guardian.push_on') %>"><%= t('guardian.push_on') %></button>
-      <p id="push-msg" class="g-msg" hidden></p>
-    </section>
-
-    <!-- 6. Het logboek (FEP-633c §4.2). Onderaan en ingeklapt, want hier vraagt
-         niets om een antwoord: het staat er zodat een besluit -- en vooral de
-         REDEN ervan -- ergens na te lezen is. Zonder dit merkte een ward een
-         weigering alleen doordat er iets uit een lijst verdween. -->
-    <section id="log-section">
-      <div class="g-sec-head"><h2><%= t('guardian.panel_history') %></h2></div>
-      <div id="g-log"></div>
-    </section>
-  </main>
-
-  <script type="application/json" id="guardian-state"><%- JSON.stringify(state) %></script>
-  <script src="/guardian/app.js" defer></script>
-</body>
-</html>
Index: src/views/pages/home.ejs
===================================================================
--- src/views/pages/home.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/home.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -11,5 +11,4 @@
 
 const allPosts = [...pinnedPosts, ...posts];
-const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
 %>
 
@@ -18,18 +17,4 @@
     non-admin page. %>
 
-<%# FEP-7628 slice 3: this account moved. The signpost comes FIRST, before
-    any post: a visitor should not scroll a dead feed to find out we left.
-    typeof-guard because an old route may render this template for one
-    request right after a deploy (view cache fills on first hit). %>
-<% if (typeof movedTo !== 'undefined' && movedTo) { %>
-  <div class="container">
-    <aside class="moved-banner">
-      <p class="moved-banner-lead"><%= t('phome.moved_lead') %></p>
-      <a class="moved-banner-link" href="<%= movedTo %>" rel="me noopener"><%= movedToLabel || movedTo %></a>
-      <p class="moved-banner-hint"><%= t('phome.moved_hint') %></p>
-    </aside>
-  </div>
-<% } %>
-
 <!-- ========== TIMELINE ========== -->
 <div class="container feed-timeline">
@@ -37,12 +22,12 @@
   <% if (allPosts.length === 0) { %>
     <div class="empty">
-      <h1><%= t('phome.empty_title') %></h1>
-      <p><%= t('phome.empty_sub') %></p>
-      <% if (user && canMutate && permissions.canCreatePost(user, site)) { %>
+      <h1>Hier is het nog stil.</h1>
+      <p>Nog geen posts. Spannend.</p>
+      <% if (user && permissions.canCreatePost(user, site)) { %>
         <p style="margin-top:2rem">
-          <a class="btn" href="<%= _base %>/posts/new"
-             hx-get="<%= _base %>/posts/new?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
-             hx-push-url="<%= _base %>/posts/new" hx-indicator="#pcms-loading">
-            <%= t('phome.write_first') %> →
+          <a class="btn" href="/posts/new"
+             hx-get="/posts/new?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
+             hx-push-url="/posts/new" hx-indicator="#pcms-loading">
+            Schrijf je eerste post →
           </a>
         </p>
@@ -50,5 +35,5 @@
     </div>
   <% } else { %>
-    <ul class="post-list feed-timeline-list" id="post-list">
+    <ul class="post-list feed-timeline-list">
       <% allPosts.forEach(function(post) { %>
         <%- include('../partials/post-card', { post: post }) %>
@@ -59,41 +44,8 @@
 </div>
 
-<!-- ========== LEZEN (alleen zichtbaar bij body[data-feed-view="reader"]) ==========
-     Hetzelfde stel berichten, maar heel: één per scherm, met de grenzen door
-     CSS-snapping. Geen eigen route en geen eigen shell -- de balken blijven
-     staan, want je bent nog gewoon op de feed en moet terug kunnen schakelen.
-     Leeg? Dan valt de lege staat van de tijdlijn hierboven in beeld; deze
-     sectie rendert dan niet. -->
-<% if (typeof readerItems !== 'undefined' && readerItems && readerItems.length) { %>
-<section class="feed-reader" aria-label="<%= t('switch.reader') %>">
-  <%# Terug naar boven. Staat IN de HTML en niet in JavaScript, zodat hij er ook
-      is als de module niet laadt -- dan doet hij niets, maar hij springt ook niet
-      opeens in beeld. Verschijnt pas als je ver genoeg bent (mod/read.js). %>
-  <%# Een bericht omhoog of omlaag. Alleen in paginamodus zichtbaar (CSS). %>
-  <%# Twee echte balken over de volle breedte. Tikzones zonder zichtbaar element
-      werkten niet: de onderste strook ligt onder de mini-speler en de tabbalk,
-      en die vangen de tik voordat de leesstroom hem ziet. Een balk kan daar wel
-      bovenop. %>
-  <button type="button" class="read-bar read-bar--top" id="read-prev" aria-label="<%= t('read.prev') %>">
-    <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="18 15 12 9 6 15"/></svg>
-  </button>
-  <button type="button" class="read-bar read-bar--bottom" id="read-next-nav" aria-label="<%= t('read.next') %>">
-    <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
-  </button>
-  <button type="button" id="read-top" class="read-top" aria-label="<%= t('read.to_top') %>" title="<%= t('read.to_top') %>">
-    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
-  </button>
-  <div class="read-stream" id="read-stream" data-base="<%= _base %>">
-    <% readerItems.forEach(function(it) { %>
-      <%- include('../partials/read-article', { post: it.post, entry: it.entry }) %>
-    <% }); %>
-  </div>
-</section>
-<% } %>
-
 <!-- ========== GRID (only visible when body[data-feed-view="grid"]) ========== -->
 <% if (allPosts.length > 0) { %>
-<section class="feed-grid container" aria-label="<%= t('phome.grid_view') %>">
-  <div class="grid-tiles" id="grid-tiles">
+<section class="feed-grid container" aria-label="Grid-weergave">
+  <div class="grid-tiles">
     <% allPosts.forEach(function(post) { %>
       <%- include('../partials/post-tile', { post: post }) %>
@@ -102,7 +54,2 @@
 </section>
 <% } %>
-<% if (typeof hasMore !== 'undefined' && allPosts.length > 0) { %>
-<div class="container feed-more-wrap">
-  <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#post-list', morePath: '/' }) %>
-</div>
-<% } %>
Index: src/views/pages/linkbio.ejs
===================================================================
--- src/views/pages/linkbio.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,44 +1,0 @@
-<%
-  var links = (typeof lbLinks !== 'undefined') ? lbLinks : [];
-  var _photo = (site && site.profile_photo) ? site.profile_photo : (typeof siteOwnerAvatar !== 'undefined' ? siteOwnerAvatar : '');
-  var _name = (site && (site.profile_name || site.title)) || '';
-  var _bio = (site && (site.profile_bio || site.tagline)) || '';
-%>
-<section class="lb">
-  <div class="lb-head">
-    <% if (_photo) { %><span class="lb-photo" style="background-image:url('<%= _photo %>')"></span><% } else { %><span class="lb-photo lb-photo-empty">♪</span><% } %>
-    <h1 class="lb-name"><%= _name %></h1>
-    <% if (_bio) { %><p class="lb-bio"><%= _bio %></p><% } %>
-  </div>
-
-  <% if (!links.length) { %>
-    <p class="lb-empty"><%= t('lbio.empty') %></p>
-  <% } else { %>
-    <div class="lb-links">
-      <% links.forEach(function(l){ %>
-        <a class="lb-link" href="<%= siteUrlBase %>/links/go/<%= l.i %>" target="_blank" rel="noopener" <%= l.brand ? ('style="--lb-brand:' + l.brand + '"') : '' %>>
-          <% if (l.svg) { %><span class="lb-ico"><%- l.svg %></span><% } %>
-          <span class="lb-label"><%= l.label %></span>
-        </a>
-      <% }); %>
-    </div>
-  <% } %>
-
-  <p class="lb-foot"><a href="<%= siteUrlBase %>/">← <%= t('lbio.back_to_site') %></a></p>
-</section>
-
-<style>
-  .lb { max-width: 480px; margin: 0 auto; padding: 40px 18px 64px; text-align: center; }
-  .lb-photo { display: inline-block; width: 96px; height: 96px; border-radius: 50%; background-size: cover; background-position: center; margin-bottom: 14px; }
-  .lb-photo-empty { background: var(--accent,#6b8f71); color: #fff; display: grid; place-items: center; font-size: 36px; }
-  .lb-name { font-size: 24px; margin: 0 0 6px; }
-  .lb-bio { opacity: .8; margin: 0 0 26px; }
-  .lb-links { display: flex; flex-direction: column; gap: 12px; }
-  .lb-link { display: flex; align-items: center; gap: 12px; justify-content: center; padding: 14px 18px; border-radius: 14px; border: 1px solid rgba(128,128,128,.28); text-decoration: none; color: inherit; font-weight: 600; transition: transform .08s ease, border-color .15s ease; }
-  .lb-link:hover { transform: translateY(-1px); border-color: var(--lb-brand, var(--accent,#6b8f71)); }
-  .lb-ico { width: 22px; height: 22px; display: inline-flex; color: var(--lb-brand, currentColor); }
-  .lb-ico svg { width: 22px; height: 22px; }
-  .lb-empty { opacity: .7; }
-  .lb-foot { margin-top: 30px; font-size: 13px; opacity: .6; }
-  .lb-foot a { color: inherit; }
-</style>
Index: src/views/pages/messages.ejs
===================================================================
--- src/views/pages/messages.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,244 +1,0 @@
-<div class="msg-wrap">
-  <%- include('../partials/fedi-tabs', { active: 'berichten' }) %>
-  <h1 class="msg-title"><%= t('msg.title') %></h1>
-  <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success === 'guardian_accepted' ? t('msg.guard_accepted') : (success === 'guardian_rejected' ? t('msg.guard_rejected') : (success === 'wave_sent' ? t('msg.wave_sent') : (success === 'reply_sent' ? t('msg.reply_sent') : success))) %></div><% } %>
-  <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error === 'guardianship' ? t('msg.guard_failed') : (error === 'reply_empty' ? t('msg.reply_empty') : ((error === 'reply_failed' || error === 'reply_target') ? t('msg.reply_failed') : error)) %></div><% } %>
-
-  <%# FEP-633c: a pending guardianship offer is a special message: the kid
-      answers here (accept/reject travels the same C2S pipeline as the apps). %>
-  <% if (typeof guardianOffers !== 'undefined' && guardianOffers.length) { %>
-    <% guardianOffers.forEach(function(o){ %>
-    <div class="alert" style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;border-left:3px solid #ff6b35;">
-      <span style="font-size:1.4em;">&#128735;</span>
-      <div style="flex:1;min-width:200px;">
-        <strong><%= o['shaer:candidateHandle'] || o['shaer:candidate'] %></strong><br>
-        <span><%= t('msg.guard_offer') %></span>
-      </div>
-      <form method="post" action="<%= (typeof moreBase !== 'undefined' ? moreBase : '') %>/messages/guardianship" style="display:flex;gap:8px;">
-        <input type="hidden" name="offer" value="<%= o.id %>">
-        <button class="btn" type="submit" name="answer" value="accept"><%= t('msg.guard_accept') %></button>
-        <button class="btn" type="submit" name="answer" value="reject" style="opacity:.7;"><%= t('msg.guard_reject') %></button>
-      </form>
-    </div>
-    <% }); %>
-  <% } %>
-
-  <div class="msg-filters" role="tablist" aria-label="<%= t('msg.title') %>">
-    <%# Vier chips, geen zes: Berichten, Gesprekken en Verzonden gingen op in
-        een enkele Gesprekken-view, waarin verzonden en ontvangen in dezelfde
-        draad staan. Activiteit en Moderatie blijven wat ze waren. %>
-    <button type="button" class="msg-chip is-on" data-show="all"><%= t('msg.filter_all') %></button>
-    <button type="button" class="msg-chip" data-show="conv"><%= t('msg.filter_conv') %></button>
-    <button type="button" class="msg-chip" data-show="act"><%= t('msg.filter_act') %></button>
-    <button type="button" class="msg-chip" data-show="mod"><%= t('msg.filter_mod') %></button>
-  </div>
-  <div class="msg-search">
-    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
-    <input type="search" id="msg-q" placeholder="<%= t('msg.search_ph') %>" autocomplete="off" spellcheck="false" aria-label="<%= t('msg.search_ph') %>">
-  </div>
-
-  <% var _seen = (typeof seenAt !== 'undefined' && seenAt) ? seenAt : 0; %>
-  <% if (!items || !items.length) { %>
-    <p class="msg-empty"><%= t('msg.empty') %></p>
-  <% } else { %>
-    <ul class="msg-list" data-show="all" id="msg-list">
-      <% items.forEach(function(n){ %><%- include('../partials/msg-item', { n: n, seen: _seen }) %><% }); %>
-    </ul>
-    <p class="msg-nomatch" hidden><%= t('msg.no_match') %></p>
-    <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#msg-list', morePath: '/messages' }) %>
-  <% } %>
-
-  <%# Interact bookmarklet — moved here from the old Reacties page. %>
-  <details class="msg-bm">
-    <summary><%= t('fedi.bm_label') %></summary>
-    <p class="msg-bm-help"><%= t('fedi.bm_help') %></p>
-    <a class="fedi-bm-btn" id="fedi-bm-btn" href="#" draggable="true" title="<%= t('fedi.bm_help') %>">
-      <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
-      <%= t('fedi.bm_label') %>
-    </a>
-  </details>
-</div>
-
-<%# Filteren, zoeken en het in-/uitklappen zitten in assets/js/mod/messages.js.
-    Inline script hier wordt door de CSP geweigerd zodra je deze pagina via een
-    link BINNEN de site opent -- zie shaer-0i6. De shell laadt de module op
-    body[data-js]; deze pagina vraagt erom met pageJs (routes/posts.js). %>
-
-<style>
-  .msg-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
-  .msg-title { margin: 0 0 .9rem; }
-  .msg-empty { color: var(--ink-soft, #888); }
-
-  .msg-filters { display: flex; gap: .4rem; margin: 0 0 1.1rem; flex-wrap: wrap; }
-  .msg-chip { border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: none;
-    color: var(--ink-soft, #888); font: inherit; font-size: .84rem; font-weight: 600;
-    padding: .3rem .85rem; border-radius: 999px; cursor: pointer; }
-  .msg-chip.is-on { background: var(--accent, #06c); border-color: var(--accent, #06c); color: #fff; }
-
-  .msg-search { display: flex; align-items: center; gap: .5rem; margin: 0 0 1.1rem;
-    padding: .45rem .7rem; border-radius: 10px;
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); }
-  .msg-search:focus-within { border-color: var(--accent, #06c); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #06c) 18%, transparent); }
-  .msg-search svg { width: 16px; height: 16px; color: var(--ink-soft, #888); flex-shrink: 0; }
-  .msg-search input { flex: 1; border: none; background: none; color: inherit; font: inherit; outline: none; min-width: 0; }
-  .msg-nomatch { color: var(--ink-soft, #888); text-align: center; padding: 1.2rem 0; }
-
-  .msg-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .55rem; }
-
-
-  .msg-item { display: flex; gap: .8rem; padding: .85rem .95rem; border-radius: 14px; align-items: flex-start;
-    background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent);
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent);
-    transition: border-color .15s ease, box-shadow .15s ease; }
-  .msg-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 30%, transparent);
-    box-shadow: 0 2px 12px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
-  .msg-item.is-new { border-color: color-mix(in srgb, var(--accent, #06c) 45%, transparent);
-    background: color-mix(in srgb, var(--accent, #06c) 4%, transparent); }
-  .msg-sent { border-left: 3px solid color-mix(in srgb, var(--accent, #06c) 55%, transparent); }
-
-  /* ── Gesprekken ──────────────────────────────────────────────────────
-     NA .msg-item, en dat is geen smaak: .msg-item zet display:flex, en bij
-     gelijke specificiteit wint de laatste regel. Stond dit ervoor, dan kwam de
-     draadkop NAAST de bubbels te staan in plaats van erboven. */
-  .msg-thread { display: block; }
-  .msg-thread-head { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-bottom: .4rem; }
-  /* De tegenpartij is een labeltje bij het gesprek, geen kop erboven: een
-     kleine verzonken uitsparing, zodat de bubbels eronder de ruimte krijgen.
-     Het aantal zit in dezelfde uitsparing in plaats van ernaast. */
-  /* Ook de knop die het gesprek in- en uitklapt. Een button erft geen font en
-     geen kleur, dus die staan hier expliciet; zonder dat wordt het ineens
-     systeemblauw in 13px. */
-  .msg-thread-who { display: inline-flex; align-items: center; gap: .3rem; min-width: 0;
-    font: inherit; font-size: .78em; line-height: 1.5; font-weight: 500;
-    padding: .05rem .5rem; border-radius: 999px; border: 0; cursor: pointer;
-    color: color-mix(in srgb, var(--ink, #000) 62%, transparent);
-    background: color-mix(in srgb, var(--ink, #000) 5%, transparent);
-    box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink, #000) 9%, transparent);
-    max-width: 100%; overflow: hidden; white-space: nowrap;
-    /* Ruimer aanraakvlak dan de tekst: een chip van 18px hoog is op een telefoon
-       niet te raken. De uitsparing blijft klein, het doel wordt groter. */
-    min-height: 32px; }
-  .msg-thread-who:hover { background: color-mix(in srgb, var(--ink, #000) 9%, transparent); }
-  .msg-thread-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-  .msg-thread-chevron { width: 12px; height: 12px; flex: none; opacity: .55;
-    transition: transform .18s ease; }
-  /* Ingeklapt: de bubbels en het antwoordveld verdwijnen, de uitsparing met de
-     naam en het AANTAL blijft staan -- dat aantal is juist ingeklapt het meest
-     waard. */
-  .msg-thread.is-collapsed .msg-thread-msgs,
-  .msg-thread.is-collapsed .msg-thread-actions { display: none; }
-  /* Tijdens het zoeken wint de treffer van de dichtgeklapte stand. */
-  .msg-thread.is-collapsed.is-search-open .msg-thread-msgs { display: flex; }
-  .msg-thread.is-collapsed.is-search-open .msg-thread-actions { display: flex; }
-  .msg-thread.is-collapsed .msg-thread-chevron { transform: rotate(-90deg); }
-  .msg-thread.is-collapsed .msg-thread-head { margin-bottom: 0; }
-  @media (prefers-reduced-motion: reduce) { .msg-thread-chevron { transition: none; } }
-  /* De zwaai in de kop: klein, en met dezelfde rust als het aantal ernaast.
-     Hij staat er ook als het gesprek open is -- dan zegt de bubbel het al, maar
-     een kop die van stand verandert leest als iets dat gebeurde. */
-  .msg-thread-wave { flex: 0 0 auto; font-size: .95em; line-height: 1; }
-  .msg-thread-count { font-variant-numeric: tabular-nums; opacity: .75; flex: 0 0 auto;
-    padding-inline-start: .3rem; border-inline-start: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent); }
-  .msg-thread-post { display: inline-flex; align-items: center; gap: .3rem; font-size: .85em;
-    margin-inline-start: auto; min-width: 0; max-width: 100%;
-    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-  .msg-thread-post svg { width: 13px; height: 13px; flex: none; }
-  .msg-thread-msgs { list-style: none; margin: 0; padding: 0 0 0 .7rem; display: flex; flex-direction: column; gap: .5rem;
-    border-inline-start: 2px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); }
-  .msg-sub { display: flex; gap: .6rem; align-items: flex-start; min-width: 0; }
-  .msg-sub > .msg-body { min-width: 0; }
-  /* Jouw eigen bijdrage onderscheidt zich met een tint en de accentlijn die
-     .msg-sent al had -- niet met row-reverse: dat duwde de avatar buiten de
-     kaart zodra de regel smaller werd dan zijn inhoud. */
-  .msg-sub.msg-sent { border-radius: 10px; padding: .3rem .45rem;
-    background: color-mix(in srgb, var(--accent, #06c) 5%, transparent); }
-  /* Een lange naam of tijd mag de bubbel niet uit de kaart duwen. */
-  .msg-sub .msg-line { flex-wrap: wrap; }
-
-  /* Antwoorden en zwaaien naast elkaar, ingeklapt, en sinds 25-8 BOVEN de
-     berichten -- daar staat het nieuwste, en daar antwoord je dus op. Heette
-     msg-thread-foot toen het onderaan stond; de marge verhuisde mee van top
-     naar bottom, anders plakt de rij tegen de eerste bubbel.
-     Een open editor onder elk gesprek maakt de lijst weer onleesbaar -- precies
-     wat deze weergave moest oplossen. */
-  .msg-thread-actions { display: flex; align-items: flex-start; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
-  .msg-reply { flex: 1 1 12rem; min-width: 0; }
-  .msg-reply > summary { cursor: pointer; font-size: .82rem; color: var(--ink-soft, #888); }
-  .msg-reply[open] > summary { margin-bottom: .4rem; }
-  .msg-reply-form { min-width: 0; }
-  .msg-wave button { line-height: 1.2; }
-
-  /* Zwaaien gebeurt ter plekke (mod/messages.js). Twee toestanden na de klik:
-     gelukt, dan vervangt de bevestiging de knoppen -- en mislukt, dan komen de
-     knoppen terug met een schud, zodat een tweede poging voor de hand ligt.
-     De bestaande klasse `nudge` kon hier niet voor dienen: die heeft alleen
-     CSS op authorize-interaction, dus op deze pagina zou hij niets doen. */
-  .msg-wave-done { font-size: .82rem; color: var(--ink-soft, #888); white-space: nowrap; }
-  .msg-wave.is-failed, .msg-quickreply.is-failed { animation: msg-wave-shake .4s ease; }
-  @keyframes msg-wave-shake {
-    0%, 100% { transform: translateX(0); }
-    25% { transform: translateX(-4px); }
-    75% { transform: translateX(4px); }
-  }
-  @media (prefers-reduced-motion: reduce) {
-    .msg-wave.is-failed, .msg-quickreply.is-failed { animation: none; outline: 2px solid var(--accent); }
-  }
-
-  @media (max-width: 560px) {
-    /* Op een telefoon is 40px avatar per bubbel puur verlies: de naam staat er
-       al naast, en de inhoud heeft de breedte harder nodig. */
-    .msg-sub > .msg-av { display: none; }
-    .msg-thread-msgs { padding-inline-start: .55rem; }
-    .msg-thread-post { margin-inline-start: 0; }
-    .msg-item { padding: .7rem .6rem; gap: .6rem; }
-  }
-
-  .msg-av { position: relative; flex: 0 0 40px; width: 40px; height: 40px; border-radius: 50%;
-    display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
-    background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); overflow: visible; }
-  .msg-av img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; background: #fff; }
-  .msg-av-sent svg { width: 17px; height: 17px; }
-  .msg-dot { position: absolute; right: -3px; bottom: -3px; width: 18px; height: 18px; border-radius: 50%;
-    display: inline-flex; align-items: center; justify-content: center;
-    border: 2px solid var(--paper, #fff); background: var(--accent, #06c); color: #fff; }
-  .msg-dot svg { width: 10px; height: 10px; }
-  .msg-dot-like { background: #e8b04b; } .msg-dot-boost { background: #2fa85a; }
-  .msg-dot-report { background: #c0392b; } .msg-dot-mention { background: #8e6cf0; }
-
-  .msg-body { flex: 1; min-width: 0; }
-  .msg-line { display: flex; align-items: baseline; gap: .4rem; }
-  .msg-who { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-  a.msg-who:hover { text-decoration: underline; }
-  .msg-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
-  .msg-new { width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #06c); flex: 0 0 auto; align-self: center; }
-  .msg-time { color: var(--ink-soft, #999); font-size: .76rem; margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
-  .msg-what { color: var(--ink-soft, #aaa); margin-top: .12rem; line-height: 1.4; }
-  .msg-what .msg-post { color: var(--accent, #06c); font-weight: 600; margin-left: .25rem; }
-  .msg-priv { display: inline-block; margin-left: .3rem; padding: .05rem .5rem; border-radius: 999px; font-size: .72rem; font-weight: 700;
-    background: color-mix(in srgb, #8e6cf0 14%, transparent); color: #8e6cf0; }
-  .msg-content { margin: .45rem 0 0; padding: .55rem .75rem; border-radius: 10px; line-height: 1.5;
-    background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, inherit); overflow-wrap: anywhere; }
-  .msg-content p { margin: .2rem 0; } .msg-content p:first-child { margin-top: 0; } .msg-content p:last-child { margin-bottom: 0; }
-
-  .msg-poll { margin: .5rem 0 0; display: flex; flex-direction: column; gap: .4rem; }
-  .msg-poll-opt { display: flex; flex-direction: column; gap: .2rem; }
-  .msg-poll-top { display: flex; justify-content: space-between; gap: .5rem; font-size: .85rem; }
-  .msg-poll-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-  .msg-poll-pct { font-variant-numeric: tabular-nums; color: var(--ink-soft, #888); flex-shrink: 0; }
-  .msg-poll-bar { height: 7px; border-radius: 999px; overflow: hidden; background: color-mix(in srgb, var(--ink, #000) 10%, transparent); }
-  .msg-poll-bar span { display: block; height: 100%; border-radius: 999px; background: var(--accent, #06c); }
-  .msg-poll-total { font-size: .78rem; color: var(--ink-soft, #888); margin-top: .1rem; }
-
-  .msg-actions { display: flex; gap: .7rem; align-items: center; margin-top: .45rem; }
-  .msg-edit summary { cursor: pointer; font-size: .8rem; color: var(--ink-soft, #888); font-weight: 600; }
-  .msg-edit-form { margin-top: .4rem; display: flex; flex-direction: column; gap: .4rem; }
-  .msg-edit-form textarea { width: 100%; font: inherit; padding: .5rem .65rem; border-radius: 8px;
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: transparent; color: var(--ink, inherit); }
-  .msg-del { background: none; border: none; padding: 0; font: inherit; font-size: .8rem; font-weight: 600;
-    color: var(--ink-soft, #888); cursor: pointer; }
-  .msg-del:hover { color: #c0392b; }
-
-  .msg-bm { margin: 1.4rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); padding-top: .8rem; }
-  .msg-bm summary { cursor: pointer; color: var(--ink-soft, #888); font-weight: 600; font-size: .88rem; }
-  .msg-bm-help { color: var(--ink-soft, #888); font-size: .82rem; line-height: 1.5; margin: .5rem 0 .6rem; }
-</style>
Index: src/views/pages/my-site.ejs
===================================================================
--- src/views/pages/my-site.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,73 +1,0 @@
-<div class="container admin-page">
-  <p><a href="/" class="btn">&larr; <%= t('myst.overview') %></a></p>
-  <h1><%= t('myst.title') %></h1>
-  <p class="admin-tagline"><%= mySite.title %></p>
-
-  <nav class="admin-quick-links" aria-label="<%= t('myst.quick_links_aria') %>">
-    <a href="/user/<%= mySite.slug %>/posts/new" class="btn">✍️ <%= t('myst.new_post') %></a>
-    <a href="/admin/sites/<%= mySite.slug %>/edit" class="btn">🎨 <%= t('myst.appearance') %></a>
-    <a href="/user/<%= mySite.slug %>" class="btn">👁️ <%= t('myst.view_site') %></a>
-    <a href="/account" class="btn">👤 <%= t('myst.account') %></a>
-  </nav>
-
-  <div class="admin-stats">
-    <div class="stat-card"><div class="stat-num"><%= mine.posts %></div><div class="stat-label"><%= t('myst.posts') %></div></div>
-    <div class="stat-card"><div class="stat-num"><%= mine.published %></div><div class="stat-label"><%= t('myst.published') %></div></div>
-  </div>
-
-  <% if (typeof posts !== 'undefined' && posts && posts.length) { %>
-    <% var _drafts = posts.filter(function(p){ return p.isDraft; }).length; %>
-    <%
-      var _lblBadge = t('myst.draft_badge');
-      var _lblUntitled = t('myst.untitled');
-      var _lblEdit = t('myst.edit');
-      var _lblView = t('myst.view');
-    %>
-    <h2 class="ms-posts-title"><%= t('myst.posts') %><% if (_drafts) { %> <span class="pl-draftcount"><%= _drafts === 1 ? t('myst.draft_count_one', { n: _drafts }) : t('myst.draft_count_many', { n: _drafts }) %></span><% } %></h2>
-    <ul class="post-admin-list">
-      <% posts.forEach(function(p) { %>
-        <li class="pal-row<%= p.isDraft ? ' is-draft' : '' %>">
-          <a class="pal-title" href="<%= p.editUrl %>">
-            <% if (p.isDraft) { %><span class="pal-badge"><%= _lblBadge %></span><% } %>
-            <span class="pal-name"><%= p.title || _lblUntitled %></span>
-          </a>
-          <span class="pal-actions">
-            <a class="pal-link" href="<%= p.editUrl %>"><%= _lblEdit %></a>
-            <% if (!p.isDraft) { %><a class="pal-link" href="<%= p.viewUrl %>"><%= _lblView %></a><% } %>
-          </span>
-        </li>
-      <% }); %>
-    </ul>
-  <% } %>
-</div>
-
-<style>
-.admin-page { max-width: 1000px; margin: 3rem auto; padding: 0 1rem; }
-.admin-page h1 { font-family: var(--font-display, serif); font-size: 2rem; margin: 0 0 0.25rem; }
-.admin-tagline { color: var(--ink-muted); margin: 0 0 2rem; }
-.admin-quick-links {
-  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
-  gap: 0.5rem; margin: 0 0 1.5rem; padding: 0;
-}
-.admin-quick-links .btn { justify-content: center; padding: 0.7rem 0.9rem; font-weight: 600; text-align: center; }
-.admin-quick-links .btn:hover { background: color-mix(in srgb, var(--accent) 8%, var(--paper-2)); border-color: var(--accent); }
-.admin-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.75rem; }
-.stat-card { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 8px; padding: 1rem 1.25rem; text-align: center; }
-.stat-num { font-size: 2rem; font-weight: 700; color: var(--accent); line-height: 1; font-family: var(--font-display, serif); }
-.stat-label { color: var(--ink-muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.25rem; }
-
-/* Posts/concepten-lijst */
-.ms-posts-title { font-family: var(--font-display, serif); font-size: 1.4rem; margin: 2rem 0 0.75rem; }
-.pl-draftcount { font-family: var(--font-ui, system-ui); font-size: 0.8rem; font-weight: 700; color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); padding: 0.1rem 0.55rem; border-radius: 99px; vertical-align: middle; }
-.post-admin-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
-.pal-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.7rem 0.9rem; border: 1px solid var(--rule); border-radius: 10px; background: var(--paper-2); transition: border-color 120ms; }
-.pal-row:hover { border-color: var(--accent); }
-.pal-row.is-draft { border-left: 3px solid var(--accent); }
-.pal-title { display: inline-flex; align-items: center; gap: 0.6rem; min-width: 0; color: var(--ink); text-decoration: none; font-weight: 600; }
-.pal-title:hover { color: var(--accent); }
-.pal-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.pal-badge { flex-shrink: 0; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); padding: 0.1rem 0.5rem; border-radius: 99px; }
-.pal-actions { display: flex; gap: 0.75rem; flex-shrink: 0; }
-.pal-link { color: var(--ink-muted); font-size: 0.85rem; text-decoration: none; white-space: nowrap; }
-.pal-link:hover { color: var(--accent); text-decoration: underline; }
-</style>
Index: src/views/pages/news.ejs
===================================================================
--- src/views/pages/news.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,163 +1,0 @@
-<div class="tl-wrap">
-  <%- include('../partials/fedi-tabs', { active: 'feed' }) %>
-  <div class="tl-titlebar">
-    <h1 class="tl-title"><%= t('tl.title') %></h1>
-    <button type="button" id="tl-paste-btn" class="tl-paste-btn" title="<%= t('fedi.remote_interact') %>" aria-label="<%= t('fedi.remote_interact') %>">
-      <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
-    </button>
-  </div>
-  <% if (typeof movedTo !== 'undefined' && movedTo) { %>
-    <%# Eén uitleg bovenaan in plaats van een tooltip per knop: een tooltip op een
-        uitgeschakelde knop is toch onzichtbaar. Lezen blijft werken, dus de Krant
-        zelf staat er gewoon onder. %>
-    <div class="alert alert-error tl-moved">
-      <strong><%= t('tl.moved_title') %></strong>
-      <p><%= t('tl.moved_lead') %> <a href="<%= movedTo %>"><%= movedTo %></a></p>
-      <p class="form-hint"><%= t('tl.moved_hint') %></p>
-    </div>
-  <% } %>
-  <p class="tl-lead"><%= t('tl.lead') %></p>
-
-  <div class="tl-paste" id="tl-paste">
-    <div class="tl-paste-card" role="dialog" aria-label="<%= t('fedi.remote_interact') %>">
-      <button type="button" class="tl-paste-x" aria-label="<%= t('fedi.cancel') %>">&times;</button>
-      <h3 class="tl-paste-title"><%= t('fedi.remote_interact') %></h3>
-      <input type="url" class="tl-paste-input" inputmode="url" autocomplete="off" autocapitalize="none" spellcheck="false" placeholder="<%= t('tl.paste_ph') %>">
-      <div class="tl-paste-actions">
-        <button type="button" class="tl-paste-cancel"><%= t('fedi.cancel') %></button>
-        <button type="button" class="tl-paste-go"><%= t('tl.paste_go') %></button>
-      </div>
-    </div>
-  </div>
-  <style>
-    .tl-titlebar { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
-    .tl-boost-by { display: flex; align-items: center; gap: .35rem; font-size: .8rem; color: var(--ink-soft); margin: 0 0 .45rem .2rem; }
-    .tl-boost-by svg { color: #16a34a; flex: 0 0 auto; }
-    .tl-boost-by strong { color: var(--ink); font-weight: 600; }
-    .tl-paste-btn { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px;
-      border: 1px solid var(--rule); border-radius: 999px; background: var(--paper-2); color: var(--ink-soft); cursor: pointer; transition: color .15s, border-color .15s, background .15s; }
-    .tl-paste-btn:hover { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
-    .tl-paste { position: fixed; inset: 0; z-index: 2147483601; display: none; align-items: center; justify-content: center; padding: 1rem; background: rgba(0,0,0,.55); }
-    .tl-paste.is-open { display: flex; }
-    .tl-paste-card { position: relative; width: 100%; max-width: 460px; background: var(--paper); color: var(--ink); border: 1px solid var(--rule);
-      border-radius: 16px; padding: 1.4rem 1.4rem 1.2rem; box-shadow: 0 18px 50px rgba(0,0,0,.4); }
-    .tl-paste-x { position: absolute; top: .5rem; right: .6rem; width: 2rem; height: 2rem; border: 0; border-radius: 999px; background: transparent; color: var(--ink-soft); font-size: 1.4rem; line-height: 1; cursor: pointer; }
-    .tl-paste-title { margin: 0 0 .8rem; font-size: 1.1rem; }
-    .tl-paste-input { width: 100%; box-sizing: border-box; padding: .6rem .8rem; border: 1.5px solid var(--rule); border-radius: 10px; background: var(--paper-2); color: var(--ink); font: inherit; font-size: .95rem; }
-    .tl-paste-input:focus { outline: none; border-color: var(--accent); }
-    .tl-paste-actions { display: flex; justify-content: flex-end; gap: .6rem; margin-top: 1rem; }
-    .tl-paste-actions button { padding: .5rem 1.1rem; border-radius: 999px; font: inherit; font-weight: 600; font-size: .88rem; cursor: pointer; border: 0; }
-    .tl-paste-cancel { background: transparent; border: 1.5px solid var(--rule); color: var(--ink); }
-    .tl-paste-go { background: var(--accent); color: var(--paper); }
-  </style>
-  <%# Het script staat in assets/js/mod/news.js; de servergegevens gaan via partials/page-data.ejs (shaer-bqr). %>
-  <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
-  <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
-
-  <% if (!timeline || !timeline.length) { %>
-    <div class="tl-empty">
-      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
-      <p><%= t('tl.empty') %></p>
-    </div>
-  <% } else { %>
-    <ol class="tl-feed" id="tl-feed">
-      <% timeline.forEach(function(p){ %>
-        <%- include('../partials/tl-item', { p: p }) %>
-      <% }); %>
-    </ol>
-    <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#tl-feed', morePath: '/news' }) %>
-  <% } %>
-</div>
-
-<style>
-  .tl-wrap { max-width: 640px; margin: 1rem auto 3rem; padding: 0 1rem; }
-  .tl-title { margin: 0 0 .15rem; font-size: clamp(1.5rem, 4vw, 1.8rem); }
-  .tl-lead { color: var(--ink-soft, #888); margin: 0 0 1.5rem; }
-  .tl-empty { text-align: center; color: var(--ink-soft, #888); margin: 3rem auto; }
-  .tl-empty svg { width: 44px; height: 44px; color: color-mix(in srgb, var(--accent, #888) 60%, transparent); margin: 0 auto .75rem; display: block; }
-
-  .tl-feed { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 1rem; }
-  .tl-item { padding: 1.1rem 1.15rem; border-radius: 16px;
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 8%, transparent);
-    background: color-mix(in srgb, var(--ink, #000) 2.5%, transparent);
-    transition: border-color .15s ease, box-shadow .15s ease; }
-  .tl-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 32%, transparent);
-    box-shadow: 0 2px 14px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
-
-  .tl-head { display: flex; align-items: center; gap: .65rem; margin: 0 0 .65rem; }
-  .tl-avatar { flex: 0 0 44px; width: 44px; height: 44px; border-radius: 50%; overflow: hidden;
-    display: inline-flex; align-items: center; justify-content: center; font-weight: 700; font-size: 1.1rem;
-    background: color-mix(in srgb, var(--accent, #888) 22%, transparent); color: var(--accent, #555);
-    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #888) 18%, transparent); }
-  .tl-avatar img { width: 100%; height: 100%; object-fit: cover; background: #fff; }
-  .tl-id { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
-  .tl-author { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-  .tl-author:hover { text-decoration: underline; }
-  .tl-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-  .tl-time { color: var(--ink-soft, #999); font-size: .78rem; flex: 0 0 auto; align-self: flex-start; white-space: nowrap; }
-
-  /* .tl-content, .tl-quote* and .tl-media* now live in partials/shared-styles,
-     next to partials/note-body: the markup is shared with Berichten and the
-     Guardian PWA, so the styling has to be too. What stays here is Krant-only. */
-  /* Long posts collapse to a max height with a fade + "read more" (added by JS only when it overflows). */
-  .tl-content.tl-clamp { max-height: 20em; overflow: hidden;
-    -webkit-mask-image: linear-gradient(180deg, #000 72%, transparent);
-    mask-image: linear-gradient(180deg, #000 72%, transparent); }
-  .tl-readmore { display: inline-block; margin: .15rem 0 .1rem; padding: .15rem 0;
-    background: none; border: 0; color: var(--accent, #06c); font: inherit; font-weight: 600;
-    font-size: .85rem; cursor: pointer; }
-  .tl-readmore:hover { text-decoration: underline; }
-
-  .tl-poll { margin: .75rem 0 0; display: flex; flex-direction: column; gap: 8px; }
-  .tl-poll-form { display: flex; flex-direction: column; gap: 8px; }
-  .tl-poll-choice { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--line, rgba(128,128,128,.3)); border-radius: 10px; cursor: pointer; }
-  .tl-poll-choice:hover { border-color: var(--accent); }
-  .tl-poll-choice input { accent-color: var(--accent); }
-  .tl-poll-btn { align-self: flex-start; margin-top: 2px; }
-  .tl-poll-res { position: relative; padding: 9px 12px; border-radius: 10px; overflow: hidden; background: var(--paper-2, rgba(128,128,128,.08)); display: flex; align-items: center; gap: 8px; }
-  .tl-poll-fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent) 22%, transparent); z-index: 0; }
-  .tl-poll-res.is-mine .tl-poll-fill { background: color-mix(in srgb, var(--accent) 40%, transparent); }
-  .tl-poll-name { position: relative; z-index: 1; flex: 1; font-size: .95rem; }
-  .tl-poll-pct { position: relative; z-index: 1; font-variant-numeric: tabular-nums; font-weight: 500; }
-  .tl-poll-foot { font-size: .82rem; color: var(--ink-soft, #888); }
-
-  .tl-embed { margin: .75rem 0 0; border-radius: 12px; overflow: hidden; background: var(--paper-2, #111); }
-  .tl-embed-frame { width: 100%; border: 0; display: block; aspect-ratio: 16 / 9; }
-  .tl-embed-spotify { aspect-ratio: auto; height: 152px; }
-  .tl-embed-sc { aspect-ratio: auto; height: 166px; }
-  .tl-embed-klonkt { aspect-ratio: auto; height: 180px; background: var(--paper, #fff); }
-  .tl-embed-bandcamp { aspect-ratio: auto; height: 470px; }
-  .tl-embed-apple { aspect-ratio: auto; height: 175px; }
-  .tl-embed-open { display: inline-block; margin: .45rem .9rem 0 0; font-size: .82rem; font-weight: 600; color: var(--accent); text-decoration: none; }
-  .tl-embed-open:hover { text-decoration: underline; }
-
-  .tl-orig { display: inline-block; margin: .7rem 0 0; font-size: .82rem; color: var(--accent, #06c); text-decoration: none; }
-  .tl-orig:hover { text-decoration: underline; }
-
-  /* Uniform circular action buttons with coloured icons (no emoji). */
-  .tl-actions { display: flex; gap: .5rem; margin-top: .9rem; align-items: center; }
-  /* Verhuisd account: liken, boosten en reageren worden door de service toch
-     geweigerd, dus toon dat ook. Blokkeren blijft aanklikbaar (veiligheidsklep). */
-  .tl-act[disabled] { opacity: .35; cursor: not-allowed; }
-  .tl-actions.is-moved { position: relative; }
-  .tl-act-form { margin: 0; display: inline-flex; }
-  .tl-act { width: 36px; height: 36px; flex: 0 0 36px; border-radius: 50%; padding: 0; box-sizing: border-box;
-    display: inline-flex; align-items: center; justify-content: center;
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 12%, transparent);
-    background: color-mix(in srgb, var(--ink, #000) 3%, transparent);
-    color: var(--ink-soft, #888); cursor: pointer; -webkit-tap-highlight-color: transparent;
-    transition: background .12s, border-color .12s, transform .1s; }
-  .tl-act svg { width: 18px; height: 18px; display: block; }
-  .tl-act:active { transform: scale(.9); }
-  .tl-act-like svg { color: #e8b04b; }
-  .tl-act-like:hover { background: color-mix(in srgb, #e8b04b 15%, transparent); border-color: color-mix(in srgb, #e8b04b 50%, transparent); }
-  .tl-act-like.is-on { background: color-mix(in srgb, #e8b04b 18%, transparent); border-color: color-mix(in srgb, #e8b04b 60%, transparent); }
-  .tl-act-boost svg { color: #2fa85a; }
-  .tl-act-boost:hover { background: color-mix(in srgb, #2fa85a 15%, transparent); border-color: color-mix(in srgb, #2fa85a 50%, transparent); }
-  .tl-act-boost.is-on { background: color-mix(in srgb, #2fa85a 18%, transparent); border-color: color-mix(in srgb, #2fa85a 60%, transparent); }
-  .tl-act-reply svg { color: var(--accent, #06c); }
-  .tl-act-reply:hover { background: color-mix(in srgb, var(--accent, #888) 14%, transparent); border-color: color-mix(in srgb, var(--accent, #888) 50%, transparent); }
-  .tl-act-block svg { color: #e0524d; }
-  .tl-act-block:hover { background: color-mix(in srgb, #d9534f 14%, transparent); border-color: color-mix(in srgb, #d9534f 50%, transparent); }
-</style>
-<%- include('../partials/page-data', { pageData: { readMore: t('tl.read_more'), showLess: t('tl.show_less') } }) %>
Index: src/views/pages/newsletter.ejs
===================================================================
--- src/views/pages/newsletter.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,54 +1,0 @@
-<%
-  var _site = site || {};
-  var _title = _site.title || t('news.this_artist');
-  var st = (typeof nlState !== 'undefined') ? nlState : 'form';
-%>
-<section class="nl">
-  <div class="nl-card">
-    <% if (st === 'form') { %>
-      <h1 class="nl-h1"><%= t('news.form_title') %></h1>
-      <p class="nl-sub"><%= t('news.form_sub_before') %><strong><%= _title %></strong><%= t('news.form_sub_after') %></p>
-      <form method="POST" action="<%= siteUrlBase %>/nieuwsbrief" class="nl-form">
-        <input type="email" name="email" required placeholder="<%= t('news.email_ph') %>" value="<%= (typeof nlPrefill !== 'undefined') ? nlPrefill : '' %>" autocomplete="email">
-        <button type="submit" class="nl-btn"><%= t('news.subscribe') %></button>
-      </form>
-    <% } else if (st === 'check') { %>
-      <h1 class="nl-h1"><%= t('news.check_title') %></h1>
-      <p class="nl-sub"><%= t('news.check_sub_before') %><strong><%= (typeof nlEmail!=='undefined')?nlEmail:t('news.your_address') %></strong><%= t('news.check_sub_after') %></p>
-    <% } else if (st === 'done') { %>
-      <h1 class="nl-h1"><%= t('news.done_title') %></h1>
-      <p class="nl-sub"><%= t('news.done_sub_before') %><strong><%= _title %></strong><%= t('news.done_sub_after') %></p>
-    <% } else if (st === 'confirmed') { %>
-      <h1 class="nl-h1"><%= t('news.confirmed_title') %></h1>
-      <p class="nl-sub"><%= t('news.confirmed_sub_before') %><strong><%= _title %></strong><%= t('news.confirmed_sub_after') %></p>
-    <% } else if (st === 'unsubbed') { %>
-      <h1 class="nl-h1"><%= t('news.unsubbed_title') %></h1>
-      <p class="nl-sub"><%= t('news.unsubbed_sub') %></p>
-    <% } else if (st === 'invalid') { %>
-      <h1 class="nl-h1"><%= t('news.invalid_title') %></h1>
-      <p class="nl-sub"><%= t('news.invalid_sub') %></p>
-      <p><a class="nl-btn nl-btn-ghost" href="<%= siteUrlBase %>/nieuwsbrief"><%= t('news.back') %></a></p>
-    <% } else if (st === 'smtperror') { %>
-      <h1 class="nl-h1"><%= t('news.smtperror_title') %></h1>
-      <p class="nl-sub"><%= t('news.smtperror_sub') %></p>
-    <% } else if (st === 'badtoken') { %>
-      <h1 class="nl-h1"><%= t('news.badtoken_title') %></h1>
-      <p class="nl-sub"><%= t('news.badtoken_sub') %></p>
-      <p><a class="nl-btn nl-btn-ghost" href="<%= siteUrlBase %>/nieuwsbrief"><%= t('news.to_subscribe') %></a></p>
-    <% } else { %>
-      <h1 class="nl-h1"><%= t('news.error_title') %></h1>
-      <p class="nl-sub"><%= t('news.error_sub') %></p>
-    <% } %>
-  </div>
-</section>
-
-<style>
-  .nl { max-width: 560px; margin: 0 auto; padding: 48px 18px; }
-  .nl-card { border: 1px solid rgba(128,128,128,.2); border-radius: 18px; padding: 32px 28px; }
-  .nl-h1 { font-size: clamp(24px, 5vw, 34px); margin: 0 0 10px; }
-  .nl-sub { font-size: 15.5px; line-height: 1.6; opacity: .85; margin: 0 0 20px; }
-  .nl-form { display: flex; gap: 10px; flex-wrap: wrap; }
-  .nl-form input { flex: 1 1 220px; padding: 12px 14px; border-radius: 10px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; font-size: 15px; }
-  .nl-btn { padding: 12px 20px; border-radius: 10px; border: none; background: var(--accent, #6b8f71); color: #fff; font-weight: 600; font-size: 15px; cursor: pointer; text-decoration: none; display: inline-block; }
-  .nl-btn-ghost { background: transparent; color: inherit; border: 1px solid rgba(128,128,128,.4); }
-</style>
Index: src/views/pages/oauth-consent.ejs
===================================================================
--- src/views/pages/oauth-consent.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,60 +1,0 @@
-<div class="oa-wrap">
-  <h1 class="oa-title"><%= t('oauth.title') %></h1>
-  <p class="oa-lead">
-    <strong><%= client.client_name %></strong>
-    <%= t('oauth.wants_access') %>
-  </p>
-
-  <form method="post" action="/oauth/authorize" class="oa-form">
-    <input type="hidden" name="client_id" value="<%= params.client_id %>">
-    <input type="hidden" name="redirect_uri" value="<%= params.redirect_uri %>">
-    <input type="hidden" name="code_challenge" value="<%= params.code_challenge %>">
-    <input type="hidden" name="scope" value="<%= params.scope %>">
-    <input type="hidden" name="state" value="<%= params.state %>">
-
-    <label class="oa-label"><%= t('oauth.post_as') %></label>
-    <% if (sites.length === 1) { %>
-      <input type="hidden" name="site_slug" value="<%= sites[0].slug %>">
-      <div class="oa-single"><%= sites[0].title || sites[0].slug %> <span class="oa-slug">@<%= sites[0].slug %></span></div>
-    <% } else { %>
-      <select name="site_slug" class="oa-select">
-        <% sites.forEach(function(s){ %>
-          <option value="<%= s.slug %>"><%= s.title || s.slug %> (@<%= s.slug %>)</option>
-        <% }); %>
-      </select>
-    <% } %>
-
-    <ul class="oa-scopes">
-      <li><%= t('oauth.scope_read') %></li>
-      <li><%= t('oauth.scope_write') %></li>
-    </ul>
-
-    <div class="oa-actions">
-      <button type="submit" name="decision" value="deny" class="btn oa-deny"><%= t('oauth.deny') %></button>
-      <button type="submit" name="decision" value="allow" class="btn btn-primary oa-allow"><%= t('oauth.allow') %></button>
-    </div>
-  </form>
-  <p class="oa-foot"><%= t('oauth.foot') %></p>
-</div>
-
-<style>
-  .oa-wrap { max-width: 460px; margin: 2.5rem auto; padding: 1.75rem 1.5rem; border-radius: 16px;
-    background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent);
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); }
-  .oa-title { margin: 0 0 .6rem; font-size: 1.4rem; }
-  .oa-lead { color: var(--ink-soft, #999); line-height: 1.55; margin: 0 0 1.4rem; }
-  .oa-lead strong { color: var(--ink, inherit); }
-  .oa-label { display: block; font-weight: 700; font-size: .82rem; margin: 0 0 .4rem; color: var(--ink-soft, #888); text-transform: uppercase; letter-spacing: .03em; }
-  .oa-single { padding: .6rem .8rem; border-radius: 10px; background: color-mix(in srgb, var(--ink, #000) 5%, transparent); font-weight: 600; }
-  .oa-slug { color: var(--ink-soft, #999); font-weight: 400; }
-  .oa-select { width: 100%; padding: .6rem .8rem; border-radius: 10px; font: inherit;
-    border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: transparent; color: var(--ink, inherit); }
-  .oa-scopes { list-style: none; padding: .9rem 0 0; margin: .9rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 8%, transparent);
-    display: flex; flex-direction: column; gap: .5rem; color: var(--ink-soft, #aaa); }
-  .oa-scopes li { position: relative; padding-left: 1.5rem; line-height: 1.4; }
-  .oa-scopes li::before { content: "✓"; position: absolute; left: 0; color: #2fa85a; font-weight: 700; }
-  .oa-actions { display: flex; gap: .7rem; margin-top: 1.5rem; }
-  .oa-deny { flex: 0 0 auto; }
-  .oa-allow { flex: 1; }
-  .oa-foot { color: var(--ink-soft, #888); font-size: .78rem; line-height: 1.5; margin: 1.1rem 0 0; }
-</style>
Index: src/views/pages/owa-consent.ejs
===================================================================
--- src/views/pages/owa-consent.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,69 +1,0 @@
-<%
-// Het toestemmingsscherm van /magic.
-//
-// Dit scherm bestaat omdat de FEP er onder "Information leakage" om vraagt:
-// OpenWebAuth geeft een STERKE identiteitsclaim af aan elke site die erom
-// vraagt, en desnoods zonder dat je het merkt. De omweg langs je eigen server is
-// het enige moment waarop je nog kunt zeggen: deze site niet.
-//
-// Daarom staat de doelhost groot en apart. Niet als versiering: wie hier
-// doorklikt vertelt die host wie hij is, en dat hoort de zin te zijn die hij
-// gelezen heeft.
-var _sites = (typeof owaSites !== 'undefined' && owaSites) ? owaSites : [];
-%>
-
-<section class="owa">
-  <div class="owa-card">
-    <h1 class="owa-h1"><%= t('owa.title') %></h1>
-
-    <p class="owa-host"><%= owaHost %></p>
-    <p class="owa-sub"><%= t('owa.sub') %></p>
-
-    <form method="post" action="/magic">
-      <input type="hidden" name="bdest" value="<%= owaBdest %>">
-
-      <% if (_sites.length === 1) { %>
-        <input type="hidden" name="slug" value="<%= _sites[0].slug %>">
-        <p class="owa-as"><%= t('owa.as') %> <strong><%= _sites[0].title || _sites[0].slug %></strong></p>
-      <% } else { %>
-        <%# Meer dan één site: kiezen is hier geen luxe. Ondertekenen en
-            ontsleutelen kan alleen met een sleutel die hij ook echt beheert. %>
-        <p class="owa-as"><%= t('owa.choose') %></p>
-        <ul class="owa-list">
-          <% _sites.forEach(function(s, i) { %>
-            <li>
-              <label>
-                <input type="radio" name="slug" value="<%= s.slug %>"<%= i === 0 ? ' checked' : '' %>>
-                <span><%= s.title || s.slug %></span>
-              </label>
-            </li>
-          <% }); %>
-        </ul>
-      <% } %>
-
-      <div class="owa-actions">
-        <button class="owa-btn" type="submit"><%= t('owa.go') %></button>
-        <a class="owa-no" href="/"><%= t('owa.cancel') %></a>
-      </div>
-    </form>
-
-    <p class="owa-fine"><%= t('owa.fine') %></p>
-  </div>
-</section>
-
-<style>
-  .owa { max-width: 480px; margin: 0 auto; padding: 56px 18px; text-align: center; }
-  .owa-card { border: 1px solid rgba(128,128,128,.2); border-radius: 18px; padding: 36px 28px; }
-  .owa-h1 { font-size: 22px; margin: 0 0 14px; }
-  .owa-host { font-size: 20px; font-weight: 600; margin: 0 0 6px; word-break: break-all; }
-  .owa-sub { opacity: .85; line-height: 1.6; margin: 0 0 20px; }
-  .owa-as { font-size: .95rem; margin: 0 0 10px; }
-  .owa-list { list-style: none; padding: 0; margin: 0 0 18px; text-align: left; }
-  .owa-list li { padding: 6px 0; }
-  .owa-list label { display: flex; gap: 10px; align-items: center; cursor: pointer; }
-  .owa-actions { display: flex; gap: 14px; align-items: center; justify-content: center; flex-wrap: wrap; }
-  .owa-btn { padding: 12px 22px; border-radius: 10px; border: 0; background: var(--accent,#6b8f71);
-             color: #fff; font-weight: 600; font-family: inherit; font-size: 1rem; cursor: pointer; }
-  .owa-no { font-size: .9rem; opacity: .7; }
-  .owa-fine { font-size: .8rem; opacity: .6; line-height: 1.5; margin: 18px 0 0; }
-</style>
Index: src/views/pages/paid-gate.ejs
===================================================================
--- src/views/pages/paid-gate.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,66 +1,0 @@
-<div class="container pg-navwrap">
-  <%- include('../partials/post-nav', { newerPost: (typeof newerPost !== 'undefined' ? newerPost : null), olderPost: (typeof olderPost !== 'undefined' ? olderPost : null) }) %>
-</div>
-
-<article class="pg-page" id="pg-page">
-  <% if (typeof pgTitle !== 'undefined' && pgTitle) { %><h1 class="pg-title"><%= pgTitle %></h1><% } %>
-
-  <% if (typeof pgTeaser !== 'undefined' && pgTeaser) { %>
-    <div class="pg-teaser"><p><%= pgTeaser %></p></div>
-  <% } %>
-
-  <%# De opengezette muziek staat VOOR de poort en niet erachter: wie hem in een
-      hub of in Mastodon al kan afspelen, hoort hem ook hier te kunnen afspelen.
-      Alleen de tekst zit achter de poort, en die staat hier niet -- de route
-      geeft alleen de audio-shortcodes mee (paidOpenAudioHtml). %>
-  <% if (typeof pgAudio !== 'undefined' && pgAudio) { %>
-    <div class="pg-audio post-content"><%- pgAudio %></div>
-  <% } %>
-
-  <% var _hasPatron = (typeof pgPatronUrl !== 'undefined' && pgPatronUrl); %>
-  <section class="pg-card">
-    <div class="pg-lock">💶</div>
-    <h2 class="pg-h2"><%= t('pgate.h') %></h2>
-    <p class="pg-sub">
-      <% if (typeof pgCents !== 'undefined' && pgCents) { %><%= t('pgate.sub_cents', { eur: (pgCents/100).toFixed(2) }) %><% } else { %><%= t('pgate.sub') %><% } %>
-    </p>
-
-    <div class="pg-actions">
-      <% if (_hasPatron) { %>
-        <a class="pg-btn" href="<%= pgPatronUrl %>" target="_blank" rel="noopener"><%= t('pgate.join') %></a>
-        <button type="button" id="pg-unlock" class="pg-btn pg-btn-ghost"><%= t('pgate.unlock_have') %></button>
-      <% } else { %>
-        <button type="button" id="pg-unlock" class="pg-btn"><%= t('pgate.unlock') %></button>
-      <% } %>
-    </div>
-    <p id="pg-status" class="pg-status" hidden></p>
-  </section>
-</article>
-
-<%# Het script staat in assets/js/mod/paid-gate.js; de gegevens via partials/page-data.ejs (shaer-bqr).
-    De WebAuthn-bibliotheek stond hier als `<script src>` en is weg (shaer-0i6):
-    deze pagina komt via een link binnen de site als htmx-fragment binnen, en
-    dan draagt zo'n tag de nonce van een ander verzoek en weigert de CSP hem.
-    De module haalt hem nu zelf op met loadWebAuthn() uit lib.js. %>
-
-<style>
-  .pg-navwrap { max-width: 720px; margin: 0.5rem auto 1.5rem; padding: 0 1rem; }
-  .pg-page { max-width: 720px; margin: 0 auto 3rem; padding: 0 1rem; }
-  .pg-title { font-family: var(--font-display, serif); font-size: clamp(1.6rem, 4vw, 2.2rem); margin: 0 0 1rem; }
-  .pg-teaser { font-family: var(--font-body, serif); font-size: 1.1rem; line-height: 1.7; color: var(--ink); opacity: .95;
-    -webkit-mask-image: linear-gradient(180deg, #000 55%, transparent); mask-image: linear-gradient(180deg, #000 55%, transparent); }
-  .pg-audio { margin: 1.5rem 0 0; }
-  .pg-card { margin: 1.5rem 0 0; border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); border-radius: 18px; padding: 30px 26px; text-align: center; }
-  .pg-lock { font-size: 38px; margin-bottom: 6px; }
-  .pg-h2 { font-size: 22px; margin: 0 0 8px; }
-  .pg-sub { opacity: .85; line-height: 1.6; margin: 0 auto 12px; max-width: 34em; }
-  .pg-btn { display: inline-block; text-align: center; text-decoration: none; padding: 12px 24px; border: none; border-radius: 10px; font: inherit; font-weight: 600; cursor: pointer;
-    background: var(--accent, #6b8f71); color: #fff; }
-  .pg-btn:disabled { opacity: .6; cursor: default; }
-  .pg-btn:hover { filter: brightness(1.05); }
-  .pg-actions { display: flex; flex-direction: column; gap: 10px; align-items: stretch; max-width: 320px; margin: 0 auto; }
-  .pg-btn-ghost { background: transparent; color: var(--ink, #222); border: 1px solid color-mix(in srgb, var(--ink, #000) 22%, transparent); }
-  .pg-status { margin: 12px 0 0; opacity: .9; }
-  .pg-status.is-err { color: #c0392b; }
-</style>
-<%- include('../partials/page-data', { pageData: { base: (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : ''), slug: pgSlug, hasPatron: !!_hasPatron, i18n: { join: t('pgate.join_short'), confirm: t('pgate.confirm'), failed: t('pgate.failed'), error: t('pgate.error') } } }) %>
Index: src/views/pages/paid-passkey.ejs
===================================================================
--- src/views/pages/paid-passkey.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,30 +1,0 @@
-<section class="pk">
-  <div class="pk-card">
-    <div class="pk-ic">🔑</div>
-    <h1 class="pk-h1"><%= t('ppk.h') %></h1>
-    <p class="pk-sub">
-      <%= t('ppk.sub') %>
-    </p>
-    <button type="button" id="pk-go" class="pk-btn"><%= t('ppk.make') %></button>
-    <p id="pk-status" class="pk-status" hidden></p>
-  </div>
-</section>
-
-<%# Het script staat in assets/js/mod/paid-passkey.js; de gegevens via partials/page-data.ejs (shaer-bqr).
-    De WebAuthn-bibliotheek stond hier als `<script src>` en is weg (shaer-0i6):
-    deze pagina komt via een link binnen de site als htmx-fragment binnen, en
-    dan draagt zo'n tag de nonce van een ander verzoek en weigert de CSP hem.
-    De module haalt hem nu zelf op met loadWebAuthn() uit lib.js. %>
-
-<style>
-  .pk { max-width: 480px; margin: 0 auto; padding: 56px 18px; text-align: center; }
-  .pk-card { border: 1px solid color-mix(in srgb, var(--ink,#000) 16%, transparent); border-radius: 18px; padding: 34px 26px; }
-  .pk-ic { font-size: 40px; margin-bottom: 8px; }
-  .pk-h1 { font-size: 24px; margin: 0 0 6px; }
-  .pk-sub { opacity: .85; line-height: 1.6; margin: 0 0 20px; }
-  .pk-btn { padding: 12px 24px; border: none; border-radius: 10px; background: var(--accent,#6b8f71); color: #fff; font-weight: 600; font: inherit; font-weight: 600; cursor: pointer; }
-  .pk-btn:disabled { opacity: .6; cursor: default; }
-  .pk-status { margin: 14px 0 0; opacity: .9; }
-  .pk-status.is-err { color: #c0392b; }
-</style>
-<%- include('../partials/page-data', { pageData: { options: JSON.parse(optionsJson), blob: regBlob, i18n: { unsupported: t('ppk.unsupported'), follow: t('ppk.follow'), done: t('ppk.done'), failed: t('ppk.failed'), cancelled: t('ppk.cancelled'), error: t('pgate.error') }, postUrl: (typeof siteUrlBase !== 'undefined' && siteUrlBase ? siteUrlBase : '') + '/' + postSlug } }) %>
Index: src/views/pages/paid-result.ejs
===================================================================
--- src/views/pages/paid-result.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,45 +1,0 @@
-<%
-  var _slug = (typeof postSlug !== 'undefined' && postSlug) ? postSlug : '';
-  var _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
-  var _patron = (typeof patronUrl !== 'undefined' && patronUrl) ? patronUrl : '';
-%>
-<section class="pr">
-  <div class="pr-card">
-    <div class="pr-ic">🔒</div>
-    <% if (reason === 'notpatron') { %>
-      <h1 class="pr-h1"><%= t('pres.notpatron_h') %></h1>
-      <p class="pr-sub"><%= t('pres.notpatron_p') %></p>
-    <% } else if (reason === 'tier') { %>
-      <h1 class="pr-h1"><%= t('pres.tier_h') %></h1>
-      <p class="pr-sub"><%= t('pres.tier_p', { need: ((typeof neededCents !== 'undefined' ? neededCents : 0)/100).toFixed(2), have: ((typeof haveCents !== 'undefined' ? haveCents : 0)/100).toFixed(2) }) %></p>
-    <% } else if (reason === 'expired') { %>
-      <h1 class="pr-h1"><%= t('pres.expired_h') %></h1>
-      <p class="pr-sub"><%= t('pres.expired_p') %></p>
-    <% } else { %>
-      <h1 class="pr-h1"><%= t('pres.declined_h') %></h1>
-      <p class="pr-sub"><%= t('pres.declined_p') %></p>
-    <% } %>
-
-    <div class="pr-actions">
-      <% if ((reason === 'notpatron' || reason === 'tier') && _patron) { %>
-        <a class="pr-btn" href="<%= _patron %>" target="_blank" rel="noopener"><%= t('pres.join') %></a>
-      <% } %>
-      <% if (_slug) { %>
-        <a class="pr-btn pr-btn-ghost" href="<%= _base %>/<%= _slug %>"><%= t('pres.back_post') %></a>
-      <% } else { %>
-        <a class="pr-btn pr-btn-ghost" href="<%= _base %>/"><%= t('pres.back_site') %></a>
-      <% } %>
-    </div>
-  </div>
-</section>
-
-<style>
-  .pr { max-width: 460px; margin: 0 auto; padding: 56px 18px; text-align: center; }
-  .pr-card { border: 1px solid color-mix(in srgb, var(--ink,#000) 16%, transparent); border-radius: 18px; padding: 34px 26px; }
-  .pr-ic { font-size: 38px; margin-bottom: 6px; }
-  .pr-h1 { font-size: 23px; margin: 0 0 8px; }
-  .pr-sub { opacity: .85; line-height: 1.6; margin: 0 0 20px; }
-  .pr-actions { display: flex; flex-direction: column; gap: 10px; align-items: stretch; }
-  .pr-btn { display: inline-block; padding: 11px 20px; border-radius: 10px; background: var(--accent,#6b8f71); color: #fff; text-decoration: none; font-weight: 600; }
-  .pr-btn-ghost { background: transparent; color: var(--ink,#222); border: 1px solid color-mix(in srgb, var(--ink,#000) 22%, transparent); }
-</style>
Index: src/views/pages/post-edit.ejs
===================================================================
--- src/views/pages/post-edit.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/post-edit.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -9,100 +9,29 @@
 // and styling changed.
 const _hasCover = !!post.cover_image_url;
-// In hub mode, save/create/cancel must carry the /user/<slug>/ prefix, otherwise
-// the request falls back to the primary site (siteUrlBase empty) and the post
-// renders headerless (bareChrome) after saving. In solo mode _base is empty.
-const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
 %>
 <div class="container post-edit-page">
-  <h1><%= isNew ? t('pedit.title_new') : t('pedit.title_edit') %></h1>
-
-  <form method="post" action="<%= _base %><%= isNew ? '/posts/create' : '/posts/' + post.slug + '/save' %>" class="post-edit-form">
-
-    <%# ── TYPE (segmented) + type-specific inputs ────────────────
-        Post type lives at the TOP (above the body), and the selected type
-        reveals its own input: Audio → inline upload, Video → embed URL,
-        Foto → cover/insert hint. %>
-    <% var ptype = post.type || 'post'; %>
-    <% var TYPE_ICONS = { post: '📝', foto: '📷', video: '🎬', album: '💿', playlist: '📃', mixtape: '📼', audio: '🎵' }; %>
-    <%# Audio is geen keuze meer (shaer-cyg): alles wat muziek is landt op Album
-        of Playlist. De knop blijft alleen staan zolang deze post nog audio IS,
-        zodat een oude post niet stilzwijgend van type verandert bij het openen. %>
-    <% var TYPES = (typeof keuzeTypes !== 'undefined' && keuzeTypes.length)
-                     ? keuzeTypes.slice() : ['post','foto','video','album','playlist','mixtape']; %>
-    <% if (ptype === 'audio' && TYPES.indexOf('audio') < 0) TYPES.push('audio'); %>
-    <section class="pe-card pe-type-card">
-      <div class="pe-section-title"><%= t('pedit.s_type') %></div>
-      <input type="hidden" name="type" id="pe-type-input" value="<%= ptype %>">
-      <div class="pe-typeseg" role="radiogroup" aria-label="<%= t('pedit.s_type') %>">
-        <% TYPES.forEach(function (tt) { %>
-          <button type="button" class="pe-typeseg-btn<%= ptype === tt ? ' is-active' : '' %>"
-                  data-type="<%= tt %>" role="radio" aria-checked="<%= ptype === tt ? 'true' : 'false' %>">
-            <span class="pe-typeseg-ic" aria-hidden="true"><%= TYPE_ICONS[tt] %></span>
-            <span><%= t('pedit.type_' + tt) %></span>
-          </button>
-        <% }); %>
-      </div>
-
-      <%# Muziek: inline upload (transcodes + drops [[track]] into the post).
-          Album, Playlist en Mixtape delen dit paneel -- het verschil zit in de
-          playlist die je insluit, niet in wat je hier uploadt. Audio staat er
-          alleen nog voor oude posts. %>
-      <div class="pe-type-panel" data-panel="album playlist mixtape audio" hidden>
-        <div class="pe-audio-up" id="pe-audio-drop" tabindex="0" role="button" aria-label="<%= t('pedit.audio_up_drop') %>">
-          <input type="file" id="pe-audio-file" accept="audio/*,.mp3,.m4a,.ogg,.opus,.flac,.wav" multiple hidden>
-          <span class="pe-audio-up-ic" aria-hidden="true">🎵</span>
-          <strong><%= t('pedit.audio_up_drop') %></strong>
-          <small><%= t('pedit.audio_up_hint') %></small>
-        </div>
-        <ul class="pe-audio-list" id="pe-audio-list"></ul>
-      </div>
-
-      <%# Video: paste a URL → embed chip %>
-      <div class="pe-type-panel" data-panel="video" hidden>
-        <label class="pe-field">
-          <span><%= t('pedit.video_up_title') %></span>
-          <div class="pe-video-row">
-            <input type="url" id="pe-video-url" inputmode="url" autocapitalize="none" spellcheck="false"
-                   placeholder="<%= t('pedit.video_up_ph') %>">
-            <button type="button" class="pe-btn pe-btn-secondary" id="pe-video-insert"><%= t('pedit.video_up_btn') %></button>
-          </div>
-        </label>
-      </div>
-
-      <%# Foto: light hint toward cover + insert-image %>
-      <div class="pe-type-panel" data-panel="foto" hidden>
-        <p class="pe-foto-hint">🖼 <%= t('pedit.foto_up_hint') %></p>
-      </div>
-    </section>
+  <h1><%= isNew ? 'Nieuwe post' : 'Post bewerken' %></h1>
+
+  <form method="post" action="<%= isNew ? '/posts/create' : '/posts/' + post.slug + '/save' %>" class="post-edit-form">
 
     <%# ── IDENTITY ─────────────────────────────────────────────── %>
     <section class="pe-card">
       <label class="pe-field">
-        <span><%= t('pedit.f_title') %></span>
-        <input type="text" name="title" value="<%= post.title %>" required>
+        <span>Titel</span>
+        <input type="text" name="title" value="<%= post.title %>" required autofocus>
       </label>
       <div class="pe-row pe-row-2">
         <label class="pe-field">
-          <span><%= t('pedit.f_slug') %></span>
-          <input type="text" name="slug" value="<%= post.slug %>" placeholder="<%= t('pedit.slug_placeholder') %>">
+          <span>Slug (URL)</span>
+          <input type="text" name="slug" value="<%= post.slug %>" placeholder="auto van titel als leeg">
         </label>
         <label class="pe-field">
-          <span><%= t('pedit.f_tags') %> <small><%= t('pedit.tags_hint') %></small></span>
+          <span>Tags <small>(komma-gescheiden)</small></span>
           <input type="text" name="tags" value="<%= Array.isArray(post.tags) ? post.tags.join(', ') : '' %>">
         </label>
       </div>
       <label class="pe-field">
-        <span><%= t('pedit.f_excerpt') %></span>
+        <span>Excerpt <small>(preview tekst)</small></span>
         <textarea name="excerpt" rows="2"><%= post.excerpt %></textarea>
-        <small class="pe-hint" style="opacity:.7"><%- t('pedit.excerpt_hint') %></small>
-      </label>
-      <% var _postLang = (typeof post.language !== 'undefined' && post.language) ? post.language : (typeof lang !== 'undefined' ? lang : 'en'); %>
-      <label class="pe-field">
-        <span><%= t('pedit.f_language') %> <small><%= t('pedit.language_hint') %></small></span>
-        <select name="language" id="pe-language" style="max-width:220px">
-          <% [['en','English'],['nl','Nederlands'],['de','Deutsch'],['fr','Français'],['es','Español'],['it','Italiano'],['pt','Português'],['pl','Polski'],['ru','Русский'],['uk','Українська'],['sv','Svenska'],['da','Dansk'],['nb','Norsk'],['fi','Suomi'],['tr','Türkçe'],['ja','日本語'],['zh','中文'],['ar','العربية']].forEach(function (l) { %>
-            <option value="<%= l[0] %>" <%= l[0] === _postLang ? 'selected' : '' %>><%= l[1] %></option>
-          <% }); %>
-        </select>
       </label>
     </section>
@@ -110,5 +39,5 @@
     <%# ── COVER ────────────────────────────────────────────────── %>
     <section class="pe-card">
-      <div class="pe-section-title"><%= t('pedit.s_cover') %></div>
+      <div class="pe-section-title">Cover</div>
       <div class="pe-cover-row">
         <div class="pe-cover-thumb" id="cover-preview-wrap" <%= _hasCover ? '' : 'data-empty' %>>
@@ -118,21 +47,12 @@
         <div class="pe-cover-actions">
           <label class="pe-field">
-            <span><%= t('pedit.f_cover_url') %></span>
-            <input type="text" name="cover_image_url" id="cover-url-field"
+            <span>Cover URL</span>
+            <input type="url" name="cover_image_url" id="cover-url-field"
                    value="<%= post.cover_image_url || '' %>"
-                   inputmode="url" autocapitalize="none" spellcheck="false"
-                   placeholder="<%= t('pedit.cover_url_placeholder') %>">
+                   placeholder="https://…  of upload met de knop">
           </label>
-          <label class="pe-field">
-            <span><%= t('pedit.f_cover_alt') %></span>
-            <input type="text" name="cover_alt" id="cover-alt-field" maxlength="1500"
-                   value="<%= post.cover_alt || '' %>"
-                   placeholder="<%= t('pedit.cover_alt_placeholder') %>">
-          </label>
-          <%# Muted loop MP4 for an animated cover (auto-made from an animated WebP). Hidden — set by the uploader. %>
-          <input type="hidden" name="cover_video_url" id="cover-video-field" value="<%= post.cover_video_url || '' %>">
           <div class="pe-cover-upload">
             <button type="button" class="pe-btn pe-btn-secondary" id="cover-upload-trigger">
-              📷 <%= t('pedit.cover_upload_btn') %>
+              📷 Upload nieuwe cover
             </button>
             <input type="file" id="cover-upload-field" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
@@ -146,59 +66,51 @@
     <section class="pe-card pe-card-content">
       <div class="pe-section-title">
-        <%= t('pedit.s_content') %>
-        <small class="pe-content-hint"><%= t('pedit.content_hint') %></small>
+        Content
+        <small>Sleep een afbeelding om in te voegen · selecteer tekst om op te maken</small>
       </div>
       <div class="pe-editor-frame">
         <div class="pe-toolbar" id="pe-toolbar" role="toolbar" aria-label="Format">
-          <button type="button" id="pe-fs-done" class="pe-fs-done" title="<%= t('pedit.tb_done_title') %>">✓ <%= t('pedit.tb_done') %></button>
-          <button type="button" data-cmd="bold" title="<%= t('pedit.tb_bold_title') %>" aria-label="<%= t('pedit.tb_bold') %>">
+          <button type="button" data-cmd="bold" title="Vet (Ctrl+B)" aria-label="Vet">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 4h8a4 4 0 0 1 0 8H6z"/><path d="M6 12h9a4 4 0 0 1 0 8H6z"/></svg>
           </button>
-          <button type="button" data-cmd="italic" title="<%= t('pedit.tb_italic_title') %>" aria-label="<%= t('pedit.tb_italic') %>">
+          <button type="button" data-cmd="italic" title="Cursief (Ctrl+I)" aria-label="Cursief">
             <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="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>
           </button>
-          <button type="button" data-cmd="underline" title="<%= t('pedit.tb_underline') %>" aria-label="<%= t('pedit.tb_underline') %>">
+          <button type="button" data-cmd="underline" title="Onderstreept" aria-label="Onderstreept">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 4v6a6 6 0 0 0 12 0V4"/><line x1="4" y1="20" x2="20" y2="20"/></svg>
           </button>
           <span class="pe-toolbar-sep" aria-hidden="true"></span>
-          <button type="button" class="pe-tb-text" data-cmd="formatBlock" data-arg="h2" title="<%= t('pedit.tb_h2') %>">H2</button>
-          <button type="button" class="pe-tb-text" data-cmd="formatBlock" data-arg="h3" title="<%= t('pedit.tb_h3') %>">H3</button>
-          <button type="button" class="pe-tb-text" data-cmd="formatBlock" data-arg="p"  title="<%= t('pedit.tb_p') %>">¶</button>
+          <button type="button" class="pe-tb-text" data-cmd="formatBlock" data-arg="h2" title="Kop">H2</button>
+          <button type="button" class="pe-tb-text" data-cmd="formatBlock" data-arg="h3" title="Subkop">H3</button>
+          <button type="button" class="pe-tb-text" data-cmd="formatBlock" data-arg="p"  title="Paragraaf">¶</button>
           <span class="pe-toolbar-sep" aria-hidden="true"></span>
-          <button type="button" data-cmd="insertUnorderedList" title="<%= t('pedit.tb_ul') %>" aria-label="<%= t('pedit.tb_ul') %>">
+          <button type="button" data-cmd="insertUnorderedList" title="Lijst" aria-label="Lijst">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4.5" cy="6" r="1.2"/><circle cx="4.5" cy="12" r="1.2"/><circle cx="4.5" cy="18" r="1.2"/></svg>
           </button>
-          <button type="button" data-cmd="insertOrderedList" title="<%= t('pedit.tb_ol') %>" aria-label="<%= t('pedit.tb_ol') %>">
+          <button type="button" data-cmd="insertOrderedList" title="Genummerde lijst" aria-label="Genummerde lijst">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>
           </button>
-          <button type="button" data-cmd="formatBlock" data-arg="blockquote" title="<%= t('pedit.tb_quote') %>" aria-label="<%= t('pedit.tb_quote') %>">
+          <button type="button" data-cmd="formatBlock" data-arg="blockquote" title="Quote" aria-label="Quote">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.75-2-2-2H4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h2.5C6 17.5 4 19 3 19v2z"/><path d="M14 21c3 0 7-1 7-8V5c0-1.25-.75-2-2-2h-4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h2.5c-.5 4.5-2.5 6-3.5 6v2z"/></svg>
           </button>
-          <button type="button" data-cmd="link-prompt" title="<%= t('pedit.tb_link_title') %>" aria-label="<%= t('pedit.tb_link') %>">
+          <button type="button" data-cmd="link-prompt" title="Link (Ctrl+K)" aria-label="Link">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
           </button>
-          <button type="button" data-cmd="code-wrap" title="<%= t('pedit.tb_code_title') %>" aria-label="<%= t('pedit.tb_code') %>">
+          <button type="button" data-cmd="code-wrap" title="Code (inline)" aria-label="Code">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
           </button>
           <span class="pe-toolbar-sep" aria-hidden="true"></span>
-          <button type="button" id="insert-image-btn" title="<%= t('pedit.tb_image_title') %>" aria-label="<%= t('pedit.tb_image') %>">
+          <button type="button" id="insert-image-btn" title="Afbeelding invoegen" aria-label="Afbeelding">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
           </button>
-          <button type="button" id="insert-track-btn" title="<%= t('pedit.tb_track_title') %>" aria-label="<%= t('pedit.tb_track') %>">
+          <button type="button" id="insert-track-btn" title="Track invoegen" aria-label="Track">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
           </button>
-          <button type="button" id="insert-playlist-btn" title="<%= t('pedit.tb_playlist_title') %>" aria-label="<%= t('pedit.tb_playlist') %>">
+          <button type="button" id="insert-playlist-btn" title="Playlist invoegen" aria-label="Playlist">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="15" y2="18"/><polygon points="3 5 3 13 9 9"/></svg>
           </button>
-          <button type="button" id="insert-embed-btn" title="<%= t('pedit.tb_embed_title') %>" aria-label="<%= t('pedit.tb_embed') %>">
-            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><polygon points="10 9 15.5 12 10 15"/></svg>
-          </button>
           <span class="pe-toolbar-spacer"></span>
-          <button type="button" data-cmd="removeFormat" title="<%= t('pedit.tb_clear') %>" aria-label="<%= t('pedit.tb_clear') %>">
+          <button type="button" data-cmd="removeFormat" title="Wis opmaak" aria-label="Wis opmaak">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M5 5l14 14" stroke-linecap="round"/></svg>
-          </button>
-          <button type="button" id="pe-fullscreen-btn" title="<%= t('pedit.tb_fullscreen') %>" aria-label="<%= t('pedit.tb_fullscreen') %>" aria-pressed="false">
-            <svg class="fs-icon-expand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
-            <svg class="fs-icon-compress" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
           </button>
         </div>
@@ -209,15 +121,10 @@
              role="textbox"
              aria-multiline="true"
-             aria-label="<%= t('pedit.editor_aria') %>"></div>
-
-        <%# Sticky "tap to edit" hint (only visible on touch + non-fullscreen
-            via CSS). Purely visual (pointer-events:none); tapping the field opens fullscreen. %>
-        <div class="pe-edit-hint" aria-hidden="true">✎ <%= t('pedit.tap_to_edit') %></div>
+             aria-label="Content"
+             data-placeholder="Begin met schrijven…"></div>
 
         <%# Hidden field is what actually submits to the server. The visible
             contenteditable's serialized HTML is copied here on submit. %>
-        <%# value = de BESTAANDE inhoud: laadt de module niet (oude cache, js uit),
-    dan slaat het formulier de post ongewijzigd op in plaats van hem te wissen. %>
-        <input type="hidden" name="content" id="content-hidden" value="<%= post.content || '' %>">
+        <input type="hidden" name="content" id="content-hidden" value="">
 
         <%# Initial content is injected as a JSON string in a script tag so we
@@ -228,5 +135,5 @@
         <div class="pe-editor-status">
           <small id="content-upload-status" class="pe-status"></small>
-          <span class="pe-char-count"><span id="char-count">0</span> <%= t('pedit.chars') %></span>
+          <span class="pe-char-count"><span id="char-count">0</span> tekens</span>
         </div>
 
@@ -237,130 +144,38 @@
     <%# ── META ─────────────────────────────────────────────────── %>
     <section class="pe-card">
-      <div class="pe-section-title"><%= t('pedit.s_publication') %></div>
-      <label class="pe-field">
-        <span><%= t('pedit.f_status') %></span>
-        <% var _st = isNew ? 'published' : (post.status || 'draft'); %>
-        <select name="status">
-          <option value="published" <%= _st === 'published' ? 'selected' : '' %>><%= t('pedit.status_published') %></option>
-          <option value="draft"     <%= _st === 'draft'     ? 'selected' : '' %>><%= t('pedit.status_draft') %></option>
-          <option value="archived"  <%= _st === 'archived'  ? 'selected' : '' %>><%= t('pedit.status_archived') %></option>
-        </select>
-      </label>
+      <div class="pe-section-title">Publicatie</div>
+      <div class="pe-row pe-row-2">
+        <label class="pe-field">
+          <span>Status</span>
+          <select name="status">
+            <option value="draft"     <%= post.status === 'draft'     ? 'selected' : '' %>>Draft</option>
+            <option value="published" <%= post.status === 'published' ? 'selected' : '' %>>Published</option>
+            <option value="archived"  <%= post.status === 'archived'  ? 'selected' : '' %>>Archived</option>
+          </select>
+        </label>
+        <label class="pe-field">
+          <span>Type</span>
+          <select name="type">
+            <% var ptype = post.type || 'post'; %>
+            <option value="post"  <%= ptype === 'post'  ? 'selected' : '' %>>Post</option>
+            <option value="foto"  <%= ptype === 'foto'  ? 'selected' : '' %>>Foto</option>
+            <option value="video" <%= ptype === 'video' ? 'selected' : '' %>>Video</option>
+            <option value="audio" <%= ptype === 'audio' ? 'selected' : '' %>>Audio</option>
+          </select>
+        </label>
+      </div>
       <div class="pe-checkboxes">
-        <div class="pe-pin">
-          <label class="pe-checkbox">
-            <input type="checkbox" id="pin-toggle" <%= (Number(post.pinned) > 0) ? 'checked' : '' %>>
-            <span>📌 <%= t('pedit.pin_label') %></span>
-          </label>
-          <input type="hidden" name="pinned" id="pin-rank" value="<%= post.pinned || 0 %>">
-          <div class="pe-pin-pos" id="pin-pos" <%= (Number(post.pinned) > 0) ? '' : 'hidden' %>>
-            <span class="pe-pin-steps">
-              <button type="button" class="pe-pin-btn" id="pin-up" aria-label="<%= t('pedit.pin_up') %>">▲</button>
-              <button type="button" class="pe-pin-btn" id="pin-down" aria-label="<%= t('pedit.pin_down') %>">▼</button>
-            </span>
-            <span class="pe-pin-label" id="pin-label"></span>
-          </div>
-        </div>
+        <label class="pe-pinned-rank">
+          <span>📌 Pinned rank</span>
+          <input type="number" name="pinned" min="0" step="1"
+                 inputmode="numeric" pattern="[0-9]*"
+                 value="<%= post.pinned || 0 %>"
+                 title="0 = niet pinned. 1 = bovenaan, 2 = daaronder, etc.">
+          <small>0 = niet pinned · 1 = bovenaan · 2 daaronder · …</small>
+        </label>
         <label class="pe-checkbox">
           <input type="checkbox" name="noindex" value="1" <%= post.noindex ? 'checked' : '' %>>
-          <span>🚫 <%= t('pedit.noindex_label') %></span>
+          <span>🚫 noindex (verberg voor zoekmachines)</span>
         </label>
-        <label class="pe-checkbox">
-          <input type="checkbox" name="nsfw" value="1" id="pe-nsfw" <%= post.nsfw ? 'checked' : '' %>>
-          <span>🔞 <%= t('pedit.nsfw_label') %></span>
-        </label>
-        <input type="text" name="content_warning" id="pe-cw" 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">
-        <% var _fediOpen = (typeof fediOpenAudio !== 'undefined' && fediOpenAudio); %>
-        <label class="pe-checkbox">
-          <%# One-way: once opened, the file has federated — re-gating would be false security, so the box locks. %>
-          <input type="checkbox" name="fedi_open_audio" value="1" id="pe-fedi-audio" <%= _fediOpen ? 'checked disabled' : '' %>>
-          <span>🌐 <%= t('pedit.fedi_audio_label') %></span>
-        </label>
-        <% if (_fediOpen) { %>
-          <p style="font-size:12px;opacity:.7;margin:2px 0 0 26px"><%= t('pedit.fedi_audio_locked') %></p>
-        <% } else { %>
-          <p id="pe-fedi-audio-warn" style="font-size:12px;color:#c0392b;margin:2px 0 0 26px" hidden>⚠️ <%= t('pedit.fedi_audio_oneway') %></p>
-          <%# De scripts van deze pagina staan in assets/js/mod/post-edit.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
-        <% } %>
-        
-        <% // Poll (federates as an AS2 Question). Free feature. A poll with votes is frozen.
-           var _poll = null; try { _poll = post.poll_json ? JSON.parse(post.poll_json) : null; } catch (e) { _poll = null; }
-           var _pollLocked = (typeof pollLocked !== 'undefined' && pollLocked);
-           var _pollOpts = (_poll && Array.isArray(_poll.options) && _poll.options.length) ? _poll.options : [{ name: '' }, { name: '' }]; %>
-        <label class="pe-checkbox" style="margin-top:8px">
-          <input type="checkbox" name="poll_enabled" value="1" id="pe-poll-toggle" <%= _poll ? 'checked' : '' %> <%= _pollLocked ? 'disabled' : '' %>>
-          <span>📊 <%= t('pedit.poll_label') %></span>
-        </label>
-        <style>
-          .pe-poll { margin-top: 6px; }
-          .pe-poll-locked { font-size: 12px; opacity: .7; margin: 0 0 6px; }
-          .pe-poll-row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
-          .pe-poll-opt { flex: 1; min-width: 0; box-sizing: border-box; font-size: 13px; padding: 7px 9px; border-radius: 7px; border: 1px solid var(--rule, rgba(128,128,128,.35)); background: transparent; color: inherit; }
-          .pe-poll-opt:focus { outline: none; border-color: var(--accent); }
-          .pe-poll-del { flex: 0 0 auto; width: 30px; height: 32px; display: inline-flex; align-items: center; justify-content: center; font-size: 18px; line-height: 1; border: 1px solid var(--rule, rgba(128,128,128,.35)); border-radius: 7px; background: transparent; color: var(--ink-muted, #999); cursor: pointer; transition: color .12s, border-color .12s; }
-          .pe-poll-del:hover { border-color: #c0392b; color: #c0392b; }
-          .pe-poll-del[hidden] { display: none; }
-          .pe-poll-add { display: block; width: fit-content; font-size: 12.5px; padding: 6px 12px; border-radius: 7px; border: 1px dashed var(--rule, rgba(128,128,128,.45)); background: transparent; color: inherit; cursor: pointer; margin: 0 0 10px; transition: color .12s, border-color .12s; }
-          .pe-poll-add:hover { border-color: var(--accent); color: var(--accent); }
-          .pe-poll-add:disabled { opacity: .4; cursor: default; border-style: dashed; }
-          .pe-poll-durlabel { display: block; font-size: 12.5px; opacity: .8; margin: 8px 0 4px; }
-          .pe-poll-dur { padding: 7px 9px; border-radius: 7px; border: 1px solid var(--rule, rgba(128,128,128,.35)); background: transparent; color: inherit; font-size: 13px; }
-        </style>
-        <%# NB: keep the condition INSIDE the quoted attribute — escaped output (equals-tag)
-            turns a whole emitted style attribute into entity soup (fields showed always). %>
-        <div id="pe-poll-fields" class="pe-poll" style="<%= _poll ? '' : 'display:none' %>">
-          <% if (_pollLocked) { %><p class="pe-poll-locked"><%= t('pedit.poll_locked') %></p><% } %>
-          <div id="pe-poll-opts" data-ph="<%= t('pedit.poll_option_ph') %>" data-del="<%= t('pedit.poll_remove') %>">
-            <% _pollOpts.forEach(function (o) { %>
-              <div class="pe-poll-row">
-                <input type="text" name="poll_option" class="pe-poll-opt" maxlength="100" value="<%= (o && o.name) || '' %>" placeholder="<%= t('pedit.poll_option_ph') %>" <%= _pollLocked ? 'disabled' : '' %>>
-                <% if (!_pollLocked) { %><button type="button" class="pe-poll-del" aria-label="<%= t('pedit.poll_remove') %>" title="<%= t('pedit.poll_remove') %>">&times;</button><% } %>
-              </div>
-            <% }); %>
-          </div>
-          <% if (!_pollLocked) { %><button type="button" id="pe-poll-add" class="pe-poll-add">+ <%= t('pedit.poll_add') %></button><% } %>
-          <label class="pe-checkbox">
-            <input type="checkbox" name="poll_multiple" value="1" <%= (_poll && _poll.multiple) ? 'checked' : '' %> <%= _pollLocked ? 'disabled' : '' %>>
-            <span><%= t('pedit.poll_multiple') %></span>
-          </label>
-          <label class="pe-poll-durlabel"><%= t('pedit.poll_duration') %></label>
-          <select name="poll_duration" class="pe-poll-dur" <%= _pollLocked ? 'disabled' : '' %>>
-            <% [['300','5m'],['1800','30m'],['3600','1h'],['21600','6h'],['43200','12h'],['86400','1d'],['259200','3d'],['604800','7d']].forEach(function (d) { %>
-              <option value="<%= d[0] %>" <%= d[0] === '86400' ? 'selected' : '' %>><%= t('pedit.poll_dur_' + d[1]) %></option>
-            <% }); %>
-          </select>
-        </div>
-        
-        <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
-          <label class="pe-checkbox">
-            <input type="checkbox" name="fan_only" value="1" <%= post.fan_only ? 'checked' : '' %>>
-            <span>🔒 <%= t('pedit.fan_only_label') %></span>
-          </label>
-          <label class="pe-checkbox" style="margin-top:8px">
-            <input type="checkbox" name="paid" value="1" id="pe-paid" <%= post.paid ? 'checked' : '' %>>
-            <span>💶 Betaalde post (supporters ontgrendelen met Patreon)</span>
-          </label>
-          <div id="pe-paid-price" style="margin:6px 0 0 26px;<%= post.paid ? '' : 'display:none' %>">
-            <label style="font-size:.85rem;opacity:.8">Vereist steunbedrag (euro, leeg = standaard)
-              <input type="text" name="paid_min_eur" inputmode="decimal" style="width:100px"
-                     value="<%= post.paid_min_cents ? (post.paid_min_cents/100).toFixed(2) : '' %>">
-            </label>
-          </div>
-          
-          <% var _scheduled = !!(post.publish_at && (post.status === 'scheduled' || Date.parse(String(post.publish_at).replace(' ', 'T')) > Date.now())); %>
-          <label class="pe-checkbox" style="margin-top:8px">
-            <input type="checkbox" name="schedule_enabled" value="1" id="pe-sched-toggle" <%= _scheduled ? 'checked' : '' %>>
-            <span>📅 <%= t('pedit.schedule_label') %></span>
-          </label>
-          <div id="pe-sched-fields" style="margin-top:6px;<%= _scheduled ? '' : 'display:none' %>">
-            <label style="display:block;font-size:12.5px;opacity:.8;margin-bottom:4px"><%= t('pedit.publish_at_label') %></label>
-            <input type="datetime-local" id="pe-publish-at" name="publish_at" <%= _scheduled ? '' : 'disabled' %> data-iso="<%= post.publish_at || '' %>" value="" style="padding:8px 10px;border-radius:8px;border:1px solid var(--rule,rgba(128,128,128,.4));background:transparent;color:inherit">
-            <% if (post.status === 'scheduled' && post.publish_at) { %><div style="font-size:12px;opacity:.7;margin-top:4px"><span id="pe-sched-when" data-iso="<%= post.publish_at %>" data-label="<%= t('pedit.scheduled_prefix') %>">⏳ <%= t('pedit.scheduled_for', { d: post.publish_at }) %></span></div><% } %>
-            <div style="font-size:11.5px;opacity:.65;margin-top:4px"><%= t('pedit.schedule_hint') %></div>
-          </div>
-          
-        <% } %>
       </div>
     </section>
@@ -368,10 +183,10 @@
     <%# ── ACTIONS (sticky at bottom) ──────────────────────────── %>
     <div class="pe-actions">
-      <a href="<%= _base %><%= isNew ? '/' : '/' + post.slug %>" class="pe-btn"><%= t('pedit.cancel') %></a>
+      <a href="<%= isNew ? '/' : '/' + post.slug %>" class="pe-btn">Annuleren</a>
       <div class="pe-actions-spacer"></div>
       <% if (!isNew && post.status !== 'published') { %>
-        <button type="submit" name="action" value="publish" class="pe-btn pe-btn-success">📤 <%= t('pedit.publish') %></button>
+        <button type="submit" name="action" value="publish" class="pe-btn pe-btn-success">📤 Publish</button>
       <% } %>
-      <button type="submit" class="pe-btn pe-btn-primary">💾 <%= t('pedit.save') %></button>
+      <button type="submit" class="pe-btn pe-btn-primary">💾 Opslaan</button>
     </div>
   </form>
@@ -526,86 +341,4 @@
 }
 
-/* ─── Post type: segmented control + type-aware panels ──────────── */
-.pe-typeseg {
-  display: grid;
-  grid-template-columns: repeat(4, 1fr);
-  gap: 0.4rem;
-}
-.pe-typeseg-btn {
-  display: flex; flex-direction: column; align-items: center; gap: 0.25rem;
-  padding: 0.7rem 0.4rem;
-  border: 1px solid var(--rule);
-  border-radius: 9px;
-  background: var(--paper-2);
-  color: var(--ink-soft);
-  font-size: 0.85rem; font-weight: 600;
-  cursor: pointer;
-  transition: border-color 120ms, background 120ms, color 120ms;
-}
-.pe-typeseg-btn:hover { border-color: var(--accent); color: var(--ink); }
-.pe-typeseg-btn .pe-typeseg-ic { font-size: 1.3rem; line-height: 1; }
-.pe-typeseg-btn.is-active {
-  border-color: var(--accent);
-  background: color-mix(in srgb, var(--accent) 14%, var(--paper-2));
-  color: var(--ink);
-}
-.pe-typeseg-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
-@media (max-width: 480px) {
-  .pe-typeseg-btn { font-size: 0.78rem; padding: 0.6rem 0.25rem; }
-  .pe-typeseg-btn .pe-typeseg-ic { font-size: 1.15rem; }
-}
-
-.pe-type-panel { margin-top: 0.2rem; }
-
-/* Audio drop/upload zone */
-.pe-audio-up {
-  display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
-  text-align: center;
-  padding: 1.4rem 1rem;
-  border: 2px dashed var(--rule);
-  border-radius: 10px;
-  background: var(--paper-2);
-  color: var(--ink-soft);
-  cursor: pointer;
-  transition: border-color 120ms, background 120ms;
-}
-.pe-audio-up:hover,
-.pe-audio-up:focus-visible { border-color: var(--accent); outline: none; }
-.pe-audio-up.is-drag {
-  border-color: var(--accent);
-  background: color-mix(in srgb, var(--accent) 12%, var(--paper-2));
-}
-.pe-audio-up .pe-audio-up-ic { font-size: 1.7rem; line-height: 1; }
-.pe-audio-up strong { color: var(--ink); font-size: 0.95rem; }
-.pe-audio-up small { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); max-width: 42ch; }
-
-.pe-audio-list { list-style: none; margin: 0.7rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
-.pe-audio-item {
-  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
-  padding: 0.5rem 0.7rem;
-  border: 1px solid var(--rule);
-  border-radius: 7px;
-  background: var(--paper-2);
-  font-size: 0.85rem;
-}
-.pe-audio-item-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.pe-audio-item-state { flex: none; font-size: 0.8rem; color: var(--ink-soft); }
-.pe-audio-item.is-done { border-color: color-mix(in srgb, var(--accent) 50%, var(--rule)); }
-.pe-audio-item.is-done .pe-audio-item-state { color: var(--accent); }
-.pe-audio-item.is-fail .pe-audio-item-state { color: #d9534f; }
-
-/* Video URL row */
-.pe-video-row { display: flex; gap: 0.5rem; }
-.pe-video-row input { flex: 1 1 auto; min-width: 0; }
-.pe-video-row input {
-  box-sizing: border-box; padding: 0.6rem 0.75rem;
-  border: 1px solid var(--rule); border-radius: 6px;
-  background: var(--paper-2); color: var(--ink); font-size: 0.95rem;
-}
-.pe-video-row input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
-.pe-video-row .pe-btn { flex: none; }
-
-.pe-foto-hint { margin: 0; font-size: 0.88rem; color: var(--ink-soft); }
-
 /* ─── Editor frame (WYSIWYG: top toolbar, contenteditable body, status bar) ─── */
 .pe-editor-frame {
@@ -633,6 +366,4 @@
   width: 32px; height: 32px;
   padding: 0;
-  touch-action: manipulation; /* snappy taps on mobile, no double-tap zoom */
-  -webkit-user-select: none; user-select: none;
   background: transparent;
   border: 1px solid transparent;
@@ -645,21 +376,14 @@
   -webkit-tap-highlight-color: transparent;
 }
-/* Hover only on real hover-capable devices — on touch :hover otherwise "sticks"
-   after a tap, making the active/inactive state unreadable. */
-@media (hover: hover) {
-  .pe-toolbar button:hover {
-    background: var(--paper-2);
-    color: var(--accent);
-  }
+.pe-toolbar button:hover {
+  background: var(--paper-2);
+  color: var(--accent);
 }
 .pe-toolbar button:active { transform: scale(.94); }
-/* Active formatting = unmistakably filled with the accent colour. On mobile
-   it's immediately clear when e.g. bold is ON (tap again = off). */
 .pe-toolbar button.is-active {
-  background: var(--accent);
-  color: #fff;
-  border-color: var(--accent);
-}
-.pe-toolbar button.is-active svg { color: #fff; }
+  background: color-mix(in srgb, var(--accent) 14%, var(--paper-2));
+  color: var(--accent);
+  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
+}
 .pe-toolbar button svg { width: 16px; height: 16px; }
 .pe-toolbar button.pe-tb-text { font-family: var(--font-display, serif); font-weight: 700; }
@@ -671,42 +395,4 @@
 }
 .pe-toolbar-spacer { flex: 1; }
-
-/* ─── Full-screen writing mode ─── */
-.fs-icon-compress { display: none; }
-.pe-editor-frame.pe-fs .fs-icon-expand { display: none; }
-.pe-editor-frame.pe-fs .fs-icon-compress { display: inline; }
-.pe-editor-frame.pe-fs {
-  position: fixed; inset: 0; z-index: 1000;
-  margin: 0; border-top: 0;
-  height: 100dvh;
-  background: var(--paper);
-  overflow: hidden;   /* only the text field scrolls, not the frame itself */
-}
-/* In fullscreen the writing field fills the remaining space and scrolls itself
-   (max-height: none overrides the mobile box limit below). */
-.pe-editor-frame.pe-fs .pe-editor {
-  flex: 1 1 auto; min-height: 0; height: auto; max-height: none;
-}
-/* iOS: in fullscreen the toolbar sits at the very top → push it below the
-   camera / Dynamic Island with the top safe-area inset. */
-.pe-editor-frame.pe-fs .pe-toolbar { padding-top: calc(.4rem + env(safe-area-inset-top, 0px)); }
-/* NB: deliberately NO overflow:hidden on html/body in fullscreen — that could get
-   stuck (e.g. leaving fullscreen via navigation) and would block scrolling on the
-   whole page. The fullscreen frame (position:fixed, inset:0) already covers the page,
-   and on touch scrollbars are hidden → no double bar needed. */
-
-/* "Done" button: only visible in fullscreen (left side of toolbar), clear
-   accent-pill instead of a square icon. */
-/* Hide rule more specific than ".pe-toolbar button" (otherwise that wins and the
-   Done button also shows inline as a small button). Only shown in fullscreen → large. */
-.pe-toolbar button.pe-fs-done { display: none; }
-.pe-editor-frame.pe-fs .pe-fs-done {
-  display: inline-flex; align-items: center; gap: .35rem;
-  width: auto !important; height: auto !important; min-height: 40px;
-  padding: .55rem 1.3rem !important; margin-right: .5rem;
-  background: var(--accent); color: #fff;
-  font-weight: 700; font-size: 1.05rem; border-radius: 999px;
-}
-.pe-editor-frame.pe-fs .pe-fs-done:hover { background: var(--accent); color: #fff; }
 
 /* Editor body — looks like prose, behaves like a textarea */
@@ -726,40 +412,14 @@
 }
 .pe-editor:focus { outline: none; }
-/* Inline (non-fullscreen) the writing field simply grows with the content — no
-   internal scroll-box (scroll-within-scroll is confusing). On mobile/tablet typing
-   always goes fullscreen (see JS), where the field fills the page and is the sole
-   scroller. */
-.pe-editor-frame:not(.pe-fs) .pe-editor { overflow: visible; }
-
-/* Touch: the inline content field is not a text field but a tap target → fullscreen
-   editing. A "✎ Tap to edit" pill sticks to the bottom of the container,
-   so the hint is always visible without sitting in the middle of the text. */
-.pe-editor.pe-tap-to-edit { cursor: pointer; }
-.pe-edit-hint { display: none; }
-@media (pointer: coarse) {
-  .pe-editor-frame:not(.pe-fs) .pe-edit-hint {
-    display: block;
-    position: sticky;
-    bottom: 4.5rem;   /* above the sticky Save/Cancel bar */
-    width: max-content;
-    max-width: calc(100% - 2rem);
-    margin: .4rem auto;
-    background: var(--accent); color: #fff;
-    font-size: .9rem; font-weight: 700; padding: .5rem 1.1rem; border-radius: 999px;
-    box-shadow: 0 4px 14px rgba(0,0,0,.35);
-    pointer-events: none; text-align: center; white-space: nowrap;
-  }
-  /* Not relevant on touch (drag/select belongs to inline editing on desktop). */
-  .pe-content-hint { display: none; }
-  /* Inline (non-fullscreen) on touch: keep it simple — you don't edit here anyway,
-     so no formatting toolbar and no border. Just the content preview + the
-     "tap to edit" pill. The toolbar only appears in fullscreen. */
-  .pe-editor-frame:not(.pe-fs) .pe-toolbar { display: none; }
-  .pe-editor-frame:not(.pe-fs) { border-top: 0; }
-  .pe-editor-frame:not(.pe-fs) .pe-editor { min-height: 8rem; }
-}
 .pe-editor.is-dragover {
   outline: 2px dashed var(--accent);
   outline-offset: -10px;
+}
+.pe-editor[data-placeholder]:empty::before,
+.pe-editor[data-placeholder]:has(br:only-child)::before {
+  content: attr(data-placeholder);
+  color: var(--ink-muted, #999);
+  pointer-events: none;
+  opacity: .55;
 }
 /* Inline prose styles inside the editor — match the public post styling so
@@ -1118,19 +778,29 @@
 }
 
-/* Pin: checkbox + ▲▼-stepper with description (instead of a raw rank number). */
-.pe-pin { display: flex; flex-direction: column; gap: 0.45rem; }
-.pe-pin-pos { display: flex; align-items: center; gap: 0.55rem; padding-left: 1.65rem; }
-.pe-pin-pos[hidden] { display: none; }
-.pe-pin-steps { display: inline-flex; border: 1px solid var(--rule); border-radius: 6px; overflow: hidden; }
-.pe-pin-btn {
-  width: 30px; height: 28px; padding: 0;
-  border: none; background: var(--paper-2); color: var(--ink);
-  cursor: pointer; font-size: 0.68rem; line-height: 1;
-  display: inline-flex; align-items: center; justify-content: center;
-}
-.pe-pin-btn + .pe-pin-btn { border-left: 1px solid var(--rule); }
-.pe-pin-btn:hover { background: color-mix(in srgb, var(--accent) 14%, var(--paper-2)); color: var(--accent); }
-.pe-pin-btn:disabled { opacity: 0.35; cursor: default; }
-.pe-pin-label { font-size: 0.85rem; color: var(--accent); font-weight: 600; }
+/* Pinned rank: replaces the old boolean checkbox with a number input.
+   Stack the label / input / hint vertically — small footprint. */
+.pe-pinned-rank {
+  display: flex; flex-direction: column; gap: 0.25rem;
+  font-size: 0.95rem;
+  color: var(--ink);
+}
+.pe-pinned-rank input[type="number"] {
+  width: 80px;
+  padding: 0.4rem 0.6rem; min-height: 36px;
+  border: 1px solid var(--rule); border-radius: 6px;
+  background: var(--paper-2); color: var(--ink);
+  font-family: var(--font-ui, system-ui), sans-serif;
+  font-size: 0.95rem;
+  font-variant-numeric: tabular-nums;
+  -webkit-appearance: none; appearance: textfield;
+}
+.pe-pinned-rank input[type="number"]:focus {
+  outline: 2px solid var(--accent); outline-offset: -1px;
+  border-color: var(--accent);
+}
+.pe-pinned-rank small {
+  font-size: 0.78rem;
+  color: var(--ink-muted, var(--ink-soft));
+}
 
 /* ─── Sticky action footer ──────────────────────────────────────── */
@@ -1141,5 +811,5 @@
   gap: 0.5rem;
   padding: 0.85rem 1rem;
-  margin: 0.5rem -1rem 0;   /* extend to viewport edges on a narrow container */
+  margin: 0.5rem -1rem 0;   /* extend to viewport edges on tight container */
   background: color-mix(in srgb, var(--paper) 94%, transparent);
   -webkit-backdrop-filter: blur(8px);
@@ -1150,48 +820,507 @@
 .pe-actions-spacer { flex: 1; }
 
-/* The editor is a focus screen: hide the mobile site tab bar (Home/Search/…)
-   so two bars don't stack at the bottom (Save bar + tab bar). The
-   Save/Cancel bar then becomes the only bottom bar → plain bottom:0,
-   no tab offset needed. On desktop the tab bar is already hidden. */
-body:has(.post-edit-page) .bottom-tab { display: none; }
-/* Audio player (if playing) no longer lifted 56px for the now-hidden
-   tab bar, otherwise it would float above the action bar. */
-body:has(.post-edit-page) .audio-player { bottom: 0; }
-
-/* ── Image editor (rotate / crop / mirror) ── */
-.imed-backdrop {
-  position: fixed; inset: 0; z-index: 9999;
-  display: flex; align-items: center; justify-content: center;
-  background: rgba(0,0,0,.62); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
-  padding: 14px;
-}
-.imed-modal {
-  display: flex; flex-direction: column; gap: 12px;
-  width: 100%; max-width: 640px; max-height: 92vh;
-  background: var(--paper, #fff); color: var(--ink, #111);
-  border: 1px solid var(--rule, rgba(128,128,128,.3)); border-radius: 14px;
-  padding: 14px; box-sizing: border-box;
-}
-.imed-stage {
-  height: 58vh; flex: 0 0 auto;
-  background: var(--paper-2, rgba(128,128,128,.08)); border-radius: 10px; overflow: hidden;
-}
-.imed-stage img { display: block; max-width: 100%; }
-/* Cropper's container must fill the full stage height — otherwise it collapses
-   in a flex column without an explicit height → empty edit window (no image). */
-.imed-stage .cropper-container { width: 100% !important; height: 100% !important; }
-.imed-tools {
-  display: flex; flex-wrap: wrap; gap: 6px; justify-content: center;
-}
-.imed-tools button {
-  width: 42px; height: 42px; font-size: 18px; line-height: 1;
-  border: 1px solid var(--rule, rgba(128,128,128,.4)); border-radius: 10px;
-  background: var(--paper-2, transparent); color: inherit; cursor: pointer;
-}
-.imed-tools button:hover { border-color: var(--accent); }
-.imed-actions { display: flex; gap: 8px; justify-content: flex-end; }
+/* On a body that already has a fixed bottom-tab nav (mobile), nudge the
+   sticky footer up so it doesn't sit under it. The bottom-tab is ~64px tall. */
+body.has-bottom-tab .pe-actions {
+  bottom: 64px;
+}
 </style>
 
-
+<script>
+(function() {
+
+  // ── Cover upload ────────────────────────────────────────────────
+  const coverField   = document.getElementById('cover-upload-field');
+  const coverTrigger = document.getElementById('cover-upload-trigger');
+  const coverUrl     = document.getElementById('cover-url-field');
+  const coverStatus  = document.getElementById('cover-upload-status');
+  const coverWrap    = document.getElementById('cover-preview-wrap');
+  const coverImg     = document.getElementById('cover-preview-img');
+
+  async function uploadImage(file) {
+    const fd = new FormData();
+    fd.append('image', file);
+    const res = await fetch('/posts/upload-image', { method: 'POST', body: fd });
+    if (!res.ok) {
+      const j = await res.json().catch(() => ({}));
+      throw new Error(j.error || ('Upload failed (' + res.status + ')'));
+    }
+    return await res.json();   // {url, size, mime}
+  }
+
+  function showCoverPreview(url) {
+    if (!coverWrap || !coverImg) return;
+    if (url) {
+      coverImg.src = url;
+      coverImg.hidden = false;
+      coverWrap.removeAttribute('data-empty');
+      const emptyIcon = coverWrap.querySelector('.pe-cover-empty');
+      if (emptyIcon) emptyIcon.remove();
+    } else {
+      coverImg.hidden = true;
+      coverImg.src = '';
+      coverWrap.setAttribute('data-empty', '');
+      if (!coverWrap.querySelector('.pe-cover-empty')) {
+        const span = document.createElement('span');
+        span.className = 'pe-cover-empty';
+        span.textContent = '🖼';
+        coverWrap.appendChild(span);
+      }
+    }
+  }
+
+  if (coverTrigger && coverField) {
+    coverTrigger.addEventListener('click', () => coverField.click());
+  }
+  if (coverField) {
+    coverField.addEventListener('change', async () => {
+      if (!coverField.files[0]) return;
+      coverStatus.classList.remove('is-error');
+      coverStatus.textContent = 'Uploaden…';
+      try {
+        const j = await uploadImage(coverField.files[0]);
+        coverUrl.value = j.url;
+        showCoverPreview(j.url);
+        coverStatus.textContent = 'Geüpload ✓';
+        setTimeout(() => { coverStatus.textContent = ''; }, 2000);
+      } catch (e) {
+        coverStatus.classList.add('is-error');
+        coverStatus.textContent = 'Mislukt: ' + e.message;
+      }
+    });
+  }
+  // Live-update preview when user pastes a URL manually
+  if (coverUrl) {
+    coverUrl.addEventListener('input', () => {
+      const v = coverUrl.value.trim();
+      if (v) showCoverPreview(v); else showCoverPreview('');
+    });
+  }
+
+  // ── WYSIWYG editor (P58) ────────────────────────────────────────
+  // Architecture:
+  //   - Visible <div contenteditable> (`#content-editor`) is what the user
+  //     types in; it shows real HTML (formatted, not raw markup).
+  //   - Hidden <input name="content"> (`#content-hidden`) is what submits.
+  //     On submit we serialize the editor's HTML into it, with shortcode
+  //     chips reduced back to their [[track:UUID]]/[[album:Name]]/[[playlist:slug]] text.
+  //   - Initial content comes from a <script type="application/json"> tag
+  //     to avoid HTML-escape-into-DOM issues; we set innerHTML once on load
+  //     and walk text nodes to render shortcode tokens as chips.
+  const contentField  = document.getElementById('content-upload-field');
+  const contentBtn    = document.getElementById('insert-image-btn');
+  const contentStatus = document.getElementById('content-upload-status');
+  const editor        = document.getElementById('content-editor');
+  const hiddenField   = document.getElementById('content-hidden');
+  const charCountEl   = document.getElementById('char-count');
+  const initialEl     = document.getElementById('initial-content');
+  const toolbar       = document.getElementById('pe-toolbar');
+  const form          = editor && editor.closest('form');
+
+  if (!editor) return;
+
+  // ── Shortcode chip rendering / serialization ────────────────────
+  // Pattern matches [[track:UUID]] / [[album:any text]] / [[playlist:slug]]
+  // — but we DON'T want to chipify text the user is mid-typing inside an
+  // HTML attribute; since chipify only walks text nodes (never attribute
+  // values) that's already safe.
+  const SC_RE = /\[\[(track|album|playlist):([^\]]+)\]\]/g;
+
+  const SC_ICONS = {
+    track:    '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="6 4 20 12 6 20 6 4"/></svg>',
+    album:    '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>',
+    playlist: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="15" y2="18"/><polygon points="3 5 3 13 9 9"/></svg>',
+  };
+
+  function chipLabel(kind, value) {
+    if (kind === 'track') {
+      // UUIDs are noisy — show a 6-char prefix for visual hint
+      const v = String(value || '');
+      return 'Track ' + (v.length > 8 ? v.slice(0, 6) + '…' : v);
+    }
+    if (kind === 'album')    return 'Album: ' + value;
+    if (kind === 'playlist') return 'Playlist: ' + value;
+    return value;
+  }
+
+  function makeChip(kind, value) {
+    const span = document.createElement('span');
+    span.className = 'sc-chip';
+    span.contentEditable = 'false';
+    span.setAttribute('data-sc', kind + ':' + value);
+    span.innerHTML =
+      '<span class="sc-chip-icon" aria-hidden="true">' + (SC_ICONS[kind] || '') + '</span>' +
+      '<span class="sc-chip-label"></span>';
+    span.querySelector('.sc-chip-label').textContent = chipLabel(kind, value);
+    return span;
+  }
+
+  // Walk text nodes inside `root` and replace [[type:value]] tokens with chips.
+  function chipifyShortcodes(root) {
+    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
+    const targets = [];
+    while (walker.nextNode()) {
+      const n = walker.currentNode;
+      // Skip text inside existing chips (their .sc-chip-label is set via .textContent so the [[...]] text never appears)
+      if (n.parentElement && n.parentElement.closest('.sc-chip')) continue;
+      if (SC_RE.test(n.nodeValue)) targets.push(n);
+      SC_RE.lastIndex = 0;
+    }
+    for (const node of targets) {
+      const txt = node.nodeValue;
+      const frag = document.createDocumentFragment();
+      let last = 0;
+      let m;
+      SC_RE.lastIndex = 0;
+      while ((m = SC_RE.exec(txt)) !== null) {
+        if (m.index > last) frag.appendChild(document.createTextNode(txt.slice(last, m.index)));
+        frag.appendChild(makeChip(m[1], m[2].trim()));
+        last = m.index + m[0].length;
+      }
+      if (last < txt.length) frag.appendChild(document.createTextNode(txt.slice(last)));
+      node.parentNode.replaceChild(frag, node);
+    }
+  }
+
+  // Inverse of chipify: clone the editor, replace every chip with its text.
+  function serializeChips(rootClone) {
+    const chips = rootClone.querySelectorAll('.sc-chip[data-sc]');
+    for (const c of chips) {
+      const txt = '[[' + c.getAttribute('data-sc') + ']]';
+      c.replaceWith(document.createTextNode(txt));
+    }
+  }
+
+  // ── Boot: load initial content as HTML, then render shortcodes as chips
+  try {
+    const initial = JSON.parse(initialEl.textContent || '""');
+    editor.innerHTML = initial || '';
+    chipifyShortcodes(editor);
+  } catch (e) {
+    console.error('[editor] could not parse initial content', e);
+    editor.innerHTML = '';
+  }
+
+  // ── Char counter
+  function updateCharCount() {
+    const text = (editor.innerText || '').replace(/\s+/g, ' ').trim();
+    if (charCountEl) charCountEl.textContent = String(text.length);
+  }
+  updateCharCount();
+  editor.addEventListener('input', updateCharCount);
+
+  // ── Toolbar wiring
+  function execCmd(cmd, arg) {
+    editor.focus();
+    document.execCommand(cmd, false, arg);
+    updateToolbarState();
+    updateCharCount();
+  }
+  function wrapCode() {
+    const sel = window.getSelection();
+    if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return;
+    const range = sel.getRangeAt(0);
+    const code = document.createElement('code');
+    code.textContent = sel.toString();
+    range.deleteContents();
+    range.insertNode(code);
+    // Move caret after the new node
+    range.setStartAfter(code);
+    range.collapse(true);
+    sel.removeAllRanges();
+    sel.addRange(range);
+    editor.focus();
+  }
+  function linkPrompt() {
+    const url = window.prompt('Link URL (https://… of /pad)');
+    if (!url) return;
+    execCmd('createLink', url);
+  }
+
+  if (toolbar) {
+    toolbar.addEventListener('click', (e) => {
+      const btn = e.target.closest('button[data-cmd]');
+      if (!btn) return;
+      e.preventDefault();
+      const cmd = btn.dataset.cmd;
+      const arg = btn.dataset.arg || null;
+      if (cmd === 'link-prompt') linkPrompt();
+      else if (cmd === 'code-wrap') wrapCode();
+      else execCmd(cmd, arg);
+    });
+  }
+
+  // Reflect bold/italic/list state on the toolbar buttons
+  function updateToolbarState() {
+    if (!toolbar) return;
+    const cmds = ['bold', 'italic', 'underline', 'insertUnorderedList', 'insertOrderedList'];
+    for (const cmd of cmds) {
+      const btn = toolbar.querySelector('button[data-cmd="' + cmd + '"]');
+      if (!btn) continue;
+      try { btn.classList.toggle('is-active', document.queryCommandState(cmd)); } catch(_) {}
+    }
+  }
+  document.addEventListener('selectionchange', () => {
+    if (document.activeElement === editor) updateToolbarState();
+  });
+
+  // Keyboard shortcuts: Ctrl/Cmd + B/I/U/K
+  editor.addEventListener('keydown', (e) => {
+    const mod = e.ctrlKey || e.metaKey;
+    if (!mod) return;
+    const k = e.key.toLowerCase();
+    if (k === 'b') { e.preventDefault(); execCmd('bold'); }
+    else if (k === 'i') { e.preventDefault(); execCmd('italic'); }
+    else if (k === 'u') { e.preventDefault(); execCmd('underline'); }
+    else if (k === 'k') { e.preventDefault(); linkPrompt(); }
+  });
+
+  // Paste: keep it simple — strip formatting unless user wants it. Default
+  // execCommand 'paste' includes Word/Google-Docs garbage. We accept inline
+  // styles from clipboard only when shift is held — otherwise plain text.
+  editor.addEventListener('paste', (e) => {
+    if (e.shiftKey) return; // user wants formatted paste
+    const text = (e.clipboardData || window.clipboardData).getData('text/plain');
+    if (text == null) return;
+    e.preventDefault();
+    document.execCommand('insertText', false, text);
+  });
+
+  // ── Image upload (button + drag-drop into the editor)
+  async function uploadAndInsertImage(file) {
+    contentStatus.classList.remove('is-error');
+    contentStatus.textContent = 'Uploaden…';
+    try {
+      const j = await uploadImage(file);
+      const img = '<img src="' + j.url + '" alt="">';
+      editor.focus();
+      document.execCommand('insertHTML', false, img);
+      contentStatus.textContent = 'Ingevoegd ✓';
+      setTimeout(() => { contentStatus.textContent = ''; }, 2000);
+      updateCharCount();
+    } catch (e) {
+      contentStatus.classList.add('is-error');
+      contentStatus.textContent = 'Mislukt: ' + e.message;
+    }
+  }
+
+  if (contentBtn && contentField) {
+    contentBtn.addEventListener('click', () => contentField.click());
+    contentField.addEventListener('change', () => {
+      if (contentField.files[0]) uploadAndInsertImage(contentField.files[0]);
+      contentField.value = '';
+    });
+
+    editor.addEventListener('dragover', (e) => {
+      if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
+        e.preventDefault();
+        editor.classList.add('is-dragover');
+      }
+    });
+    editor.addEventListener('dragleave', () => editor.classList.remove('is-dragover'));
+    editor.addEventListener('drop', async (e) => {
+      editor.classList.remove('is-dragover');
+      const files = e.dataTransfer && e.dataTransfer.files;
+      if (!files || !files.length) return;
+      e.preventDefault();
+      for (const f of files) {
+        if (f.type.startsWith('image/')) await uploadAndInsertImage(f);
+      }
+    });
+  }
+
+  // ── Insert chip helpers (track / playlist)
+  function insertChip(kind, value) {
+    editor.focus();
+    const chip = makeChip(kind, value);
+    // Insert at caret using the Selection API (execCommand insertNode)
+    const sel = window.getSelection();
+    if (sel && sel.rangeCount > 0) {
+      const range = sel.getRangeAt(0);
+      range.deleteContents();
+      range.insertNode(chip);
+      // Insert a trailing space so the user can keep typing after the chip
+      const space = document.createTextNode('\u00A0');
+      chip.after(space);
+      range.setStartAfter(space);
+      range.collapse(true);
+      sel.removeAllRanges();
+      sel.addRange(range);
+    } else {
+      editor.appendChild(chip);
+      editor.appendChild(document.createTextNode('\u00A0'));
+    }
+    updateCharCount();
+  }
+
+  // ── Track insert: opens the track-picker modal (P59)
+  const trackBtn = document.getElementById('insert-track-btn');
+  const trackPicker = document.getElementById('track-picker');
+  if (trackBtn && trackPicker) {
+    const tpList   = document.getElementById('tp-list');
+    const tpEmpty  = document.getElementById('tp-empty');
+    const tpSearch = document.getElementById('tp-search');
+    let tpCache = null;       // cached tracks list (fetched once per page load)
+    let tpLastFocus = null;   // element to restore focus to on close
+
+    const SVG_NOTE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 17V5l12-2v12"/><circle cx="6" cy="17" r="3"/><circle cx="18" cy="15" r="3"/></svg>';
+
+    function fmtDur(sec) {
+      sec = Math.max(0, Math.floor(sec || 0));
+      const m = Math.floor(sec / 60), s = sec % 60;
+      return m + ':' + String(s).padStart(2, '0');
+    }
+    function escAttr(s) {
+      return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({
+        '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
+      }[c]));
+    }
+
+    function renderList(filter) {
+      if (!Array.isArray(tpCache)) return;
+      const q = (filter || '').trim().toLowerCase();
+      const filtered = q
+        ? tpCache.filter(t =>
+            (t.title  || '').toLowerCase().includes(q) ||
+            (t.artist || '').toLowerCase().includes(q))
+        : tpCache;
+
+      if (!filtered.length) {
+        tpList.innerHTML = '';
+        tpEmpty.textContent = q ? 'Geen tracks gevonden voor "' + q + '"' : 'Nog geen tracks. Upload via Beheer → Audio.';
+        tpList.appendChild(tpEmpty);
+        return;
+      }
+
+      tpList.innerHTML = filtered.map(t => {
+        const cov = t.cover
+          ? '<span class="tp-cover" style="background-image:url(\'' + escAttr(t.cover) + '\')"></span>'
+          : '<span class="tp-cover tp-cover-empty">' + SVG_NOTE + '</span>';
+        const dis = t.playable ? '' : ' aria-disabled="true"';
+        const sub = t.artist ? '<span class="tp-row-artist">' + escAttr(t.artist) + '</span>' : '';
+        return (
+          '<button type="button" class="tp-row" role="option" data-track-id="' + escAttr(t.id) + '"' + dis + '>' +
+            cov +
+            '<span class="tp-meta">' +
+              '<span class="tp-row-title">' + escAttr(t.title) + '</span>' +
+              sub +
+            '</span>' +
+            '<span class="tp-duration">' + fmtDur(t.duration) + '</span>' +
+          '</button>'
+        );
+      }).join('');
+    }
+
+    async function loadTracks() {
+      if (Array.isArray(tpCache)) return tpCache;
+      tpEmpty.textContent = 'Tracks laden…';
+      try {
+        const r = await fetch('/admin/playlists/api/tracks', { credentials: 'same-origin' });
+        const j = await r.json();
+        tpCache = (j && j.ok && Array.isArray(j.tracks)) ? j.tracks : [];
+      } catch (e) {
+        tpCache = [];
+        tpEmpty.textContent = 'Kon tracks niet laden: ' + e.message;
+      }
+      return tpCache;
+    }
+
+    function openPicker() {
+      tpLastFocus = document.activeElement;
+      trackPicker.hidden = false;
+      trackPicker.setAttribute('aria-hidden', 'false');
+      document.body.classList.add('tp-locked');
+      tpSearch.value = '';
+      renderList('');
+      // Defer focus so the open animation doesn't get jumped
+      setTimeout(() => tpSearch.focus(), 30);
+    }
+    function closePicker() {
+      trackPicker.hidden = true;
+      trackPicker.setAttribute('aria-hidden', 'true');
+      document.body.classList.remove('tp-locked');
+      if (tpLastFocus && typeof tpLastFocus.focus === 'function') {
+        try { tpLastFocus.focus(); } catch(_) {}
+      }
+    }
+
+    trackBtn.addEventListener('click', async () => {
+      openPicker();
+      await loadTracks();
+      renderList(tpSearch.value);
+    });
+
+    // Close: backdrop click, [data-tp-close], or Escape
+    trackPicker.addEventListener('click', (e) => {
+      if (e.target.closest('[data-tp-close]')) {
+        closePicker();
+        return;
+      }
+      const row = e.target.closest('.tp-row[data-track-id]');
+      if (row) {
+        if (row.getAttribute('aria-disabled') === 'true') return;
+        const id = row.dataset.trackId;
+        if (id) {
+          insertChip('track', id);
+          closePicker();
+        }
+      }
+    });
+    document.addEventListener('keydown', (e) => {
+      if (!trackPicker.hidden && e.key === 'Escape') {
+        e.preventDefault();
+        closePicker();
+      }
+    });
+
+    // Live filter
+    tpSearch.addEventListener('input', () => renderList(tpSearch.value));
+  }
+
+  // ── Playlist insert (open existing or create new via modal)
+  const playlistBtn = document.getElementById('insert-playlist-btn');
+  if (playlistBtn) {
+    playlistBtn.addEventListener('click', async () => {
+      if (typeof window.openPlaylistEditor !== 'function') {
+        alert('Playlist editor niet geladen');
+        return;
+      }
+      try {
+        const r = await fetch('/admin/playlists/api/list', { credentials: 'same-origin' });
+        const j = await r.json();
+        if (j.ok && Array.isArray(j.playlists) && j.playlists.length > 0) {
+          const choice = prompt(
+            'Bestaande playlists:\n\n' +
+            j.playlists.map((p, i) => `${i + 1}. ${p.title} (${p.track_count} tracks)`).join('\n') +
+            '\n\nKies een nummer om in te voegen, leeg = nieuwe maken:'
+          );
+          if (choice && /^\d+$/.test(choice.trim())) {
+            const idx = parseInt(choice.trim(), 10) - 1;
+            if (idx >= 0 && idx < j.playlists.length) {
+              insertChip('playlist', j.playlists[idx].id);
+              return;
+            }
+          }
+          if (choice === null) return;
+        }
+      } catch (_) { /* fall through to create */ }
+
+      window.openPlaylistEditor({
+        mode: 'create',
+        onSaved: ({ id }) => insertChip('playlist', id),
+      });
+    });
+  }
+
+  // ── Submit: serialize editor contents into the hidden field
+  if (form && hiddenField) {
+    form.addEventListener('submit', () => {
+      const clone = editor.cloneNode(true);
+      serializeChips(clone);
+      hiddenField.value = clone.innerHTML;
+    });
+  }
+})();
+</script>
 
 <%# ── Track picker modal (P59). Mobile-first: full-screen sheet on small
@@ -1202,6 +1331,6 @@
   <div class="tp-sheet" role="dialog" aria-modal="true" aria-labelledby="tp-title">
     <header class="tp-header">
-      <h2 id="tp-title" class="tp-h2"><%= t('pedit.tp_title') %></h2>
-      <button type="button" class="tp-close" data-tp-close aria-label="<%= t('pedit.tp_close') %>">
+      <h2 id="tp-title" class="tp-h2">Track invoegen</h2>
+      <button type="button" class="tp-close" data-tp-close aria-label="Sluiten">
         <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
       </button>
@@ -1211,8 +1340,8 @@
         <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
       </span>
-      <input type="search" class="tp-search" id="tp-search" placeholder="<%= t('pedit.tp_search_placeholder') %>" autocomplete="off" inputmode="search">
+      <input type="search" class="tp-search" id="tp-search" placeholder="Zoek op titel of artiest…" autocomplete="off" inputmode="search">
     </div>
-    <div class="tp-list" id="tp-list" role="listbox" aria-label="<%= t('pedit.tp_list_aria') %>">
-      <div class="tp-empty" id="tp-empty"><%= t('pedit.js_tracks_loading') %></div>
+    <div class="tp-list" id="tp-list" role="listbox" aria-label="Tracks">
+      <div class="tp-empty" id="tp-empty">Tracks laden…</div>
     </div>
   </div>
@@ -1225,3 +1354,2 @@
   <%- include('../partials/playlist-editor', { csrfToken: (typeof csrfToken !== 'undefined' ? csrfToken : '') }) %>
 <% } %>
-<%- include('../partials/page-data', { pageData: { _timezone: (typeof timezone !== 'undefined' ? timezone : ''), apply: t('imed.apply'), audio_up_busy: t('pedit.audio_up_busy'), audio_up_done: t('pedit.audio_up_done'), audio_up_fail: t('pedit.audio_up_fail'), cancel: t('imed.cancel'), chip_album: t('pedit.chip_album'), chip_playlist: t('pedit.chip_playlist'), chip_track: t('pedit.chip_track'), flip_h: t('imed.flip_h'), flip_v: t('imed.flip_v'), js_embed_invalid: t('pedit.js_embed_invalid'), js_embed_prompt: t('pedit.js_embed_prompt'), js_failed: t('pedit.js_failed'), js_inserted: t('pedit.js_inserted'), js_link_prompt: t('pedit.js_link_prompt'), js_no_tracks_found: t('pedit.js_no_tracks_found'), js_no_tracks_yet: t('pedit.js_no_tracks_yet'), js_playlist_choose: t('pedit.js_playlist_choose'), js_playlist_editor_missing: t('pedit.js_playlist_editor_missing'), js_playlist_existing: t('pedit.js_playlist_existing'), js_tracks_load_fail: t('pedit.js_tracks_load_fail'), js_tracks_loading: t('pedit.js_tracks_loading'), js_uploaded: t('pedit.js_uploaded'), js_uploading: t('pedit.js_uploading'), pin_nth_suffix: t('pedit.pin_nth_suffix'), pin_top: t('pedit.pin_top'), reset: t('imed.reset'), rotate_left: t('imed.rotate_left'), rotate_right: t('imed.rotate_right'), tb_done: t('pedit.tb_done'), tb_fullscreen: t('pedit.tb_fullscreen'), title: t('imed.title'), zoom_in: t('imed.zoom_in'), zoom_out: t('imed.zoom_out') } }) %>
Index: src/views/pages/post.ejs
===================================================================
--- src/views/pages/post.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/post.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,28 +1,6 @@
-<%
-// In hub mode the site is accessible under /user/<slug>; all site-scoped links
-// (tag, edit, delete, comments) must carry that prefix otherwise they fall back
-// to the primary site → 404 / wrong site. In solo mode _base = ''.
-const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
-%>
-<article class="container post-page<%= post.nsfw ? ' nsfw-gate' : '' %>">
-
-  <%# Navigation across ALL posts — ABOVE the post (shared partial). %>
-  <%- include('../partials/post-nav', { newerPost: newerPost, olderPost: olderPost }) %>
-
-  <% if (post.nsfw) { %>
-    <div class="nsfw-banner">
-      <span class="nsfw-banner-icon">🔞</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>
-  <% } %>
-
+<article class="container post-page">
   <% if (post.cover_image_url) { %>
     <figure class="post-cover">
-      <img src="<%= post.cover_image_url %>" alt="<%= post.cover_alt || '' %>"<% if (post.cover_video_url) { %> data-ios-mp4="<%= post.cover_video_url %>"<% } %>>
-    </figure>
-  <% } else if (post.cover_video_url) { %>
-    <figure class="post-cover">
-      <video src="<%= post.cover_video_url %>" poster="<%= thumb(post.cover_video_url, 1280) %>" controls loop muted playsinline preload="metadata"></video>
+      <img src="<%= post.cover_image_url %>" alt="">
     </figure>
   <% } %>
@@ -49,34 +27,11 @@
   </div>
 
-  <% if (typeof poll !== 'undefined' && poll) { %>
-    <section class="poll" aria-label="<%= t('poll.aria') %>">
-      <% poll.options.forEach(function(o){ var _lead = poll.total > 0 && o.count === Math.max.apply(null, poll.options.map(function(x){return x.count;})); %>
-        <div class="poll-opt<%= _lead ? ' is-lead' : '' %>">
-          <div class="poll-bar" style="width:<%= o.pct %>%"></div>
-          <span class="poll-name"><%= o.name %></span>
-          <span class="poll-pct"><%= o.pct %>%</span>
-        </div>
-      <% }); %>
-      <div class="poll-meta">
-        <span><%= poll.voters %> <%= poll.voters === 1 ? t('poll.voter_one') : t('poll.voter_many') %></span>
-        <span aria-hidden="true">·</span>
-        <% if (poll.closed) { %>
-          <span><%= t('poll.closed') %></span>
-        <% } else if (poll.endTime) { %>
-          <span><%= t('poll.closes') %> <%= new Date(poll.endTime).toLocaleString() %></span>
-        <% } %>
-        <% if (poll.multiple) { %><span aria-hidden="true">·</span><span><%= t('poll.multiple') %></span><% } %>
-      </div>
-      <p class="poll-note"><%= t('poll.fedi_only') %></p>
-    </section>
-  <% } %>
-
   <% if (post.tags && post.tags.length > 0) { %>
     <div class="post-tags">
       <% post.tags.forEach(t => { %>
-        <a class="tag" href="<%= _base %>/tag/<%= encodeURIComponent(t) %>"
-           hx-get="<%= _base %>/tag/<%= encodeURIComponent(t) %>?partial=1"
+        <a class="tag" href="/tag/<%= encodeURIComponent(t) %>"
+           hx-get="/tag/<%= encodeURIComponent(t) %>?partial=1"
            hx-target="#pcms-main" hx-swap="innerHTML"
-           hx-push-url="<%= _base %>/tag/<%= encodeURIComponent(t) %>"
+           hx-push-url="/tag/<%= encodeURIComponent(t) %>"
            hx-indicator="#pcms-loading">#<%= t %></a>
       <% }); %>
@@ -84,31 +39,16 @@
   <% } %>
 
-  <div class="post-share" style="margin:1.25rem 0;display:flex;align-items:center;gap:.6rem;flex-wrap:wrap">
-    <button type="button" class="btn post-share-btn" data-share data-share-title="<%= post.title %>">🔗 <%= t('post.share') %></button>
-    <%# Interact via the fediverse — for VISITORS (reply/like/boost from their own
-        server). Same flow as the CTA in the fediverse section; hidden for the
-        owner/admin (their own post) and when AP is off. %>
-    <% if (post.status === 'published' && (typeof apEnabled === 'undefined' || apEnabled) && (typeof canManageSite === 'undefined' || !canManageSite)) { %>
-      <% var _shareAbsUrl = (typeof ogOrigin !== 'undefined' && ogOrigin ? ogOrigin : '') + (typeof _base !== 'undefined' && _base ? _base : '') + '/' + post.slug; %>
-      <button type="button" class="btn fedi-remote-reply-btn" data-fedi-uri="<%= _shareAbsUrl %>" data-fedi-ph="<%= t('fedi.remote_ph') %>">
-        <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
-        <%= t('fedi.remote_interact') %>
-      </button>
-    <% } %>
-    <span class="post-share-feedback" id="post-share-feedback" style="color:var(--accent);font-size:.85rem" hidden><%= t('post.share_copied') %></span>
-  </div>
-
   <!-- Inline admin actions: only visible if user has permission -->
   <% if (user && (permissions.canEditPost(user, post, site) || permissions.canDeletePost(user, post, site))) { %>
     <aside class="post-actions">
       <% if (permissions.canEditPost(user, post, site)) { %>
-        <a href="<%= _base %>/posts/<%= post.slug %>/edit" class="btn"
-           hx-get="<%= _base %>/posts/<%= post.slug %>/edit?partial=1" hx-target="#pcms-main"
-           hx-push-url="<%= _base %>/posts/<%= post.slug %>/edit" hx-indicator="#pcms-loading">
+        <a href="/posts/<%= post.slug %>/edit" class="btn"
+           hx-get="/posts/<%= post.slug %>/edit?partial=1" hx-target="#pcms-main"
+           hx-push-url="/posts/<%= post.slug %>/edit" hx-indicator="#pcms-loading">
           ✏️ Edit
         </a>
       <% } %>
       <% if (permissions.canDeletePost(user, post, site)) { %>
-        <form method="post" action="<%= _base %>/posts/<%= post.slug %>/delete" style="display:inline" data-confirm="Delete this post?">
+        <form method="post" action="/posts/<%= post.slug %>/delete" style="display:inline" onsubmit="return confirm('Delete this post?')">
           <button type="submit" class="btn btn-danger">🗑 Delete</button>
         </form>
@@ -117,38 +57,140 @@
   <% } %>
 
-  <!-- Fediverse interactions — single place: ⭐ like (clickable) + 🔁/💬 counts + reply -->
-  <% var _fedi = (typeof fediverse !== 'undefined' && fediverse) ? fediverse : { thread: [], likeCount: 0, announceCount: 0 }; %>
-  <% if (post.status === 'published' && (typeof apEnabled === 'undefined' || apEnabled)) { %>
-    <section class="post-fediverse post-comments" id="fediverse">
-      <h2 class="comments-heading"><%= t('fedi.heading') %></h2>
-      <div class="fedi-stats">
-        <span class="fedi-stat" title="<%= t('fedi.likes') %>">
-          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
-          <span><%= _fedi.likeCount %></span>
-        </span>
-        <span class="fedi-stat" title="<%= t('fedi.boosts') %>">
-          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
-          <span><%= _fedi.announceCount %></span>
-        </span>
-        <span class="fedi-stat" title="<%= t('fedi.replies') %>">
-          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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><%= (_fedi.thread || []).length %></span>
-        </span>
-      </div>
-      <% if (_fedi.thread && _fedi.thread.length) { %>
-        <ol class="comments-list">
-          <% _fedi.thread.forEach(function(n){ %>
-            <li class="comment"><%- include('../partials/fedi-node', { n: n, t: t, canManageSite: (typeof canManageSite !== 'undefined' ? canManageSite : false), _base: _base, siteAvatar: (typeof siteAvatar !== 'undefined' ? siteAvatar : null), formatDateTime: formatDateTime, postSlug: post.slug }) %></li>
-          <% }); %>
-        </ol>
-      <% } %>
-      <%# The "Interact via the fediverse" button for visitors now lives next to
-          the Share button at the top of the post (single, prominent place). %>
-    </section>
-  <% } %>
-  <%# Het remote-reply-veld zit in assets/js/mod/post.js. Inline script hier wordt
-    door de CSP geweigerd zodra je deze pagina via een link binnen de site opent
-    -- zie shaer-0i6. %>
-
+  <!-- Comments -->
+  <section class="post-comments" id="comments">
+    <h2 class="comments-heading">
+      <%= totalComments %> comment<%= totalComments === 1 ? '' : 's' %>
+    </h2>
+
+    <% if (typeof currentPath === 'string' && currentPath && currentPath.indexOf('/' + post.slug) === 0) { %>
+      <% /* If the URL has ?pending=1, the previous submit landed in the queue */ %>
+    <% } %>
+    <script>
+      (function() {
+        if (location.search.indexOf('pending=1') !== -1) {
+          var s = document.createElement('div');
+          s.className = 'comments-pending-flash';
+          s.textContent = 'Your comment is awaiting moderation. It will appear once an admin approves it.';
+          document.currentScript.parentNode.insertBefore(s, document.currentScript);
+        }
+      })();
+    </script>
+
+    <% if (!comments || !comments.length) { %>
+      <p class="comments-empty">No comments yet.<% if (!user) { %> <a href="/auth/login?next=<%= encodeURIComponent('/' + post.slug + '#comments') %>">Log in</a> to start the conversation.<% } %></p>
+    <% } else { %>
+      <ol class="comments-list">
+        <% comments.forEach(function(c) { %>
+          <li class="comment" id="comment-<%= c.id %>">
+            <div class="comment-avatar">
+              <% if (c.author_avatar) { %>
+                <img src="<%= c.author_avatar %>" alt="">
+              <% } else { %>
+                <span><%= c.author_username.charAt(0).toUpperCase() %></span>
+              <% } %>
+            </div>
+            <div class="comment-body">
+              <div class="comment-meta">
+                <a class="comment-author" href="/users/<%= encodeURIComponent(c.author_username) %>"><%= c.author_username %></a>
+                <span class="comment-time" title="<%= c.created_at %>"><%= formatDateTime(c.created_at) %></span>
+              </div>
+              <div class="comment-content"><%= c.content %></div>
+              <div class="comment-actions">
+                <% if (user) { %>
+                  <button type="button" class="comment-reply-btn" data-reply-to="<%= c.id %>">Reply</button>
+                <% } %>
+                <% if (user && permissions.canDeleteComment(user, c, site)) { %>
+                  <form method="post" action="/comments/<%= c.id %>/delete" style="display:inline" onsubmit="return confirm('Delete this comment?')">
+                    <button type="submit" class="comment-delete-btn">Delete</button>
+                  </form>
+                <% } %>
+              </div>
+
+              <!-- Inline reply form (hidden by default) -->
+              <% if (user) { %>
+                <form method="post" action="/comments" class="comment-reply-form" hidden data-reply-form-for="<%= c.id %>">
+                  <input type="hidden" name="post_slug" value="<%= post.slug %>">
+                  <input type="hidden" name="parent_comment_id" value="<%= c.id %>">
+                  <textarea name="content" rows="3" maxlength="4000" placeholder="Reply to <%= c.author_username %>…" required></textarea>
+                  <div class="comment-reply-form-actions">
+                    <button type="submit" class="btn btn-primary">Reply</button>
+                    <button type="button" class="btn comment-cancel-reply">Cancel</button>
+                  </div>
+                </form>
+              <% } %>
+
+              <!-- Replies (1 level deep) -->
+              <% if (c.replies && c.replies.length) { %>
+                <ol class="comment-replies">
+                  <% c.replies.forEach(function(r) { %>
+                    <li class="comment comment-reply" id="comment-<%= r.id %>">
+                      <div class="comment-avatar">
+                        <% if (r.author_avatar) { %>
+                          <img src="<%= r.author_avatar %>" alt="">
+                        <% } else { %>
+                          <span><%= r.author_username.charAt(0).toUpperCase() %></span>
+                        <% } %>
+                      </div>
+                      <div class="comment-body">
+                        <div class="comment-meta">
+                          <a class="comment-author" href="/users/<%= encodeURIComponent(r.author_username) %>"><%= r.author_username %></a>
+                          <span class="comment-time"><%= formatDateTime(r.created_at) %></span>
+                        </div>
+                        <div class="comment-content"><%= r.content %></div>
+                        <div class="comment-actions">
+                          <% if (user && permissions.canDeleteComment(user, r, site)) { %>
+                            <form method="post" action="/comments/<%= r.id %>/delete" style="display:inline" onsubmit="return confirm('Delete this comment?')">
+                              <button type="submit" class="comment-delete-btn">Delete</button>
+                            </form>
+                          <% } %>
+                        </div>
+                      </div>
+                    </li>
+                  <% }); %>
+                </ol>
+              <% } %>
+            </div>
+          </li>
+        <% }); %>
+      </ol>
+    <% } %>
+
+    <!-- Top-level comment form (only for logged-in users for now) -->
+    <% if (user) { %>
+      <form method="post" action="/comments" class="comment-form">
+        <input type="hidden" name="post_slug" value="<%= post.slug %>">
+        <label class="comment-form-label">
+          <span>Add a comment as <strong><%= user.username %></strong></span>
+          <textarea name="content" rows="3" maxlength="4000" placeholder="Share your thoughts…" required></textarea>
+        </label>
+        <button type="submit" class="btn btn-primary">Post comment</button>
+      </form>
+    <% } else { %>
+      <p class="comments-login-cta">
+        <a href="/auth/login?next=<%= encodeURIComponent('/' + post.slug + '#comments') %>" class="btn">Log in to comment</a>
+      </p>
+    <% } %>
+  </section>
+
+  <!-- Reply form toggle -->
+  <script>
+  (function() {
+    document.querySelectorAll('.comment-reply-btn').forEach(function(btn) {
+      btn.addEventListener('click', function() {
+        var id = btn.dataset.replyTo;
+        var form = document.querySelector('[data-reply-form-for="' + id + '"]');
+        if (form) {
+          form.hidden = !form.hidden;
+          if (!form.hidden) form.querySelector('textarea').focus();
+        }
+      });
+    });
+    document.querySelectorAll('.comment-cancel-reply').forEach(function(btn) {
+      btn.addEventListener('click', function() {
+        btn.closest('.comment-reply-form').hidden = true;
+      });
+    });
+  })();
+  </script>
 
   <%# ── Related posts (3-card grid) ─────────────────────────────
@@ -158,15 +200,17 @@
   <% if (relatedPosts && relatedPosts.length > 0) { %>
     <section class="post-related" aria-labelledby="related-heading">
-      <h2 class="post-related-heading" id="related-heading"><%= t('related.title') %></h2>
+      <h2 class="post-related-heading" id="related-heading">Gerelateerde posts</h2>
       <ul class="post-related-grid">
         <% relatedPosts.forEach(function(rp) { %>
           <li class="post-related-card">
-            <a href="<%= rp._urlBase || '' %>/<%= rp.slug %>"<% if (!rp._urlBase) { %>
+            <a href="/<%= rp.slug %>"
                hx-get="/<%= rp.slug %>?partial=1" hx-target="#pcms-main"
-               hx-push-url="/<%= rp.slug %>" hx-indicator="#pcms-loading"<% } %>>
+               hx-push-url="/<%= rp.slug %>" hx-indicator="#pcms-loading">
               <% if (rp.cover_image_url) { %>
-                <span class="post-related-cover<%= rp.nsfw ? ' nsfw-media' : '' %>" aria-hidden="true"><img class="post-related-img" src="<%= rp.cover_image_url %>" alt="" loading="lazy" decoding="async"<% if (rp.cover_video_url) { %> data-ios-mp4="<%= rp.cover_video_url %>"<% } %>><% if (rp.nsfw) { %><span class="nsfw-veil"><%- include('../partials/nsfw-veil', { cw: rp.content_warning }) %></span><% } %></span>
+                <span class="post-related-cover"
+                      style="background-image: url('<%= rp.cover_image_url %>')"
+                      aria-hidden="true"></span>
               <% } else { %>
-                <span class="post-related-cover post-related-cover--empty<%= rp.nsfw ? ' nsfw-media' : '' %>" aria-hidden="true"><% if (rp.nsfw) { %><span class="nsfw-veil"><%- include('../partials/nsfw-veil', { cw: rp.content_warning }) %></span><% } %></span>
+                <span class="post-related-cover post-related-cover--empty" aria-hidden="true"></span>
               <% } %>
               <span class="post-related-meta">
@@ -181,9 +225,80 @@
   <% } %>
 
+  <%# ── Pinned navigation (only when current post is pinned) ───
+      Two card-style links: "Vorige vastgepinde" / "Volgende vastgepinde".
+      Edge-of-stack states show "← BOVENAAN — Nieuwste post" or the equivalent
+      for the bottom. Layout mirrors v9 exactly. %>
+  <% if (post.pinned) { %>
+    <nav class="post-pinned-nav" aria-label="Pinned navigation">
+      <%# LEFT card: post one step UP the pinned stack (towards #1).
+          Uses prevPinnedPost which now means "smaller rank". %>
+      <% if (prevPinnedPost) { %>
+        <a class="post-pinned-card post-pinned-card--prev" href="/<%= prevPinnedPost.slug %>"
+           hx-get="/<%= prevPinnedPost.slug %>?partial=1" hx-target="#pcms-main"
+           hx-push-url="/<%= prevPinnedPost.slug %>" hx-indicator="#pcms-loading">
+          <span class="post-pinned-arrow" aria-hidden="true">←</span>
+          <span class="post-pinned-body">
+            <span class="post-pinned-label">📌 Vorige vastgepinde</span>
+            <span class="post-pinned-title"><%= prevPinnedPost.title %></span>
+            <span class="post-pinned-cta">Lees deze post →</span>
+          </span>
+        </a>
+      <% } else if (pinnedTopOfStack) { %>
+        <span class="post-pinned-card post-pinned-card--edge">
+          <span class="post-pinned-edge-label">— BOVENAAN —</span>
+          <span class="post-pinned-edge-title">Nieuwste post</span>
+        </span>
+      <% } %>
+
+      <%# RIGHT card: post one step DOWN the pinned stack (away from #1).
+          Uses nextPinnedPost which now means "larger rank". %>
+      <% if (nextPinnedPost) { %>
+        <a class="post-pinned-card post-pinned-card--next" href="/<%= nextPinnedPost.slug %>"
+           hx-get="/<%= nextPinnedPost.slug %>?partial=1" hx-target="#pcms-main"
+           hx-push-url="/<%= nextPinnedPost.slug %>" hx-indicator="#pcms-loading">
+          <span class="post-pinned-arrow" aria-hidden="true">→</span>
+          <span class="post-pinned-body">
+            <span class="post-pinned-label">📌 Volgende vastgepinde</span>
+            <span class="post-pinned-title"><%= nextPinnedPost.title %></span>
+            <span class="post-pinned-cta">← Lees deze post</span>
+          </span>
+        </a>
+      <% } else if (pinnedBottomOfStack) { %>
+        <span class="post-pinned-card post-pinned-card--edge">
+          <span class="post-pinned-edge-label">— ONDERAAN —</span>
+          <span class="post-pinned-edge-title">Oudste post</span>
+        </span>
+      <% } %>
+    </nav>
+  <% } %>
+
+  <%# ── Chronological prev/next (kept for non-pinned posts) ──────
+      For pinned posts the pinned-nav above replaces this. Non-pinned
+      regular posts still get the simple older/newer pair. %>
+  <% if (!post.pinned && (prevPost || nextPost)) { %>
+    <nav class="post-nav">
+      <% if (prevPost) { %>
+        <a class="post-nav-prev" href="/<%= prevPost.slug %>"
+           hx-get="/<%= prevPost.slug %>?partial=1" hx-target="#pcms-main"
+           hx-push-url="/<%= prevPost.slug %>" hx-indicator="#pcms-loading">
+          <span class="post-nav-label">← Older</span>
+          <span class="post-nav-title"><%= prevPost.title %></span>
+        </a>
+      <% } else { %><span></span><% } %>
+      <% if (nextPost) { %>
+        <a class="post-nav-next" href="/<%= nextPost.slug %>"
+           hx-get="/<%= nextPost.slug %>?partial=1" hx-target="#pcms-main"
+           hx-push-url="/<%= nextPost.slug %>" hx-indicator="#pcms-loading">
+          <span class="post-nav-label">Newer →</span>
+          <span class="post-nav-title"><%= nextPost.title %></span>
+        </a>
+      <% } %>
+    </nav>
+  <% } %>
 </article>
 <style>
-.post-page { max-width: 720px; margin: 0.5rem auto 2rem; padding: 0 1rem; }
+.post-page { max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
 .post-cover { margin: 0 0 2rem; border-radius: 8px; overflow: hidden; }
-.post-cover img, .post-cover video { width: 100%; height: auto; display: block; }
+.post-cover img { width: 100%; height: auto; display: block; }
 .post-header { margin-bottom: 2rem; }
 .post-title { font-family: var(--font-display, serif); font-size: 2.5rem; line-height: 1.1; margin: 0 0 1rem; }
@@ -194,25 +309,4 @@
 .post-content { font-family: var(--font-body, serif); font-size: 1.1rem; line-height: 1.7; color: var(--ink); }
 .post-content p { margin: 1.2em 0; }
-/* Poll (a hosted AS2 Question) — display-only; voting happens from the fediverse. */
-.poll { margin: 1.75rem 0; display: flex; flex-direction: column; gap: .5rem; }
-.poll-opt { position: relative; display: flex; align-items: center; gap: .5rem; padding: .55rem .75rem; border: 1px solid var(--line, rgba(128,128,128,.25)); border-radius: 8px; overflow: hidden; font-size: .98rem; }
-.poll-bar { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent) 18%, transparent); z-index: 0; transition: width .3s ease; }
-.poll-opt.is-lead .poll-bar { background: color-mix(in srgb, var(--accent) 30%, transparent); }
-.poll-name { position: relative; z-index: 1; flex: 1; color: var(--ink); }
-.poll-opt.is-lead .poll-name { font-weight: 600; }
-.poll-pct { position: relative; z-index: 1; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
-.poll-meta { display: flex; flex-wrap: wrap; gap: .4rem; font-size: .85rem; color: var(--ink-muted); }
-.poll-note { font-size: .8rem; color: var(--ink-muted); margin: .1rem 0 0; }
-.like-btn {
-  display: inline-flex; align-items: center; gap: 0.45rem;
-  padding: 0.4rem 0.85rem; border: 1px solid var(--rule); border-radius: 999px;
-  background: var(--paper-2); color: var(--ink); font: inherit; font-size: 0.95rem;
-  cursor: pointer; text-decoration: none; transition: border-color 120ms, color 120ms, background 120ms;
-}
-.like-btn:hover { border-color: var(--accent); }
-.like-btn .like-heart { font-size: 1.1rem; line-height: 1; }
-.like-btn.is-liked { border-color: var(--accent); color: var(--accent); }
-.like-btn.is-liked .like-heart { color: var(--accent); }
-.like-btn .like-count { font-variant-numeric: tabular-nums; }
 .post-content h2 { font-family: var(--font-display, serif); font-size: 1.6rem; margin: 2rem 0 1rem; }
 .post-content h3 { font-family: var(--font-display, serif); font-size: 1.3rem; margin: 1.5rem 0 0.75rem; }
@@ -226,4 +320,10 @@
 .tag:hover { background: var(--accent); color: white; }
 .post-actions { display: flex; gap: 0.5rem; margin: 2rem 0; padding: 1rem; background: var(--paper-2); border-radius: 6px; flex-wrap: wrap; }
+.post-nav { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--rule); }
+.post-nav-prev, .post-nav-next { display: flex; flex-direction: column; padding: 1rem; background: var(--paper-2); border-radius: 6px; text-decoration: none; color: var(--ink); }
+.post-nav-next { text-align: right; align-items: flex-end; }
+.post-nav-label { font-size: 0.8rem; color: var(--ink-muted); margin-bottom: 0.25rem; }
+.post-nav-title { font-weight: 500; }
+@media (max-width: 600px) { .post-nav { grid-template-columns: 1fr; } }
 
 /* ============================================================
@@ -290,6 +390,4 @@
   gap: 0.5rem;
   align-items: baseline;
-  flex-wrap: wrap;
-  row-gap: 0.1rem;
   font-size: 0.85rem;
   margin-bottom: 0.3rem;
@@ -298,8 +396,4 @@
 .comment-author:hover { color: var(--accent); }
 .comment-time { color: var(--ink-muted, var(--ink-soft)); }
-.comment-meta .fedi-handle { overflow-wrap: anywhere; }
-@media (max-width: 560px) {
-  .comment-meta .comment-time { flex-basis: 100%; font-size: 0.78rem; color: var(--ink-faint, var(--ink-muted)); }
-}
 .comment-content {
   white-space: pre-wrap;
@@ -314,10 +408,5 @@
   gap: 0.5rem;
   margin-top: 0.4rem;
-  flex-wrap: wrap;
-  align-items: center;
-}
-.fedi-owner-react { display: inline; }
-.fedi-owner-reply { display: inline-block; }
-.fedi-owner-reply[open] { flex-basis: 100%; }
+}
 .comment-reply-btn, .comment-delete-btn {
   background: none;
@@ -334,23 +423,4 @@
 .comment-delete-btn:hover { color: #c33; }
 
-/* Owner comment-action icons — same visual language as the News feed, compact (30px). */
-.fedi-cact { width: 30px; height: 30px; flex: 0 0 30px; border-radius: 50%; padding: 0; box-sizing: border-box;
-  display: inline-flex; align-items: center; justify-content: center; list-style: none;
-  border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent);
-  background: color-mix(in srgb, var(--ink, #000) 3%, transparent);
-  color: var(--ink-soft, #888); cursor: pointer; -webkit-tap-highlight-color: transparent;
-  transition: background .12s, border-color .12s, transform .1s; }
-.fedi-cact::-webkit-details-marker { display: none; }
-.fedi-cact svg { width: 15px; height: 15px; display: block; }
-.fedi-cact:active { transform: scale(.9); }
-.fedi-cact-like svg { color: #e8b04b; }
-.fedi-cact-like:hover { background: color-mix(in srgb, #e8b04b 15%, transparent); border-color: color-mix(in srgb, #e8b04b 50%, transparent); }
-.fedi-cact-like.is-on { background: color-mix(in srgb, #e8b04b 18%, transparent); border-color: color-mix(in srgb, #e8b04b 60%, transparent); }
-.fedi-cact-boost svg { color: #2fa85a; }
-.fedi-cact-boost:hover { background: color-mix(in srgb, #2fa85a 15%, transparent); border-color: color-mix(in srgb, #2fa85a 50%, transparent); }
-.fedi-cact-boost.is-on { background: color-mix(in srgb, #2fa85a 18%, transparent); border-color: color-mix(in srgb, #2fa85a 60%, transparent); }
-.fedi-cact-reply svg { color: var(--accent, #06c); }
-.fedi-cact-reply:hover { background: color-mix(in srgb, var(--accent, #888) 14%, transparent); border-color: color-mix(in srgb, var(--accent, #888) 50%, transparent); }
-
 .comment-replies {
   margin-top: 1rem;
@@ -380,5 +450,4 @@
   resize: vertical;
   min-height: 70px;
-  max-height: 200px;
   padding: 0.65rem 0.85rem;
   border: 1px solid var(--rule);
@@ -393,13 +462,4 @@
 .comment-form button { align-self: flex-start; }
 .comment-reply-form-actions { display: flex; gap: 0.5rem; }
-/* Show the reply form only AFTER clicking REPLY. The display:flex above would
-   otherwise override the [hidden] attribute → form was always open. */
-.comment-reply-form[hidden] { display: none; }
-.fedi-owner-reply summary { list-style: none; cursor: pointer; }
-.fedi-owner-reply summary::-webkit-details-marker { display: none; }
-.fedi-owner-reply[open] summary { margin-bottom: .45rem; }
-/* DELETE button is inside a <form>; display:contents makes it a direct flex-sibling
-   of REPLY → pixel-perfect alignment + equal gap. */
-.comment-actions form { display: contents; }
 
 .comments-login-cta { margin-top: 1.5rem; text-align: center; }
@@ -447,10 +507,7 @@
 .post-related-cover {
   display: block;
-  position: relative;
-  overflow: hidden;
   aspect-ratio: 16 / 10;
   background: var(--paper-2) center/cover no-repeat;
 }
-.post-related-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
 .post-related-cover--empty {
   background-image: linear-gradient(135deg,
Index: src/views/pages/prutter-conversation.ejs
===================================================================
--- src/views/pages/prutter-conversation.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
+++ src/views/pages/prutter-conversation.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -0,0 +1,165 @@
+<div class="container prutter-conv-page" data-conversation-id="<%= conversation.id %>" data-me="<%= user.id %>">
+  <header class="prutter-conv-header">
+    <p class="prutter-back"><a href="<%= siteUrlBase %>/prutter">&larr; Inbox</a></p>
+    <div class="prutter-conv-headline">
+      <div class="prutter-conv-avatar prutter-conv-avatar--lg">
+        <% if (other && other.avatar_url) { %>
+          <img src="<%= other.avatar_url %>" alt="">
+        <% } else { %>
+          <span><%= (other && other.username || '?').charAt(0).toUpperCase() %></span>
+        <% } %>
+      </div>
+      <div>
+        <h1><%= other ? other.username : 'Unknown' %></h1>
+        <% if (other) { %>
+          <p class="prutter-conv-sub"><a href="/users/<%= encodeURIComponent(other.username) %>">View profile</a></p>
+        <% } %>
+      </div>
+    </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"
+        method="post"
+        action="<%= siteUrlBase %>/prutter/<%= conversation.id %>/send"
+        hx-post="<%= siteUrlBase %>/prutter/<%= conversation.id %>/send"
+        hx-target="#prutter-thread"
+        hx-swap="beforeend"
+        hx-on::after-request="this.querySelector('textarea').value=''; window.__prutterScroll && window.__prutterScroll();">
+    <textarea name="content" rows="2" maxlength="2000" placeholder="Type a message…" required></textarea>
+    <button type="submit" class="btn btn-primary">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);
+  }
+})();
+</script>
+
+<style>
+.prutter-conv-page { max-width: 720px; margin: 2rem auto; padding: 0 1rem; display: flex; flex-direction: column; gap: 1rem; }
+.prutter-back { margin: 0; }
+.prutter-back a { color: var(--accent); text-decoration: none; }
+
+.prutter-conv-headline {
+  display: flex; align-items: center; gap: 0.85rem;
+  padding-bottom: 1rem; border-bottom: 1px solid var(--rule);
+}
+.prutter-conv-headline h1 {
+  font-family: var(--font-display, serif);
+  font-size: 1.5rem; margin: 0;
+}
+.prutter-conv-sub { margin: 0; font-size: 0.85rem; color: var(--ink-muted, var(--ink-soft)); }
+.prutter-conv-sub a { color: var(--accent); }
+
+.prutter-conv-avatar--lg { width: 56px; height: 56px; }
+.prutter-conv-avatar--lg span { font-size: 1.2rem; }
+
+.prutter-thread {
+  list-style: none; padding: 0; margin: 0;
+  display: flex; flex-direction: column; gap: 0.5rem;
+  background: var(--paper-2);
+  border: 1px solid var(--rule);
+  border-radius: 8px;
+  padding: 1rem;
+  height: 60vh;
+  overflow-y: auto;
+}
+
+.prutter-msg { display: flex; flex-direction: column; max-width: 80%; }
+.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 {
+  display: flex; gap: 0.5rem; align-items: flex-end;
+  padding-bottom: env(safe-area-inset-bottom, 0);
+}
+.prutter-composer textarea {
+  flex: 1;
+  padding: 0.6rem 0.85rem;
+  border: 1px solid var(--rule);
+  border-radius: 8px;
+  background: var(--paper);
+  color: var(--ink);
+  font-family: inherit;
+  font-size: 0.95rem;
+  resize: vertical;
+  min-height: 44px;
+}
+</style>
Index: src/views/pages/prutter-inbox.ejs
===================================================================
--- src/views/pages/prutter-inbox.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
+++ src/views/pages/prutter-inbox.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -0,0 +1,108 @@
+<div class="container prutter-inbox-page">
+  <header class="prutter-header">
+    <h1>Prutter</h1>
+    <p class="prutter-tagline">Your direct messages on this site.</p>
+  </header>
+
+  <% if (!conversations.length) { %>
+    <p class="prutter-empty">
+      No conversations yet. Open someone's profile and click <em>Send DM</em> to start one.
+    </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">Empty conversation</div>
+              <% } %>
+            </div>
+            <% if (c.unread_count > 0) { %>
+              <span class="prutter-unread-badge"><%= c.unread_count %></span>
+            <% } %>
+          </a>
+        </li>
+      <% }); %>
+    </ol>
+  <% } %>
+</div>
+
+<style>
+.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/search.ejs
===================================================================
--- src/views/pages/search.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/search.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,84 +1,41 @@
-<% const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : ''; %>
-<% const _tracks = (typeof tracks !== 'undefined' && tracks) ? tracks : []; %>
-<% const _events = (typeof events !== 'undefined' && events) ? events : []; %>
-<% const _pages = (typeof pages !== 'undefined' && pages) ? pages : []; %>
 <div class="container search-page">
-  <h1><%= t('nav.search') %></h1>
+  <h1>Search</h1>
 
-  <form method="get" action="<%= _base %>/search" class="search-page-form">
+  <form method="get" action="/search" class="search-page-form">
     <input
       type="search"
       name="q"
       value="<%= query %>"
-      placeholder="<%= t('search.placeholder') %>"
+      placeholder="Search posts…"
       autocomplete="off"
       autofocus
-      aria-label="<%= t('nav.search') %>">
-    <button type="submit" class="btn btn-primary"><%= t('search.button') %></button>
+      aria-label="Search query">
+    <button type="submit" class="btn btn-primary">Search</button>
   </form>
 
   <% if (queryError) { %>
-    <p class="search-error"><%= t('search.error') %></p>
+    <p class="search-error">Couldn't run that query. Try a simpler term.</p>
   <% } %>
 
   <% if (query && !queryError) { %>
     <p class="search-meta">
-      <%= t(total === 1 ? 'search.results_one' : 'search.results_other', { n: total, q: query }) %>
+      <%= total %> result<%= total === 1 ? '' : 's' %> for &ldquo;<%= query %>&rdquo;
     </p>
   <% } %>
 
-  <% if (_tracks.length) { %>
-    <h2 class="search-section-title"><%= t('search.section_tracks') %></h2>
-    <% var _inPost = t('search.in_post'); /* t() vóór de loop ophalen: function(t) overschaduwt 'm */ %>
-    <div class="search-tracks">
-      <% _tracks.forEach(function(t) {
-           const tj = JSON.stringify({ id: t.id, url: t.url, title: t.title, artist: t.artist || '', cover: t.cover || '' })
-             .replace(/&/g, '&amp;').replace(/'/g, '&#39;').replace(/</g, '&lt;'); %>
-        <div class="post-audio-track search-track" id="track-<%= t.id %>"
-             data-pcms-track-id="<%= t.id %>" data-pcms-track-url="<%= t.url %>"
-             data-pcms-track='<%- tj %>'>
-          <button type="button" class="pat-play" aria-label="Speel <%= t.title %>">
-            <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 4l12 8-12 8z"/></svg>
-          </button>
-          <% if (t.cover) { %>
-            <span class="search-track-cover" style="background-image:url('<%= t.cover %>')" aria-hidden="true"></span>
-          <% } %>
-          <div class="pat-info">
-            <div class="pat-title"><%= t.title %></div>
-            <% if (t.artist || t.album) { %>
-              <div class="pat-artist">
-                <%= t.artist %><% if (t.artist && t.album) { %> &middot; <% } %><%= t.album %>
-              </div>
-            <% } %>
-          </div>
-          <% if (t.postUrl) { %>
-            <a class="search-track-link"
-               href="<%= t.postUrl %>"
-               hx-get="<%= t.postUrl %>?partial=1"
-               hx-target="#pcms-main"
-               hx-swap="innerHTML"
-               hx-push-url="<%= t.postUrl %>"
-               aria-label="<%= _inPost %>"><%= _inPost %></a>
-          <% } %>
-        </div>
-      <% }); %>
-    </div>
-  <% } %>
-
   <% if (results && results.length) { %>
-    <% if (_tracks.length) { %><h2 class="search-section-title"><%= t('search.section_posts') %></h2><% } %>
     <ol class="search-results">
       <% results.forEach(function(r) { %>
         <li class="search-result">
-          <h3 class="search-result-title">
-            <a href="<%= _base %>/<%= r.slug %>"
-               hx-get="<%= _base %>/<%= r.slug %>?partial=1"
+          <h2 class="search-result-title">
+            <a href="/<%= r.slug %>"
+               hx-get="/<%= r.slug %>?partial=1"
                hx-target="#pcms-main"
                hx-swap="innerHTML"
-               hx-push-url="<%= _base %>/<%= r.slug %>"
+               hx-push-url="/<%= r.slug %>"
                hx-indicator="#pcms-loading">
-              <%= r.title || '(zonder titel)' %>
+              <%= r.title || '(untitled)' %>
             </a>
-          </h3>
+          </h2>
           <p class="search-result-meta">
             <%= r.author_username %>
@@ -89,29 +46,6 @@
       <% }); %>
     </ol>
-  <% } %>
-
-  <% if (_events.length) { %>
-    <h2 class="search-section-title"><%= t('search.section_events') %></h2>
-    <ol class="search-results">
-      <% _events.forEach(function(ev) { %>
-        <li class="search-result">
-          <h3 class="search-result-title"><a href="<%= ev.url %>"><%= ev.where || '—' %></a></h3>
-          <p class="search-result-meta"><%= [ev.date, ev.time].filter(Boolean).join(' ') %></p>
-        </li>
-      <% }); %>
-    </ol>
-  <% } %>
-
-  <% if (_pages.length) { %>
-    <h2 class="search-section-title"><%= t('search.section_pages') %></h2>
-    <ul class="search-pages">
-      <% _pages.forEach(function(pg) { %>
-        <li><a class="search-page-link" href="<%= pg.url %>"><%= pg.label %> <span aria-hidden="true">→</span></a></li>
-      <% }); %>
-    </ul>
-  <% } %>
-
-  <% if (query && !queryError && !results.length && !_tracks.length && !_events.length && !_pages.length) { %>
-    <p class="search-empty"><%= t('search.empty') %></p>
+  <% } else if (query && !queryError) { %>
+    <p class="search-empty">No posts found.</p>
   <% } %>
 </div>
@@ -145,12 +79,4 @@
   margin: 0 0 1rem;
 }
-.search-section-title {
-  font-family: var(--font-display, serif);
-  font-size: 1.1rem;
-  margin: 1.75rem 0 0.75rem;
-  color: var(--ink-soft);
-  text-transform: uppercase;
-  letter-spacing: 0.05em;
-}
 .search-error {
   background: rgba(200, 60, 60, 0.15);
@@ -166,37 +92,9 @@
 }
 
-/* Nummer-resultaten */
-.search-tracks { display: flex; flex-direction: column; gap: 0.25rem; }
-.search-track.post-audio-track {
-  display: flex;
-  align-items: center;
-  gap: 0.75rem;
-  padding: 0.5rem 0.6rem;
-  border-radius: 8px;
-}
-.search-track .search-track-cover {
-  width: 38px; height: 38px;
-  flex: 0 0 38px;
-  border-radius: 5px;
-  background-size: cover;
-  background-position: center;
-  background-color: var(--paper-2);
-}
-.search-track .pat-info { flex: 1; min-width: 0; }
-.search-track .pat-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.search-track .pat-artist { font-size: 0.82rem; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.search-track-link {
-  flex: 0 0 auto;
-  font-size: 0.82rem;
-  color: var(--accent);
-  text-decoration: none;
-  white-space: nowrap;
-}
-.search-track-link:hover { text-decoration: underline; }
-
 .search-results {
   list-style: none;
   margin: 0;
   padding: 0;
+  counter-reset: search-result;
 }
 .search-result {
@@ -234,10 +132,3 @@
   border-radius: 3px;
 }
-.search-pages { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
-.search-page-link {
-  display: inline-flex; align-items: center; gap: 0.3rem;
-  padding: 0.45rem 0.8rem; border: 1px solid var(--rule); border-radius: 999px;
-  color: var(--ink); text-decoration: none; font-size: 0.9rem; background: var(--paper-2);
-}
-.search-page-link:hover { border-color: var(--accent); color: var(--accent); }
 </style>
Index: src/views/pages/shows.ejs
===================================================================
--- src/views/pages/shows.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,67 +1,0 @@
-<%
-  var ss = (typeof shows !== 'undefined') ? shows : [];
-  var st = (typeof notifyState !== 'undefined') ? notifyState : null;
-%>
-<section class="sh">
-  <h1 class="sh-h1"><%= t('agenda.title') %></h1>
-
-  <% if (st === 'done') { %><div class="sh-msg"><%= t('agenda.msg_done') %></div>
-  <% } else if (st === 'check') { %><div class="sh-msg"><%= t('agenda.msg_check') %></div>
-  <% } else if (st === 'error') { %><div class="sh-msg sh-bad"><%= (typeof notifyMsg!=='undefined')?notifyMsg:'Er ging iets mis.' %></div><% } %>
-
-  <% if (!ss.length) { %>
-    <p class="sh-empty"><%= t('agenda.empty') %></p>
-  <% } else { %>
-    <ul class="sh-list">
-      <% ss.forEach(function(s){ %>
-        <li class="sh-item">
-          <span class="sh-date">
-            <span class="sh-day"><%= (s.date||'').slice(8) %></span>
-            <span class="sh-mon"><%= (s.date||'').slice(5,7) %>/<%= (s.date||'').slice(0,4) %></span>
-          </span>
-          <span class="sh-meta">
-            <span class="sh-city"><%= s.city %><% if (s.country) { %>, <%= s.country %><% } %></span>
-            <% if (s.venue) { %><span class="sh-venue"><%= s.venue %><% if (s.time) { %> · <%= s.time %><% } %></span><% } else if (s.time) { %><span class="sh-venue"><%= s.time %></span><% } %>
-            <% if (s.notes) { %><span class="sh-notes"><%= s.notes %></span><% } %>
-          </span>
-          <% if (s.ticket_url) { %><a class="sh-tix" href="<%= s.ticket_url %>" target="_blank" rel="noopener"><%= t('agenda.tickets') %></a><% } %>
-        </li>
-      <% }); %>
-    </ul>
-  <% } %>
-
-  <% if (!(typeof footerNewsletter !== 'undefined' && footerNewsletter)) { %>
-  <div class="sh-notify">
-    <h2 class="sh-h2"><%= t('agenda.notify_h') %></h2>
-    <p class="sh-sub"><%= t('agenda.notify_sub') %></p>
-    <form method="POST" action="<%= siteUrlBase %>/shows/notify" class="sh-form">
-      <input type="email" name="email" required placeholder="<%= t('common.email_placeholder') %>" autocomplete="email">
-      <button type="submit" class="sh-btn"><%= t('agenda.notify_btn') %></button>
-    </form>
-  </div>
-  <% } %>
-</section>
-
-<style>
-  .sh { max-width: 640px; margin: 0 auto; padding: 32px 18px 64px; }
-  .sh-h1 { font-size: clamp(26px,5vw,38px); margin: 0 0 18px; }
-  .sh-msg { padding: 11px 14px; border-radius: 10px; background: rgba(60,160,90,.15); margin-bottom: 16px; }
-  .sh-bad { background: rgba(200,60,60,.15); }
-  .sh-list { list-style: none; margin: 0 0 36px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
-  .sh-item { display: flex; align-items: center; gap: 16px; padding: 12px 14px; border: 1px solid rgba(128,128,128,.2); border-radius: 12px; }
-  .sh-date { flex: 0 0 auto; text-align: center; min-width: 54px; }
-  .sh-day { display: block; font-size: 22px; font-weight: 700; line-height: 1; }
-  .sh-mon { font-size: 11px; opacity: .6; }
-  .sh-meta { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
-  .sh-city { font-weight: 600; }
-  .sh-venue { font-size: 13px; opacity: .7; }
-  .sh-notes { font-size: 12.5px; opacity: .6; margin-top: 2px; }
-  .sh-tix { flex: 0 0 auto; padding: 8px 14px; border-radius: 999px; background: var(--accent,#6b8f71); color: #fff; text-decoration: none; font-weight: 600; font-size: 13px; }
-  .sh-empty { opacity: .7; margin-bottom: 36px; }
-  .sh-notify { border-top: 1px solid rgba(128,128,128,.2); padding-top: 24px; }
-  .sh-h2 { margin: 0 0 6px; font-size: 18px; }
-  .sh-sub { opacity: .8; margin: 0 0 14px; }
-  .sh-form { display: flex; gap: 10px; flex-wrap: wrap; }
-  .sh-form input { flex: 1 1 200px; padding: 12px 14px; border-radius: 10px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; font-size: 15px; }
-  .sh-btn { padding: 12px 18px; border-radius: 10px; border: none; background: var(--accent,#6b8f71); color: #fff; font-weight: 600; cursor: pointer; }
-</style>
Index: src/views/pages/tag.ejs
===================================================================
--- src/views/pages/tag.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/tag.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,13 +1,13 @@
 <div class="container tag-page">
   <header class="tag-page-header">
-    <p class="tag-eyebrow"><%= t('ptag.eyebrow') %></p>
+    <p class="tag-eyebrow">Tag</p>
     <h1>#<%= tag %></h1>
     <p class="tag-meta">
-      <%= posts.length === 1 ? t('ptag.count_one', { n: posts.length }) : t('ptag.count_many', { n: posts.length }) %>
+      <%= posts.length %> post<%= posts.length === 1 ? '' : 's' %>
     </p>
   </header>
 
   <% if (!posts.length) { %>
-    <p class="tag-empty"><%= t('ptag.empty') %></p>
+    <p class="tag-empty">No posts with this tag yet.</p>
   <% } else { %>
     <ol class="tag-results">
@@ -26,5 +26,5 @@
                  hx-get="/<%= p.slug %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
                  hx-push-url="/<%= p.slug %>" hx-indicator="#pcms-loading">
-                <%= p.title || t('ptag.untitled') %>
+                <%= p.title || '(untitled)' %>
               </a>
             </h2>
Index: src/views/pages/type.ejs
===================================================================
--- src/views/pages/type.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/type.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,11 +1,11 @@
 <div class="container type-page">
   <header class="type-page-header">
-    <p class="type-eyebrow"><%= t('ptype.eyebrow') %></p>
+    <p class="type-eyebrow">Type</p>
     <h1><%= type.charAt(0).toUpperCase() + type.slice(1) %></h1>
-    <p class="type-meta"><%= posts.length === 1 ? t('ptype.count_one', { n: posts.length }) : t('ptype.count_many', { n: posts.length }) %></p>
+    <p class="type-meta"><%= posts.length %> post<%= posts.length === 1 ? '' : 's' %></p>
   </header>
 
   <% if (!posts.length) { %>
-    <p class="type-empty"><%= t('ptype.empty') %></p>
+    <p class="type-empty">No posts of this type yet.</p>
   <% } else { %>
     <ol class="type-results">
@@ -24,5 +24,5 @@
                  hx-get="/<%= p.slug %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
                  hx-push-url="/<%= p.slug %>" hx-indicator="#pcms-loading">
-                <%= p.title || t('ptype.untitled') %>
+                <%= p.title || '(untitled)' %>
               </a>
             </h2>
Index: src/views/pages/user.ejs
===================================================================
--- src/views/pages/user.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/user.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -3,5 +3,5 @@
     <div class="user-avatar-large">
       <% if (author.avatar_url) { %>
-        <img src="<%= avatar(author.avatar_url, 320) %>" alt="">
+        <img src="<%= author.avatar_url %>" alt="">
       <% } else { %>
         <span><%= author.username.charAt(0).toUpperCase() %></span>
@@ -19,19 +19,24 @@
       <% } %>
       <p class="user-stats">
-        <%= posts.length %> <%= posts.length === 1 ? t('pusr.post_one') : t('pusr.post_many') %> <%= t('pusr.on_this_site') %>
+        <%= posts.length %> post<%= posts.length === 1 ? '' : 's' %> on this site
         <% if (totalPosts > posts.length) { %>
-          &middot; <%= t('pusr.total', { n: totalPosts }) %>
+          &middot; <%= totalPosts %> total
         <% } %>
         <% if (author.created_at) { %>
-          &middot; <%= t('pusr.joined', { date: formatDate(author.created_at) }) %>
+          &middot; joined <%= formatDate(author.created_at) %>
         <% } %>
       </p>
+      <% if (user && user.id !== author.id && site && site.enable_prutter) { %>
+        <p class="user-actions">
+          <a href="<%= siteUrlBase %>/prutter/new?to=<%= encodeURIComponent(author.username) %>" class="btn btn-primary">💬 Send DM</a>
+        </p>
+      <% } %>
     </div>
   </header>
 
   <% if (!posts.length) { %>
-    <p class="user-empty"><%= t('pusr.empty') %></p>
+    <p class="user-empty">No posts on this site yet.</p>
   <% } else { %>
-    <h2 class="user-posts-heading"><%= t('pusr.posts_heading') %></h2>
+    <h2 class="user-posts-heading">Posts</h2>
     <ol class="user-posts">
       <% posts.forEach(function(p) { %>
@@ -49,5 +54,5 @@
                  hx-get="/<%= p.slug %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
                  hx-push-url="/<%= p.slug %>" hx-indicator="#pcms-loading">
-                <%= p.title || t('pusr.untitled') %>
+                <%= p.title || '(untitled)' %>
               </a>
             </h3>
Index: src/views/pages/viewer-blocked.ejs
===================================================================
--- src/views/pages/viewer-blocked.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ 	(revision )
@@ -1,45 +1,0 @@
-<div class="container vb-page">
-  <div class="vb-card">
-    <div class="vb-ico" aria-hidden="true">👁️</div>
-    <h1 class="vb-title"><%= t('vblk.title') %></h1>
-    <p class="vb-text">
-      <%= t('vblk.text_before') %>
-      <strong><%= t('vblk.text_strong') %></strong> <%= t('vblk.text_after') %>
-    </p>
-    <div class="vb-actions">
-      <button type="button" class="vb-btn vb-btn-primary" data-back>← <%= t('vblk.back') %></button>
-      <a class="vb-btn" href="/"><%= t('vblk.to_home') %></a>
-    </div>
-  </div>
-</div>
-
-<style>
-.vb-page { max-width: 540px; margin: 3rem auto 4rem; padding: 0 1rem; }
-.vb-card {
-  text-align: center;
-  background: var(--paper);
-  border: 1px solid var(--rule);
-  border-radius: 16px;
-  padding: 2.5rem 1.75rem;
-}
-.vb-ico {
-  width: 64px; height: 64px; margin: 0 auto 1rem;
-  display: flex; align-items: center; justify-content: center;
-  font-size: 1.8rem; border-radius: 50%;
-  background: color-mix(in srgb, var(--accent) 14%, var(--paper-2));
-}
-.vb-title { font-family: var(--font-display, serif); font-size: 1.6rem; margin: 0 0 0.5rem; }
-.vb-text { color: var(--ink-muted, var(--ink-soft)); font-size: 0.95rem; line-height: 1.55; margin: 0 auto 1.5rem; max-width: 42ch; }
-.vb-actions { display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; }
-.vb-btn {
-  display: inline-flex; align-items: center; justify-content: center;
-  padding: 0.6rem 1.1rem; min-height: 42px;
-  border: 1px solid var(--rule); border-radius: 8px;
-  background: var(--paper-2); color: var(--ink);
-  font-family: var(--font-ui, system-ui), sans-serif; font-size: 0.9rem; font-weight: 600;
-  text-decoration: none; cursor: pointer; transition: border-color 120ms, opacity 120ms;
-}
-.vb-btn:hover { border-color: var(--accent); }
-.vb-btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
-.vb-btn-primary:hover { opacity: 0.92; }
-</style>
Index: src/views/pages/welcome.ejs
===================================================================
--- src/views/pages/welcome.ejs	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/views/pages/welcome.ejs	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -1,14 +1,14 @@
 <div class="container welcome-page">
-  <h1><%= t('welcome.title') %></h1>
-  <p class="welcome-tagline"><%= t('welcome.tagline') %></p>
-
+  <h1>Welcome to PrutCMS</h1>
+  <p class="welcome-tagline">A self-hosted publishing platform.</p>
+  
   <% if (!user) { %>
     <div class="welcome-actions">
-      <a href="/auth/register" class="btn btn-primary"><%= t('auth.create_admin') %></a>
-      <a href="/auth/login" class="btn"><%= t('welcome.have_account') %></a>
+      <a href="/auth/register" class="btn btn-primary">Create your account</a>
+      <a href="/auth/login" class="btn">Already have one? Login</a>
     </div>
-    <p class="welcome-note"><%= t('welcome.note') %></p>
+    <p class="welcome-note">The first user becomes <strong>god</strong> and gets a personal site auto-created.</p>
   <% } else { %>
-    <p><%= t('welcome.nosite', { user: user.username }) %></p>
+    <p>Hi <%= user.username %>! No site is configured yet.</p>
   <% } %>
 </div>
