/* ===== app/floorplan.jsx =====
   Two-level interactive floor plan for the Scanner v2.6 React shell.

     Level 1 — Building Overview: rooms as draggable/resizable boxes on
                                  a canvas. Click a room header to enter.
     Level 2 — Room View: full-screen canvas for one room. Shelves are
                          draggable/resizable boxes containing container
                          tiles. Tiles can be dragged between shelves.
                          Click a tile to open the container inspector.

   Ported from the design-system prototype
   (lonewolf-biotech-design-system/project/floorplan.jsx) and adapted to:
     - render inside <Shell> with useRoute/useToast from index.html
     - source rooms from window.LW_DATA.containers (no JSZip / file picker)
     - rebuild automatically when index.html dispatches 'cloudSyncComplete'
     - reconcile stage codes (IN/RT/RD/MS/MO/AC/DC) with the live 7-stage
       list rather than the prototype's "In Vitro"/"Veg"/"Mixed" labels
   ============================================================ */

(function () {

  /* ── Stage palette ──────────────────────────────────────────────────
     Keys are the canonical stage *names* (not codes). The 7 production
     stages plus a derived "Mixed" bucket and an empty fallback. */
  const FP_STAGE_COLORS = {
    "Initiation":     { bg: "#fce7f3", border: "#ec4899", text: "#9d174d" },
    "Rooting":        { bg: "#fef3c7", border: "#f59e0b", text: "#92400e" },
    "Rooted":         { bg: "#d1fae5", border: "#10b981", text: "#065f46" },
    "Multiplication": { bg: "#dbeafe", border: "#3b82f6", text: "#1e40af" },
    "Mother":         { bg: "#f3e8ff", border: "#a855f7", text: "#6b21a8" },
    "Acclimated":     { bg: "#ecfeff", border: "#06b6d4", text: "#155e75" },
    "Discarded":      { bg: "#ffe4e6", border: "#f43f5e", text: "#9f1239" },
    "Mixed":          { bg: "#f1f5f9", border: "#64748b", text: "#334155" },
    "--":             { bg: "#f8fafc", border: "#cbd5e1", text: "#64748b" },
  };

  /* Map LW_DATA.stages codes to names. Built lazily so changes to
     LW_DATA after sign-in (live Ref_Stages override the mock seed) are
     reflected on the next read. */
  function stageNameFor(codeOrName) {
    if (!codeOrName) return "--";
    if (FP_STAGE_COLORS[codeOrName]) return codeOrName;
    const stages = (window.LW_DATA && window.LW_DATA.stages) || [];
    const hit = stages.find(s => s.code === codeOrName || s.name === codeOrName);
    return (hit && hit.name) || codeOrName;
  }

  function stageColor(stage) {
    return FP_STAGE_COLORS[stage] || FP_STAGE_COLORS["--"];
  }

  /* Design's preferred container icons (chat2 final). Jar/Flask kept as
     legacy fallbacks so historical inventory rows still get a glyph. */
  const FP_CONTAINER_ICONS = {
    "Magenta Box":         "📦",
    "Petri Dish":          "🧫",
    "Petri Dish 8oz lid":  "⚗️",
    "Petri Dish 16oz lid": "⚗️",
    "Pot":                 "🪴",
    "Rooting Cube":        "🟩",
    "Jar":                 "🫙",
    "Flask":               "🧪",
  };

  const SNAP = 20, GRID = 20, MIN_W = 7, MIN_H = 5;
  const SH_MIN_W = 5, SH_MIN_H = 4;
  const STORAGE_KEY = "lw-floorplan-v3";
  const FLOORPLAN_AUTOSAVE_DELAY_MS = 2500;

  function fpSnap(v) { return Math.round(v / SNAP) * SNAP; }

  let _uid = 2000;
  function fpUid(p) { return `${p}-${++_uid}`; }

  /* Bump _uid past every numeric suffix found in `rooms`. Without this,
     IDs generated by fpUid in a fresh session collide with IDs that
     were assigned in a prior session and persisted to HQ — producing
     duplicate React keys (e.g. two containers with id "c-2008"). */
  function fpAdvanceUidPast(rooms) {
    const grab = (id) => {
      if (typeof id !== "string") return;
      const m = id.match(/-(\d+)$/);
      if (m) {
        const n = parseInt(m[1], 10);
        if (n > _uid) _uid = n;
      }
    };
    (rooms || []).forEach(r => {
      grab(r && r.id);
      ((r && r.shelves) || []).forEach(s => {
        grab(s && s.id);
        ((s && s.containers) || []).forEach(c => grab(c && c.id));
      });
    });
  }

  /* Heal any duplicate IDs already present in the loaded data — a
     pre-existing record in HQ may have collisions from earlier code.
     Mutates rooms in place; safe to call right after fpAdvanceUidPast
     so any reissued IDs are guaranteed past the existing range. */
  function fpDedupeIds(rooms) {
    const seen = new Set();
    const fix = (obj, prefix) => {
      if (!obj) return;
      if (typeof obj.id !== "string" || !obj.id) {
        obj.id = fpUid(prefix);
      } else if (seen.has(obj.id)) {
        obj.id = fpUid(prefix);
      }
      seen.add(obj.id);
    };
    (rooms || []).forEach(r => {
      fix(r, "R");
      ((r && r.shelves) || []).forEach(s => {
        fix(s, "s");
        ((s && s.containers) || []).forEach(c => fix(c, "c"));
      });
    });
    return rooms;
  }

  function dominantStage(shelves) {
    const counts = {};
    (shelves || []).forEach(s => (s.containers || []).forEach(c => {
      counts[c.stage] = (counts[c.stage] || 0) + 1;
    }));
    const keys = Object.keys(counts);
    if (!keys.length) return "--";
    const total = Object.values(counts).reduce((a, b) => a + b, 0);
    const top = Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];
    return keys.length > 1 && counts[top] / total < 0.8 ? "Mixed" : top;
  }

  /* ── Location parser ────────────────────────────────────────────────
     Handles the HQ workbook naming conventions and the demo
     R{n}-S{n}-P{n} pattern. Anything unrecognised buckets per location
     so no containers get silently dropped. */
  function parseLocation(raw) {
    const loc = (raw || "").trim();
    if (!loc) return { room: "Unassigned", shelf: "General", raw };

    let m;

    m = loc.match(/^(HLX)-(\d+)-GC(\d+)(.*)$/i);
    if (m) return { room: `HLX-${m[2]} · Grow Chamber ${m[3]}`, shelf: m[4] || "General", raw };

    m = loc.match(/^HLX-(\d+)-Tent-(\d+)$/i);
    if (m) return { room: `HLX-${m[1]} · Tents`, shelf: `Tent ${m[2]}`, raw };

    m = loc.match(/^HLX-(\d+)-Rack(\d+)(.+)$/i);
    if (m) return { room: `HLX-${m[1]} · Racks`, shelf: `Rack ${m[2]} ${m[3]}`, raw };

    m = loc.match(/^S24-Rack(\d+)(.+)$/i);
    if (m) return { room: "S24 · Racks", shelf: `Rack ${m[1]} ${m[2]}`, raw };

    m = loc.match(/^HLX-4x8-Tent-Rack(\d+)(.*)$/i);
    if (m) return { room: "HLX · 4×8 Tent", shelf: `Rack ${m[1]} ${m[2]}`, raw };

    m = loc.match(/^(HLX)-(\d+)-(.+)$/i);
    if (m) return { room: `HLX-${m[2]}`, shelf: m[3], raw };

    m = loc.match(/^R(\d+)-S(\d+)(?:-P(\d+))?$/i);
    if (m) return { room: `Room R${m[1]}`, shelf: `S${m[2]}`, raw };

    return { room: loc, shelf: "General", raw };
  }

  /* ── Live-placement helpers ─────────────────────────────────────────
     A shelf with a non-empty `locationCode` claims every Active_Inventory
     row whose `Location` exactly matches the code. Inventory rows whose
     Location doesn't match any shelf's code are funnelled into a virtual
     "Unassigned" room (rendered but never persisted to floorplan.json).
     Rooms tagged `virtual: true` are stripped before any save. */
  function inventoryRowToContainer(it) {
    const loc = String(it.location || "").trim();
    const identity = (window.LW_FP_DATA && window.LW_FP_DATA.inventoryIdentity)
      ? window.LW_FP_DATA.inventoryIdentity(it)
      : {
          displayId: String(it.id == null ? "" : it.id).trim(),
          databaseId: String(it.containerId || it.container_id || it.id || "").trim(),
        };
    return {
      // Stable UI id derived from the printed barcode so selection survives
      // a re-placement. containerID stays the numeric database key used by
      // writes. Use a 3-part shape so this cannot collide with fpUid("c").
      id: identity.displayId ? `c-INV-${identity.displayId}` : fpUid("c"),
      containerID: identity.databaseId,
      strain: it.strain || it.strainName || "Unknown",
      owner: it.owner || "",
      stage: stageNameFor(it.stage),
      type: it.containerType || it.type || "Magenta Box",
      count: parseInt(it.tissues || it.quantity || it.count || 1, 10) || 1,
      qr: it.qrUrl || it.qrLink || "",
      notes: it.notes || "",
      locRaw: loc,
    };
  }

  function placeInventoryIntoRooms(roomsArr, inv) {
    const codeToShelfId = {};
    for (const r of roomsArr) {
      if (r.virtual) continue;
      for (const s of (r.shelves || [])) {
        const code = String(s.locationCode || "").trim();
        if (code && !codeToShelfId[code]) codeToShelfId[code] = s.id;
      }
    }

    const placements = {};       // shelfId → containers[]
    const unassigned = [];
    for (const it of inv || []) {
      const status = String(it.status || "active").toLowerCase();
      if (status !== "active") continue;
      const loc = String(it.location || "").trim();
      if (!loc) continue;
      const cont = inventoryRowToContainer(it);
      const target = codeToShelfId[loc];
      if (target) (placements[target] || (placements[target] = [])).push(cont);
      else unassigned.push(cont);
    }

    // Merge, don't replace: any non-inventory (c-N) containers already
    // sitting on a shelf are manual placements that haven't been intaked
    // into Active_Inventory yet. They must survive every re-render.
    // Inventory-derived containers (c-INV-*) are stripped and re-added
    // from the freshly-built placements map.
    const mergeFn = (window.LW_FP_DATA && window.LW_FP_DATA.mergeShelfContainers)
      || ((existing, fresh) => (existing || []).filter(c => c && !(typeof c.id === 'string' && c.id.indexOf('c-INV-') === 0)).concat(fresh || []));
    const placedReal = roomsArr.filter(r => !r.virtual).map(r => ({
      ...r,
      shelves: (r.shelves || []).map(s => ({
        ...s,
        containers: mergeFn(s.containers, placements[s.id]),
      })),
    }));

    if (!unassigned.length) return placedReal;
    const rows = Math.ceil(unassigned.length / 3);
    const virtualRoom = {
      id: "R-unassigned",
      label: "📦 Unassigned (no matching shelf)",
      stage: "Mixed",
      virtual: true,
      x: 30, y: 30, w: 280, h: Math.max(180, 80 + rows * 36),
      shelves: [{
        id: "s-unassigned",
        label: "Set a shelf locationCode to claim these",
        locationCode: "",
        containers: unassigned,
      }],
    };
    return [virtualRoom, ...placedReal];
  }

  // Persistence shape: virtual rooms never go to disk, and every shelf
  // is stripped of inventory-derived (c-INV-*) containers so HQ stores
  // layout + manual containers only. Inventory rows always re-derive at
  // render time from Active_Inventory via placeInventoryIntoRooms.
  // The actual implementation lives in app/floorplan-data.js so a Node
  // test harness can import it; this wrapper falls back to an inline
  // version if the script load order somehow skipped it.
  function persistableRooms(rs) {
    if (window.LW_FP_DATA && typeof window.LW_FP_DATA.persistableRooms === 'function') {
      return window.LW_FP_DATA.persistableRooms(rs);
    }
    return (rs || [])
      .filter(r => r && !r.virtual)
      .map(r => ({
        ...r,
        shelves: (r.shelves || []).map(s => ({
          ...s,
          containers: (s.containers || []).filter(c => c && !(typeof c.id === 'string' && c.id.indexOf('c-INV-') === 0)),
        })),
      }));
  }

  /* ── Build rooms from LW_DATA.containers ─────────────────────────── */
  function buildRoomsFromInventory() {
    const inv = (window.LW_DATA && window.LW_DATA.containers) || [];

    const roomsMap = {};
    inv.forEach(it => {
      const { room, shelf, raw } = parseLocation(it.location);
      const identity = (window.LW_FP_DATA && window.LW_FP_DATA.inventoryIdentity)
        ? window.LW_FP_DATA.inventoryIdentity(it)
        : {
            displayId: String(it.id == null ? "" : it.id).trim(),
            databaseId: String(it.containerId || it.container_id || it.id || "").trim(),
          };
      if (!roomsMap[room]) roomsMap[room] = {};
      if (!roomsMap[room][shelf]) roomsMap[room][shelf] = [];
      roomsMap[room][shelf].push({
        // c-INV-* id flags this as an inventory-derived container so
        // persistableRooms strips it before save and live placement
        // re-derives it from Active_Inventory at render time. Without
        // this, Rebuild-from-HQ would persist these containers to
        // floorplan.json and they'd duplicate against live placement
        // on the next visit.
        id: identity.displayId ? `c-INV-${identity.displayId}` : fpUid("c"),
        containerID: identity.databaseId,
        strain: it.strain || it.strainName || "Unknown",
        owner: it.owner || "",
        stage: stageNameFor(it.stage),
        type: it.containerType || it.type || "Magenta Box",
        count: parseInt(it.tissues || it.quantity || it.count || 1, 10) || 1,
        qr: it.qrUrl || it.qrLink || "",
        notes: it.notes || "",
        locRaw: raw,
      });
    });

    const COLS = 3, COL_W = 280, GAP = 30;
    return Object.entries(roomsMap).map(([roomName, shelvesMap], ri) => {
      const col = ri % COLS;
      const row = Math.floor(ri / COLS);
      const shelves = Object.entries(shelvesMap).map(([shelfName, containers]) => {
        // If every container in this bucket came from the same raw HLX
        // code, adopt it as the canonical locationCode so live-placement
        // can join Active_Inventory.Location → shelf without parsing.
        const rawSet = new Set(containers.map(c => c.locRaw).filter(Boolean));
        const locationCode = rawSet.size === 1 ? containers[0].locRaw : "";
        return {
          id: fpUid("s"),
          label: shelfName,
          locationCode,
          containers,
        };
      });
      const h = Math.max(MIN_H * GRID, Math.min(320, 80 + shelves.length * 36));
      return {
        id: fpUid("R"),
        label: roomName,
        stage: dominantStage(shelves),
        x: 30 + col * (COL_W + GAP),
        y: 30 + row * (h + GAP),
        w: COL_W, h,
        shelves,
        fromInventory: true,
      };
    });
  }

  /* Map an inspector patch to canonical workbook field names. Only fields
     accepted by PUT /api/container/:id (see editableFields in server.js)
     are forwarded; anything else stays local-only. */
  function mapPatchToWorkbook(patch) {
    const out = {};
    if ("strain"   in patch) out.Strain         = patch.strain;
    if ("owner"    in patch) out.Owner          = patch.owner;
    if ("stage"    in patch) out.Stage          = patch.stage;
    if ("count"    in patch) out.Tissue_Count   = patch.count;
    if ("type"     in patch) out.Container_Type = patch.type;
    if ("notes"    in patch) out.Notes          = patch.notes;
    if ("location" in patch) out.Location       = patch.location;
    return out;
  }

  /* Auto-layout shelves that have no x/y/w/h yet — used the first time
     a room is opened in the room view. Shelves the user has already
     dragged keep their saved positions. */
  function fpAutoLayoutShelves(shelves) {
    const COLS = 2, SW = 320, SH = 180, GAP = 24;
    return (shelves || []).map((s, i) => {
      if (s.x != null) return s;
      const col = i % COLS, row = Math.floor(i / COLS);
      return { ...s, x: 30 + col * (SW + GAP), y: 30 + row * (SH + GAP), w: SW, h: SH };
    });
  }

  /* ── Persistence ────────────────────────────────────────────────── */
  function loadSaved() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (raw) return JSON.parse(raw);
    } catch (e) { /* fall through */ }
    return null;
  }

  /* Off-device transfer: download the current plan as a JSON file the
     user can email, drop into SharePoint, or load on another device. */
  const FP_EXPORT_VERSION = 1;
  function exportPlanToFile(rooms) {
    const payload = {
      schema: "lw-floorplan",
      version: FP_EXPORT_VERSION,
      exportedAt: new Date().toISOString(),
      rooms,
    };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
    const a = document.createElement("a");
    a.href = url;
    a.download = `lonewolf-floorplan-${stamp}.json`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  function parseImportedPlan(text) {
    const data = JSON.parse(text);
    const rooms = Array.isArray(data) ? data
                : Array.isArray(data && data.rooms) ? data.rooms
                : null;
    if (!rooms) throw new Error("file does not contain a rooms array");
    rooms.forEach((r, i) => {
      if (!r || typeof r !== "object") throw new Error(`room ${i} is not an object`);
      if (!r.id || !Array.isArray(r.shelves)) throw new Error(`room ${i} is missing id or shelves`);
    });
    return rooms;
  }

  function initialRooms() {
    const saved = loadSaved();
    if (saved && Array.isArray(saved) && saved.length) {
      fpAdvanceUidPast(saved); fpDedupeIds(saved);
      return saved;
    }
    const built = buildRoomsFromInventory();
    if (built.length) { fpAdvanceUidPast(built); fpDedupeIds(built); return built; }
    return seedRooms();
  }

  function seedRooms() {
    return [{
      id: "R-seed1", label: "Room A · Multiplication", stage: "Multiplication",
      x: 30, y: 30, w: 280, h: 200,
      shelves: [
        { id: "s-s1", label: "S1", containers: [] },
        { id: "s-s2", label: "S2", containers: [] },
      ],
    }];
  }

  /* ══════════════════════════════════════════════════════════════════
     MAIN COMPONENT — manages top-level view state and CRUD
     ══════════════════════════════════════════════════════════════════ */
  function FloorPlan() {
    const { go } = useRoute();
    const { toast } = useToast();

    const [rooms, setRooms]               = React.useState(initialRooms);
    const [view, setView]                 = React.useState("overview"); // 'overview' | 'room'
    const [activeRoomId, setActiveRoomId] = React.useState(null);
    const [rebuilding, setRebuilding]     = React.useState(false);
    /* QR scan landings (?focusContainer=<id>) seed this id; the
       navigation effect below finds the matching room once placement
       has populated `rooms` and FPRoomView selects the tile. */
    const [focusContainerID, setFocusContainerID] = React.useState(() => {
      if (typeof window === "undefined") return null;
      const fid = window.__lwFocusContainer || null;
      try { window.__lwFocusContainer = null; } catch (e) {}
      return fid;
    });

    /* HQ-level layout sync (the whole rooms/shelves graph as one JSON
       doc in SharePoint). Distinct from per-container sync below. */
    const [hqStatus, setHqStatus]         = React.useState({ state: "loading" });
    // state: 'loading' | 'saved' | 'saving' | 'offline' | 'conflict' | 'unauthed' | 'empty-remote' | 'error'
    const hqEtagRef                       = React.useRef(null);
    const hqLoadedRef                     = React.useRef(false); // true once initial GET resolved
    const hqSaveTimer                     = React.useRef(null);
    /* hqStatusRef mirrors hqStatus so the autosave effect can read the
       latest state without depending on it (which would re-trigger the
       effect every time we set 'saving' → 'saved' and create a loop). */
    const hqStatusRef                     = React.useRef({ state: "loading" });
    React.useEffect(() => { hqStatusRef.current = hqStatus; }, [hqStatus]);
    /* lastSavedRoomsJsonRef holds the JSON we most recently sent (or
       received) successfully. The autosave skips when current rooms
       match — prevents a no-op echo of the GET response back to HQ. */
    const lastSavedRoomsJsonRef           = React.useRef(null);

    /* HQ sync plumbing — debounced PUT /api/container/:id keyed by HQ
       containerID. Containers without a containerID (locally created via
       +Add) stay local-only. */
    const syncTimers      = React.useRef({}); // { containerID: timeoutId }
    const pendingPatch    = React.useRef({}); // { containerID: { Strain: ..., Stage: ..., ... } }
    const syncStatusRef   = React.useRef({}); // { containerID: { state, message?, status?, at? } }
    const containerLabelRef = React.useRef({}); // { containerID: display barcode/label for error toasts }
    const [, forceTick] = React.useReducer(x => x + 1, 0); // re-render inspector when status changes

    /* Always-on offline cache — survives reloads and lets the UI render
       instantly while the HQ fetch is in flight. */
    React.useEffect(() => {
      try { localStorage.setItem(STORAGE_KEY, JSON.stringify(persistableRooms(rooms))); } catch (e) {}
    }, [rooms]);

    /* ── Load shared plan from HQ on mount ─────────────────────────── */
    React.useEffect(() => {
      let cancelled = false;
      (async () => {
        if (!window.LW_API || typeof window.LW_API.getFloorPlan !== "function") {
          setHqStatus({ state: "offline", message: "HQ API unavailable" });
          hqLoadedRef.current = true;
          return;
        }
        try {
          const res = await window.LW_API.getFloorPlan();
          if (cancelled) return;
          hqEtagRef.current = res.etag || null;
          if (res.exists && Array.isArray(res.rooms) && res.rooms.length) {
            fpAdvanceUidPast(res.rooms);
            // Capture the pre-dedupe JSON so that if HQ has duplicate
            // IDs from earlier code, the next autosave pushes the
            // healed version up and heals HQ. If dedupe is a no-op,
            // the snapshots match and no save fires.
            // Snapshots track the persistence shape, so re-applying live
            // placement (which prepends the virtual Unassigned room) does
            // not register as a "changed" room and does not trigger a
            // spurious save back to HQ.
            lastSavedRoomsJsonRef.current = JSON.stringify(persistableRooms(res.rooms));
            fpDedupeIds(res.rooms);
            // Apply live placement immediately if inventory is already
            // in window.LW_DATA — avoids a flash of empty shelves before
            // cloudSyncComplete fires.
            const inv = (window.LW_DATA && window.LW_DATA.containers) || [];
            setRooms(inv.length ? placeInventoryIntoRooms(res.rooms, inv) : res.rooms);
            setHqStatus({ state: "saved", at: Date.now() });
          } else {
            // No plan yet on the server. Keep whatever we already have
            // locally (from localStorage / inventory rebuild) and offer
            // the user a one-click migration.
            setHqStatus({ state: "empty-remote" });
          }
        } catch (e) {
          if (cancelled) return;
          if (e.status === 401) setHqStatus({ state: "unauthed", message: "Sign in to sync the floor plan" });
          else                  setHqStatus({ state: "offline",  message: e.message });
        } finally {
          hqLoadedRef.current = true;
        }
      })();
      return () => { cancelled = true; };
    }, []);

    /* ── Debounced autosave to HQ ──────────────────────────────────────
       Depends ONLY on rooms — never on hqStatus.state, since this effect
       itself sets state to 'saving' / 'saved' and including it in deps
       would create an infinite re-save loop. Skip-state checks read
       hqStatusRef so they always see the latest value. The
       lastSavedRoomsJsonRef guard prevents echoing the GET response
       back to HQ on initial load. */
    React.useEffect(() => {
      if (!hqLoadedRef.current) return;
      const s = hqStatusRef.current.state;
      if (s === "unauthed" || s === "conflict" || s === "empty-remote") return;
      if (!window.LW_API || typeof window.LW_API.saveFloorPlan !== "function") return;

      const pRooms = persistableRooms(rooms);
      const json = JSON.stringify(pRooms);
      if (json === lastSavedRoomsJsonRef.current) return; // nothing to save

      clearTimeout(hqSaveTimer.current);
      setHqStatus(prev => {
        if (prev && (prev.state === "unauthed" || prev.state === "conflict" || prev.state === "empty-remote")) return prev;
        return { ...prev, state: "queued", message: "Auto-save queued; keep working." };
      });
      hqSaveTimer.current = setTimeout(async () => {
        setHqStatus(prev => ({ ...prev, state: "saving" }));
        try {
          const result = await window.LW_API.saveFloorPlan(pRooms, hqEtagRef.current);
          hqEtagRef.current = result.etag || hqEtagRef.current;
          lastSavedRoomsJsonRef.current = json;
          setHqStatus({ state: "saved", at: Date.now() });
        } catch (e) {
          if (e.status === 409 || e.status === 412) {
            setHqStatus({ state: "conflict", message: "Someone else updated the plan — reload to merge." });
          } else if (e.status === 401) {
            setHqStatus({ state: "unauthed", message: "Sign in to sync the floor plan" });
          } else {
            setHqStatus({ state: "error", message: e.message || String(e) });
          }
        }
      }, FLOORPLAN_AUTOSAVE_DELAY_MS);
      return () => clearTimeout(hqSaveTimer.current);
    }, [rooms]);

    /* Safety-net dedup: every time rooms changes, scan for duplicate IDs
       and heal them. The data-load paths all call fpDedupeIds, but if a
       duplicate ever sneaks past (older HQ payload, partial migration,
       a future regression), this catches it before React renders and
       throws the "two children with the same key" warning. */
    React.useEffect(() => {
      const seen = new Set();
      let dupId = null;
      outer: for (const r of (rooms || [])) {
        if (r && typeof r.id === "string") {
          if (seen.has(r.id)) { dupId = r.id; break; }
          seen.add(r.id);
        }
        for (const s of (r && r.shelves) || []) {
          if (s && typeof s.id === "string") {
            if (seen.has(s.id)) { dupId = s.id; break outer; }
            seen.add(s.id);
          }
          for (const c of (s && s.containers) || []) {
            if (c && typeof c.id === "string") {
              if (seen.has(c.id)) { dupId = c.id; break outer; }
              seen.add(c.id);
            }
          }
        }
      }
      if (!dupId) return;
      console.warn("[FloorPlan] Healing duplicate id in rooms state:", dupId);
      const cloned = JSON.parse(JSON.stringify(rooms));
      fpAdvanceUidPast(cloned);
      fpDedupeIds(cloned);
      setRooms(cloned);
    }, [rooms]);

    /* ── Reload from HQ (used by the Conflict banner) ──────────────── */
    async function reloadFromHQ() {
      if (!window.LW_API || typeof window.LW_API.getFloorPlan !== "function") return;
      setHqStatus({ state: "loading" });
      try {
        const res = await window.LW_API.getFloorPlan();
        hqEtagRef.current = res.etag || null;
        if (res.exists && Array.isArray(res.rooms)) {
          fpAdvanceUidPast(res.rooms);
          lastSavedRoomsJsonRef.current = JSON.stringify(persistableRooms(res.rooms));
          fpDedupeIds(res.rooms);
          const inv = (window.LW_DATA && window.LW_DATA.containers) || [];
          setRooms(inv.length ? placeInventoryIntoRooms(res.rooms, inv) : res.rooms);
          setHqStatus({ state: "saved", at: Date.now() });
          toast("✓ Reloaded latest floor plan from HQ");
        } else {
          setHqStatus({ state: "empty-remote" });
        }
      } catch (e) {
        setHqStatus({ state: "error", message: e.message || String(e) });
        toast("⚠ Reload failed: " + (e.message || e));
      }
    }

    /* ── One-time migration: push the local plan up to HQ ──────────── */
    async function pushLocalToHQ() {
      if (!window.LW_API || typeof window.LW_API.saveFloorPlan !== "function") {
        toast("⚠ HQ API unavailable");
        return;
      }
      if (hqStatus.state !== "empty-remote") {
        const ok = window.confirm(
          "A floor plan already exists on HQ. Pushing your local plan will overwrite it. Continue?"
        );
        if (!ok) return;
      }
      setHqStatus({ state: "saving" });
      try {
        // Force-overwrite on first migration: pass etag=null so no
        // If-Match header is sent. After this PUT, the server's etag
        // becomes the new source of truth.
        const pRooms = persistableRooms(rooms);
        const json = JSON.stringify(pRooms);
        const result = await window.LW_API.saveFloorPlan(pRooms, null);
        hqEtagRef.current = result.etag || hqEtagRef.current;
        lastSavedRoomsJsonRef.current = json;
        setHqStatus({ state: "saved", at: Date.now() });
        toast(`✓ Pushed ${pRooms.length} rooms to HQ — everyone with access will see this plan`);
      } catch (e) {
        setHqStatus({ state: "error", message: e.message || String(e) });
        toast("⚠ Push failed: " + (e.message || e));
      }
    }

    /* Live placement: re-derive every shelf's container list from
       Active_Inventory whenever LW_DATA refreshes. The user-owned
       structure (rooms, shelf positions, locationCodes) is preserved;
       only `containers` arrays are rewritten. Containers whose
       inventory.Location doesn't match any shelf.locationCode flow into
       the virtual "Unassigned" room. */
    React.useEffect(() => {
      const runPlacement = () => {
        const inv = (window.LW_DATA && window.LW_DATA.containers) || [];
        if (!inv.length) return;
        setRooms(rs => placeInventoryIntoRooms(rs, inv));
      };
      document.addEventListener("cloudSyncComplete", runPlacement);
      // Bootstrap: if LW_DATA is already hydrated (common when the user
      // navigates to Floor Plan after the dashboard has finished loading),
      // place immediately rather than waiting for the next sync event.
      runPlacement();
      return () => document.removeEventListener("cloudSyncComplete", runPlacement);
    }, []);

    /* QR scan navigation: when a focusContainerID is set (seeded from
       window.__lwFocusContainer in the router boot), find the room that
       holds the matching containerID and switch to room view. The room
       view picks up the same id and selects the tile. The URL param is
       stripped after navigation so a reload doesn't re-trigger focus. */
    React.useEffect(() => {
      if (!focusContainerID) return;
      for (const r of rooms) {
        for (const s of (r.shelves || [])) {
          for (const c of (s.containers || [])) {
            if (c.containerID === focusContainerID) {
              setActiveRoomId(r.id);
              setView("room");
              try {
                const u = new URL(window.location.href);
                if (u.searchParams.has("focusContainer")) {
                  u.searchParams.delete("focusContainer");
                  window.history.replaceState({}, "", u.pathname + (u.search ? `?${u.searchParams.toString()}` : "") + u.hash);
                }
              } catch (e) {}
              return;
            }
          }
        }
      }
    }, [rooms, focusContainerID]);

    /* ── Room CRUD ─────────────────────────────────────────────────── */
    const updRoom = (id, patch) => setRooms(rs => rs.map(r => r.id === id ? { ...r, ...patch } : r));
    const delRoom = (id) => {
      setRooms(rs => rs.filter(r => r.id !== id));
      if (activeRoomId === id) { setActiveRoomId(null); setView("overview"); }
    };
    const addRoom = () => {
      const id = fpUid("R");
      setRooms(rs => [...rs, {
        id, label: "New Room", stage: "Multiplication",
        x: 60, y: 60, w: 260, h: 180,
        shelves: [{ id: fpUid("s"), label: "Shelf 1", containers: [] }],
      }]);
      setActiveRoomId(id);
      setView("room");
    };

    /* ── Shelf CRUD ────────────────────────────────────────────────── */
    const updShelf = (rId, sId, patch) => setRooms(rs => rs.map(r =>
      r.id === rId ? { ...r, shelves: r.shelves.map(s => s.id === sId ? { ...s, ...patch } : s) } : r
    ));
    const delShelf = (rId, sId) => setRooms(rs => rs.map(r =>
      r.id === rId ? { ...r, shelves: r.shelves.filter(s => s.id !== sId) } : r
    ));
    const addShelf = (rId) => {
      const id = fpUid("s");
      const room = rooms.find(r => r.id === rId);
      const i = (room?.shelves || []).length;
      const COLS = 2, SW = 320, SH = 180, GAP = 24;
      const col = i % COLS, row = Math.floor(i / COLS);
      setRooms(rs => rs.map(r => r.id === rId
        ? { ...r, shelves: [...r.shelves, {
            id, label: `Shelf ${r.shelves.length + 1}`, locationCode: "", containers: [],
            x: 30 + col * (SW + GAP), y: 30 + row * (SH + GAP), w: SW, h: SH,
          }] }
        : r));
    };

    /* ── Container CRUD ────────────────────────────────────────────── */
    const updCt = (rId, sId, cId, patch) => {
      let containerID = null;
      let displayLabel = null;
      setRooms(rs => rs.map(r => {
        if (r.id !== rId) return r;
        return { ...r, shelves: r.shelves.map(s => {
          if (s.id !== sId) return s;
          return { ...s, containers: s.containers.map(c => {
            if (c.id !== cId) return c;
            const next = { ...c, ...patch };
            containerID = next.containerID || c.containerID || null;
            displayLabel = next.id || c.id || null;
            return next;
          }) };
        }) };
      }));
      if (containerID) scheduleSync(containerID, patch, displayLabel);
    };

    // Strip the c-INV- prefix so a failure toast shows the printed HQ
    // barcode ("INIT-Royale 91-Vibe-2026-08-26-3365") rather than the local
    // React-state id. Manual (non-HQ) containers have no barcode to show,
    // so their plain c-<n> id is used as-is -- still more legible than the
    // bare numeric containerID.
    function displayLabelFromId(id) {
      return typeof id === "string" && id.indexOf("c-INV-") === 0
        ? id.slice("c-INV-".length)
        : id;
    }

    function scheduleSync(containerID, patch, displayLabel) {
      const fields = mapPatchToWorkbook(patch);
      if (!Object.keys(fields).length) return; // nothing the workbook accepts
      if (displayLabel) containerLabelRef.current[containerID] = displayLabelFromId(displayLabel);
      const buf = pendingPatch.current[containerID] || (pendingPatch.current[containerID] = {});
      Object.assign(buf, fields);

      clearTimeout(syncTimers.current[containerID]);
      syncTimers.current[containerID] = setTimeout(async () => {
        const updates = pendingPatch.current[containerID];
        delete pendingPatch.current[containerID];
        if (!updates) return;
        syncStatusRef.current[containerID] = { state: "syncing" };
        forceTick();
        try {
          if (!window.LW_API || typeof window.LW_API.updateContainer !== "function") {
            throw new Error("LW_API.updateContainer unavailable");
          }
          await window.LW_API.updateContainer(containerID, updates);
          syncStatusRef.current[containerID] = { state: "ok", at: Date.now() };
        } catch (e) {
          syncStatusRef.current[containerID] = {
            state: e.status === 403 ? "forbidden" : e.status === 401 ? "unauthorized" : "error",
            message: e.message || String(e),
            status: e.status,
          };
          // "was not moved" only applies when the failed batch actually
          // included a shelf move (Location); every other field edit
          // (strain/owner/stage/count/type/notes) failing said the same
          // misleading thing before this fix. Name the tile's display
          // barcode/label, not the bare numeric Postgres PK.
          const label = containerLabelRef.current[containerID] || containerID;
          const verb = ("Location" in updates) ? "was not moved" : "update failed";
          toast(`Container ${label} ${verb}: ${e.message || e}`, "error");
        }
        forceTick();
      }, 800);
    }

    function getSyncState(containerID) {
      if (!containerID) return { state: "local" };
      if (pendingPatch.current[containerID]) return { state: "pending" };
      return syncStatusRef.current[containerID] || { state: "idle" };
    }
    const delCt = (rId, sId, cId) => setRooms(rs => rs.map(r =>
      r.id === rId ? {
        ...r, shelves: r.shelves.map(s => s.id === sId ? {
          ...s, containers: s.containers.filter(c => c.id !== cId),
        } : s),
      } : r));
    const addCt = (rId, sId, spec) => {
      const id = fpUid("c");
      setRooms(rs => rs.map(r => r.id === rId ? {
        ...r, shelves: r.shelves.map(s => s.id === sId ? {
          ...s, containers: [...s.containers, { id, ...spec }],
        } : s),
      } : r));
    };
    const moveCt = (rId, fromSId, toSId, cId) => {
      const room = rooms.find(r => r.id === rId);
      if (!room) return;
      const fromShelf = room.shelves.find(s => s.id === fromSId);
      const toShelf   = room.shelves.find(s => s.id === toSId);
      const ct = fromShelf && fromShelf.containers.find(c => c.id === cId);
      if (!ct || !toShelf) return;

      // Decide whether this move should sync to the workbook, refuse,
      // or stay local. See classifyMove in app/floorplan-data.js for
      // the full decision table.
      const classify = (window.LW_FP_DATA && window.LW_FP_DATA.classifyMove)
        || ((c, d) => {
          const cid = c && typeof c.containerID === 'string' ? c.containerID.trim() : '';
          const dc  = d && typeof d.locationCode === 'string' ? d.locationCode.trim() : '';
          if (!cid) return { kind: 'local-only' };
          if (!dc)  return { kind: 'refuse', reason: 'dest-shelf-has-no-locationCode' };
          return { kind: 'sync', containerID: cid, location: dc };
        });
      const plan = classify(ct, toShelf);

      if (plan.kind === 'refuse') {
        toast(`⚠ Set a location code on "${toShelf.label || 'this shelf'}" before moving HQ-tracked containers here`);
        return;
      }

      setRooms(rs => rs.map(r => {
        if (r.id !== rId) return r;
        return { ...r, shelves: r.shelves.map(s => {
          if (s.id === fromSId) return { ...s, containers: s.containers.filter(c => c.id !== cId) };
          if (s.id === toSId)   return { ...s, containers: [...s.containers, ct] };
          return s;
        }) };
      }));

      if (plan.kind === 'sync') {
        scheduleSync(plan.containerID, { location: plan.location }, ct.id);
      }
    };

    /* ── Build from live HQ inventory (button) ─────────────────────── */
    async function rebuildFromHQ() {
      setRebuilding(true);
      try {
        if (window.LW_API && typeof window.LW_API.getInventory === "function") {
          const res = await window.LW_API.getInventory({ limit: 500 });
          if (res && Array.isArray(res.containers) && res.containers.length) {
            // containerId (and qrSerial) must survive this remap -- without
            // it, inventoryIdentity falls back to the printed barcode as
            // the write-path id, and a subsequent drag PUTs
            // /api/container/<barcode> instead of /api/container/<pk>,
            // which Postgres 400s and silently loses the move (AC10,
            // codex commit 6a2de6a post-hoc fix #2).
            window.LW_DATA.containers = res.containers.map(c => ({
              id: c.id,
              containerId: c.containerId != null ? c.containerId : c.container_id,
              qrSerial: c.qrSerial,
              stage: c.stage,
              strain: c.strainName,
              owner: c.owner,
              date: c.dateCreated,
              tissues: c.quantity || 0,
              location: c.location,
              status: c.status || "active",
              containerType: c.containerType,
              notes: c.notes,
            }));
          }
        }
        const built = buildRoomsFromInventory();
        if (!built.length) { toast("⚠ No inventory locations to plot"); return; }
        fpAdvanceUidPast(built); fpDedupeIds(built);
        setRooms(built);
        setActiveRoomId(null);
        setView("overview");
        toast(`✓ Floor plan rebuilt — ${built.length} rooms`);
      } catch (err) {
        toast(`⚠ Rebuild failed: ${err.message}`);
      } finally {
        setRebuilding(false);
      }
    }

    const activeRoom = rooms.find(r => r.id === activeRoomId);

    const crumb = view === "room" && activeRoom ? `Floor Plan › ${activeRoom.label}` : "Floor Plan";

    /* Force an immediate (non-debounced) save to HQ. Used by the
       toolbar 💾 button. Same code path as the debounced effect, just
       fired synchronously and with its own toast feedback.

       Save adapts to the current HQ state instead of bailing out:
         - empty-remote → promote to a first-time push (no confirm needed,
           there's no remote plan to overwrite)
         - conflict     → caller is told to reload first; no point sending
           a save we know will 412
         - everything else → normal If-Match save */
    async function saveNowToHQ() {
      // Normalize to the same persistable shape the autosave uses,
      // otherwise the manual save would push c-INV-* containers and
      // virtual rooms to HQ and leave a stale skip-snapshot behind.
      const pRooms = persistableRooms(rooms);
      try { localStorage.setItem(STORAGE_KEY, JSON.stringify(pRooms)); } catch (e) {}

      if (!window.LW_API || typeof window.LW_API.saveFloorPlan !== "function") {
        toast("Saved locally — HQ API unavailable");
        return;
      }
      if (hqStatus.state === "unauthed") {
        toast("⚠ Saved locally — sign in to sync to HQ");
        return;
      }
      if (hqStatus.state === "empty-remote") {
        await pushLocalToHQ();
        return;
      }
      if (hqStatus.state === "conflict") {
        toast("⚠ Reload from HQ first — your save would overwrite remote changes");
        return;
      }

      clearTimeout(hqSaveTimer.current);
      setHqStatus(s => ({ ...s, state: "saving" }));
      try {
        const json = JSON.stringify(pRooms);
        const result = await window.LW_API.saveFloorPlan(pRooms, hqEtagRef.current);
        hqEtagRef.current = result.etag || hqEtagRef.current;
        lastSavedRoomsJsonRef.current = json;
        setHqStatus({ state: "saved", at: Date.now() });
        toast(`✓ Floor plan saved to HQ — ${pRooms.length} rooms`);
      } catch (e) {
        if (e.status === 409 || e.status === 412) {
          setHqStatus({ state: "conflict", message: e.message });
          toast("⚠ Conflict — someone else updated the plan. Reload from HQ.");
        } else if (e.status === 401) {
          setHqStatus({ state: "unauthed", message: "Sign in to sync the floor plan" });
          toast("⚠ Sign-in expired — saved locally only");
        } else {
          setHqStatus({ state: "error", message: e.message || String(e) });
          toast("⚠ HQ save failed: " + (e.message || e));
        }
      }
    }

    return (
      <Shell crumb={crumb}>
        <div style={{ display: "flex", flexDirection: "column", height: "calc(100vh - 60px)", overflow: "hidden" }}>
          {view === "overview" ? (
            <FPOverview
              rooms={rooms}
              rebuilding={rebuilding}
              hqStatus={hqStatus}
              onEnterRoom={id => { setActiveRoomId(id); setView("room"); }}
              onAddRoom={addRoom}
              onRebuildFromHQ={rebuildFromHQ}
              onUpdRoom={updRoom}
              onBack={() => go("locations")}
              onSave={saveNowToHQ}
              onPushLocalToHQ={pushLocalToHQ}
              onReloadFromHQ={reloadFromHQ}
              onExport={() => {
                try { const p = persistableRooms(rooms); exportPlanToFile(p); toast(`✓ Exported ${p.length} rooms`); }
                catch (e) { toast("⚠ Export failed: " + e.message); }
              }}
              onImport={async (file) => {
                try {
                  const text = await file.text();
                  const incoming = parseImportedPlan(text);
                  fpAdvanceUidPast(incoming); fpDedupeIds(incoming);
                  setRooms(incoming);
                  setActiveRoomId(null);
                  toast(`✓ Imported ${incoming.length} rooms from ${file.name}`);
                } catch (e) {
                  toast("⚠ Import failed: " + e.message);
                }
              }}
            />
          ) : (
            activeRoom && (
              <FPRoomView
                room={activeRoom}
                onBack={() => { setView("overview"); }}
                onUpdRoom={p => updRoom(activeRoom.id, p)}
                onDelRoom={() => delRoom(activeRoom.id)}
                onAddShelf={() => addShelf(activeRoom.id)}
                onUpdShelf={(sId, p) => updShelf(activeRoom.id, sId, p)}
                onDelShelf={sId => delShelf(activeRoom.id, sId)}
                onAddCt={(sId, spec) => addCt(activeRoom.id, sId, spec)}
                onUpdCt={(sId, cId, p) => updCt(activeRoom.id, sId, cId, p)}
                onDelCt={(sId, cId) => delCt(activeRoom.id, sId, cId)}
                onMoveCt={(fromS, toS, cId) => moveCt(activeRoom.id, fromS, toS, cId)}
                getSyncState={getSyncState}
                onSave={saveNowToHQ}
                focusContainerID={focusContainerID}
                onFocusConsumed={() => setFocusContainerID(null)}
              />
            )
          )}
        </div>
      </Shell>
    );
  }

  /* ══════════════════════════════════════════════════════════════════
     LEVEL 1 — BUILDING OVERVIEW
     Rooms as draggable/resizable boxes. Click header → enter room.
     ══════════════════════════════════════════════════════════════════ */
  function FPOverview({ rooms, rebuilding, hqStatus, onEnterRoom, onAddRoom, onRebuildFromHQ, onUpdRoom, onBack, onSave, onExport, onImport, onPushLocalToHQ, onReloadFromHQ }) {
    const [dragging, setDragging] = React.useState(null);
    const [resizing, setResizing] = React.useState(null);
    const [zoom, setZoom]         = React.useState(1);
    const [showGrid, setShowGrid] = React.useState(true);
    const importInputRef          = React.useRef(null);

    React.useEffect(() => {
      const mm = e => {
        if (dragging) {
          const dx = (e.clientX - dragging.sx) / zoom, dy = (e.clientY - dragging.sy) / zoom;
          onUpdRoom(dragging.id, { x: fpSnap(Math.max(0, dragging.ox + dx)), y: fpSnap(Math.max(0, dragging.oy + dy)) });
        }
        if (resizing) {
          const dx = (e.clientX - resizing.sx) / zoom, dy = (e.clientY - resizing.sy) / zoom;
          onUpdRoom(resizing.id, { w: fpSnap(Math.max(MIN_W * GRID, resizing.ow + dx)), h: fpSnap(Math.max(MIN_H * GRID, resizing.oh + dy)) });
        }
      };
      const mu = () => { setDragging(null); setResizing(null); };
      window.addEventListener("mousemove", mm);
      window.addEventListener("mouseup", mu);
      return () => { window.removeEventListener("mousemove", mm); window.removeEventListener("mouseup", mu); };
    }, [dragging, resizing, zoom, onUpdRoom]);

    const totalRooms      = rooms.length;
    const totalContainers = rooms.reduce((a, r) => a + r.shelves.reduce((b, s) => b + s.containers.length, 0), 0);
    const totalTissues    = rooms.reduce((a, r) => a + r.shelves.reduce((b, s) => b + s.containers.reduce((c, ct) => c + (ct.count || 0), 0), 0), 0);
    const canvasW = Math.max(1000, ...rooms.map(r => r.x + r.w + 80));
    const canvasH = Math.max(700,  ...rooms.map(r => r.y + r.h + 80));

    return (
      <>
        {/* Toolbar */}
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 18px", background: "#fff", borderBottom: "1px solid var(--lw-border)", flexShrink: 0, flexWrap: "wrap" }}>
          <button className="lw-btn lw-btn-ghost lw-btn-sm" onClick={onBack}>← Locations</button>
          <div style={{ width: 1, height: 20, background: "var(--lw-border)" }} />
          <button className="lw-btn lw-btn-primary lw-btn-sm" onClick={onAddRoom}>+ Add Room</button>
          <button className="lw-btn lw-btn-secondary lw-btn-sm" onClick={onRebuildFromHQ} disabled={rebuilding}>
            {rebuilding ? "⏳ Rebuilding…" : "🔄 Build from HQ"}
          </button>
          <div style={{ flex: 1 }} />
          <div style={{ display: "flex", gap: 12, fontSize: "0.78rem", color: "var(--lw-text-muted)", alignItems: "center" }}>
            <span><strong style={{ color: "var(--lw-text-primary)" }}>{totalRooms}</strong> rooms</span>
            <span>·</span>
            <span><strong style={{ color: "var(--lw-text-primary)" }}>{totalContainers}</strong> containers</span>
            <span>·</span>
            <span><strong style={{ color: "var(--lw-emerald-600)" }}>{totalTissues}</strong> tissues</span>
          </div>
          <div style={{ width: 1, height: 20, background: "var(--lw-border)" }} />
          <label style={{ display: "flex", alignItems: "center", gap: 5, fontSize: "0.78rem", color: "var(--lw-text-muted)", cursor: "pointer" }}>
            <input type="checkbox" checked={showGrid} onChange={e => setShowGrid(e.target.checked)} /> Grid
          </label>
          <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
            <button className="lw-btn lw-btn-ghost lw-btn-sm" onClick={() => setZoom(z => Math.max(0.4, +(z - 0.1).toFixed(1)))}>−</button>
            <span style={{ fontSize: "0.78rem", minWidth: 36, textAlign: "center", color: "var(--lw-text-muted)" }}>{Math.round(zoom * 100)}%</span>
            <button className="lw-btn lw-btn-ghost lw-btn-sm" onClick={() => setZoom(z => Math.min(2, +(z + 0.1).toFixed(1)))}>+</button>
          </div>
          <FPHqStatusPill hqStatus={hqStatus} />
          <button className="lw-btn lw-btn-secondary lw-btn-sm" onClick={onSave}>💾 Save</button>
          {hqStatus && hqStatus.state === "empty-remote" && (
            <button className="lw-btn lw-btn-primary lw-btn-sm" onClick={onPushLocalToHQ} title="Upload this device's floor plan to SharePoint so everyone with access can see it">📤 Push to HQ</button>
          )}
          <button className="lw-btn lw-btn-secondary lw-btn-sm" onClick={onExport} title="Download the current floor plan as a JSON file (offline backup)">⬇ Export</button>
          <button className="lw-btn lw-btn-secondary lw-btn-sm" onClick={() => importInputRef.current && importInputRef.current.click()} title="Load a previously exported floor plan JSON file">⬆ Import</button>
          <input
            ref={importInputRef}
            type="file"
            accept="application/json,.json"
            style={{ display: "none" }}
            onChange={e => {
              const f = e.target.files && e.target.files[0];
              if (f) onImport(f);
              e.target.value = "";
            }}
          />
        </div>

        {/* HQ banners — conflict / empty-remote / unauthed */}
        {hqStatus && hqStatus.state === "conflict" && (
          <div style={{ background: "#fff7ed", borderBottom: "1px solid #fdba74", padding: "8px 18px", fontSize: "0.8rem", color: "#9a3412", display: "flex", alignItems: "center", gap: 12 }}>
            <span>⚠ {hqStatus.message || "Someone else updated the plan."}</span>
            <button className="lw-btn lw-btn-secondary lw-btn-sm" onClick={onReloadFromHQ}>🔄 Reload from HQ</button>
          </div>
        )}
        {hqStatus && hqStatus.state === "empty-remote" && (
          <div style={{ background: "#eff6ff", borderBottom: "1px solid #93c5fd", padding: "8px 18px", fontSize: "0.8rem", color: "#1e3a8a", display: "flex", alignItems: "center", gap: 12 }}>
            <span>ℹ No shared floor plan on HQ yet. Click <strong>📤 Push to HQ</strong> to publish this device's plan to everyone.</span>
          </div>
        )}
        {hqStatus && hqStatus.state === "unauthed" && (
          <div style={{ background: "#fef2f2", borderBottom: "1px solid #fca5a5", padding: "8px 18px", fontSize: "0.8rem", color: "#991b1b" }}>
            ⚠ Not signed in — edits stay local only and are not shared. Sign in to sync the floor plan.
          </div>
        )}

        {/* Hint bar */}
        <div style={{ background: "var(--lw-slate-50)", borderBottom: "1px solid var(--lw-border)", padding: "6px 20px", fontSize: "0.72rem", color: "var(--lw-text-muted)", flexShrink: 0, display: "flex", alignItems: "center", gap: 16, flexWrap: "wrap" }}>
          <span>🖱 <strong>Drag</strong> to reposition rooms</span>
          <span>↘ <strong>Resize</strong> via corner handle</span>
          <span>🖱 <strong>Click</strong> a room header to open its layout</span>
          <div style={{ marginLeft: "auto", display: "flex", gap: 8, flexWrap: "wrap" }}>
            {Object.entries(FP_STAGE_COLORS).filter(([k]) => k !== "--").map(([s, c]) => (
              <div key={s} style={{ display: "flex", alignItems: "center", gap: 3 }}>
                <div style={{ width: 8, height: 8, borderRadius: 2, background: c.border }} />
                <span style={{ fontSize: "0.62rem", color: "var(--lw-text-muted)" }}>{s}</span>
              </div>
            ))}
          </div>
        </div>

        {/* Canvas */}
        <div style={{ flex: 1, overflow: "auto", background: "#eef0f3" }}>
          <div style={{
            position: "relative",
            width: canvasW * zoom, height: canvasH * zoom,
            transform: `scale(${zoom})`, transformOrigin: "0 0",
            backgroundImage: showGrid
              ? "linear-gradient(rgba(0,0,0,0.07) 1px, transparent 1px), linear-gradient(90deg, rgba(0,0,0,0.07) 1px, transparent 1px)"
              : "none",
            backgroundSize: `${GRID}px ${GRID}px`,
          }}>
            {rooms.map(room => {
              const col = stageColor(room.stage);
              const cCount = room.shelves.reduce((a, s) => a + s.containers.length, 0);
              const tCount = room.shelves.reduce((a, s) => a + s.containers.reduce((b, c) => b + (c.count || 0), 0), 0);
              return (
                <div key={room.id}
                     onMouseDown={e => {
                       if (e.target.classList.contains("fp-rsz")) return;
                       e.preventDefault();
                       setDragging({ id: room.id, sx: e.clientX, sy: e.clientY, ox: room.x, oy: room.y });
                     }}
                     style={{
                       position: "absolute", left: room.x, top: room.y, width: room.w, height: room.h,
                       background: col.bg, border: `2px solid ${col.border}`, borderRadius: 10,
                       cursor: dragging?.id === room.id ? "grabbing" : "grab",
                       boxShadow: "0 3px 12px rgba(0,0,0,0.1)", userSelect: "none",
                       display: "flex", flexDirection: "column", overflow: "hidden",
                       transition: "box-shadow 0.15s",
                     }}>

                  {/* Room header — click to enter */}
                  <div onClick={() => onEnterRoom(room.id)}
                       onMouseDown={e => e.stopPropagation()}
                       style={{
                         padding: "8px 12px", background: col.border, color: "#fff",
                         fontSize: "0.75rem", fontWeight: 700, flexShrink: 0, cursor: "pointer",
                         display: "flex", justifyContent: "space-between", alignItems: "center",
                         transition: "filter 0.15s",
                       }}
                       onMouseEnter={e => e.currentTarget.style.filter = "brightness(1.1)"}
                       onMouseLeave={e => e.currentTarget.style.filter = ""}>
                    <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{room.label}</span>
                    <span style={{ marginLeft: 8, opacity: 0.85, fontSize: "0.65rem", flexShrink: 0, display: "flex", alignItems: "center", gap: 4 }}>
                      {cCount} ct · {tCount}🌿 <span style={{ background: "rgba(255,255,255,0.25)", borderRadius: 4, padding: "1px 5px", fontSize: "0.6rem" }}>Open →</span>
                    </span>
                  </div>

                  {/* Shelf previews — click any to enter the room */}
                  <div style={{ flex: 1, padding: "5px 6px", display: "flex", flexDirection: "column", gap: 3, overflow: "hidden" }}>
                    {room.shelves.map(shelf => {
                      const ct = shelf.containers;
                      return (
                        <div key={shelf.id} onClick={() => onEnterRoom(room.id)}
                             style={{
                               background: "rgba(255,255,255,0.5)", border: "1px solid rgba(255,255,255,0.5)",
                               borderRadius: 4, padding: "2px 5px", cursor: "pointer",
                               display: "flex", alignItems: "center", gap: 3, minHeight: 20, flexShrink: 0,
                             }}>
                          <span style={{ fontSize: "0.58rem", fontWeight: 700, color: col.text, flexShrink: 0, minWidth: 30 }}>
                            {shelf.label.length > 8 ? shelf.label.slice(0, 8) : shelf.label}
                          </span>
                          <div style={{ display: "flex", gap: 2, flexWrap: "wrap", flex: 1, overflow: "hidden" }}>
                            {ct.slice(0, 28).map(c => {
                              const cc = stageColor(c.stage);
                              return <div key={c.id} title={`${c.strain} · ${c.stage} · ×${c.count}`}
                                          style={{ width: 10, height: 10, borderRadius: 2, background: cc.border, border: "1px solid rgba(0,0,0,0.15)", flexShrink: 0 }} />;
                            })}
                            {ct.length > 28 && <span style={{ fontSize: "0.5rem", color: "rgba(0,0,0,0.4)" }}>+{ct.length - 28}</span>}
                            {ct.length === 0 && <span style={{ fontSize: "0.52rem", color: "rgba(0,0,0,0.25)", fontStyle: "italic" }}>empty</span>}
                          </div>
                          <span style={{ fontSize: "0.54rem", color: "rgba(0,0,0,0.35)", flexShrink: 0 }}>{ct.length}</span>
                        </div>
                      );
                    })}
                    {room.shelves.length === 0 && (
                      <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.65rem", color: "rgba(0,0,0,0.3)", fontStyle: "italic" }}>
                        No shelves
                      </div>
                    )}
                  </div>

                  {/* Stage label */}
                  <div style={{ position: "absolute", bottom: 16, right: 16, fontSize: "0.55rem", color: col.text, opacity: 0.5, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.06em" }}>
                    {room.stage}
                  </div>

                  {/* Resize handle */}
                  <div className="fp-rsz"
                       onMouseDown={e => { e.preventDefault(); e.stopPropagation(); setResizing({ id: room.id, sx: e.clientX, sy: e.clientY, ow: room.w, oh: room.h }); }}
                       style={{ position: "absolute", right: 0, bottom: 0, width: 14, height: 14, cursor: "se-resize", background: col.border, opacity: 0.5, borderTopLeftRadius: 4, zIndex: 2 }} />
                </div>
              );
            })}

            {rooms.length === 0 && (
              <div style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%,-50%)", textAlign: "center", color: "var(--lw-text-muted)" }}>
                <div style={{ fontSize: "3rem", marginBottom: 12 }}>🏢</div>
                <div style={{ fontWeight: 700, marginBottom: 6 }}>No rooms yet</div>
                <div style={{ fontSize: "0.8rem", marginBottom: 12 }}>Add a room or pull layout from HQ inventory.</div>
                <div style={{ display: "flex", gap: 8, justifyContent: "center" }}>
                  <button className="lw-btn lw-btn-primary" onClick={onAddRoom}>+ Add Room</button>
                  <button className="lw-btn lw-btn-secondary" onClick={onRebuildFromHQ} disabled={rebuilding}>🔄 Build from HQ</button>
                </div>
              </div>
            )}
          </div>
        </div>
      </>
    );
  }

  /* ══════════════════════════════════════════════════════════════════
     LEVEL 2 — ROOM VIEW
     Full-screen canvas for one room. Shelves are draggable/resizable
     boxes. Container tiles inside each shelf are draggable between
     shelves and clickable to inspect.
     ══════════════════════════════════════════════════════════════════ */
  function FPRoomView({ room, onBack, onUpdRoom, onDelRoom, onAddShelf, onUpdShelf, onDelShelf, onAddCt, onUpdCt, onDelCt, onMoveCt, getSyncState, onSave, focusContainerID, onFocusConsumed }) {
    const col = stageColor(room.stage);

    // Auto-position shelves that don't have x/y yet (first time entering a room)
    const shelves = React.useMemo(() => fpAutoLayoutShelves(room.shelves), [room.shelves]);

    const [selectedCt,    setSelectedCt]    = React.useState(null); // { shelfId, containerId }
    const [selectedShelf, setSelectedShelf] = React.useState(null); // shelfId

    /* QR scan focus: when the parent passes a containerID, look it up
       in this room's shelves and select the matching tile. */
    React.useEffect(() => {
      if (!focusContainerID) return;
      for (const s of shelves) {
        for (const c of (s.containers || [])) {
          if (c.containerID === focusContainerID) {
            setSelectedShelf(s.id);
            setSelectedCt({ shelfId: s.id, containerId: c.id });
            if (typeof onFocusConsumed === "function") onFocusConsumed();
            return;
          }
        }
      }
    }, [focusContainerID, shelves]);
    const [addModal,      setAddModal]      = React.useState(null); // shelfId
    const [editRoomOpen,  setEditRoomOpen]  = React.useState(false);
    const [shelfDragging, setShelfDragging] = React.useState(null);
    const [shelfResizing, setShelfResizing] = React.useState(null);
    const [zoom,          setZoom]          = React.useState(1);
    const [showGrid,      setShowGrid]      = React.useState(true);
    const [dragCt,        setDragCt]        = React.useState(null);  // { shelfId, containerId }
    const [dragOver,      setDragOver]      = React.useState(null);  // shelfId

    const totalContainers = shelves.reduce((a, s) => a + s.containers.length, 0);
    const totalTissues    = shelves.reduce((a, s) => a + s.containers.reduce((b, c) => b + (c.count || 0), 0), 0);

    const selShelf = selectedCt ? shelves.find(s => s.id === selectedCt.shelfId) : null;
    const selCt    = selShelf   ? selShelf.containers.find(c => c.id === selectedCt.containerId) : null;

    /* Shelf drag/resize listeners */
    React.useEffect(() => {
      const mm = e => {
        if (shelfDragging) {
          const dx = (e.clientX - shelfDragging.sx) / zoom, dy = (e.clientY - shelfDragging.sy) / zoom;
          onUpdShelf(shelfDragging.id, { x: fpSnap(Math.max(0, shelfDragging.ox + dx)), y: fpSnap(Math.max(0, shelfDragging.oy + dy)) });
        }
        if (shelfResizing) {
          const dx = (e.clientX - shelfResizing.sx) / zoom, dy = (e.clientY - shelfResizing.sy) / zoom;
          onUpdShelf(shelfResizing.id, { w: fpSnap(Math.max(SH_MIN_W * GRID, shelfResizing.ow + dx)), h: fpSnap(Math.max(SH_MIN_H * GRID, shelfResizing.oh + dy)) });
        }
      };
      const mu = () => { setShelfDragging(null); setShelfResizing(null); };
      window.addEventListener("mousemove", mm);
      window.addEventListener("mouseup", mu);
      return () => { window.removeEventListener("mousemove", mm); window.removeEventListener("mouseup", mu); };
    }, [shelfDragging, shelfResizing, zoom, onUpdShelf]);

    /* Container HTML5 drag between shelves */
    function onCtDragStart(shelfId, containerId, e) { e.dataTransfer.effectAllowed = "move"; setDragCt({ shelfId, containerId }); }
    function onShelfDragOver(shelfId, e) { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDragOver(shelfId); }
    function onShelfDrop(shelfId, e) {
      e.preventDefault();
      if (dragCt && dragCt.shelfId !== shelfId) {
        onMoveCt(dragCt.shelfId, shelfId, dragCt.containerId);
        setSelectedCt({ shelfId, containerId: dragCt.containerId });
      }
      setDragCt(null); setDragOver(null);
    }

    const canvasW = Math.max(900, ...shelves.map(s => (s.x || 0) + (s.w || 320) + 80));
    const canvasH = Math.max(600, ...shelves.map(s => (s.y || 0) + (s.h || 180) + 80));

    return (
      <div style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>

        {/* Toolbar */}
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 18px", background: "#fff", borderBottom: "1px solid var(--lw-border)", flexShrink: 0, flexWrap: "wrap" }}>
          <button className="lw-btn lw-btn-ghost lw-btn-sm" onClick={onBack}>← Floor Plan</button>
          <div style={{ width: 1, height: 20, background: "var(--lw-border)" }} />
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <div style={{ width: 10, height: 10, borderRadius: 3, background: col.border, flexShrink: 0 }} />
            <span style={{ fontWeight: 700, fontSize: "0.95rem" }}>{room.label}</span>
            <span style={{ fontSize: "0.72rem", background: col.bg, color: col.text, border: `1px solid ${col.border}`, padding: "2px 8px", borderRadius: 20, fontWeight: 600 }}>{room.stage}</span>
          </div>
          <div style={{ flex: 1 }} />
          <div style={{ fontSize: "0.78rem", color: "var(--lw-text-muted)", display: "flex", gap: 12 }}>
            <span><strong style={{ color: "var(--lw-text-primary)" }}>{shelves.length}</strong> shelves</span>
            <span><strong style={{ color: "var(--lw-text-primary)" }}>{totalContainers}</strong> containers</span>
            <span><strong style={{ color: "var(--lw-emerald-600)" }}>{totalTissues.toLocaleString()}</strong> tissues</span>
          </div>
          <div style={{ width: 1, height: 20, background: "var(--lw-border)" }} />
          <label style={{ display: "flex", alignItems: "center", gap: 5, fontSize: "0.78rem", color: "var(--lw-text-muted)", cursor: "pointer" }}>
            <input type="checkbox" checked={showGrid} onChange={e => setShowGrid(e.target.checked)} /> Grid
          </label>
          <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
            <button className="lw-btn lw-btn-ghost lw-btn-sm" onClick={() => setZoom(z => Math.max(0.4, +(z - 0.1).toFixed(1)))}>−</button>
            <span style={{ fontSize: "0.75rem", minWidth: 36, textAlign: "center", color: "var(--lw-text-muted)" }}>{Math.round(zoom * 100)}%</span>
            <button className="lw-btn lw-btn-ghost lw-btn-sm" onClick={() => setZoom(z => Math.min(2.5, +(z + 0.1).toFixed(1)))}>+</button>
          </div>
          <button className="lw-btn lw-btn-ghost lw-btn-sm" onClick={() => setEditRoomOpen(true)}>✏️ Edit Room</button>
          <button className="lw-btn lw-btn-ghost lw-btn-sm" onClick={onAddShelf}>+ Shelf</button>
          <button className="lw-btn lw-btn-secondary lw-btn-sm" onClick={onSave}>💾 Save</button>
        </div>

        {/* Hint bar */}
        <div style={{ background: col.bg, borderBottom: `1px solid ${col.border}`, padding: "5px 18px", fontSize: "0.72rem", color: col.text, flexShrink: 0, display: "flex", gap: 16, alignItems: "center", flexWrap: "wrap" }}>
          <span>🖱 <strong>Drag</strong> shelf header to move · ↘ <strong>Resize</strong> via corner · 📦 <strong>Drag</strong> container tiles between shelves · <strong>Click</strong> tile to inspect</span>
          <div style={{ marginLeft: "auto", display: "flex", gap: 8, flexWrap: "wrap" }}>
            {Object.entries(FP_STAGE_COLORS).filter(([k]) => k !== "--").map(([s, c]) => (
              <div key={s} style={{ display: "flex", alignItems: "center", gap: 3 }}>
                <div style={{ width: 8, height: 8, borderRadius: 2, background: c.border }} />
                <span style={{ fontSize: "0.62rem", color: col.text, opacity: 0.8 }}>{s}</span>
              </div>
            ))}
          </div>
        </div>

        {/* Body: canvas + inspector */}
        <div style={{ display: "flex", flex: 1, overflow: "hidden" }}>

          {/* Canvas */}
          <div style={{ flex: 1, overflow: "auto", background: "#eef0f3" }}
               onClick={e => { if (e.target === e.currentTarget) { setSelectedCt(null); setSelectedShelf(null); } }}>
            <div style={{
              position: "relative",
              width: canvasW * zoom, height: canvasH * zoom,
              transform: `scale(${zoom})`, transformOrigin: "0 0",
              backgroundImage: showGrid
                ? "linear-gradient(rgba(0,0,0,0.07) 1px, transparent 1px), linear-gradient(90deg, rgba(0,0,0,0.07) 1px, transparent 1px)"
                : "none",
              backgroundSize: `${GRID}px ${GRID}px`,
            }}>

              {shelves.length === 0 && (
                <div style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%,-50%)", textAlign: "center", color: "var(--lw-text-muted)" }}>
                  <div style={{ fontSize: "3rem", marginBottom: 12 }}>🗄</div>
                  <div style={{ fontWeight: 700, marginBottom: 6 }}>No shelves yet</div>
                  <button className="lw-btn lw-btn-primary" onClick={onAddShelf}>+ Add Shelf</button>
                </div>
              )}

              {shelves.map(shelf => {
                const isDragOver = dragOver === shelf.id;
                const isSelShelf = selectedShelf === shelf.id;
                const shelfTissues = shelf.containers.reduce((a, c) => a + (c.count || 0), 0);
                const sx = shelf.x ?? 30, sy = shelf.y ?? 30;
                const sw = shelf.w ?? 320, sh = shelf.h ?? 180;

                return (
                  <div key={shelf.id}
                       onDragOver={e => onShelfDragOver(shelf.id, e)}
                       onDragLeave={() => setDragOver(null)}
                       onDrop={e => onShelfDrop(shelf.id, e)}
                       style={{
                         position: "absolute", left: sx, top: sy, width: sw, height: sh,
                         background: "#fff",
                         border: `2px solid ${isDragOver ? col.border : isSelShelf ? "#0f172a" : "var(--lw-border)"}`,
                         borderRadius: 10, overflow: "hidden", userSelect: "none",
                         display: "flex", flexDirection: "column",
                         boxShadow: isSelShelf
                           ? "0 0 0 3px rgba(15,23,42,0.12), 0 6px 20px rgba(0,0,0,0.12)"
                           : isDragOver ? `0 0 0 3px ${col.bg}` : "0 2px 8px rgba(0,0,0,0.08)",
                         transition: "border-color 0.15s, box-shadow 0.15s",
                       }}>

                    {/* Shelf header — drag handle */}
                    <div
                      onMouseDown={e => {
                        if (e.target.classList.contains("sh-rsz")) return;
                        if (e.target.tagName === "INPUT" || e.target.tagName === "BUTTON") return;
                        e.preventDefault();
                        setSelectedShelf(shelf.id); setSelectedCt(null);
                        setShelfDragging({ id: shelf.id, sx: e.clientX, sy: e.clientY, ox: sx, oy: sy });
                      }}
                      style={{
                        padding: "7px 10px", background: col.border, color: "#fff",
                        fontSize: "0.72rem", fontWeight: 700, flexShrink: 0, cursor: "grab",
                        display: "flex", flexDirection: "column", gap: 4,
                      }}>
                      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                        {/* Inline-editable label */}
                        <input
                          value={shelf.label}
                          onChange={e => onUpdShelf(shelf.id, { label: e.target.value })}
                          onMouseDown={e => e.stopPropagation()}
                          style={{
                            fontWeight: 700, fontSize: "0.72rem", border: "none", background: "transparent",
                            outline: "none", color: "#fff", flex: 1, cursor: "text", minWidth: 0,
                          }} />
                        <span style={{ opacity: 0.8, fontSize: "0.62rem", flexShrink: 0, fontFamily: "var(--lw-font-mono)" }}>
                          {shelf.containers.length}ct · {shelfTissues}🌿
                        </span>
                        <button
                          onMouseDown={e => e.stopPropagation()}
                          onClick={() => setAddModal(shelf.id)}
                          style={{
                            background: "rgba(255,255,255,0.25)", border: "none", color: "#fff",
                            borderRadius: 4, padding: "2px 7px", fontSize: "0.62rem", fontWeight: 700, cursor: "pointer", flexShrink: 0,
                          }}>
                          + Add
                        </button>
                        <button
                          onMouseDown={e => e.stopPropagation()}
                          onClick={() => { if (confirm("Delete shelf?")) onDelShelf(shelf.id); }}
                          style={{
                            background: "rgba(255,255,255,0.15)", border: "none", color: "#fff",
                            borderRadius: 4, padding: "2px 6px", fontSize: "0.62rem", cursor: "pointer", flexShrink: 0, opacity: 0.8,
                          }}>
                          ✕
                        </button>
                      </div>
                      {/* Inventory link: an Active_Inventory.Location code
                           (e.g. HLX-236-GC1S2). When set, every container
                           with that exact Location is rendered into this
                           shelf by the live-placement effect. */}
                      <input
                        value={shelf.locationCode || ""}
                        placeholder="Location code (e.g. HLX-236-GC1S2)"
                        onChange={e => onUpdShelf(shelf.id, { locationCode: e.target.value })}
                        onBlur={e => onUpdShelf(shelf.id, { locationCode: e.target.value.trim() })}
                        onMouseDown={e => e.stopPropagation()}
                        style={{
                          background: "rgba(255,255,255,0.15)", border: "1px solid rgba(255,255,255,0.3)",
                          color: "#fff", borderRadius: 4, padding: "2px 6px", outline: "none",
                          fontFamily: "var(--lw-font-mono)", fontSize: "0.62rem", letterSpacing: "0.01em",
                        }} />
                    </div>

                    {/* Container tiles */}
                    <div style={{ flex: 1, padding: "8px 10px", display: "flex", flexWrap: "wrap", gap: 6, alignContent: "flex-start", overflow: "auto" }}>
                      {shelf.containers.map(ct => {
                        const cc = stageColor(ct.stage);
                        const icon = FP_CONTAINER_ICONS[ct.type] || "📦";
                        const isSel = selectedCt?.containerId === ct.id;
                        return (
                          <div key={ct.id}
                               draggable
                               onDragStart={e => onCtDragStart(shelf.id, ct.id, e)}
                               onClick={e => { e.stopPropagation(); setSelectedCt(isSel ? null : { shelfId: shelf.id, containerId: ct.id }); setSelectedShelf(null); }}
                               title={`${ct.strain} · ${ct.stage} · ×${ct.count}`}
                               style={{
                                 width: 74, minHeight: 68, padding: "5px 6px",
                                 background: isSel ? cc.border : cc.bg,
                                 border: `2px solid ${cc.border}`,
                                 borderRadius: 7, cursor: "grab", userSelect: "none",
                                 display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 2,
                                 boxShadow: isSel ? "0 0 0 2px rgba(0,0,0,0.2)" : "none",
                                 transition: "transform 0.1s, box-shadow 0.1s",
                                 flexShrink: 0,
                               }}
                               onMouseEnter={e => { if (!isSel) e.currentTarget.style.transform = "translateY(-2px)"; }}
                               onMouseLeave={e => { e.currentTarget.style.transform = ""; }}>
                            <span style={{ fontSize: "1.2rem" }}>{icon}</span>
                            <div style={{ fontSize: "0.56rem", fontWeight: 700, color: isSel ? "#fff" : cc.text, textAlign: "center", lineHeight: 1.2, maxWidth: 62, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                              {ct.strain || "—"}
                            </div>
                            <div style={{ fontSize: "0.55rem", color: isSel ? "rgba(255,255,255,0.75)" : cc.text, opacity: 0.8, fontFamily: "var(--lw-font-mono)" }}>×{ct.count}</div>
                            <div style={{ fontSize: "0.5rem", background: isSel ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.08)", color: isSel ? "#fff" : cc.text, padding: "1px 4px", borderRadius: 8, marginTop: 1, fontWeight: 700 }}>
                              {ct.stage}
                            </div>
                          </div>
                        );
                      })}
                      {shelf.containers.length === 0 && (
                        <div style={{ display: "flex", alignItems: "center", justifyContent: "center", width: "100%", minHeight: 40, fontSize: "0.72rem", color: "var(--lw-text-muted)", fontStyle: "italic", border: "2px dashed var(--lw-border)", borderRadius: 7 }}>
                          Empty — drag containers here or + Add
                        </div>
                      )}
                    </div>

                    {/* Resize handle */}
                    <div className="sh-rsz"
                         onMouseDown={e => { e.preventDefault(); e.stopPropagation(); setShelfResizing({ id: shelf.id, sx: e.clientX, sy: e.clientY, ow: sw, oh: sh }); }}
                         style={{ position: "absolute", right: 0, bottom: 0, width: 14, height: 14, cursor: "se-resize", background: col.border, opacity: 0.45, borderTopLeftRadius: 4, zIndex: 2 }} />
                  </div>
                );
              })}
            </div>
          </div>

          {/* Inspector */}
          {selCt && selShelf && (
            <FPContainerInspector
              container={selCt} shelf={selShelf} room={room}
              syncState={getSyncState ? getSyncState(selCt.containerID) : { state: "idle" }}
              onUpdate={p => onUpdCt(selShelf.id, selCt.id, p)}
              onDelete={() => { onDelCt(selShelf.id, selCt.id); setSelectedCt(null); }}
              onClose={() => setSelectedCt(null)}
            />
          )}
        </div>

        {/* Add container modal */}
        {addModal && (
          <FPAddContainerModal
            defaultStage={room.stage}
            onAdd={spec => { onAddCt(addModal, spec); setAddModal(null); }}
            onClose={() => setAddModal(null)}
          />
        )}

        {/* Edit room modal */}
        {editRoomOpen && (
          <FPEditRoomModal
            room={room}
            onUpdate={p => onUpdRoom(p)}
            onDelete={() => { onDelRoom(); onBack(); }}
            onClose={() => setEditRoomOpen(false)}
          />
        )}
      </div>
    );
  }

  /* ── Sync status pill (header of container inspector) ───────────── */
  function FPSyncPill({ syncState }) {
    if (!syncState) return null;
    const s = syncState.state;
    const pill = {
      local:        { bg: "#f1f5f9", border: "#cbd5e1", text: "#334155", label: "📍 Local only", title: "No HQ container ID — edits stay in this browser." },
      idle:         { bg: "#ecfdf5", border: "#10b981", text: "#065f46", label: "✓ Saved to HQ" },
      pending:      { bg: "#fef3c7", border: "#f59e0b", text: "#92400e", label: "✎ Pending…" },
      syncing:      { bg: "#dbeafe", border: "#3b82f6", text: "#1e40af", label: "⟳ Syncing…" },
      ok:           { bg: "#ecfdf5", border: "#10b981", text: "#065f46", label: "✓ Saved to HQ" },
      forbidden:    { bg: "#fef2f2", border: "#f87171", text: "#991b1b", label: "🔒 Admin only" },
      unauthorized: { bg: "#fef2f2", border: "#f87171", text: "#991b1b", label: "🔑 Sign in" },
      error:        { bg: "#fef2f2", border: "#f87171", text: "#991b1b", label: "⚠ Sync failed" },
    }[s] || { bg: "#f1f5f9", border: "#cbd5e1", text: "#334155", label: s };
    const tip = pill.title || syncState.message || "";
    return (
      <span title={tip} style={{
        fontSize: "0.62rem", fontWeight: 700, letterSpacing: "0.02em",
        background: pill.bg, color: pill.text, border: `1px solid ${pill.border}`,
        padding: "2px 7px", borderRadius: 999, fontFamily: "var(--lw-font-mono)", whiteSpace: "nowrap",
      }}>{pill.label}</span>
    );
  }

  /* ── HQ floor-plan-doc sync pill (top-level toolbar). Distinct from
        FPSyncPill, which reports per-container workbook sync. ──────── */
  function FPHqStatusPill({ hqStatus }) {
    if (!hqStatus) return null;
    const pill = {
      loading:      { bg: "#dbeafe", border: "#3b82f6", text: "#1e40af", label: "⟳ Loading from HQ…" },
      queued:       { bg: "#fef3c7", border: "#f59e0b", text: "#92400e", label: "⏱ Save queued…" },
      saving:       { bg: "#dbeafe", border: "#3b82f6", text: "#1e40af", label: "⟳ Saving to HQ…" },
      saved:        { bg: "#ecfdf5", border: "#10b981", text: "#065f46", label: "✓ Saved to HQ" },
      offline:      { bg: "#fef3c7", border: "#f59e0b", text: "#92400e", label: "⚠ Offline (local only)" },
      unauthed:     { bg: "#fef2f2", border: "#f87171", text: "#991b1b", label: "🔑 Sign in to sync" },
      conflict:     { bg: "#fff7ed", border: "#fb923c", text: "#9a3412", label: "⚠ Conflict — reload" },
      "empty-remote": { bg: "#eff6ff", border: "#60a5fa", text: "#1e3a8a", label: "ℹ No HQ plan yet" },
      error:        { bg: "#fef2f2", border: "#f87171", text: "#991b1b", label: "⚠ HQ save failed" },
    }[hqStatus.state] || { bg: "#f1f5f9", border: "#cbd5e1", text: "#334155", label: hqStatus.state || "—" };
    const tip = hqStatus.message
      || (hqStatus.state === "saved" && hqStatus.at ? `Last saved ${new Date(hqStatus.at).toLocaleTimeString()}` : "")
      || "";
    return (
      <span title={tip} style={{
        fontSize: "0.66rem", fontWeight: 700, letterSpacing: "0.02em",
        background: pill.bg, color: pill.text, border: `1px solid ${pill.border}`,
        padding: "3px 9px", borderRadius: 999, fontFamily: "var(--lw-font-mono)", whiteSpace: "nowrap",
      }}>{pill.label}</span>
    );
  }

  /* ── Container Inspector (right panel in room view) ─────────────── */
  function FPContainerInspector({ container, shelf, room, syncState, onUpdate, onDelete, onClose }) {
    const col = stageColor(container.stage);
    const icon = FP_CONTAINER_ICONS[container.type] || "📦";
    const allStrains = window.LW_STRAINS || [];
    const ownerMap = window.LW_OWNER_MAP || {};
    const [sq, setSq] = React.useState(container.strain || "");
    const [sOpen, setSOpen] = React.useState(false);
    const matches = sq.length >= 1 ? allStrains.filter(n => n.toLowerCase().includes(sq.toLowerCase())).slice(0, 8) : [];

    React.useEffect(() => { setSq(container.strain || ""); }, [container.id]);

    const linkedCodes = fpLinkedOwnerCodesForStrain(container.strain);
    const primaryLinked = linkedCodes[0] || "";
    const primaryLinkedName = primaryLinked ? (ownerMap[primaryLinked] || primaryLinked) : "";
    const ownerMatchesLink = !!primaryLinked && container.owner === primaryLinked;

    /* Picking a different strain auto-updates the owner if the current
       owner is empty or already matches the previous strain's link.
       If the user has a custom owner that doesn't match, we leave it
       alone and surface a "Use linked" callout instead. */
    const pickStrainInInspector = (name) => {
      setSq(name);
      setSOpen(false);
      const codes = fpLinkedOwnerCodesForStrain(name);
      const prevLink = primaryLinked;
      const ownerIsBoundToOldStrain = !container.owner || container.owner === prevLink;
      const patch = { strain: name };
      if (codes[0] && ownerIsBoundToOldStrain) patch.owner = codes[0];
      onUpdate(patch);
    };

    return (
      <div style={{ width: 296, background: "#fff", borderLeft: "1px solid var(--lw-border)", display: "flex", flexDirection: "column", overflow: "hidden", flexShrink: 0 }}>
        <div style={{ padding: "12px 14px", borderBottom: "1px solid var(--lw-border)", background: "var(--lw-slate-50)", display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
          <div style={{ fontWeight: 700, fontSize: "0.88rem", flexShrink: 0 }}>Container</div>
          <FPSyncPill syncState={syncState} />
          <button className="lw-btn lw-btn-ghost lw-btn-sm" onClick={onClose} style={{ padding: "3px 7px", fontSize: "0.78rem", marginLeft: "auto" }}>✕</button>
        </div>

        <div style={{ flex: 1, overflow: "auto", padding: 14, display: "flex", flexDirection: "column", gap: 12 }}>
          {/* Badge */}
          <div style={{ background: col.bg, border: `1px solid ${col.border}`, borderRadius: 10, padding: "12px 14px", display: "flex", gap: 12, alignItems: "center" }}>
            <span style={{ fontSize: "2rem" }}>{icon}</span>
            <div>
              <div style={{ fontWeight: 700, color: col.text, fontSize: "0.9rem" }}>{container.type}</div>
              <div style={{ fontSize: "0.72rem", color: col.text, opacity: 0.8, marginTop: 2 }}>{container.stage} · ×{container.count} · {container.owner}</div>
              {container.containerID && <div style={{ fontSize: "0.65rem", color: "var(--lw-text-muted)", fontFamily: "var(--lw-font-mono)", marginTop: 2 }}>#{container.containerID}</div>}
            </div>
          </div>

          {/* Properties */}
          <FPSection title="Properties">
            <FPField label="Type">
              <select className="lw-select" style={fieldStyle} value={container.type} onChange={e => onUpdate({ type: e.target.value })}>
                {Object.keys(FP_CONTAINER_ICONS).map(t => <option key={t} value={t}>{FP_CONTAINER_ICONS[t]} {t}</option>)}
              </select>
            </FPField>
            <FPField label="Stage">
              <select className="lw-select" style={fieldStyle} value={container.stage} onChange={e => onUpdate({ stage: e.target.value })}>
                {Object.keys(FP_STAGE_COLORS).filter(k => k !== "--" && k !== "Mixed").map(s => <option key={s} value={s}>{s}</option>)}
              </select>
            </FPField>
            <FPField label="Tissues">
              <input className="lw-input" type="number" min="1" style={fieldStyle} value={container.count} onChange={e => onUpdate({ count: parseInt(e.target.value, 10) || 1 })} />
            </FPField>
            <FPField label="Owner">
              <div style={{ display: "flex", flexDirection: "column", gap: 4, flex: 1 }}>
                <input className="lw-input" style={fieldStyle} placeholder="Owner code"
                       value={container.owner || ""}
                       onChange={e => onUpdate({ owner: e.target.value })} />
                {primaryLinked && !ownerMatchesLink && (
                  <div style={{
                    display: "flex", alignItems: "center", gap: 6,
                    background: "#eff6ff", border: "1px solid #93c5fd", color: "#1e3a8a",
                    borderRadius: 5, padding: "3px 7px", fontSize: "0.68rem",
                  }}>
                    <span style={{ color: "#fbbf24" }}>★</span>
                    <span>Linked: <strong>{primaryLinked}</strong>{primaryLinkedName !== primaryLinked ? ` (${primaryLinkedName})` : ""}{linkedCodes.length > 1 ? ` +${linkedCodes.length - 1}` : ""}</span>
                    <button type="button" className="lw-btn lw-btn-ghost lw-btn-sm" style={{ marginLeft: "auto", padding: "1px 7px", fontSize: "0.65rem" }}
                            onClick={() => onUpdate({ owner: primaryLinked })}>Use</button>
                  </div>
                )}
                {primaryLinked && ownerMatchesLink && (
                  <div style={{ fontSize: "0.65rem", color: "var(--lw-emerald-600)" }}>
                    ★ Linked to strain{primaryLinkedName !== primaryLinked ? ` — ${primaryLinkedName}` : ""}
                  </div>
                )}
              </div>
            </FPField>
            <FPField label="Strain">
              <div style={{ position: "relative", flex: 1 }}>
                <input className="lw-input" style={fieldStyle} placeholder="Search strain…"
                       value={sq}
                       onChange={e => {
                         const v = e.target.value;
                         setSq(v);
                         setSOpen(true);
                         const codes = fpLinkedOwnerCodesForStrain(v);
                         const ownerBound = !container.owner || container.owner === primaryLinked;
                         const patch = { strain: v };
                         if (codes[0] && ownerBound) patch.owner = codes[0];
                         onUpdate(patch);
                       }}
                       onFocus={() => setSOpen(true)}
                       onBlur={() => setTimeout(() => setSOpen(false), 150)} />
                {sOpen && matches.length > 0 && (
                  <div style={{ position: "absolute", top: "100%", left: 0, right: 0, zIndex: 300, background: "#fff", border: "1px solid var(--lw-border)", borderRadius: 7, boxShadow: "0 8px 20px rgba(0,0,0,0.12)", maxHeight: 180, overflowY: "auto" }}>
                    {matches.map(n => {
                      const ownerCodes = fpLinkedOwnerCodesForStrain(n);
                      return (
                        <button key={n} onMouseDown={() => pickStrainInInspector(n)}
                                style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, width: "100%", textAlign: "left", padding: "7px 12px", border: "none", borderBottom: "1px solid var(--lw-border)", background: "none", cursor: "pointer", fontSize: "0.8rem", fontFamily: "inherit" }}>
                          <span>{n}</span>
                          {ownerCodes.length > 0 && (
                            <span style={{ fontSize: "0.65rem", color: "var(--lw-text-muted)", fontFamily: "var(--lw-font-mono)" }}>
                              {ownerCodes.join(", ")}
                            </span>
                          )}
                        </button>
                      );
                    })}
                  </div>
                )}
              </div>
            </FPField>
            {container.qr && (
              <FPField label="QR">
                <a href={container.qr} target="_blank" rel="noreferrer" style={{ fontSize: "0.72rem", color: "var(--lw-blue-600)", textDecoration: "underline", wordBreak: "break-all" }}>View QR ↗</a>
              </FPField>
            )}
            {container.notes && (
              <FPField label="Notes">
                <span style={{ fontSize: "0.72rem", color: "var(--lw-text-muted)", fontStyle: "italic" }}>{container.notes}</span>
              </FPField>
            )}
          </FPSection>

          {/* Location */}
          <FPSection title="Location">
            <FPField label="Shelf"><span style={{ fontSize: "0.8rem", fontWeight: 600 }}>{shelf.label}</span></FPField>
            <FPField label="Room"><span style={{ fontSize: "0.8rem", color: "var(--lw-text-muted)" }}>{room.label}</span></FPField>
          </FPSection>
        </div>

        <div style={{ padding: "10px 12px", borderTop: "1px solid var(--lw-border)", background: "var(--lw-slate-50)", display: "flex", justifyContent: "flex-end" }}>
          <button className="lw-btn lw-btn-danger lw-btn-sm" onClick={() => { if (confirm("Remove this container?")) onDelete(); }}>Remove</button>
        </div>
      </div>
    );
  }

  /* ── Edit Room Modal ─────────────────────────────────────────────── */
  function FPEditRoomModal({ room, onUpdate, onDelete, onClose }) {
    const [label, setLabel] = React.useState(room.label);
    const [stage, setStage] = React.useState(room.stage);
    return (
      <div style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,0.5)", zIndex: 999, display: "grid", placeItems: "center" }} onClick={onClose}>
        <div onClick={e => e.stopPropagation()} style={{ background: "#fff", borderRadius: 14, width: 380, boxShadow: "0 20px 60px rgba(0,0,0,0.25)", overflow: "hidden" }}>
          <div style={{ padding: "16px 20px", borderBottom: "1px solid var(--lw-border)" }}>
            <div style={{ fontWeight: 700, fontSize: "1rem" }}>Edit Room</div>
          </div>
          <div style={{ padding: "16px 20px", display: "flex", flexDirection: "column", gap: 12 }}>
            <div className="lw-field">
              <label>Room name</label>
              <input className="lw-input" value={label} onChange={e => setLabel(e.target.value)} />
            </div>
            <div className="lw-field">
              <label>Stage</label>
              <select className="lw-select" value={stage} onChange={e => setStage(e.target.value)}>
                {Object.keys(FP_STAGE_COLORS).filter(k => k !== "--" && k !== "Mixed").map(s => <option key={s} value={s}>{s}</option>)}
              </select>
            </div>
          </div>
          <div style={{ padding: "12px 20px 16px", borderTop: "1px solid var(--lw-border)", background: "var(--lw-slate-50)", display: "flex", gap: 8, justifyContent: "space-between" }}>
            <button className="lw-btn lw-btn-danger lw-btn-sm" onClick={() => { if (confirm("Delete this room?")) { onDelete(); onClose(); } }}>Delete Room</button>
            <div style={{ display: "flex", gap: 8 }}>
              <button className="lw-btn lw-btn-ghost" onClick={onClose}>Cancel</button>
              <button className="lw-btn lw-btn-primary" onClick={() => { onUpdate({ label, stage }); onClose(); }}>Save</button>
            </div>
          </div>
        </div>
      </div>
    );
  }

  /* Look up owner codes linked to a strain in the HQ workbook
     (Ref_Strains → window.LW_STRAIN_OWNERS, populated by
     applyLiveStrainRefs in index.html). Returns an array of trimmed
     codes — most strains have one, but the workbook permits multiple
     comma-separated owners per strain. */
  function fpLinkedOwnerCodesForStrain(strainName) {
    if (!strainName) return [];
    const raw = (window.LW_STRAIN_OWNERS || {})[strainName];
    if (!raw) return [];
    return String(raw).split(",").map(s => s.trim()).filter(Boolean);
  }

  /* ── Add Container Modal ─────────────────────────────────────────── */
  function FPAddContainerModal({ defaultStage, onAdd, onClose }) {
    const allStrains = window.LW_STRAINS || [];
    const ownerMap = window.LW_OWNER_MAP || {};
    const [form, setForm] = React.useState({ type: "Magenta Box", stage: defaultStage || "Multiplication", strain: "", owner: "", count: 6 });
    const [sq, setSq] = React.useState("");
    const [sOpen, setSOpen] = React.useState(false);
    /* Tracks whether the user has manually edited the Owner field. Once
       true, picking a different strain will not silently overwrite their
       custom value — they get a "Use linked" callout instead. */
    const [ownerOverridden, setOwnerOverridden] = React.useState(false);
    const matches = sq.length >= 1 ? allStrains.filter(n => n.toLowerCase().includes(sq.toLowerCase())).slice(0, 8) : [];

    const linkedCodes = fpLinkedOwnerCodesForStrain(form.strain);
    const primaryLinked = linkedCodes[0] || "";
    const primaryLinkedName = primaryLinked ? (ownerMap[primaryLinked] || primaryLinked) : "";
    const ownerMatchesLink = !!primaryLinked && form.owner === primaryLinked;

    const pickStrain = (name) => {
      setSq(name);
      setSOpen(false);
      const codes = fpLinkedOwnerCodesForStrain(name);
      setForm(f => ({
        ...f,
        strain: name,
        owner: ownerOverridden ? f.owner : (codes[0] || f.owner),
      }));
    };

    const useLinkedOwner = () => {
      if (!primaryLinked) return;
      setOwnerOverridden(false);
      setForm(f => ({ ...f, owner: primaryLinked }));
    };

    return (
      <div style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,0.5)", zIndex: 999, display: "grid", placeItems: "center" }} onClick={onClose}>
        <div onClick={e => e.stopPropagation()} style={{ background: "#fff", borderRadius: 14, width: 420, boxShadow: "0 20px 60px rgba(0,0,0,0.25)", overflow: "hidden" }}>
          <div style={{ padding: "16px 20px", borderBottom: "1px solid var(--lw-border)" }}>
            <div style={{ fontWeight: 700, fontSize: "1rem" }}>Add Container</div>
            <div style={{ fontSize: "0.78rem", color: "var(--lw-text-muted)", marginTop: 2 }}>Place a new container on this shelf</div>
          </div>
          <div style={{ padding: "16px 20px", display: "flex", flexDirection: "column", gap: 12 }}>
            <div className="lw-field">
              <label>Container type</label>
              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 7 }}>
                {Object.entries(FP_CONTAINER_ICONS).map(([t, icon]) => (
                  <button key={t} onClick={() => setForm(f => ({ ...f, type: t }))} style={{
                    padding: "8px 10px", borderRadius: 7, cursor: "pointer", display: "flex", alignItems: "center", gap: 6,
                    border: `2px solid ${form.type === t ? "var(--lw-emerald-500)" : "var(--lw-border)"}`,
                    background: form.type === t ? "var(--lw-emerald-50)" : "#fff",
                    fontSize: "0.8rem", fontWeight: 600,
                  }}>
                    <span>{icon}</span> {t}
                  </button>
                ))}
              </div>
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
              <div className="lw-field">
                <label>Stage</label>
                <select className="lw-select" style={{ padding: "7px 10px", fontSize: "0.85rem" }} value={form.stage} onChange={e => setForm(f => ({ ...f, stage: e.target.value }))}>
                  {Object.keys(FP_STAGE_COLORS).filter(k => k !== "--" && k !== "Mixed").map(s => <option key={s} value={s}>{s}</option>)}
                </select>
              </div>
              <div className="lw-field">
                <label>Tissues</label>
                <input className="lw-input" type="number" min="1" style={{ padding: "7px 10px", fontSize: "0.85rem" }}
                       value={form.count} onChange={e => setForm(f => ({ ...f, count: parseInt(e.target.value, 10) || 1 }))} />
              </div>
            </div>
            <div className="lw-field">
              <label>Strain</label>
              <div style={{ position: "relative" }}>
                <input className="lw-input" placeholder="Search strain…" style={{ padding: "7px 10px", fontSize: "0.85rem" }}
                       value={sq}
                       onChange={e => {
                         const v = e.target.value;
                         setSq(v);
                         setSOpen(true);
                         const codes = fpLinkedOwnerCodesForStrain(v);
                         setForm(f => ({
                           ...f,
                           strain: v,
                           owner: ownerOverridden ? f.owner : (codes[0] || f.owner),
                         }));
                       }}
                       onFocus={() => setSOpen(true)}
                       onBlur={() => setTimeout(() => setSOpen(false), 150)} />
                {sOpen && matches.length > 0 && (
                  <div style={{ position: "absolute", top: "100%", left: 0, right: 0, zIndex: 400, background: "#fff", border: "1px solid var(--lw-border)", borderRadius: 7, boxShadow: "0 8px 20px rgba(0,0,0,0.15)", maxHeight: 180, overflowY: "auto" }}>
                    {matches.map(n => {
                      const ownerCodes = fpLinkedOwnerCodesForStrain(n);
                      return (
                        <button key={n} onMouseDown={() => pickStrain(n)}
                                style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, width: "100%", textAlign: "left", padding: "8px 12px", border: "none", borderBottom: "1px solid var(--lw-border)", background: "none", cursor: "pointer", fontSize: "0.85rem", fontFamily: "inherit" }}>
                          <span>{n}</span>
                          {ownerCodes.length > 0 && (
                            <span style={{ fontSize: "0.7rem", color: "var(--lw-text-muted)", fontFamily: "var(--lw-font-mono)" }}>
                              {ownerCodes.join(", ")}
                            </span>
                          )}
                        </button>
                      );
                    })}
                  </div>
                )}
              </div>
            </div>
            <div className="lw-field">
              <label>Owner</label>
              {primaryLinked && !ownerMatchesLink && (
                <div style={{
                  display: "flex", alignItems: "center", gap: 8, marginBottom: 6,
                  background: "#eff6ff", border: "1px solid #93c5fd", color: "#1e3a8a",
                  borderRadius: 6, padding: "5px 9px", fontSize: "0.74rem",
                }}>
                  <span style={{ color: "#fbbf24" }}>★</span>
                  <span>Linked to strain <em>{form.strain}</em>: <strong>{primaryLinked}</strong>{primaryLinkedName !== primaryLinked ? ` (${primaryLinkedName})` : ""}{linkedCodes.length > 1 ? ` +${linkedCodes.length - 1} more` : ""}</span>
                  <button type="button" className="lw-btn lw-btn-ghost lw-btn-sm" style={{ marginLeft: "auto", padding: "2px 8px", fontSize: "0.72rem" }} onClick={useLinkedOwner}>Use</button>
                </div>
              )}
              {primaryLinked && ownerMatchesLink && (
                <div style={{ fontSize: "0.7rem", color: "var(--lw-emerald-600)", marginBottom: 4 }}>
                  ★ Auto-linked from strain <em>{form.strain}</em>{primaryLinkedName !== primaryLinked ? ` — ${primaryLinkedName}` : ""}
                </div>
              )}
              <input className="lw-input" style={{ padding: "7px 10px", fontSize: "0.85rem" }} placeholder="Owner code"
                     value={form.owner}
                     onChange={e => { setOwnerOverridden(true); setForm(f => ({ ...f, owner: e.target.value })); }} />
            </div>
          </div>
          <div style={{ padding: "12px 20px 16px", borderTop: "1px solid var(--lw-border)", background: "var(--lw-slate-50)", display: "flex", gap: 8, justifyContent: "flex-end" }}>
            <button className="lw-btn lw-btn-ghost" onClick={onClose}>Cancel</button>
            <button className="lw-btn lw-btn-primary" onClick={() => onAdd(form)}>Add Container</button>
          </div>
        </div>
      </div>
    );
  }

  /* ── Shared sub-components ───────────────────────────────────────── */
  const fieldStyle = { padding: "6px 9px", fontSize: "0.82rem" };

  function FPSection({ title, action, children }) {
    return (
      <div>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 6 }}>
          <div style={{ fontSize: "0.62rem", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.08em", color: "var(--lw-text-muted)" }}>{title}</div>
          {action}
        </div>
        <div style={{ display: "flex", flexDirection: "column", gap: 7 }}>{children}</div>
      </div>
    );
  }

  function FPField({ label, children }) {
    return (
      <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
        <div style={{ width: 52, fontSize: "0.72rem", color: "var(--lw-text-muted)", fontWeight: 600, flexShrink: 0 }}>{label}</div>
        <div style={{ flex: 1, minWidth: 0 }}>{children}</div>
      </div>
    );
  }

  /* Expose so the App() screens map in index.html can resolve it. */
  window.FloorPlan = FloorPlan;
})();
