Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 255e3d35582549cc0de8fa12acbc354d08fcbfdb)
+++ src/config/database.js	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
@@ -188,4 +188,34 @@
   // Tags van de originele post — getoond in de cirkel (comma-separated string).
   ensureColumn('remote_posts', 'tags', 'TEXT');
+
+  // Nieuwsbrief / mailinglijst (premium). Abonnees per site; double opt-in als SMTP
+  // er is (status 'pending' tot bevestigd), anders single opt-in ('confirmed').
+  // 'unsub' = uitgeschreven. token = confirm/unsubscribe-sleutel (in de e-maillinks).
+  db.exec(`
+    CREATE TABLE IF NOT EXISTS subscribers (
+      id TEXT PRIMARY KEY,
+      site_id TEXT NOT NULL,
+      email TEXT NOT NULL,
+      status TEXT NOT NULL DEFAULT 'pending',
+      source TEXT DEFAULT 'widget',
+      token TEXT NOT NULL,
+      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+      confirmed_at DATETIME,
+      UNIQUE(site_id, email)
+    );
+    CREATE INDEX IF NOT EXISTS idx_subscribers_site_status ON subscribers(site_id, status);
+  `);
+
+  // Verstuurde nieuwsbrieven (historie + aantallen).
+  db.exec(`
+    CREATE TABLE IF NOT EXISTS newsletters (
+      id TEXT PRIMARY KEY,
+      site_id TEXT NOT NULL,
+      subject TEXT NOT NULL,
+      body TEXT NOT NULL,
+      sent_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+      recipient_count INTEGER DEFAULT 0
+    );
+  `);
 }
 
Index: src/routes/admin-newsletter.js
===================================================================
--- src/routes/admin-newsletter.js	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
+++ src/routes/admin-newsletter.js	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
@@ -0,0 +1,96 @@
+/**
+ * Nieuwsbrief — beheerkant (premium feature #1).
+ *
+ *   GET  /admin/newsletter        -> opstellen + abonnee-aantallen + historie
+ *   POST /admin/newsletter/send   -> verstuur naar alle BEVESTIGDE abonnees (SMTP)
+ *
+ * Premium-gated + site-beheerder. Versturen vereist ingestelde SMTP; zonder SMTP
+ * worden aanmeldingen wél verzameld (single opt-in), alleen versturen kan dan niet.
+ */
+
+import express from 'express';
+import db from '../config/database.js';
+import { v4 as uuid } from 'uuid';
+import { renderPage } from '../middleware/render.js';
+import { requireSiteManager } from '../middleware/auth.js';
+import { premiumUnlocked } from '../services/PatreonService.js';
+import { mailerConfigured, sendMail } from '../config/mailer.js';
+import { confirmedFor, counts } from '../services/SubscriberService.js';
+
+const router = express.Router();
+
+function esc(s) {
+  return String(s || '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
+}
+function fullUrl(req, p) {
+  const base = (process.env.PUBLIC_BASE_URL || ('https://' + (req.get('host') || ''))).replace(/\/$/, '');
+  return base + (res_siteUrlBase(req)) + p;
+}
+function res_siteUrlBase(req) {
+  return req.res && req.res.locals ? (req.res.locals.siteUrlBase || '') : '';
+}
+
+function premiumGate(req, res, next) {
+  if (!premiumUnlocked()) {
+    return res.status(403).send('Nieuwsbrief is premium — koppel Patreon in Beheer → Instellingen.');
+  }
+  next();
+}
+
+function renderCompose(req, res, extra = {}) {
+  const site = res.locals.site;
+  const c = counts(site.id);
+  const history = db.prepare(
+    'SELECT subject, sent_at, recipient_count FROM newsletters WHERE site_id = ? ORDER BY sent_at DESC LIMIT 10'
+  ).all(site.id);
+  const subscribeUrl = fullUrl(req, '/nieuwsbrief');
+  renderPage(req, res, 'pages/admin-newsletter', {
+    pageTitle: 'Nieuwsbrief',
+    bodyClass: 'on-admin',
+    nlCounts: c,
+    nlHistory: history,
+    nlSubscribeUrl: subscribeUrl,
+    nlSmtp: mailerConfigured(),
+    ...extra,
+  });
+}
+
+router.get('/', requireSiteManager, premiumGate, (req, res) => {
+  if (!res.locals.site) return res.status(404).send('Geen site.');
+  renderCompose(req, res);
+});
+
+router.post('/send', requireSiteManager, premiumGate, async (req, res) => {
+  const site = res.locals.site;
+  if (!site) return res.status(404).send('Geen site.');
+  if (!mailerConfigured()) return renderCompose(req, res, { nlMsg: 'SMTP is niet ingesteld — versturen kan nog niet.', nlMsgKind: 'bad' });
+
+  const subject = (req.body.subject || '').trim();
+  const body = (req.body.body || '').trim();
+  if (!subject || !body) return renderCompose(req, res, { nlMsg: 'Onderwerp en bericht zijn verplicht.', nlMsgKind: 'bad', nlSubject: subject, nlBody: body });
+
+  const subs = confirmedFor(site.id);
+  const bodyHtml = esc(body).replace(/\n/g, '<br>');
+  let sent = 0;
+  for (const s of subs) {
+    const unsub = fullUrl(req, '/nieuwsbrief/uitschrijven/' + s.token);
+    try {
+      await sendMail({
+        to: s.email,
+        subject,
+        text: body + '\n\n—\nUitschrijven: ' + unsub,
+        html: '<div>' + bodyHtml + '</div>' +
+              '<hr style="margin-top:24px;border:none;border-top:1px solid #ddd">' +
+              '<p style="color:#888;font-size:12px">Je ontvangt dit omdat je je aanmeldde voor de nieuwsbrief van ' +
+              esc(site.title) + '. <a href="' + unsub + '">Uitschrijven</a>.</p>',
+      });
+      sent++;
+    } catch (e) { /* sla deze ontvanger over, ga door */ }
+  }
+  db.prepare('INSERT INTO newsletters (id, site_id, subject, body, recipient_count) VALUES (?,?,?,?,?)')
+    .run(uuid(), site.id, subject, body, sent);
+
+  renderCompose(req, res, { nlMsg: 'Verstuurd naar ' + sent + ' van ' + subs.length + ' abonnee(s).', nlMsgKind: 'ok' });
+});
+
+export default router;
Index: src/routes/newsletter.js
===================================================================
--- src/routes/newsletter.js	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
+++ src/routes/newsletter.js	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
@@ -0,0 +1,90 @@
+/**
+ * Nieuwsbrief — publieke kant (premium feature #1).
+ *
+ *   GET  /nieuwsbrief                      -> aanmeldformulier (premium; anders 404)
+ *   POST /nieuwsbrief                      -> aanmelden (double opt-in als SMTP er is)
+ *   GET  /nieuwsbrief/bevestigen/:token    -> opt-in bevestigen
+ *   GET  /nieuwsbrief/uitschrijven/:token  -> uitschrijven (ALTIJD toegestaan)
+ *
+ * In hub-modus loopt dit via /user/:slug/nieuwsbrief (resolveSite zet siteUrlBase).
+ * Confirm-/unsub-links in de mail zijn absoluut (PUBLIC_BASE_URL + siteUrlBase).
+ */
+
+import express from 'express';
+import { renderPage } from '../middleware/render.js';
+import { premiumUnlocked } from '../services/PatreonService.js';
+import { mailerConfigured, sendMail } from '../config/mailer.js';
+import { addSubscriber, confirm, unsubscribe } from '../services/SubscriberService.js';
+
+const router = express.Router();
+
+function esc(s) {
+  return String(s || '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
+}
+function fullUrl(req, siteUrlBase, p) {
+  const base = (process.env.PUBLIC_BASE_URL || ('https://' + (req.get('host') || ''))).replace(/\/$/, '');
+  return base + (siteUrlBase || '') + p;
+}
+function show(req, res, state, extra = {}) {
+  renderPage(req, res, 'pages/newsletter', {
+    pageTitle: 'Nieuwsbrief' + (res.locals.site ? ' — ' + res.locals.site.title : ''),
+    bodyClass: 'on-newsletter',
+    nlState: state,
+    ...extra,
+  });
+}
+
+router.get('/nieuwsbrief', (req, res, next) => {
+  if (!premiumUnlocked()) return next();
+  if (!res.locals.site) return next();
+  const fan = req.session && req.session.user;
+  const prefill = (fan && fan.email && fan.email.includes('@')) ? fan.email : '';
+  show(req, res, 'form', { nlPrefill: prefill });
+});
+
+router.post('/nieuwsbrief', async (req, res, next) => {
+  if (!premiumUnlocked()) return next();
+  const site = res.locals.site;
+  if (!site) return next();
+  const email = (req.body.email || '').trim();
+  const doubleOptin = mailerConfigured();
+  const r = addSubscriber(site.id, email, 'widget', { doubleOptin });
+  if (!r.ok) return show(req, res, r.error === 'invalid_email' ? 'invalid' : 'error', { nlPrefill: email });
+
+  if (r.status === 'pending') {
+    // Double opt-in: stuur de bevestigingsmail.
+    const link = fullUrl(req, res.locals.siteUrlBase, '/nieuwsbrief/bevestigen/' + r.token);
+    const unsub = fullUrl(req, res.locals.siteUrlBase, '/nieuwsbrief/uitschrijven/' + r.token);
+    try {
+      await sendMail({
+        to: email,
+        subject: 'Bevestig je inschrijving — ' + (site.title || 'nieuwsbrief'),
+        text: 'Bevestig je inschrijving op de nieuwsbrief van ' + (site.title || '') + ':\n' + link +
+              '\n\nNiet aangevraagd? Negeer deze mail. Uitschrijven: ' + unsub,
+        html: '<p>Bevestig je inschrijving op de nieuwsbrief van <strong>' + esc(site.title) + '</strong>:</p>' +
+              '<p><a href="' + link + '">Inschrijving bevestigen</a></p>' +
+              '<p style="color:#888;font-size:12px">Niet aangevraagd? Negeer deze mail. ' +
+              '<a href="' + unsub + '">Uitschrijven</a></p>',
+      });
+    } catch (e) {
+      return show(req, res, 'smtperror');
+    }
+    return show(req, res, 'check', { nlEmail: email });
+  }
+  return show(req, res, 'done', { nlEmail: email });
+});
+
+router.get('/nieuwsbrief/bevestigen/:token', (req, res, next) => {
+  if (!premiumUnlocked()) return next();
+  const ok = confirm(req.params.token);
+  show(req, res, ok ? 'confirmed' : 'badtoken');
+});
+
+// Uitschrijven mag altijd (ook als de premium-laag later uit zou gaan): een abonnee
+// moet zich altijd kunnen afmelden. Niet premium-gated.
+router.get('/nieuwsbrief/uitschrijven/:token', (req, res) => {
+  const ok = unsubscribe(req.params.token);
+  show(req, res, ok ? 'unsubbed' : 'badtoken');
+});
+
+export default router;
Index: src/server.js
===================================================================
--- src/server.js	(revision 255e3d35582549cc0de8fa12acbc354d08fcbfdb)
+++ src/server.js	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
@@ -53,4 +53,6 @@
 import circleRoutes from './routes/circle.js';
 import epkRoutes from './routes/epk.js';
+import newsletterRoutes from './routes/newsletter.js';
+import adminNewsletterRoutes from './routes/admin-newsletter.js';
 
 if (!process.env.SESSION_SECRET) {
@@ -252,4 +254,5 @@
 app.use('/admin/patreon', adminPatreonRoutes);
 app.use('/admin/stats', adminStatsRoutes);
+app.use('/admin/newsletter', adminNewsletterRoutes);
 app.use('/admin', adminRoutes);
 app.use('/prutter', prutterRoutes);
@@ -267,4 +270,5 @@
 app.use('/', circleRoutes); // /cirkel-feed (solo/hub: next() -> postsRoutes)
 app.use('/', epkRoutes); // /pers perskit (premium; niet-premium: next() -> 404)
+app.use('/', newsletterRoutes); // /nieuwsbrief in/uitschrijven (premium; niet-premium: next())
 app.use('/', postsRoutes);
 
Index: src/services/SubscriberService.js
===================================================================
--- src/services/SubscriberService.js	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
+++ src/services/SubscriberService.js	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
@@ -0,0 +1,79 @@
+/**
+ * SubscriberService — nieuwsbrief-abonnees per site (premium feature #1).
+ *
+ * Double opt-in als SMTP er is (status 'pending' → 'confirmed' via confirm-link),
+ * anders single opt-in ('confirmed' meteen). Elke abonnee heeft een token dat zowel
+ * de confirm- als de unsubscribe-link draagt. Hergebruikt door #2 (download-voor-
+ * email) en #8 (notify-me) als gedeelde abonnee-opslag.
+ */
+
+import crypto from 'crypto';
+import db from '../config/database.js';
+import { v4 as uuid } from 'uuid';
+
+const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+export function isValidEmail(email) {
+  return typeof email === 'string' && email.length <= 254 && EMAIL_RE.test(email.trim());
+}
+
+function newToken() {
+  return crypto.randomBytes(24).toString('hex');
+}
+
+/**
+ * Voeg een abonnee toe (of heractiveer een uitgeschreven/bestaande).
+ * @returns {{ok:boolean, status?:string, token?:string, created?:boolean, error?:string}}
+ *   status 'pending'  → er moet nog bevestigd worden (stuur confirm-mail)
+ *   status 'confirmed'→ direct actief (single opt-in)
+ */
+export function addSubscriber(siteId, email, source = 'widget', { doubleOptin = false } = {}) {
+  email = (email || '').trim().toLowerCase();
+  if (!siteId) return { ok: false, error: 'no_site' };
+  if (!isValidEmail(email)) return { ok: false, error: 'invalid_email' };
+
+  const existing = db.prepare('SELECT * FROM subscribers WHERE site_id = ? AND email = ?').get(siteId, email);
+  const status = doubleOptin ? 'pending' : 'confirmed';
+
+  if (existing) {
+    // Al actief → niets te doen (idempotent, geen dubbele mail).
+    if (existing.status === 'confirmed') return { ok: true, status: 'confirmed', token: existing.token, created: false };
+    // Pending of uitgeschreven → opnieuw uitnodigen/activeren met een verse token.
+    const token = newToken();
+    db.prepare("UPDATE subscribers SET status = ?, token = ?, source = ?, confirmed_at = CASE WHEN ? = 'confirmed' THEN CURRENT_TIMESTAMP ELSE NULL END WHERE id = ?")
+      .run(status, token, source, status, existing.id);
+    return { ok: true, status, token, created: false };
+  }
+
+  const token = newToken();
+  db.prepare(
+    "INSERT INTO subscribers (id, site_id, email, status, source, token, confirmed_at) VALUES (?,?,?,?,?,?, CASE WHEN ? = 'confirmed' THEN CURRENT_TIMESTAMP ELSE NULL END)"
+  ).run(uuid(), siteId, email, status, source, token, status);
+  return { ok: true, status, token, created: true };
+}
+
+export function confirm(token) {
+  if (!token) return false;
+  const row = db.prepare('SELECT id FROM subscribers WHERE token = ?').get(token);
+  if (!row) return false;
+  db.prepare("UPDATE subscribers SET status = 'confirmed', confirmed_at = CURRENT_TIMESTAMP WHERE id = ?").run(row.id);
+  return true;
+}
+
+export function unsubscribe(token) {
+  if (!token) return false;
+  const row = db.prepare('SELECT id FROM subscribers WHERE token = ?').get(token);
+  if (!row) return false;
+  db.prepare("UPDATE subscribers SET status = 'unsub' WHERE id = ?").run(row.id);
+  return true;
+}
+
+/** Bevestigde abonnees (email + token) voor een site — voor het versturen. */
+export function confirmedFor(siteId) {
+  return db.prepare("SELECT email, token FROM subscribers WHERE site_id = ? AND status = 'confirmed'").all(siteId);
+}
+
+export function counts(siteId) {
+  const c = (st) => db.prepare('SELECT COUNT(*) AS n FROM subscribers WHERE site_id = ? AND status = ?').get(siteId, st).n;
+  return { confirmed: c('confirmed'), pending: c('pending'), unsub: c('unsub') };
+}
Index: src/views/pages/admin-newsletter.ejs
===================================================================
--- src/views/pages/admin-newsletter.ejs	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
+++ src/views/pages/admin-newsletter.ejs	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
@@ -0,0 +1,66 @@
+<%
+  var c = (typeof nlCounts !== 'undefined') ? nlCounts : { confirmed:0, pending:0, unsub:0 };
+  var msg = (typeof nlMsg !== 'undefined') ? nlMsg : '';
+  var msgKind = (typeof nlMsgKind !== 'undefined') ? nlMsgKind : '';
+  var hist = (typeof nlHistory !== 'undefined') ? nlHistory : [];
+  var smtp = (typeof nlSmtp !== 'undefined') ? nlSmtp : false;
+%>
+<section class="anl">
+  <h1>Nieuwsbrief</h1>
+
+  <% if (msg) { %><div class="anl-msg anl-<%= msgKind %>"><%= msg %></div><% } %>
+
+  <div class="anl-stats">
+    <div class="anl-stat"><span class="anl-n"><%= c.confirmed %></span><span class="anl-l">bevestigd</span></div>
+    <div class="anl-stat"><span class="anl-n"><%= c.pending %></span><span class="anl-l">in afwachting</span></div>
+    <div class="anl-stat"><span class="anl-n"><%= c.unsub %></span><span class="anl-l">uitgeschreven</span></div>
+  </div>
+
+  <% if (!smtp) { %>
+    <div class="anl-msg anl-warn">⚠ SMTP is nog niet ingesteld. Aanmeldingen worden wél verzameld, maar versturen kan pas als je SMTP-gegevens invult (<code>SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM</code> in <code>.env</code>).</div>
+  <% } %>
+
+  <p class="anl-share">Aanmeldlink om te delen: <a href="<%= nlSubscribeUrl %>"><%= nlSubscribeUrl %></a></p>
+
+  <form method="POST" action="<%= siteUrlBase %>/admin/newsletter/send" class="anl-form">
+    <label>Onderwerp
+      <input type="text" name="subject" required maxlength="200" value="<%= (typeof nlSubject!=='undefined')?nlSubject:'' %>" placeholder="Nieuwe single uit!">
+    </label>
+    <label>Bericht
+      <textarea name="body" rows="10" required placeholder="Schrijf je update…"><%= (typeof nlBody!=='undefined')?nlBody:'' %></textarea>
+    </label>
+    <button type="submit" class="anl-send" <%= smtp ? '' : 'disabled' %> onclick="return confirm('Nieuwsbrief versturen naar <%= c.confirmed %> bevestigde abonnee(s)?')">Versturen naar <%= c.confirmed %> abonnee(s)</button>
+  </form>
+
+  <% if (hist.length) { %>
+    <h2 class="anl-h2">Verstuurd</h2>
+    <ul class="anl-hist">
+      <% hist.forEach(function(h){ %>
+        <li><span class="anl-hist-subj"><%= h.subject %></span><span class="anl-hist-meta"><%= h.recipient_count %> ontvanger(s) · <%= formatDateTime ? formatDateTime(h.sent_at) : h.sent_at %></span></li>
+      <% }); %>
+    </ul>
+  <% } %>
+</section>
+
+<style>
+  .anl { max-width: 720px; margin: 0 auto; padding: 24px 18px 64px; }
+  .anl h1 { margin: 0 0 18px; }
+  .anl-msg { padding: 11px 14px; border-radius: 10px; margin-bottom: 16px; font-size: 14px; }
+  .anl-ok { background: rgba(60,160,90,.15); }
+  .anl-bad { background: rgba(200,60,60,.15); }
+  .anl-warn { background: rgba(210,160,40,.15); }
+  .anl-stats { display: flex; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
+  .anl-stat { flex: 1 1 100px; border: 1px solid rgba(128,128,128,.2); border-radius: 12px; padding: 14px; text-align: center; }
+  .anl-n { display: block; font-size: 28px; font-weight: 700; }
+  .anl-l { font-size: 12px; opacity: .65; text-transform: uppercase; letter-spacing: .06em; }
+  .anl-share { font-size: 13px; opacity: .8; margin: 0 0 20px; }
+  .anl-form { display: flex; flex-direction: column; gap: 14px; }
+  .anl-form label { display: flex; flex-direction: column; gap: 6px; font-weight: 600; font-size: 13.5px; }
+  .anl-form input, .anl-form textarea { padding: 11px 13px; border-radius: 10px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; font: inherit; font-weight: 400; }
+  .anl-send { align-self: flex-start; padding: 11px 20px; border-radius: 10px; border: none; background: var(--accent, #6b8f71); color: #fff; font-weight: 600; cursor: pointer; }
+  .anl-send:disabled { opacity: .5; cursor: not-allowed; }
+  .anl-h2 { margin: 30px 0 12px; font-size: 13px; text-transform: uppercase; letter-spacing: .08em; opacity: .6; }
+  .anl-hist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
+  .anl-hist li { display: flex; justify-content: space-between; gap: 12px; padding: 10px 12px; border: 1px solid rgba(128,128,128,.18); border-radius: 10px; flex-wrap: wrap; }
+  .anl-hist-meta { opacity: .6; font-size: 12.5px; }
+</style>
Index: src/views/pages/admin.ejs
===================================================================
--- src/views/pages/admin.ejs	(revision 255e3d35582549cc0de8fa12acbc354d08fcbfdb)
+++ src/views/pages/admin.ejs	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
@@ -33,4 +33,6 @@
     <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
       <a href="/admin/stats" class="btn">📊 Statistieken</a>
+      <a href="/admin/newsletter" class="btn">✉️ Nieuwsbrief</a>
+      <% if (tenancy !== 'hub' && primarySite) { %><a href="/pers" class="btn" target="_blank">📰 Perskit</a><% } %>
     <% } %>
     <a href="/admin/updates" class="btn">🔄 Updates</a>
Index: src/views/pages/newsletter.ejs
===================================================================
--- src/views/pages/newsletter.ejs	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
+++ src/views/pages/newsletter.ejs	(revision 2e247e4c529b09d55619100d5bd4e50de5a95e54)
@@ -0,0 +1,54 @@
+<%
+  var _site = site || {};
+  var _title = _site.title || 'deze artiest';
+  var st = (typeof nlState !== 'undefined') ? nlState : 'form';
+%>
+<section class="nl">
+  <div class="nl-card">
+    <% if (st === 'form') { %>
+      <h1 class="nl-h1">Blijf op de hoogte</h1>
+      <p class="nl-sub">Schrijf je in voor de nieuwsbrief van <strong><%= _title %></strong> — nieuwe muziek, shows en updates, rechtstreeks in je inbox. Uitschrijven kan altijd met één klik.</p>
+      <form method="POST" action="<%= siteUrlBase %>/nieuwsbrief" class="nl-form">
+        <input type="email" name="email" required placeholder="jouw@email.nl" value="<%= (typeof nlPrefill !== 'undefined') ? nlPrefill : '' %>" autocomplete="email">
+        <button type="submit" class="nl-btn">Inschrijven</button>
+      </form>
+    <% } else if (st === 'check') { %>
+      <h1 class="nl-h1">Bijna klaar ✉</h1>
+      <p class="nl-sub">We hebben een bevestigingsmail gestuurd naar <strong><%= (typeof nlEmail!=='undefined')?nlEmail:'je adres' %></strong>. Klik op de link in die mail om je inschrijving te bevestigen.</p>
+    <% } else if (st === 'done') { %>
+      <h1 class="nl-h1">Je bent ingeschreven ✓</h1>
+      <p class="nl-sub">Bedankt — je staat op de lijst van <strong><%= _title %></strong>.</p>
+    <% } else if (st === 'confirmed') { %>
+      <h1 class="nl-h1">Inschrijving bevestigd ✓</h1>
+      <p class="nl-sub">Top! Je ontvangt voortaan de nieuwsbrief van <strong><%= _title %></strong>.</p>
+    <% } else if (st === 'unsubbed') { %>
+      <h1 class="nl-h1">Uitgeschreven</h1>
+      <p class="nl-sub">Je bent afgemeld. Je ontvangt geen nieuwsbrieven meer. Van gedachten veranderd? Je kunt je altijd opnieuw inschrijven.</p>
+    <% } else if (st === 'invalid') { %>
+      <h1 class="nl-h1">Ongeldig e-mailadres</h1>
+      <p class="nl-sub">Controleer je adres en probeer 't opnieuw.</p>
+      <p><a class="nl-btn nl-btn-ghost" href="<%= siteUrlBase %>/nieuwsbrief">← Terug</a></p>
+    <% } else if (st === 'smtperror') { %>
+      <h1 class="nl-h1">Even niet gelukt</h1>
+      <p class="nl-sub">De bevestigingsmail kon niet verstuurd worden. Probeer 't later nog eens.</p>
+    <% } else if (st === 'badtoken') { %>
+      <h1 class="nl-h1">Link ongeldig of verlopen</h1>
+      <p class="nl-sub">Deze link werkt niet meer. Schrijf je eventueel opnieuw in.</p>
+      <p><a class="nl-btn nl-btn-ghost" href="<%= siteUrlBase %>/nieuwsbrief">Naar inschrijven</a></p>
+    <% } else { %>
+      <h1 class="nl-h1">Er ging iets mis</h1>
+      <p class="nl-sub">Probeer 't later nog eens.</p>
+    <% } %>
+  </div>
+</section>
+
+<style>
+  .nl { max-width: 560px; margin: 0 auto; padding: 48px 18px; }
+  .nl-card { border: 1px solid rgba(128,128,128,.2); border-radius: 18px; padding: 32px 28px; }
+  .nl-h1 { font-size: clamp(24px, 5vw, 34px); margin: 0 0 10px; }
+  .nl-sub { font-size: 15.5px; line-height: 1.6; opacity: .85; margin: 0 0 20px; }
+  .nl-form { display: flex; gap: 10px; flex-wrap: wrap; }
+  .nl-form input { flex: 1 1 220px; padding: 12px 14px; border-radius: 10px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; font-size: 15px; }
+  .nl-btn { padding: 12px 20px; border-radius: 10px; border: none; background: var(--accent, #6b8f71); color: #fff; font-weight: 600; font-size: 15px; cursor: pointer; text-decoration: none; display: inline-block; }
+  .nl-btn-ghost { background: transparent; color: inherit; border: 1px solid rgba(128,128,128,.4); }
+</style>
