Index: src/views/partials/msg-item.ejs
===================================================================
--- src/views/partials/msg-item.ejs	(revision b258a79c2fe38a0e36e15b6b37200f98dc0c951c)
+++ src/views/partials/msg-item.ejs	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
@@ -55,4 +55,5 @@
               <% } else if (_t === 'boost') { %><%= t('notif.boosted') %>
               <% } else if (_t === 'report') { %><%= t('notif.reported') %>
+              <% } else if (_t === 'mention' && n.help_request) { %>🛟 <%= t('msg.help_request') %>
               <% } else if (_t === 'mention' && n.wave) { %>👋 <%= t('msg.waved_at_you') %>
               <% } else if (_t === 'mention') { %><%= t('notif.mentioned') %>
@@ -67,5 +68,5 @@
 
             <% if (_t === 'report' && n.content) { %><div class="msg-content"><%= n.content %></div>
-            <% } else if ((_t === 'reply' || _t === 'mention' || _t === 'sent') && n.content) { %><div class="msg-content"><%- emojiHtml(n.content, n.emoji_json) %></div>
+            <% } else if ((_t === 'reply' || _t === 'mention' || _t === 'sent') && n.content) { %><div class="msg-content msg-note"><%- include('../partials/note-body', { nb: n }) %></div>
               <% if (_t === 'mention' && n.wave && n.actorUri && canMutate) { %>
                 <form class="msg-quickreply" method="post" action="<%= (typeof siteUrlBase !== 'undefined' ? siteUrlBase : '') %>/messages/quick-reply" style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px">
Index: src/views/partials/note-body.ejs
===================================================================
--- src/views/partials/note-body.ejs	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
+++ src/views/partials/note-body.ejs	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
@@ -0,0 +1,47 @@
+<%
+  /* The body of a post, rendered the same way wherever a post shows up: de
+     Krant, Berichten and the Guardian PWA. Takes `nb` with the column names an
+     ap_timeline row uses (content, emoji_json, media_json, quote_json,
+     embed_json, nsfw, cw); ap_mentions and ap_interactions carry the same names
+     so a mention or a reply can be handed straight to it.
+
+     Krant-only trimmings (the boost byline, polls, the audio player iframe, the
+     action bar) stay in tl-item: they are about the feed, not about the post. */
+  var _m = []; try { _m = JSON.parse(nb.media_json || '[]'); } catch (e) { _m = []; }
+  var _imgs = _m.filter(function (m) { return m && m.url && (!m.type || /^image\//.test(m.type)); });
+  var _vids = _m.filter(function (m) { return m && m.url && m.type && /^video\//.test(m.type); });
+  var _auds = _m.filter(function (m) { return m && m.url && m.type && /^audio\//.test(m.type); });
+  var _hasVisual = _imgs.length || _vids.length || _auds.length;
+  var _nsfwVisual = !!nb.nsfw && _hasVisual;
+  var _nsfwText = !!nb.nsfw && !_hasVisual;
+  // One card for both: a fediverse quote and an external link preview look
+  // identical; only where they came from differs. An embed carries a title and
+  // a thumbnail, never an iframe.
+  var _quote = noteQuote(nb.quote_json);
+  if (!_quote) {
+    var _emb = noteQuote(nb.embed_json);
+    // The title comes from a third-party oEmbed provider, so it is escaped
+    // here: the quote card renders `content` as HTML (fine for AP content,
+    // which we sanitise on the way in, but not for this).
+    if (_emb) _quote = { url: _emb.url, author: _emb.author || (_emb.provider ? { name: _emb.provider } : null),
+                         content: _emb.title ? ('<p>' + emojiName(_emb.title, null) + '</p>') : '', media: _emb.media || [] };
+  }
+  var _noAudio = !!nb.suppressAudio;   // the Klonkt player iframe already covers these tracks
+%>
+<% if (_nsfwText) { %>
+  <div class="tl-content nsfw-media"><span class="nsfw-veil"><%- include('../partials/nsfw-veil', { cw: nb.cw }) %></span><%- emojiHtml(nb.content, nb.emoji_json) %></div>
+<% } else { %>
+  <div class="tl-content"><%- emojiHtml(nb.content, nb.emoji_json) %></div>
+<% } %>
+<% if (_quote) { %><%- include('../partials/quote-card', { q: _quote }) %><% } %>
+<% if (_nsfwVisual) { %><div class="nsfw-media"><% } %>
+<% if (_imgs.length) { %>
+  <div class="tl-media">
+    <% _imgs.forEach(function (m) { %>
+      <a class="tl-media-img" href="<%= m.url %>" target="_blank" rel="noopener"><img src="<%= thumb(m.url, 1280) %>" alt="" loading="lazy" decoding="async"></a>
+    <% }); %>
+  </div>
+<% } %>
+<% _vids.forEach(function (m) { %><video class="tl-media-video" src="<%= m.url %>" poster="<%= thumb(m.url, 1280) %>" controls preload="metadata" playsinline></video><% }); %>
+<% if (!_noAudio) { _auds.forEach(function (m) { %><audio class="tl-media-audio" src="<%= m.url %>" controls preload="none"></audio><% }); } %>
+<% if (_nsfwVisual) { %><div class="nsfw-veil"><%- include('../partials/nsfw-veil', { cw: nb.cw }) %></div></div><% } %>
Index: src/views/partials/shared-styles.ejs
===================================================================
--- src/views/partials/shared-styles.ejs	(revision b258a79c2fe38a0e36e15b6b37200f98dc0c951c)
+++ src/views/partials/shared-styles.ejs	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
@@ -202,3 +202,50 @@
 .tl-author img.emoji, .tl-boost-by img.emoji, .tl-quote-name img.emoji, .msg-who img.emoji, .comment-author img.emoji {
   max-width: none; height: 1.3em; border-radius: 0; background: none; box-shadow: none; vertical-align: -0.2em; }
+
+/* ── The body of a post ────────────────────────────────────────────────────
+   Shared with partials/note-body.ejs, so a post looks the same in de Krant, in
+   Berichten and in the Guardian PWA. These used to live in the Krant's own
+   <style> block, which is why a post outside de Krant came out unstyled.
+   Krant-only behaviour (the clamp + "read more", the poll, the action bar)
+   stays in pages/news.ejs. */
+.tl-content { line-height: 1.55; overflow-wrap: anywhere; }
+.tl-content p { margin: .4rem 0; } .tl-content p:first-child { margin-top: 0; } .tl-content p:last-child { margin-bottom: 0; }
+.tl-content a { color: var(--accent, #06c); }
+.tl-content img { max-width: 100%; height: auto; border-radius: 10px; background: #fff; }
+
+/* FEP-044f embedded quote card (mirror of the Shaer QuoteCard). Also carries
+   an external link preview: same card, different origin. */
+.tl-quote { margin: .75rem 0 0; padding: .6rem .7rem; border-radius: 12px;
+  border: 1px solid color-mix(in srgb, var(--ink, #000) 12%, transparent);
+  background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent); }
+.tl-quote-head { display: flex; align-items: center; gap: .4rem; margin: 0 0 .35rem; min-width: 0; }
+.tl-quote-avatar { flex: 0 0 auto; width: 22px; height: 22px; border-radius: 50%; object-fit: cover; background: #fff; }
+.tl-quote-name { font-weight: 700; font-size: .85rem; color: var(--ink, inherit); white-space: nowrap; }
+.tl-quote-handle { color: var(--ink-soft, #888); font-size: .78rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
+.tl-quote-body { line-height: 1.5; font-size: .92rem; color: var(--ink-soft, #555);
+  overflow-wrap: anywhere; max-height: 16em; overflow: hidden; }
+.tl-quote-body p { margin: .3rem 0; } .tl-quote-body p:first-child { margin-top: 0; } .tl-quote-body p:last-child { margin-bottom: 0; }
+.tl-quote-body a { color: var(--accent, #06c); }
+.tl-quote-media { display: block; margin: .45rem 0 0; }
+.tl-quote-media img { max-width: 100%; height: auto; border-radius: 8px; }
+
+/* Always show the FULL image at its natural ratio — never cropped, no letterbox. */
+.tl-media { display: flex; flex-direction: column; gap: .4rem; margin: .75rem 0 0; }
+.tl-media-img { display: block; border-radius: 12px; overflow: hidden;
+  background-image: linear-gradient(135deg,
+    color-mix(in srgb, var(--accent, #888) 22%, var(--paper-2, var(--paper))) 0%,
+    color-mix(in srgb, var(--accent, #888) 6%, var(--paper-2, var(--paper))) 100%); }
+.tl-media-img img { width: 100%; height: auto; display: block; background: #fff; transition: opacity .4s ease; }
+.tl-media-img img.is-loading { opacity: 0; }
+/* Reserve placeholder space while the (natural-aspect) feed image loads. */
+.tl-media-img:has(img.is-loading) { min-height: 220px; }
+.tl-media-video, .tl-media-audio { width: 100%; margin: .75rem 0 0; border-radius: 12px; display: block; }
+.tl-media-video { max-height: 480px; background: #000; }
+
+/* In Berichten a post sits inside a notification row, so it starts tighter and
+   its media stays modest: the row is a pointer to the post, not the post page. */
+.msg-note .tl-content { line-height: 1.5; }
+.msg-note .tl-quote, .msg-note .tl-media, .msg-note .tl-media-video, .msg-note .tl-media-audio { margin-top: .5rem; }
+.msg-note .tl-media-img:has(img.is-loading) { min-height: 120px; }
+.msg-note .tl-media-video { max-height: 320px; }
 </style>
Index: src/views/partials/tl-item.ejs
===================================================================
--- src/views/partials/tl-item.ejs	(revision b258a79c2fe38a0e36e15b6b37200f98dc0c951c)
+++ src/views/partials/tl-item.ejs	(revision d9ad6c564eccd0e4c23c3d8c2c8a32334f2443bc)
@@ -10,46 +10,10 @@
           </div>
 
-          <%
-            var media = []; try { media = JSON.parse(p.media_json || '[]'); } catch (e) { media = []; }
-            var imgs = media.filter(function(m){ return m && m.url && (!m.type || /^image\//.test(m.type)); });
-            var vids = media.filter(function(m){ return m && m.url && m.type && /^video\//.test(m.type); });
-            var auds = media.filter(function(m){ return m && m.url && m.type && /^audio\//.test(m.type); });
-            var _nsfwVisual = !!p.nsfw && (imgs.length || vids.length || auds.length);
-            var _nsfwText = !!p.nsfw && !(imgs.length || vids.length || auds.length);
-          %>
-          <% if (_nsfwText) { %>
-            <div class="tl-content nsfw-media"><span class="nsfw-veil"><%- include('../partials/nsfw-veil', { cw: p.cw }) %></span><%- emojiHtml(p.content, p.emoji_json) %></div>
-          <% } else { %>
-            <div class="tl-content"><%- emojiHtml(p.content, p.emoji_json) %></div>
-          <% } %>
-          <%
-            // One card for both: a fediverse quote and an external embed look
-            // identical; only where they came from differs. An embed carries a
-            // title and a thumbnail, never an iframe.
-            var _quote = noteQuote(p.quote_json);
-            if (!_quote) {
-              var _emb = noteQuote(p.embed_json);
-              // The title comes from a third-party oEmbed provider, so it is
-              // escaped here: the quote card renders `content` as HTML (fine for
-              // AP content, which we sanitise on the way in, but not for this).
-              if (_emb) _quote = { url: _emb.url, author: _emb.author || (_emb.provider ? { name: _emb.provider } : null),
-                                   content: _emb.title ? ('<p>' + emojiName(_emb.title, null) + '</p>') : '', media: _emb.media || [] };
-            }
-          %>
-          <% if (_quote) { %><%- include('../partials/quote-card', { q: _quote }) %><% } %>
-          <% if (_nsfwVisual) { %><div class="nsfw-media"><% } %>
-          <% if (imgs.length) { %>
-            <div class="tl-media">
-              <% imgs.forEach(function(m){ %>
-                <a class="tl-media-img" href="<%= m.url %>" target="_blank" rel="noopener"><img src="<%= thumb(m.url, 1280) %>" alt="" loading="lazy" decoding="async"></a>
-              <% }); %>
-            </div>
-          <% } %>
-          <% vids.forEach(function(m){ %><video class="tl-media-video" src="<%= m.url %>" poster="<%= thumb(m.url, 1280) %>" controls preload="metadata" playsinline></video><% }); %>
-          <% /* A Klonkt audio post carries an embed player (embedHtml/embedUrl) that already
-                covers these tracks, so don't ALSO render the raw Audio attachments as bare
-                players here. Posts without a Klonkt embed (e.g. a plain remote audio) keep them. */ %>
-          <% if (!p.embedHtml && !p.embedUrl) { auds.forEach(function(m){ %><audio class="tl-media-audio" src="<%= m.url %>" controls preload="none"></audio><% }); } %>
-          <% if (_nsfwVisual) { %><div class="nsfw-veil"><%- include('../partials/nsfw-veil', { cw: p.cw }) %></div></div><% } %>
+          <% /* Content, quote/preview card and media: shared with Berichten and the
+                Guardian PWA so a post looks the same wherever it turns up.
+                suppressAudio: a Klonkt audio post carries an embed player below
+                that already covers these tracks, so don't ALSO render the raw
+                Audio attachments as bare players. */ %>
+          <%- include('../partials/note-body', { nb: Object.assign({}, p, { suppressAudio: !!(p.embedHtml || p.embedUrl) }) }) %>
 
           <% if (p.embedHtml) { %><div class="tl-embed"><%- p.embedHtml %></div><% } %>
