Index: src/assets/js/guardian.js
===================================================================
--- src/assets/js/guardian.js	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
+++ src/assets/js/guardian.js	(revision a7bcf669de39441a2bbc7c41545fc9e17db4b233)
@@ -32,5 +32,12 @@
     catch (e) { return uri; }
   }
-  function when(s) { return String(s || '').slice(0, 16).replace('T', ' '); }
+  // The server hands over a timestamp already formatted in the site's timezone
+  // (Beheer -> Instellingen), the same clock de Krant and Berichten show. The
+  // slice is only a fallback for a row that predates that field: it shows raw
+  // UTC, which is what made a 20:20 call for help read 18:20.
+  function when(item, raw) {
+    if (item && item.when_text) return item.when_text;
+    return String(raw || '').slice(0, 16).replace('T', ' ');
+  }
   function show(id, on) { document.getElementById(id).hidden = !on; }
 
@@ -49,5 +56,5 @@
       else who.textContent = h.actor_name || handleOf(h.actor_uri, h.actor_handle);
       row.appendChild(who);
-      row.appendChild(el('span', 'when', when(h.published || h.created_at)));
+      row.appendChild(el('span', 'when', when(h, h.published || h.created_at)));
       card.appendChild(row);
       var body = el('div', 'body g-note');
@@ -197,5 +204,5 @@
       var head = el('div', 'row');
       head.appendChild(el('span', 'who grow', p.author));
-      if (p.published) head.appendChild(el('span', 'g-when', when(p.published)));
+      if (p.published) head.appendChild(el('span', 'g-when', when(p, p.published)));
       card.appendChild(head);
       var body = el('div', 'feed-body');
Index: src/middleware/render.js
===================================================================
--- src/middleware/render.js	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
+++ src/middleware/render.js	(revision a7bcf669de39441a2bbc7c41545fc9e17db4b233)
@@ -85,12 +85,33 @@
 const siteTimezone = () => getSetting('timezone') || undefined;
 
+/**
+ * Read a stored timestamp as the moment it actually is.
+ *
+ * SQLite's CURRENT_TIMESTAMP writes UTC without saying so ("2026-07-28
+ * 18:20:33"), and new Date() reads a string in that shape as LOCAL time. That
+ * is right only as long as the server runs on UTC; set the machine to
+ * Europe/Amsterdam and every stored date silently shifts two hours. So say UTC
+ * out loud. Anything already carrying a zone (AP `published` ends in Z) is left
+ * to the normal parser.
+ */
+const parseStamp = (v) => {
+  if (!v) return null;
+  const s = String(v);
+  const d = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?$/.test(s)
+    ? new Date(`${s.replace(' ', 'T')}Z`)
+    : new Date(s);
+  return Number.isNaN(d.getTime()) ? null : d;
+};
+
 const formatDate = (iso) => {
-  if (!iso) return '';
-  return new Date(iso).toLocaleDateString('nl-NL', { timeZone: siteTimezone(), day: 'numeric', month: 'long', year: 'numeric' });
+  const d = parseStamp(iso);
+  return d ? d.toLocaleDateString('nl-NL', { timeZone: siteTimezone(), day: 'numeric', month: 'long', year: 'numeric' }) : '';
 };
 
-const formatDateTime = (iso) => {
-  if (!iso) return '';
-  return new Date(iso).toLocaleString('nl-NL', { timeZone: siteTimezone(), dateStyle: 'medium', timeStyle: 'short' });
+/** A timestamp in the site's own timezone (Beheer → Instellingen). Exported so
+ *  surfaces outside the EJS pages (the Guardian PWA) read the same clock. */
+export const formatDateTime = (iso) => {
+  const d = parseStamp(iso);
+  return d ? d.toLocaleString('nl-NL', { timeZone: siteTimezone(), dateStyle: 'medium', timeStyle: 'short' }) : '';
 };
 
Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
+++ src/routes/guardian.js	(revision a7bcf669de39441a2bbc7c41545fc9e17db4b233)
@@ -19,5 +19,5 @@
 import * as Guardianship from '../services/guardianship/index.js';
 import { t as i18nT, resolveLang } from '../services/i18n.js';
-import { injectCspNonce, renderNoteBody } from '../middleware/render.js';
+import { injectCspNonce, renderNoteBody, formatDateTime } from '../middleware/render.js';
 import { emojiName } from '../services/NoteRender.js';
 
@@ -62,4 +62,7 @@
     body_html: renderNoteBody(h, L),
     name_html: emojiName(h.actor_name || '', h.actor_emoji_json),
+    // In the site's own timezone, the same as everywhere else in Klonkt. The
+    // PWA used to slice the raw UTC string, so a 20:20 call for help read 18:20.
+    when_text: formatDateTime(h.published || h.created_at),
   }));
   return {
@@ -141,4 +144,5 @@
       url: p.url,
       published: p.published || p.created_at,
+      when_text: formatDateTime(p.published || p.created_at),
       cw: p.cw || null,
       media: p.media_json ? JSON.parse(p.media_json) : [],
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
+++ src/services/ActivityPubService.js	(revision a7bcf669de39441a2bbc7c41545fc9e17db4b233)
@@ -3521,5 +3521,5 @@
   try {
     const rows = db.prepare(`
-      SELECT i.kind, i.actor_name, i.actor_handle, i.actor_url, i.actor_icon, i.content, i.created_at, i.visibility,
+      SELECT i.kind, i.actor_name, i.actor_handle, i.actor_url, i.actor_icon, i.content, i.created_at, i.published, i.visibility,
              i.emoji_json, i.actor_emoji_json, i.media_json, i.quote_json, i.embed_json,
              p.slug AS post_slug, p.title AS post_title
@@ -3531,4 +3531,8 @@
       type: r.kind, name: r.actor_name, handle: r.actor_handle, url: r.actor_url, icon: r.actor_icon,
       content: stripLeadingMentions(r.content), post_slug: r.post_slug, post_title: r.post_title, created_at: r.created_at,
+      // When the post was written, for display. created_at (when it reached us)
+      // stays the sort key and the unread watermark: a note that federated late
+      // is still new to you.
+      published: r.published,
       emoji_json: r.emoji_json, actor_emoji_json: r.actor_emoji_json,   // FEP-9098 (messages render)
       media_json: r.media_json, quote_json: r.quote_json, embed_json: r.embed_json,   // rendered like a Krant post
@@ -3555,8 +3559,8 @@
   } catch { /* ignore */ }
   try {
-    for (const r of db.prepare(`SELECT object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, wave, help_request, created_at,
+    for (const r of db.prepare(`SELECT object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, wave, help_request, created_at, published,
                                        emoji_json, actor_emoji_json, media_json, quote_json, embed_json
                                 FROM ap_mentions WHERE slug = ? ORDER BY created_at DESC LIMIT ?`).all(slug, L)) {
-      out.push({ type: 'mention', name: r.actor_name, handle: r.actor_handle, url: r.actor_url || r.actor_uri, icon: r.actor_icon, content: stripLeadingMentions(r.content), note_url: r.note_url || r.object_uri, wave: r.wave ? 1 : 0, help_request: r.help_request ? 1 : 0, actorUri: r.actor_uri, created_at: r.created_at,
+      out.push({ type: 'mention', name: r.actor_name, handle: r.actor_handle, url: r.actor_url || r.actor_uri, icon: r.actor_icon, content: stripLeadingMentions(r.content), note_url: r.note_url || r.object_uri, wave: r.wave ? 1 : 0, help_request: r.help_request ? 1 : 0, actorUri: r.actor_uri, created_at: r.created_at, published: r.published,
         // Same trimmings a Krant row has, so Berichten renders the post identically.
         emoji_json: r.emoji_json, actor_emoji_json: r.actor_emoji_json, media_json: r.media_json, quote_json: r.quote_json, embed_json: r.embed_json });
Index: src/views/partials/msg-item.ejs
===================================================================
--- src/views/partials/msg-item.ejs	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
+++ src/views/partials/msg-item.ejs	(revision a7bcf669de39441a2bbc7c41545fc9e17db4b233)
@@ -44,5 +44,8 @@
               <% } %>
               <% if (_new) { %><span class="msg-new" title="<%= t('msg.new') %>"></span><% } %>
-              <% if (n.created_at) { %><span class="msg-time"><%= formatDateTime(n.created_at) %></span><% } %>
+              <% /* When the post was written, like de Krant and de Guardian PWA
+                    show it. created_at (when it reached us) stays the sort key
+                    and drives the "new since your last visit" dot above. */ %>
+              <% if (n.published || n.created_at) { %><span class="msg-time"><%= formatDateTime(n.published || n.created_at) %></span><% } %>
             </div>
 
