Index: src/views/pages/account.ejs
===================================================================
--- src/views/pages/account.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/account.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -90,5 +90,5 @@
   </section>
 
-  <%# ── PERSOONLIJKE TAAL ───────────────────────────────────── %>
+  <%# ── PERSONAL LANGUAGE ──────────────────────────────────── %>
   <% if (canMutate) { %>
   <section class="ax-card">
@@ -110,5 +110,5 @@
   <% } %>
 
-  <%# ── SITE (eigenaar mag de naam van z'n site aanpassen) ── %>
+  <%# ── SITE (owner may update their site name) ─────────── %>
   <% if (typeof editableSite !== 'undefined' && editableSite && canMutate) { %>
   <section class="ax-card">
@@ -130,6 +130,6 @@
   <% } %>
 
-  <%# ── WACHTWOORD ────────────────────────────────────────── %>
-  <%# In kijker-modus geen wachtwoord-sectie (niets te wijzigen). %>
+  <%# ── PASSWORD ──────────────────────────────────────────── %>
+  <%# In viewer mode there is no password section (nothing to change). %>
   <% if (canMutate) { %>
   <% if (hasPassword) { %>
Index: src/views/pages/admin-circle.ejs
===================================================================
--- src/views/pages/admin-circle.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/admin-circle.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -79,5 +79,5 @@
                   <span class="circ-badge">&#9208; <%= _cPaused %></span>
                 <% } else { %>
-                  <%# Onbekende status: toon 'm rauw + eventuele reden i.p.v. stil "gepauzeerd". %>
+                  <%# Unknown status: show it raw + any reason instead of silently showing "paused". %>
                   <span class="circ-badge"><%= l.status %></span>
                   <% if (l.last_error) { %><div class="circ-reason"><%= l.last_error %></div><% } %>
@@ -123,6 +123,6 @@
 .circ-status { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem .5rem; margin-top: .25rem; font-size: .78rem; color: var(--ink-muted); }
 .circ-meta { color: var(--ink-muted); }
-/* Reden op een eigen regel onder de badge; normale woordafbreking (geen break-all
-   die hele zinnen midden in woorden knipt — dat zit op de URL-small, niet hier). */
+/* Reason on its own line below the badge; normal word breaking (no break-all
+   that cuts whole sentences mid-word — that is applied to the URL small, not here). */
 .circ-reason { flex-basis: 100%; line-height: 1.4; color: var(--ink-muted); word-break: normal; overflow-wrap: anywhere; }
 .btn-danger { background: #dc2626; color: #fff; border-color: #dc2626; }
Index: src/views/pages/admin-google.ejs
===================================================================
--- src/views/pages/admin-google.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/admin-google.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -103,5 +103,5 @@
 .set-redirect-warn { margin: 0.85rem 0 0; font-size: 0.85rem; color: var(--ink-muted); }
 .set-redirect-warn code { background: var(--paper-2); padding: 0.05rem 0.35rem; border-radius: 4px; }
-/* Genummerde, altijd-zichtbare stap-voor-stap-uitleg (beginnervriendelijk) */
+/* Numbered, always-visible step-by-step instructions (beginner-friendly) */
 .gg-steps { list-style: none; margin: 0 0 1rem; padding: 0; display: flex; flex-direction: column; gap: 0.85rem; }
 .gg-steps > li { display: flex; gap: 0.7rem; align-items: flex-start; }
Index: src/views/pages/admin-seo.ejs
===================================================================
--- src/views/pages/admin-seo.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/admin-seo.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -110,6 +110,6 @@
 
 .site-form { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }
-/* <fieldset> krimpt door de UA min-inline-size:min-content niet mee → mobiel-overflow.
-   min-width:0 dwingt meekrimpen. */
+/* <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; }
Index: src/views/pages/admin-settings.ejs
===================================================================
--- src/views/pages/admin-settings.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/admin-settings.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -40,5 +40,5 @@
   </section>
 
-  <%# ── Standaardtaal voor bezoekers ── %>
+  <%# ── Default language for visitors ── %>
   <section class="set-card" style="margin-top:1rem">
     <h2><%= t('aset.default_lang') %></h2>
@@ -58,5 +58,5 @@
   </section>
 
-  <%# ── Tijdzone ── %>
+  <%# ── Timezone ── %>
   <section class="set-card" style="margin-top:1rem">
     <h2><%= t('aset.timezone') %></h2>
@@ -78,6 +78,6 @@
   </section>
 
-  <%# Modus-specifieke config — staat direct onder de Modus-kaart zodat 'ie bij
-      de gekozen modus hoort (Hub bij hub, Cirkel bij circle). %>
+  <%# Mode-specific config — placed directly below the Mode card so it visually
+      belongs to the selected mode (Hub under hub, Circle under circle). %>
   <% if (tenancy === 'hub') { %>
   <section class="set-card set-card--mode" style="margin-top:1rem">
@@ -141,6 +141,6 @@
   <% } %>
 
-  <%# Premium (Patreon) — alleen zichtbaar als de self-hoster de premium-laag aanzet
-      (env KLONKT_PREMIUM_ENABLED=on). Standaard onzichtbaar; niets gegate. %>
+  <%# 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">
@@ -162,5 +162,5 @@
   <% } %>
 
-  <%# Google-login staat op een eigen Beheer-pagina (compacte link hier). %>
+  <%# Google login has its own admin page (compact link shown here). %>
   <section class="set-card" style="margin-top:1rem">
     <h2><%= t('aset.google_login') %></h2>
@@ -170,5 +170,5 @@
   </section>
 
-  <%# Nieuwsbrief-aanmelding in de footer (premium). %>
+  <%# Newsletter signup in the footer (premium). %>
   <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
   <section class="set-card" style="margin-top:1rem">
@@ -185,5 +185,5 @@
   <% } %>
 
-  <%# E-mail / SMTP — nodig voor nieuwsbrief versturen, notify-mails en wachtwoord-reset. %>
+  <%# 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>
@@ -233,10 +233,10 @@
 .admin-tagline a { color: var(--accent); }
 .set-card { background: var(--paper-2); border: 1px solid var(--rule); border-radius: 12px; padding: 1.25rem; }
-/* Modus-specifieke kaart (Hub/Cirkel): accent-rand links zodat 'ie zichtbaar bij
-   de gekozen modus hoort, direct onder de Modus-kaart. */
+/* 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-gids (uitklapbaar) */
+/* 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; }
@@ -249,6 +249,6 @@
 .set-google-guide .set-help { padding: 0 0.85rem; }
 .set-form { display: flex; flex-direction: column; gap: 1rem; max-width: 460px; }
-/* Kale <label>'s in een set-form krijgen dezelfde nette stacked-stijl als .set-field:
-   label-tekst boven, input full-width. (.set-toggle = checkbox-rij, uitgezonderd.) */
+/* 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)); }
@@ -277,6 +277,6 @@
 .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 dezelfde nette stijl als de inputs i.p.v. de kale native dropdown:
-   eigen chevron, accent-focus, volle breedte. */
+/* 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;
Index: src/views/pages/admin-shows.ejs
===================================================================
--- src/views/pages/admin-shows.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/admin-shows.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -63,6 +63,6 @@
   .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; }
-  /* Mobiel-eerst: één kolom (twee 1fr-kolommen met niet-krimpbare date/text-inputs
-     liepen samen ~374px > telefoonbreedte). Twee kolommen pas vanaf 520px. */
+  /* 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; } }
Index: src/views/pages/admin-site-edit.ejs
===================================================================
--- src/views/pages/admin-site-edit.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/admin-site-edit.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -24,7 +24,7 @@
         <input type="text" name="title" value="<%= site.title || '' %>" required maxlength="200">
       </label>
-      <%# Eigenaar (her)toewijzen — ALLEEN god ÉN alleen in hub-modus. In hub geef
-          je zo elke gebruiker z'n eigen, zelf te beheren Klonkt. In solo (één site,
-          één eigenaar) is dit zinloos/verwarrend, dus verborgen. %>
+      <%# Assign (or reassign) owner — ONLY god AND only in hub mode. In hub this
+          gives each user their own self-managed Klonkt. In solo (one site,
+          one owner) this is pointless/confusing, so it's hidden. %>
       <% if (user && user.role === 'god' && (typeof tenancy !== 'undefined' && tenancy === 'hub')) { %>
       <label>
@@ -250,6 +250,6 @@
 <script>
 (function() {
-  // Slug-URL-veld: toon de echte host als gedimde prefix (de slug zelf is
-  // gekleurd via CSS). Hub → host/user/<slug>, anders host/<slug>.
+  // Slug URL field: show the real host as a dimmed prefix (the slug itself is
+  // coloured via CSS). Hub → host/user/<slug>, otherwise host/<slug>.
   var sh = document.getElementById('slug-host');
   if (sh) sh.textContent = location.host + (sh.dataset.prefix || '/');
@@ -354,8 +354,8 @@
 
 .site-form { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }
-/* Mobiel: een <fieldset> heeft een browser-default min-inline-size:min-content en
-   krimpt daardoor NIET mee → het hele formulier werd breder dan het scherm. min-width:0
-   forceert meekrimpen; de form-velden krijgen max-width:100% + border-box zodat ze
-   nooit buiten hun fieldset lopen. */
+/* 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; }
@@ -398,5 +398,5 @@
 .site-form input[readonly] { opacity: 0.6; cursor: not-allowed; background: var(--paper-2); }
 
-/* Slug als URL-veld: gedimde host-prefix + de slug zelf in de accentkleur. */
+/* Slug as URL field: dimmed host prefix + the slug itself in the accent colour. */
 .slug-url {
   display: flex; align-items: stretch;
@@ -425,6 +425,6 @@
 }
 
-/* Selects: zelfde look als de inputs, met een eigen chevron i.p.v. de kale
-   native pijl. De geopende optielijst blijft door de browser gerenderd. */
+/* 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;
@@ -562,7 +562,7 @@
   .photo-controls { gap: .55rem; }
 }
-/* Mobiel-eerst: platform-keuze op een eigen regel (volle breedte), daaronder de
-   URL-input + verwijderknop. Voorkomt horizontale overflow op telefoons (de vaste
-   160px-kolom + niet-krimpbare input liepen samen ~447px > schermbreedte). */
+/* 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;
Index: src/views/pages/admin-users.ejs
===================================================================
--- src/views/pages/admin-users.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/admin-users.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -71,6 +71,6 @@
 
           <div class="ax-user-controls">
-            <%# Hub: geef deze gebruiker een eigen, zelf te beheren Klonkt — opent het
-                nieuw-site-formulier met deze gebruiker al als eigenaar voorgeselecteerd. %>
+            <%# Hub: give this user their own self-managed Klonkt — opens the
+                new-site form with this user already pre-selected as owner. %>
             <% if (typeof tenancy !== 'undefined' && tenancy === 'hub' && canMutate) { %>
               <a href="/admin/sites/new?owner=<%= u.id %>" class="ax-icon-btn"
@@ -98,5 +98,5 @@
               </form>
             <% } else { %>
-              <%# Lege plek zodat de rol-kolom op elke rij uitlijnt. %>
+              <%# Empty placeholder so the role column aligns on every row. %>
               <span class="ax-icon-btn-spacer" aria-hidden="true"></span>
             <% } %>
Index: src/views/pages/auth-login.ejs
===================================================================
--- src/views/pages/auth-login.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/auth-login.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -1,7 +1,7 @@
 <%
-  // adminLogin = de verborgen beheerders-loginpagina (/auth/admin): toont het
-  // gebruikersnaam/wachtwoord-formulier. De publieke /auth/login toont voor
-  // bezoekers ALLEEN Google-login (luisteraars/fans). Zo staat de admin-login
-  // niet zichtbaar op de plek waar bezoekers heen gestuurd worden.
+  // adminLogin = the hidden admin login page (/auth/admin): shows the
+  // username/password form. The public /auth/login shows visitors
+  // ONLY Google login (listeners/fans). This keeps the admin login
+  // off the page where visitors are directed.
   var _admin = (typeof adminLogin !== 'undefined' && adminLogin);
 %>
@@ -32,5 +32,5 @@
 
   <% if (_admin) { %>
-    <%# Verborgen beheerders-login (wachtwoord). %>
+    <%# Hidden admin login (password). %>
     <form method="post" action="/auth/login" class="auth-form">
       <% if (typeof next !== 'undefined' && next) { %>
@@ -49,5 +49,5 @@
     </form>
   <% } else if (googleReady) { %>
-    <%# Publieke login: alleen Google, voor luisteraars/fans. %>
+    <%# Public login: Google only, for listeners/fans. %>
     <p class="auth-sub"><%= t('auth.public_sub') %></p>
     <a class="btn btn-google" href="/auth/google<%= (typeof next !== 'undefined' && next) ? '?next=' + encodeURIComponent(next) : '' %>">
@@ -65,5 +65,5 @@
 
   <% if (!_admin) { %>
-    <%# Klikbare box voor de beheerder → naar de verborgen wachtwoord-login. %>
+    <%# Clickable box for the admin → navigates to the hidden password login. %>
     <a class="auth-admin-box" href="/auth/admin<%= (typeof next !== 'undefined' && next) ? '?next=' + encodeURIComponent(next) : '' %>">
       <span class="auth-admin-box-icon" aria-hidden="true">🔑</span>
@@ -85,6 +85,6 @@
   .auth-notice-list li { margin: 4px 0; }
   .auth-sub { color: var(--ink-muted); margin: 0 0 1rem; text-align: center; }
-  /* Google-knop is wit → tekst altijd donker houden, ook op hover (de globale
-     a.btn:hover-regel maakt 'm anders bijna-wit op wit = onleesbaar). */
+  /* Google button is white → keep text dark at all times, including hover (the global
+     a.btn:hover rule would otherwise make it near-white on white = unreadable). */
   a.btn-google, a.btn-google:hover { color: #1f1f1f; }
   .auth-admin-box {
Index: src/views/pages/circle-feed.ejs
===================================================================
--- src/views/pages/circle-feed.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/circle-feed.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -26,5 +26,5 @@
     </div>
     <% } else { %>
-      <%# Meer dan 5: geen chips (zou een muur worden) — alleen het aantal. %>
+      <%# More than 5: no chips (would become a wall) — show the count only. %>
       <span class="cirkel-count"><%= t('cfeed.count', { n: sites.length }) %></span>
     <% } %>
@@ -38,8 +38,8 @@
 <% } else { %>
 
-  <%# Dezelfde view als de hoofdpagina: timeline (post-card) + grid (post-tile),
-      gewisseld door de view-switcher via body[data-feed-view]. De remote-posts
-      zijn in circle.js naar het lokale post-shape gevormd (met external_url,
-      zodat de kaarten naar de bron-site linken). %>
+  <%# 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) ===== -->
Index: src/views/pages/fan-gate.ejs
===================================================================
--- src/views/pages/fan-gate.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/fan-gate.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -1,6 +1,6 @@
 <div class="container fg-navwrap">
-  <%# Zelfde post-navigatie (Newer/Older) als op een gewone post, zodat een
-      bezoeker op de fan-gate niet vastloopt maar verder kan bladeren. Zelfde
-      maten als .post-page (720px / padding 0 1rem) zodat de nav pixel-gelijk is. %>
+  <%# 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>
@@ -17,6 +17,6 @@
 
 <style>
-  /* EXACT dezelfde class-combinatie (.container + override) én waarden als
-     .post-page in post.ejs, zodat de post-nav cascade-identiek uitlijnt. */
+  /* 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; }
Index: src/views/pages/hub-home.ejs
===================================================================
--- src/views/pages/hub-home.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/hub-home.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -1,6 +1,6 @@
 <div class="hub<%= hub.heroImage ? ' hub--img' : '' %>">
 
-  <%# ── HERO (label) — de mini topnav-balk staat erboven (chrome.ejs);
-        de hero zelf is de "grote" header van de hub-home ──────────────── %>
+  <%# ── HERO (label) — the mini topnav bar sits above it (chrome.ejs);
+        the hero itself is the "large" header of the hub home ──────────────── %>
   <header class="hub-hero<%= hub.heroImage ? ' has-img' : '' %>"<% if (hub.heroImage) { %> style="--hero-img:url('<%= hub.heroImage %>'); --hero-ov:<%= ((hub.heroOverlay == null ? 45 : hub.heroOverlay)/100).toFixed(2) %>"<% } %>>
     <div class="hub-hero-inner container">
@@ -17,5 +17,5 @@
   <div class="container hub-body">
 
-    <%# ── HOOFDPAGINA (label-/hoofdaccount — geen artiest) ──────────── %>
+    <%# ── MAIN PAGE (label-/primary account — not an artist) ──────────── %>
     <% if (mainSite) { %>
     <section class="hub-sec hub-sec-main">
@@ -37,5 +37,5 @@
     <% } %>
 
-    <%# ── ARTIESTEN-ROSTER ─────────────────────────────────────────── %>
+    <%# ── ARTIST ROSTER ─────────────────────────────────────────── %>
     <% if (artists && artists.length) { %>
     <% var _total = (typeof totalArtists !== 'undefined') ? totalArtists : artists.length; %>
@@ -69,5 +69,5 @@
     <% } %>
 
-    <%# ── LAATSTE POSTS ────────────────────────────────────────────── %>
+    <%# ── LATEST POSTS ────────────────────────────────────────────── %>
     <% if (posts && posts.length) { %>
     <section class="hub-sec">
@@ -99,6 +99,6 @@
 .hub { width: 100%; position: relative; }
 
-/* Hero sluit direct aan op de mini topnav-balk — geen tussenruimte boven de
-   hub-home (override van de globale main-padding-top). */
+/* Hero connects directly to the mini topnav bar — no gap above the
+   hub home (overrides the global main-padding-top). */
 .pcms-main { padding-top: 0; }
 .hub-hero { margin-top: 0; }
@@ -160,5 +160,5 @@
 .hub-more-link:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, var(--paper-2)); }
 
-/* ── Hoofdpagina-kaart (label-/hoofdaccount, apart van de artiesten) ── */
+/* ── Main page card (label-/primary account, separate from the artists) ── */
 .hub-sec-main { margin-top: 2.25rem; }
 .hub-main-card {
@@ -199,5 +199,5 @@
 }
 
-/* ── Roster (artiesten) ───────────────────────────────────────────── */
+/* ── Roster (artists) ───────────────────────────────────────────── */
 .roster {
   display: grid;
@@ -231,5 +231,5 @@
 }
 
-/* ── Feed (laatste posts) ─────────────────────────────────────────── */
+/* ── Feed (latest posts) ─────────────────────────────────────────── */
 .hub-feed { display: flex; flex-direction: column; gap: 0.6rem; }
 .feed-item {
Index: src/views/pages/post-edit.ejs
===================================================================
--- src/views/pages/post-edit.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/post-edit.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -9,7 +9,7 @@
 // and styling changed.
 const _hasCover = !!post.cover_image_url;
-// In hub-modus moeten save/create/cancel de /user/<slug>/-prefix dragen, anders
-// valt de request terug op de primaire site (siteUrlBase leeg) en rendert de
-// post na opslaan headerless (bareChrome). In solo is _base leeg.
+// 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 : '';
 %>
@@ -138,6 +138,6 @@
              data-placeholder="<%= t('pedit.editor_placeholder') %>"></div>
 
-        <%# Sticky "tik om te bewerken"-hint (alleen zichtbaar op touch + niet-fullscreen
-            via CSS). Puur visueel (pointer-events:none); de tik op het veld opent fullscreen. %>
+        <%# 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>
 
@@ -225,7 +225,7 @@
               if (!cb || !box) return;
               var inp = document.getElementById('pe-publish-at');
-              var SITE_TZ = '<%= timezone || '' %>'; // ingestelde site-tijdzone; leeg = browser-lokaal
+              var SITE_TZ = '<%= timezone || '' %>'; // configured site timezone; empty = browser local
               var pad = function (n) { return String(n).padStart(2, '0'); };
-              // Offset (ms) tussen een tijdzone en UTC op een bepaald moment.
+              // Offset (ms) between a timezone and UTC at a given moment.
               function tzOffset(date, tz) {
                 var f = new Intl.DateTimeFormat('en-US', { timeZone: tz, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
@@ -233,5 +233,5 @@
                 return Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second) - date.getTime();
               }
-              // datetime-local "wandtijd" (in de site-zone) → UTC Date.
+              // datetime-local "wall time" (in the site zone) → UTC Date.
               function wallToUtc(wall) {
                 if (!SITE_TZ) return new Date(wall);
@@ -239,5 +239,5 @@
                 return new Date(guess - tzOffset(new Date(guess), SITE_TZ));
               }
-              // UTC-ISO → "YYYY-MM-DDTHH:MM" wandtijd in de site-zone.
+              // UTC-ISO → "YYYY-MM-DDTHH:MM" wall time in the site zone.
               function utcToWall(iso) {
                 var d = new Date(iso); if (isNaN(d)) return '';
@@ -247,7 +247,7 @@
                 return p.year + '-' + p.month + '-' + p.day + 'T' + p.hour + ':' + p.minute;
               }
-              // Prefill: opgeslagen UTC → wandtijd in de site-zone.
+              // Prefill: stored UTC → wall time in the site zone.
               if (inp && inp.dataset.iso) inp.value = utcToWall(inp.dataset.iso);
-              // "Ingepland voor" in leesbare tijd van de site-zone.
+              // "Scheduled for" in human-readable time in the site zone.
               var when = document.getElementById('pe-sched-when');
               if (when && when.dataset.iso) {
@@ -257,5 +257,5 @@
               function sync() { box.style.display = cb.checked ? '' : 'none'; if (inp) inp.disabled = !cb.checked; }
               cb.addEventListener('change', sync); sync();
-              // Bij opslaan: wandtijd in de site-zone → UTC-ISO via een hidden veld.
+              // On save: wall time in the site zone → UTC-ISO via a hidden field.
               var form = cb.closest('form');
               if (form) {
@@ -466,5 +466,5 @@
   width: 32px; height: 32px;
   padding: 0;
-  touch-action: manipulation; /* snappy taps op mobiel, geen dubbeltik-zoom */
+  touch-action: manipulation; /* snappy taps on mobile, no double-tap zoom */
   -webkit-user-select: none; user-select: none;
   background: transparent;
@@ -478,6 +478,6 @@
   -webkit-tap-highlight-color: transparent;
 }
-/* Hover alleen op echte hover-apparaten — op touch blijft :hover anders "plakken"
-   na een tik, waardoor de actief/inactief-staat onleesbaar wordt. */
+/* 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 {
@@ -487,6 +487,6 @@
 }
 .pe-toolbar button:active { transform: scale(.94); }
-/* Actieve opmaak = onmiskenbaar gevuld met de accentkleur. Zo is op mobiel
-   meteen duidelijk dat bv. vet AAN staat (tik nogmaals = uit). */
+/* 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);
@@ -505,5 +505,5 @@
 .pe-toolbar-spacer { flex: 1; }
 
-/* ─── Volledig-scherm schrijfmodus ─── */
+/* ─── Full-screen writing mode ─── */
 .fs-icon-compress { display: none; }
 .pe-editor-frame.pe-fs .fs-icon-expand { display: none; }
@@ -514,20 +514,20 @@
   height: 100dvh;
   background: var(--paper);
-  overflow: hidden;   /* alléén het tekstveld scrollt, niet het frame zelf */
-}
-/* In fullscreen vult het schrijfveld de resterende ruimte en scrollt het zelf
-   (max-height: none overschrijft de mobiele box-begrenzing hieronder). */
+  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;
 }
-/* NB: bewust GEEN overflow:hidden op html/body in fullscreen — dat kon blijven
-   hangen (bv. fullscreen verlaten via navigatie) en blokkeerde dan het scrollen van
-   de hele pagina. Het fullscreen-frame (position:fixed, inset:0) dekt de pagina al
-   af, en op touch zijn de scrollbalken verborgen → geen dubbele balk nodig. */
-
-/* "Klaar"-knop: alleen in fullscreen zichtbaar (links in de toolbar), duidelijke
-   accent-pill i.p.v. een vierkant icoontje. */
-/* Verberg-regel specifieker dan ".pe-toolbar button" (anders wint die en zie je
-   de Klaar-knop ook inline als kleine knop). Alleen in fullscreen tonen → groot. */
+/* 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 {
@@ -556,13 +556,13 @@
 }
 .pe-editor:focus { outline: none; }
-/* Inline (niet-fullscreen) groeit het schrijfveld gewoon mee met de inhoud — geen
-   interne scroll-box (scroll-binnen-scroll is verwarrend). Op mobiel/tablet gaat
-   typen sowieso fullscreen (zie JS), waar het veld de pagina vult en als enige
-   scrollt. */
+/* 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: het inline content-veld is geen tekstveld maar een tap-vlak → fullscreen
-   bewerken. Een "✎ Tik om te bewerken"-pill plakt sticky onderaan de container,
-   zodat de hint altijd in beeld is zonder midden in de tekst te staan. */
+/* 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; }
@@ -571,5 +571,5 @@
     display: block;
     position: sticky;
-    bottom: 4.5rem;   /* boven de sticky Opslaan/Annuleren-balk */
+    bottom: 4.5rem;   /* above the sticky Save/Cancel bar */
     width: max-content;
     max-width: calc(100% - 2rem);
@@ -580,9 +580,9 @@
     pointer-events: none; text-align: center; white-space: nowrap;
   }
-  /* Niet relevant op touch (slepen/selecteren hoort bij inline-bewerken op desktop). */
+  /* Not relevant on touch (drag/select belongs to inline editing on desktop). */
   .pe-content-hint { display: none; }
-  /* Inline (niet-fullscreen) op touch: simpel houden — je bewerkt hier toch niet,
-     dus geen opmaak-toolbar en geen losse rand. Alleen de content-preview + de
-     "tik om te bewerken"-pill. De toolbar verschijnt pas in fullscreen. */
+  /* 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; }
@@ -955,5 +955,5 @@
 }
 
-/* Vastpinnen: checkbox + ▲▼-stepper met beschrijving (i.p.v. een ruw rang-getal). */
+/* 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; }
@@ -978,5 +978,5 @@
   gap: 0.5rem;
   padding: 0.85rem 1rem;
-  margin: 0.5rem -1rem 0;   /* extend to viewport edges on tight container */
+  margin: 0.5rem -1rem 0;   /* extend to viewport edges on a narrow container */
   background: color-mix(in srgb, var(--paper) 94%, transparent);
   -webkit-backdrop-filter: blur(8px);
@@ -987,14 +987,14 @@
 .pe-actions-spacer { flex: 1; }
 
-/* De editor is een focus-scherm: verberg de mobiele site-tabbalk (Home/Zoek/…)
-   zodat er niet twee balken onderaan stapelen (Opslaan-balk + tabbalk). De
-   Opslaan/Annuleren-balk wordt dan de enige onderbalk → gewoon op bottom:0,
-   geen tab-offset meer nodig. Op desktop is de tabbalk toch al verborgen. */
+/* 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; }
-/* Audiospeler (indien aan het spelen) niet meer 56px optillen voor de nu-verborgen
-   tabbalk, anders zou die zweven boven de actiebalk. */
+/* 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; }
 
-/* ── Afbeeldings-editor (draaien / bijsnijden / spiegelen) ── */
+/* ── Image editor (rotate / crop / mirror) ── */
 .imed-backdrop {
   position: fixed; inset: 0; z-index: 9999;
@@ -1015,6 +1015,6 @@
 }
 .imed-stage img { display: block; max-width: 100%; }
-/* Cropper z'n container moet de volle stage-hoogte vullen — anders klapt 'ie dicht
-   in een flex-kolom zonder expliciete hoogte → leeg bewerk-venster (geen afbeelding). */
+/* 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 {
@@ -1052,6 +1052,6 @@
   }
 
-  // ── Afbeeldings-editor (draaien / bijsnijden / spiegelen) ──────────
-  // Lazy-load Cropper.js (lokaal gevendord) pas bij het eerste gebruik.
+  // ── Image editor (rotate / crop / mirror) ──────────
+  // Lazy-load Cropper.js (locally vendored) on first use.
   let _cropperReady = null;
   function ensureCropper() {
@@ -1073,11 +1073,11 @@
   }
 
-  // Opent de editor voor een gekozen bestand; resolved met een bewerkt File,
-  // of null als de gebruiker annuleert. Animatie-GIF's worden niet door de
-  // canvas-editor gehaald (zouden statisch worden) — die uploaden direct.
+  // Opens the editor for a chosen file; resolves with an edited File,
+  // or null if the user cancels. Animated GIFs are not sent through the
+  // canvas editor (they would become static) — those upload directly.
   async function openImageEditor(file) {
     if (!file || !file.type || !file.type.startsWith('image/')) return file;
-    if (file.type === 'image/gif') return file; // behoud animatie
-    try { await ensureCropper(); } catch (_) { return file; } // editor onbeschikbaar → direct uploaden
+    if (file.type === 'image/gif') return file; // preserve animation
+    try { await ensureCropper(); } catch (_) { return file; } // editor unavailable → upload directly
 
     return new Promise((resolve) => {
@@ -1118,5 +1118,5 @@
         cropper = new Cropper(img, { viewMode: 1, autoCropArea: 1, background: false, responsive: true });
       };
-      img.onerror = () => { cleanup(); resolve(file); }; // kon niet laden → upload het origineel
+      img.onerror = () => { cleanup(); resolve(file); }; // could not load → upload the original
       img.src = url;
 
@@ -1179,5 +1179,5 @@
       const edited = await openImageEditor(coverField.files[0]);
       coverField.value = '';
-      if (!edited) return; // geannuleerd
+      if (!edited) return; // cancelled
       coverStatus.classList.remove('is-error');
       coverStatus.textContent = '<%= t('pedit.js_uploading') %>';
@@ -1224,6 +1224,6 @@
   if (!editor) return;
 
-  // Titel alleen automatisch focussen op desktop (muis/trackpad). Op touch zou
-  // dit meteen het toetsenbord openen bij het openen van de editor — niet gewenst.
+  // Auto-focus the title only on desktop (mouse/trackpad). On touch this would
+  // immediately open the keyboard when the editor opens — not desired.
   try {
     const titleInput = form && form.querySelector('input[name="title"]');
@@ -1329,9 +1329,9 @@
 
   // ── Toolbar wiring
-  // Houd de scrollpositie vast rond een edit-commando. execCommand/insert scrollt
-  // standaard de caret in beeld → het beeld "verspringt" bij het aanklikken van een
-  // opmaakknop. We leggen ALLE scrollbare voorouders (editor, frame, #pcms-main, …)
-  // + de pagina vast en zetten ze terug — sync én over een paar frames, want Chrome
-  // scrollt soms pas een frame later. De gebruiker scrollt zo zelf.
+  // Lock the scroll position around an edit command. execCommand/insert scrolls
+  // the caret into view by default → the view "jumps" when clicking a formatting
+  // button. We lock ALL scrollable ancestors (editor, frame, #pcms-main, …)
+  // + the page and restore them — sync and over a few frames, because Chrome
+  // sometimes scrolls a frame later. The user scrolls themselves.
   function scrollableAncestors(el) {
     const list = [];
@@ -1345,6 +1345,6 @@
   }
   function keepScroll(fn) {
-    // In fullscreen staat de pagina vast (body overflow:hidden) en mag het veld
-    // gewoon naar de caret scrollen — geen pagina-sprong mogelijk, dus niets fixen.
+    // In fullscreen the page is locked (body overflow:hidden) and the field may
+    // scroll to the caret freely — no page jump possible, so nothing to fix.
     const frame = document.querySelector('.pe-editor-frame');
     if (frame && frame.classList.contains('pe-fs')) { fn(); return; }
@@ -1389,5 +1389,5 @@
     execCmd('createLink', url);
   }
-  // De huidige selectie zit in een <blockquote> binnen de editor? Geef 'm terug.
+  // Is the current selection inside a <blockquote> within the editor? Return it.
   function blockquoteAncestor() {
     const sel = window.getSelection();
@@ -1400,7 +1400,7 @@
     return null;
   }
-  // Echte toggle: execCommand('formatBlock','blockquote') zet 'm wél AAN maar
-  // krijgt 'm nooit meer UIT (browser-quirk). Staat de caret al in een quote →
-  // pak de wrapper uit; anders pas blockquote toe.
+  // Real toggle: execCommand('formatBlock','blockquote') does turn it ON but
+  // can never turn it OFF (browser quirk). If the caret is already in a quote →
+  // unwrap it; otherwise apply blockquote.
   function toggleBlockquote() {
     keepScroll(function () {
@@ -1409,5 +1409,5 @@
       if (bq) {
         const parent = bq.parentNode;
-        // Inhoud uit de quote halen, op z'n plek, en de lege wrapper verwijderen.
+        // Extract content from the quote in place, then remove the empty wrapper.
         const ref = bq;
         let firstMoved = null;
@@ -1418,5 +1418,5 @@
         }
         parent.removeChild(bq);
-        // Caret terugzetten in de uitgepakte inhoud.
+        // Restore the caret inside the unwrapped content.
         if (firstMoved) {
           const sel = window.getSelection();
@@ -1436,9 +1436,9 @@
 
   if (toolbar) {
-    // CRUCIAAL (mobiel + desktop): voorkom dat een toolbar-knop de focus/selectie
-    // uit het editor-veld steelt. Zonder dit raakt de selectie kwijt bij het tikken
-    // → execCommand werkt op een lege selectie (vet kan niet meer UIT) én de browser
-    // scrollt de caret opnieuw in beeld (de "sprong naar beneden"). preventDefault op
-    // mousedown houdt de focus in de editor; de click blijft gewoon vuren.
+    // CRUCIAL (mobile + desktop): prevent a toolbar button from stealing focus/selection
+    // from the editor field. Without this the selection is lost on tap
+    // → execCommand operates on an empty selection (bold can no longer be toggled OFF)
+    // and the browser scrolls the caret back into view (the "jump down"). preventDefault
+    // on mousedown keeps focus in the editor; the click still fires normally.
     toolbar.addEventListener('mousedown', (e) => {
       if (e.target.closest('button')) e.preventDefault();
@@ -1457,13 +1457,13 @@
   }
 
-  // ── Volledig-scherm schrijfmodus: het schrijfveld vult de hele pagina.
+  // ── Full-screen writing mode: the writing field fills the whole page.
   const fsBtn = document.getElementById('pe-fullscreen-btn');
   const editorFrame = document.querySelector('.pe-editor-frame');
   const isTouch = !!(window.matchMedia && window.matchMedia('(pointer: coarse)').matches);
 
-  // Op mobiel duwt het toetsenbord de zichtbare (visual) viewport omhoog terwijl
-  // een position:fixed-frame aan de LAYOUT-viewport blijft hangen → de toolbar
-  // schuift uit beeld. Houd het fullscreen-frame daarom gelijk aan de visual
-  // viewport (top + hoogte), zodat de toolbar altijd bovenaan zichtbaar blijft.
+  // On mobile the keyboard pushes the visible (visual) viewport up while
+  // a position:fixed frame stays pinned to the LAYOUT viewport → the toolbar
+  // slides out of view. Keep the fullscreen frame aligned to the visual
+  // viewport (top + height) so the toolbar stays visible at the top.
   function syncFsViewport() {
     if (!editorFrame || !editorFrame.classList.contains('pe-fs')) return;
@@ -1499,5 +1499,5 @@
       }
     }
-    // Op touch is het veld inline NIET bewerkbaar; alleen in fullscreen wel.
+    // On touch the field is NOT editable inline; only in fullscreen.
     if (isTouch) editor.setAttribute('contenteditable', on ? 'true' : 'false');
     if (on) {
@@ -1505,6 +1505,6 @@
     } else {
       if (isTouch) editor.blur();
-      // Bij sluiten: naar de TOP van de content scrollen i.p.v. ergens onderaan
-      // (footer) te blijven hangen.
+      // On close: scroll to the TOP of the content instead of staying
+      // somewhere at the bottom (footer).
       requestAnimationFrame(function () {
         try { editorFrame.scrollIntoView({ block: 'start' }); } catch (_) {}
@@ -1512,7 +1512,7 @@
     }
   }
-  // De fullscreen schrijf-"pagina": openen pusht een history-state zodat de browser-
-  // back-knop (en de Klaar-knop) 'm sluit en je terug bent in het formulier — voelt
-  // als een aparte pagina, maar álle formuliervelden blijven intact (zelfde DOM).
+  // The fullscreen writing "page": opening pushes a history state so the browser
+  // back button (and the Done button) closes it and returns you to the form — feels
+  // like a separate page, but all form fields remain intact (same DOM).
   function openFs() {
     if (isFs()) return;
@@ -1522,5 +1522,5 @@
   function closeFs() {
     if (!isFs()) return;
-    if (history.state && history.state.peFs) history.back(); // → popstate sluit af
+    if (history.state && history.state.peFs) history.back(); // → popstate closes it
     else applyFs(false);
   }
@@ -1534,7 +1534,7 @@
   });
 
-  // Op mobiel/tablet (touch): het content-veld is INLINE NIET bewerkbaar — het is
-  // dan geen tekstveld. Eén tik → fullscreen, waar het wél bewerkbaar wordt
-  // (toggleFullscreen zet contenteditable aan/uit). Zo kun je nooit inline typen.
+  // On mobile/tablet (touch): the content field is NOT editable inline — it is
+  // not a text field there. One tap → fullscreen, where it becomes editable
+  // (toggleFullscreen toggles contenteditable). This prevents inline typing.
   if (isTouch) {
     editor.setAttribute('contenteditable', 'false');
@@ -1555,5 +1555,5 @@
       try { btn.classList.toggle('is-active', document.queryCommandState(cmd)); } catch(_) {}
     }
-    // Quote-knop: actief als de caret in een <blockquote> staat (toggle-feedback).
+    // Quote button: active when the caret is inside a <blockquote> (toggle feedback).
     const bqBtn = toolbar.querySelector('button[data-cmd="formatBlock"][data-arg="blockquote"]');
     if (bqBtn) bqBtn.classList.toggle('is-active', !!blockquoteAncestor());
@@ -1588,5 +1588,5 @@
   async function uploadAndInsertImage(file) {
     const edited = await openImageEditor(file);
-    if (!edited) return; // geannuleerd
+    if (!edited) return; // cancelled
     contentStatus.classList.remove('is-error');
     contentStatus.textContent = '<%= t('pedit.js_uploading') %>';
@@ -1654,6 +1654,6 @@
   }
 
-  // ── Embed insert: plak een platform-URL -> [[embed:url]]-chip die server-side
-  //    een iframe wordt (YouTube/Spotify/SoundCloud/Vimeo/Apple Music/Bandcamp).
+  // ── Embed insert: paste a platform URL -> [[embed:url]]-chip that becomes
+  //    an iframe server-side (YouTube/Spotify/SoundCloud/Vimeo/Apple Music/Bandcamp).
   const embedBtn = document.getElementById('insert-embed-btn');
   if (embedBtn) {
@@ -1674,5 +1674,5 @@
     const tpEmpty  = document.getElementById('tp-empty');
     const tpSearch = document.getElementById('tp-search');
-    let tpCache = null;       // cached tracks list (fetched once per page load)
+    let tpCache = null;       // cached track list (fetched once per page load)
     let tpLastFocus = null;   // element to restore focus to on close
 
@@ -1870,11 +1870,11 @@
 <script>
 (function () {
-  // Vastpinnen: checkbox toggelt het verborgen rang-veld (0 = niet gepind),
-  // ▲▼ verschuift de plek, met een leesbare beschrijving i.p.v. een ruw getal.
+  // Pin: checkbox toggles the hidden rank field (0 = not pinned),
+  // ▲▼ shifts the position, with a readable description instead of a raw number.
   var toggle = document.getElementById('pin-toggle');
   var rank   = document.getElementById('pin-rank');
   var pos    = document.getElementById('pin-pos');
   var label  = document.getElementById('pin-label');
-  var up     = document.getElementById('pin-up');    // hoger = lager getal (richting 1/bovenaan)
+  var up     = document.getElementById('pin-up');    // higher = lower number (towards 1/top)
   var down   = document.getElementById('pin-down');
   if (!toggle || !rank || !pos) return;
@@ -1900,11 +1900,11 @@
 
 (function () {
-  // De Opslaan/Annuleren-balk (position: sticky; bottom:0) net boven twee mogelijke
-  // obstakels houden door een dynamische bottom-offset te zetten = het grootste van:
-  //  1) de hoogte van het toetsenbord-deel dat de layout-viewport NIET dekt
-  //     (op iOS verschuift de visual viewport; op Android verkleint de layout-
-  //     viewport door interactive-widget=resizes-content → offset ≈ 0);
-  //  2) de hoogte van de spelende audiospeler (fixed, z-index 1000).
-  // We blijven bij sticky (geen fixed/top-gegoochel → geen balk midden in beeld).
+  // Keep the Save/Cancel bar (position: sticky; bottom:0) just above two possible
+  // obstacles by setting a dynamic bottom offset = the greater of:
+  //  1) the height of the keyboard area NOT covered by the layout viewport
+  //     (on iOS the visual viewport shifts; on Android the layout viewport shrinks
+  //     due to interactive-widget=resizes-content → offset ≈ 0);
+  //  2) the height of the playing audio player (fixed, z-index 1000).
+  // We stick with sticky (no fixed/top tricks → no bar floating in the middle).
   var bar = document.querySelector('.pe-actions');
   if (!bar) return;
@@ -1922,5 +1922,5 @@
   window.addEventListener('resize', position);
   if (vv) { vv.addEventListener('resize', position); vv.addEventListener('scroll', position); }
-  // has-audio-player wisselt via een body-class → daarop reageren.
+  // has-audio-player is toggled via a body class → observe it.
   try { new MutationObserver(position).observe(document.body, { attributes: true, attributeFilter: ['class'] }); } catch (_) {}
 })();
Index: src/views/pages/post.ejs
===================================================================
--- src/views/pages/post.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/pages/post.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -1,11 +1,11 @@
 <%
-// In hub-mode is de site bereikbaar onder /user/<slug>; alle site-scoped links
-// (tag, edit, delete, comments) moeten met die prefix anders vallen ze terug op
-// de primaire site → 404 / verkeerde site. In solo is _base = ''.
+// 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">
 
-  <%# Navigatie over ALLE posts — BOVEN de post (gedeeld partial). %>
+  <%# Navigation across ALL posts — ABOVE the post (shared partial). %>
   <%- include('../partials/post-nav', { newerPost: newerPost, olderPost: olderPost }) %>
 
@@ -79,5 +79,5 @@
 
     <% if (typeof currentPath === 'string' && currentPath && currentPath.indexOf('/' + post.slug) === 0) { %>
-      <% /* If the URL has ?pending=1, the previous submit landed in the queue */ %>
+      <% /* If the URL has ?pending=1, the previous submit landed in the moderation queue */ %>
     <% } %>
     <script>
@@ -164,6 +164,6 @@
                         </div>
 
-                        <%# Reageren op een antwoord — landt (via resolvedParent op de
-                            server) in dezelfde thread, dus 1 niveau diep blijft behouden. %>
+                        <%# Replying to a reply — lands (via resolvedParent on the
+                            server) in the same thread, so the 1-level depth is preserved. %>
                         <% if (user && canMutate) { %>
                           <form method="post" action="<%= _base %>/comments" class="comment-reply-form" hidden data-reply-form-for="<%= r.id %>">
@@ -431,9 +431,9 @@
 .comment-form button { align-self: flex-start; }
 .comment-reply-form-actions { display: flex; gap: 0.5rem; }
-/* Reply-formulier pas tonen NA klik op REPLY. De display:flex hierboven overschreef
-   anders het [hidden]-attribuut → formulier stond altijd open. */
+/* 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; }
-/* DELETE-knop zit in een <form>; display:contents laat 'm als directe flex-sibling
-   van REPLY meedoen → pixel-perfecte uitlijning + gelijke gap. */
+/* 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; }
 
Index: src/views/partials/bottom-tab.ejs
===================================================================
--- src/views/partials/bottom-tab.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/partials/bottom-tab.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -15,6 +15,6 @@
 const _ownProfile  = user ? ('/users/' + user.username) : null;
 const _isHub       = (typeof tenancy !== 'undefined' && tenancy === 'hub');
-// Home gaat in hub-modus ALTIJD naar de hub-root (/), niet naar de sub-home van
-// een artiest. In solo/circle is _siteUrlBase leeg, dus dat blijft '/'.
+// Home in hub mode ALWAYS goes to the hub root (/), not to an artist's sub-home.
+// In solo/circle _siteUrlBase is empty, so it stays '/'.
 const _homeHref    = (_isHub ? '' : _siteUrlBase) + '/';
 
@@ -27,5 +27,5 @@
 
 let _active = null;
-// In hub is Home alleen actief op de échte hub-root, niet op een artiest-sub-home.
+// In hub, Home is only active on the real hub root, not on an artist sub-home.
 if (_isHub ? (_path === '/') : (_p === '/' || _p === '')) _active = 'home';
 else if (_p.indexOf('/search') === 0 || _p.indexOf('/tag/') === 0 || _p.indexOf('/type/') === 0) _active = 'search';
@@ -38,7 +38,7 @@
 
   <!-- Home -->
-  <%# htmx-navigatie (geen full reload): de persistente audioplayer in document.body
-      overleeft dan, dus de muziek verspringt niet bij het navigeren. Op mobiel is de
-      top-balk toch verborgen, dus een context-reset via full-load is hier niet nodig. %>
+  <%# htmx navigation (no full reload): the persistent audio player in document.body
+      survives, so music does not skip when navigating. On mobile the top bar is hidden
+      anyway, so a context reset via full-load is not needed here. %>
   <a class="bottom-tab-item<%= _active === 'home' ? ' is-active' : '' %>"
      href="<%= _homeHref %>"
@@ -308,8 +308,8 @@
 
 <script>
-// Bottom-tab zoekknop → opent #search-overlay. Via event-delegatie + de overlay
-// ELKE klik opnieuw opzoeken: de overlay zit in de chrome die bij htmx-navigatie
-// OOB vervangen wordt, dus een één-keer-opgevangen referentie verschaalt
-// (knop deed daarna niets). Guard tegen dubbel-wiren.
+// Bottom-tab search button → opens #search-overlay. Using event delegation + looking
+// up the overlay on EVERY click: the overlay lives in the chrome that is OOB-replaced
+// during htmx navigation, so a once-captured reference goes stale
+// (button did nothing afterwards). Guard against double-wiring.
 (function() {
   if (window.__btSearchWired) return;
Index: src/views/partials/footer.ejs
===================================================================
--- src/views/partials/footer.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/partials/footer.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -13,6 +13,6 @@
       <span>© <%= new Date().getFullYear() %> <%= (site && site.title) || 'Klonkt' %></span>
     </div>
-    <%# Install-app knop — in de footer-rij (midden), bespaart een hele extra rij.
-        JS detecteert platform & opent een modal met PWA-install-stappen. %>
+    <%# Install-app button — placed in the footer row (centre), saves a whole extra row.
+        JS detects the platform & opens a modal with PWA install steps. %>
     <button type="button" class="install-app-btn install-app-btn--footer" data-pcms-install-app>
       <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M5 20h14v-2H5v2zm7-18L5.33 9h3.84v6h5.66V9h3.84L12 2z" fill="currentColor"/></svg>
@@ -114,7 +114,7 @@
 }
 .footer-lang:hover { border-color: var(--accent); }
-/* Open dropdown-opties leesbaar (anders wit-op-wit op dark mode). */
+/* Make open dropdown options readable (otherwise white-on-white in dark mode). */
 .footer-lang option { background: var(--paper); color: var(--ink); }
-/* Op desktop staat de taalkeuze in de header-nav → footer-versie verbergen. */
+/* On desktop the language picker lives in the header nav → hide the footer version. */
 @media (min-width: 768px) { .footer-lang { display: none; } }
 .footer-version { color: inherit; text-decoration: none; }
@@ -144,9 +144,9 @@
 }
 
-/* Compacte install-knop ín de footer-rij (midden) i.p.v. een grote losse CTA
-   op een eigen regel — bespaart verticale ruimte. */
+/* Compact install button inside the footer row (centre) instead of a large standalone
+   CTA on its own line — saves vertical space. */
 .install-app-btn--footer {
   flex: 0 0 auto;
-  width: auto !important;          /* override de mobile .install-app-btn width:100% */
+  width: auto !important;          /* override the mobile .install-app-btn width:100% */
   padding: 0.42rem 0.95rem;
   font-size: 0.82rem;
@@ -154,5 +154,5 @@
 }
 .install-app-btn--footer svg { width: 15px; height: 15px; }
-/* Op smal scherm: laat de drie items netjes centreren als ze wrappen. */
+/* On narrow screens: let the three items centre neatly when they wrap. */
 @media (max-width: 600px) {
   .site-footer-inner { justify-content: center; text-align: center; }
Index: src/views/partials/like-button.ejs
===================================================================
--- src/views/partials/like-button.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/partials/like-button.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -1,5 +1,5 @@
-<%# Like-/favorietknop voor een post. Ingelogd → htmx-toggle (POST /posts/:id/like,
-    swap outerHTML met deze partial terug). Niet ingelogd → link naar login.
-    Verwacht: post (met .id), likedByMe, likeCount, loggedIn, loginNext. %>
+<%# Like/favourite button for a post. Logged in → htmx toggle (POST /posts/:id/like,
+    swaps outerHTML back with this partial). Not logged in → link to login.
+    Expects: post (with .id), likedByMe, likeCount, loggedIn, loginNext. %>
 <%
   var _liked = (typeof likedByMe !== 'undefined' && likedByMe);
Index: src/views/partials/post-nav.ejs
===================================================================
--- src/views/partials/post-nav.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/partials/post-nav.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -1,5 +1,5 @@
-<%# Post-navigatie (Newer links / Older rechts) — gedeeld door post.ejs én
-    fan-gate.ejs. Verwacht newerPost + olderPost in scope. Ontbreekt een buur,
-    dan een uitgegrijsde placeholder zodat elke post dezelfde structuur houdt. %>
+<%# Post navigation (Newer link left / Older right) — shared by post.ejs and
+    fan-gate.ejs. Expects newerPost + olderPost in scope. If a neighbour is missing,
+    a greyed-out placeholder keeps every post in the same two-card structure. %>
 <% if ((typeof newerPost !== 'undefined' && newerPost) || (typeof olderPost !== 'undefined' && olderPost)) { %>
   <nav class="post-nav post-nav--top">
@@ -35,9 +35,9 @@
 <style>
 .post-nav { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--rule); }
-/* Boven de post: geen lijn/gap erboven, maar een scheidingslijn ERONDER (naar de content). */
+/* Above the post: no line/gap above it, but a dividing line BELOW (toward the content). */
 .post-nav--top { margin-top: 0; padding-top: 0; border-top: 0; margin-bottom: 2rem; padding-bottom: 2rem; border-bottom: 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); }
-/* Lege plek (geen nieuwere/oudere post): zelfde kaart, uitgegrijsd + niet klikbaar,
-   zodat elke post dezelfde twee-kaarten-structuur houdt. */
+/* Empty slot (no newer/older post): same card, greyed out + not clickable,
+   so every post keeps the same two-card structure. */
 .post-nav-prev.is-empty, .post-nav-next.is-empty { opacity: 0.4; cursor: default; }
 .post-nav-prev.is-empty .post-nav-title, .post-nav-next.is-empty .post-nav-title { font-weight: 400; font-style: italic; color: var(--ink-muted); }
Index: src/views/partials/shared-styles.ejs
===================================================================
--- src/views/partials/shared-styles.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/partials/shared-styles.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -6,14 +6,14 @@
   border-bottom: 1px solid var(--rule);
   background: var(--paper);
-  /* Geen extra margin-top: de pill wordt zo gecentreerd tussen de profielkop-
-     streep (boven) en de eigen border (onder). De gelijke lucht boven/onder de
-     pill komt uit de symmetrische container-padding hieronder. De margin-bottom
-     (1.5rem in style.css) blijft lucht naar de feed. */
+  /* No extra margin-top: this centres the pill between the profile-header
+     rule (above) and the own border (below). Equal space above/below the
+     pill comes from the symmetric container padding below. The margin-bottom
+     (1.5rem in style.css) keeps breathing room toward the feed. */
   margin-top: 0;
 }
 .view-switcher-wrap .container {
   display: flex; align-items: center; justify-content: center; gap: 0.75rem;
-  /* Symmetrische padding → even veel lucht boven als onder de pill (≈8px),
-     gecentreerd tussen de twee lijnen. */
+  /* Symmetric padding → equal space above and below the pill (≈8px),
+     centred between the two dividing lines. */
   padding: 0.5rem 1rem;
   max-width: 1200px; margin: 0 auto;
@@ -36,12 +36,12 @@
 }
 .view-switch-btn:hover { color: var(--ink); }
-/* Actief = wit (lichte achtergrond, donkere tekst). Via aria-selected (element-
-   attribuut, door syncAria gezet + op htmx:afterSettle) → htmx-robuust. Op niet-
-   feed-pagina's zet syncAria aria-selected=false → beide grijs. */
+/* Active = white (light background, dark text). Via aria-selected (element
+   attribute, set by syncAria + on htmx:afterSettle) → htmx-robust. On non-
+   feed pages syncAria sets aria-selected=false → both grey. */
 .view-switch-btn[aria-selected="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); }
 
-/* Agenda = een EIGEN pill (los van Tijdlijn/Grid, zodat de feed-toggle nooit
-   verstoord raakt). Zelfde look als de view-switcher-pill. Zichtbaar in alle modi. */
-.agenda-pill { display: inline-flex; align-items: center; }   /* geen box — losse icoon+tekst link */
+/* Agenda = its OWN pill (separate from Timeline/Grid, so the feed-toggle is never
+   disturbed). Same look as the view-switcher pill. Visible in all modes. */
+.agenda-pill { display: inline-flex; align-items: center; }   /* no box — standalone icon+text link */
 .vs-agenda {
   display: inline-flex; align-items: center; gap: 0.4rem;
@@ -53,11 +53,11 @@
 }
 .vs-agenda:hover { color: var(--ink); }
-/* Actief (op de agendapagina) = helder/wit. Element-class (in de OOB-gerenderde
-   chrome) i.p.v. body-class-CSS → werkt betrouwbaar ook na htmx-navigatie. */
+/* Active (on the agenda page) = bright/white. Element-class (in the OOB-rendered
+   chrome) rather than body-class CSS → stays reliable after htmx navigation. */
 .vs-agenda.is-active { color: var(--ink); font-weight: 600; }
 
-/* Mobiel: Agenda op een eigen rij bovenaan, en Solo/Cirkel + Tijdlijn/Grid samen
-   op de rij eronder (samen ~330px → past binnen een telefoon, i.p.v. de oude
-   onevenwichtige split Agenda+Solo / Tijdlijn). */
+/* Mobile: Agenda on its own row at the top, Solo/Circle + Timeline/Grid together
+   on the row below (together ~330px → fits within a phone, instead of the old
+   unbalanced split Agenda+Solo / Timeline). */
 @media (max-width: 520px) {
   .view-switcher-wrap .agenda-pill { flex: 0 0 100%; justify-content: center; }
@@ -92,10 +92,10 @@
 .on-admin .view-switcher-wrap { display: none; }
 
-/* Op een losse post is er geen feed om te schakelen, dus geen knop hoort "actief"
-   (gekleurd) te ogen. De actieve staat zit in style.css op body[data-feed-view]
-   / body[data-grid-cols] (background:var(--ink)); we overrulen die SPECIFIEKER
-   via body.on-post zodat de knoppen neutraal/grijs worden — maar wél klikbaar
-   blijven (de klik-handler navigeert dan naar de feed in die weergave). De
-   body-class wisselt client-side bij HTMX-nav, dus dit werkt ook zonder reload. */
+/* On a standalone post there is no feed to switch, so no button should look "active"
+   (coloured). The active state lives in style.css on body[data-feed-view]
+   / body[data-grid-cols] (background:var(--ink)); we override that MORE SPECIFICALLY
+   via body.on-post so the buttons become neutral/grey — but remain clickable
+   (the click-handler then navigates to the feed in that view). The
+   body-class switches client-side on HTMX-nav, so this works without a reload too. */
 body.on-post[data-feed-view="timeline"] .view-switch-btn[data-view="timeline"],
 body.on-post[data-feed-view="grid"] .view-switch-btn[data-view="grid"],
Index: src/views/partials/topnav.ejs
===================================================================
--- src/views/partials/topnav.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/partials/topnav.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -27,7 +27,7 @@
         <a href="<%= _siteUrlBase %>/" class="nav-link nav-link-admin"><%= t('nav.back_to_site') %></a>
       <% } else if (typeof tenancy !== 'undefined' && tenancy === 'hub') { %>
-        <%# Hub: de brand gaat naar de hub-home via HTMX. De chrome wordt in hub-modus
-            altijd gerenderd en via body.on-hub (CSS) verborgen op de landing, dus htmx
-            kan schakelen zonder full-reload → de audioplayer blijft leven. %>
+        <%# Hub: the brand navigates to the hub home via HTMX. The chrome is always
+            rendered in hub mode and hidden on the landing via body.on-hub (CSS), so htmx
+            can switch without a full reload → the audio player stays alive. %>
         <a class="nav-hub-home" href="/" aria-label="Home" title="Home"
            hx-get="/?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="/" hx-indicator="#pcms-loading">
@@ -207,9 +207,9 @@
 <script>
 (function() {
-  // EÉN keer bedraden. Deze chrome (topnav) wordt bij htmx-navigatie out-of-band
-  // opnieuw ingevoegd → zonder deze guard zou dit script bij elke navigatie EXTRA
-  // listeners stapelen, waardoor de thema-toggle 2× (of vaker) vuurde = geen netto
-  // wisseling ("toggle werkt niet meer"). Alles hieronder is event-delegatie op
-  // body/document, dus het werkt ook voor knoppen die ná deze run (OOB) verschijnen.
+  // Wired ONCE. This chrome (topnav) is re-inserted out-of-band during htmx navigation
+  // → without this guard the script would stack EXTRA listeners on every navigation,
+  // causing the theme toggle to fire 2× (or more) = no net change ("toggle stops working").
+  // Everything below uses event delegation on body/document, so it also works for
+  // buttons that appear after this run (OOB).
   if (window.__pcmsChromeWired) return;
   window.__pcmsChromeWired = true;
@@ -229,5 +229,5 @@
     if (e.target.closest('#search-toggle')) { openSearch(); return; }
     if (e.target.closest('#search-close')) { closeSearch(); return; }
-    // Open dropdowns (gebruikersmenu + taalkeuze) sluiten bij klik erbuiten.
+    // Close open dropdowns (user menu + language picker) on click outside.
     document.querySelectorAll('.user-menu[open], .lang-menu[open]').forEach(function(d) {
       if (!d.contains(e.target)) d.removeAttribute('open');
@@ -239,5 +239,5 @@
   });
 
-  // ── Live resultaten tijdens typen ───────────────────────────────
+  // ── Live results while typing ───────────────────────────────────
   var _ST = {
     posts: '<%= t('search.section_posts') %>', tracks: '<%= t('search.section_tracks') %>',
@@ -280,5 +280,5 @@
     }, 200);
   });
-  // Klik op een suggestie → overlay sluiten (de link/boost doet de navigatie).
+  // Click on a suggestion → close the overlay (the link/boost handles navigation).
   document.addEventListener('click', function(e) {
     if (e.target.closest && e.target.closest('#search-suggest a')) { var o = overlay(); if (o) o.hidden = true; }
@@ -313,5 +313,5 @@
 }
 
-/* Hub-home brand is WÉL zichtbaar — de thuisknop op artiest-/hub-pagina's. */
+/* Hub-home brand IS visible — the home button on artist/hub pages. */
 .masthead-brand .nav-hub-home {
   display: inline-flex; align-items: center; justify-content: center;
@@ -436,5 +436,5 @@
   min-width: 220px;
   padding: .35rem;
-  /* Frosted glass: transparant + zware blur zodat het frosted effect duidelijk is. */
+  /* Frosted glass: transparent + heavy blur so the frosted effect is clearly visible. */
   background: color-mix(in srgb, var(--paper) 60%, transparent);
   backdrop-filter: blur(34px) saturate(190%);
@@ -565,5 +565,5 @@
 .ss-all { display: block; margin-top: .35rem; padding: .55rem .6rem; border-top: 1px solid var(--rule); color: var(--accent); font-weight: 600; text-decoration: none; font-size: .9rem; }
 .ss-all:hover { text-decoration: underline; }
-/* Meldingen-bel + ongelezen-badge */
+/* Notification bell + unread badge */
 .nav-notif { position: relative; }
 .notif-badge {
Index: src/views/partials/track-editor.ejs
===================================================================
--- src/views/partials/track-editor.ejs	(revision 475eeda4d9fd076c58564f8e83535d4df89f46fd)
+++ src/views/partials/track-editor.ejs	(revision 834bcc3c41c52321679550863b74ca035538659f)
@@ -195,5 +195,5 @@
 }
 
-/* ── Credit-veld met ©-invoegknop ──────────────────────────── */
+/* ── Credit field with © insert button ──────────────────────────── */
 .te-credit-row { display: flex; gap: 0.4rem; align-items: stretch; }
 .te-credit-row input { flex: 1; min-width: 0; }
@@ -638,5 +638,5 @@
     }
 
-    // ── ©-teken invoegen in het credit-veld ─────────────────
+    // ── Insert © symbol into the credit field ─────────────────
     const copyrBtn = $('#te-credit-copyr');
     if (copyrBtn) {
@@ -693,14 +693,14 @@
     });
 
-    // ── Auto-duur ───────────────────────────────────────────
-    // De server bepaalt de duur al automatisch bij upload. Dit is de vangnet/
-    // UX-laag: opent een admin een bestaande track zónder duur, dan lezen we 'm
-    // hier uit de audio-metadata en vullen het veld — zodat je nooit seconden
-    // hoeft te typen. Bestaande waarde wordt nooit overschreven. We halen de
-    // bytes via dezelfde header-gate als de speler (X-Audio-Player).
+    // ── Auto-duration ───────────────────────────────────────────
+    // The server already determines duration automatically on upload. This is the
+    // fallback/UX layer: if an admin opens an existing track without a duration,
+    // we read it from the audio metadata and fill the field — so you never need
+    // to type seconds manually. An existing value is never overwritten. We fetch
+    // the bytes via the same header gate as the player (X-Audio-Player).
     (async function autoDuration() {
       const durEl = $('#te-duration');
       if (!durEl || !track.stream_url) return;
-      if (durEl.value && Number(durEl.value) > 0) return;  // al ingevuld → met rust laten
+      if (durEl.value && Number(durEl.value) > 0) return;  // already filled → leave it alone
       let objUrl = null;
       try {
