// ============================================
// Gedeelde plattegrond-renderer — dansshow-module (2D tribune-editor)
// Gebruikt door de admin (zaalplan-editor + verkoopmonitor) en de publieke
// stoelkiezer (page-shows.jsx). Eén bron van waarheid voor de geometrie zodat
// editor, publiek en monitor er identiek uitzien.
//
// Model (achterwaarts compatibel met de oude rij-stapel):
//   layout = {
//     stage?:   { x, y, w, h, label }        // podium-positie (cellen)
//     sections: [ {                          // = tribune / blok
//       id, label, x?, y?,                   // x/y = positie linksboven in cellen
//       rows: [ { id, label, seats: [ { id, label, col, rankId?, blocked?, aisle? } ] } ]
//     } ]
//     markers?: [ { id, type, x, y, w, h, text } ]   // nooduitgang/brandtrap/trap/techniek/label
//   }
// Stoelen blijven de bron voor prijs/verkoop; x/y/stage/markers zijn puur ruimtelijk.
// Beschikbaar als window.LM_SEATMAP.
// ============================================
(function () {
  const CELL_MIN = 16, CELL_MAX = 38;   // px per cel — auto-fit kiest hiertussen
  const GUTTER = 1;                     // cellen links van een blok voor rij-labels
  const HEADER = 1;                     // cellen boven een blok voor de bloknaam
  const PAD = 1;                        // padding rond de hele plattegrond (cellen)

  const STATUS_COLORS = {
    sold:    { bg: '#1f2937', border: '#1f2937', fg: '#6b7280', opacity: 0.6 },
    held:    { bg: '#9ca3af', border: '#9ca3af', fg: '#4b5563', opacity: 1 },
    blocked: { bg: '#475569', border: '#334155', fg: '#cbd5e1', opacity: 1 },
    selected:{ bg: '#0ea5a4', border: '#064e4b', fg: '#ffffff', opacity: 1 },
  };

  // Officieel veiligheidsgroen (ISO 7010 / EN-norm voor vluchtweg-aanduiding).
  const SAFETY_GREEN = '#0E8A3E';
  // Marker-types (uitgangen + officieel vluchtroute-pictogram, ruimtes, maatlijnen, techniek, tekst).
  const MARKERS = {
    exit:  { name: 'Nooduitgang', text: 'NOODUITGANG', bg: SAFETY_GREEN, fg: '#fff', man: true, dw: 3, dh: 1 },
    fire:  { name: 'Uitgang', text: 'UITGANG', bg: SAFETY_GREEN, fg: '#fff', man: true, dw: 3, dh: 1 },
    stair: { name: 'Trap / uitgang', text: 'TRAP', bg: '#0ea5a4', fg: '#fff', icon: '🪜', dw: 2, dh: 1 },
    arrow: { name: 'Vluchtroute', text: '', bg: SAFETY_GREEN, fg: '#fff', route: true, dw: 2, dh: 1 },
    zone:  { name: 'Ruimte / vlak', text: 'RUIMTE', bg: '#eef2f7', fg: '#334155', zone: true, dw: 4, dh: 2 },
    dim:   { name: 'Maatlijn', text: '0 m', bg: 'transparent', fg: '#0f172a', dim: true, dw: 4, dh: 1 },
    firstaid: { name: 'EHBO-post', text: 'EHBO', bg: '#ffffff', fg: '#16a34a', safety: true, cross: true, dw: 2, dh: 2 },
    aed:      { name: 'AED / defibrillator', text: 'AED', bg: '#16a34a', fg: '#ffffff', safety: true, cross: true, dw: 2, dh: 2 },
    ext:      { name: 'Brandblusser', text: 'BLUSSER', bg: '#dc2626', fg: '#ffffff', safety: true, dw: 2, dh: 1 },
    hose:     { name: 'Brandhaspel', text: 'HASPEL', bg: '#dc2626', fg: '#ffffff', safety: true, dw: 2, dh: 1 },
    assembly: { name: 'Verzamelplaats', text: 'VERZAMELPLAATS', bg: '#16a34a', fg: '#ffffff', safety: true, assembly: true, dw: 3, dh: 2 },
    tech:  { name: 'Techniek', text: 'TECHNIEK', bg: '#475569', fg: '#fff', icon: '🎛', dw: 3, dh: 2, dashed: true },
    label: { name: 'Tekst', text: 'Tekst', bg: '#e2e8f0', fg: '#0f172a', dw: 3, dh: 1 },
  };

  // Capaciteits-/uitgangscontrole (zelfde logica als server planCapacity).
  function capacity(layout) {
    const scale = Math.max(5, Number(layout && layout.scale) || 50);
    const norm = Math.max(10, Number(layout && layout.exitNorm) || 125);
    let seats = 0, wheelchair = 0, standing = 0;
    for (const sec of ((layout && layout.sections) || [])) for (const row of (sec.rows || [])) for (const seat of (row.seats || [])) {
      if (seat.aisle) continue; seats++; if (seat.wheelchair) wheelchair++;
    }
    for (const z of ((layout && layout.standing) || [])) standing += Math.max(0, Number(z.capacity) || 0);
    seats += standing;
    let exitWidthM = 0, exitCount = 0;
    for (const m of ((layout && layout.markers) || [])) {
      if (m.type !== 'exit' && m.type !== 'fire') continue;
      exitCount++; exitWidthM += Math.max(Number(m.w) || 1, Number(m.h) || 1) * scale / 100;
    }
    const cap = Math.floor(exitWidthM * norm);
    return { seats, wheelchair, exitWidthM: Math.round(exitWidthM * 100) / 100, exitCount, norm, scale, capacity: cap, ok: seats <= cap };
  }

  function rankColor(ranks, rankId) {
    const r = (ranks || []).find(x => x.id === rankId);
    return (r && r.color) || '#cbd5e1';
  }

  // ---- Geometrie ----------------------------------------------------------
  function maxRowLen(block) {
    return (block.rows || []).reduce((m, r) => Math.max(m, (r.seats || []).length), 0);
  }
  function normRot(r) { return ((Math.round((Number(r) || 0) / 90) * 90) % 360 + 360) % 360; }
  // Niet-geroteerde celafmetingen van een blok (incl. gutter voor rij-labels + header).
  function blockDims(block) {
    return { cols: GUTTER + Math.max(1, maxRowLen(block)), rows: HEADER + Math.max(1, (block.rows || []).length) };
  }
  // Roteer een cel (px,py) in een cols×rows-raster naar het geroteerde raster.
  function rotCell(px, py, cols, rows, rot) {
    const r = normRot(rot);
    if (r === 90) return { x: rows - 1 - py, y: px };
    if (r === 180) return { x: cols - 1 - px, y: rows - 1 - py };
    if (r === 270) return { x: py, y: cols - 1 - px };
    return { x: px, y: py };
  }
  // Continue variant (voor het centreren van labels).
  function rotPoint(px, py, cols, rows, rot) {
    const r = normRot(rot);
    if (r === 90) return { x: rows - py, y: px };
    if (r === 180) return { x: cols - px, y: rows - py };
    if (r === 270) return { x: py, y: cols - px };
    return { x: px, y: py };
  }
  // Aantal verkoopbare stoelen aan een tafel.
  function tableSeatCount(block) {
    return (block.rows || []).reduce((a, r) => a + (r.seats || []).filter(s => !s.aisle).length, 0);
  }
  // Diameter (cellen) van een ronde tafel obv. aantal stoelen.
  function tableDiameter(block) {
    return Math.max(4, Math.ceil(Math.max(1, tableSeatCount(block)) / 3) + 2);
  }
  // Geroteerde footprint (cellen) — voor collision & omvang. Tafels = vierkant.
  function blockFootprint(block) {
    if (block && block.kind === 'table') { const d = tableDiameter(block); return { w: d, h: d }; }
    const { cols, rows } = blockDims(block);
    const r = normRot(block && block.rot);
    return (r === 90 || r === 270) ? { w: rows, h: cols } : { w: cols, h: rows };
  }
  function rectsOverlap(a, b) {
    return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
  }

  // Los alle posities op (gebruik opgeslagen x/y, val terug op nette stapeling),
  // en geef rechthoeken + totale omvang terug. Pure functie → editor & publiek
  // berekenen exact dezelfde plattegrond.
  function geometry(layout) {
    const sections = (layout && layout.sections) || [];
    const blocks = sections.map((b, i) => ({ block: b, i, fp: blockFootprint(b) }));

    // Stapel-cursor voor blokken zonder opgeslagen positie.
    let stackY = (layout && layout.stage && Number.isFinite(layout.stage.y))
      ? layout.stage.y + (layout.stage.h || 1) + 1
      : 2; // ruimte voor (virtueel) podium bovenaan
    for (const b of blocks) {
      const hasPos = Number.isFinite(b.block.x) && Number.isFinite(b.block.y);
      b.x = hasPos ? b.block.x : 0;
      b.y = hasPos ? b.block.y : stackY;
      b.w = b.fp.w; b.h = b.fp.h;
      if (!hasPos) stackY = b.y + b.h + 1;
    }

    const markers = ((layout && layout.markers) || []).map((m, i) => ({
      marker: m, i,
      x: Math.max(0, Number(m.x) || 0), y: Math.max(0, Number(m.y) || 0),
      w: Math.max(1, Number(m.w) || 2), h: Math.max(1, Number(m.h) || 1),
    }));
    const standing = ((layout && layout.standing) || []).map((z, i) => ({
      zone: z, i,
      x: Math.max(0, Number(z.x) || 0), y: Math.max(0, Number(z.y) || 0),
      w: Math.max(2, Number(z.w) || 4), h: Math.max(1, Number(z.h) || 2),
    }));

    // Omvang van alles behalve het podium (dat past zich daarna aan de breedte aan).
    let cols = 1, rows = 1;
    for (const b of blocks) { cols = Math.max(cols, b.x + b.w); rows = Math.max(rows, b.y + b.h); }
    for (const m of markers) { cols = Math.max(cols, m.x + m.w); rows = Math.max(rows, m.y + m.h); }
    for (const z of standing) { cols = Math.max(cols, z.x + z.w); rows = Math.max(rows, z.y + z.h); }

    // Podium: opgeslagen of een virtueel podium dat de breedte overspant.
    let stage = null;
    if (layout && layout.stage) {
      const s = layout.stage;
      stage = { x: Math.max(0, Number(s.x) || 0), y: Math.max(0, Number(s.y) || 0),
                w: Math.max(2, Number(s.w) || cols), h: Math.max(1, Number(s.h) || 1),
                label: s.label || 'PODIUM', virtual: false };
    } else {
      stage = { x: 0, y: 0, w: Math.max(6, cols), h: 1, label: 'PODIUM', virtual: true };
    }
    cols = Math.max(cols, stage.x + stage.w);
    rows = Math.max(rows, stage.y + stage.h);

    return { blocks, markers, standing, stage, cols: cols + PAD, rows: rows + PAD };
  }

  // Verzamel rechthoeken voor collision-detectie.
  //  opts.physicalOnly = enkel tribunes + podium (markers zijn vrije overlays:
  //  nooduitgangen/vluchtgangen mogen breder zijn dan de rijen en vrij geplaatst).
  function allRects(layout, exclude, opts) {
    const physicalOnly = !!(opts && opts.physicalOnly);
    const g = geometry(layout);
    const out = [];
    if (!g.stage.virtual && (!exclude || exclude.kind !== 'stage'))
      out.push({ kind: 'stage', id: 'stage', x: g.stage.x, y: g.stage.y, w: g.stage.w, h: g.stage.h });
    for (const b of g.blocks)
      if (!(exclude && exclude.kind === 'block' && exclude.id === b.block.id))
        out.push({ kind: 'block', id: b.block.id, x: b.x, y: b.y, w: b.w, h: b.h });
    if (!physicalOnly) for (const m of g.markers)
      if (!(exclude && exclude.kind === 'marker' && exclude.id === m.marker.id))
        out.push({ kind: 'marker', id: m.marker.id, x: m.x, y: m.y, w: m.w, h: m.h });
    return out;
  }
  // Botst een kandidaat-rechthoek met iets anders?
  function collides(layout, exclude, rect, opts) {
    return allRects(layout, exclude, opts).some(r => rectsOverlap(r, rect));
  }

  // ---- Sleep-hook (blok/marker/podium verplaatsen in edit-modus) ----------
  function useDragMove(cellPx, current, onCommit, onSelect, enabled) {
    const [drag, setDrag] = React.useState(null);
    const start = React.useRef(null);
    if (!enabled) return { drag: null, handlers: {} };
    const onPointerDown = (e) => {
      e.stopPropagation();
      start.current = { px: e.clientX, py: e.clientY, x: current.x, y: current.y, moved: false };
      setDrag({ dx: 0, dy: 0 });
      try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {}
    };
    const onPointerMove = (e) => {
      if (!start.current) return;
      const dx = Math.round((e.clientX - start.current.px) / cellPx);
      const dy = Math.round((e.clientY - start.current.py) / cellPx);
      if (dx || dy) start.current.moved = true;
      setDrag({ dx, dy });
    };
    const onPointerUp = (e) => {
      if (!start.current) return;
      const dx = Math.round((e.clientX - start.current.px) / cellPx);
      const dy = Math.round((e.clientY - start.current.py) / cellPx);
      const moved = start.current.moved;
      const nx = Math.max(0, start.current.x + dx);
      const ny = Math.max(0, start.current.y + dy);
      start.current = null; setDrag(null);
      if (moved) onCommit && onCommit(nx, ny);
      else onSelect && onSelect();
    };
    return { drag, handlers: { onPointerDown, onPointerMove, onPointerUp, style: { touchAction: 'none' } } };
  }

  // ---- Stoel --------------------------------------------------------------
  function SeatCell({ seat, status, color, size, clickable, onClick }) {
    const sc = STATUS_COLORS[status];
    const wheel = !!seat.wheelchair;
    const bg = sc ? sc.bg : color;
    const border = (wheel && !sc) ? '#2563eb' : (sc ? sc.border : color);
    const fg = sc ? sc.fg : (wheel ? '#1d4ed8' : '#06302b');
    const opacity = sc ? sc.opacity : 1;
    const inner = Math.max(8, size - 4);
    const title = `Rij ${seat.rowLabel || ''} · ${wheel ? 'rolstoelplaats ' : 'stoel '}${seat.label || '?'}` +
      (seat.rankLabel ? ` · ${seat.rankLabel}` : '') +
      (status === 'sold' ? ' · verkocht' : status === 'held' ? ' · gereserveerd' : status === 'blocked' ? ' · geblokkeerd' : '');
    return (
      <div style={{ width: size, height: size, display: 'flex', alignItems: 'center', justifyContent: 'center', flex: '0 0 auto' }}>
        <button
          type="button" title={title} disabled={!clickable}
          onClick={clickable ? onClick : undefined}
          className="fp-seat"
          style={{
            width: inner, height: inner, borderRadius: Math.max(3, size * 0.18),
            border: `2px solid ${border}`, background: bg, color: fg,
            fontSize: wheel ? Math.max(9, Math.round(size * 0.5)) : Math.max(7, Math.round(size * 0.32)), fontWeight: 700,
            cursor: clickable ? 'pointer' : 'not-allowed', opacity,
            padding: 0, lineHeight: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}
        >{wheel ? '♿' : (seat.label || '')}</button>
      </div>
    );
  }

  function AisleCell({ size, editable, onClick }) {
    return (
      <div onClick={editable ? onClick : undefined}
        title={editable ? 'Gangpad — klik om er een stoel van te maken' : 'Gangpad'}
        style={{
          width: size, height: size, flex: '0 0 auto', display: 'flex', alignItems: 'center', justifyContent: 'center',
          cursor: editable ? 'pointer' : 'default',
        }}>
        <div style={{
          width: Math.max(6, size - 8), height: Math.max(6, size - 8), borderRadius: 3,
          border: editable ? '1px dashed #cbd5e1' : '1px dashed transparent', background: 'transparent',
        }} />
      </div>
    );
  }

  // ---- Eén blok / tribune -------------------------------------------------
  function Block(props) {
    const { g, cellPx, ranks, mode, statusOf, sel, selectedBlockId,
      onSeatClick, onAisleClick, onSelectBlock, onMoveBlock } = props;
    const block = g.block;
    const editable = mode === 'edit';
    const selected = editable && selectedBlockId === block.id;
    const { drag, handlers } = useDragMove(cellPx, { x: g.x, y: g.y },
      (nx, ny) => onMoveBlock && onMoveBlock(block.id, nx, ny),
      () => onSelectBlock && onSelectBlock(block.id),
      editable);
    const ox = g.x + (drag ? drag.dx : 0), oy = g.y + (drag ? drag.dy : 0);
    const rot = normRot(block.rot);
    const { cols, rows } = blockDims(block);          // niet-geroteerd raster
    const place = (px, py) => { const c = rotCell(px, py, cols, rows, rot); return { left: c.x * cellPx, top: c.y * cellPx }; };

    const renderSeat = (seat, rowLabel) => {
      const enriched = { ...seat, rowLabel, rankLabel: (ranks || []).find(r => r.id === seat.rankId)?.label || '' };
      const color = rankColor(ranks, seat.rankId);
      let status = 'free', clickable = false;
      if (mode === 'edit') { status = seat.blocked ? 'blocked' : 'free'; clickable = true; }
      else if (mode === 'monitor') { status = (statusOf && statusOf(seat)) || 'free'; clickable = true; }
      else { const base = (statusOf && statusOf(seat)) || 'free'; status = sel.has(seat.id) ? 'selected' : base; clickable = status === 'free' || status === 'selected'; }
      return <SeatCell key={seat.id} seat={enriched} status={status} color={color} size={cellPx}
        clickable={clickable} onClick={() => onSeatClick && onSeatClick(seat)} />;
    };

    // Bloknaam (sleephandvat) — gecentreerd op de header-rand, blijft rechtop.
    const np = rotPoint(cols / 2, 0.5, cols, rows, rot);

    // ---- Ronde tafel (gala) ----
    if (block.kind === 'table') {
      const D = g.w, box = D * cellPx, cx = box / 2, cy = box / 2;
      const seatsAll = (block.rows || []).flatMap(r => (r.seats || []).filter(s => !s.aisle).map(s => ({ s, rowLabel: r.label })));
      const N = Math.max(1, seatsAll.length);
      const radiusPx = (D / 2 - 0.85) * cellPx;
      return (
        <div style={{ position: 'absolute', left: ox * cellPx, top: oy * cellPx, width: box, height: box, zIndex: drag ? 50 : (selected ? 20 : 5) }}>
          <div style={{ position: 'absolute', left: cellPx * 0.6, top: cellPx * 0.6, right: cellPx * 0.6, bottom: cellPx * 0.6, borderRadius: '50%',
            border: selected ? '2px solid #0ea5a4' : '1px solid rgba(148,163,184,.5)', background: selected ? 'rgba(14,165,164,.08)' : 'rgba(148,163,184,.10)',
            boxShadow: drag ? '0 8px 24px rgba(0,0,0,.18)' : 'none' }} />
          <div {...handlers}
            onClick={(e) => { if (editable) { e.stopPropagation(); onSelectBlock && onSelectBlock(block.id); } }}
            title={editable ? 'Sleep om de tafel te verplaatsen' : block.label}
            style={{ position: 'absolute', left: cx, top: cy, transform: 'translate(-50%,-50%)', maxWidth: box * 0.6, textAlign: 'center',
              fontSize: Math.max(8, Math.min(12, cellPx * 0.4)), fontWeight: 700, color: selected ? '#0f766e' : '#64748b',
              cursor: editable ? 'grab' : 'default', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', zIndex: 2 }}>
            {block.label}
          </div>
          {seatsAll.map((it, i) => {
            const ang = -Math.PI / 2 + (i * 2 * Math.PI) / N;
            const left = cx + radiusPx * Math.cos(ang) - cellPx / 2, top = cy + radiusPx * Math.sin(ang) - cellPx / 2;
            return <div key={it.s.id} style={{ position: 'absolute', left, top, width: cellPx, height: cellPx }}>{renderSeat(it.s, it.rowLabel)}</div>;
          })}
        </div>
      );
    }

    return (
      <div style={{
        position: 'absolute', left: ox * cellPx, top: oy * cellPx,
        width: g.w * cellPx, height: g.h * cellPx,
        zIndex: drag ? 50 : (selected ? 20 : 5),
      }}>
        <div style={{
          position: 'absolute', inset: 0, borderRadius: 10,
          border: selected ? '2px solid #0ea5a4' : '1px solid rgba(148,163,184,.35)',
          background: selected ? 'rgba(14,165,164,.06)' : 'rgba(148,163,184,.06)',
          boxShadow: drag ? '0 8px 24px rgba(0,0,0,.18)' : 'none',
        }} />
        {/* rij-labels */}
        {(block.rows || []).map((row, vi) => {
          const p = place(0, HEADER + vi);
          return <div key={row.id + ':lbl'} style={{ position: 'absolute', left: p.left, top: p.top, width: cellPx, height: cellPx, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: Math.max(8, cellPx * 0.34), opacity: 0.55 }}>{row.label}</div>;
        })}
        {/* stoelen / gangpaden */}
        {(block.rows || []).map((row, vi) => (row.seats || []).map((seat, ui) => {
          const p = place(GUTTER + ui, HEADER + vi);
          return <div key={seat.id} style={{ position: 'absolute', left: p.left, top: p.top, width: cellPx, height: cellPx }}>
            {seat.aisle
              ? <AisleCell size={cellPx} editable={editable} onClick={() => onAisleClick && onAisleClick(seat)} />
              : renderSeat(seat, row.label)}
          </div>;
        }))}
        {/* bloknaam / sleephandvat */}
        <div {...handlers}
          onClick={(e) => { if (editable) { e.stopPropagation(); onSelectBlock && onSelectBlock(block.id); } }}
          title={editable ? 'Sleep om de tribune te verplaatsen' : block.label}
          style={{
            position: 'absolute', left: np.x * cellPx, top: np.y * cellPx, transform: 'translate(-50%,-50%)',
            maxWidth: Math.max(cols, rows) * cellPx, padding: '0 6px',
            fontSize: Math.max(9, Math.min(13, cellPx * 0.42)), fontWeight: 700,
            color: selected ? '#0f766e' : '#475569', cursor: editable ? 'grab' : 'default',
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
            display: 'flex', alignItems: 'center', gap: 5, zIndex: 2,
          }}>
          {editable && <span style={{ opacity: 0.4, fontSize: cellPx * 0.5 }}>⠿</span>}
          {block.label}
        </div>
        {(block.rows || []).length === 0 && (
          <div style={{ position: 'absolute', left: 6, top: cellPx + 4, right: 6, fontSize: Math.max(9, cellPx * 0.34), opacity: 0.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>Nog geen rijen</div>
        )}
      </div>
    );
  }

  // Officieel "rennend mannetje" — ISO 7010 vluchtweg-pictogram. Standaard naar rechts.
  function RunningMan({ size, color, flip }) {
    return (
      <svg width={size * 0.78} height={size} viewBox="0 0 40 48"
        style={{ display: 'block', transform: flip ? 'scaleX(-1)' : 'none' }}>
        <g stroke={color} strokeWidth="4.6" strokeLinecap="round" strokeLinejoin="round" fill="none">
          <line x1="23" y1="13" x2="18" y2="29" />
          <polyline points="18,29 11,34 5,45" />
          <polyline points="18,29 26,33 30,45" />
          <polyline points="20,16 12,20 7,27" />
          <polyline points="20,16 28,21 34,16" />
        </g>
        <circle cx="22" cy="8" r="5" fill={color} />
      </svg>
    );
  }
  // Richtingspijl (vierkante viewBox → schone 90°-rotatie). Standaard naar rechts.
  function RouteArrow({ size, color, rot }) {
    return (
      <svg width={size} height={size} viewBox="0 0 40 40"
        style={{ display: 'block', transform: `rotate(${rot || 0}deg)` }}>
        <g stroke={color} strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" fill={color}>
          <line x1="5" y1="20" x2="29" y2="20" />
          <polygon points="25,9 39,20 25,31" stroke="none" />
        </g>
      </svg>
    );
  }
  // Kruis (EHBO / AED).
  function CrossGlyph({ size, color }) {
    const t = size * 0.28, o = (size - t) / 2;
    return (
      <svg width={size} height={size} viewBox="0 0 40 40" style={{ display: 'block' }}>
        <rect x={(40 - 40 * 0.28) / 2} y="6" width={40 * 0.28} height="28" rx="2" fill={color} />
        <rect x="6" y={(40 - 40 * 0.28) / 2} width="28" height={40 * 0.28} rx="2" fill={color} />
      </svg>
    );
  }
  // Verzamelplaats — 4 pijlen naar het midden (ISO).
  function AssemblyGlyph({ size, color }) {
    return (
      <svg width={size} height={size} viewBox="0 0 40 40" style={{ display: 'block' }}>
        <g fill={color}>
          <polygon points="20,14 15,5 25,5" />
          <polygon points="20,26 15,35 25,35" />
          <polygon points="14,20 5,15 5,25" />
          <polygon points="26,20 35,15 35,25" />
          <circle cx="20" cy="20" r="2.5" />
        </g>
      </svg>
    );
  }
  // Maatlijn (afmeting): lijn met eindstreepjes + tekst.
  function DimGlyph({ w, h, color, label }) {
    const horiz = w >= h;
    return (
      <div style={{ position: 'relative', width: w, height: h, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        <svg width={w} height={h} style={{ position: 'absolute', inset: 0 }}>
          {horiz ? (
            <g stroke={color} strokeWidth="1.5">
              <line x1="6" y1={h / 2} x2={w - 6} y2={h / 2} />
              <line x1="6" y1={h * 0.25} x2="6" y2={h * 0.75} />
              <line x1={w - 6} y1={h * 0.25} x2={w - 6} y2={h * 0.75} />
            </g>
          ) : (
            <g stroke={color} strokeWidth="1.5">
              <line x1={w / 2} y1="6" x2={w / 2} y2={h - 6} />
              <line x1={w * 0.25} y1="6" x2={w * 0.75} y2="6" />
              <line x1={w * 0.25} y1={h - 6} x2={w * 0.75} y2={h - 6} />
            </g>
          )}
        </svg>
        <span style={{ position: 'relative', background: '#fff', padding: '0 4px', fontSize: Math.max(9, Math.min(14, h * 0.5)), fontWeight: 700, color }}>{label}</span>
      </div>
    );
  }

  // ---- Marker (uitgang / richting / ruimte / maatlijn / techniek / tekst) -
  function MarkerBox(props) {
    const { g, cellPx, mode, selectedMarkerId, onSelectMarker, onMoveMarker } = props;
    const m = g.marker;
    const cfg = MARKERS[m.type] || MARKERS.label;
    const editable = mode === 'edit';
    const selected = editable && selectedMarkerId === m.id;
    const { drag, handlers } = useDragMove(cellPx, { x: g.x, y: g.y },
      (nx, ny) => onMoveMarker && onMoveMarker(m.id, nx, ny),
      () => onSelectMarker && onSelectMarker(m.id),
      editable);
    const ox = g.x + (drag ? drag.dx : 0), oy = g.y + (drag ? drag.dy : 0);
    const text = (m.text != null && m.text !== '') ? m.text : cfg.text;
    const innerW = g.w * cellPx - 4, innerH = g.h * cellPx - 4;
    const tall = innerH > innerW * 1.6;  // smal+hoog → label verticaal
    const fontSize = Math.max(8, Math.min(15, cellPx * 0.42));

    const rot = m.rot || 0;
    let content, boxStyle;
    if (cfg.route) {
      // Officieel vluchtroute-bord: rennend mannetje + richtingspijl, in veiligheidsgroen.
      const manSize = Math.min(innerH - 4, innerW * 0.5);
      const arrSize = Math.min(innerH - 6, innerW * 0.42);
      boxStyle = { background: cfg.bg, borderRadius: 6, border: selected ? '2px solid #0f172a' : '1px solid rgba(0,0,0,.15)' };
      content = (
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: Math.max(2, cellPx * 0.12), width: '100%', height: '100%' }}>
          <RunningMan size={manSize} color="#fff" flip={rot === 180} />
          <RouteArrow size={arrSize} color="#fff" rot={rot} />
        </div>
      );
    } else if (cfg.man) {
      // Nooduitgang/Uitgang met officieel pictogram + opschrift.
      const manSize = Math.min(innerH - 4, innerW * 0.5, cellPx * 1.6);
      boxStyle = { background: cfg.bg, borderRadius: 6, border: selected ? '2px solid #0f172a' : '1px solid rgba(0,0,0,.15)' };
      content = tall ? (
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4, height: '100%' }}>
          <RunningMan size={Math.min(innerW - 6, innerH * 0.34)} color={cfg.fg} />
          <span style={{ transform: 'rotate(-90deg)', whiteSpace: 'nowrap', color: cfg.fg, fontWeight: 700, fontSize, maxWidth: innerH * 0.55 }}>{text}</span>
        </div>
      ) : (
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, width: '100%', height: '100%' }}>
          <RunningMan size={manSize} color={cfg.fg} />
          <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', color: cfg.fg, fontWeight: 700, fontSize, letterSpacing: 0.3, maxWidth: innerW - manSize - 14 }}>{text}</span>
        </div>
      );
    } else if (cfg.safety) {
      // Veiligheidsvoorziening: EHBO/AED (kruis), verzamelplaats (pijlen) of blusser/haspel.
      const gly = Math.min(innerH - 4, innerW * 0.5, cellPx * 1.7);
      boxStyle = { background: cfg.bg, borderRadius: 6, border: selected ? '2px solid #0f172a' : '1px solid rgba(0,0,0,.2)' };
      content = (
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, width: '100%', height: '100%' }}>
          {cfg.cross && <CrossGlyph size={gly} color={cfg.fg} />}
          {cfg.assembly && <AssemblyGlyph size={gly} color={cfg.fg} />}
          <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', color: cfg.fg, fontWeight: 800, letterSpacing: 0.3, fontSize: Math.max(8, Math.min(13, cellPx * 0.4)), maxWidth: innerW - (cfg.cross || cfg.assembly ? gly : 0) - 12 }}>{text}</span>
        </div>
      );
    } else if (cfg.dim) {
      boxStyle = { background: 'transparent', border: selected ? '2px dashed #0f172a' : 'none' };
      content = <DimGlyph w={innerW} h={innerH} color={cfg.fg} label={text} />;
    } else if (cfg.zone) {
      boxStyle = { background: cfg.bg, border: selected ? '2px solid #0f172a' : '2px dashed #94a3b8', borderRadius: 8 };
      content = <span style={{ fontSize: Math.max(10, Math.min(18, cellPx * 0.5)), fontWeight: 800, letterSpacing: 1, color: cfg.fg, textTransform: 'uppercase', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', padding: '0 6px' }}>{text}</span>;
    } else {
      boxStyle = {
        background: cfg.bg, borderRadius: 8,
        border: selected ? '2px solid #0f172a' : cfg.dashed ? `2px dashed ${cfg.fg}` : '1px solid rgba(0,0,0,.15)',
      };
      content = (
        <span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, color: cfg.fg, fontSize, fontWeight: 700, letterSpacing: 0.5, padding: '0 6px', transform: tall ? 'rotate(-90deg)' : 'none' }}>
          {cfg.icon && !tall && <span style={{ fontSize: fontSize * 1.15 }}>{cfg.icon}</span>}
          <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: tall ? innerH - 6 : innerW - 12 }}>{text}</span>
        </span>
      );
    }

    return (
      <div {...handlers}
        onClick={(e) => { if (editable) { e.stopPropagation(); onSelectMarker && onSelectMarker(m.id); } }}
        title={editable ? cfg.name + ' — sleep om te verplaatsen' : (text || cfg.name)}
        style={{
          position: 'absolute', left: ox * cellPx, top: oy * cellPx,
          width: innerW, height: innerH, margin: 2,
          display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center',
          cursor: editable ? 'grab' : 'default', boxShadow: drag ? '0 8px 24px rgba(0,0,0,.22)' : 'none',
          zIndex: drag ? 50 : (selected ? 20 : 8), overflow: 'hidden', userSelect: 'none', ...boxStyle,
        }}>
        {content}
      </div>
    );
  }

  // ---- Podium -------------------------------------------------------------
  function Stage(props) {
    const { g, cellPx, mode, onMoveStage, onSelectStage, selected } = props;
    const editable = mode === 'edit' && !g.virtual;
    const { drag, handlers } = useDragMove(cellPx, { x: g.x, y: g.y },
      (nx, ny) => onMoveStage && onMoveStage(nx, ny),
      () => onSelectStage && onSelectStage(),
      editable);
    const ox = g.x + (drag ? drag.dx : 0), oy = g.y + (drag ? drag.dy : 0);
    return (
      <div {...handlers}
        title={editable ? 'Sleep het podium' : 'Podium'}
        style={{
          position: 'absolute', left: ox * cellPx, top: oy * cellPx,
          width: g.w * cellPx - 4, height: g.h * cellPx - 4, margin: 2,
          background: 'linear-gradient(180deg,#0ea5a4,#06302b)', color: '#fff',
          borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center',
          fontSize: Math.max(10, Math.min(16, cellPx * 0.5)), fontWeight: 800, letterSpacing: 5,
          cursor: editable ? 'grab' : 'default', boxShadow: drag ? '0 8px 24px rgba(0,0,0,.25)' : 'none',
          border: (mode === 'edit' && selected) ? '2px solid #fff' : 'none', zIndex: drag ? 50 : 4, userSelect: 'none',
        }}>{g.label || 'PODIUM'}</div>
    );
  }

  // ---- Staanplaatszone (verkoop per capaciteit) --------------------------
  function StandingZone(props) {
    const { g, cellPx, mode, selectedStandingId, onSelectStanding, onMoveStanding, standingAvail, onStandingClick, selectedQty } = props;
    const z = g.zone;
    const editable = mode === 'edit';
    const selected = editable && selectedStandingId === z.id;
    const { drag, handlers } = useDragMove(cellPx, { x: g.x, y: g.y },
      (nx, ny) => onMoveStanding && onMoveStanding(z.id, nx, ny),
      () => onSelectStanding && onSelectStanding(z.id), editable);
    const ox = g.x + (drag ? drag.dx : 0), oy = g.y + (drag ? drag.dy : 0);
    const a = standingAvail && standingAvail[z.id];
    const free = a ? a.free : z.capacity;
    const clickable = mode === 'select' && free > 0;
    return (
      <div {...(editable ? handlers : {})}
        onClick={(e) => { if (editable) { e.stopPropagation(); onSelectStanding && onSelectStanding(z.id); } else if (clickable) { e.stopPropagation(); onStandingClick && onStandingClick(z.id); } }}
        title={editable ? 'Staanplaatszone — sleep om te verplaatsen' : (z.name || 'Staanplaatsen')}
        style={{
          position: 'absolute', left: ox * cellPx, top: oy * cellPx, width: g.w * cellPx - 4, height: g.h * cellPx - 4, margin: 2,
          background: 'repeating-linear-gradient(45deg,#fef3c7,#fef3c7 7px,#fde68a 7px,#fde68a 14px)',
          border: selected ? '2px solid #0f172a' : '2px dashed #d97706', borderRadius: 8,
          display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center', gap: 1,
          cursor: editable ? 'grab' : (clickable ? 'pointer' : 'default'), color: '#92400e', userSelect: 'none', overflow: 'hidden',
          boxShadow: drag ? '0 8px 24px rgba(0,0,0,.22)' : 'none', zIndex: drag ? 50 : (selected ? 20 : 7), padding: '0 6px',
        }}>
        <span style={{ fontWeight: 800, fontSize: Math.max(10, Math.min(15, cellPx * 0.46)), whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '98%' }}>🧍 {z.name || 'Staanplaatsen'}</span>
        <span style={{ fontSize: Math.max(8, cellPx * 0.34) }}>{mode === 'edit' || !a ? `${z.capacity} plaatsen` : `${free} van ${a.capacity} vrij`}{selectedQty ? ` · ${selectedQty} gekozen` : ''}</span>
        {clickable && <span style={{ fontSize: Math.max(7, cellPx * 0.28), opacity: 0.75 }}>+ klik om te kiezen</span>}
      </div>
    );
  }

  // ---- Hoofd-plattegrond --------------------------------------------------
  function FloorPlan(props) {
    const {
      layout, ranks, mode = 'select',
      statusOf, selected, onSeatClick, onAisleClick,
      selectedBlockId, onSelectBlock, onMoveBlock,
      selectedMarkerId, onSelectMarker, onMoveMarker,
      onMoveStage, onSelectStage, stageSelected,
      selectedStandingId, onSelectStanding, onMoveStanding, standingAvail, onStandingClick, standingQty,
      onBackgroundClick, minCell = CELL_MIN, maxCell = CELL_MAX,
    } = props;
    const sel = selected || new Set();
    const wrapRef = React.useRef(null);
    const [width, setWidth] = React.useState(0);

    React.useEffect(() => {
      const el = wrapRef.current; if (!el) return;
      const measure = () => setWidth(el.clientWidth);
      measure();
      let ro;
      if (typeof ResizeObserver !== 'undefined') { ro = new ResizeObserver(measure); ro.observe(el); }
      else window.addEventListener('resize', measure);
      return () => { if (ro) ro.disconnect(); else window.removeEventListener('resize', measure); };
    }, []);

    const g = geometry(layout);
    const avail = Math.max(0, width - 4);
    let cellPx = avail > 0 ? Math.floor(avail / g.cols) : maxCell;
    cellPx = Math.max(minCell, Math.min(maxCell, cellPx));
    const canvasW = g.cols * cellPx, canvasH = g.rows * cellPx;
    const showTech = mode !== 'select'; // techniek is enkel organisatie-info

    return (
      <div ref={wrapRef} style={{ width: '100%', overflowX: 'auto', overflowY: 'hidden' }}>
        <div
          onClick={() => onBackgroundClick && onBackgroundClick()}
          style={{ position: 'relative', width: canvasW, height: canvasH, margin: '0 auto',
            backgroundImage: mode === 'edit'
              ? `linear-gradient(rgba(148,163,184,.10) 1px, transparent 1px), linear-gradient(90deg, rgba(148,163,184,.10) 1px, transparent 1px)`
              : 'none',
            backgroundSize: `${cellPx}px ${cellPx}px` }}>
          <Stage g={g.stage} cellPx={cellPx} mode={mode} onMoveStage={onMoveStage} onSelectStage={onSelectStage} selected={stageSelected} />
          {g.blocks.map(b => (
            <Block key={b.block.id} g={b} cellPx={cellPx} ranks={ranks} mode={mode}
              statusOf={statusOf} sel={sel} selectedBlockId={selectedBlockId}
              onSeatClick={onSeatClick} onAisleClick={onAisleClick}
              onSelectBlock={onSelectBlock} onMoveBlock={onMoveBlock} />
          ))}
          {g.markers.map(m => (
            (m.marker.type === 'tech' && !showTech) ? null :
            <MarkerBox key={m.marker.id} g={m} cellPx={cellPx} mode={mode}
              selectedMarkerId={selectedMarkerId} onSelectMarker={onSelectMarker} onMoveMarker={onMoveMarker} />
          ))}
          {g.standing.map(z => (
            <StandingZone key={z.zone.id} g={z} cellPx={cellPx} mode={mode}
              selectedStandingId={selectedStandingId} onSelectStanding={onSelectStanding} onMoveStanding={onMoveStanding}
              standingAvail={standingAvail} onStandingClick={onStandingClick} selectedQty={standingQty && standingQty[z.zone.id]} />
          ))}
        </div>
      </div>
    );
  }

  // ---- Legende ------------------------------------------------------------
  function SeatMapLegend({ ranks, showStatuses = true }) {
    const dot = (color, label, extra) => (
      <span key={label} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, marginRight: 14 }}>
        <span style={{ width: 14, height: 14, borderRadius: 4, background: color, border: '1px solid rgba(0,0,0,.15)', ...extra }} />
        {label}
      </span>
    );
    return (
      <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 4, marginTop: 12 }}>
        {(ranks || []).map(r => dot(r.color, `${r.label} · €${(Number(r.price) || 0).toFixed(2)}`))}
        {showStatuses && dot('#9ca3af', 'Gereserveerd')}
        {showStatuses && dot('#1f2937', 'Verkocht')}
        {showStatuses && dot('#0ea5a4', 'Jouw keuze')}
      </div>
    );
  }

  window.LM_SEATMAP = {
    FloorPlan, SeatMapLegend, rankColor,
    geometry, blockFootprint, rectsOverlap, allRects, collides, MARKERS, capacity,
  };
})();
