Index: src/views/pages/messages.ejs
===================================================================
--- src/views/pages/messages.ejs	(revision 3e7dde2b4f2346227507fb54dc8dfc774722b25f)
+++ src/views/pages/messages.ejs	(revision 2c0c33844b942102315e27fbf3ae0155f374b9a6)
@@ -134,4 +134,8 @@
   .msg-thread.is-collapsed .msg-thread-head { margin-bottom: 0; }
   @media (prefers-reduced-motion: reduce) { .msg-thread-chevron { transition: none; } }
+  /* De zwaai in de kop: klein, en met dezelfde rust als het aantal ernaast.
+     Hij staat er ook als het gesprek open is -- dan zegt de bubbel het al, maar
+     een kop die van stand verandert leest als iets dat gebeurde. */
+  .msg-thread-wave { flex: 0 0 auto; font-size: .95em; line-height: 1; }
   .msg-thread-count { font-variant-numeric: tabular-nums; opacity: .75; flex: 0 0 auto;
     padding-inline-start: .3rem; border-inline-start: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent); }
Index: src/views/partials/msg-item.ejs
===================================================================
--- src/views/partials/msg-item.ejs	(revision 3e7dde2b4f2346227507fb54dc8dfc774722b25f)
+++ src/views/partials/msg-item.ejs	(revision 2c0c33844b942102315e27fbf3ae0155f374b9a6)
@@ -11,4 +11,11 @@
            var _tto = ((n.messages || []).find(function (m) { return m.to_handle; }) || {}).to_handle;
            var _tlabel = _tpeople.length ? _tpeople.join(', ') : (_tto || t('msg.you'));
+           // Zwaaide er iemand in dit gesprek? Dan hoort dat in de KOP
+           // (Robins aanwijzing, 25-8). Ingeklapt zijn de bubbels weg, en juist
+           // een zwaai is het bericht dat om een antwoord vraagt zonder iets te
+           // zeggen -- die mag niet achter een dichtgeklapt gesprek verdwijnen.
+           // Alleen een ONTVANGEN zwaai: je eigen zwaai staat er als 'sent' en
+           // is geen uitnodiging aan jezelf.
+           var _twave = (n.messages || []).some(function (m) { return m.type === 'mention' && m.wave; });
       %>
         <li class="msg-item msg-thread<%= _tnew ? ' is-new' : '' %>" data-kind="conv" data-who="<%= (_tpeople.join(' ') + ' ' + (_tto || '')).toLowerCase() %>">
@@ -30,4 +37,7 @@
               <svg class="msg-thread-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
             </button>
+            <%# Blijft staan als het gesprek dicht is, net als het aantal in de
+                uitsparing hierboven -- daar is hij juist het meest waard. %>
+            <% if (_twave) { %><span class="msg-thread-wave" role="img" aria-label="<%= t('msg.waved_at_you') %>" title="<%= t('msg.waved_at_you') %>">&#128075;</span><% } %>
             <% if (_tnew) { %><span class="msg-new" title="<%= t('msg.new') %>"></span><% } %>
             <%# De context bij de draad: waar gaat dit gesprek over? Zonder deze
@@ -40,6 +50,21 @@
             <% } %>
           </div>
+          <%# NIEUWSTE BOVENAAN binnen een gesprek (Robins besluit, 25-8). De
+              LIJST van gesprekken en de meldingen blijven staan zoals ze
+              stonden; alleen binnen een draad leest het van nieuw naar oud, dus
+              wat er net bij kwam staat er meteen en niet onderaan een lange
+              draad.
+
+              Op een KOPIE, want groupConversations sorteert `messages`
+              oplopend en de rest van deze pagina rekent daarop -- `_tnew` en
+              `_tto` hierboven lezen dezelfde array. Een reverse() zonder
+              slice() draait het origineel om en dat werkt door naar alles wat
+              er daarna nog naar kijkt.
+
+              In de MARKUP en niet met column-reverse: die draait alleen het
+              beeld om, terwijl een schermlezer en het toetsenbord de oude
+              volgorde houden. %>
           <ul class="msg-thread-msgs">
-            <% (n.messages || []).forEach(function (m) { %><%- include('msg-item', { n: m, seen: _tseen, inThread: true }) %><% }); %>
+            <% (n.messages || []).slice().reverse().forEach(function (m) { %><%- include('msg-item', { n: m, seen: _tseen, inThread: true }) %><% }); %>
           </ul>
           <%# Antwoorden vanuit de draad zelf. Ingeklapt, want een open editor
Index: test/messages-thread-order.test.js
===================================================================
--- test/messages-thread-order.test.js	(revision 2c0c33844b942102315e27fbf3ae0155f374b9a6)
+++ test/messages-thread-order.test.js	(revision 2c0c33844b942102315e27fbf3ae0155f374b9a6)
@@ -0,0 +1,70 @@
+// Binnen een gesprek leest Berichten van NIEUW naar OUD (Robins besluit,
+// 25-8), en een ontvangen zwaai staat in de KOP van de draad -- want ingeklapt
+// zijn de bubbels weg, en juist een zwaai vraagt om een antwoord.
+//
+// Dit rendert de echte partial. EJS-fouten bestaan alleen tijdens het renderen:
+// een verkeerde variabelenaam of een scheve `<% %>` valt hier om en nergens
+// anders in de suite.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import ejs from 'ejs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const partial = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'views', 'partials', 'msg-item.ejs');
+
+// De helpers die de pagina normaal meegeeft. Ze geven hun invoer terug, zodat
+// wat er in het antwoord staat uit de PARTIAL komt en niet uit een stub.
+const helpers = { t: (k) => k, avatar: (u) => u, emojiName: (n) => n, formatDateTime: (d) => String(d), canMutate: false, seen: 0 };
+
+// De naam is het herkenningspunt: de inhoud loopt via note-body, dat eigen
+// velden leest. De kop draagt alleen `people`, dus deze drie namen kunnen
+// alleen uit de bubbels zelf komen.
+function draad(extra = {}) {
+  return {
+    type: 'thread', key: 'k1', post: null, count: 3, people: [{ name: 'Bart' }],
+    created_at: '2026-08-25T10:00:00Z',
+    messages: [
+      { type: 'mention', name: 'OUDSTE', handle: '@a', created_at: '2026-08-25T09:00:00Z' },
+      { type: 'mention', name: 'MIDDELSTE', handle: '@b', created_at: '2026-08-25T09:30:00Z' },
+      { type: 'mention', name: 'NIEUWSTE', handle: '@c', wave: true, created_at: '2026-08-25T10:00:00Z' },
+    ],
+    ...extra,
+  };
+}
+
+const render = (n) => ejs.renderFile(partial, { ...helpers, n });
+
+test('binnen een gesprek staat het nieuwste bovenaan', async () => {
+  const html = await render(draad());
+  const nieuw = html.indexOf('NIEUWSTE'), midden = html.indexOf('MIDDELSTE'), oud = html.indexOf('OUDSTE');
+  assert.ok(nieuw > -1 && midden > -1 && oud > -1, 'alle drie de berichten staan er');
+  assert.ok(nieuw < midden && midden < oud, `nieuwste bovenaan, kreeg N=${nieuw} M=${midden} O=${oud}`);
+});
+
+test('de draad-array zelf blijft oplopend', async () => {
+  // groupConversations sorteert oplopend en de rest van de pagina rekent
+  // daarop: `_tnew` en `_tto` lezen dezelfde array. Draaide de partial het
+  // origineel om, dan werkt dat door naar alles wat er daarna naar kijkt.
+  const d = draad();
+  await render(d);
+  assert.deepEqual(d.messages.map((m) => m.name), ['OUDSTE', 'MIDDELSTE', 'NIEUWSTE']);
+});
+
+test('een ontvangen zwaai staat in de kop, voor de bubbels', async () => {
+  const html = await render(draad());
+  const kop = html.indexOf('msg-thread-wave');
+  assert.ok(kop > -1, 'de kop draagt een zwaai-teken');
+  assert.ok(kop < html.indexOf('msg-thread-msgs'), 'en staat in de kop, niet tussen de bubbels');
+});
+
+test('geen zwaai, geen teken', async () => {
+  const stil = draad();
+  stil.messages = stil.messages.map((m) => ({ ...m, wave: false }));
+  assert.equal((await render(stil)).includes('msg-thread-wave'), false);
+});
+
+test('een zwaai die JIJ stuurde is geen uitnodiging aan jezelf', async () => {
+  const eigen = draad({ messages: [{ type: 'sent', to_handle: '@bart@pruts.nl', wave: true, created_at: '2026-08-25T10:00:00Z' }] });
+  assert.equal((await render(eigen)).includes('msg-thread-wave'), false);
+});
