// Rooms — 2D room planner. Draw the room outline (walls as a polygon in
// centimetres), mark doors / windows, then drop items in — either quick
// custom boxes (name / colour / size) or things pulled from your lists.
//
// Everything is in cm. Rendering is an SVG scaled to fit; the editor
// works in room coordinates so zoom never changes the numbers.
//
// Persistence: public.rooms (walls / openings / items as jsonb). Saves
// are debounced ~700ms after the last change. Undo / redo is in-memory
// per room for the session, with a visible history list.

const { useState: _rm_s, useEffect: _rm_e, useMemo: _rm_m, useRef: _rm_r, useCallback: _rm_cb } = React;

// ── Data layer ──────────────────────────────────────────────────────────
(function () {
  if (!window.MR) window.MR = {};
  if (window.MR.rooms) return;
  const client = () => window.MR && window.MR.supabase;
  const uid = async () => {
    const s = window.MR.user && window.MR.user._session;
    if (s && s.user) return s.user.id;
    try { const { data } = await client().auth.getSession(); return data && data.session && data.session.user && data.session.user.id; } catch { return null; }
  };
  window.MR.rooms = {
    async list() {
      if (!client()) return { ok: false, reason: 'no-client', rooms: [] };
      const { data, error } = await client().from('rooms').select('*').order('position', { ascending: true }).order('created_at', { ascending: true });
      if (error) return { ok: false, reason: error.message, rooms: [] };
      return { ok: true, rooms: data || [] };
    },
    async create(room) {
      const u = await uid();
      if (!u || !client()) return { ok: false, reason: 'signed-out' };
      const { data, error } = await client().from('rooms').insert({
        owner_id: u,
        name: room.name || 'Room',
        walls: room.walls || [],
        openings: room.openings || [],
        items: room.items || [],
        settings: room.settings || {},
        position: room.position || 0,
      }).select().single();
      if (error) return { ok: false, reason: error.message, error };
      return { ok: true, room: data };
    },
    async save(id, patch) {
      if (!client()) return { ok: false };
      const { data, error } = await client().from('rooms').update(patch).eq('id', id).select().single();
      if (error) return { ok: false, reason: error.message, error };
      return { ok: true, room: data };
    },
    async remove(id) {
      if (!client()) return { ok: false };
      const { error } = await client().from('rooms').delete().eq('id', id);
      return error ? { ok: false, error } : { ok: true };
    },
    // Sharing
    async claimInvites() {
      if (!client()) return { ok: false, claimed: 0 };
      const { data, error } = await client().rpc('room_claim_invites');
      return error ? { ok: false, claimed: 0 } : { ok: true, claimed: Number(data) || 0 };
    },
    async members(roomId) {
      if (!client()) return { ok: false, members: [] };
      const { data, error } = await client().rpc('room_members', { p_room: roomId });
      return error ? { ok: false, reason: error.message, members: [] } : { ok: true, members: data || [] };
    },
    async addMemberByEmail(roomId, email) {
      if (!client()) return { ok: false };
      const { data, error } = await client().rpc('room_add_member_by_email', { p_room: roomId, p_email: email });
      if (error) return { ok: false, reason: error.message, error };
      const row = Array.isArray(data) ? data[0] : data;
      return { ok: true, status: row && row.status, userId: row && row.user_id };
    },
    async removeMember(roomId, userId) {
      if (!client()) return { ok: false };
      const { error } = await client().rpc('room_remove_member', { p_room: roomId, p_user: userId });
      return error ? { ok: false, reason: error.message } : { ok: true };
    },
    async sendInviteEmail(email) {
      if (!client()) return { ok: false };
      const { error } = await client().auth.signInWithOtp({
        email,
        options: { emailRedirectTo: window.location.origin + '/#rooms', shouldCreateUser: true },
      });
      return error ? { ok: false, reason: error.message } : { ok: true };
    },
  };
})();

// ── Geometry helpers ────────────────────────────────────────────────────
const RM_SNAP = 5;   // cm
// Wall band thickness, drawn entirely OUTSIDE the polygon — the polygon
// is the inside face of the wall (what you measure), so the inner edge
// of the black band is the true wall line.
const RM_WALL_T = 8; // cm
const rmSnap = (v, step = RM_SNAP) => Math.round(v / step) * step;
const rmDist = (a, b) => Math.hypot(b.x - a.x, b.y - a.y);
const rmUid = () => Math.random().toString(36).slice(2, 10);
function rmBounds(pts) {
  if (!pts.length) return { minX: 0, minY: 0, maxX: 400, maxY: 300 };
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  pts.forEach(p => { minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y); });
  return { minX, minY, maxX, maxY };
}
function rmArea(pts) {
  let a = 0;
  for (let i = 0; i < pts.length; i++) { const p = pts[i], q = pts[(i + 1) % pts.length]; a += p.x * q.y - q.x * p.y; }
  return Math.abs(a) / 2;
}
function rmWallFrame(walls, i, offset) {
  const a = walls[i], b = walls[(i + 1) % walls.length];
  const len = rmDist(a, b) || 1;
  const ux = (b.x - a.x) / len, uy = (b.y - a.y) / len;
  return { x: a.x + ux * offset, y: a.y + uy * offset, ux, uy, nx: -uy, ny: ux, len };
}
function rmParseCm(v) {
  if (v == null) return null;
  if (typeof v === 'number') return Number.isFinite(v) ? v : null;
  const s = String(v).trim().toLowerCase();
  const m = s.match(/([\d.]+)\s*(mm|cm|m)?/);
  if (!m) return null;
  const n = parseFloat(m[1]);
  if (!Number.isFinite(n)) return null;
  if (m[2] === 'mm') return n / 10;
  if (m[2] === 'm') return n * 100;
  return n;
}
const rmSnapshot = (r) => ({ name: r.name, walls: r.walls, openings: r.openings, items: r.items, settings: r.settings || {} });

// Floor footprint from an extracted dimensions object. Prefers explicit
// width / depth (or length), then falls back to a compound "130 × 69 × 12
// cm" string — first two numbers are the footprint under the usual
// W × D × H / L × W × H retail conventions (height comes last).
function rmFootprint(dims) {
  if (!dims || typeof dims !== 'object') return { w: null, d: null };
  let w = rmParseCm(dims.width || dims.w);
  let d = rmParseCm(dims.depth || dims.d || dims.length);
  if ((w == null || d == null) && dims.size) {
    const s = String(dims.size);
    const unitM = s.match(/\b(mm|cm|m)\b/i);
    const k = unitM ? ({ mm: 0.1, cm: 1, m: 100 })[unitM[1].toLowerCase()] : 1;
    const vals = (s.match(/\d+(?:[.,]\d+)?/g) || []).map(x => parseFloat(x.replace(',', '.')) * k).filter(n => Number.isFinite(n) && n > 0);
    if (vals.length >= 2) { if (w == null) w = vals[0]; if (d == null) d = vals[1]; }
    else if (vals.length === 1 && dims.diameter == null) { if (w == null) w = vals[0]; if (d == null) d = vals[0]; }
  }
  if ((w == null || d == null) && dims.diameter) {
    const dia = rmParseCm(dims.diameter);
    if (dia) { if (w == null) w = dia; if (d == null) d = dia; }
  }
  return { w: w && w >= 1 ? Math.round(w) : null, d: d && d >= 1 ? Math.round(d) : null };
}

// Fit an item label inside its box: shrink the font down to a floor,
// wrap to two lines if there's room, then truncate with an ellipsis.
// Char width ≈ 0.56em for the UI font.
function rmFitLabel(name, w, d) {
  const text = String(name || '').trim();
  const pad = 6;
  const avail = Math.max(10, w - pad * 2);
  const CW = 0.56;
  const base = Math.max(6, Math.min(12, Math.min(w, d) / 4));
  const widthAt = (s, fs) => s.length * fs * CW;
  const trunc = (s, fs) => {
    if (widthAt(s, fs) <= avail) return s;
    const maxChars = Math.max(1, Math.floor(avail / (fs * CW)) - 1);
    return s.slice(0, maxChars).replace(/\s+\S*$/, '') .trim() + '…';
  };
  // 1. Single line at base size?
  if (widthAt(text, base) <= avail) return { fs: base, lines: [text] };
  // 2. Shrink (not below 6.5) for a single line.
  const fit1 = Math.max(6.5, Math.min(base, avail / (text.length * CW)));
  if (fit1 >= 8) return { fs: fit1, lines: [text] };
  // 3. Two lines if the box is tall enough.
  const fs2 = Math.max(6.5, Math.min(base, 9));
  if (d >= fs2 * 4.2) {
    const words = text.split(/\s+/);
    let a = '', b = '';
    for (const wd of words) {
      const t = a ? a + ' ' + wd : wd;
      if (widthAt(t, fs2) <= avail || !a) a = t; else b = b ? b + ' ' + wd : wd;
    }
    if (!b && widthAt(a, fs2) > avail) { // single long word — hard split
      const cut = Math.max(1, Math.floor(avail / (fs2 * CW)));
      b = a.slice(cut); a = a.slice(0, cut);
    }
    return { fs: fs2, lines: [trunc(a, fs2), trunc(b, fs2)].filter(Boolean) };
  }
  // 4. One line, floor size, truncated.
  return { fs: 6.5, lines: [trunc(text, 6.5)] };
}
const rmTimeAgo = (ts) => {
  const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
  if (s < 5) return 'just now';
  if (s < 60) return `${s}s ago`;
  const m = Math.round(s / 60);
  if (m < 60) return `${m}m ago`;
  return `${Math.round(m / 60)}h ago`;
};

// Simon + Lucie's room, from the paper sketch. Clockwise from the
// top-left corner (door end of the top wall). cm.
const RM_DEFAULT_ROOM = {
  name: "Baby's room",
  walls: [
    { x: 0,     y: 0 },
    { x: 312,   y: 0 },
    { x: 312,   y: 264 },
    { x: 358.5, y: 264 },
    { x: 358.5, y: 365 },
    { x: 0,     y: 365 },
  ],
  openings: [
    // Saloon door on the left wall (wall 5 runs bottom→top): two 48 cm leaves.
    { id: 'door1',   type: 'door',   style: 'saloon', wall: 5, offset: 269, width: 96, swing: 'in' },
    // Window on the bay's outer wall (wall 3, runs top→bottom).
    { id: 'window1', type: 'window', wall: 3, offset: 20, width: 60 },
  ],
  items: [],
  settings: { grid: 10 },
};

const RM_COLORS = ['#d9a066', '#8fb996', '#7aa6d6', '#c98bb9', '#e4b04a', '#8c8c8c', '#e07a5f', '#5f9ea0'];

// ── Layout variations (per room) ────────────────────────────────────────
// Stored in room.settings.layouts = [{ id, name, items }], with
// settings.activeLayout naming the one currently in room.items.
function rmLayouts(room) {
  const s = (room && room.settings) || {};
  const ls = Array.isArray(s.layouts) && s.layouts.length ? s.layouts : [{ id: 'base', name: 'Layout 1', items: room ? (room.items || []) : [] }];
  const active = s.activeLayout && ls.some(l => l.id === s.activeLayout) ? s.activeLayout : ls[0].id;
  return { layouts: ls, active };
}
// Room with the current items written into the active layout entry.
function rmWithCurrentSaved(room) {
  const { layouts, active } = rmLayouts(room);
  const ls = layouts.map(l => l.id === active ? { ...l, items: room.items || [] } : l);
  return { ...room, settings: { ...(room.settings || {}), layouts: ls, activeLayout: active } };
}

// ── Arrangement engine ──────────────────────────────────────────────────
// Greedy placement with scoring, run many times with jitter to produce a
// few distinct, valid arrangements. Knows about: walls (things go flush),
// corners (big items like them), door swings (keep clear), windows (don't
// put a cot under one; chairs like them), and companions (change table /
// drawers near the cot). Only rectilinear rooms are considered — that's
// every room this editor draws.
function rmPointInPoly(pt, poly) {
  let inside = false;
  for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
    const xi = poly[i].x, yi = poly[i].y, xj = poly[j].x, yj = poly[j].y;
    const hit = ((yi > pt.y) !== (yj > pt.y)) && (pt.x < ((xj - xi) * (pt.y - yi)) / ((yj - yi) || 1e-9) + xi);
    if (hit) inside = !inside;
  }
  return inside;
}
function rmBoxInPoly(b, poly) {
  const e = 0.5;
  const pts = [
    { x: b.x0 + e, y: b.y0 + e }, { x: b.x1 - e, y: b.y0 + e }, { x: b.x0 + e, y: b.y1 - e }, { x: b.x1 - e, y: b.y1 - e },
    { x: (b.x0 + b.x1) / 2, y: b.y0 + e }, { x: (b.x0 + b.x1) / 2, y: b.y1 - e }, { x: b.x0 + e, y: (b.y0 + b.y1) / 2 }, { x: b.x1 - e, y: (b.y0 + b.y1) / 2 },
    { x: (b.x0 + b.x1) / 2, y: (b.y0 + b.y1) / 2 },
  ];
  return pts.every(p => rmPointInPoly(p, poly));
}
const rmOverlap = (a, b, tol = 0.5) => a.x0 < b.x1 - tol && a.x1 > b.x0 + tol && a.y0 < b.y1 - tol && a.y1 > b.y0 + tol;
function rmItemKind(name) {
  const n = String(name || '').toLowerCase();
  if (/\b(cot|crib|bassinet|cradle|co-?sleeper|bed)\b/.test(n)) return 'cot';
  if (/change|changing|drawer|dresser|chest|tallboy|wardrobe|shelf|shelves|bookcase|storage/.test(n)) return 'storage';
  if (/rocker|glider|chair|armchair|feeding|nursing|ottoman|sofa|couch/.test(n)) return 'chair';
  if (/desk|table|monitor/.test(n)) return 'desk';
  if (/rug|mat|playmat|play mat/.test(n)) return 'rug';
  return 'other';
}
// Small anchored dropdown used by the rooms command bar. Closes on
// outside click / Escape. `render(close)` returns the menu body.
function RoomsMenu({ label, title, className = '', align = 'left', isOn = false, render }) {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDoc); document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
  }, [open]);
  return (
    <div className={`rooms-menu${open ? ' is-open' : ''}`} ref={ref}>
      <button type="button" className={`rooms-btn${isOn || open ? ' is-on' : ''} ${className}`} title={title} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(v => !v)}>{label}<span className="rooms-menu-caret">▾</span></button>
      {open && <div className={`rooms-menu-pop rooms-menu-pop--${align}`} role="menu">{render(() => setOpen(false))}</div>}
    </div>
  );
}

function rmSuggestLayouts(room, count = 4) {
  const walls = room.walls || [];
  const items = (room.items || []).filter(it => it.w > 0 && it.d > 0);
  const openings = room.openings || [];
  if (walls.length < 3 || items.length === 0) return [];
  const T = 0.5;

  // Door clearance zones + window spans (as AABBs / wall refs).
  const doorZones = [];
  const windowWalls = new Map(); // wall idx → [{s,e}]
  openings.forEach(op => {
    if (op.wall >= walls.length) return;
    const f = rmWallFrame(walls, op.wall, op.offset), g = rmWallFrame(walls, op.wall, op.offset + op.width);
    if (op.type === 'door') {
      const depth = (op.style === 'saloon' || op.style === 'double') ? op.width / 2 : op.width;
      const sgn = op.swing === 'out' ? -1 : 1;
      const pts = [f, g, { x: g.x + f.nx * sgn * depth, y: g.y + f.ny * sgn * depth }, { x: f.x + f.nx * sgn * depth, y: f.y + f.ny * sgn * depth }];
      const xs = pts.map(p => p.x), ys = pts.map(p => p.y);
      // Also keep a small walkway in front of the door even if it swings out.
      const walk = 40;
      const wp = [f, g, { x: g.x + f.nx * walk, y: g.y + f.ny * walk }, { x: f.x + f.nx * walk, y: f.y + f.ny * walk }];
      doorZones.push({ x0: Math.min(...xs, ...wp.map(p => p.x)), y0: Math.min(...ys, ...wp.map(p => p.y)), x1: Math.max(...xs, ...wp.map(p => p.x)), y1: Math.max(...ys, ...wp.map(p => p.y)) });
    } else {
      (windowWalls.get(op.wall) || windowWalls.set(op.wall, []).get(op.wall)).push({ s: op.offset, e: op.offset + op.width });
    }
  });

  // Candidate boxes flush against each axis-aligned wall.
  const wallCandidates = (bw, bh, rnd) => {
    const out = [];
    for (let i = 0; i < walls.length; i++) {
      const a = walls[i], b = walls[(i + 1) % walls.length];
      const horiz = Math.abs(a.y - b.y) < 0.01, vert = Math.abs(a.x - b.x) < 0.01;
      if (!horiz && !vert) continue;
      const f = rmWallFrame(walls, i, 0);
      const along = horiz ? bw : bh, deep = horiz ? bh : bw;
      if (f.len < along - 0.01) continue;
      const ts = [0, f.len - along, (f.len - along) / 2];
      for (let k = 0; k < 3; k++) ts.push(rnd() * (f.len - along));
      for (const t of ts) {
        const s = rmWallFrame(walls, i, t);
        const e = rmWallFrame(walls, i, t + along);
        const inn = { x: s.x + f.nx * deep, y: s.y + f.ny * deep };
        const xs = [s.x, e.x, inn.x, e.x + f.nx * deep], ys = [s.y, e.y, inn.y, e.y + f.ny * deep];
        const box = { x0: Math.min(...xs), y0: Math.min(...ys), x1: Math.max(...xs), y1: Math.max(...ys) };
        const atStart = t <= 0.01, atEnd = t >= f.len - along - 0.01;
        const win = (windowWalls.get(i) || []).some(w => w.s < t + along && w.e > t);
        out.push({ box, wall: i, corner: atStart || atEnd, underWindow: win });
      }
    }
    return out;
  };

  const solve = (seed) => {
    let s = seed;
    const rnd = () => { s = (s * 1664525 + 1013904223) % 4294967296; return s / 4294967296; };
    const order = [...items].sort((a, b) => (b.w * b.d) - (a.w * a.d) + (rnd() - 0.5) * 2000);
    const placed = [];
    let total = 0;
    for (const it of order) {
      const kind = rmItemKind(it.name);
      const orients = [{ rot: 0, bw: it.w, bh: it.d }, { rot: 90, bw: it.d, bh: it.w }];
      let best = null;
      for (const o of orients) {
        for (const c of wallCandidates(o.bw, o.bh, rnd)) {
          const b = c.box;
          if (!rmBoxInPoly(b, walls)) continue;
          if (placed.some(p => rmOverlap(b, p.box))) continue;
          if (doorZones.some(z => rmOverlap(b, z))) continue;
          let sc = 3 + (c.corner ? 2 : 0) + rnd() * 1.2;
          if (kind === 'cot' && c.underWindow) sc -= 4;
          if (kind === 'chair' && c.underWindow) sc += 1.5;
          if (kind === 'rug') sc -= 2;               // rugs shouldn't hug walls
          if (kind === 'cot' && !c.corner) sc -= 0.8;
          if (kind === 'storage') {
            const cot = placed.find(p => p.kind === 'cot');
            if (cot) {
              const dx = Math.max(0, Math.max(b.x0, cot.box.x0) - Math.min(b.x1, cot.box.x1));
              const dy = Math.max(0, Math.max(b.y0, cot.box.y0) - Math.min(b.y1, cot.box.y1));
              const gap = Math.hypot(dx, dy);
              if (gap < 80) sc += 1.5; else if (gap > 200) sc -= 0.5;
            }
          }
          if (kind === 'chair') {
            const cot = placed.find(p => p.kind === 'cot');
            if (cot) { const gap = Math.hypot(Math.max(0, Math.max(b.x0, cot.box.x0) - Math.min(b.x1, cot.box.x1)), Math.max(0, Math.max(b.y0, cot.box.y0) - Math.min(b.y1, cot.box.y1))); if (gap < 120) sc += 0.8; }
          }
          if (!best || sc > best.sc) best = { box: b, rot: o.rot, sc };
        }
      }
      if (!best) {
        // Free-floating fallback: scan the interior on a 10 cm grid.
        const bb = rmBounds(walls);
        outer: for (const o of orients) {
          for (let y = bb.minY; y + o.bh <= bb.maxY; y += 10) {
            for (let x = bb.minX; x + o.bw <= bb.maxX; x += 10) {
              const b = { x0: x, y0: y, x1: x + o.bw, y1: y + o.bh };
              if (!rmBoxInPoly(b, walls) || placed.some(p => rmOverlap(b, p.box)) || doorZones.some(z => rmOverlap(b, z))) continue;
              best = { box: b, rot: o.rot, sc: kind === 'rug' ? 2 : 0.5 };
              break outer;
            }
          }
        }
      }
      if (!best) return null;
      placed.push({ id: it.id, box: best.box, rot: best.rot, kind, item: it });
      total += best.sc;
    }
    const out = placed.map(p => {
      const cx = (p.box.x0 + p.box.x1) / 2, cy = (p.box.y0 + p.box.y1) / 2;
      return { ...p.item, rot: p.rot, x: Math.round((cx - p.item.w / 2) * 10) / 10, y: Math.round((cy - p.item.d / 2) * 10) / 10 };
    });
    return { items: out, score: total };
  };

  const seen = new Set();
  const results = [];
  for (let k = 1; k <= 40 && results.length < count * 4; k++) {
    const r = solve(k * 7919 + items.length);
    if (!r) continue;
    const sig = r.items.map(i => `${i.id}:${Math.round(i.x / 10)}:${Math.round(i.y / 10)}:${i.rot}`).sort().join('|');
    if (seen.has(sig)) continue;
    seen.add(sig);
    results.push(r);
  }
  results.sort((a, b) => b.score - a.score);
  // Prefer variety: greedily pick results that differ in where the biggest item sits.
  const picked = [];
  const bigId = [...items].sort((a, b) => (b.w * b.d) - (a.w * a.d))[0].id;
  for (const r of results) {
    const big = r.items.find(i => i.id === bigId);
    const key = big ? `${Math.round(big.x / 40)}:${Math.round(big.y / 40)}:${big.rot}` : '';
    if (picked.some(p => p.key === key) && picked.length < count) continue;
    picked.push({ ...r, key });
    if (picked.length >= count) break;
  }
  if (picked.length < count) for (const r of results) { if (picked.length >= count) break; if (!picked.includes(r)) picked.push(r); }
  return picked.slice(0, count).map(r => r.items);
}

// Tiny read-only rendering of a room + items (suggestion thumbnails).
function RoomThumb({ walls, items, openings, width = 170 }) {
  const b = rmBounds(walls);
  const pad = 14;
  const vb = `${b.minX - pad} ${b.minY - pad} ${(b.maxX - b.minX) + pad * 2} ${(b.maxY - b.minY) + pad * 2}`;
  return (
    <svg viewBox={vb} width={width} className="rooms-thumb" preserveAspectRatio="xMidYMid meet">
      <polygon points={walls.map(p => `${p.x},${p.y}`).join(' ')} fill="none" stroke="#1c1a14" strokeWidth={RM_WALL_T * 2} strokeLinejoin="miter" />
      <polygon points={walls.map(p => `${p.x},${p.y}`).join(' ')} fill="#fdf8ee" />
      {(openings || []).map(op => {
        if (op.wall >= walls.length) return null;
        const f = rmWallFrame(walls, op.wall, op.offset), g = rmWallFrame(walls, op.wall, op.offset + op.width);
        return <polygon key={op.id} points={`${f.x},${f.y} ${g.x},${g.y} ${g.x - f.nx * (RM_WALL_T + 0.5)},${g.y - f.ny * (RM_WALL_T + 0.5)} ${f.x - f.nx * (RM_WALL_T + 0.5)},${f.y - f.ny * (RM_WALL_T + 0.5)}`} fill={op.type === 'door' ? '#f5efe1' : '#cfe0f5'} />;
      })}
      {(items || []).map(it => {
        const cx = it.x + it.w / 2, cy = it.y + it.d / 2;
        const fs = Math.max(8, Math.min(14, Math.min(it.w, it.d) / 3.2));
        const label = String(it.name || '').split(/\s+/).slice(0, 2).join(' ');
        return (
          <g key={it.id} transform={`rotate(${it.rot || 0} ${cx} ${cy})`}>
            <rect x={it.x} y={it.y} width={it.w} height={it.d} rx="2" fill={it.color || '#d9a066'} fillOpacity="0.85" stroke="rgba(28,26,20,0.5)" strokeWidth="1" />
            <text x={cx} y={cy + fs * 0.35} textAnchor="middle" fontSize={fs} fill="#1c1a14" style={{ fontWeight: 600 }}>{label}</text>
          </g>
        );
      })}
    </svg>
  );
}
const RM_HISTORY_CAP = 120;

// ── Page ────────────────────────────────────────────────────────────────
function RoomsPage({ userLists, productMap, onOpenProduct, myUserId, onOpenList, onOpenItemInList }) {
  const [rooms, setRooms] = _rm_s([]);
  const [activeId, setActiveId] = _rm_s(null);
  const [loading, setLoading] = _rm_s(true);
  const [err, setErr] = _rm_s('');
  const [sel, setSel] = _rm_s(null);            // { kind: 'vertex'|'item'|'opening', id|idx }
  const [tool, setTool] = _rm_s('select');      // 'select' | 'addcorner'
  const [addOpen, setAddOpen] = _rm_s(null);    // null | 'custom' | 'list' | 'opening'
  const [saveState, setSaveState] = _rm_s('idle');
  const [zoom, setZoom] = _rm_s(1);
  const [showHistory, setShowHistory] = _rm_s(false);
  const [shareOpen, setShareOpen] = _rm_s(false);
  const [histTick, setHistTick] = _rm_s(0);
  const svgRef = _rm_r(null);
  const dragRef = _rm_r(null);
  const saveTimer = _rm_r(null);
  const histRef = _rm_r({});   // roomId → { past: [...], future: [...] }

  const room = _rm_m(() => rooms.find(r => r.id === activeId) || null, [rooms, activeId]);

  // Load rooms (claim any pending invites first); seed the default one on
  // first visit. On a hard refresh this page can mount before the
  // Supabase client / session exist, so wait for them (up to ~10s) and
  // re-run whenever the signed-in user id arrives.
  _rm_e(() => {
    let cancelled = false;
    (async () => {
      setErr('');
      setLoading(true);
      const ready = async () => {
        if (!window.MR || !window.MR.rooms || !window.MR.supabase) return false;
        try {
          const s = window.MR.user && window.MR.user._session;
          if (s && s.user) return true;
          const { data } = await window.MR.supabase.auth.getSession();
          return !!(data && data.session && data.session.user);
        } catch { return false; }
      };
      for (let i = 0; i < 50 && !cancelled; i++) {
        if (await ready()) break;
        await new Promise(r => setTimeout(r, 200));
      }
      if (cancelled) return;
      if (!(await ready())) {
        // Genuinely signed out (or auth never came up) — don't show a
        // scary error, just say what's needed.
        setErr('Sign in to see your rooms.');
        setLoading(false);
        return;
      }
      try { await window.MR.rooms.claimInvites(); } catch {}
      let res = await window.MR.rooms.list();
      // One retry for the transient "no-client / signed-out" race.
      if (!res.ok && /no-client|signed-out/i.test(res.reason || '')) {
        await new Promise(r => setTimeout(r, 400));
        res = await window.MR.rooms.list();
      }
      if (cancelled) return;
      if (!res.ok) { setErr(res.reason || 'Could not load rooms'); setLoading(false); return; }
      let list = res.rooms;
      if (list.length === 0) {
        const c = await window.MR.rooms.create(RM_DEFAULT_ROOM);
        if (c.ok) list = [c.room];
      }
      setRooms(list);
      setActiveId(prev => prev && list.some(r => r.id === prev) ? prev : (list[0] ? list[0].id : null));
      setLoading(false);
    })();
    return () => { cancelled = true; };
  }, [myUserId]);

  // ── History ──────────────────────────────────────────────────────────
  const histFor = (id) => (histRef.current[id] = histRef.current[id] || { past: [], future: [] });
  const pushHistory = (prevRoom, label, coalesce) => {
    const h = histFor(prevRoom.id);
    const last = h.past[h.past.length - 1];
    if (coalesce && last && last.coalesce === coalesce && Date.now() - last.ts < 2500) {
      last.ts = Date.now();
      return;
    }
    h.past.push({ label, ts: Date.now(), snap: rmSnapshot(prevRoom), coalesce: coalesce || null });
    if (h.past.length > RM_HISTORY_CAP) h.past.splice(0, h.past.length - RM_HISTORY_CAP);
    h.future = [];
    setHistTick(t => t + 1);
  };

  // Local mutate + history + debounced save.
  const patchRoom = _rm_cb((fn, label = 'Edit', coalesce = null) => {
    setRooms(prev => prev.map(r => {
      if (r.id !== activeId) return r;
      const next = fn(r);
      if (next === r) return r;
      pushHistory(r, label, coalesce);
      return { ...next, __dirty: true };
    }));
  }, [activeId]);

  const applySnapshot = (id, snap) => {
    setRooms(prev => prev.map(r => r.id === id ? { ...r, ...snap, __dirty: true } : r));
  };
  const undo = () => {
    if (!room) return;
    const h = histFor(room.id);
    const e = h.past.pop();
    if (!e) return;
    h.future.push({ label: e.label, ts: Date.now(), snap: rmSnapshot(room) });
    applySnapshot(room.id, e.snap);
    setSel(null);
    setHistTick(t => t + 1);
  };
  const redo = () => {
    if (!room) return;
    const h = histFor(room.id);
    const e = h.future.pop();
    if (!e) return;
    h.past.push({ label: e.label, ts: Date.now(), snap: rmSnapshot(room), coalesce: null });
    applySnapshot(room.id, e.snap);
    setSel(null);
    setHistTick(t => t + 1);
  };
  const revertTo = (index) => {
    // Undo until `index` entries remain — i.e. return to the state
    // BEFORE history entry #index was made.
    if (!room) return;
    const h = histFor(room.id);
    if (index < 0 || index >= h.past.length) return;
    let cur = room;
    while (h.past.length > index) {
      const e = h.past.pop();
      h.future.push({ label: e.label, ts: Date.now(), snap: rmSnapshot(cur) });
      cur = { ...cur, ...e.snap };
    }
    applySnapshot(room.id, rmSnapshot(cur));
    setSel(null);
    setHistTick(t => t + 1);
  };

  _rm_e(() => {
    const dirty = rooms.filter(r => r.__dirty);
    if (!dirty.length) return;
    if (saveTimer.current) clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(async () => {
      setSaveState('saving');
      let ok = true;
      for (const r of dirty) {
        const res = await window.MR.rooms.save(r.id, { name: r.name, walls: r.walls, openings: r.openings, items: r.items, settings: r.settings || {} });
        if (!res.ok) ok = false;
      }
      setRooms(prev => prev.map(r => r.__dirty ? { ...r, __dirty: false } : r));
      setSaveState(ok ? 'saved' : 'error');
      setTimeout(() => setSaveState(s => s === 'saved' ? 'idle' : s), 1500);
    }, 700);
    return () => { if (saveTimer.current) clearTimeout(saveTimer.current); };
  }, [rooms]);

  // ── Viewport ─────────────────────────────────────────────────────────
  const walls = room ? (room.walls || []) : [];
  const b = rmBounds(walls);
  const PAD = 60;
  const vbW = (b.maxX - b.minX) + PAD * 2, vbH = (b.maxY - b.minY) + PAD * 2;
  const viewBox = `${b.minX - PAD} ${b.minY - PAD} ${vbW / zoom} ${vbH / zoom}`;

  const toRoom = (evt) => {
    const svg = svgRef.current;
    if (!svg) return { x: 0, y: 0 };
    const pt = svg.createSVGPoint();
    pt.x = evt.clientX; pt.y = evt.clientY;
    const ctm = svg.getScreenCTM();
    if (!ctm) return { x: 0, y: 0 };
    const p = pt.matrixTransform(ctm.inverse());
    return { x: p.x, y: p.y };
  };

  // ── Drag handling (vertices + items + openings) ──────────────────────
  const startDrag = (evt, payload) => {
    if (evt.button !== 0) return;
    evt.stopPropagation();
    const p = toRoom(evt);
    const session = `drag:${payload.kind}:${payload.id ?? payload.idx}:${Date.now()}`;
    dragRef.current = { ...payload, startX: p.x, startY: p.y, moved: false, session };
    const onMove = (e) => {
      const d = dragRef.current; if (!d) return;
      const q = toRoom(e);
      const dx = q.x - d.startX, dy = q.y - d.startY;
      if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) d.moved = true;
      if (d.kind === 'vertex') {
        patchRoom(r => ({ ...r, walls: r.walls.map((v, i) => i === d.idx ? { x: rmSnap(d.ox + dx), y: rmSnap(d.oy + dy) } : v) }), `Move corner ${d.idx + 1}`, d.session);
      } else if (d.kind === 'item') {
        const mode = e.altKey ? 'off' : (e.shiftKey ? 'strong' : 'near');
        patchRoom(r => ({ ...r, items: r.items.map(it => {
          if (it.id !== d.id) return it;
          const base = { x: rmSnap(d.ox + dx), y: rmSnap(d.oy + dy) };
          const sn = snapItemToSurroundings(it, base.x, base.y, mode);
          return { ...it, x: sn.x, y: sn.y };
        }) }), `Move ${d.label || 'item'}`, d.session);
      } else if (d.kind === 'rotate') {
        // Angle of the pointer around the item centre; handle sits "up"
        // so +90 makes the handle track the cursor. 15° steps, magnetic
        // to right angles; Shift for 1° precision.
        let ang = (Math.atan2(q.y - d.cy, q.x - d.cx) * 180) / Math.PI + 90;
        ang = ((ang % 360) + 360) % 360;
        if (!e.shiftKey) {
          const near90 = Math.round(ang / 90) * 90;
          ang = Math.abs(ang - near90) <= 6 ? near90 : Math.round(ang / 15) * 15;
        } else ang = Math.round(ang);
        ang = ((ang % 360) + 360) % 360;
        patchRoom(r => ({ ...r, items: r.items.map(it => it.id === d.id ? { ...it, rot: ang } : it) }), `Rotate ${d.label || 'item'}`, d.session);
      } else if (d.kind === 'resize') {
        // Work in the item's local (unrotated) frame; the corner opposite
        // the handle stays fixed on screen.
        const lp = rotV(q.x - d.cx, q.y - d.cy, -d.orot);
        const sx = d.handle.includes('e') ? 1 : -1, sy = d.handle.includes('s') ? 1 : -1;
        const ax = -sx * d.ow / 2, ay = -sy * d.od / 2;            // anchor (opposite corner), local
        let nw = Math.abs(lp.x - ax), nd = Math.abs(lp.y - ay);
        nw = Math.max(5, e.shiftKey ? Math.round(nw) : rmSnap(nw));
        nd = Math.max(5, e.shiftKey ? Math.round(nd) : rmSnap(nd));
        const cl = { x: ax + sx * nw / 2, y: ay + sy * nd / 2 };  // new centre, local
        const cs = rotV(cl.x, cl.y, d.orot);
        const ncx = d.cx + cs.x, ncy = d.cy + cs.y;
        const next = { w: nw, d: nd, x: Math.round((ncx - nw / 2) * 10) / 10, y: Math.round((ncy - nd / 2) * 10) / 10 };
        d.last = next;
        patchRoom(r => ({ ...r, items: r.items.map(it => it.id === d.id ? { ...it, ...next } : it) }), `Resize ${d.label || 'item'}`, d.session);
      } else if (d.kind === 'opening') {
        patchRoom(r => ({ ...r, openings: r.openings.map(op => {
          if (op.id !== d.id) return op;
          const f = rmWallFrame(r.walls, op.wall, 0);
          const along = (q.x - r.walls[op.wall].x) * f.ux + (q.y - r.walls[op.wall].y) * f.uy;
          const off = Math.max(0, Math.min(f.len - op.width, rmSnap(along - op.width / 2)));
          return { ...op, offset: off };
        }) }), `Move ${d.label || 'opening'}`, d.session);
      }
    };
    const onUp = () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
      const d = dragRef.current;
      if (d && d.kind === 'resize' && d.last && d.item && d.item.nodeId) {
        persistFootprint({ ...d.item, w: d.last.w, d: d.last.d });
      }
      dragRef.current = null;
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
  };

  // ── Wall ops ─────────────────────────────────────────────────────────
  const setWallLength = (i, len) => {
    const n = parseFloat(len);
    if (!Number.isFinite(n) || n <= 0) return;
    patchRoom(r => {
      const a = r.walls[i], bb = r.walls[(i + 1) % r.walls.length];
      const cur = rmDist(a, bb) || 1;
      const ux = (bb.x - a.x) / cur, uy = (bb.y - a.y) / cur;
      const nb = { x: Math.round((a.x + ux * n) * 10) / 10, y: Math.round((a.y + uy * n) * 10) / 10 };
      return { ...r, walls: r.walls.map((v, j) => j === (i + 1) % r.walls.length ? nb : v) };
    }, `Wall ${i + 1} → ${n} cm`);
  };
  const insertCorner = (i, at) => {
    patchRoom(r => {
      const w = [...r.walls];
      w.splice(i + 1, 0, { x: rmSnap(at.x), y: rmSnap(at.y) });
      const ops = (r.openings || []).map(op => op.wall > i ? { ...op, wall: op.wall + 1 } : op);
      return { ...r, walls: w, openings: ops };
    }, 'Add corner');
  };
  const deleteVertex = (idx) => {
    patchRoom(r => {
      if (r.walls.length <= 3) return r;
      const w = r.walls.filter((_, j) => j !== idx);
      const ops = (r.openings || []).filter(op => op.wall !== idx && op.wall !== idx - 1).map(op => op.wall > idx ? { ...op, wall: op.wall - 1 } : op);
      return { ...r, walls: w, openings: ops };
    }, `Remove corner ${idx + 1}`);
    setSel(null);
  };

  // ── Item ops ─────────────────────────────────────────────────────────
  const addItem = (it) => {
    const centre = { x: (b.minX + b.maxX) / 2, y: (b.minY + b.maxY) / 2 };
    const item = {
      id: rmUid(), name: it.name || 'Item', color: it.color || RM_COLORS[0],
      w: Math.max(5, rmSnap(it.w || 60)), d: Math.max(5, rmSnap(it.d || 60)),
      x: rmSnap(centre.x - (it.w || 60) / 2), y: rmSnap(centre.y - (it.d || 60) / 2),
      rot: 0, nodeId: it.nodeId || null, productId: it.productId || null, image: it.image || null, listName: it.listName || null, listId: it.listId || null,
    };
    patchRoom(r => ({ ...r, items: [...(r.items || []), item] }), `Add ${item.name}`);
    setSel({ kind: 'item', id: item.id });
    setAddOpen(null);
  };
  const updateItem = (id, patch, label, coalesceKey) => {
    const it = room && room.items.find(x => x.id === id);
    const nm = (it && it.name) || 'item';
    patchRoom(r => ({ ...r, items: r.items.map(x => x.id === id ? { ...x, ...patch } : x) }), label || `Edit ${nm}`, coalesceKey || null);
  };
  const removeItem = (id) => {
    const it = room && room.items.find(x => x.id === id);
    patchRoom(r => ({ ...r, items: r.items.filter(x => x.id !== id) }), `Remove ${(it && it.name) || 'item'}`);
    setSel(null);
  };
  const duplicateItem = (id) => {
    const src = room && room.items.find(it => it.id === id); if (!src) return;
    const copy = { ...src, id: rmUid(), x: src.x + 20, y: src.y + 20 };
    patchRoom(r => ({ ...r, items: [...r.items, copy] }), `Duplicate ${src.name}`);
    setSel({ kind: 'item', id: copy.id });
  };

  // ── Opening ops ──────────────────────────────────────────────────────
  const addOpening = (type, style = 'single') => {
    const op = { id: rmUid(), type, style: type === 'door' ? style : undefined, wall: 0, offset: 20, width: type === 'door' ? (style === 'saloon' ? 96 : 82) : 90, swing: 'in' };
    patchRoom(r => ({ ...r, openings: [...(r.openings || []), op] }), `Add ${style === 'saloon' ? 'saloon door' : type}`);
    setSel({ kind: 'opening', id: op.id });
    setAddOpen(null);
  };
  const updateOpening = (id, patch, label, coalesceKey) => {
    const op = room && (room.openings || []).find(o => o.id === id);
    patchRoom(r => ({ ...r, openings: r.openings.map(o => o.id === id ? { ...o, ...patch } : o) }), label || `Edit ${(op && op.type) || 'opening'}`, coalesceKey || null);
  };
  const removeOpening = (id) => {
    const op = room && (room.openings || []).find(o => o.id === id);
    patchRoom(r => ({ ...r, openings: r.openings.filter(o => o.id !== id) }), `Remove ${(op && op.type) || 'opening'}`);
    setSel(null);
  };

  // ── Room ops ─────────────────────────────────────────────────────────
  const createRoom = async () => {
    const name = window.prompt('Room name:', 'New room');
    if (name === null) return;
    const res = await window.MR.rooms.create({
      name: name.trim() || 'New room',
      walls: [{ x: 0, y: 0 }, { x: 400, y: 0 }, { x: 400, y: 300 }, { x: 0, y: 300 }],
      openings: [], items: [], settings: { grid: 10 }, position: rooms.length,
    });
    if (res.ok) { setRooms(prev => [...prev, res.room]); setActiveId(res.room.id); setSel(null); }
  };
  const renameRoom = () => {
    if (!room) return;
    const name = window.prompt('Room name:', room.name);
    if (name === null || !name.trim()) return;
    patchRoom(r => ({ ...r, name: name.trim() }), 'Rename room');
  };
  const deleteRoom = async () => {
    if (!room) return;
    if (!window.confirm(`Delete "${room.name}"? This removes its layout and placed items.`)) return;
    const res = await window.MR.rooms.remove(room.id);
    if (res.ok) {
      const rest = rooms.filter(r => r.id !== room.id);
      setRooms(rest); setActiveId(rest[0] ? rest[0].id : null); setSel(null);
    }
  };

  // Keyboard: undo/redo, delete, rotate, nudge.
  _rm_e(() => {
    const onKey = (e) => {
      if (['INPUT', 'TEXTAREA', 'SELECT'].includes((e.target && e.target.tagName) || '')) return;
      const mod = e.metaKey || e.ctrlKey;
      if (mod && (e.key === 'z' || e.key === 'Z')) { e.preventDefault(); if (e.shiftKey) redo(); else undo(); return; }
      if (mod && (e.key === 'y' || e.key === 'Y')) { e.preventDefault(); redo(); return; }
      if (!sel) return;
      if (e.key === 'Escape') { setSel(null); return; }
      if (e.key === 'Delete' || e.key === 'Backspace') {
        e.preventDefault();
        if (sel.kind === 'item') removeItem(sel.id);
        else if (sel.kind === 'vertex') deleteVertex(sel.idx);
        else if (sel.kind === 'opening') {
          const op = room && (room.openings || []).find(o => o.id === sel.id);
          if (op && window.confirm(`Remove this ${op.type}? (⌘Z undoes)`)) removeOpening(sel.id);
        }
      }
      if ((e.key === 'r' || e.key === 'R') && sel.kind === 'item') {
        const it = room && room.items.find(x => x.id === sel.id);
        if (it) updateItem(it.id, { rot: ((it.rot || 0) + 90) % 360 }, `Rotate ${it.name}`);
      }
      if (e.key.startsWith('Arrow') && sel.kind === 'item') {
        e.preventDefault();
        const step = e.shiftKey ? 1 : RM_SNAP;
        const dx = e.key === 'ArrowLeft' ? -step : e.key === 'ArrowRight' ? step : 0;
        const dy = e.key === 'ArrowUp' ? -step : e.key === 'ArrowDown' ? step : 0;
        const it = room && room.items.find(x => x.id === sel.id);
        if (it) updateItem(it.id, { x: it.x + dx, y: it.y + dy }, `Nudge ${it.name}`, `nudge:${it.id}`);
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [sel, room]);

  // ── Searchable pool of list items ───────────────────────────────────
  const listItemsPool = _rm_m(() => {
    const out = [];
    (userLists || []).forEach(l => {
      (l.items || []).forEach(it => {
        const p = it.productId && productMap ? productMap[it.productId] : null;
        const c = it.custom || {};
        const name = (p && p.name) || c.name || it.name || 'Item';
        const dims = (p && p.dimensions) || c.dimensions || {};
        const fp = (it.metadata && it.metadata.footprint) || null;   // saved from a room
        const ex = rmFootprint((it.metadata && it.metadata.dimensions) || dims);
        out.push({
          key: it.id, nodeId: it.id, productId: it.productId || null, listId: l.id, listName: l.name,
          name, brand: (p && p.brand) || c.brand || '',
          image: (p && p.img) || c.image || null,
          placeholder: !!c.placeholder,
          metadata: it.metadata || {},
          custom: c,
          w: (fp && Number(fp.w)) || ex.w || null,
          d: (fp && Number(fp.d)) || ex.d || null,
        });
      });
    });
    return out;
  }, [userLists, productMap]);
  const poolById = _rm_m(() => { const m = {}; listItemsPool.forEach(p => { m[p.nodeId] = p; }); return m; }, [listItemsPool]);

  // Size edits on a placed item that came from a list write back to that
  // list item (metadata.footprint) so the next placement — or Lucie's —
  // starts with the right footprint. Debounced per node.
  const fpTimers = _rm_r({});
  const persistFootprint = (item) => {
    if (!item.nodeId || !window.MR || !window.MR.nodes) return;
    const key = item.nodeId;
    if (fpTimers.current[key]) clearTimeout(fpTimers.current[key]);
    fpTimers.current[key] = setTimeout(async () => {
      const base = (poolById[key] && poolById[key].metadata) || {};
      await window.MR.nodes.updateNode(key, { metadata: { ...base, footprint: { w: item.w, d: item.d } } });
    }, 900);
  };

  // ── Layout variations ───────────────────────────────────────────────
  const { layouts, active: activeLayout } = rmLayouts(room);
  const [suggestions, setSuggestions] = _rm_s(null);   // null | [items[]]
  const [suggestSeed, setSuggestSeed] = _rm_s(0);
  const switchLayout = (id) => {
    if (!room || id === activeLayout) return;
    patchRoom(r => {
      const saved = rmWithCurrentSaved(r);
      const target = rmLayouts(saved).layouts.find(l => l.id === id);
      if (!target) return r;
      return { ...saved, items: target.items || [], settings: { ...saved.settings, activeLayout: id } };
    }, 'Switch layout');
    setSel(null);
  };
  const addLayout = (items, name) => {
    if (!room) return;
    const id = rmUid();
    patchRoom(r => {
      const saved = rmWithCurrentSaved(r);
      const ls = rmLayouts(saved).layouts;
      const nm = name || `Layout ${ls.length + 1}`;
      return { ...saved, items: items.map(it => ({ ...it })), settings: { ...saved.settings, layouts: [...ls, { id, name: nm, items: items.map(it => ({ ...it })) }], activeLayout: id } };
    }, `New layout${name ? ` "${name}"` : ''}`);
    setSel(null);
  };
  const duplicateLayout = () => addLayout(room ? (room.items || []) : []);
  const renameLayout = (id) => {
    const cur = layouts.find(l => l.id === id);
    const name = window.prompt('Layout name:', cur ? cur.name : '');
    if (name === null || !name.trim()) return;
    patchRoom(r => { const saved = rmWithCurrentSaved(r); return { ...saved, settings: { ...saved.settings, layouts: rmLayouts(saved).layouts.map(l => l.id === id ? { ...l, name: name.trim() } : l) } }; }, 'Rename layout');
  };
  const [layoutDrag, setLayoutDrag] = _rm_s(null);   // { id, overId, pos }
  const reorderLayouts = (fromId, toId, pos) => {
    if (!fromId || !toId || fromId === toId) return;
    patchRoom(r => {
      const saved = rmWithCurrentSaved(r);
      const ls = [...rmLayouts(saved).layouts];
      const fi = ls.findIndex(l => l.id === fromId); if (fi < 0) return r;
      const [moved] = ls.splice(fi, 1);
      let ti = ls.findIndex(l => l.id === toId); if (ti < 0) return r;
      if (pos === 'after') ti += 1;
      ls.splice(ti, 0, moved);
      return { ...saved, settings: { ...saved.settings, layouts: ls } };
    }, 'Reorder layouts');
  };
  const deleteLayout = (id) => {
    if (layouts.length <= 1) return;
    const cur = layouts.find(l => l.id === id);
    if (!window.confirm(`Delete layout "${cur ? cur.name : ''}"? The other layouts are untouched.`)) return;
    patchRoom(r => {
      const saved = rmWithCurrentSaved(r);
      const ls = rmLayouts(saved).layouts.filter(l => l.id !== id);
      const nextActive = rmLayouts(saved).active === id ? ls[0].id : rmLayouts(saved).active;
      const target = ls.find(l => l.id === nextActive);
      return { ...saved, items: target ? target.items : saved.items, settings: { ...saved.settings, layouts: ls, activeLayout: nextActive } };
    }, 'Delete layout');
    setSel(null);
  };
  const runSuggest = () => {
    if (!room) return;
    const seed = suggestSeed + 1;
    setSuggestSeed(seed);
    // Rotate the item order per run so "More" gives fresh variety.
    const rotated = { ...room, items: [...(room.items || []).slice(seed % Math.max(1, (room.items || []).length)), ...(room.items || []).slice(0, seed % Math.max(1, (room.items || []).length))] };
    const out = rmSuggestLayouts(rotated, 4);
    setSuggestions(out);
    if (!out.length) window.__mr_showToast && window.__mr_showToast(room.items.length ? "Couldn't find arrangements that fit — try fewer / smaller items" : 'Add some items first');
  };

  // Open the standard item modal right here (no navigation). Catalog
  // items pass the product straight through; custom items get a
  // synthesised product from their list blob, same shape the list page
  // builds.
  const openItemModal = (item) => {
    if (!onOpenProduct) return;
    if (item.productId && productMap && productMap[item.productId]) { onOpenProduct(productMap[item.productId]); return; }
    const pe = item.nodeId ? poolById[item.nodeId] : null;
    const c = (pe && pe.custom) || {};
    const sourceUrl = c.sourceUrl || c.source_url || c.url || '';
    let host = ''; try { host = new URL(sourceUrl).hostname.replace(/^www\./, ''); } catch {}
    const img = c.image || item.image || '';
    onOpenProduct({
      id: item.nodeId || item.id,
      name: c.name || item.name,
      brand: c.brand || '',
      img,
      gallery: (Array.isArray(c.gallery) && c.gallery.length) ? c.gallery : (img ? [img] : []),
      price: c.price != null ? c.price : 0,
      currency: c.currency || 'AUD',
      description: c.description || '',
      descriptionRaw: c.description || '',
      why: c.description || '',
      primaryUrl: sourceUrl || null,
      retailers: sourceUrl ? [{ name: host || 'Source', url: sourceUrl }] : [],
      materials: c.materials || null,
      certifications: Array.isArray(c.certifications) ? c.certifications : [],
      dimensions: c.dimensions || null,
      madeIn: c.madeIn || null,
      features: Array.isArray(c.features) ? c.features : [],
      ageMin: 0, ageMax: 0,
      isCustom: true,
      placeholder: !!c.placeholder,
    });
  };

  // ── Options (swap-in alternatives on one placed item) ───────────────
  // A placed item can carry several candidates (four cots, say); one is
  // active and drawn at its size, the others sit in the panel one click
  // away. Switching swaps name / image / size / list link in place.
  const altFrom = (src) => ({
    id: rmUid(), name: src.name || 'Option', image: src.image || null,
    w: Math.max(5, Math.round(src.w || 60)), d: Math.max(5, Math.round(src.d || 60)),
    nodeId: src.nodeId || null, productId: src.productId || null, listId: src.listId || null, listName: src.listName || null,
  });
  const withSeededAlts = (it) => {
    if (Array.isArray(it.alts) && it.alts.length) return it;
    const me = { ...altFrom(it), x: it.x, y: it.y, rot: it.rot || 0 };
    return { ...it, alts: [me], altId: me.id };
  };
  const addAltTo = (itemId, src) => {
    const alt = altFrom(src);
    patchRoom(r => ({ ...r, items: r.items.map(x => {
      if (x.id !== itemId) return x;
      const seeded = withSeededAlts(x);
      // Remember the outgoing option's spot, then add + switch to the new
      // one at the same centre (re-snapped so flush stays flush).
      const alts = seeded.alts.map(a => a.id === seeded.altId ? { ...a, x: x.x, y: x.y, rot: x.rot || 0, w: x.w, d: x.d } : a);
      const cx = x.x + x.w / 2, cy = x.y + x.d / 2;
      const next = { ...seeded, alts: [...alts, alt], altId: alt.id, name: alt.name, image: alt.image, w: alt.w, d: alt.d, nodeId: alt.nodeId, productId: alt.productId, listId: alt.listId, listName: alt.listName, rot: x.rot || 0 };
      const sn = snapItemToSurroundings(next, cx - alt.w / 2, cy - alt.d / 2, 'near');
      return { ...next, x: sn.x, y: sn.y };
    }) }), `Add option ${alt.name}`);
    setAddOpen(null);
  };
  const switchAlt = (itemId, altId) => {
    patchRoom(r => ({ ...r, items: r.items.map(x => {
      if (x.id !== itemId) return x;
      const alt = (x.alts || []).find(a => a.id === altId);
      if (!alt || x.altId === altId) return x;
      // Remember where the outgoing option sits (position, rotation, size)
      // so it comes back to exactly this spot next time.
      const alts = (x.alts || []).map(a => a.id === x.altId ? { ...a, x: x.x, y: x.y, rot: x.rot || 0, w: x.w, d: x.d } : a);
      let next = { ...x, alts, altId, name: alt.name, image: alt.image, w: alt.w, d: alt.d, nodeId: alt.nodeId, productId: alt.productId, listId: alt.listId, listName: alt.listName };
      if (alt.x != null && alt.y != null) {
        next.x = alt.x; next.y = alt.y; next.rot = alt.rot != null ? alt.rot : (x.rot || 0);
      } else {
        // Never placed before: keep the same centre, then re-snap so a
        // flush item stays flush despite the size difference.
        const cx = x.x + x.w / 2, cy = x.y + x.d / 2;
        next.rot = x.rot || 0;
        const sn = snapItemToSurroundings(next, cx - alt.w / 2, cy - alt.d / 2, 'near');
        next.x = sn.x; next.y = sn.y;
      }
      return next;
    }) }), 'Switch option');
  };
  const removeAlt = (itemId, altId) => {
    patchRoom(r => ({ ...r, items: r.items.map(x => {
      if (x.id !== itemId) return x;
      const alts = (x.alts || []).filter(a => a.id !== altId);
      if (alts.length === 0) return { ...x, alts: [], altId: null };
      if (x.altId !== altId) return { ...x, alts };
      const alt = alts[0];
      return { ...x, alts, altId: alt.id, name: alt.name, image: alt.image, w: alt.w, d: alt.d, nodeId: alt.nodeId, productId: alt.productId, listId: alt.listId, listName: alt.listName };
    }) }), 'Remove option');
  };
  const [altTargetId, setAltTargetId] = _rm_s(null);

  // ── Wall hover + selected-item spacing ──────────────────────────────
  const [hoverWall, setHoverWall] = _rm_s(null);
  // Axis-aligned bounding box of an item at any rotation (about its centre).
  const itemBox = (it, ox, oy) => {
    const x = ox != null ? ox : it.x, y = oy != null ? oy : it.y;
    const cx = x + it.w / 2, cy = y + it.d / 2;
    const r = ((it.rot || 0) * Math.PI) / 180, c = Math.cos(r), s = Math.sin(r);
    const hw = it.w / 2, hd = it.d / 2;
    const ex = Math.abs(hw * c) + Math.abs(hd * s), ey = Math.abs(hw * s) + Math.abs(hd * c);
    return { x0: cx - ex, y0: cy - ey, x1: cx + ex, y1: cy + ey, cx, cy };
  };
  // Magnetic snapping while moving: pull an edge flush against the
  // nearest wall line or another item's edge when within `thr` cm.
  // mode: 'near' (default, 6 cm) | 'strong' (Shift, 40 cm) | 'off' (Alt).
  const snapItemToSurroundings = (it, x, y, mode) => {
    if (mode === 'off') return { x, y };
    const rot = ((it.rot || 0) % 360 + 360) % 360;
    if (Math.abs(rot % 90) > 0.5) return { x, y };            // only axis-aligned boxes snap
    const thr = mode === 'strong' ? 40 : 6;
    const me = itemBox(it, x, y);
    let bestDx = null, bestDy = null;
    const consider = (axis, delta) => {
      if (Math.abs(delta) > thr) return;
      if (axis === 'x') { if (bestDx == null || Math.abs(delta) < Math.abs(bestDx)) bestDx = delta; }
      else { if (bestDy == null || Math.abs(delta) < Math.abs(bestDy)) bestDy = delta; }
    };
    for (let i = 0; i < walls.length; i++) {
      const a = walls[i], bb = walls[(i + 1) % walls.length];
      if (Math.abs(a.x - bb.x) < 0.01) {                       // vertical wall
        const wy0 = Math.min(a.y, bb.y), wy1 = Math.max(a.y, bb.y);
        if (me.y0 < wy1 && me.y1 > wy0) { consider('x', a.x - me.x0); consider('x', a.x - me.x1); }
      } else if (Math.abs(a.y - bb.y) < 0.01) {                // horizontal wall
        const wx0 = Math.min(a.x, bb.x), wx1 = Math.max(a.x, bb.x);
        if (me.x0 < wx1 && me.x1 > wx0) { consider('y', a.y - me.y0); consider('y', a.y - me.y1); }
      }
    }
    (room ? room.items : []).forEach(o => {
      if (o.id === it.id) return;
      const ob = itemBox(o);
      if (me.y0 < ob.y1 && me.y1 > ob.y0) { consider('x', ob.x1 - me.x0); consider('x', ob.x0 - me.x1); }
      if (me.x0 < ob.x1 && me.x1 > ob.x0) { consider('y', ob.y1 - me.y0); consider('y', ob.y0 - me.y1); }
    });
    return { x: Math.round((x + (bestDx || 0)) * 10) / 10, y: Math.round((y + (bestDy || 0)) * 10) / 10 };
  };
  // Rotate a vector by deg.
  const rotV = (vx, vy, deg) => { const r = (deg * Math.PI) / 180, c = Math.cos(r), s = Math.sin(r); return { x: vx * c - vy * s, y: vx * s + vy * c }; };
  // Distance along a ray from p in direction (dx,dy) to the nearest wall edge.
  const rayToWall = (p, dx, dy) => {
    let best = null;
    for (let i = 0; i < walls.length; i++) {
      const a = walls[i], bb = walls[(i + 1) % walls.length];
      const ex = bb.x - a.x, ey = bb.y - a.y;
      const den = dx * ey - dy * ex;
      if (Math.abs(den) < 1e-9) continue;
      const t = ((a.x - p.x) * ey - (a.y - p.y) * ex) / den;
      const u = ((a.x - p.x) * dy - (a.y - p.y) * dx) / den;
      if (t > 0.01 && u >= -1e-6 && u <= 1 + 1e-6) best = best == null ? t : Math.min(best, t);
    }
    return best;
  };
  const spacingFor = (it) => {
    const me = itemBox(it);
    const others = (room ? room.items : []).filter(o => o.id !== it.id).map(itemBox);
    const out = [];
    const dirs = [
      { k: 'right', dx: 1, dy: 0, from: { x: me.x1, y: me.cy }, pick: (o) => (o.x0 >= me.x1 - 0.01 && o.y0 < me.y1 && o.y1 > me.y0) ? o.x0 - me.x1 : null },
      { k: 'left',  dx: -1, dy: 0, from: { x: me.x0, y: me.cy }, pick: (o) => (o.x1 <= me.x0 + 0.01 && o.y0 < me.y1 && o.y1 > me.y0) ? me.x0 - o.x1 : null },
      { k: 'down',  dx: 0, dy: 1, from: { x: me.cx, y: me.y1 }, pick: (o) => (o.y0 >= me.y1 - 0.01 && o.x0 < me.x1 && o.x1 > me.x0) ? o.y0 - me.y1 : null },
      { k: 'up',    dx: 0, dy: -1, from: { x: me.cx, y: me.y0 }, pick: (o) => (o.y1 <= me.y0 + 0.01 && o.x0 < me.x1 && o.x1 > me.x0) ? me.y0 - o.y1 : null },
    ];
    for (const d of dirs) {
      let gap = null, kind = 'wall';
      for (const o of others) { const g = d.pick(o); if (g != null && (gap == null || g < gap)) { gap = g; kind = 'item'; } }
      const wgap = rayToWall(d.from, d.dx, d.dy);
      if (wgap != null && (gap == null || wgap < gap)) { gap = wgap; kind = 'wall'; }
      if (gap == null || gap < 0.5) continue;
      out.push({ k: d.k, from: d.from, to: { x: d.from.x + d.dx * gap, y: d.from.y + d.dy * gap }, gap: Math.round(gap * 10) / 10, kind });
    }
    return out;
  };

  // Paste a link → create the item in "Uncategorised" (made on demand),
  // extract its details, refresh lists, then place it in the room.
  const [linkBusy, setLinkBusy] = _rm_s(false);
  const addFromLink = async (rawUrl) => {
    let url = String(rawUrl || '').trim();
    if (!/^https?:\/\//i.test(url)) url = 'https://' + url.replace(/^\/+/, '');
    try { new URL(url); } catch { window.__mr_showToast && window.__mr_showToast("That doesn't look like a link"); return; }
    if (!window.MR || !window.MR.nodes) return;
    setLinkBusy(true);
    try {
      let bucket = (userLists || []).find(l => l.isMine !== false && /^uncategori[sz]ed$/i.test(l.name || ''));
      let rootId = bucket && bucket.id;
      if (!rootId) {
        const c = await window.MR.nodes.createRoot({ name: 'Uncategorised', kind: 'private', description: 'Things added on the fly — move them into a proper list whenever.' });
        if (!c.ok) { window.__mr_showToast && window.__mr_showToast('Could not create the Uncategorised list'); return; }
        rootId = c.root.id;
      }
      let host = ''; try { host = new URL(url).hostname.replace(/^www\./, ''); } catch {}
      const added = await window.MR.nodes.addChild({ parentId: rootId, rootId, type: 'item', name: host || 'New item', custom: { name: host || 'New item', sourceUrl: url, placeholder: true } });
      if (!added.ok || !added.node) { window.__mr_showToast && window.__mr_showToast('Could not add the item'); return; }
      let node = added.node;
      let data = null;
      if (window.MR.enrich && typeof window.MR.enrich.enrichItem === 'function') {
        const probe = { ...node, custom: { ...(node.custom || {}), name: '', sourceUrl: url } };
        const res = await window.MR.enrich.enrichItem(probe);
        if (res && res.ok && res.data && !res.partial) {
          data = res.data;
          // Created from a bare link, so the label is just the host — use the product title.
          const label = data.itemType || data.name;
          if (label) { try { await window.MR.nodes.updateNode(node.id, { name: label }); } catch {} }
        }
        else if (res && res.partial && res.data && res.data.name) {
          await window.MR.nodes.updateNode(node.id, { name: res.data.itemType || res.data.name, custom: { ...(node.custom || {}), name: res.data.name, sourceUrl: url, placeholder: true } });
          data = { name: res.data.name };
          window.__mr_showToast && window.__mr_showToast(`${host} blocks readers — named it from the link`);
        }
      }
      if (typeof window.__mr_refreshUserLists === 'function') { try { await window.__mr_refreshUserLists(); } catch {} }
      const { w, d } = rmFootprint(data && data.dimensions);
      addItem({
        name: (data && data.name) || host || 'New item',
        w: w || 60, d: d || 60,
        nodeId: node.id, productId: null, listId: rootId, listName: 'Uncategorised',
        image: (data && data.image) || null, color: RM_COLORS[4],
      });
      window.__mr_showToast && window.__mr_showToast(w && d ? `Added to Uncategorised and placed at ${w}×${d} cm` : `Added to Uncategorised and placed — no size on the page, set it in the panel`);
    } catch (err) {
      console.warn('[rooms] addFromLink failed', err);
      window.__mr_showToast && window.__mr_showToast('Could not add from that link');
    } finally { setLinkBusy(false); }
  };

  if (loading) return <main className="page rooms-page"><div className="page-head"><div><div className="page-eyebrow">Rooms</div><h1 className="page-title">Loading…</h1></div></div></main>;
  if (err) return <main className="page rooms-page"><div className="page-head"><div><div className="page-eyebrow">Rooms</div><h1 className="page-title">Couldn't load rooms</h1><p className="page-sub">{err}</p></div></div></main>;

  const grid = (room && room.settings && room.settings.grid) || 10;
  const selItem = sel && sel.kind === 'item' && room ? room.items.find(it => it.id === sel.id) : null;
  const selOpening = sel && sel.kind === 'opening' && room ? (room.openings || []).find(op => op.id === sel.id) : null;
  const selVertex = sel && sel.kind === 'vertex' && room ? room.walls[sel.idx] : null;
  const hist = room ? histFor(room.id) : { past: [], future: [] };
  const isOwner = room && myUserId && room.owner_id === myUserId;

  return (
    <main className="page rooms-page">
      {!room ? (
        <>
          <div className="rooms-cmd">
            <button type="button" className="rooms-btn rooms-btn--primary" onClick={createRoom}>+ New room</button>
          </div>
          <div className="empty"><h3 className="empty-title">No rooms yet</h3><p>Create one to start laying it out.</p></div>
        </>
      ) : (
        <div className="rooms-body">
          {/* One command bar: room switcher · undo/redo · add items · share · more */}
          <div className="rooms-cmd">
            <RoomsMenu
              className="rooms-btn--room"
              title="Switch room"
              label={<><span className="rooms-cmd-roomname">{room.name}</span>{myUserId && room.owner_id !== myUserId ? <span className="rooms-tab-shared">·shared</span> : null}</>}
              render={(close) => (
                <>
                  <div className="rooms-menu-h">Rooms</div>
                  {rooms.map(r => (
                    <button key={r.id} type="button" role="menuitemradio" aria-checked={r.id === activeId} className={`rooms-menu-item${r.id === activeId ? ' is-on' : ''}`} onClick={() => { setActiveId(r.id); setSel(null); setShowHistory(false); close(); }}>
                      <span className="rooms-menu-check">{r.id === activeId ? '✓' : ''}</span>{r.name}{myUserId && r.owner_id !== myUserId ? <span className="rooms-tab-shared">·shared</span> : null}
                    </button>
                  ))}
                  <button type="button" role="menuitem" className="rooms-menu-item" onClick={() => { close(); createRoom(); }}><span className="rooms-menu-check">+</span>New room</button>
                  <div className="rooms-menu-sep" />
                  <button type="button" role="menuitem" className="rooms-menu-item" onClick={() => { close(); renameRoom(); }}><span className="rooms-menu-check" />Rename “{room.name}”</button>
                  {isOwner && <button type="button" role="menuitem" className="rooms-menu-item rooms-menu-item--danger" onClick={() => { close(); deleteRoom(); }}><span className="rooms-menu-check" />Delete room…</button>}
                </>
              )}
            />
            <div className="rooms-cmd-sep" />
            <div className="rooms-toolgroup rooms-toolgroup--tight">
              <button type="button" className="rooms-btn rooms-btn--icon" onClick={undo} disabled={!hist.past.length} title="Undo (⌘Z)" aria-label="Undo">↶</button>
              <button type="button" className="rooms-btn rooms-btn--icon" onClick={redo} disabled={!hist.future.length} title="Redo (⇧⌘Z)" aria-label="Redo">↷</button>
            </div>
            <div className="rooms-cmd-sep" />
            <div className="rooms-toolgroup">
              <button type="button" className={`rooms-btn rooms-btn--primary${addOpen === 'link' ? ' is-on' : ''}`} onClick={() => setAddOpen(addOpen === 'link' ? null : 'link')} title="Paste a product link — it's added to Uncategorised and placed here">+ From link</button>
              <button type="button" className={`rooms-btn${addOpen === 'list' ? ' is-on' : ''}`} onClick={() => setAddOpen(addOpen === 'list' ? null : 'list')}>+ From a list</button>
              <button type="button" className={`rooms-btn${addOpen === 'custom' ? ' is-on' : ''}`} onClick={() => setAddOpen(addOpen === 'custom' ? null : 'custom')}>+ Custom</button>
            </div>
            <div className="rooms-toolgroup rooms-toolgroup--right">
              <span className={`rooms-save rooms-save--${saveState}`}>{saveState === 'saving' ? 'Saving…' : saveState === 'saved' ? 'Saved' : saveState === 'error' ? 'Save failed' : ''}</span>
              <button type="button" className="rooms-btn" onClick={() => setShareOpen(true)}>Share</button>
              <RoomsMenu
                label="More"
                title="Room editing, view and history"
                align="right"
                isOn={tool !== 'select' || showHistory}
                render={(close) => (
                  <>
                    <div className="rooms-menu-h">Edit room</div>
                    <button type="button" role="menuitemradio" aria-checked={tool === 'select'} className={`rooms-menu-item${tool === 'select' ? ' is-on' : ''}`} onClick={() => { setTool('select'); close(); }}><span className="rooms-menu-check">{tool === 'select' ? '✓' : ''}</span>Select / move</button>
                    <button type="button" role="menuitemradio" aria-checked={tool === 'addcorner'} className={`rooms-menu-item${tool === 'addcorner' ? ' is-on' : ''}`} onClick={() => { setTool('addcorner'); close(); }} title="Then click a wall to add a corner"><span className="rooms-menu-check">{tool === 'addcorner' ? '✓' : ''}</span>Add corner</button>
                    <button type="button" role="menuitem" className="rooms-menu-item" onClick={() => { setAddOpen(addOpen === 'opening' ? null : 'opening'); close(); }}><span className="rooms-menu-check" />Add door / window</button>
                    <div className="rooms-menu-sep" />
                    <div className="rooms-menu-h">View</div>
                    <div className="rooms-menu-row">
                      <button type="button" className="rooms-btn rooms-btn--icon" onClick={() => setZoom(z => Math.max(0.5, +(z - 0.25).toFixed(2)))} title="Zoom out">−</button>
                      <button type="button" className="rooms-btn" onClick={() => setZoom(1)} title="Fit">Fit</button>
                      <button type="button" className="rooms-btn rooms-btn--icon" onClick={() => setZoom(z => Math.min(4, +(z + 0.25).toFixed(2)))} title="Zoom in">+</button>
                      <span className="rooms-meta">{Math.round(zoom * 100)}%</span>
                    </div>
                    <div className="rooms-menu-sep" />
                    <button type="button" role="menuitemcheckbox" aria-checked={showHistory} className={`rooms-menu-item${showHistory ? ' is-on' : ''}`} onClick={() => { setShowHistory(v => !v); close(); }}><span className="rooms-menu-check">{showHistory ? '✓' : ''}</span>History{hist.past.length ? ` (${hist.past.length})` : ''}</button>
                    <div className="rooms-menu-foot">{Math.round(rmArea(walls) / 10000 * 100) / 100} m² · {walls.length} corners · {room.items.length} items</div>
                  </>
                )}
              />
            </div>
          </div>

          {/* Layout variations strip */}
          <div className="rooms-layouts">
            <span className="rooms-layouts-lbl">Layouts</span>
            {layouts.map(l => (
              <span
                key={l.id}
                className={`rooms-layout-chip${l.id === activeLayout ? ' is-on' : ''}${layoutDrag && layoutDrag.id === l.id ? ' is-dragging' : ''}${layoutDrag && layoutDrag.overId === l.id ? ' is-over-' + layoutDrag.pos : ''}`}
                draggable
                title="Drag to reorder"
                onDragStart={(e) => { try { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', 'layout:' + l.id); } catch {} setLayoutDrag({ id: l.id, overId: null, pos: null }); }}
                onDragOver={(e) => { if (!layoutDrag || layoutDrag.id === l.id) return; e.preventDefault(); const r = e.currentTarget.getBoundingClientRect(); const pos = (e.clientX - r.left) < r.width / 2 ? 'before' : 'after'; if (layoutDrag.overId !== l.id || layoutDrag.pos !== pos) setLayoutDrag({ ...layoutDrag, overId: l.id, pos }); }}
                onDrop={(e) => { e.preventDefault(); if (layoutDrag && layoutDrag.id !== l.id) reorderLayouts(layoutDrag.id, l.id, layoutDrag.pos || 'after'); setLayoutDrag(null); }}
                onDragEnd={() => setLayoutDrag(null)}
              >
                <button type="button" className="rooms-layout-name" onClick={() => switchLayout(l.id)} onDoubleClick={() => renameLayout(l.id)} title="Click to switch · double-click to rename">{l.name}</button>
                {l.id === activeLayout && (
                  <>
                    <button type="button" className="rooms-layout-mini" onClick={() => renameLayout(l.id)} title="Rename">✎</button>
                    {layouts.length > 1 && <button type="button" className="rooms-layout-mini" onClick={() => deleteLayout(l.id)} title="Delete this layout">×</button>}
                  </>
                )}
              </span>
            ))}
            <button type="button" className="rooms-btn" onClick={duplicateLayout} title="Copy the current arrangement into a new layout you can move around">+ Duplicate</button>
            <button type="button" className="rooms-btn rooms-btn--ghost rooms-layouts-suggest" onClick={runSuggest} disabled={!room.items.length} title="Generate a few arrangements of the current items">✦ Suggest</button>
          </div>

          {suggestions && (
            <div className="rooms-panel rooms-suggest">
              <div className="rooms-panel-head">
                <span className="rooms-panel-h">Suggested arrangements{suggestions.length ? ` · ${suggestions.length}` : ''}</span>
                <div style={{ display: 'flex', gap: 6 }}>
                  <button type="button" className="rooms-btn" onClick={runSuggest}>More</button>
                  <button type="button" className="rooms-btn rooms-btn--ghost" onClick={() => setSuggestions(null)}>Close</button>
                </div>
              </div>
              <div className="rooms-hint" style={{ marginBottom: 8 }}>Big things go flush to walls and into corners, cots stay out from under windows and door swings, storage sits near the cot. Pick one and fine-tune it.</div>
              <div className="rooms-suggest-grid">
                {suggestions.map((items, i) => (
                  <div key={i} className="rooms-suggest-card">
                    <RoomThumb walls={walls} items={items} openings={room.openings} width={190} />
                    <div className="rooms-suggest-actions">
                      <button type="button" className="rooms-btn rooms-btn--primary" onClick={() => { addLayout(items, `Option ${String.fromCharCode(65 + i)}`); setSuggestions(null); }}>Use as new layout</button>
                      <button type="button" className="rooms-btn" onClick={() => { patchRoom(r => ({ ...r, items: items.map(it => ({ ...it })) }), `Apply option ${String.fromCharCode(65 + i)}`); setSuggestions(null); }} title="Replace the current layout's placements (undoable)">Apply here</button>
                    </div>
                  </div>
                ))}
                {suggestions.length === 0 && <div className="rooms-hint">Nothing fit. Try removing an item or making the room bigger.</div>}
              </div>
            </div>
          )}

          {addOpen === 'link' && <RoomLinkForm busy={linkBusy} onSubmit={addFromLink} onCancel={() => setAddOpen(null)} />}
          {addOpen === 'custom' && <RoomCustomItemForm onAdd={addItem} onCancel={() => setAddOpen(null)} />}
          {addOpen === 'list' && <RoomListPicker pool={listItemsPool} lists={(userLists || []).map(l => ({ id: l.id, name: l.name, count: (l.items || []).length }))} onAdd={addItem} onAddFromLink={addFromLink} linkBusy={linkBusy} onCancel={() => setAddOpen(null)} />}
          {addOpen === 'alt-custom' && altTargetId && <RoomCustomItemForm heading="New option for this spot" onAdd={(it) => addAltTo(altTargetId, it)} onCancel={() => setAddOpen(null)} />}
          {addOpen === 'alt-list' && altTargetId && <RoomListPicker heading="Add an option from your lists" pool={listItemsPool} lists={(userLists || []).map(l => ({ id: l.id, name: l.name, count: (l.items || []).length }))} onAdd={(it) => addAltTo(altTargetId, it)} onAddFromLink={null} linkBusy={false} onCancel={() => setAddOpen(null)} />}
          {addOpen === 'opening' && (
            <div className="rooms-panel rooms-panel--inline">
              <span className="rooms-panel-h">Add to a wall</span>
              <button type="button" className="rooms-btn" onClick={() => addOpening('door', 'single')}>Door</button>
              <button type="button" className="rooms-btn" onClick={() => addOpening('door', 'saloon')} title="Two half-doors hinged either side, both swinging in">Saloon door</button>
              <button type="button" className="rooms-btn" onClick={() => addOpening('window')}>Window</button>
              <button type="button" className="rooms-btn rooms-btn--ghost" onClick={() => setAddOpen(null)}>Cancel</button>
              <span className="rooms-hint">Then drag it along the wall, or pick the wall + position in the side panel.</span>
            </div>
          )}

          <div className="rooms-stage">
            <svg
              ref={svgRef}
              className={`rooms-svg${tool === 'addcorner' ? ' is-addcorner' : ''}`}
              viewBox={viewBox}
              preserveAspectRatio="xMidYMid meet"
              onMouseDown={(e) => { if (e.target === e.currentTarget || e.target.classList.contains('rooms-floor-bg')) setSel(null); }}
            >
              <defs>
                <pattern id="rm-grid" width={grid} height={grid} patternUnits="userSpaceOnUse">
                  <path d={`M ${grid} 0 L 0 0 0 ${grid}`} fill="none" stroke="rgba(28,26,20,0.07)" strokeWidth="0.6" />
                </pattern>
                <pattern id="rm-grid-major" width={grid * 10} height={grid * 10} patternUnits="userSpaceOnUse">
                  <rect width={grid * 10} height={grid * 10} fill="url(#rm-grid)" />
                  <path d={`M ${grid * 10} 0 L 0 0 0 ${grid * 10}`} fill="none" stroke="rgba(28,26,20,0.16)" strokeWidth="0.8" />
                </pattern>
              </defs>
              <rect className="rooms-floor-bg" x={b.minX - PAD * 4} y={b.minY - PAD * 4} width={vbW * 4} height={vbH * 4} fill="#f5efe1" />
              {/* Wall band: a wide stroke centred on the polygon, then the
                  opaque floor painted over its inner half — so only the
                  outer half shows and the polygon edge is the wall face. */}
              <polygon points={walls.map(p => `${p.x},${p.y}`).join(' ')} fill="none" stroke="#1c1a14" strokeWidth={RM_WALL_T * 2} strokeLinejoin="miter" strokeMiterlimit="8" style={{ pointerEvents: 'none' }} />
              <polygon points={walls.map(p => `${p.x},${p.y}`).join(' ')} fill="#fdf8ee" stroke="none" />
              <polygon points={walls.map(p => `${p.x},${p.y}`).join(' ')} fill="url(#rm-grid-major)" stroke="none" />
              <polygon points={walls.map(p => `${p.x},${p.y}`).join(' ')} fill="rgba(255,250,240,0.35)" stroke="none" />

              {/* Items */}
              {room.items.map(it => {
                const on = sel && sel.kind === 'item' && sel.id === it.id;
                const cx = it.x + it.w / 2, cy = it.y + it.d / 2;
                const showImg = !!it.image && it.w >= 30 && it.d >= 30;
                const imgSz = showImg ? Math.min(it.w, it.d) * 0.42 : 0;
                // Label lives in the band below the thumbnail when there is
                // one, otherwise it's centred in the box.
                const labelTop = showImg ? it.y + 3 + imgSz + 2 : it.y;
                const labelH = showImg ? it.y + it.d - labelTop : it.d;
                const { fs, lines } = rmFitLabel(it.name, it.w, Math.max(10, labelH));
                const dimFs = Math.max(5.5, fs * 0.75);
                const blockH = lines.length * fs * 1.15 + dimFs * 1.2;
                const y0 = labelTop + Math.max(2, (labelH - blockH) / 2) + fs;
                return (
                  <g key={it.id} transform={`rotate(${it.rot || 0} ${cx} ${cy})`} className={`rooms-item${on ? ' is-selected' : ''}`}
                     onMouseDown={(e) => { if (tool !== 'select') return; setSel({ kind: 'item', id: it.id }); startDrag(e, { kind: 'item', id: it.id, ox: it.x, oy: it.y, label: it.name }); }}
                     onDoubleClick={(e) => { e.stopPropagation(); if (it.productId && onOpenProduct && productMap && productMap[it.productId]) onOpenProduct(productMap[it.productId]); }}
                  >
                    <title>{it.name} — {it.w}×{it.d} cm</title>
                    <rect x={it.x} y={it.y} width={it.w} height={it.d} rx="2" fill={it.color} fillOpacity="0.82" stroke={on ? '#1c1a14' : 'rgba(28,26,20,0.45)'} strokeWidth={on ? 1.6 : 0.8} />
                    {showImg && (
                      <image href={it.image} x={cx - imgSz / 2} y={it.y + 3} width={imgSz} height={imgSz} preserveAspectRatio="xMidYMid slice" opacity="0.92" />
                    )}
                    {lines.map((ln, li) => (
                      <text key={li} x={cx} y={y0 + li * fs * 1.15} textAnchor="middle" fontSize={fs} fill="#1c1a14" style={{ pointerEvents: 'none', fontWeight: 600 }}>{ln}</text>
                    ))}
                    <text x={cx} y={y0 + lines.length * fs * 1.15 + dimFs * 0.1} textAnchor="middle" fontSize={dimFs} fill="rgba(28,26,20,0.7)" style={{ pointerEvents: 'none' }}>{it.w}×{it.d}</text>
                    {Array.isArray(it.alts) && it.alts.length > 1 && (
                      <g style={{ pointerEvents: 'none' }}>
                        <rect x={it.x + it.w - 22} y={it.y + 2} width="20" height="10" rx="5" fill="#1c1a14" opacity="0.85" />
                        <text x={it.x + it.w - 12} y={it.y + 9.5} textAnchor="middle" fontSize="7" fill="#fffaf0">⇄ {it.alts.length}</text>
                      </g>
                    )}
                    {on && tool === 'select' && (() => {
                      const base = { id: it.id, label: it.name, cx, cy, ow: it.w, od: it.d, orot: it.rot || 0, item: it };
                      const hs = 4.5;
                      const corners = [
                        { h: 'nw', x: it.x, y: it.y, cur: 'nwse-resize' },
                        { h: 'ne', x: it.x + it.w, y: it.y, cur: 'nesw-resize' },
                        { h: 'sw', x: it.x, y: it.y + it.d, cur: 'nesw-resize' },
                        { h: 'se', x: it.x + it.w, y: it.y + it.d, cur: 'nwse-resize' },
                      ];
                      return (
                        <g className="rooms-handles">
                          <line x1={cx} y1={it.y} x2={cx} y2={it.y - 12} stroke="#1c1a14" strokeWidth="0.8" />
                          <circle cx={cx} cy={it.y - 15} r="4" fill="#fffaf0" stroke="#1c1a14" strokeWidth="1.2" style={{ cursor: 'grab' }}
                                  onMouseDown={(e) => startDrag(e, { ...base, kind: 'rotate' })}>
                            <title>Drag to rotate · Shift for 1°</title>
                          </circle>
                          {corners.map(c => (
                            <rect key={c.h} x={c.x - hs} y={c.y - hs} width={hs * 2} height={hs * 2} fill="#fffaf0" stroke="#1c1a14" strokeWidth="1.2" style={{ cursor: c.cur }}
                                  onMouseDown={(e) => startDrag(e, { ...base, kind: 'resize', handle: c.h })}>
                              <title>Drag to resize · Shift for 1 cm steps</title>
                            </rect>
                          ))}
                        </g>
                      );
                    })()}
                  </g>
                );
              })}

              {/* Spacing guides for the selected item — distance to the
                  nearest item or wall in each direction. */}
              {selItem && spacingFor(selItem).map(s => (
                <g key={s.k} style={{ pointerEvents: 'none' }}>
                  <line x1={s.from.x} y1={s.from.y} x2={s.to.x} y2={s.to.y} stroke={s.kind === 'item' ? '#2a5aa0' : '#cf5d4a'} strokeWidth="0.9" strokeDasharray="3 2" />
                  <line x1={s.to.x + (s.k === 'left' || s.k === 'right' ? 0 : -4)} y1={s.to.y + (s.k === 'up' || s.k === 'down' ? 0 : -4)} x2={s.to.x + (s.k === 'left' || s.k === 'right' ? 0 : 4)} y2={s.to.y + (s.k === 'up' || s.k === 'down' ? 0 : 4)} stroke={s.kind === 'item' ? '#2a5aa0' : '#cf5d4a'} strokeWidth="0.9" />
                  <rect x={(s.from.x + s.to.x) / 2 - 12} y={(s.from.y + s.to.y) / 2 - 6} width="24" height="12" rx="6" fill="#fffaf0" stroke={s.kind === 'item' ? '#2a5aa0' : '#cf5d4a'} strokeWidth="0.6" />
                  <text x={(s.from.x + s.to.x) / 2} y={(s.from.y + s.to.y) / 2 + 3} textAnchor="middle" fontSize="7.5" fill="#1c1a14" style={{ fontWeight: 600 }}>{s.gap}</text>
                </g>
              ))}

              {/* Walls */}
              {walls.map((p, i) => {
                const q = walls[(i + 1) % walls.length];
                const len = rmDist(p, q);
                const mx = (p.x + q.x) / 2, my = (p.y + q.y) / 2;
                const f = rmWallFrame(walls, i, 0);
                const lx = mx + f.nx * -(RM_WALL_T + 11), ly = my + f.ny * -(RM_WALL_T + 11);
                const ox = -f.nx * RM_WALL_T, oy = -f.ny * RM_WALL_T;   // outward offset
                // Hover: break the wall into solid stretches between its
                // doors / windows and label each one on the inside.
                const hovered = hoverWall === i;
                let segs = [];
                if (hovered) {
                  const ops = (room.openings || []).filter(o => o.wall === i).map(o => ({ s: Math.max(0, o.offset), e: Math.min(len, o.offset + o.width), type: o.type })).sort((a, b) => a.s - b.s);
                  let cur = 0;
                  ops.forEach(o => { if (o.s - cur > 0.5) segs.push({ s: cur, e: o.s, kind: 'wall' }); segs.push({ s: o.s, e: o.e, kind: o.type }); cur = Math.max(cur, o.e); });
                  if (len - cur > 0.5) segs.push({ s: cur, e: len, kind: 'wall' });
                  if (segs.length === 1) segs = [];   // nothing to break down
                }
                return (
                  <g key={`w${i}`} onMouseEnter={() => setHoverWall(i)} onMouseLeave={() => setHoverWall(h => (h === i ? null : h))}>
                    {/* The wall's outer band — hover / click target. Sits
                        exactly on the polygon edge and extends outward. */}
                    <polygon
                      points={`${p.x},${p.y} ${q.x},${q.y} ${q.x + ox},${q.y + oy} ${p.x + ox},${p.y + oy}`}
                      fill={hovered ? '#3a362c' : '#1c1a14'}
                      className="rooms-wall"
                      onClick={(e) => { if (tool === 'addcorner') { e.stopPropagation(); insertCorner(i, toRoom(e)); setTool('select'); } }}
                      onDoubleClick={(e) => { e.stopPropagation(); insertCorner(i, toRoom(e)); }}
                    />
                    {hovered && <line x1={p.x} y1={p.y} x2={q.x} y2={q.y} stroke="#ffaf3a" strokeWidth="1.2" style={{ pointerEvents: 'none' }} />}
                    <g className="rooms-wall-label" onClick={(e) => { e.stopPropagation(); const v = window.prompt('Wall length (cm):', String(Math.round(len * 10) / 10)); if (v !== null) setWallLength(i, v); }}>
                      <rect x={lx - 20} y={ly - 7} width="40" height="14" rx="7" fill="#fffaf0" stroke="rgba(28,26,20,0.25)" strokeWidth="0.6" />
                      <text x={lx} y={ly + 3.5} textAnchor="middle" fontSize="9" fill="#1c1a14" style={{ fontWeight: 600 }}>{Math.round(len * 10) / 10}</text>
                    </g>
                    {segs.map((sg, si) => {
                      const m = rmWallFrame(walls, i, (sg.s + sg.e) / 2);
                      const ix = m.x + f.nx * 13, iy = m.y + f.ny * 13;
                      const L = Math.round((sg.e - sg.s) * 10) / 10;
                      const isWall = sg.kind === 'wall';
                      return (
                        <g key={si} style={{ pointerEvents: 'none' }}>
                          <line x1={rmWallFrame(walls, i, sg.s).x + f.nx * 6} y1={rmWallFrame(walls, i, sg.s).y + f.ny * 6} x2={rmWallFrame(walls, i, sg.e).x + f.nx * 6} y2={rmWallFrame(walls, i, sg.e).y + f.ny * 6} stroke={isWall ? '#1c1a14' : '#2a5aa0'} strokeWidth="0.8" strokeDasharray={isWall ? '' : '2 2'} />
                          <rect x={ix - 15} y={iy - 6} width="30" height="12" rx="6" fill={isWall ? '#fffaf0' : '#eaf1fb'} stroke={isWall ? 'rgba(28,26,20,0.35)' : '#2a5aa0'} strokeWidth="0.6" />
                          <text x={ix} y={iy + 3} textAnchor="middle" fontSize="7.5" fill="#1c1a14" style={{ fontWeight: 600 }}>{L}</text>
                        </g>
                      );
                    })}
                  </g>
                );
              })}

              {/* Openings */}
              {(room.openings || []).map(op => {
                if (op.wall >= walls.length) return null;
                const f = rmWallFrame(walls, op.wall, op.offset);
                const g = rmWallFrame(walls, op.wall, op.offset + op.width);
                const on = sel && sel.kind === 'opening' && sel.id === op.id;
                const sgn = op.swing === 'out' ? -1 : 1;
                const inX = f.nx * sgn, inY = f.ny * sgn;
                const stroke = on ? '#cf5d4a' : '#1c1a14';
                const arcStroke = on ? '#cf5d4a' : 'rgba(28,26,20,0.5)';
                const saloon = op.type === 'door' && (op.style === 'saloon' || op.style === 'double');
                let doorMarkup = null;
                if (op.type === 'door' && !saloon) {
                  const tipX = f.x + inX * op.width, tipY = f.y + inY * op.width;
                  const arc = `M ${g.x} ${g.y} A ${op.width} ${op.width} 0 0 ${sgn === 1 ? 0 : 1} ${tipX} ${tipY}`;
                  doorMarkup = (
                    <>
                      <line x1={f.x} y1={f.y} x2={tipX} y2={tipY} stroke={stroke} strokeWidth="2" />
                      <path d={arc} fill="none" stroke={arcStroke} strokeWidth="1" strokeDasharray="3 2" />
                    </>
                  );
                } else if (saloon) {
                  const leaf = op.width / 2;
                  // Leaf A hinged at the wall-start jamb, leaf B at the wall-end jamb; both swing in.
                  const aTipX = f.x + inX * leaf, aTipY = f.y + inY * leaf;
                  const aArcFromX = f.x + f.ux * leaf, aArcFromY = f.y + f.uy * leaf;
                  const bTipX = g.x + inX * leaf, bTipY = g.y + inY * leaf;
                  const bArcFromX = g.x - f.ux * leaf, bArcFromY = g.y - f.uy * leaf;
                  const arcA = `M ${aArcFromX} ${aArcFromY} A ${leaf} ${leaf} 0 0 ${sgn === 1 ? 0 : 1} ${aTipX} ${aTipY}`;
                  const arcB = `M ${bArcFromX} ${bArcFromY} A ${leaf} ${leaf} 0 0 ${sgn === 1 ? 1 : 0} ${bTipX} ${bTipY}`;
                  doorMarkup = (
                    <>
                      <line x1={f.x} y1={f.y} x2={aTipX} y2={aTipY} stroke={stroke} strokeWidth="2" />
                      <path d={arcA} fill="none" stroke={arcStroke} strokeWidth="1" strokeDasharray="3 2" />
                      <line x1={g.x} y1={g.y} x2={bTipX} y2={bTipY} stroke={stroke} strokeWidth="2" />
                      <path d={arcB} fill="none" stroke={arcStroke} strokeWidth="1" strokeDasharray="3 2" />
                      <text x={(f.x + g.x) / 2 + inX * (leaf + 6)} y={(f.y + g.y) / 2 + inY * (leaf + 6)} textAnchor="middle" fontSize="7" fill="rgba(28,26,20,0.6)" style={{ pointerEvents: 'none' }}>2 × {leaf}</text>
                    </>
                  );
                }
                return (
                  <g key={op.id} className={`rooms-opening${on ? ' is-selected' : ''}`}
                     onMouseDown={(e) => { if (tool !== 'select') return; setSel({ kind: 'opening', id: op.id }); startDrag(e, { kind: 'opening', id: op.id, label: op.type }); }}>
                    {/* Cut the opening through the outer wall band. */}
                    <polygon
                      points={`${f.x},${f.y} ${g.x},${g.y} ${g.x - f.nx * (RM_WALL_T + 0.5)},${g.y - f.ny * (RM_WALL_T + 0.5)} ${f.x - f.nx * (RM_WALL_T + 0.5)},${f.y - f.ny * (RM_WALL_T + 0.5)}`}
                      fill="#f5efe1"
                    />
                    {op.type === 'door' ? doorMarkup : (
                      <>
                        {/* Window: sill line on the wall face + two glazing lines within the band. */}
                        <line x1={f.x} y1={f.y} x2={g.x} y2={g.y} stroke={on ? '#cf5d4a' : '#2a5aa0'} strokeWidth="1.4" />
                        <line x1={f.x - f.nx * (RM_WALL_T * 0.35)} y1={f.y - f.ny * (RM_WALL_T * 0.35)} x2={g.x - f.nx * (RM_WALL_T * 0.35)} y2={g.y - f.ny * (RM_WALL_T * 0.35)} stroke={on ? '#cf5d4a' : '#2a5aa0'} strokeWidth="1" />
                        <line x1={f.x - f.nx * (RM_WALL_T * 0.7)} y1={f.y - f.ny * (RM_WALL_T * 0.7)} x2={g.x - f.nx * (RM_WALL_T * 0.7)} y2={g.y - f.ny * (RM_WALL_T * 0.7)} stroke={on ? '#cf5d4a' : '#2a5aa0'} strokeWidth="1" />
                        <line x1={f.x - f.nx * RM_WALL_T} y1={f.y - f.ny * RM_WALL_T} x2={g.x - f.nx * RM_WALL_T} y2={g.y - f.ny * RM_WALL_T} stroke={on ? '#cf5d4a' : '#2a5aa0'} strokeWidth="1.4" />
                      </>
                    )}
                    <rect x={Math.min(f.x, g.x) - 6} y={Math.min(f.y, g.y) - 6} width={Math.abs(g.x - f.x) + 12} height={Math.abs(g.y - f.y) + 12} fill="transparent" style={{ cursor: 'grab' }} />
                  </g>
                );
              })}

              {/* Corner handles */}
              {walls.map((p, i) => {
                const on = sel && sel.kind === 'vertex' && sel.idx === i;
                return (
                  <circle key={`v${i}`} cx={p.x} cy={p.y} r={on ? 7 : 5.5} className={`rooms-vertex${on ? ' is-selected' : ''}`}
                          fill={on ? '#cf5d4a' : '#fffaf0'} stroke="#1c1a14" strokeWidth="1.6"
                          onMouseDown={(e) => { if (tool !== 'select') return; setSel({ kind: 'vertex', idx: i }); startDrag(e, { kind: 'vertex', idx: i, ox: p.x, oy: p.y }); }}
                          onContextMenu={(e) => { e.preventDefault(); deleteVertex(i); }}
                  />
                );
              })}
            </svg>

            <aside className="rooms-side">
              {showHistory ? (
                <div className="rooms-panel">
                  <div className="rooms-panel-head">
                    <span className="rooms-panel-h">History</span>
                    <button type="button" className="rooms-btn rooms-btn--ghost" onClick={() => setShowHistory(false)}>Close</button>
                  </div>
                  {hist.past.length === 0 ? (
                    <div className="rooms-hint">No changes yet this session. Every edit will show up here — click one to go back to just before it.</div>
                  ) : (
                    <ul className="rooms-history">
                      {hist.past.slice().reverse().map((e, ri) => {
                        const idx = hist.past.length - 1 - ri;
                        return (
                          <li key={`${e.ts}-${idx}`}>
                            <button type="button" onClick={() => revertTo(idx)} title="Undo back to before this change">
                              <span className="rooms-history-label">{e.label}</span>
                              <span className="rooms-history-time">{rmTimeAgo(e.ts)}</span>
                            </button>
                          </li>
                        );
                      })}
                    </ul>
                  )}
                  {hist.future.length > 0 && <div className="rooms-hint" style={{ marginTop: 8 }}>{hist.future.length} undone — press Redo (⇧⌘Z) to bring back.</div>}
                </div>
              ) : selItem ? (
                <RoomItemPanel
                  item={selItem}
                  onChange={(p, label) => {
                    updateItem(selItem.id, p, label, `field:${selItem.id}:${Object.keys(p).join(',')}`);
                    if (('w' in p || 'd' in p) && selItem.nodeId) persistFootprint({ ...selItem, ...p });
                  }}
                  onRemove={() => removeItem(selItem.id)}
                  onDuplicate={() => duplicateItem(selItem.id)}
                  productMap={productMap}
                  onOpenProduct={onOpenProduct}
                  onOpenList={selItem.listId && onOpenList ? () => onOpenList(selItem.listId) : null}
                  onOpenItem={(selItem.nodeId || selItem.productId) && onOpenProduct ? () => openItemModal(selItem) : null}
                  spacing={spacingFor(selItem)}
                  onSwitchAlt={(altId) => switchAlt(selItem.id, altId)}
                  onRemoveAlt={(altId) => removeAlt(selItem.id, altId)}
                  onAddAltFromList={() => { setAltTargetId(selItem.id); setAddOpen('alt-list'); }}
                  onAddAltCustom={() => { setAltTargetId(selItem.id); setAddOpen('alt-custom'); }}
                />
              ) : selOpening ? (
                <div className="rooms-panel">
                  <div className="rooms-panel-h">{selOpening.type === 'door' ? (selOpening.style === 'saloon' ? 'Saloon door' : 'Door') : 'Window'}</div>
                  {selOpening.type === 'door' && (
                    <label className="rooms-field"><span>Style</span>
                      <select value={selOpening.style || 'single'} onChange={(e) => updateOpening(selOpening.id, { style: e.target.value }, 'Door style')}>
                        <option value="single">Single leaf</option>
                        <option value="saloon">Saloon — two half-doors, both swing in</option>
                      </select>
                    </label>
                  )}
                  <label className="rooms-field"><span>Wall</span>
                    <select value={selOpening.wall} onChange={(e) => updateOpening(selOpening.id, { wall: Number(e.target.value), offset: 0 }, 'Move to wall')}>
                      {walls.map((_, i) => <option key={i} value={i}>Wall {i + 1} · {Math.round(rmDist(walls[i], walls[(i + 1) % walls.length]))} cm</option>)}
                    </select>
                  </label>
                  <label className="rooms-field"><span>From wall start (cm)</span><RoomNum value={selOpening.offset} min={0} step={0.5} onCommit={(n) => updateOpening(selOpening.id, { offset: n }, 'Move opening', `field:${selOpening.id}:offset`)} /></label>
                  <label className="rooms-field"><span>{selOpening.style === 'saloon' ? 'Total width (cm)' : 'Width (cm)'}</span><RoomNum value={selOpening.width} min={10} step={0.5} onCommit={(n) => updateOpening(selOpening.id, { width: n }, 'Resize opening', `field:${selOpening.id}:width`)} /></label>
                  {selOpening.type === 'door' && selOpening.style === 'saloon' && (
                    <div className="rooms-hint" style={{ marginTop: -4, marginBottom: 10 }}>Each leaf: {selOpening.width / 2} cm — that's how far they stick into the room when open.</div>
                  )}
                  {selOpening.type === 'door' && (
                    <label className="rooms-field"><span>Swing</span>
                      <select value={selOpening.swing || 'in'} onChange={(e) => updateOpening(selOpening.id, { swing: e.target.value }, 'Door swing')}>
                        <option value="in">Into the room</option><option value="out">Out of the room</option>
                      </select>
                    </label>
                  )}
                  <div className="rooms-panel-actions">
                    <button type="button" className="rooms-btn rooms-btn--danger" onClick={() => removeOpening(selOpening.id)}>Remove</button>
                  </div>
                </div>
              ) : selVertex ? (
                <div className="rooms-panel">
                  <div className="rooms-panel-h">Corner {sel.idx + 1}</div>
                  <label className="rooms-field"><span>X (cm)</span><RoomNum value={selVertex.x} step={0.5} onCommit={(n) => patchRoom(r => ({ ...r, walls: r.walls.map((v, j) => j === sel.idx ? { ...v, x: n } : v) }), `Move corner ${sel.idx + 1}`, `field:v${sel.idx}:x`)} /></label>
                  <label className="rooms-field"><span>Y (cm)</span><RoomNum value={selVertex.y} step={0.5} onCommit={(n) => patchRoom(r => ({ ...r, walls: r.walls.map((v, j) => j === sel.idx ? { ...v, y: n } : v) }), `Move corner ${sel.idx + 1}`, `field:v${sel.idx}:y`)} /></label>
                  <div className="rooms-panel-actions">
                    <button type="button" className="rooms-btn rooms-btn--danger" disabled={walls.length <= 3} onClick={() => deleteVertex(sel.idx)}>Remove corner</button>
                  </div>
                </div>
              ) : (
                <div className="rooms-panel rooms-panel--help">
                  <div className="rooms-panel-h">{room.name}</div>
                  <ul className="rooms-help">
                    <li><strong>Drag a corner</strong> to reshape. <strong>Right-click</strong> a corner to remove it.</li>
                    <li><strong>Click a wall length</strong> to type an exact size in cm.</li>
                    <li><strong>Double-click a wall</strong> (or use + Corner) to add a corner.</li>
                    <li><strong>Drag items</strong> to place them — they snap flush to walls and other items when close. Hold <kbd>⇧</kbd> to pull flush from further away, <kbd>⌥</kbd> to place freely.</li>
                    <li>Select an item for <strong>corner handles</strong> (drag to resize) and the <strong>rotate handle</strong> above it. <kbd>R</kbd> rotates 90°, arrows nudge 5 cm (<kbd>⇧</kbd> 1 cm), <kbd>⌫</kbd> deletes.</li>
                    <li>Doors / windows drag along their wall.</li>
                    <li><kbd>⌘Z</kbd> undo · <kbd>⇧⌘Z</kbd> redo · History shows every change.</li>
                  </ul>
                  {room.items.length > 0 && (
                    <>
                      <div className="rooms-panel-h" style={{ marginTop: 14 }}>Items in this room</div>
                      <ul className="rooms-itemlist">
                        {room.items.map(it => (
                          <li key={it.id}><button type="button" onClick={() => setSel({ kind: 'item', id: it.id })}><span className="rooms-swatch" style={{ background: it.color }} />{it.name}<span className="rooms-itemlist-dims">{it.w}×{it.d}</span></button></li>
                        ))}
                      </ul>
                    </>
                  )}
                </div>
              )}
            </aside>
          </div>
        </div>
      )}

      {shareOpen && room && <RoomShareModal room={room} isOwner={!!isOwner} myUserId={myUserId} onClose={() => setShareOpen(false)} />}
    </main>
  );
}

// Numeric field that lets you clear it while typing. Keeps a local draft,
// commits every valid number as you type, and only snaps back to the
// saved value on blur if what's left isn't a number.
function RoomNum({ value, min = null, step = 1, onCommit }) {
  const [draft, setDraft] = _rm_s(value == null ? '' : String(value));
  const [focused, setFocused] = _rm_s(false);
  _rm_e(() => { if (!focused) setDraft(value == null ? '' : String(value)); }, [value, focused]);
  const parse = (s) => {
    const n = parseFloat(String(s).replace(',', '.'));
    if (!Number.isFinite(n)) return null;
    return min != null && n < min ? null : n;
  };
  return (
    <input
      type="number"
      inputMode="decimal"
      step={step}
      min={min == null ? undefined : min}
      value={draft}
      onFocus={(e) => { setFocused(true); e.target.select(); }}
      onChange={(e) => { const s = e.target.value; setDraft(s); const n = parse(s); if (n != null) onCommit(n); }}
      onBlur={() => { setFocused(false); const n = parse(draft); if (n == null) setDraft(value == null ? '' : String(value)); else if (n !== value) onCommit(n); }}
      onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur(); }}
    />
  );
}

function RoomItemPanel({ item, onChange, onRemove, onDuplicate, productMap, onOpenProduct, onOpenList, onOpenItem, spacing = [], onSwitchAlt, onRemoveAlt, onAddAltFromList, onAddAltCustom }) {
  const product = item.productId && productMap ? productMap[item.productId] : null;
  const alts = Array.isArray(item.alts) ? item.alts : [];
  const DIR = { left: '←', right: '→', up: '↑', down: '↓' };
  return (
    <div className="rooms-panel">
      <div className="rooms-panel-h">Item</div>
      {spacing.length > 0 && (
        <div className="rooms-spacing">
          {spacing.map(s => (
            <span key={s.k} className={`rooms-spacing-chip rooms-spacing-chip--${s.kind}`} title={`${s.gap} cm to the nearest ${s.kind} ${s.k}`}>
              {DIR[s.k]} {s.gap}<small>cm {s.kind}</small>
            </span>
          ))}
        </div>
      )}
      <div className="rooms-alts">
        <div className="rooms-alts-head">
          <span className="rooms-panel-h" style={{ margin: 0 }}>Options in this spot{alts.length > 1 ? ` · ${alts.length}` : ''}</span>
          <div className="rooms-alts-add">
            <button type="button" className="rooms-btn" onClick={onAddAltFromList}>+ list</button>
            <button type="button" className="rooms-btn" onClick={onAddAltCustom}>+ custom</button>
          </div>
        </div>
        {alts.length > 1 ? (
          <ul className="rooms-alts-list">
            {alts.map(a => {
              const on = a.id === item.altId;
              return (
                <li key={a.id}>
                  <button type="button" className={`rooms-alt${on ? ' is-on' : ''}`} onClick={() => onSwitchAlt(a.id)} title={on ? 'Currently placed' : 'Swap this one in'}>
                    {a.image ? <img src={a.image} alt="" /> : <span className="rooms-result-noimg" />}
                    <span className="rooms-alt-txt">
                      <span className="rooms-alt-name">{a.name}</span>
                      <span className="rooms-alt-sub">{a.w}×{a.d} cm{a.listName ? ` · ${a.listName}` : ''}</span>
                    </span>
                    {on && <span className="rooms-alt-tick">✓</span>}
                  </button>
                  <button type="button" className="rooms-alt-x" onClick={() => onRemoveAlt(a.id)} title="Remove this option" aria-label="Remove option">×</button>
                </li>
              );
            })}
          </ul>
        ) : (
          <div className="rooms-hint">Choosing between a few? Add them here and click to swap which one sits in this spot — size and details follow.</div>
        )}
      </div>
      <label className="rooms-field"><span>Name</span><input type="text" value={item.name} onChange={(e) => onChange({ name: e.target.value }, 'Rename item')} /></label>
      <div className="rooms-field-row">
        <label className="rooms-field"><span>Width (cm)</span><RoomNum value={item.w} min={5} onCommit={(n) => onChange({ w: n }, `Resize ${item.name}`)} /></label>
        <label className="rooms-field"><span>Depth (cm)</span><RoomNum value={item.d} min={5} onCommit={(n) => onChange({ d: n }, `Resize ${item.name}`)} /></label>
      </div>
      <div className="rooms-field-row">
        <label className="rooms-field"><span>X (cm)</span><RoomNum value={item.x} onCommit={(n) => onChange({ x: n }, `Move ${item.name}`)} /></label>
        <label className="rooms-field"><span>Y (cm)</span><RoomNum value={item.y} onCommit={(n) => onChange({ y: n }, `Move ${item.name}`)} /></label>
      </div>
      <label className="rooms-field"><span>Rotation</span>
        <div className="rooms-rot">
          {[0, 90, 180, 270].map(r => <button key={r} type="button" className={`rooms-btn${(item.rot || 0) === r ? ' is-on' : ''}`} onClick={() => onChange({ rot: r }, `Rotate ${item.name}`)}>{r}°</button>)}
          <span style={{ width: 70 }}><RoomNum value={Math.round(item.rot || 0)} step={1} onCommit={(n) => onChange({ rot: ((n % 360) + 360) % 360 }, `Rotate ${item.name}`)} /></span>
        </div>
      </label>
      <label className="rooms-field"><span>Colour</span>
        <div className="rooms-colors">
          {RM_COLORS.map(c => <button key={c} type="button" className={`rooms-color${item.color === c ? ' is-on' : ''}`} style={{ background: c }} onClick={() => onChange({ color: c }, `Recolour ${item.name}`)} aria-label={c} />)}
          <input type="color" value={item.color} onChange={(e) => onChange({ color: e.target.value }, `Recolour ${item.name}`)} title="Custom colour" />
        </div>
      </label>
      {(product || item.listName) && (
        <div className="rooms-linked">
          {item.image && <img src={item.image} alt="" />}
          <div style={{ minWidth: 0 }}>
            <div className="rooms-linked-name">{product ? product.name : item.name}</div>
            {item.listName && <div className="rooms-linked-list">from {item.listName}{item.nodeId ? ' · size saves back to the list' : ''}</div>}
            <div className="rooms-linked-actions">
              {onOpenItem && <button type="button" className="rooms-btn" onClick={onOpenItem}>Open item</button>}
              {onOpenList && <button type="button" className="rooms-btn" onClick={onOpenList}>Open list ↗</button>}
              {product && onOpenProduct && !onOpenItem && <button type="button" className="rooms-btn" onClick={() => onOpenProduct(product)}>Open product</button>}
            </div>
          </div>
        </div>
      )}
      <div className="rooms-panel-actions">
        <button type="button" className="rooms-btn" onClick={onDuplicate}>Duplicate</button>
        <button type="button" className="rooms-btn rooms-btn--danger" onClick={onRemove}>Remove</button>
      </div>
    </div>
  );
}

// Paste-a-link lane: paste (or type + Enter) → item is created in
// "Uncategorised", details are extracted, and it's placed in the room.
function RoomLinkForm({ busy, onSubmit, onCancel }) {
  const [url, setUrl] = _rm_s('');
  const ref = _rm_r(null);
  _rm_e(() => { const t = setTimeout(() => ref.current && ref.current.focus(), 30); return () => clearTimeout(t); }, []);
  const go = (v) => { const u = String(v || '').trim(); if (!u || busy) return; onSubmit(u); setUrl(''); };
  return (
    <form className="rooms-panel rooms-panel--inline" onSubmit={(e) => { e.preventDefault(); go(url); }}>
      <span className="rooms-panel-h">From a link</span>
      <input
        ref={ref} type="url" inputMode="url" autoComplete="off" className="rooms-inp" style={{ flex: 1, minWidth: 320 }}
        placeholder="Paste a product link — it adds itself"
        value={url}
        onChange={(e) => setUrl(e.target.value)}
        onPaste={(e) => { const txt = (e.clipboardData && e.clipboardData.getData('text')) || ''; if (/^\s*(https?:\/\/|www\.)\S+/i.test(txt)) { e.preventDefault(); go(txt); } }}
        disabled={busy}
      />
      <button type="submit" className="rooms-btn rooms-btn--primary" disabled={busy || !url.trim()}>{busy ? 'Adding…' : 'Add to room'}</button>
      <button type="button" className="rooms-btn rooms-btn--ghost" onClick={onCancel}>Cancel</button>
      {busy && <span className="rooms-hint">Fetching the product and adding it to <strong>Uncategorised</strong>…</span>}
    </form>
  );
}

function RoomCustomItemForm({ onAdd, onCancel, heading = 'Custom item' }) {
  const [name, setName] = _rm_s('');
  const [w, setW] = _rm_s(60);
  const [d, setD] = _rm_s(60);
  const [color, setColor] = _rm_s(RM_COLORS[0]);
  const ref = _rm_r(null);
  _rm_e(() => { ref.current && ref.current.focus(); }, []);
  const submit = (e) => { e.preventDefault(); if (!name.trim()) return; onAdd({ name: name.trim(), w: Number(w) || 60, d: Number(d) || 60, color }); };
  return (
    <form className="rooms-panel rooms-panel--inline" onSubmit={submit}>
      <span className="rooms-panel-h">{heading}</span>
      <input ref={ref} type="text" placeholder="Name (e.g. Cot)" value={name} onChange={(e) => setName(e.target.value)} className="rooms-inp" />
      <label className="rooms-inl"><span>W</span><input type="number" min="5" value={w} onChange={(e) => setW(e.target.value)} className="rooms-inp rooms-inp--num" /><span>cm</span></label>
      <label className="rooms-inl"><span>D</span><input type="number" min="5" value={d} onChange={(e) => setD(e.target.value)} className="rooms-inp rooms-inp--num" /><span>cm</span></label>
      <div className="rooms-colors">
        {RM_COLORS.map(c => <button key={c} type="button" className={`rooms-color${color === c ? ' is-on' : ''}`} style={{ background: c }} onClick={() => setColor(c)} aria-label={c} />)}
      </div>
      <button type="submit" className="rooms-btn rooms-btn--primary" disabled={!name.trim()}>Add</button>
      <button type="button" className="rooms-btn rooms-btn--ghost" onClick={onCancel}>Cancel</button>
    </form>
  );
}

function RoomListPicker({ pool, lists, onAdd, onAddFromLink, linkBusy, onCancel, heading = 'From your lists' }) {
  const [q, setQ] = _rm_s('');
  const [listId, setListId] = _rm_s('all');
  const [picked, setPicked] = _rm_s(null);
  const [w, setW] = _rm_s('');
  const [d, setD] = _rm_s('');
  const ref = _rm_r(null);
  _rm_e(() => { ref.current && ref.current.focus(); }, []);
  const isUrl = /^\s*(https?:\/\/|www\.)\S+/i.test(q);
  const results = _rm_m(() => {
    const s = q.trim().toLowerCase();
    let list = listId === 'all' ? pool : pool.filter(p => p.listId === listId);
    if (s && !isUrl) list = list.filter(p => p.name.toLowerCase().includes(s) || (p.brand || '').toLowerCase().includes(s) || (p.listName || '').toLowerCase().includes(s));
    return list.slice(0, 300);
  }, [q, pool, listId, isUrl]);
  // Group by list so "All lists" reads as a browse, not a jumble.
  const groups = _rm_m(() => {
    const m = new Map();
    results.forEach(p => { if (!m.has(p.listId)) m.set(p.listId, { name: p.listName, items: [] }); m.get(p.listId).items.push(p); });
    return Array.from(m.values());
  }, [results]);
  const choose = (p) => { setPicked(p); setW(p.w || ''); setD(p.d || ''); };
  const add = () => {
    if (!picked) return;
    onAdd({ name: picked.name, w: Number(w) || 60, d: Number(d) || 60, nodeId: picked.nodeId, productId: picked.productId, image: picked.image, listName: picked.listName, listId: picked.listId, color: RM_COLORS[2] });
  };
  const canLink = typeof onAddFromLink === 'function';
  const submitLink = (e) => { e.preventDefault(); if (canLink && isUrl && !linkBusy) { onAddFromLink(q.trim()); setQ(''); } };
  return (
    <div className="rooms-panel rooms-picker">
      <form className="rooms-picker-head" onSubmit={submitLink}>
        <span className="rooms-panel-h">{heading}</span>
        <input
          ref={ref} type="text" className="rooms-inp"
          placeholder={canLink ? 'Search every list — or paste a product link to add a new item…' : 'Search every list…'}
          value={q}
          onChange={(e) => { setQ(e.target.value); setPicked(null); }}
          onPaste={(e) => {
            const txt = (e.clipboardData && e.clipboardData.getData('text')) || '';
            if (canLink && /^\s*(https?:\/\/|www\.)\S+/i.test(txt) && !linkBusy) { e.preventDefault(); onAddFromLink(txt.trim()); setQ(''); }
          }}
        />
        {canLink && isUrl && <button type="submit" className="rooms-btn rooms-btn--primary" disabled={linkBusy}>{linkBusy ? 'Adding…' : 'Add from link'}</button>}
        <button type="button" className="rooms-btn rooms-btn--ghost" onClick={onCancel}>Close</button>
      </form>
      {linkBusy && <div className="rooms-hint" style={{ marginTop: 6 }}>Fetching the product and adding it to <strong>Uncategorised</strong>…</div>}
      <div className="rooms-listchips">
        <button type="button" className={`rooms-chip${listId === 'all' ? ' is-on' : ''}`} onClick={() => setListId('all')}>All lists <span>{pool.length}</span></button>
        {lists.filter(l => l.count > 0).map(l => (
          <button key={l.id} type="button" className={`rooms-chip${listId === l.id ? ' is-on' : ''}`} onClick={() => setListId(l.id)}>{l.name} <span>{l.count}</span></button>
        ))}
      </div>
      {pool.length === 0 ? (
        <div className="rooms-hint">No list items yet — paste a product link above to add one.</div>
      ) : (
        <div className="rooms-picker-body">
          <div className="rooms-results-wrap">
            {groups.map(g => (
              <div key={g.name} className="rooms-result-group">
                {listId === 'all' && <div className="rooms-result-group-h">{g.name} <span>{g.items.length}</span></div>}
                <ul className="rooms-results">
                  {g.items.map(p => (
                    <li key={p.key}>
                      <button type="button" className={`rooms-result${picked && picked.key === p.key ? ' is-on' : ''}${p.placeholder ? ' is-placeholder' : ''}`} onClick={() => choose(p)}>
                        {p.image ? <img src={p.image} alt="" /> : <span className="rooms-result-noimg" />}
                        <span className="rooms-result-txt">
                          <span className="rooms-result-name">{p.name}</span>
                          <span className="rooms-result-sub">{p.brand ? p.brand + ' · ' : ''}{p.placeholder ? 'placeholder' : ''}{p.w && p.d ? `${p.brand || p.placeholder ? ' · ' : ''}${p.w}×${p.d} cm` : ''}</span>
                        </span>
                      </button>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
            {results.length === 0 && <div className="rooms-hint">Nothing matches{isUrl ? ' — press Add from link to create it' : ''}.</div>}
          </div>
          <div className="rooms-picker-side">
            {picked ? (
              <>
                <div className="rooms-panel-h">{picked.name}</div>
                <div className="rooms-hint">{picked.w && picked.d ? 'Size on file. Adjust if needed — it saves back to the list.' : 'No size on file — enter the footprint; it saves back to the list.'}</div>
                <label className="rooms-field"><span>Width (cm)</span><input type="number" min="5" value={w} onChange={(e) => setW(e.target.value)} /></label>
                <label className="rooms-field"><span>Depth (cm)</span><input type="number" min="5" value={d} onChange={(e) => setD(e.target.value)} /></label>
                <button type="button" className="rooms-btn rooms-btn--primary" onClick={add} disabled={!w || !d}>Place in room</button>
              </>
            ) : <div className="rooms-hint">Pick an item to set its footprint, or paste a link to add something new.</div>}
          </div>
        </div>
      )}
    </div>
  );
}

// Share a room: add people by email (existing users are added at once;
// anyone else gets a magic-link email and is attached automatically the
// first time they open Rooms), see the roster, remove members.
function RoomShareModal({ room, isOwner, myUserId, onClose }) {
  const [members, setMembers] = _rm_s([]);
  const [email, setEmail] = _rm_s('');
  const [busy, setBusy] = _rm_s(false);
  const [msg, setMsg] = _rm_s('');
  const [errMsg, setErrMsg] = _rm_s('');
  const ref = _rm_r(null);
  const load = async () => {
    const res = await window.MR.rooms.members(room.id);
    if (res.ok) setMembers(res.members);
  };
  _rm_e(() => { load(); setTimeout(() => ref.current && ref.current.focus(), 60); }, [room.id]);
  const submit = async (e) => {
    e.preventDefault();
    const em = email.trim().toLowerCase();
    if (!/.+@.+\..+/.test(em)) { setErrMsg("That doesn't look like an email"); return; }
    setBusy(true); setErrMsg(''); setMsg('');
    try {
      const res = await window.MR.rooms.addMemberByEmail(room.id, em);
      if (!res.ok) { setErrMsg(res.reason || 'Could not share'); return; }
      if (res.status === 'added') setMsg(`${em} can now open this room.`);
      else if (res.status === 'already') setMsg(`${em} already has access.`);
      else if (res.status === 'invited') {
        const mail = await window.MR.rooms.sendInviteEmail(em);
        setMsg(mail.ok
          ? `No account yet for ${em} — sent them a sign-in link. The room attaches the moment they open Rooms.`
          : `No account yet for ${em}. They'll get access when they sign up with that email${mail.reason ? ` (email not sent: ${mail.reason})` : ''}.`);
      }
      setEmail('');
      await load();
    } finally { setBusy(false); }
  };
  const remove = async (m) => {
    if (!window.confirm(`Remove ${m.email} from "${room.name}"?`)) return;
    const res = await window.MR.rooms.removeMember(room.id, m.user_id);
    if (res.ok) await load(); else setErrMsg(res.reason || 'Could not remove');
  };
  return (
    <>
      <div className="share-scrim is-open" onClick={busy ? undefined : onClose} />
      <div className="share-modal is-open rooms-share-modal" role="dialog" aria-label="Share room">
        <div className="share-partner-eyebrow">Share room</div>
        <h3 className="share-partner-h">Who can edit <em>{room.name}</em></h3>
        <p className="share-partner-sub">Everyone here can move things around, reshape walls and add items. Changes save for all of you.</p>
        <form onSubmit={submit} className="rooms-share-form">
          <input ref={ref} type="email" className="auth-input" placeholder="partner@email.com" value={email} onChange={(e) => setEmail(e.target.value)} />
          <button type="submit" className="btn" disabled={busy || !email.trim()} style={{ width: 'auto', padding: '10px 16px' }}>
            <span className="btn-row">{busy ? 'Sharing…' : 'Share'}</span>
          </button>
        </form>
        {msg && <div className="rooms-share-msg">{msg}</div>}
        {errMsg && <div className="auth-err">{errMsg}</div>}
        <div className="share-partner-existing">
          <div className="share-partner-existing-lbl">Has access</div>
          <ul className="share-partner-existing-list">
            {members.map((m, i) => (
              <li key={`${m.user_id || m.email}-${i}`} className="share-partner-existing-row">
                <span className="share-partner-existing-email">{m.email}{m.user_id === myUserId ? ' (you)' : ''}</span>
                <span className="share-partner-existing-role">{m.role}</span>
                {m.role === 'member' && (isOwner || m.user_id === myUserId) && (
                  <button type="button" className="rooms-link" onClick={() => remove(m)} style={{ marginLeft: 8 }}>{m.user_id === myUserId ? 'leave' : 'remove'}</button>
                )}
              </li>
            ))}
            {members.length === 0 && <li className="rooms-hint">Just you so far.</li>}
          </ul>
        </div>
        <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 14 }}>
          <button type="button" className="btn btn-ghost" onClick={onClose} style={{ width: 'auto', padding: '10px 16px' }}><span>Done</span></button>
        </div>
      </div>
    </>
  );
}

window.RoomsPage = RoomsPage;
