Index: src/routes/admin-settings.js
===================================================================
--- src/routes/admin-settings.js	(revision 4c5169ff7471245757c6df394d609751ad9bac3a)
+++ src/routes/admin-settings.js	(revision 63edb0d519500782be884d350a3d2a41569553c2)
@@ -2,8 +2,10 @@
  * Admin: globale instellingen.
  *  - tenancy-modus (Solo/Hub)
- *  - hub-branding (naam/tagline/intro van de generieke hub-hoofdpagina)
+ *  - hub-branding (naam/tagline/intro/hero van de generieke hub-hoofdpagina)
  *
  * GET  /admin/settings   -> toon huidige instellingen
- * POST /admin/settings   -> sla op (god-only)
+ * POST /admin/settings   -> sla op (god-only). Accepteert nu ook een geuploade
+ *                           hero-afbeelding (multipart); een upload wint van het
+ *                           URL-tekstveld. Zonder upload blijft het URL-veld leidend.
  *
  * De hub-pagina is generiek (van geen enkele user); deze branding leeft in
@@ -12,4 +14,9 @@
 
 import express from 'express';
+import path from 'path';
+import fs from 'fs';
+import { fileURLToPath } from 'url';
+import multer from 'multer';
+import { v4 as uuid } from 'uuid';
 import { renderPage } from '../middleware/render.js';
 import { requireGod } from '../middleware/auth.js';
@@ -17,4 +24,43 @@
 
 const router = express.Router();
+
+// Hero dark-overlay: percentage 0-100 (0 = geen overlay, 100 = volledig zwart).
+// Default 45 = de oude hardgecodeerde waarde, zodat bestaande hubs niet wijzigen.
+function clampOverlay(raw) {
+  const v = parseInt(raw, 10);
+  return Number.isFinite(v) ? Math.max(0, Math.min(100, v)) : 45;
+}
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+// Hero-uploads landen in storage/media/hero → bereikbaar als /media/hero/<file>
+// (de /media static handler serveert storage/media). Zelfde model als avatars.
+const HERO_DIR = path.resolve(
+  process.env.HERO_PATH || path.join(__dirname, '..', '..', 'storage', 'media', 'hero')
+);
+fs.mkdirSync(HERO_DIR, { recursive: true });
+
+// Alleen raster-formaten voor de upload. SVG mag bewust NIET via upload (raw
+// SVG kan script bevatten → opgeslagen-XSS bij direct openen); een SVG-hero kan
+// nog steeds via het URL-veld (zoals de meegeleverde demo-placeholder).
+const ALLOWED_HERO_EXT = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']);
+const MAX_HERO_BYTES = 5 * 1024 * 1024;
+
+const heroUpload = multer({
+  storage: multer.diskStorage({
+    destination: (req, file, cb) => cb(null, HERO_DIR),
+    filename: (req, file, cb) => {
+      const ext = path.extname(file.originalname).toLowerCase();
+      cb(null, `${uuid()}${ext}`);
+    },
+  }),
+  limits: { fileSize: MAX_HERO_BYTES },
+  fileFilter: (req, file, cb) => {
+    const ext = path.extname(file.originalname).toLowerCase();
+    if (!ALLOWED_HERO_EXT.has(ext)) {
+      return cb(new Error('Hero-afbeelding moet jpg/png/webp/gif zijn'));
+    }
+    cb(null, true);
+  },
+});
 
 router.get('/', requireGod, (req, res) => {
@@ -27,25 +73,50 @@
     hubIntro: getSetting('hub_intro') || '',
     hubHeroImage: getSetting('hub_hero_image') || '',
+    hubHeroOverlay: clampOverlay(getSetting('hub_hero_overlay')),
     success: req.query.success || null,
+    error: req.query.error || null,
   });
 });
 
 router.post('/', requireGod, (req, res) => {
-  if (typeof req.body.tenancy !== 'undefined') {
-    setTenancy(req.body.tenancy === 'hub' ? 'hub' : 'solo');
-  }
-  if (typeof req.body.hub_title !== 'undefined') {
-    setSetting('hub_title', (req.body.hub_title || '').toString().slice(0, 80).trim());
-  }
-  if (typeof req.body.hub_tagline !== 'undefined') {
-    setSetting('hub_tagline', (req.body.hub_tagline || '').toString().slice(0, 120).trim());
-  }
-  if (typeof req.body.hub_intro !== 'undefined') {
-    setSetting('hub_intro', (req.body.hub_intro || '').toString().slice(0, 400).trim());
-  }
-  if (typeof req.body.hub_hero_image !== 'undefined') {
-    setSetting('hub_hero_image', (req.body.hub_hero_image || '').toString().slice(0, 300).trim());
-  }
-  res.redirect('/admin/settings?success=' + encodeURIComponent('Opgeslagen'));
+  // multer.single verwerkt multipart (hub-branding form). Bij een gewone
+  // urlencoded POST (tenancy-form) doet multer niets en blijft req.body intact.
+  heroUpload.single('hub_hero_file')(req, res, (err) => {
+    if (err) {
+      return res.redirect('/admin/settings?error=' + encodeURIComponent(err.message));
+    }
+
+    if (typeof req.body.tenancy !== 'undefined') {
+      setTenancy(req.body.tenancy === 'hub' ? 'hub' : 'solo');
+    }
+    if (typeof req.body.hub_title !== 'undefined') {
+      setSetting('hub_title', (req.body.hub_title || '').toString().slice(0, 80).trim());
+    }
+    if (typeof req.body.hub_tagline !== 'undefined') {
+      setSetting('hub_tagline', (req.body.hub_tagline || '').toString().slice(0, 120).trim());
+    }
+    if (typeof req.body.hub_intro !== 'undefined') {
+      setSetting('hub_intro', (req.body.hub_intro || '').toString().slice(0, 400).trim());
+    }
+
+    // Hero: een geüploade afbeelding wint; anders het URL-tekstveld.
+    if (req.file) {
+      const newUrl = `/media/hero/${req.file.filename}`;
+      // Ruim een vorige geüploade hero op (alleen als die uit onze hero-map kwam).
+      const old = getSetting('hub_hero_image') || '';
+      if (old.startsWith('/media/hero/')) {
+        try { fs.unlinkSync(path.join(HERO_DIR, path.basename(old))); } catch {}
+      }
+      setSetting('hub_hero_image', newUrl);
+    } else if (typeof req.body.hub_hero_image !== 'undefined') {
+      setSetting('hub_hero_image', (req.body.hub_hero_image || '').toString().slice(0, 300).trim());
+    }
+
+    if (typeof req.body.hub_hero_overlay !== 'undefined') {
+      setSetting('hub_hero_overlay', String(clampOverlay(req.body.hub_hero_overlay)));
+    }
+
+    res.redirect('/admin/settings?success=' + encodeURIComponent('Opgeslagen'));
+  });
 });
 
