Index: src/views/partials/bottom-tab.ejs
===================================================================
--- src/views/partials/bottom-tab.ejs	(revision 3f4049e019e5ade13fd4befbb7e918eb3237cf7e)
+++ src/views/partials/bottom-tab.ejs	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -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 3f4049e019e5ade13fd4befbb7e918eb3237cf7e)
+++ src/views/partials/footer.ejs	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -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 3f4049e019e5ade13fd4befbb7e918eb3237cf7e)
+++ src/views/partials/like-button.ejs	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -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 3f4049e019e5ade13fd4befbb7e918eb3237cf7e)
+++ src/views/partials/post-nav.ejs	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -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 3f4049e019e5ade13fd4befbb7e918eb3237cf7e)
+++ src/views/partials/shared-styles.ejs	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -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 3f4049e019e5ade13fd4befbb7e918eb3237cf7e)
+++ src/views/partials/topnav.ejs	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -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 3f4049e019e5ade13fd4befbb7e918eb3237cf7e)
+++ src/views/partials/track-editor.ejs	(revision d7746798ec30f65765731ccbef3852624ec58f96)
@@ -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 {
