// ============================================================================
// LEON Casework & Millwork
// ----------------------------------------------------------------------------
// ONE parametric engine, every room. A kitchen, a vanity, a wardrobe, a bar and
// a reception desk are the same object graph with different numbers in it —
// which is why there is no "closet tool" here beside a "kitchen tool". A tool
// per room type is how a business ends up with five cut-list formats that
// disagree.
//
// The hierarchy the whole file is organised around:
//
//   Room -> Wall -> Cabinet Run -> Cabinet Module -> Carcass
//                                                 -> Front
//                                                 -> Interior
//                                                 -> Hardware / Finish / Panel
//   ... and out the other side: Drawing -> BOM -> Production -> Installation.
//
// Two rules hold it up:
//
//  1. A CABINET IS A DATABASE OBJECT, not a picture. The schedule, the
//     elevation, the cut list and the BOM all read the SAME record, so a width
//     changed in the schedule is the width the drawing redraws from. Nothing
//     here stores a cabinet as an image.
//
//  2. TYPE vs INSTANCE, twice over. A Cabinet Type carries the module standard
//     (B2D is 30" × 34½" × 24"). A Casework Type — "Kitchen Type K-A" — carries
//     a whole room, and 40 units point at it. An instance stores ONLY what it
//     disagrees with, so re-issuing K-A reaches 40 units without erasing the
//     field measurement someone took in unit 1204.
//
// NOT BUILT, and the UI says so where a user would go looking:
//   · no freehand CAD canvas — runs stay ordered lists so the cut list can read them
//   · no CNC output — machining is stored as structured operations, not G-code,
//     and an operation with no manufacturer drilling data SAYS so rather than
//     guessing a pattern
//   · no AI
// The nesting IS built, and is a real optimiser — but a heuristic one, so it
// produces a proposal a person approves, never a silent production decision.
// ============================================================================

// ---- units ----------------------------------------------------------------
// Everything is stored in millimetres. North American casework is designed in
// inches, so the seed library is written as cwIn(...) and reads like the shop
// drawing it came from, while the engine never has to ask what system a number
// is in.
function cwIn(inches) { return inches * MM_PER_INCH; }
function cwNum(v) {
  if (v === null || v === undefined || v === '') return 0;
  const n = typeof v === 'number' ? v : parseFloat(String(v).replace(/[^0-9.\-]/g, ''));
  return isFinite(n) ? n : 0;
}
function cwRound(v) { return Math.round(v * 10) / 10; }
// Short numeric label for the arithmetic strings. The point of showing the
// working is that someone can check it, and "590.55" checks no better than
// "591" while reading much worse.
function cwN(v) { return String(Math.round(cwNum(v))); }

// ---- vocabularies ---------------------------------------------------------

// ── The planner: 2020 Design's workspace, built on our own records ─────────
// Studied from 2020 Design Live, whose working shape is four things: a CATALOG
// panel you place from (quick search, history, categories), a WORK AREA
// toolbar of placement aids (item snap, collision, ortho), an ELEVATION area
// where cabinets are placed and stacked on a wall, and an ITEMS LIST of what
// has been placed. Everything here writes the SAME run members the Run Designer
// edits — this is a faster way into the same records, not a second set.
//
// Click-to-place rather than drag-and-drop: 2020 places on double-click too,
// and a click is reliable where a drag across a scrolling catalog is not.
const CW_PLANNER_GROUPS = [
  { key: 'Base', label: 'Base' },
  { key: 'Wall', label: 'Wall' },
  { key: 'Tall', label: 'Tall' },
  { key: 'Corner', label: 'Corner' },
];
const CW_PLANNER_RECENT_KEY = 'leon-cw-planner-recent-v1';

function CwPlanner({ ctx, project, system, editable }) {
  const rooms = cwRooms(project);
  const [roomId, setRoomId] = useState(rooms[0] ? rooms[0].id : '');
  const [wallId, setWallId] = useState('');
  const [runId, setRunId] = useState('');
  const [q, setQ] = useState('');
  const [group, setGroup] = useState('Base');
  // With the other hooks, not down beside the catalogue it belongs to: a hook
  // below an early return breaks the order React relies on, and it was only one
  // edit away from being below one.
  const [openMod, setOpenMod] = useState('');
  const [sel, setSel] = useState(null);              // selected member id
  const [snap, setSnap] = useState(true);            // 2020's Item Snap
  const [collide, setCollide] = useState(true);      // 2020's Collision
  const [autoDim, setAutoDim] = useState(true);
  const [recent, setRecent] = useState(() => {
    try { return JSON.parse(localStorage.getItem(CW_PLANNER_RECENT_KEY) || '[]'); } catch (e) { return []; }
  });

  const room = rooms.find(r => r.id === roomId) || rooms[0] || null;
  const ct = room ? cwCaseworkTypes(project).find(t => t.id === room.caseworkTypeId) : null;
  const walls = (ct && ct.walls) || [];
  const wall = walls.find(w => w.id === wallId) || walls[0] || null;
  const runs = wall ? ((ct && ct.runs) || []).filter(r => r.wallId === wall.id) : [];
  const run = runs.find(r => r.id === runId) || runs[0] || null;
  // EVERY RUN ON THE WALL, not just the one being edited. 2020's elevation
  // area exists to "stack cabinets, place items vertically and raise or lower
  // items on a wall" — you cannot do any of that looking at one run at a time,
  // which is what this drew before. The active run is highlighted; the others
  // are there so a wall cabinet can be placed against the base beneath it.
  const layouts = (room && wall)
    ? runs.map(r => ({ run: r, layout: cwRunLayout(project, ctx, room, wall, r) }))
    : [];
  const layout = (room && wall && run) ? cwRunLayout(project, ctx, room, wall, run) : null;
  const wallLen = room && wall ? cwWallLength(room, wall) : 0;

  function remember(code) {
    setRecent(prev => {
      const next = [code, ...prev.filter(x => x !== code)].slice(0, 8);
      try { localStorage.setItem(CW_PLANNER_RECENT_KEY, JSON.stringify(next)); } catch (e) {}
      return next;
    });
  }

  // ── Placing ──────────────────────────────────────────────────────────────
  // A cabinet TYPE has to exist before a run can point at one. Rather than
  // making someone leave and build it first — which is the step that made this
  // slow — a code with no matching type creates the type from the library and
  // places it in the same act.
  function placeType(t) {
    if (!editable || !run) return;
    cwUpdate(ctx, project, draft => {
      const dct = (draft.caseworkRoomTypes || []).find(x => x.id === (ct && ct.id));
      const drun = dct && (dct.runs || []).find(x => x.id === run.id);
      if (!drun) return;
      if (!Array.isArray(drun.members)) drun.members = [];
      drun.members.push(cwMakeMember({ kind: 'cabinet', cabTypeId: t.id, label: '' }));
    }, `LEON Casework — ${t.code} placed on ${wall ? wall.name : 'the run'}.`);
    remember(t.code);
  }

  function placeCode(raw) {
    if (!editable || !run) return;
    const parsed = typeof cwParseCode === 'function' ? cwParseCode(raw) : null;
    if (!parsed) return false;
    const wIn = parsed.widthIn || 24;
    const code = cwMakeCode(parsed.category, parsed.type, wIn, parsed.heightIn);
    // An existing type of the same code and width is reused — placing B15
    // twice must not leave two B15 types behind.
    const existing = cwAllTypes(project).find(t => t.code === code
      && Math.abs(cwNum(t.width) - cwIn(wIn)) < 1);
    if (existing) { placeType(existing); return true; }
    const mod = (typeof CW_LIB_MODULES !== 'undefined' ? CW_LIB_MODULES : [])
      .find(m => m.type === parsed.type) || null;
    const preset = k => {
      const p = (typeof CW_LIB_PRESETS !== 'undefined' ? CW_LIB_PRESETS : []).find(x => x.param === k);
      return p ? Number(p.value) : null;
    };
    const hIn = parsed.heightIn || (parsed.category === 'Wall' ? 30
      : parsed.category === 'Tall' ? 84 : (preset('Base Box Height') || 34.5));
    const dIn = parsed.category === 'Wall' ? (preset('Wall Box Depth') || 12)
      : (preset('Base Box Depth') || 24);
    // The catalogue item carries the FRONT CONFIGURATION — 1 door, 3 drawer,
    // false front over doors. Guessing it from the module name was a guess;
    // the generic library says what each module is actually sold as.
    const item = (typeof cwCatalogFind === 'function' ? cwCatalogFind(code) : [])[0] || null;
    const rows = item ? item.rows.map(r => (r.hIn
        ? { kind: r.kind, count: r.count, h: cwIn(r.hIn) }
        : { kind: r.kind, count: r.count, weight: r.weight }))
      : (/Drawer/i.test(parsed.type) ? [{ kind: 'drawers', count: 3, weight: 1 }]
        : /Sink|Apron/i.test(parsed.type) ? [{ kind: 'falseFront', h: cwIn(6) }, { kind: 'doors', count: 2, weight: 1 }]
        : [{ kind: 'doors', count: wIn >= 24 ? 2 : 1, weight: 1 }]);
    let made = null;
    cwUpdate(ctx, project, draft => {
      if (!Array.isArray(draft.caseworkTypes)) draft.caseworkTypes = [];
      made = cwMakeType({
        code, name: `${parsed.type} ${wIn}"`, category: parsed.category === 'Corner' ? 'Base' : parsed.category,
        width: cwIn(wIn), height: cwIn(hIn), depth: cwIn(dIn), rows,
        toeKick: parsed.category !== 'Wall',
        special: /Blind/i.test(parsed.type) ? 'blindCorner'
          : /Susan/i.test(parsed.type) ? 'lazySusan'
          : /Corner Wall/i.test(parsed.type) ? 'cornerWall' : null,
        notes: mod ? `From the LEON cabinet library — ${mod.id} · ${mod.fronts}.` : '',
      }, ctx.currentUserName);
      draft.caseworkTypes.push(made);
      const dct = (draft.caseworkRoomTypes || []).find(x => x.id === (ct && ct.id));
      const drun = dct && (dct.runs || []).find(x => x.id === run.id);
      if (drun) {
        if (!Array.isArray(drun.members)) drun.members = [];
        drun.members.push(cwMakeMember({ kind: 'cabinet', cabTypeId: made.id, label: '' }));
      }
    }, `LEON Casework — ${code} created from the library and placed on ${wall ? wall.name : 'the run'}.`);
    remember(code);
    return true;
  }

  // Place a specific catalogue ITEM — the 2020 way round: you pick the cabinet
  // you want, not a module and then a shape.
  function placeItem(item) {
    if (!editable || !run || !item) return;
    const existing = cwAllTypes(project).find(t => t.code === item.code
      && Math.abs(cwNum(t.width) - cwIn(item.widthIn)) < 1
      && (t.frontKey || '') === item.front);
    if (existing) { placeType(existing); return; }
    const preset = k => {
      const p = (typeof CW_LIB_PRESETS !== 'undefined' ? CW_LIB_PRESETS : []).find(x => x.param === k);
      return p ? Number(p.value) : null;
    };
    const hIn = item.heightIn || (item.category === 'Wall' ? 30
      : item.category === 'Tall' ? 84 : (preset('Base Box Height') || 34.5));
    const dIn = Number(String(item.depthIn).match(/[\d.]+/) ? String(item.depthIn).match(/[\d.]+/)[0] : 0)
      || (item.category === 'Wall' ? 12 : 24);
    cwUpdate(ctx, project, draft => {
      if (!Array.isArray(draft.caseworkTypes)) draft.caseworkTypes = [];
      const made = cwMakeType({
        code: item.code, name: item.name,
        category: item.category === 'Corner' ? 'Base' : item.category,
        width: cwIn(item.widthIn), height: cwIn(hIn), depth: cwIn(dIn),
        rows: item.rows.map(r => (r.hIn ? { kind: r.kind, count: r.count, h: cwIn(r.hIn) }
                                        : { kind: r.kind, count: r.count, weight: r.weight })),
        toeKick: item.category !== 'Wall',
        frontKey: item.front,
        special: /Blind/i.test(item.module) ? 'blindCorner'
          : /Susan/i.test(item.module) ? 'lazySusan'
          : /Corner Wall/i.test(item.module) ? 'cornerWall' : null,
        notes: `LEON generic library — ${item.module}, ${item.frontLabel}.`,
      }, ctx.currentUserName);
      draft.caseworkTypes.push(made);
      const dct = (draft.caseworkRoomTypes || []).find(x => x.id === (ct && ct.id));
      const drun = dct && (dct.runs || []).find(x => x.id === run.id);
      if (drun) {
        if (!Array.isArray(drun.members)) drun.members = [];
        drun.members.push(cwMakeMember({ kind: 'cabinet', cabTypeId: made.id, label: '' }));
      }
    }, `LEON Casework — ${item.code} (${item.frontLabel}) placed on ${wall ? wall.name : 'the run'}.`);
    remember(item.code);
  }
  function placeComponent(kind) {
    if (!editable || !run) return;
    cwUpdate(ctx, project, draft => {
      const dct = (draft.caseworkRoomTypes || []).find(x => x.id === (ct && ct.id));
      const drun = dct && (dct.runs || []).find(x => x.id === run.id);
      if (!drun) return;
      if (!Array.isArray(drun.members)) drun.members = [];
      drun.members.push(cwMakeMember({
        kind,
        width: kind === 'filler' ? cwIn(3) : kind === 'panel' ? cwIn(0.75) : cwIn(24),
        openingW: kind === 'appliance' ? cwIn(24) : null,
        openingH: kind === 'appliance' ? cwIn(34.5) : null,
      }));
    }, `LEON Casework — ${kind} placed on ${wall ? wall.name : 'the run'}.`);
  }

  function moveSel(dir) {
    if (!editable || !sel) return;
    const owner = layouts.find(L => (L.run.members || []).some(m => m.id === sel));
    cwUpdate(ctx, project, draft => {
      const dct = (draft.caseworkRoomTypes || []).find(x => x.id === (ct && ct.id));
      const drun = dct && (dct.runs || []).find(x => x.id === (owner ? owner.run.id : run.id));
      if (!drun) return;
      const i = (drun.members || []).findIndex(x => x.id === sel), j = i + dir;
      if (i < 0 || j < 0 || j >= drun.members.length) return;
      const t = drun.members[i]; drun.members[i] = drun.members[j]; drun.members[j] = t;
    }, `LEON Casework — ${wall ? wall.name : 'run'} order changed.`);
  }
  // RAISE AND LOWER. A wall cabinet sits at a height off the floor and a base
  // one does not; that height belongs to the RUN, which is how the model already
  // holds it, so this moves the run the selected member is in.
  function raiseSel(deltaIn) {
    if (!editable || !sel) return;
    const owner = layouts.find(L => (L.run.members || []).some(m => m.id === sel));
    if (!owner) return;
    cwUpdate(ctx, project, draft => {
      const dct = (draft.caseworkRoomTypes || []).find(x => x.id === (ct && ct.id));
      const drun = dct && (dct.runs || []).find(x => x.id === owner.run.id);
      if (drun) drun.zBottom = Math.max(0, cwNum(drun.zBottom) + cwIn(deltaIn));
    }, `LEON Casework — ${owner.run.name} ${deltaIn > 0 ? 'raised' : 'lowered'} ${Math.abs(deltaIn)}" on ${wall.name}.`);
  }
  // A WALL CUT-OUT — a window, a door or a pass-through — placed from the
  // elevation, which is where you can see what it will clash with. The run
  // layout already checks obstructions in both axes, so a cabinet that fouls
  // one says so the moment this is added.
  function addCutout(kind) {
    if (!editable || !wall) return;
    cwUpdate(ctx, project, draft => {
      const dct = (draft.caseworkRoomTypes || []).find(x => x.id === (ct && ct.id));
      const dwall = dct && (dct.walls || []).find(x => x.id === wall.id);
      if (!dwall) return;
      if (!Array.isArray(dwall.obstructions)) dwall.obstructions = [];
      dwall.obstructions.push(cwMakeObstruction({
        kind, label: kind,
        fromLeft: cwIn(12) + dwall.obstructions.length * cwIn(6),
        width: kind === 'Door' ? cwIn(36) : cwIn(36),
        sillHeight: kind === 'Door' ? 0 : cwIn(42),
        height: kind === 'Door' ? cwIn(84) : cwIn(36),
      }));
    }, `LEON Casework — ${kind} cut-out added to ${wall.name}.`);
  }
  function removeSel() {
    if (!editable || !sel) return;
    const owner = layouts.find(L => (L.run.members || []).some(m => m.id === sel));
    cwUpdate(ctx, project, draft => {
      const dct = (draft.caseworkRoomTypes || []).find(x => x.id === (ct && ct.id));
      const drun = dct && (dct.runs || []).find(x => x.id === (owner ? owner.run.id : run.id));
      if (drun) drun.members = (drun.members || []).filter(x => x.id !== sel);
    }, `LEON Casework — member removed from ${wall ? wall.name : 'the run'}.`);
    setSel(null);
  }

  // ── The catalog ──────────────────────────────────────────────────────────
  const allItems = typeof cwCatalogItems === 'function' ? cwCatalogItems() : [];
  const needle = q.trim().toLowerCase();
  const parsedQ = typeof cwParseCode === 'function' ? cwParseCode(q) : null;
  // The generic library, filtered the way 2020's Selection area is: by
  // category, then by module, then the items in it.
  const groupItems = allItems.filter(i => i.category === group);
  const modulesIn = [...new Set(groupItems.map(i => i.module))];
  const searchHits = !needle ? [] : allItems.filter(i =>
    [i.code, i.name, i.module, i.frontLabel].some(v => String(v).toLowerCase().includes(needle))).slice(0, 60);

  if (!rooms.length) {
    return <EmptyState text="No rooms yet. Add one under Rooms & Walls — a planner needs a wall to place against." />;
  }

  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
  const over = layout ? layout.remaining < -1 : false;

  return (
    <div className="space-y-3">
      <div>
        <h3 className="font-bold">🧱 Planner</h3>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
          Place cabinets on a wall the way a kitchen is actually laid out: pick from the catalog on the
          left, or type a code. This writes the <b>same run</b> the Run Designer edits.
        </p>
      </div>

      <div className="flex items-end gap-2 flex-wrap">
        <Field label="Room">
          <Select className="!w-44" value={room ? room.id : ''}
            onChange={e => { setRoomId(e.target.value); setWallId(''); setRunId(''); setSel(null); }}>
            {rooms.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
          </Select>
        </Field>
        <Field label="Wall">
          <Select className="!w-36" value={wall ? wall.id : ''}
            onChange={e => { setWallId(e.target.value); setRunId(''); setSel(null); }}>
            {walls.map(w => <option key={w.id} value={w.id}>{w.name}</option>)}
          </Select>
        </Field>
        <Field label="Run">
          <Select className="!w-40" value={run ? run.id : ''} onChange={e => { setRunId(e.target.value); setSel(null); }}>
            {runs.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
          </Select>
        </Field>
        {/* 2020's Work Area toolbar: the placement aids, as toggles. */}
        <div className="flex items-center gap-1 ml-auto">
          {[['Item snap', snap, setSnap, 'Widths snap to the 3" module the library is made in.'],
            ['Collision', collide, setCollide, 'Warn when the run overruns the wall or hits an obstruction.'],
            ['Auto-dim', autoDim, setAutoDim, 'Dimension every cabinet and the overall, as an elevation is dimensioned.']]
            .map(([label, on, setOn, hint]) => (
            <button key={label} title={hint} onClick={() => setOn(!on)}
              className={`px-2 py-1 rounded text-[11px] border ${on
                ? 'bg-[var(--leon-brown)] text-white border-[var(--leon-brown)]'
                : 'border-[var(--leon-line)] text-[var(--leon-black)]/55'}`}>
              {label}
            </button>
          ))}
        </div>
      </div>

      {!walls.length ? (
        <EmptyState text="This room's casework type has no walls yet. Add one under Rooms & Walls." />
      ) : !run ? (
        <EmptyState text="No run on this wall yet. Add one in the Run Designer — a run is what cabinets are placed into." />
      ) : (
        <div className="grid lg:grid-cols-[260px_1fr] gap-3">
          {/* ── Catalog ─────────────────────────────────────────────────── */}
          <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden self-start">
            <div className="px-2.5 py-2 bg-[var(--leon-cream)] text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55">
              Catalog
            </div>
            <div className="p-2.5 space-y-2">
              <TextInput value={q} onChange={e => setQ(e.target.value)}
                placeholder="Quick search — B15, W2430, sink…" />
              {parsedQ && (
                <button disabled={!editable} onClick={() => { placeCode(q); setQ(''); }}
                  className="w-full rounded border border-[var(--leon-brown)] bg-[var(--leon-cream)] p-2 text-left">
                  <div className="text-xs font-bold">
                    Place {cwMakeCode(parsedQ.category, parsedQ.type, parsedQ.widthIn, parsedQ.heightIn)}
                  </div>
                  <div className="text-[10px] text-[var(--leon-black)]/55">
                    {parsedQ.type} · {parsedQ.widthIn}&Prime;{parsedQ.heightIn ? ` × ${parsedQ.heightIn}"` : ''}
                  </div>
                </button>
              )}
              {!!recent.length && !needle && (
                <div>
                  <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40 mb-1">Recent</div>
                  <div className="flex flex-wrap gap-1">
                    {recent.map(c => (
                      <button key={c} disabled={!editable} onClick={() => placeCode(c)}
                        className="px-1.5 py-0.5 rounded border border-[var(--leon-line)] text-[11px] font-mono hover:border-[var(--leon-brown)]">
                        {c}
                      </button>
                    ))}
                  </div>
                </div>
              )}
              {!needle && (
                <div className="flex flex-wrap gap-1">
                  {CW_PLANNER_GROUPS.map(g => (
                    <button key={g.key} onClick={() => setGroup(g.key)}
                      className={`px-1.5 py-0.5 rounded text-[11px] ${group === g.key
                        ? 'bg-[var(--leon-brown)] text-white' : 'hover:bg-[var(--leon-cream)]'}`}>
                      {g.label}
                    </button>
                  ))}
                </div>
              )}
              {/* 2020's Selection area: double-click down through category and
                  module until you reach the item you want to place. Here the
                  module opens and its items are listed with their front
                  configuration, because "B15" alone does not say whether it is
                  a door or a drawer. */}
              <div className="max-h-[420px] overflow-y-auto space-y-1">
                {needle ? searchHits.map(i => (
                  <button key={i.id} disabled={!editable} onClick={() => placeItem(i)}
                    className="w-full text-left rounded border border-[var(--leon-line)] p-1.5 hover:border-[var(--leon-brown)] hover:bg-[var(--leon-cream)]">
                    <div className="flex items-baseline gap-1.5">
                      <span className="text-[11px] font-mono font-bold">{i.code}</span>
                      <span className="text-[10px] text-[var(--leon-black)]/45 truncate">{i.frontLabel}</span>
                    </div>
                    <div className="text-[10px] text-[var(--leon-black)]/55 truncate">{i.module}</div>
                  </button>
                )) : modulesIn.map(mod => {
                  const items = groupItems.filter(i => i.module === mod);
                  const open = openMod === mod;
                  return (
                    <div key={mod} className="rounded border border-[var(--leon-line)]">
                      <button onClick={() => setOpenMod(open ? '' : mod)}
                        className="w-full flex items-center gap-1 px-1.5 py-1 text-left hover:bg-[var(--leon-cream)]/60">
                        <span className="text-[10px] text-[var(--leon-black)]/35">{open ? '▾' : '▸'}</span>
                        <span className="text-[11px] font-semibold leading-tight flex-1">{mod}</span>
                        <span className="text-[10px] text-[var(--leon-black)]/35">{items.length}</span>
                      </button>
                      {open && (
                        <div className="px-1.5 pb-1.5 max-h-56 overflow-y-auto space-y-0.5">
                          {items.map(i => (
                            <button key={i.id} disabled={!editable} onClick={() => placeItem(i)}
                              title={`Place ${i.name}`}
                              className="w-full flex items-baseline gap-1.5 px-1 py-0.5 rounded text-left hover:bg-[var(--leon-cream)]">
                              <span className="text-[10px] font-mono font-bold w-14 shrink-0">{i.code}</span>
                              <span className="text-[10px] text-[var(--leon-black)]/55 truncate">{i.frontLabel}</span>
                            </button>
                          ))}
                        </div>
                      )}
                    </div>
                  );
                })}
                {needle && !searchHits.length && !parsedQ && (
                  <p className="text-[11px] text-[var(--leon-black)]/45 py-3 text-center">Nothing matches.</p>
                )}
              </div>
              <div className="pt-2 border-t border-[var(--leon-line)]">
                <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40 mb-1">Components</div>
                <div className="flex flex-wrap gap-1">
                  {CW_MEMBER_KINDS.filter(k => k.key !== 'cabinet').map(k => (
                    <button key={k.key} disabled={!editable} onClick={() => placeComponent(k.key)}
                      className="px-1.5 py-0.5 rounded border border-[var(--leon-line)] text-[11px] hover:border-[var(--leon-brown)]">
                      {k.label}
                    </button>
                  ))}
                </div>
              </div>
            </div>
          </div>

          {/* ── Work area: the elevation, then the items list ────────────── */}
          <div className="space-y-3">
            <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
              <div className="flex items-baseline gap-2 mb-2">
                <span className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55">
                  {wall.name} — elevation
                </span>
                <span className="text-[11px] text-[var(--leon-black)]/45">
                  wall {fmt(wallLen)} · used {fmt(cwNum(run.startOffset) + (layout ? layout.used : 0))}
                </span>
                {collide && over && (
                  <span className="ml-auto text-[11px] font-semibold text-[var(--leon-red)]">
                    Over the wall by {fmt(-layout.remaining)}
                  </span>
                )}
                {collide && !over && layout && (
                  <span className="ml-auto text-[11px] text-[var(--leon-black)]/45">
                    {fmt(layout.remaining)} left
                  </span>
                )}
              </div>
              <CwPlannerElevation project={project} room={room} wall={wall} run={run} layouts={layouts} system={system}
                sel={sel} onSel={setSel} autoDim={autoDim} wallLen={wallLen}
                onRaise={raiseSel} onCut={addCutout} editable={editable} />
              {editable && (
                <div className="flex items-center gap-1.5 mt-2">
                  <Button size="sm" variant="outline" disabled={!sel} onClick={() => moveSel(-1)}>← Move</Button>
                  <Button size="sm" variant="outline" disabled={!sel} onClick={() => moveSel(1)}>Move →</Button>
                  <Button size="sm" variant="outline" disabled={!sel} onClick={removeSel}>Remove</Button>
                  <span className="text-[11px] text-[var(--leon-black)]/40 ml-2">
                    {sel ? 'Selected — placing adds to the right-hand end.' : 'Click a cabinet to select it.'}
                  </span>
                </div>
              )}
            </div>

            {/* 2020's Items List — what has been placed, priced by nothing here
                but counted and coded, which is what the shop reads off. */}
            <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden">
              <div className="px-2.5 py-2 bg-[var(--leon-cream)] flex items-baseline gap-2">
                <span className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55">
                  Items list
                </span>
                <span className="text-[11px] text-[var(--leon-black)]/45">
                  {layout ? layout.members.length : 0} on this run
                </span>
              </div>
              <table className="w-full text-xs">
                <thead className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">
                  <tr className="border-b border-[var(--leon-line)]">
                    <th className="text-left py-1 px-2">#</th><th className="text-left py-1 px-2">Code</th>
                    <th className="text-left py-1 px-2">Item</th><th className="text-right py-1 px-2">W</th>
                    <th className="text-right py-1 px-2">H</th><th className="text-right py-1 px-2">From left</th>
                  </tr>
                </thead>
                <tbody>
                  {(layout ? layout.members : []).map((m, i) => (
                    <tr key={m.member.id}
                      onClick={() => setSel(m.member.id)}
                      className={`border-b border-[var(--leon-line)]/60 cursor-pointer ${
                        sel === m.member.id ? 'bg-[var(--leon-cream)]' : 'hover:bg-[var(--leon-cream)]/50'}`}>
                      <td className="py-1 px-2 text-[var(--leon-black)]/40">{i + 1}</td>
                      <td className="py-1 px-2 font-mono">{m.type ? m.type.code : '—'}</td>
                      <td className="py-1 px-2">{m.label}</td>
                      <td className="py-1 px-2 text-right tabular-nums">{fmt(m.w)}</td>
                      <td className="py-1 px-2 text-right tabular-nums">{fmt(m.h)}</td>
                      <td className="py-1 px-2 text-right tabular-nums">{fmt(m.x)}</td>
                    </tr>
                  ))}
                  {!(layout && layout.members.length) && (
                    <tr><td colSpan="6" className="py-4 text-center text-[var(--leon-black)]/40">
                      Nothing placed yet. Pick a width from the catalog, or type a code.
                    </td></tr>
                  )}
                </tbody>
              </table>
            </div>

            {collide && layout && !!layout.issues.length && (
              <div className="space-y-1">
                {layout.issues.map((x, i) => (
                  <div key={i} className={`rounded border text-xs px-2.5 py-1.5 ${x.level === 'error'
                    ? 'bg-red-50 border-red-200 text-red-700'
                    : 'bg-amber-50 border-amber-200 text-amber-900'}`}>{x.msg}</div>
                ))}
              </div>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

// The elevation someone places into. Same percentage technique every Gantt and
// elevation in this app uses; the dimension band below follows the convention a
// kitchen elevation is dimensioned to — each cabinet, then the overall.
// One member's face, filling whatever box the elevation gives it. Drawn as an
// SVG with a `viewBox` in the cabinet's own millimetres and
// preserveAspectRatio="none", so the fronts stretch to the button exactly and
// nothing has to be scaled by hand.
function CwMemberFace({ project, member, w, h, label }) {
  const F = (w > 0 && h > 0) ? cwMemberFronts(project, member, w, h) : null;
  if (!F) {
    return (
      <span className="flex items-center justify-center w-full h-full text-[9px] text-[var(--leon-black)]/55 px-0.5 truncate">
        {label}
      </span>
    );
  }
  const L = F.layout;
  const px = mm => mm;                       // the viewBox IS millimetres
  return (
    <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none"
      style={{ display: 'block', width: '100%', height: '100%' }} aria-hidden="true">
      <rect x="0" y="0" width={w} height={h} fill="#fdfcfa" />
      {L.toeH > 0 && (
        <rect x="0" y={h - L.toeH} width={w} height={L.toeH} fill={CW_LINE} opacity="0.7" />
      )}
      {L.cells.map(c => c.kind === 'opening' ? (
        <rect key={c.key} x={c.x} y={c.y} width={c.w} height={c.h}
          fill="var(--leon-cream)" stroke={CW_INK} strokeWidth={Math.max(1, w / 260)} strokeDasharray="14 10" />
      ) : (
        <CwFrontFace key={c.key} x={c.x} y={c.y} w={c.w} h={c.h} style={F.res.frontStyle}
          kind={c.kind} handing={member.handing} showPull px={px} />
      ))}
    </svg>
  );
}

function CwPlannerElevation({ project, room, wall, run, layouts, system, sel, onSel, autoDim, wallLen, onRaise, onCut, editable }) {
  if (!wall) return null;
  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
  const wallH = cwNum(wall.height) || cwIn(96);
  const H = 300, dimH = autoDim ? 46 : 10;
  const pctX = mm => (wallLen > 0 ? (mm / wallLen) * 100 : 0);
  const active = layouts.filter(L => L.run.id === (run && run.id));
  const actLayout = active[0] ? active[0].layout : null;

  return (
    <div className="w-full">
      {/* 2020 calls this the ELEVATION AREA, and it is where cabinets are
          stacked, raised and lowered and cut-outs are made — so it has to show
          the whole wall, every run on it, at once. */}
      <div className="relative bg-[var(--leon-cream)]/40 border border-[var(--leon-line)] rounded"
        style={{ height: H + dimH }}>
        {/* The wall, on a GRID. 2020 draws one behind the elevation area and it
            is not decoration: it is what lets you read a height off the screen
            without measuring. A foot is the major line, six inches the minor,
            so the grid means the same thing at any wall length. */}
        <div className="absolute left-0 right-0 bg-white border-b-2 border-[var(--leon-black)]/60"
          style={{ top: 0, height: H,
            backgroundImage:
              'linear-gradient(to right, rgba(43,33,24,0.14) 1px, transparent 1px),'
              + 'linear-gradient(to bottom, rgba(43,33,24,0.14) 1px, transparent 1px),'
              + 'linear-gradient(to right, rgba(43,33,24,0.06) 1px, transparent 1px),'
              + 'linear-gradient(to bottom, rgba(43,33,24,0.06) 1px, transparent 1px)',
            backgroundSize:
              `${wallLen > 0 ? (cwIn(12) / wallLen) * 100 : 10}% ${(cwIn(12) / wallH) * H}px,`
              + `${wallLen > 0 ? (cwIn(12) / wallLen) * 100 : 10}% ${(cwIn(12) / wallH) * H}px,`
              + `${wallLen > 0 ? (cwIn(6) / wallLen) * 100 : 5}% ${(cwIn(6) / wallH) * H}px,`
              + `${wallLen > 0 ? (cwIn(6) / wallLen) * 100 : 5}% ${(cwIn(6) / wallH) * H}px`,
            backgroundPosition: 'bottom left' }} />

        {/* Wall cut-outs — a base run passes under a window and a wall run does
            not, which is what the layout check tests in both axes. */}
        {(wall.obstructions || []).map(o => {
          const za = o.kind === 'Door' ? 0 : cwNum(o.sillHeight);
          const zb = za + cwNum(o.height);
          return (
            <div key={o.id} className="absolute border-2 border-dashed border-[var(--leon-black)]/45 bg-white/70"
              style={{ left: `${pctX(cwNum(o.fromLeft))}%`, width: `${pctX(cwNum(o.width))}%`,
                bottom: dimH + (za / wallH) * H, height: ((zb - za) / wallH) * H }}>
              <span className="absolute top-0.5 left-1 text-[9px] text-[var(--leon-black)]/55">
                {o.label || o.kind} {fmt(cwNum(o.width))}
              </span>
            </div>
          );
        })}

        {/* Every run, stacked. The one being placed into is solid; the rest are
            quieter but still selectable, because moving a wall cabinet while
            looking at the base beneath it is the whole point of this view. */}
        {layouts.map(({ run: r, layout }) => {
          const isActive = r.id === (run && run.id);
          return layout.members.map(m => (
            <button key={m.member.id} onClick={() => onSel(m.member.id)}
              className={`absolute border text-[10px] leading-tight overflow-hidden transition ${
                sel === m.member.id
                  ? 'border-[var(--leon-brown)] ring-1 ring-[var(--leon-brown)] z-10'
                  : isActive
                    ? 'border-[var(--leon-black)]/55 hover:brightness-95'
                    : 'border-[var(--leon-black)]/25 opacity-70 hover:opacity-100'}`}
              style={{ left: `${pctX(m.x)}%`, width: `${pctX(m.w)}%`,
                bottom: dimH + (m.z0 / wallH) * H, height: Math.max(8, (m.h / wallH) * H),
                background: sel === m.member.id ? 'rgba(176,137,104,0.18)' : '#fdfcfa', padding: 0 }}
              title={`${r.name} — ${m.label}, ${fmt(m.w)} wide, ${fmt(m.z0)} off the floor`}>
              {/* The FRONTS, not a blank box. A placement strip tells you a
                  cabinet is there; an elevation tells you what it looks like,
                  and that is what this view is for. */}
              <CwMemberFace project={project} member={m.member} w={m.w} h={m.h} label={m.type ? m.type.code : m.label} />
            </button>
          ));
        })}

        {/* Dimensions: the active run item by item, then the wall overall —
            the way a kitchen elevation is dimensioned. */}
        {autoDim && actLayout && (
          <div className="absolute left-0 right-0" style={{ bottom: 0, height: dimH }}>
            {actLayout.members.map(m => (
              <div key={m.member.id} className="absolute border-l border-r border-[var(--leon-black)]/35 text-center"
                style={{ left: `${pctX(m.x)}%`, width: `${pctX(m.w)}%`, top: 5, height: 14 }}>
                <span className="text-[9px] text-[var(--leon-black)]/60">{fmt(m.w)}</span>
              </div>
            ))}
            <div className="absolute border-l border-r border-[var(--leon-black)]/60 text-center"
              style={{ left: 0, width: '100%', top: 24, height: 16 }}>
              <span className="text-[10px] font-semibold text-[var(--leon-black)]/70">{fmt(wallLen)} WALL</span>
            </div>
          </div>
        )}
      </div>

      {/* The elevation's own tools — raising an item on the wall and cutting an
          opening in it are things you do while looking at the elevation, which
          is where 2020 puts them too. */}
      {editable && (
        <div className="flex items-center gap-1.5 mt-2 flex-wrap">
          <span className="text-[11px] text-[var(--leon-black)]/45">Selected run:</span>
          <Button size="sm" variant="outline" disabled={!sel} onClick={() => onRaise(1)}>↑ Raise 1&Prime;</Button>
          <Button size="sm" variant="outline" disabled={!sel} onClick={() => onRaise(-1)}>↓ Lower 1&Prime;</Button>
          <Button size="sm" variant="outline" disabled={!sel} onClick={() => onRaise(6)}>↑ 6&Prime;</Button>
          <span className="mx-2 text-[11px] text-[var(--leon-black)]/45">Cut the wall:</span>
          {['Window', 'Door', 'Opening'].map(k => (
            <Button key={k} size="sm" variant="outline" onClick={() => onCut(k)}>+ {k}</Button>
          ))}
        </div>
      )}
    </div>
  );
}

// ── The LEON cabinet library ───────────────────────────────────────────────
// Read from LEON_Cabinet_Appliance_Library_USA.xlsx and shipped as a plain
// catalog file. These are LISTS, not editable records: the workbook is the
// source, the same way the door model catalog and the finish catalogs are. What
// IS editable — a job's own construction defaults, its cabinet types, its
// hardware — lives in the sections beside them.
function CwLibRule({ id }) {
  const r = typeof CW_LIB_RULES !== 'undefined' ? CW_LIB_RULES.find(x => x.id === id) : null;
  if (!r) return null;
  return (
    <p className="text-[11px] text-[var(--leon-black)]/50 mt-1">
      <b>{r.id}</b> · {r.rule} &mdash; {r.logic}. <i>{r.implementation}.</i>
    </p>
  );
}

function CwLibraryModulesPanel({ ctx, system }) {
  const [cat, setCat] = useState('');
  const [q, setQ] = useState('');
  // Two views of one library, because they answer different questions: the
  // MODULES say what LEON builds and in what sizes; the ITEMS are the things
  // you actually place, each with its front configuration. 2020 ships both.
  const [view, setView] = useState('items');
  const all = typeof CW_LIB_MODULES !== 'undefined' ? CW_LIB_MODULES : [];
  const items = typeof cwCatalogItems === 'function' ? cwCatalogItems() : [];
  const needleI = q.trim().toLowerCase();
  const itemHits = items.filter(i => (!cat || i.category === cat)
    && (!needleI || [i.code, i.name, i.module, i.frontLabel].some(v => String(v).toLowerCase().includes(needleI))));
  const cats = [...new Set(all.map(m => m.category))];
  const needle = q.trim().toLowerCase();
  const hits = all.filter(m => (!cat || m.category === cat)
    && (!needle || [m.id, m.type, m.fronts, m.construction].some(v => String(v).toLowerCase().includes(needle))));
  return (
    <div className="space-y-3">
      <div>
        <h3 className="font-bold">📖 Cabinet Library</h3>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
          The modules LEON builds, the widths each is made in and how its front is configured.
          Read from the company's own US cabinet library.
        </p>
        <p className="text-[11px] text-[var(--leon-black)]/45 max-w-3xl mt-1">
          A cabinet is named by its <b>code</b> &mdash; B15, W2430, SB30 &mdash; and typing one into the Run
          Designer builds it. The sizes below are what each module is actually made in; anything else is a
          custom width and is priced as one.
        </p>
      </div>
      <div className="flex items-end gap-3 flex-wrap">
        <div className="flex gap-1 pb-2">
          {[['items', `Items (${items.length})`], ['modules', `Modules (${all.length})`]].map(([k, label]) => (
            <button key={k} onClick={() => setView(k)}
              className={`px-2 py-1 rounded text-[12px] ${view === k
                ? 'bg-[var(--leon-brown)] text-white font-semibold' : 'hover:bg-[var(--leon-cream)]'}`}>
              {label}
            </button>
          ))}
        </div>
        <Field label="Category">
          <Select className="!w-44" value={cat} onChange={e => setCat(e.target.value)}>
            <option value="">Every category</option>
            {cats.map(c => <option key={c}>{c}</option>)}
          </Select>
        </Field>
        <Field label="Search" className="flex-1">
          <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Code, module or front" />
        </Field>
        <span className="text-xs text-[var(--leon-black)]/45 pb-2">
          {view === 'items' ? `${itemHits.length} of ${items.length}` : `${hits.length} of ${all.length}`}
        </span>
      </div>

      {view === 'items' && (
        <div className="overflow-x-auto">
          <table className="w-full text-xs">
            <thead className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">
              <tr className="border-b border-[var(--leon-line)]">
                <th className="text-left py-1 px-2">Code</th><th className="text-left py-1 px-2">Module</th>
                <th className="text-left py-1 px-2">Front</th><th className="text-right py-1 px-2">W</th>
                <th className="text-right py-1 px-2">H</th><th className="text-left py-1 px-2">D</th>
              </tr>
            </thead>
            <tbody>
              {itemHits.slice(0, 400).map(i => (
                <tr key={i.id} className="border-b border-[var(--leon-line)]/60">
                  <td className="py-1 px-2 font-mono font-semibold">{i.code}</td>
                  <td className="py-1 px-2">{i.module}</td>
                  <td className="py-1 px-2 text-[var(--leon-black)]/60">{i.frontLabel}</td>
                  <td className="py-1 px-2 text-right tabular-nums">{i.widthIn}&Prime;</td>
                  <td className="py-1 px-2 text-right tabular-nums">{i.heightIn ? `${i.heightIn}"` : '—'}</td>
                  <td className="py-1 px-2 text-[var(--leon-black)]/55">{i.depthIn}&Prime;</td>
                </tr>
              ))}
            </tbody>
          </table>
          {itemHits.length > 400 && (
            <p className="text-[11px] text-[var(--leon-black)]/45 py-2">
              Showing the first 400 of {itemHits.length}. Narrow it with the category or the search.
            </p>
          )}
        </div>
      )}
      {view === 'modules' && <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
        {hits.map(m => {
          const sizes = typeof cwModuleSizes === 'function' ? cwModuleSizes(m) : [];
          const code = typeof cwMakeCode === 'function' ? cwMakeCode(m.category, m.type, sizes[0] || 0, null) : '';
          return (
            <div key={m.id} className="rounded-lg border border-[var(--leon-line)] bg-white p-2.5">
              <div className="flex items-baseline gap-2">
                <span className="font-semibold text-sm">{m.type}</span>
                <span className="text-[11px] text-[var(--leon-black)]/45">{m.category}</span>
                {code && <span className="ml-auto text-[11px] font-mono text-[var(--leon-brown)]">{code}…</span>}
              </div>
              <div className="text-[11px] text-[var(--leon-black)]/60 mt-1">
                {m.widthIn}&Prime; wide · {m.heightIn}&Prime; high · {m.depthIn}&Prime; deep
              </div>
              <div className="text-[11px] text-[var(--leon-black)]/45">{m.fronts} · {m.sizing}</div>
              {sizes.length > 1 && (
                <div className="text-[10px] text-[var(--leon-black)]/40 mt-1 truncate" title={sizes.join(', ')}>
                  Made in: {sizes.join(', ')}
                </div>
              )}
            </div>
          );
        })}
      </div>}
      <CwLibRule id="CAB-004" />
    </div>
  );
}

function CwLibraryAppliancesPanel({ ctx }) {
  const [q, setQ] = useState('');
  const [status, setStatus] = useState('');
  const all = typeof CW_LIB_APPLIANCES !== 'undefined' ? CW_LIB_APPLIANCES : [];
  const needle = q.trim().toLowerCase();
  const hits = all.filter(a => (!status || a.status === status)
    && (!needle || [a.category, a.subtype, a.notes, a.note].some(v => String(v || '').toLowerCase().includes(needle))));
  const tone = st => st === 'Generic' ? 'bg-amber-50 border-amber-200 text-amber-900'
    : st === 'Model Required' ? 'bg-red-50 border-red-200 text-red-700'
    : st === 'Typical Range' ? 'bg-[var(--leon-cream)] border-[var(--leon-line)] text-[var(--leon-black)]/70'
    : 'bg-green-50 border-green-200 text-green-800';
  return (
    <div className="space-y-3">
      <div>
        <h3 className="font-bold">🔌 Appliance Templates</h3>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
          What each appliance needs from the casework. <b>The data status is the point</b>: a nominal size
          is not a cutout, and most of these cannot be inferred from one.
        </p>
      </div>
      <div className="rounded-lg border border-amber-200 bg-amber-50 p-3">
        <p className="text-xs text-amber-900">
          {typeof CW_DATA_STATUS_NOTE !== 'undefined' && (
            <>
              <b>Generic</b> &mdash; {CW_DATA_STATUS_NOTE['Generic']} <b className="ml-2">Model Required</b> &mdash;
              {' '}{CW_DATA_STATUS_NOTE['Model Required']} <b className="ml-2">Production Verified</b> &mdash;
              {' '}{CW_DATA_STATUS_NOTE['Production Verified']}
            </>
          )}
        </p>
      </div>
      <div className="flex items-end gap-3 flex-wrap">
        <Field label="Data status">
          <Select className="!w-48" value={status} onChange={e => setStatus(e.target.value)}>
            <option value="">Every status</option>
            {[...new Set(all.map(a => a.status))].map(x => <option key={x}>{x}</option>)}
          </Select>
        </Field>
        <Field label="Search" className="flex-1">
          <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Appliance or note" />
        </Field>
        <span className="text-xs text-[var(--leon-black)]/45 pb-2">{hits.length} of {all.length}</span>
      </div>
      <div className="overflow-x-auto">
        <table className="w-full text-xs">
          <thead className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">
            <tr className="border-b border-[var(--leon-line)]">
              <th className="text-left py-1 px-2">Appliance</th>
              <th className="text-left py-1 px-2">Nominal</th>
              <th className="text-left py-1 px-2">Cutout W × H × D</th>
              <th className="text-left py-1 px-2">Status</th>
              <th className="text-left py-1 px-2">Note</th>
            </tr>
          </thead>
          <tbody>
            {hits.map((a, i) => (
              <tr key={i} className="border-b border-[var(--leon-line)]/60">
                <td className="py-1 px-2">
                  <div className="font-semibold">{a.category}</div>
                  <div className="text-[10px] text-[var(--leon-black)]/45">{a.subtype}</div>
                </td>
                <td className="py-1 px-2 tabular-nums">{a.nominal}&Prime;</td>
                <td className="py-1 px-2 tabular-nums">{a.cutW} × {a.cutH} × {a.cutD}</td>
                <td className="py-1 px-2">
                  <span className={`inline-block rounded border px-1.5 py-0.5 text-[10px] ${tone(a.status)}`}>
                    {a.status}
                  </span>
                </td>
                <td className="py-1 px-2 text-[var(--leon-black)]/55">{a.note}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <CwLibRule id="APP-001" />
      <CwLibRule id="APP-003" />
      {typeof CW_LIB_MANUFACTURERS !== 'undefined' && (
        <div className="pt-2 border-t border-[var(--leon-line)]">
          <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55 mb-1.5">
            Manufacturers we specify
          </div>
          <div className="flex flex-wrap gap-1.5">
            {CW_LIB_MANUFACTURERS.map(m => (
              <span key={m.name} className="rounded border border-[var(--leon-line)] px-2 py-0.5 text-[11px]">
                {m.name} <span className="text-[var(--leon-black)]/40">{m.segment}</span>
              </span>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function CwLibraryComponentsPanel({ ctx }) {
  const all = typeof CW_LIB_COMPONENTS !== 'undefined' ? CW_LIB_COMPONENTS : [];
  return (
    <div className="space-y-3">
      <div>
        <h3 className="font-bold">🧱 Components</h3>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
          The parts that are not cabinets and are on every job &mdash; fillers, scribes, finished ends,
          panels, mouldings. A run that does not account for them does not fit the wall it was measured for.
        </p>
      </div>
      <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
        {all.map(c => (
          <div key={c.name} className="rounded-lg border border-[var(--leon-line)] bg-white p-2.5">
            <div className="font-semibold text-sm">{c.name}</div>
            <div className="text-[11px] text-[var(--leon-black)]/60 mt-0.5">{c.sizes}</div>
            <div className="text-[11px] text-[var(--leon-black)]/40">{c.dimType} · {c.note}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

function CwLibraryRulesPanel({ ctx }) {
  const all = typeof CW_LIB_RULES !== 'undefined' ? CW_LIB_RULES : [];
  const src = typeof CW_LIB_SOURCES !== 'undefined' ? CW_LIB_SOURCES : [];
  const pre = typeof CW_LIB_PRESETS !== 'undefined' ? CW_LIB_PRESETS : [];
  return (
    <div className="space-y-4">
      <div>
        <h3 className="font-bold">📏 Planning Rules</h3>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
          The company's own rules, written out so a warning on a drawing can say <i>why</i> it fired rather
          than only that it did.
        </p>
      </div>
      <div className="overflow-x-auto">
        <table className="w-full text-xs">
          <thead className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">
            <tr className="border-b border-[var(--leon-line)]">
              <th className="text-left py-1 px-2">Rule</th><th className="text-left py-1 px-2">What it says</th>
              <th className="text-left py-1 px-2">Value</th><th className="text-left py-1 px-2">How it is used</th>
            </tr>
          </thead>
          <tbody>
            {all.map(r => (
              <tr key={r.id} className="border-b border-[var(--leon-line)]/60">
                <td className="py-1 px-2 font-mono text-[11px]">{r.id}</td>
                <td className="py-1 px-2 font-semibold">{r.rule}</td>
                <td className="py-1 px-2">{r.logic}</td>
                <td className="py-1 px-2 text-[var(--leon-black)]/55">{r.implementation}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <div>
        <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55 mb-1.5">
          Construction defaults
        </div>
        <div className="grid gap-1.5 sm:grid-cols-2 lg:grid-cols-3">
          {pre.map(x => (
            <div key={x.param} className="flex items-baseline gap-2 text-xs border-b border-[var(--leon-line)]/50 py-1">
              <span className="flex-1">{x.param}</span>
              <span className="font-semibold tabular-nums">{x.value}{x.unit === 'in' ? '"' : ` ${x.unit}`}</span>
              <span className="text-[10px] text-[var(--leon-black)]/40 w-28 text-right truncate" title={x.note}>{x.note}</span>
            </div>
          ))}
        </div>
        <p className="text-[11px] text-[var(--leon-black)]/45 mt-1.5">
          These are what a NEW cabinet starts from. A job's own values are set under <b>Construction</b>,
          and changing them there never re-cuts what is already drawn.
        </p>
      </div>
      {!!src.length && (
        <div className="text-[11px] text-[var(--leon-black)]/45">
          Sizing referenced from {src.map((x, i) => (
            <span key={x.url}>{i ? ', ' : ''}<a href={x.url} target="_blank" rel="noreferrer"
              className="underline">{x.source}</a></span>
          ))}.
        </div>
      )}
    </div>
  );
}

const CW_SW_SECTIONS = [
  // THIS JOB — the work. Grouped the way LEON Doors is, because seventeen flat
  // entries put the settings a coordinator opens once a year in the same list
  // as the run they are drawing today.
  { key: 'dashboard', label: 'Dashboard', icon: '📊', group: 'This job' },
  { key: 'rooms', label: 'Rooms & Walls', icon: '🏠', group: 'This job' },
  { key: 'planner', label: 'Planner', icon: '🧱', group: 'This job' },
  { key: 'runs', label: 'Run Designer', icon: '📏', group: 'This job' },
  { key: 'design3d', label: '3D Design', icon: '🧊', group: 'This job' },
  // Rendering is not a separate tool — it renders the room this module already
  // models, from the same runs and the same finishes.
  { key: 'render', label: 'Render', icon: '🎬', group: 'This job' },
  { key: 'pano', label: '360 Panorama', icon: '🔄', group: 'This job' },
  { key: 'casework', label: 'Casework Types', icon: '🅰️', group: 'This job' },
  { key: 'schedule', label: 'Cabinet Schedule', icon: '📋', group: 'This job' },
  { key: 'designer', label: 'Cabinet Designer', icon: '🪵', group: 'This job' },
  { key: 'sheets', label: 'Shop Drawing', icon: '📄', group: 'This job' },
  { key: 'panels', label: 'Panels & BOM', icon: '✂️', group: 'This job' },
  { key: 'nesting', label: 'Nesting', icon: '🧮', group: 'This job' },
  { key: 'tags', label: 'Finish Tags', icon: '🏷️', group: 'This job' },
  { key: 'keynotes', label: 'Keynotes', icon: '📌', group: 'This job' },
  { key: 'submittals', label: 'Submittals', icon: '📑', group: 'This job' },
  // CASEWORK SETTINGS — the standards every future cabinet inherits. Same
  // split, and the same capability, as Door Settings.
  { key: 'cabLibrary', label: 'Cabinet Library', icon: '📖', group: 'Casework Settings' },
  { key: 'appliances', label: 'Appliances', icon: '🔌', group: 'Casework Settings' },
  { key: 'types', label: 'Cabinet Types', icon: '🧩', group: 'Casework Settings' },
  { key: 'global', label: 'Global Library', icon: '🌍', group: 'Casework Settings' },
  { key: 'construction', label: 'Construction', icon: '📐', group: 'Casework Settings' },
  { key: 'components', label: 'Components', icon: '🧱', group: 'Casework Settings' },
  { key: 'hardware', label: 'Hardware', icon: '🔩', group: 'Casework Settings' },
  { key: 'rules', label: 'Planning Rules', icon: '📏', group: 'Casework Settings' },
];


const CW_CATEGORIES = ['Base', 'Wall', 'Tall', 'Vanity', 'Custom'];
const CW_TIERS = ['Base', 'Wall', 'Tall', 'Vanity', 'Custom'];
const CW_STATUSES = ['Draft', 'For Review', 'Released', 'In Production', 'Produced', 'Delivered', 'Installed', 'On Hold'];
// A cabinet at or past this point has been cut. Re-issuing a Casework Type over
// one of these is not a drawing change, it is scrap — which is exactly why the
// impact review counts them separately and loudly.
const CW_PRODUCED_STATUSES = ['In Production', 'Produced', 'Delivered', 'Installed'];

const CW_ROOM_KINDS = ['Kitchen', 'Pantry', 'Vanity / Bathroom', 'Closet', 'Wardrobe', 'Laundry',
  'Mudroom', 'Bar', 'Coffee Station', 'Reception Desk', 'TV / Media Unit', 'Shelving', 'Custom Millwork'];

const CW_MEMBER_KINDS = [
  { key: 'cabinet', label: 'Cabinet module' },
  { key: 'filler', label: 'Filler' },
  { key: 'panel', label: 'Finished end panel' },
  { key: 'appliance', label: 'Appliance / fixture opening' },
  { key: 'gap', label: 'Open gap / reserved' },
];

// PLACEMENT ZONES — 2020 splits its whole catalogue into "Wall Mounted Items"
// and "Floor Mounted Items", and that is not decoration: it is what decides
// where an item can land and what height it defaults to. The clash check here
// already compares height bands correctly (a base run passes under a window and
// a wall run does not), so the zone is not a second rule — it is the same fact
// made visible, so the catalogue can be split the way theirs is and a planner
// can offer only what belongs on the surface being clicked.
const CW_PLACEMENT_ZONES = [
  { key: 'floor', label: 'Floor Mounted Items', hint: 'Stands on the floor — base, corner and tall units.' },
  { key: 'wall',  label: 'Wall Mounted Items',  hint: 'Hung off the wall — wall cabinets and their panels.' },
  { key: 'both',  label: 'Floor to Ceiling',    hint: 'Meets both — a tall unit occupies the base and the wall band.' },
];
// Derived from the category rather than stored, so it can never disagree with
// what the cabinet actually is.
function cwPlacementZone(category) {
  const c = String(category || '');
  if (c === 'Wall') return 'wall';
  if (c === 'Tall') return 'both';
  if (c === 'Panel' || c === 'Filler') return 'both';
  return 'floor';
}
function cwPlacementZoneLabel(category) {
  const z = cwPlacementZone(category);
  return (CW_PLACEMENT_ZONES.find(x => x.key === z) || {}).label || '';
}
const CW_OBSTRUCTION_KINDS = ['Door', 'Window', 'Column', 'Soffit', 'Plumbing', 'Electrical', 'HVAC', 'Other'];

// A front row is what the face of a cabinet is divided into, top to bottom. Row
// heights that are not fixed share whatever is left by weight, which is how a
// drawer bank keeps working when the cabinet height changes.
const CW_ROW_KINDS = [
  { key: 'doors', label: 'Doors' },
  { key: 'drawers', label: 'Drawers' },
  { key: 'falseFront', label: 'False front (sink tip-out)' },
  { key: 'opening', label: 'Open / appliance opening' },
];

const CW_GRAIN = ['Length', 'Width', 'None'];
const CW_EDGES = ['front', 'back', 'left', 'right'];
const CW_EDGE_LABELS = { front: 'Front (L1)', back: 'Back (L2)', left: 'Left (W1)', right: 'Right (W2)' };

// Structured machining, deliberately NOT machine code. A 35 mm cup bore is a
// fact about the part; the post-processor that turns it into a program belongs
// to whoever owns the machine, and pretending otherwise here would produce
// files no one should run.
const CW_OPERATIONS = ['Hinge cup bore 35mm', 'System 32 shelf holes', 'Dowel', 'Back groove',
  'Drawer bottom groove', 'Toe notch', 'Sink cutout', 'Handle bore', 'Edge band'];

// ---- front styles ---------------------------------------------------------
// Parametric, never pictures — a rail height changed here redraws every door
// that uses the style rather than invalidating a folder of images.
const CW_FRONT_STYLES = [
  { key: 'slab', label: 'Flat Slab', kind: 'slab' },
  { key: 'shaker', label: 'Shaker', kind: 'frame', stile: cwIn(2.25), rail: cwIn(2.25) },
  { key: 'slimshaker', label: 'Slim Shaker', kind: 'frame', stile: cwIn(1.5), rail: cwIn(1.5) },
  { key: 'fluted', label: 'Fluted', kind: 'grooved', grooves: 11, orientation: 'Vertical', round: true },
  { key: 'reeded', label: 'Reeded', kind: 'grooved', grooves: 15, orientation: 'Vertical', round: true },
  { key: 'glass', label: 'Glass Frame', kind: 'glass', stile: cwIn(1.75), rail: cwIn(1.75) },
];
function cwFrontStyle(key) { return CW_FRONT_STYLES.find(s => s.key === key) || CW_FRONT_STYLES[0]; }

// ---- construction rules ---------------------------------------------------
// This is the part that must NOT be hard-coded. Every derived part size in the
// engine comes out of one of these numbers, and shops disagree about all of
// them — a 32 mm frameless shop and a face-frame shop do not stack up the same
// way, and neither is wrong. So they live as data and the engine reads them.
function cwMakeConstruction(data) {
  const d = data || {};
  return {
    id: d.id || uid('cwcon'),
    name: d.name || 'New construction standard',
    kind: d.kind || 'Frameless',                    // Frameless | Face Frame
    panelThickness: d.panelThickness != null ? d.panelThickness : cwIn(0.75),
    doorThickness: d.doorThickness != null ? d.doorThickness : cwIn(0.75),
    backThickness: d.backThickness != null ? d.backThickness : cwIn(0.25),
    backInset: d.backInset != null ? d.backInset : cwIn(0.5),      // set in from the rear edge
    backGrooveDepth: d.backGrooveDepth != null ? d.backGrooveDepth : 6,
    backCaptured: d.backCaptured !== false,
    // Frameless: the front field is the carcass face less an even reveal.
    reveal: d.reveal != null ? d.reveal : 3,
    gap: d.gap != null ? d.gap : 3,                 // between two adjacent fronts
    // Face frame: the frame covers the carcass face and the front overlays it.
    ffStile: d.ffStile != null ? d.ffStile : cwIn(1.5),
    ffRail: d.ffRail != null ? d.ffRail : cwIn(1.5),
    ffThickness: d.ffThickness != null ? d.ffThickness : cwIn(0.75),
    overlay: d.overlay != null ? d.overlay : cwIn(0.5),             // per edge
    hingeBoreSetback: d.hingeBoreSetback != null ? d.hingeBoreSetback : 22.5,  // cup centre from the door edge
    hingeBoreDia: d.hingeBoreDia != null ? d.hingeBoreDia : 35,
    shelfSideClearance: d.shelfSideClearance != null ? d.shelfSideClearance : 1.5,
    shelfDepthSetback: d.shelfDepthSetback != null ? d.shelfDepthSetback : cwIn(0.5),
    drawerSideClearance: d.drawerSideClearance != null ? d.drawerSideClearance : cwIn(0.5),
    drawerBoxThickness: d.drawerBoxThickness != null ? d.drawerBoxThickness : cwIn(0.5),
    drawerBottomThickness: d.drawerBottomThickness != null ? d.drawerBottomThickness : cwIn(0.25),
    drawerBottomGroove: d.drawerBottomGroove != null ? d.drawerBottomGroove : 6,
    drawerBoxHeightDeduction: d.drawerBoxHeightDeduction != null ? d.drawerBoxHeightDeduction : cwIn(2),
    slideTakeUp: d.slideTakeUp != null ? d.slideTakeUp : cwIn(1),   // depth lost at the back to the slide
    toeKickHeight: d.toeKickHeight != null ? d.toeKickHeight : cwIn(4),
    toeKickSetback: d.toeKickSetback != null ? d.toeKickSetback : cwIn(3),
    stretcherWidth: d.stretcherWidth != null ? d.stretcherWidth : cwIn(4),
    nailerWidth: d.nailerWidth != null ? d.nailerWidth : cwIn(4),
    edgeBandThickness: d.edgeBandThickness != null ? d.edgeBandThickness : 0.5,
    scribeAllowance: d.scribeAllowance != null ? d.scribeAllowance : cwIn(0.75),
    notes: d.notes || '',
  };
}

const CW_DEFAULT_CONSTRUCTIONS = [
  cwMakeConstruction({ id: 'cwcon-frameless32', name: 'Frameless — 32 mm system', kind: 'Frameless',
    notes: 'European frameless. Sides run to the floor and are notched for the toe kick; the base gets stretchers rather than a full top.' }),
  cwMakeConstruction({ id: 'cwcon-faceframe', name: 'Face Frame — ½" overlay', kind: 'Face Frame',
    ffStile: cwIn(1.5), ffRail: cwIn(1.5), overlay: cwIn(0.5), gap: cwIn(0.125),
    notes: 'Traditional face frame. The carcass is set back by the frame thickness and the front overlays the opening by ½" on every edge.' }),
];

// ---- sheet materials ------------------------------------------------------
// The cut list groups by these and the sheet estimate reads their sheet size,
// so a 5×5 Baltic birch sheet and a 4×8 melamine sheet are not averaged into
// each other.
function cwMakeMaterial(data) {
  const d = data || {};
  return {
    id: d.id || uid('cwmat'),
    name: d.name || 'New material',
    thickness: d.thickness != null ? d.thickness : cwIn(0.75),
    sheetW: d.sheetW != null ? d.sheetW : cwIn(48),
    sheetH: d.sheetH != null ? d.sheetH : cwIn(96),
    grained: !!d.grained,
    kind: d.kind || 'Panel',                        // Panel | Front | Drawer box | Back | Edge band
    finishRef: d.finishRef || null,                 // supplierCatalog() record, never a retyped copy
    notes: d.notes || '',
  };
}
const CW_DEFAULT_MATERIALS = [
  cwMakeMaterial({ id: 'cwmat-mel19', name: 'White Melamine 19 mm', kind: 'Panel' }),
  cwMakeMaterial({ id: 'cwmat-ply19', name: 'Prefinished Maple Ply ¾"', kind: 'Panel', grained: true }),
  cwMakeMaterial({ id: 'cwmat-mdf19', name: 'Paint Grade MDF ¾"', kind: 'Front' }),
  cwMakeMaterial({ id: 'cwmat-back6', name: 'White Melamine ¼" (backs)', kind: 'Back', thickness: cwIn(0.25) }),
  cwMakeMaterial({ id: 'cwmat-box12', name: 'Baltic Birch ½" (drawer boxes)', kind: 'Drawer box',
    thickness: cwIn(0.5), sheetW: cwIn(60), sheetH: cwIn(60), grained: true }),
  cwMakeMaterial({ id: 'cwmat-boxbtm', name: 'Baltic Birch ¼" (drawer bottoms)', kind: 'Drawer box',
    thickness: cwIn(0.25), sheetW: cwIn(60), sheetH: cwIn(60) }),
];

// ---- hardware -------------------------------------------------------------
// Hardware drives geometry. Choosing a hinge is what creates the cup bore and
// the plate position; a runner is what sets the drawer box width and setback; a
// handle is what sets the hole centres. Those numbers therefore live ON the
// hardware item — and when they are absent the engine says "manufacturer
// drilling data not loaded" rather than inventing a pattern, because a guessed
// bore is a scrapped door.
function cwMakeHardwareItem(data) {
  const d = data || {};
  return {
    id: d.id || uid('cwhw'),
    name: d.name || 'New item', category: d.category || 'Other',
    kind: d.kind || 'Other',          // Hinge | Slide | Handle | Other
    productNumber: d.productNumber || '', manufacturer: d.manufacturer || '',
    img: d.img || '', notes: d.notes || '',
    supplierKey: d.supplierKey || '', supplierId: d.supplierId || '',
    // Hinge
    cupDia: d.cupDia != null ? d.cupDia : null,
    cupSetback: d.cupSetback != null ? d.cupSetback : null,   // cup CENTRE from the door edge
    plateOffset: d.plateOffset != null ? d.plateOffset : null, // plate screw line from the carcass front edge
    // Slide
    slideLength: d.slideLength != null ? d.slideLength : null,
    slideSideClearance: d.slideSideClearance != null ? d.slideSideClearance : null,
    slideSetback: d.slideSetback != null ? d.slideSetback : null,
    // Handle
    boreCentres: d.boreCentres != null ? d.boreCentres : null,
  };
}
// True only when the item carries the numbers its own geometry needs. Anything
// else is an honest blank, never a default that looks like data.
function cwDrillingKnown(item) {
  if (!item) return false;
  if (item.kind === 'Hinge') return cwNum(item.cupDia) > 0 && cwNum(item.cupSetback) > 0;
  if (item.kind === 'Slide') return cwNum(item.slideLength) > 0;
  if (item.kind === 'Handle') return cwNum(item.boreCentres) > 0;
  return false;
}
// A set is a recipe. Assigning HW-02 to sixty drawer banks fills in the slides,
// the pulls and the count on all sixty — and changing the set reaches them all.
function cwMakeHardwareSet(data) {
  const d = data || {};
  return {
    id: d.id || uid('cwhwset'),
    code: d.code || 'HW-01', name: d.name || 'New hardware set',
    lines: d.lines || [],        // [{ itemId, qty, per: 'cabinet'|'door'|'drawer' }]
    notes: d.notes || '',
  };
}
const CW_DEFAULT_HARDWARE_SETS = [
  cwMakeHardwareSet({ id: 'cwhwset-door', code: 'HW-01', name: 'Standard door', lines: [] }),
  cwMakeHardwareSet({ id: 'cwhwset-drawer', code: 'HW-02', name: 'Standard drawer', lines: [] }),
];
// Which supplier catalog constructions are cabinet hardware. Pulling from the
// catalog rather than retyping is the point — Imundex alone ships 76 hinges,
// 42 slides and 87 handles with photos and codes already on file.
const CW_HARDWARE_CATS = /hinge|slide|handle|knob|pull|drawer|cabinet accessor|wardrobe accessor/i;

// ---- cabinet types --------------------------------------------------------
// A type is the module standard: "B2D is a 30 wide, 34½ high, 24 deep two-door
// base with one shelf." A mark is the cabinet actually being built. Forty marks
// can point at B2D, and changing B2D reaches all forty — except any mark that
// set its own value, which stays as it is.
function cwMakeType(data, createdBy) {
  const d = data || {};
  return {
    id: d.id || uid('cwtype'),
    code: d.code || 'NEW', name: d.name || 'New cabinet type',
    category: d.category || 'Base',
    width: d.width != null ? d.width : cwIn(24),
    height: d.height != null ? d.height : cwIn(34.5),
    depth: d.depth != null ? d.depth : cwIn(24),
    // Rows are the face, top to bottom. `h` fixes a row's height; rows without
    // one share what is left by `weight`.
    rows: d.rows ? cloneDeep(d.rows) : [{ kind: 'doors', count: 2, weight: 1 }],
    shelfCount: d.shelfCount != null ? d.shelfCount : 1,
    // 2020 carries a VERTICAL PARTITION and a HORIZONTAL PARTITION as parts in
    // their own right, and reading its category tree is what showed we had
    // neither. They are not shelves: a shelf is adjustable, sits on pins and is
    // set back from the front; a partition is FIXED, runs the full depth, and
    // is what a sink base divider or a wide cabinet split into bays is made of.
    // A cut list that calls one the other cuts the wrong part.
    vPartitionCount: d.vPartitionCount != null ? d.vPartitionCount : 0,
    hPartitionCount: d.hPartitionCount != null ? d.hPartitionCount : 0,
    toeKick: d.toeKick !== false,
    frontStyle: d.frontStyle || 'shaker',
    boxMaterialId: d.boxMaterialId || 'cwmat-mel19',
    frontMaterialId: d.frontMaterialId || 'cwmat-mdf19',
    backMaterialId: d.backMaterialId || 'cwmat-back6',
    drawerBoxMaterialId: d.drawerBoxMaterialId || 'cwmat-box12',
    edgeBanding: d.edgeBanding || 'PVC 0.5 mm — match front',
    constructionId: d.constructionId || 'cwcon-frameless32',
    hardwareSetId: d.hardwareSetId || null,
    // Corner and blind boxes are real geometry the panel engine only
    // approximates; the flag is what lets the engine say so rather than quietly
    // producing a rectangle and calling it a lazy susan.
    special: d.special || null,        // 'lazySusan' | 'blindCorner' | 'cornerWall' | 'applianceSurround'
    blindWidth: d.blindWidth != null ? d.blindWidth : 0,
    global: !!d.global,
    notes: d.notes || '',
    createdBy: createdBy || '', createdDate: todayISO(),
  };
}

// ---- the standard library -------------------------------------------------
// Real starting points, not placeholders — every one of these is a
// configuration someone can build from today, and every number in it is
// editable once imported onto a job. North American defaults: base 24" deep ×
// 34½" high, wall 12" deep at 30/36/42", tall 24" deep at 84/90/96", vanity 21"
// deep, widths on the 3" module from 9" to 48".
const CW_STANDARD_TYPES = [
  // ---- Base
  cwMakeType({ id: 'cwt-b1d', code: 'B1D', name: 'Base — 1 door', category: 'Base', global: true,
    width: cwIn(18), height: cwIn(34.5), depth: cwIn(24), shelfCount: 1,
    rows: [{ kind: 'drawers', count: 1, h: cwIn(6) }, { kind: 'doors', count: 1, weight: 1 }] }),
  cwMakeType({ id: 'cwt-b2d', code: 'B2D', name: 'Base — 2 door', category: 'Base', global: true,
    width: cwIn(30), height: cwIn(34.5), depth: cwIn(24), shelfCount: 1,
    rows: [{ kind: 'drawers', count: 1, h: cwIn(6) }, { kind: 'doors', count: 2, weight: 1 }] }),
  cwMakeType({ id: 'cwt-b3drw', code: 'B3DRW', name: 'Base — 3 drawer', category: 'Base', global: true,
    width: cwIn(24), height: cwIn(34.5), depth: cwIn(24), shelfCount: 0,
    rows: [{ kind: 'drawers', count: 3, weights: [1, 1.35, 1.35], weight: 1 }] }),
  cwMakeType({ id: 'cwt-b4drw', code: 'B4DRW', name: 'Base — 4 drawer', category: 'Base', global: true,
    width: cwIn(18), height: cwIn(34.5), depth: cwIn(24), shelfCount: 0,
    rows: [{ kind: 'drawers', count: 4, weights: [1, 1.3, 1.3, 1.3], weight: 1 }] }),
  cwMakeType({ id: 'cwt-sb', code: 'SB', name: 'Sink base', category: 'Base', global: true,
    width: cwIn(36), height: cwIn(34.5), depth: cwIn(24), shelfCount: 0,
    rows: [{ kind: 'falseFront', h: cwIn(6) }, { kind: 'doors', count: 2, weight: 1 }],
    notes: 'No shelf and no top stretcher interference under the bowl. The false front is a tip-out, not a drawer.' }),
  cwMakeType({ id: 'cwt-bls', code: 'BLS', name: 'Base — corner lazy susan', category: 'Base', global: true,
    width: cwIn(36), height: cwIn(34.5), depth: cwIn(24), shelfCount: 0, special: 'lazySusan',
    rows: [{ kind: 'doors', count: 2, weight: 1 }],
    notes: 'Bi-fold pair on the corner. The carcass is cut on the diagonal — the panel engine sizes the square box and flags the diagonal as shop-detailed.' }),
  cwMakeType({ id: 'cwt-bbc', code: 'BBC', name: 'Base — blind corner', category: 'Base', global: true,
    width: cwIn(42), height: cwIn(34.5), depth: cwIn(24), shelfCount: 1, special: 'blindCorner',
    blindWidth: cwIn(15),
    rows: [{ kind: 'drawers', count: 1, h: cwIn(6) }, { kind: 'doors', count: 1, weight: 1 }],
    notes: 'The blind portion is dead width behind the returning run. Set it to the width the adjoining run covers.' }),
  cwMakeType({ id: 'cwt-bdd', code: 'BDD', name: 'Base — drawer over door', category: 'Base', global: true,
    width: cwIn(18), height: cwIn(34.5), depth: cwIn(24), shelfCount: 1,
    rows: [{ kind: 'drawers', count: 1, h: cwIn(8) }, { kind: 'doors', count: 1, weight: 1 }] }),
  // ---- Wall
  cwMakeType({ id: 'cwt-w1d', code: 'W1D', name: 'Wall — 1 door', category: 'Wall', global: true,
    width: cwIn(15), height: cwIn(30), depth: cwIn(12), shelfCount: 2, toeKick: false,
    rows: [{ kind: 'doors', count: 1, weight: 1 }] }),
  cwMakeType({ id: 'cwt-w2d', code: 'W2D', name: 'Wall — 2 door', category: 'Wall', global: true,
    width: cwIn(30), height: cwIn(36), depth: cwIn(12), shelfCount: 2, toeKick: false,
    rows: [{ kind: 'doors', count: 2, weight: 1 }] }),
  cwMakeType({ id: 'cwt-wos', code: 'WOS', name: 'Wall — open shelf', category: 'Wall', global: true,
    width: cwIn(30), height: cwIn(30), depth: cwIn(12), shelfCount: 2, toeKick: false,
    rows: [{ kind: 'opening', weight: 1 }],
    notes: 'No fronts. Every visible edge is banded, which is why the cut list bands four edges on the shelves here and one on a closed cabinet.' }),
  cwMakeType({ id: 'cwt-wmw', code: 'WMW', name: 'Wall — microwave', category: 'Wall', global: true,
    width: cwIn(30), height: cwIn(30), depth: cwIn(15), shelfCount: 0, toeKick: false,
    special: 'applianceSurround',
    rows: [{ kind: 'doors', count: 2, weight: 1 }, { kind: 'opening', h: cwIn(18) }],
    notes: 'The opening height must be confirmed against the appliance spec — set it from the placed appliance, not from memory.' }),
  cwMakeType({ id: 'cwt-wof', code: 'WOF', name: 'Wall — over fridge', category: 'Wall', global: true,
    width: cwIn(36), height: cwIn(15), depth: cwIn(24), shelfCount: 0, toeKick: false,
    rows: [{ kind: 'doors', count: 2, weight: 1 }] }),
  cwMakeType({ id: 'cwt-wc', code: 'WC', name: 'Wall — corner', category: 'Wall', global: true,
    width: cwIn(24), height: cwIn(30), depth: cwIn(12), shelfCount: 2, toeKick: false,
    special: 'cornerWall',
    rows: [{ kind: 'doors', count: 1, weight: 1 }] }),
  // ---- Tall
  cwMakeType({ id: 'cwt-tp', code: 'TP', name: 'Tall — pantry', category: 'Tall', global: true,
    width: cwIn(24), height: cwIn(84), depth: cwIn(24), shelfCount: 5,
    rows: [{ kind: 'doors', count: 2, weight: 1.6 }, { kind: 'doors', count: 2, weight: 1 }],
    notes: 'Split doors — an 84" single door is not buildable in most fronts and will not stay flat.' }),
  cwMakeType({ id: 'cwt-tov', code: 'TOV', name: 'Tall — oven tower', category: 'Tall', global: true,
    width: cwIn(33), height: cwIn(90), depth: cwIn(24), shelfCount: 1, special: 'applianceSurround',
    rows: [{ kind: 'doors', count: 1, h: cwIn(30) }, { kind: 'opening', h: cwIn(29.5) },
           { kind: 'drawers', count: 2, weight: 1 }],
    notes: 'The opening is the oven cutout. Confirm it against the appliance spec — every manufacturer differs.' }),
  cwMakeType({ id: 'cwt-tb', code: 'TB', name: 'Tall — broom', category: 'Tall', global: true,
    width: cwIn(18), height: cwIn(84), depth: cwIn(24), shelfCount: 1,
    rows: [{ kind: 'doors', count: 1, weight: 1 }] }),
  cwMakeType({ id: 'cwt-tfs', code: 'TFS', name: 'Tall — fridge surround', category: 'Tall', global: true,
    width: cwIn(36), height: cwIn(96), depth: cwIn(24), shelfCount: 0, special: 'applianceSurround',
    rows: [{ kind: 'doors', count: 2, h: cwIn(15) }, { kind: 'opening', weight: 1 }],
    notes: 'The opening is the fridge, with its own clearances. Place the appliance in the run and the clearance check reads its spec.' }),
  // ---- Vanity
  cwMakeType({ id: 'cwt-v1d', code: 'V1D', name: 'Vanity — 1 door', category: 'Vanity', global: true,
    width: cwIn(24), height: cwIn(34.5), depth: cwIn(21), shelfCount: 1,
    rows: [{ kind: 'doors', count: 1, weight: 1 }] }),
  cwMakeType({ id: 'cwt-v2d', code: 'V2D', name: 'Vanity — 2 door', category: 'Vanity', global: true,
    width: cwIn(36), height: cwIn(34.5), depth: cwIn(21), shelfCount: 1,
    rows: [{ kind: 'falseFront', h: cwIn(6) }, { kind: 'doors', count: 2, weight: 1 }] }),
  cwMakeType({ id: 'cwt-vdb', code: 'VDB', name: 'Vanity — drawer bank', category: 'Vanity', global: true,
    width: cwIn(24), height: cwIn(34.5), depth: cwIn(21), shelfCount: 0,
    rows: [{ kind: 'drawers', count: 4, weights: [1, 1.25, 1.25, 1.25], weight: 1 }] }),
  cwMakeType({ id: 'cwt-vf', code: 'VF', name: 'Vanity — floating', category: 'Vanity', global: true,
    width: cwIn(36), height: cwIn(20), depth: cwIn(21), shelfCount: 0, toeKick: false,
    rows: [{ kind: 'drawers', count: 2, weight: 1 }],
    notes: 'No toe kick — it hangs. The blocking behind it is a site item and is not in this cut list.' }),
  cwMakeType({ id: 'cwt-vds', code: 'VDS', name: 'Vanity — double sink', category: 'Vanity', global: true,
    width: cwIn(72), height: cwIn(34.5), depth: cwIn(21), shelfCount: 1,
    rows: [{ kind: 'falseFront', h: cwIn(6) }, { kind: 'doors', count: 4, weight: 1 }],
    notes: 'Two bowls in one carcass. At 72" wide check the run arithmetic before the cabinet — a single 72" box rarely gets up a stair.' }),
];

// ---- the cabinet record ---------------------------------------------------
// The database object the whole module reads. Type-inheritable values are null
// here when they are inherited; a value present is a decision someone made on
// THIS cabinet and is shown as an override everywhere it appears.
//
// `overrides` is a different thing and deliberately so: it forces one COMPUTED
// PART size ('door.1' -> { width: 590 }) when the shop cuts a part off the
// arithmetic. Keeping it separate is what stops there being two homes for the
// same number.
function cwMakeCabinet(data, createdBy) {
  const d = data || {};
  return {
    id: d.id || uid('cwcab'),
    mark: d.mark || 'B-01',
    typeId: d.typeId || null,
    scopeId: d.scopeId || null,
    roomId: d.roomId || null,
    runId: d.runId || null,
    memberKey: d.memberKey || null,      // stable link back to the Casework Type member
    room: d.room || '', unit: d.unit || '', level: d.level || '',
    qty: d.qty != null ? d.qty : 1,
    width: d.width != null ? d.width : null,
    height: d.height != null ? d.height : null,
    depth: d.depth != null ? d.depth : null,
    rows: d.rows || null,
    shelfCount: d.shelfCount != null ? d.shelfCount : null,
    toeKick: d.toeKick != null ? d.toeKick : null,
    frontStyle: d.frontStyle || null,
    doorStyle: d.doorStyle || null,           // free text describing the front, e.g. "Shaker — paint grade"
    finish: d.finish || null,                 // supplierCatalog() reference, never a retyped copy
    boxMaterialId: d.boxMaterialId || null,
    frontMaterialId: d.frontMaterialId || null,
    edgeBanding: d.edgeBanding || null,
    hardwareSetId: d.hardwareSetId || null,
    constructionId: d.constructionId || null,
    fillerLeft: d.fillerLeft != null ? d.fillerLeft : 0,
    fillerRight: d.fillerRight != null ? d.fillerRight : 0,
    scribeLeft: d.scribeLeft != null ? d.scribeLeft : 0,
    scribeRight: d.scribeRight != null ? d.scribeRight : 0,
    finishedEndLeft: !!d.finishedEndLeft,
    finishedEndRight: !!d.finishedEndRight,
    countertopRef: d.countertopRef || null,   // a LEON Stone piece id once sent
    applianceCutouts: d.applianceCutouts || [],  // [{ id, instanceId, label, w, h, d, note }]
    handing: d.handing || 'Auto',             // Auto | L | R  — which side the hinges are on
    status: d.status || 'Draft',
    revision: d.revision != null ? d.revision : 0,
    notes: d.notes || '',
    overrides: d.overrides ? cloneDeep(d.overrides) : {},
    history: d.history ? cloneDeep(d.history) : [],
    createdBy: createdBy || '', createdDate: todayISO(),
  };
}

// ---- rooms, walls, runs ---------------------------------------------------
function cwMakeWall(data) {
  const d = data || {};
  return {
    id: d.id || uid('cwwall'),
    name: d.name || 'Wall A',
    length: d.length != null ? d.length : cwIn(144),
    height: d.height != null ? d.height : cwIn(96),
    obstructions: d.obstructions ? cloneDeep(d.obstructions) : [],
    notes: d.notes || '',
  };
}
// An obstruction is measured from the LEFT end of the wall, looking at it. Every
// clash check and every mirror reads that one convention, so there is never a
// question about which end "from" means.
function cwMakeObstruction(data) {
  const d = data || {};
  return {
    id: d.id || uid('cwobs'),
    kind: d.kind || 'Window',
    label: d.label || '',
    fromLeft: d.fromLeft != null ? d.fromLeft : 0,
    width: d.width != null ? d.width : cwIn(36),
    sillHeight: d.sillHeight != null ? d.sillHeight : cwIn(36),
    height: d.height != null ? d.height : cwIn(48),
    notes: d.notes || '',
  };
}
// ── The Countertop Wizard ───────────────────────────────────────────────────
// 2020 generates the worktop over the base cabinets rather than making anyone
// re-measure it, and that is the one connection this module was missing: it
// could send a RECTANGLE to the stone cut list, which is a number, not a
// countertop. This generates the real drawn counter — outline, depth,
// overhang, the wall side marked as splash and the room side as finished
// edge — straight into LEON Countertop's own quote record.
//
// Nothing is copied. The counter is created there and lives there; re-running
// the wizard replaces what it generated and leaves anything drawn by hand
// alone, because a generated piece is marked as one.
//
// A wall here carries a LENGTH and not an angle, so the walls are taken to
// turn 90 degrees in the order they are listed — which is how a room entered
// as wall lengths is read, and what makes an L come out as an L. A room that
// is not square is dragged into shape in LEON Countertop; the wizard says so
// rather than inventing a geometry it was never given.
function cwCounterPlan(project, ctx, room, opts) {
  const o = opts || {};
  const overhang = cwNum(o.overhangIn != null ? cwIn(o.overhangIn) : cwIn(1.5));
  const ct = cwCaseworkTypes(project).find(t => t.id === room.caseworkTypeId);
  if (!ct) return { pieces: [], issues: ['This room is not linked to a casework type, so it has no runs to read.'] };
  const walls = ct.walls || [];
  const runs = (ct.runs || []).filter(r => r.tier !== 'Wall');
  const pieces = [], issues = [];
  // Walk the walls in order, turning right at each corner. dir 0=+x, 1=+y,
  // 2=-x, 3=-y; the counter depth goes INTO the room, which is the left of
  // the direction of travel.
  const DIR = [[1, 0], [0, 1], [-1, 0], [0, -1]];
  let cx = 0, cy = 0;
  // At an inside corner two runs meet and their counters would overlap by one
  // depth — which quietly double-counts a slab's worth of stone on every L.
  // The convention is that the FIRST run keeps the corner and the returning
  // run starts at its front face, so the returning run is trimmed by the depth
  // of the piece before it. `prevDepth` carries that across the corner.
  let prevDepth = 0;
  walls.forEach((wall, wi) => {
    const [ux, uy] = DIR[wi % 4];
    const nx = -uy, ny = ux;                    // into the room
    const wallStart = { x: cx, y: cy };
    let thisWallDepth = 0;
    runs.filter(r => r.wallId === wall.id).forEach(run => {
      const layout = cwRunLayout(project, ctx, room, wall, run);
      const cabs = (layout.members || []).filter(m =>
        m.member.kind === 'cabinet' || m.member.kind === 'appliance');
      if (!cabs.length) return;
      const x1 = Math.min.apply(null, cabs.map(m => m.x));
      const x2 = Math.max.apply(null, cabs.map(m => m.x + m.w));
      const boxes = cabs.filter(m => m.type).map(m => cwNum(m.type.depth)).filter(d => d > 0);
      if (!boxes.length) {
        issues.push(`${run.name} on ${wall.name}: no cabinet carries a depth, so its counter depth had to fall back to 24".`);
      }
      const depth = (boxes.length ? Math.max.apply(null, boxes) : cwIn(24)) + overhang;
      thisWallDepth = Math.max(thisWallDepth, depth);
      // A run that starts at the corner gives the corner to the run before it.
      const atCorner = wi > 0 && prevDepth > 0 && x1 < cwIn(1);
      const startX = atCorner ? x1 + prevDepth : x1;
      if (atCorner && startX >= x2) {
        issues.push(`${run.name} on ${wall.name} is shorter than the corner it returns into, so it produced no counter of its own.`);
        return;
      }
      if (atCorner) {
        issues.push(`${wall.name} — ${run.name} starts ${(prevDepth / 25.4).toFixed(1)}" in from the corner: the run before it keeps the corner, so the two pieces meet rather than overlap.`);
      }
      // Corners in INCHES, which is the countertop module's own unit.
      const toIn = mm => mm / 25.4;
      const a = { x: toIn(wallStart.x + ux * startX), y: toIn(wallStart.y + uy * startX) };
      const b = { x: toIn(wallStart.x + ux * x2), y: toIn(wallStart.y + uy * x2) };
      const dnx = toIn(nx * depth), dny = toIn(ny * depth);
      pieces.push({
        runId: run.id, wallId: wall.id,
        name: `${wall.name} — ${run.name}`,
        lengthIn: toIn(x2 - startX), depthIn: toIn(depth), overhangIn: toIn(overhang),
        cabinetCount: cabs.length,
        // Drawn wall-side first, so segment 0 is always the run against the
        // wall and segment 2 is always the front. That is what lets the kinds
        // below be assigned without guessing.
        points: [a, b, { x: b.x + dnx, y: b.y + dny }, { x: a.x + dnx, y: a.y + dny }],
      });
    });
    cx += ux * cwNum(wall.length); cy += uy * cwNum(wall.length);
    prevDepth = thisWallDepth;
  });
  if (!pieces.length) issues.push('No base or tall run on this room carries a cabinet, so there is no countertop to generate.');
  return { pieces, issues, overhang };
}

// Writing the plan into LEON Countertop. The quote is that module's record and
// stays that module's record; this only creates or refreshes the counters the
// wizard itself generated, which is what `fromRunId` marks. A counter drawn by
// hand in there is never touched.
function cwWriteCounters(ctx, project, room, plan) {
  let quoteId = null, made = 0, replaced = 0;
  ctx.updateProject(project.id, draft => {
    if (!Array.isArray(draft.countertopQuotes)) draft.countertopQuotes = [];
    let q = draft.countertopQuotes.find(x => x.fromCaseworkRoomId === room.id);
    if (!q) {
      q = makeCtQuote({
        name: `${room.name} — countertops`,
        fromCaseworkRoomId: room.id,
        scopeId: room.scopeId || null,
        notes: 'Generated from LEON Casework. The runs are the record; re-run the wizard after a run changes.',
      }, ctx.currentUserName);
      draft.countertopQuotes.push(q);
    }
    quoteId = q.id;
    if (!Array.isArray(q.areas)) q.areas = [];
    let area = q.areas.find(a => a.fromCaseworkRoomId === room.id);
    if (!area) {
      area = makeCtArea({ name: (room.name || 'KITCHEN').toUpperCase(), fromCaseworkRoomId: room.id });
      q.areas.push(area);
    }
    if (!Array.isArray(area.counters)) area.counters = [];
    plan.pieces.forEach(pc => {
      const points = pc.points.map(p => makeCtPoint(p.x, p.y));
      // Wall side, right return, front, left return — in that order, which is
      // the order the plan draws them. The wall side carries the splash and
      // the room side is the finished edge; the shop reads that off the
      // drawing rather than being told twice.
      const segments = [
        { kind: 'Splash', edgeProfile: '', splashHeight: 0, note: 'Against the wall', parts: null },
        { kind: 'Finished', edgeProfile: '', splashHeight: 0, note: '', parts: null },
        { kind: 'Finished', edgeProfile: '', splashHeight: 0, note: 'Front', parts: null },
        { kind: 'Finished', edgeProfile: '', splashHeight: 0, note: '', parts: null },
      ];
      const existing = area.counters.find(c => c.fromRunId === pc.runId);
      if (existing) {
        existing.points = points; existing.name = pc.name;
        existing.overhangs = { front: pc.overhangIn, left: 0, right: 0, back: 0 };
        // Segment KINDS the team has changed are theirs; only the geometry is
        // regenerated. Overwriting an edge someone set to Unfinished because a
        // dishwasher went in would be the wizard undoing a real decision.
        if (!Array.isArray(existing.segments) || existing.segments.length !== 4) existing.segments = segments;
        replaced += 1;
      } else {
        area.counters.push(makeCtCounter({
          name: pc.name, template: 'Single Run', points, segments,
          fromRunId: pc.runId, fromCaseworkRoomId: room.id,
          overhangs: { front: pc.overhangIn, left: 0, right: 0, back: 0 },
        }));
        made += 1;
      }
    });
    ctx.logAction(draft, `LEON Casework — countertops generated for ${room.name}: ${made} new, ${replaced} updated.`);
  });
  return { quoteId, made, replaced };
}

function cwMakeRun(data) {
  const d = data || {};
  return {
    id: d.id || uid('cwrun'),
    wallId: d.wallId || null,
    name: d.name || 'Base run',
    tier: d.tier || 'Base',
    startOffset: d.startOffset != null ? d.startOffset : 0,
    // Height off the finished floor to the BOTTOM of the run. It is what makes
    // a clash check meaningful: a base run and a wall run cross the same window
    // in plan and only one of them actually hits it.
    zBottom: d.zBottom != null ? d.zBottom : (d.tier === 'Wall' ? cwIn(54) : 0),
    // Ordered left to right along the wall. This ordering IS the layout — there
    // is no canvas, and a list is what a shop drawing schedule actually is.
    members: d.members ? cloneDeep(d.members) : [],
    // TRIM RUNS. 2020 generates crown, light rail, toe kick and scribe along a
    // run rather than making anyone count them; this module named them in the
    // component library and then left the joiner to measure them by hand.
    // They are stored as a CHOICE per kind, never as a length — the length is
    // the run's own cabinet extent, so a cabinet added tomorrow lengthens the
    // crown with it and the two can never fall out of step.
    trims: d.trims ? cloneDeep(d.trims) : {},
    notes: d.notes || '',
  };
}
// What a trim run is, per kind: where it sits on the run, and whether the
// returns at each end add to its length. A crown returns onto the wall at both
// ends; a toe kick does not.
const CW_TRIM_KINDS = [
  { key: 'crown', label: 'Crown molding', at: 'top', tiers: ['Wall', 'Tall'],
    returns: 2, note: 'Along the top of the run, returning onto the wall at each end.' },
  { key: 'lightRail', label: 'Light rail', at: 'bottom', tiers: ['Wall'],
    returns: 2, note: 'Under the wall cabinets, hiding the under-cabinet lighting.' },
  { key: 'toeKick', label: 'Toe kick skin', at: 'bottom', tiers: ['Base', 'Tall'],
    returns: 0, note: 'The finish skin across the front of the toe space.' },
  { key: 'scribe', label: 'Scribe / filler strip', at: 'top', tiers: ['Base', 'Wall', 'Tall'],
    returns: 0, note: 'The strip that closes the gap to the wall or ceiling.' },
];
// The length of a trim run is DERIVED — the cabinet extent of the run, plus a
// return at each end where the profile has one, plus the exposed end returns.
// Nothing is stored, so it cannot go stale.
function cwTrimRuns(project, layout, run, con) {
  const out = [];
  const trims = (run && run.trims) || {};
  const cabs = (layout.members || []).filter(m => m.member.kind === 'cabinet' || m.member.kind === 'appliance');
  if (!cabs.length) return out;
  const x1 = Math.min.apply(null, cabs.map(m => m.x));
  const x2 = Math.max.apply(null, cabs.map(m => m.x + m.w));
  const extent = Math.max(0, x2 - x1);
  const depth = cwNum((con && con.baseDepth) || 0);
  CW_TRIM_KINDS.forEach(k => {
    const t = trims[k.key];
    if (!t || !t.on) return;
    const ret = cwNum(t.returnEach != null ? t.returnEach : (k.returns ? depth : 0));
    const lenIn = extent + ret * k.returns;
    out.push({
      key: k.key, label: t.label || k.label, at: k.at, profile: t.profile || '',
      lengthIn: lenIn, returnEach: ret, returns: k.returns,
      // Trim is bought in sticks, so the count is what actually gets ordered.
      stickIn: cwNum(t.stickIn) || cwIn(96),
      sticks: (cwNum(t.stickIn) || cwIn(96)) > 0 ? Math.ceil(lenIn / (cwNum(t.stickIn) || cwIn(96))) : 0,
      math: `${cwN(extent)} of cabinet${k.returns ? ` + ${k.returns} × ${cwN(ret)} return` : ''} = ${cwN(lenIn)}`,
    });
  });
  return out;
}
function cwMakeMember(data) {
  const d = data || {};
  return {
    id: d.id || uid('cwmem'),
    kind: d.kind || 'cabinet',
    cabTypeId: d.cabTypeId || null,
    label: d.label || '',
    width: d.width != null ? d.width : null,     // null on a cabinet = take the type's width
    height: d.height != null ? d.height : null,
    depth: d.depth != null ? d.depth : null,
    handing: d.handing || 'Auto',
    applianceKind: d.applianceKind || '',        // APPLIANCE_TYPES / FIXTURE_TYPES value
    // HOW FAR THE NUMBERS CAN BE TRUSTED. The company's own rule APP-001 says a
    // GENERIC appliance is for estimating and layout and is never
    // production-approved; APP-003 says an installation document must have been
    // reviewed before fabrication. Neither can be enforced unless the record
    // carries which of those it is, and it did not.
    dataStatus: d.dataStatus || 'Generic',
    libTemplate: d.libTemplate || '',            // the CW_LIB_APPLIANCES row it came from
    specKind: d.specKind || 'appliance',         // which of the two shared libraries it comes from
    specId: d.specId || null,                    // ctx.applianceLibrary / ctx.fixtureLibrary record
    openingW: d.openingW != null ? d.openingW : null,
    openingH: d.openingH != null ? d.openingH : null,
    clearanceEach: d.clearanceEach != null ? d.clearanceEach : 0,
    notes: d.notes || '',
  };
}

// ---- Casework Types (the multifamily payoff) ------------------------------
// "Kitchen Type K-A", designed once and assigned to 40+ units. It carries the
// whole room — walls, runs, members — and the unit instances carry what is true
// only of them: the field measurement, the production status, the overrides.
function cwMakeCaseworkType(data, createdBy) {
  const d = data || {};
  return {
    id: d.id || uid('cwct'),
    code: d.code || 'K-A', name: d.name || 'Kitchen Type A',
    kind: d.kind || 'Kitchen',
    ceilingHeight: d.ceilingHeight != null ? d.ceilingHeight : cwIn(96),
    walls: d.walls ? cloneDeep(d.walls) : [cwMakeWall({ name: 'Wall A' })],
    runs: d.runs ? cloneDeep(d.runs) : [],
    constructionId: d.constructionId || 'cwcon-frameless32',
    frontStyle: d.frontStyle || 'shaker',
    revision: d.revision != null ? d.revision : 1,
    issued: d.issued ? cloneDeep(d.issued) : [],   // [{ rev, date, by, note, applied }]
    notes: d.notes || '',
    createdBy: createdBy || '', createdDate: todayISO(),
  };
}
// A physical room. It points at a Casework Type and stores only what it
// disagrees with — which is the whole reason re-issuing K-A does not wipe the
// field measurement someone took in 1204.
// What a plan is traced over. A background with no scale is a picture: it can
// be lined up by eye but nothing can be measured off it, and `scaleInPerPx`
// being null is what the screen reads to say so.
function cwMakeBackground(data) {
  const d = data || {};
  return {
    id: d.id || uid('cwbg'),
    name: d.name || '', url: d.url || '',
    scaleInPerPx: d.scaleInPerPx != null ? d.scaleInPerPx : null,
    opacity: d.opacity != null ? d.opacity : 0.35,
    offsetX: d.offsetX || 0, offsetY: d.offsetY || 0,
    rotation: d.rotation || 0,
    locked: d.locked !== false,
    addedBy: d.addedBy || '', addedDate: d.addedDate || todayISO(),
  };
}
// A filed reference, not a viewer. The Hub cannot open a .skp and does not
// claim to — this records which model the plan belongs to so the two do not
// drift apart on somebody's desktop.
function cwMakeSketchupRef(data) {
  const d = data || {};
  return {
    id: d.id || uid('cwskp'),
    name: d.name || '', url: d.url || '',
    version: d.version || '', notes: d.notes || '',
    addedBy: d.addedBy || '', addedDate: d.addedDate || todayISO(),
  };
}

function cwMakeRoom(data, createdBy) {
  const d = data || {};
  return {
    id: d.id || uid('cwroom'),
    caseworkTypeId: d.caseworkTypeId || null,
    name: d.name || 'Kitchen',
    kind: d.kind || 'Kitchen',
    unit: d.unit || '', level: d.level || '',
    scopeId: d.scopeId || null,
    mirrored: !!d.mirrored,
    ceilingHeight: d.ceilingHeight != null ? d.ceilingHeight : null,
    // Field measurement, per wall id. A real number here beats the type's
    // nominal every time, and the run arithmetic says so on screen.
    fieldMeasure: d.fieldMeasure ? cloneDeep(d.fieldMeasure) : {},
    appliedRevision: d.appliedRevision != null ? d.appliedRevision : null,
    status: d.status || 'Draft',
    // BACKGROUND DRAWING — 2020 stores one per plan, and it is the thing you
    // set out over: the architect's plan, scaled and faded, with the run drawn
    // on top of it. `scaleInPerPx` is what makes it a drawing rather than a
    // picture — without a scale it can be traced but not measured against, and
    // the screen says which of the two you have.
    background: d.background ? cloneDeep(d.background) : null,
    // SKETCHUP MODEL — a reference, deliberately not a viewer. 2020 stores the
    // model with the plan; a browser cannot open .skp, and pretending otherwise
    // would be worse than filing it and saying so.
    sketchupModel: d.sketchupModel ? cloneDeep(d.sketchupModel) : null,
    notes: d.notes || '',
    createdBy: createdBy || '', createdDate: todayISO(),
  };
}

// ---- grain & veneer -------------------------------------------------------
// Not decoration. A tall pantry's upper and lower doors have to come off one
// coordinated sequence or the join reads as two different trees, so a match
// mode is a property of the PANEL and a Continuous group travels through
// nesting together instead of being optimised apart.
const CW_GRAIN_DIRS = ['Vertical', 'Horizontal', 'None'];
const CW_MATCH_MODES = [
  { key: 'Continuous', label: 'Continuous', note: 'One run of veneer across the whole group. Kept together through nesting.' },
  { key: 'Sequence', label: 'Sequence', note: 'Cut in leaf order. Kept together through nesting.' },
  { key: 'Bookmatch', label: 'Bookmatch', note: 'Alternate leaves flipped. Kept together through nesting.' },
  { key: 'Slip', label: 'Slip match', note: 'Leaves slid, not flipped. Kept together through nesting.' },
  { key: 'Random', label: 'Random', note: 'No match required — nested freely.' },
];
function cwMatchIsGrouped(mode) { return mode && mode !== 'Random'; }

// ---- per-project settings -------------------------------------------------
// Construction standards, materials, hardware and nesting settings are saved on
// the JOB. A shop standard changed for one client must not silently re-cut work
// on another job that is already in production, and there is no shared casework
// library slot in app state — so the job owns its standards and says so.
function cwSettings(project) {
  const s = (project && project.caseworkSettings) || {};
  return {
    constructions: s.constructions && s.constructions.length ? s.constructions : CW_DEFAULT_CONSTRUCTIONS,
    materials: s.materials && s.materials.length ? s.materials : CW_DEFAULT_MATERIALS,
    hardware: s.hardware || [],
    hardwareSets: s.hardwareSets && s.hardwareSets.length ? s.hardwareSets : CW_DEFAULT_HARDWARE_SETS,
    nest: Object.assign({
      kerf: 3.2,                       // 1/8" saw kerf
      mode: 'minSheets',               // minSheets | grain
      minOffcutW: cwIn(6), minOffcutH: cwIn(6),
      trim: 0,                         // edge trim taken off each sheet before nesting
    }, s.nest || {}),
    panelRemnants: s.panelRemnants || [],
    defaultBand: s.defaultBand || 'PVC 0.5 mm — match front',
    seeded: !!s.seeded,
  };
}
function cwSetSettings(ctx, project, fn, action) {
  ctx.updateProject(project.id, draft => {
    // The defaults are a SEED, not a live fallback: the first edit writes the
    // whole set down so an admin's tuning can never be quietly replaced by a
    // later change to the constants in this file.
    const base = cloneDeep(cwSettings(project));
    base.seeded = true;
    fn(base);
    draft.caseworkSettings = base;
    if (action) ctx.logAction(draft, action);
  });
}
// A throwaway cabinet used only to DRAW a type on a library card. It is built
// as a literal rather than through cwMakeCabinet on purpose: that factory calls
// uid(), and minting thousands of ids per second during render pushes the id
// counter far ahead of the clock it is seeded from — which is precisely how
// this app once aliased two unrelated records onto one id.
function cwDemoCabinet(typeId, mark) {
  return { id: `cwdemo-${typeId}`, mark: mark || '', typeId, qty: 1,
    fillerLeft: 0, fillerRight: 0, scribeLeft: 0, scribeRight: 0,
    finishedEndLeft: false, finishedEndRight: false,
    applianceCutouts: [], overrides: {}, history: [], status: 'Draft', handing: 'Auto' };
}
function cwItems(project) { return (project && project.caseworkItems) || []; }
function cwProjectTypes(project) { return (project && project.caseworkTypes) || []; }
function cwCaseworkTypes(project) { return (project && project.caseworkRoomTypes) || []; }
function cwRooms(project) { return (project && project.caseworkRooms) || []; }
function cwAllTypes(project) { return [...cwProjectTypes(project), ...CW_STANDARD_TYPES]; }
function cwType(project, id) { return cwAllTypes(project).find(t => t.id === id) || null; }
function cwConstruction(project, id) {
  const list = cwSettings(project).constructions;
  return list.find(c => c.id === id) || list[0] || CW_DEFAULT_CONSTRUCTIONS[0];
}
function cwMaterial(project, id) {
  const list = cwSettings(project).materials;
  return list.find(m => m.id === id) || null;
}

// ---- resolution: type first, then what this cabinet disagrees with --------
function cwResolve(project, cab) {
  const type = cwType(project, cab.typeId);
  const own = [];
  function pick(f, dflt) {
    const v = cab[f];
    if (v !== null && v !== undefined && v !== '') { own.push(f); return v; }
    if (type) {
      const tv = type[f];
      if (tv !== null && tv !== undefined && tv !== '') return tv;
    }
    return dflt;
  }
  const r = {
    type, ownFields: own,
    category: pick('category', 'Base'),
    width: cwNum(pick('width', cwIn(24))),
    height: cwNum(pick('height', cwIn(34.5))),
    depth: cwNum(pick('depth', cwIn(24))),
    rows: pick('rows', [{ kind: 'doors', count: 2, weight: 1 }]),
    shelfCount: cwNum(pick('shelfCount', 1)),
    // A partition count resolves type -> cabinet like every other field, or the
    // part is computed off a number the cabinet never actually carries.
    vPartitionCount: cwNum(pick('vPartitionCount', 0)),
    hPartitionCount: cwNum(pick('hPartitionCount', 0)),
    frontStyle: pick('frontStyle', 'shaker'),
    doorStyle: pick('doorStyle', ''),
    finish: pick('finish', null),
    boxMaterialId: pick('boxMaterialId', 'cwmat-mel19'),
    frontMaterialId: pick('frontMaterialId', 'cwmat-mdf19'),
    backMaterialId: (type && type.backMaterialId) || 'cwmat-back6',
    drawerBoxMaterialId: (type && type.drawerBoxMaterialId) || 'cwmat-box12',
    edgeBanding: pick('edgeBanding', 'PVC 0.5 mm — match front'),
    hardwareSetId: pick('hardwareSetId', null),
    constructionId: pick('constructionId', 'cwcon-frameless32'),
    special: (type && type.special) || null,
    blindWidth: cwNum((type && type.blindWidth) || 0),
  };
  // toeKick is a boolean, so the "empty means inherit" test above would read a
  // deliberate false as unset. It gets its own branch rather than a clever one.
  if (cab.toeKick !== null && cab.toeKick !== undefined) { own.push('toeKick'); r.toeKick = !!cab.toeKick; }
  else r.toeKick = type ? type.toeKick !== false : true;
  return r;
}
function cwOwn(res, f) { return res.ownFields.indexOf(f) >= 0; }

// ============================================================================
// THE ENGINE
// ----------------------------------------------------------------------------
// Everything below is derived. Nothing is stored. That is the whole design:
// change a cabinet from 30" to 33" and the elevation, the carcass parts, the
// door and drawer fronts, the shelves, the BOM, the edge-band totals, the
// machining coordinates and the sheet requirement all move, because none of
// them was ever written down separately to fall out of step.
// ============================================================================

// ---- front layout ---------------------------------------------------------
// The face of the cabinet, divided into rows top to bottom and cells left to
// right. Every cell carries the arithmetic that produced it — a number a user
// cannot check is a number they will not trust.
function cwFrontLayout(res, con) {
  const W = res.width, H = res.height;
  const toeH = res.toeKick ? cwNum(con.toeKickHeight) : 0;
  const frontH = H - toeH;
  const faceFrame = con.kind === 'Face Frame';
  const notes = [];
  const cells = [];

  // The blind portion of a blind-corner box carries no front — it is dead width
  // behind the returning run.
  const blind = res.special === 'blindCorner' ? cwNum(res.blindWidth) : 0;
  const faceW = W - blind;

  let fx, fw, fy, fh;
  if (faceFrame) {
    const opW = faceW - 2 * cwNum(con.ffStile);
    const opH = frontH - 2 * cwNum(con.ffRail);
    fx = cwNum(con.ffStile) - cwNum(con.overlay);
    fw = opW + 2 * cwNum(con.overlay);
    fy = cwNum(con.ffRail) - cwNum(con.overlay);
    fh = opH + 2 * cwNum(con.overlay);
    notes.push({ label: 'Face-frame opening',
      expr: `${cwN(faceW)} − 2 × ${cwN(con.ffStile)} stile = ${cwN(opW)} wide; ${cwN(frontH)} − 2 × ${cwN(con.ffRail)} rail = ${cwN(opH)} high` });
    notes.push({ label: 'Front field',
      expr: `opening ${cwN(opW)} × ${cwN(opH)} + 2 × ${cwN(con.overlay)} overlay each way = ${cwN(fw)} × ${cwN(fh)}` });
  } else {
    fx = cwNum(con.reveal); fw = faceW - 2 * cwNum(con.reveal);
    fy = cwNum(con.reveal); fh = frontH - 2 * cwNum(con.reveal);
    notes.push({ label: 'Front field',
      expr: `carcass face ${cwN(faceW)} × ${cwN(frontH)} − 2 × ${cwN(con.reveal)} reveal each way = ${cwN(fw)} × ${cwN(fh)}` });
  }

  // Expand the rows. A `drawers` row of count k becomes k rows so that every
  // front on the cabinet is one cell and the vertical split is one calculation.
  const raw = [];
  (res.rows || []).forEach((row, ri) => {
    if (row.kind === 'drawers' && cwNum(row.count) > 1) {
      const k = Math.round(cwNum(row.count));
      const ws = row.weights && row.weights.length === k ? row.weights : new Array(k).fill(1);
      const fixedEach = row.h != null ? cwNum(row.h) : null;
      for (let i = 0; i < k; i++) raw.push({ kind: 'drawers', count: 1, h: fixedEach, weight: cwNum(ws[i]) || 1, ri, si: i });
    } else {
      raw.push(Object.assign({}, row, { ri, si: 0 }));
    }
  });
  if (!raw.length) raw.push({ kind: 'opening', weight: 1, ri: 0, si: 0 });

  const gap = cwNum(con.gap);
  const gaps = Math.max(0, raw.length - 1) * gap;
  const fixed = raw.reduce((a, r) => a + (r.h != null ? cwNum(r.h) : 0), 0);
  const flexRows = raw.filter(r => r.h == null);
  const weightSum = flexRows.reduce((a, r) => a + (cwNum(r.weight) || 1), 0) || 1;
  const flexTotal = fh - gaps - fixed;
  if (raw.length > 1) {
    notes.push({ label: 'Row heights',
      expr: `field ${cwN(fh)} − ${raw.length - 1} × ${cwN(gap)} gap${fixed ? ` − ${cwN(fixed)} fixed` : ''} = ${cwN(flexTotal)} shared by ${flexRows.length} row${flexRows.length === 1 ? '' : 's'}` });
  }

  let y = fy;
  raw.forEach((row, idx) => {
    const rh = row.h != null ? cwNum(row.h) : flexTotal * ((cwNum(row.weight) || 1) / weightSum);
    if (row.kind === 'doors') {
      const n = Math.max(1, Math.round(cwNum(row.count) || 1));
      const usable = fw - (n - 1) * gap;
      const dw = usable / n;
      for (let i = 0; i < n; i++) {
        cells.push({
          key: `door.${cells.length + 1}`, kind: 'door', row: idx, index: i,
          x: fx + i * (dw + gap), y, w: dw, h: rh,
          math: n === 1
            ? `width = field ${cwN(fw)} = ${cwN(dw)}; height = ${cwN(rh)}`
            : `width = (field ${cwN(fw)} − ${n - 1} × ${cwN(gap)} gap) ÷ ${n} = ${cwN(dw)}; height = ${cwN(rh)}`,
        });
      }
    } else if (row.kind === 'drawers') {
      cells.push({
        key: `drawer.${cells.filter(c => c.kind === 'drawer').length + 1}`, kind: 'drawer', row: idx, index: 0,
        x: fx, y, w: fw, h: rh,
        math: `width = field ${cwN(fw)}; height = ${row.h != null ? `set at ${cwN(rh)}` : `share ${cwN(rh)} of ${cwN(flexTotal)}`}`,
      });
    } else if (row.kind === 'falseFront') {
      cells.push({
        key: `false.${cells.filter(c => c.kind === 'falseFront').length + 1}`, kind: 'falseFront', row: idx, index: 0,
        x: fx, y, w: fw, h: rh,
        math: `tip-out panel, no drawer box behind it — width ${cwN(fw)}, height ${cwN(rh)}`,
      });
    } else {
      cells.push({
        key: `open.${cells.filter(c => c.kind === 'opening').length + 1}`, kind: 'opening', row: idx, index: 0,
        x: fx, y, w: fw, h: rh,
        math: `clear opening ${cwN(fw)} × ${cwN(rh)}`,
      });
    }
    y += rh + gap;
  });

  return { faceW, frontH, toeH, fieldX: fx, fieldY: fy, fieldW: fw, fieldH: fh,
           blind, cells, notes, faceFrame, rowCount: raw.length };
}

// ---- panels ---------------------------------------------------------------
// One shape for every part in the module. `math` is not a nicety: a shop that
// cannot check a number will re-measure it, and then there are two numbers.
function cwPanel(o) {
  return {
    key: o.key, name: o.name, group: o.group || 'Carcass',
    materialId: o.materialId || null, materialName: o.materialName || '',
    thickness: cwNum(o.thickness),
    length: cwRound(o.length), width: cwRound(o.width), qty: Math.max(1, Math.round(cwNum(o.qty) || 1)),
    grain: o.grain || 'None',               // how the grain runs on THIS panel
    matchMode: o.matchMode || 'Random',
    grainGroup: o.grainGroup || null,
    // Per edge, not one project total. front/back run the panel's LENGTH;
    // left/right run its WIDTH — that is what makes the band total add up.
    edges: Object.assign({ front: '', back: '', left: '', right: '' }, o.edges || {}),
    ops: o.ops || [],
    math: o.math || '',
    visible: !!o.visible,
    note: o.note || '',
    overridden: false, baseLength: cwRound(o.length), baseWidth: cwRound(o.width),
  };
}
function cwPanelBandLength(p) {
  const e = p.edges || {};
  let mm = 0;
  if (e.front) mm += p.length;
  if (e.back) mm += p.length;
  if (e.left) mm += p.width;
  if (e.right) mm += p.width;
  return mm * p.qty;
}
function cwPanelArea(p) { return (p.length * p.width * p.qty) / 1e6; }   // m²

// ---- the part-size engine -------------------------------------------------
// From W × H × D plus the construction rule, every part. This is the function
// the "change 30 to 33 and everything moves" test lands on.
function cwComputeParts(project, cab, opts) {
  const res = cwResolve(project, cab);
  const con = cwConstruction(project, res.constructionId);
  const layout = cwFrontLayout(res, con);
  const settings = cwSettings(project);
  const sets = settings.hardwareSets;
  const hwItems = settings.hardware;
  const hwSet = sets.find(s => s.id === res.hardwareSetId) || null;
  const band = res.edgeBanding || settings.defaultBand;
  const issues = [];
  const math = [];
  const panels = [];
  const trace = (opts && opts.trace) || {};

  const t = cwNum(con.panelThickness);
  const W = res.width, H = res.height, D = res.depth;
  const ffT = con.kind === 'Face Frame' ? cwNum(con.ffThickness) : 0;
  const carcassD = D - ffT;
  const toeH = res.toeKick ? cwNum(con.toeKickHeight) : 0;
  const interiorW = W - 2 * t;
  const deckDepth = carcassD - cwNum(con.backInset) - cwNum(con.backThickness);
  const fullTop = res.category !== 'Base' && res.category !== 'Vanity';
  const interiorH = H - toeH - t - (fullTop ? t : 0);

  const matBox = cwMaterial(project, res.boxMaterialId);
  const matFront = cwMaterial(project, res.frontMaterialId);
  const matBack = cwMaterial(project, res.backMaterialId);
  const matDrawer = cwMaterial(project, res.drawerBoxMaterialId);
  const mName = m => (m ? m.name : 'Material not set');
  const mThk = (m, dflt) => (m ? cwNum(m.thickness) : cwNum(dflt));

  // Grain. A visible panel on a grained material runs vertically unless the
  // part is plainly horizontal — and a grouped match keeps the group together
  // through nesting rather than letting the optimiser take the parts apart.
  const frontGrainDir = (opts && opts.grainDir) || (matFront && matFront.grained ? 'Vertical' : 'None');
  const frontMatch = (opts && opts.matchMode) || (matFront && matFront.grained ? 'Continuous' : 'Random');
  const frontGroup = cwMatchIsGrouped(frontMatch) ? `${cab.id}:fronts` : null;

  math.push({ label: 'Carcass depth', expr: ffT
    ? `overall ${cwN(D)} − ${cwN(ffT)} face frame = ${cwN(carcassD)} box depth`
    : `box depth ${cwN(carcassD)}; finished depth with the front on is ${cwN(carcassD + cwNum(con.doorThickness))}` });
  math.push({ label: 'Interior width', expr: `${cwN(W)} − 2 × ${cwN(t)} side = ${cwN(interiorW)}` });
  math.push({ label: 'Deck depth', expr: `${cwN(carcassD)} − ${cwN(con.backInset)} back inset − ${cwN(con.backThickness)} back = ${cwN(deckDepth)}` });
  math.push({ label: 'Interior height', expr: `${cwN(H)}${toeH ? ` − ${cwN(toeH)} toe kick` : ''} − ${cwN(t)} bottom${fullTop ? ` − ${cwN(t)} top` : ' (stretchers, no full top)'} = ${cwN(interiorH)}` });

  if (interiorW <= 0) issues.push({ level: 'error', msg: `Interior width is ${cwN(interiorW)} — the cabinet is narrower than two sides.` });
  if (interiorH <= 0) issues.push({ level: 'error', msg: `Interior height is ${cwN(interiorH)} — the height does not clear the toe kick and the deck.` });
  if (deckDepth <= 0) issues.push({ level: 'error', msg: `Deck depth is ${cwN(deckDepth)} — the back inset is deeper than the box.` });

  // ---- carcass
  panels.push(cwPanel({
    key: 'side.L', name: 'Side — left', group: 'Carcass', materialId: res.boxMaterialId,
    materialName: mName(matBox), thickness: mThk(matBox, t), length: H, width: carcassD, qty: 1,
    grain: matBox && matBox.grained ? 'Length' : 'None',
    edges: { front: band }, visible: !!cab.finishedEndLeft,
    math: `length = cabinet height ${cwN(H)}; width = box depth ${cwN(carcassD)}`,
    note: res.toeKick ? `Notched ${cwN(toeH)} high × ${cwN(con.toeKickSetback)} deep for the toe kick.` : '',
  }));
  panels.push(cwPanel({
    key: 'side.R', name: 'Side — right', group: 'Carcass', materialId: res.boxMaterialId,
    materialName: mName(matBox), thickness: mThk(matBox, t), length: H, width: carcassD, qty: 1,
    grain: matBox && matBox.grained ? 'Length' : 'None',
    edges: { front: band }, visible: !!cab.finishedEndRight,
    math: `length = cabinet height ${cwN(H)}; width = box depth ${cwN(carcassD)}`,
    note: res.toeKick ? `Notched ${cwN(toeH)} high × ${cwN(con.toeKickSetback)} deep for the toe kick.` : '',
  }));
  panels.push(cwPanel({
    key: 'bottom', name: 'Bottom deck', group: 'Carcass', materialId: res.boxMaterialId,
    materialName: mName(matBox), thickness: mThk(matBox, t), length: interiorW, width: deckDepth, qty: 1,
    edges: { front: band },
    math: `length = ${cwN(W)} − 2 × ${cwN(t)} = ${cwN(interiorW)}; width = deck depth ${cwN(deckDepth)}`,
  }));
  if (fullTop) {
    panels.push(cwPanel({
      key: 'top', name: 'Top deck', group: 'Carcass', materialId: res.boxMaterialId,
      materialName: mName(matBox), thickness: mThk(matBox, t), length: interiorW, width: deckDepth, qty: 1,
      edges: { front: band },
      math: `length ${cwN(interiorW)}; width = deck depth ${cwN(deckDepth)}`,
    }));
  } else {
    panels.push(cwPanel({
      key: 'stretcher', name: 'Top stretcher', group: 'Carcass', materialId: res.boxMaterialId,
      materialName: mName(matBox), thickness: mThk(matBox, t), length: interiorW, width: cwNum(con.stretcherWidth), qty: 2,
      math: `2 off — length ${cwN(interiorW)}; width = ${cwN(con.stretcherWidth)} stretcher, front and back`,
      note: 'A base cabinet takes stretchers rather than a full top so the countertop can be fixed through them.',
    }));
  }
  const backCap = con.backCaptured ? 2 * cwNum(con.backGrooveDepth) : 0;
  panels.push(cwPanel({
    key: 'back', name: 'Back', group: 'Carcass', materialId: res.backMaterialId,
    materialName: mName(matBack), thickness: mThk(matBack, con.backThickness),
    length: interiorH + backCap, width: interiorW + backCap, qty: 1,
    math: con.backCaptured
      ? `captured in a groove — ${cwN(interiorH)} + 2 × ${cwN(con.backGrooveDepth)} = ${cwN(interiorH + backCap)} high; ${cwN(interiorW)} + 2 × ${cwN(con.backGrooveDepth)} = ${cwN(interiorW + backCap)} wide`
      : `applied to the back — ${cwN(interiorH)} × ${cwN(interiorW)}`,
  }));
  if (res.shelfCount > 0) {
    const shelfL = interiorW - 2 * cwNum(con.shelfSideClearance);
    const shelfW = deckDepth - cwNum(con.shelfDepthSetback);
    const openShelf = (res.rows || []).some(r => r.kind === 'opening');
    panels.push(cwPanel({
      key: 'shelf', name: 'Adjustable shelf', group: 'Carcass', materialId: res.boxMaterialId,
      materialName: mName(matBox), thickness: mThk(matBox, t), length: shelfL, width: shelfW,
      qty: res.shelfCount, edges: openShelf ? { front: band, back: band, left: band, right: band } : { front: band },
      math: `length = ${cwN(interiorW)} − 2 × ${cwN(con.shelfSideClearance)} clearance = ${cwN(shelfL)}; width = ${cwN(deckDepth)} − ${cwN(con.shelfDepthSetback)} setback = ${cwN(shelfW)}`,
      note: openShelf ? 'Open cabinet — all four edges are seen, so all four are banded.' : '',
      ops: [{ kind: 'System 32 shelf holes', note: 'Shelf pin holes are bored in the SIDES, not the shelf. Listed here so the shelf and its support read together.' }],
    }));
  }
  // A VERTICAL PARTITION splits the box into bays. It is full interior height
  // and full deck depth — a fixed structural member, not a shelf — and each one
  // eats its own thickness out of the interior, which is why the bay width is
  // stated rather than left to be worked out.
  if (res.vPartitionCount > 0) {
    const n = res.vPartitionCount;
    const bayW = (interiorW - n * t) / (n + 1);
    panels.push(cwPanel({
      key: 'partition.v', name: 'Vertical partition', group: 'Carcass', materialId: res.boxMaterialId,
      materialName: mName(matBox), thickness: mThk(matBox, t), length: interiorH, width: deckDepth, qty: n,
      edges: { front: band },
      math: `${cwN(interiorH)} high × ${cwN(deckDepth)} deep, full depth and fixed; `
          + `${n} partition${n === 1 ? ' leaves' : 's leave'} ${n + 1} bays of `
          + `(${cwN(interiorW)} − ${n} × ${cwN(t)}) ÷ ${n + 1} = ${cwN(bayW)}`,
    }));
    if (bayW <= 0) issues.push({ level: 'error', msg: `A bay works out at ${cwN(bayW)} — there are more partitions than the width carries.` });
  }
  // A HORIZONTAL PARTITION is a FIXED shelf: dadoed in, full depth, and it is
  // what makes a tall cabinet rigid. Distinct from the adjustable shelf above,
  // which sits on pins and is set back.
  if (res.hPartitionCount > 0) {
    panels.push(cwPanel({
      key: 'partition.h', name: 'Horizontal partition', group: 'Carcass', materialId: res.boxMaterialId,
      materialName: mName(matBox), thickness: mThk(matBox, t), length: interiorW, width: deckDepth,
      qty: res.hPartitionCount, edges: { front: band },
      math: `${cwN(interiorW)} × ${cwN(deckDepth)} — full interior width and full depth, fixed (not set back like an adjustable shelf)`,
    }));
  }
  if (res.toeKick) {
    panels.push(cwPanel({
      key: 'toekick', name: 'Toe kick board', group: 'Trim', materialId: res.frontMaterialId,
      materialName: mName(matFront), thickness: mThk(matFront, con.doorThickness),
      length: interiorW, width: toeH, qty: 1, visible: true,
      grain: frontGrainDir === 'None' ? 'None' : 'Length',
      edges: { front: band },
      math: `length = ${cwN(interiorW)} between the sides; height = ${cwN(toeH)}, set back ${cwN(con.toeKickSetback)}`,
    }));
  }
  if (res.category === 'Wall' || res.category === 'Tall') {
    panels.push(cwPanel({
      key: 'nailer', name: 'Nailer', group: 'Carcass', materialId: res.boxMaterialId,
      materialName: mName(matBox), thickness: mThk(matBox, t),
      length: interiorW, width: cwNum(con.nailerWidth), qty: 2,
      math: `2 off — length ${cwN(interiorW)}; width ${cwN(con.nailerWidth)}. A hung cabinet is carried by these, not by the back.`,
    }));
  }

  // ---- face frame
  if (con.kind === 'Face Frame') {
    const frontH = layout.frontH;
    panels.push(cwPanel({
      key: 'ff.stile', name: 'Face frame stile', group: 'Face frame', materialId: res.frontMaterialId,
      materialName: mName(matFront), thickness: cwNum(con.ffThickness),
      length: frontH, width: cwNum(con.ffStile), qty: 2, visible: true, grain: 'Length',
      math: `2 off — length = face height ${cwN(frontH)}; width ${cwN(con.ffStile)}`,
    }));
    const railL = layout.faceW - 2 * cwNum(con.ffStile);
    panels.push(cwPanel({
      key: 'ff.rail', name: 'Face frame rail', group: 'Face frame', materialId: res.frontMaterialId,
      materialName: mName(matFront), thickness: cwNum(con.ffThickness),
      length: railL, width: cwNum(con.ffRail), qty: 2 + Math.max(0, layout.rowCount - 1), visible: true, grain: 'Length',
      math: `length = ${cwN(layout.faceW)} − 2 × ${cwN(con.ffStile)} = ${cwN(railL)}; ${2 + Math.max(0, layout.rowCount - 1)} off (top, bottom${layout.rowCount > 1 ? ` and ${layout.rowCount - 1} mid` : ''})`,
    }));
  }

  // ---- fronts, straight off the layout
  const style = cwFrontStyle(res.frontStyle);
  const hinge = hwSet ? hwItems.find(h => h.kind === 'Hinge' && (hwSet.lines || []).some(l => l.itemId === h.id)) : null;
  const handle = hwSet ? hwItems.find(h => h.kind === 'Handle' && (hwSet.lines || []).some(l => l.itemId === h.id)) : null;
  const slide = hwSet ? hwItems.find(h => h.kind === 'Slide' && (hwSet.lines || []).some(l => l.itemId === h.id)) : null;

  // Machining comes from the hardware, or it does not come at all. A cup bore
  // invented from a plausible-looking default is a scrapped door, so an item
  // without drilling data produces an operation that SAYS it has none.
  function hingeOps(doorH) {
    if (!hinge) return [{ kind: 'Hinge cup bore', unknown: true, note: 'No hinge in the hardware set — bore positions cannot be derived.' }];
    if (!cwDrillingKnown(hinge)) {
      return [{ kind: 'Hinge cup bore', unknown: true,
        note: `${hinge.name}: manufacturer drilling data not loaded (cup diameter and setback are blank).` }];
    }
    // Hinge count by door height is a shop standard, not a manufacturer one, so
    // it lives here and is visible rather than hidden in a lookup table.
    const n = doorH <= cwIn(40) ? 2 : doorH <= cwIn(60) ? 3 : doorH <= cwIn(80) ? 4 : 5;
    const edge = cwIn(3.5);                       // centre of the end hinges from each end
    const ops = [];
    for (let i = 0; i < n; i++) {
      const y = n === 1 ? doorH / 2 : edge + (doorH - 2 * edge) * (i / (n - 1));
      ops.push({ kind: 'Hinge cup bore', x: cwRound(cwNum(hinge.cupSetback)), y: cwRound(y),
        dia: cwNum(hinge.cupDia), depth: 12.5,
        note: `${hinge.name} — cup centre ${cwN(hinge.cupSetback)} from the hinge edge, ${cwN(y)} from the bottom.` });
    }
    return ops;
  }
  function handleOps(w, h, vertical) {
    if (!handle) return [];
    if (!cwDrillingKnown(handle)) {
      return [{ kind: 'Handle bore', unknown: true,
        note: `${handle.name}: manufacturer drilling data not loaded (hole centres are blank).` }];
    }
    const c = cwNum(handle.boreCentres);
    const cx = w / 2, cy = vertical ? h - cwIn(3) : h / 2;
    return vertical
      ? [{ kind: 'Handle bore', x: cwRound(cx), y: cwRound(cy), dia: 5, note: `${handle.name} — 2 holes at ${cwN(c)} centres, horizontal.` }]
      : [{ kind: 'Handle bore', x: cwRound(cx), y: cwRound(cy), dia: 5, note: `${handle.name} — 2 holes at ${cwN(c)} centres.` }];
  }

  let doorCount = 0, drawerCount = 0;
  layout.cells.forEach(cell => {
    if (cell.kind === 'opening') return;
    const isDoor = cell.kind === 'door';
    if (isDoor) doorCount++; else drawerCount++;
    const p = cwPanel({
      key: cell.key,
      name: isDoor ? `Door ${cell.key.split('.')[1]}` : cell.kind === 'drawer' ? `Drawer front ${cell.key.split('.')[1]}` : `False front ${cell.key.split('.')[1]}`,
      group: 'Front', materialId: res.frontMaterialId, materialName: mName(matFront),
      thickness: cwNum(con.doorThickness), length: cell.h, width: cell.w, qty: 1,
      grain: frontGrainDir === 'Horizontal' ? 'Width' : frontGrainDir === 'None' ? 'None' : 'Length',
      matchMode: frontMatch, grainGroup: frontGroup, visible: true,
      edges: { front: band, back: band, left: band, right: band },
      math: `${cell.math} · ${style.label}`,
      ops: isDoor ? hingeOps(cell.h).concat(handleOps(cell.w, cell.h, true))
                  : cell.kind === 'drawer' ? handleOps(cell.w, cell.h, false) : [],
      note: cell.kind === 'falseFront' ? 'Tip-out panel — no drawer box behind it.' : '',
    });
    // A front stores the grain as it reads on the CABINET, which is what a
    // veneer match is specified in; the panel's own `grain` is relative to the
    // part and is what nesting rotates against.
    p.grainDir = frontGrainDir;
    panels.push(p);
  });

  // ---- drawer boxes
  const boxW = interiorW - 2 * cwNum(slide && slide.slideSideClearance != null ? slide.slideSideClearance : con.drawerSideClearance);
  const boxDepth = deckDepth - cwNum(slide && slide.slideSetback != null ? slide.slideSetback : con.slideTakeUp);
  const bt = mThk(matDrawer, con.drawerBoxThickness);
  const boxSource = slide && cwDrillingKnown(slide) ? `${slide.name} runner` : 'construction standard';
  if (drawerCount) {
    math.push({ label: 'Drawer box width',
      expr: `interior ${cwN(interiorW)} − 2 × ${cwN(slide && slide.slideSideClearance != null ? slide.slideSideClearance : con.drawerSideClearance)} per-side clearance (${boxSource}) = ${cwN(boxW)}` });
    math.push({ label: 'Drawer box depth',
      expr: `deck ${cwN(deckDepth)} − ${cwN(slide && slide.slideSetback != null ? slide.slideSetback : con.slideTakeUp)} slide take-up = ${cwN(boxDepth)}` });
    if (slide && cwNum(slide.slideLength) > 0 && cwNum(slide.slideLength) > boxDepth) {
      issues.push({ level: 'warn', msg: `${slide.name} is ${cwN(slide.slideLength)} long but the box depth works out at ${cwN(boxDepth)} — the runner will not fit this carcass.` });
    }
  }
  layout.cells.filter(c => c.kind === 'drawer').forEach((cell, i) => {
    const boxH = cell.h - cwNum(con.drawerBoxHeightDeduction);
    if (boxH <= 40) {
      issues.push({ level: 'warn', msg: `Drawer ${i + 1} front is only ${cwN(cell.h)} — after the ${cwN(con.drawerBoxHeightDeduction)} deduction the box is ${cwN(boxH)} and is not buildable.` });
      return;
    }
    const n = `drawer.${i + 1}`;
    panels.push(cwPanel({
      key: `${n}.side`, name: `Drawer ${i + 1} — box side`, group: 'Drawer box', materialId: res.drawerBoxMaterialId,
      materialName: mName(matDrawer), thickness: bt, length: boxDepth, width: boxH, qty: 2,
      edges: { front: band },
      math: `2 off — length = box depth ${cwN(boxDepth)}; width = front ${cwN(cell.h)} − ${cwN(con.drawerBoxHeightDeduction)} = ${cwN(boxH)}`,
      ops: [{ kind: 'Drawer bottom groove', note: `${cwN(con.drawerBottomGroove)} deep, ${cwN(bt)} up from the bottom edge.` }],
    }));
    panels.push(cwPanel({
      key: `${n}.fb`, name: `Drawer ${i + 1} — box front & back`, group: 'Drawer box', materialId: res.drawerBoxMaterialId,
      materialName: mName(matDrawer), thickness: bt, length: boxW - 2 * bt, width: boxH, qty: 2,
      edges: { front: band },
      math: `2 off — length = box ${cwN(boxW)} − 2 × ${cwN(bt)} side = ${cwN(boxW - 2 * bt)}; width ${cwN(boxH)}`,
    }));
    const g = cwNum(con.drawerBottomGroove);
    panels.push(cwPanel({
      key: `${n}.bottom`, name: `Drawer ${i + 1} — bottom`, group: 'Drawer box', materialId: 'cwmat-boxbtm',
      materialName: mName(cwMaterial(project, 'cwmat-boxbtm')), thickness: mThk(cwMaterial(project, 'cwmat-boxbtm'), con.drawerBottomThickness),
      length: boxDepth - 2 * bt + 2 * g, width: boxW - 2 * bt + 2 * g, qty: 1,
      math: `captured all round — ${cwN(boxDepth)} − 2 × ${cwN(bt)} + 2 × ${cwN(g)} groove = ${cwN(boxDepth - 2 * bt + 2 * g)} × ${cwN(boxW - 2 * bt + 2 * g)}`,
    }));
  });

  // ---- fillers, scribes and finished ends, as real parts
  ['Left', 'Right'].forEach(side => {
    const f = cwNum(side === 'Left' ? cab.fillerLeft : cab.fillerRight);
    const sc = cwNum(side === 'Left' ? cab.scribeLeft : cab.scribeRight);
    if (f > 0) {
      panels.push(cwPanel({
        key: `filler.${side.toLowerCase()}`, name: `Filler — ${side.toLowerCase()}`, group: 'Trim',
        materialId: res.frontMaterialId, materialName: mName(matFront), thickness: cwNum(con.doorThickness),
        length: layout.frontH, width: f + sc, qty: 1, visible: true,
        grain: frontGrainDir === 'None' ? 'None' : 'Length', matchMode: frontMatch, grainGroup: frontGroup,
        edges: { front: band, back: band, left: band, right: band },
        math: `height = face ${cwN(layout.frontH)}; width = ${cwN(f)} filler${sc ? ` + ${cwN(sc)} scribe allowance` : ''} = ${cwN(f + sc)}`,
        note: sc ? 'Cut oversize by the scribe allowance and trimmed to the wall on site.' : '',
      }));
    }
    const finished = side === 'Left' ? cab.finishedEndLeft : cab.finishedEndRight;
    if (finished) {
      panels.push(cwPanel({
        key: `endpanel.${side.toLowerCase()}`, name: `Finished end panel — ${side.toLowerCase()}`, group: 'Trim',
        materialId: res.frontMaterialId, materialName: mName(matFront), thickness: cwNum(con.doorThickness),
        length: H - toeH, width: carcassD + cwNum(con.doorThickness), qty: 1, visible: true,
        grain: frontGrainDir === 'None' ? 'None' : 'Length', matchMode: frontMatch, grainGroup: frontGroup,
        edges: { front: band, back: band, left: band, right: band },
        math: `height = ${cwN(H)} − ${cwN(toeH)} toe = ${cwN(H - toeH)}; depth = box ${cwN(carcassD)} + ${cwN(con.doorThickness)} front = ${cwN(carcassD + cwNum(con.doorThickness))}, so it finishes flush with the door`,
      }));
    }
  });

  // ---- appliance cutouts, as machining on the part they land on
  (cab.applianceCutouts || []).forEach((c, i) => {
    const target = panels.find(p => p.key === 'top') || panels.find(p => p.key === 'stretcher');
    const op = { kind: c.kind || 'Sink cutout', note: `${c.label || 'Cutout'} — ${cwN(c.w)} × ${cwN(c.h)}${c.note ? `. ${c.note}` : ''}` };
    if (target) target.ops = target.ops.concat([op]);
    else issues.push({ level: 'info', msg: `Cutout "${c.label || i + 1}" has no panel to land on in this cabinet.` });
    // The check that actually matters on site: a bowl wider than the inside of
    // the cabinet does not go in, whatever the countertop drawing says.
    if (cwNum(c.w) > 0 && cwNum(c.w) > interiorW) {
      issues.push({ level: 'error',
        msg: `${c.label || 'Cutout'} is ${cwN(c.w)} wide but the cabinet interior is ${cwN(interiorW)}. It does not fit — widen the cabinet or change the bowl.` });
    } else if (cwNum(c.w) > 0 && interiorW - cwNum(c.w) < cwIn(2)) {
      issues.push({ level: 'warn',
        msg: `${c.label || 'Cutout'} leaves only ${cwN(interiorW - cwNum(c.w))} inside the cabinet. Most bowls need clearance for the rim and the clips.` });
    }
  });

  // ---- shop overrides on a computed part
  // A forced size is legitimate — a scribed door gets cut 2 mm narrow — but it
  // must never look like the arithmetic. Each one is flagged, and the number
  // the engine produced is kept beside it.
  Object.keys(cab.overrides || {}).forEach(k => {
    const p = panels.find(x => x.key === k);
    if (!p) return;
    const o = cab.overrides[k] || {};
    if (o.length != null) p.length = cwRound(cwNum(o.length));
    if (o.width != null) p.width = cwRound(cwNum(o.width));
    p.overridden = true;
    p.note = `${p.note ? p.note + ' ' : ''}Cut size forced by the shop (engine said ${cwN(p.baseLength)} × ${cwN(p.baseWidth)}).`;
  });

  // ---- checks worth raising
  const inches = W / MM_PER_INCH;
  if (Math.abs(inches - Math.round(inches / 3) * 3) > 0.05 && !cwOwn(res, 'width')) {
    issues.push({ level: 'info', msg: `${cwRound(inches)}" is off the 3" module. Fine for a made-to-measure box; check it is deliberate.` });
  }
  if (W < cwIn(9) || W > cwIn(48)) {
    issues.push({ level: 'warn', msg: `${cwRound(inches)}" is outside the 9"–48" standard range. Buildable, but it will not use a library type's proportions.` });
  }
  layout.cells.filter(c => c.kind === 'door').forEach(c => {
    if (c.w > cwIn(24)) issues.push({ level: 'warn', msg: `A single door ${cwRound(c.w / MM_PER_INCH)}" wide will sag on its hinges. Split the opening.` });
  });
  if (res.special === 'lazySusan' || res.special === 'cornerWall') {
    issues.push({ level: 'info', msg: 'Corner box — the carcass is cut on the diagonal. The panel sizes below are the square box; the diagonal return is shop-detailed and is not calculated.' });
  }
  if (res.special === 'blindCorner' && !cwNum(res.blindWidth)) {
    issues.push({ level: 'warn', msg: 'Blind corner with no blind width set — the front field is being sized across the whole cabinet, which is wrong.' });
  }
  if (!matBox || !matFront) issues.push({ level: 'error', msg: 'A material on this cabinet is not in the project material list. Its parts cannot be nested or costed.' });

  const bandTotal = panels.reduce((a, p) => a + cwPanelBandLength(p), 0);
  const area = panels.reduce((a, p) => a + cwPanelArea(p), 0);
  return {
    res, con, layout, panels, issues, math, hardwareSet: hwSet,
    doorCount, drawerCount, bandTotal, area,
    interiorW, interiorH, deckDepth, carcassD, toeH,
    trace: Object.assign({ cabinetId: cab.id, mark: cab.mark }, trace),
  };
}

// ============================================================================
// RUNS — how casework is actually laid out
// ----------------------------------------------------------------------------
// Pick a wall of a known length, place modules along it in order, and watch the
// arithmetic. "Wall 3658 · used 3505 · 153 left" — or "OVER BY 102" — is the
// single most useful number in this module, because it is the one that decides
// whether the drawing is buildable before anything is cut.
// ============================================================================

// The wall length to design against: the field measurement if someone has taken
// one, otherwise the type's nominal. A measured wall always wins — that is the
// whole reason a room instance is separate from its type.
function cwWallLength(room, wall) {
  const fm = (room && room.fieldMeasure) || {};
  const measured = cwNum(fm[wall.id]);
  return measured > 0 ? measured : cwNum(wall.length);
}
// What a member takes up in the run. For an appliance these are two different
// numbers and must stay two: `width` is the space the run ALLOTS it, openingW is
// what the appliance REQUIRES. Deriving the first from the second would make
// the fit check tautological — it could never fail, which is worse than not
// having it.
function cwMemberWidth(project, m) {
  if (m.kind === 'cabinet') {
    if (m.width != null && m.width !== '') return cwNum(m.width);
    const t = cwType(project, m.cabTypeId);
    return t ? cwNum(t.width) : 0;
  }
  if (m.kind === 'appliance' && (m.width == null || m.width === '')) {
    return cwNum(m.openingW) + 2 * cwNum(m.clearanceEach);
  }
  return cwNum(m.width);
}
function cwMemberHeight(project, run, m) {
  if (m.height != null && m.height !== '') return cwNum(m.height);
  if (m.kind === 'cabinet') {
    const t = cwType(project, m.cabTypeId);
    if (t) return cwNum(t.height);
  }
  if (m.kind === 'appliance') return cwNum(m.openingH);
  return cwIn(34.5);
}

// The running arithmetic, plus every clash worth flagging. One pass, so what is
// drawn and what is warned about can never disagree.
function cwRunLayout(project, ctx, room, wall, run) {
  const wallLen = cwWallLength(room, wall);
  const measured = cwNum(((room && room.fieldMeasure) || {})[wall.id]) > 0;
  const members = [];
  const issues = [];
  let x = cwNum(run.startOffset);
  const z0 = cwNum(run.zBottom);
  (run.members || []).forEach((m, i) => {
    const w = cwMemberWidth(project, m);
    const h = cwMemberHeight(project, run, m);
    const t = m.kind === 'cabinet' ? cwType(project, m.cabTypeId) : null;
    const kindDef = CW_MEMBER_KINDS.find(k => k.key === m.kind);
    members.push({ member: m, index: i, x, w, h, z0, z1: z0 + h, type: t,
      label: m.label || (t ? `${t.code} ${cwN(w)}` : (kindDef ? kindDef.label : m.kind)) });
    if (m.kind === 'cabinet' && !t) issues.push({ level: 'error', msg: `Member ${i + 1} points at a cabinet type that is not in this project.` });
    if (w <= 0) issues.push({ level: 'warn', msg: `Member ${i + 1} (${m.label || m.kind}) has no width, so it takes up nothing in the run arithmetic.` });
    x += w;
  });
  const used = x - cwNum(run.startOffset);
  const remaining = wallLen - cwNum(run.startOffset) - used;

  // Obstructions. A cabinet only clashes if it overlaps the obstruction in BOTH
  // axes — a base run passes under a window and a wall run does not.
  (wall.obstructions || []).forEach(o => {
    const oa = cwNum(o.fromLeft), ob = oa + cwNum(o.width);
    const za = o.kind === 'Door' ? 0 : cwNum(o.sillHeight);
    const zb = za + cwNum(o.height);
    members.forEach(mm => {
      const overlapX = mm.x < ob && mm.x + mm.w > oa;
      const overlapZ = mm.z0 < zb && mm.z1 > za;
      if (overlapX && overlapZ) {
        issues.push({ level: 'error',
          msg: `${mm.label} runs from ${cwN(mm.x)} to ${cwN(mm.x + mm.w)} and collides with the ${o.kind.toLowerCase()}${o.label ? ` "${o.label}"` : ''} at ${cwN(oa)}–${cwN(ob)}.` });
      } else if (overlapX && (o.kind === 'Plumbing' || o.kind === 'Electrical')) {
        issues.push({ level: 'info',
          msg: `${o.kind}${o.label ? ` "${o.label}"` : ''} falls behind ${mm.label}. Not a clash, but the back needs cutting for it.` });
      }
    });
  });

  // Appliances and fixtures, checked against the spec they reference rather
  // than against a number someone typed twice.
  members.filter(mm => mm.member.kind === 'appliance').forEach(mm => {
    const m = mm.member;
    const lib = (m.specKind === 'fixture' ? (ctx.fixtureLibrary || []) : (ctx.applianceLibrary || []));
    const spec = lib.find(s => s.id === m.specId) || null;
    const need = cwNum(m.openingW);
    if (!spec && m.specId) issues.push({ level: 'warn', msg: `${mm.label}: its specification is no longer in the library.` });
    if (!m.specId) issues.push({ level: 'info', msg: `${mm.label} has no appliance or fixture specification linked, so its opening cannot be checked against anything.` });
    if (need <= 0) issues.push({ level: 'warn', msg: `${mm.label} has no required opening width — nothing to verify.` });
    const needTotal = need + 2 * cwNum(m.clearanceEach);
    if (need > 0 && mm.w < needTotal) {
      issues.push({ level: 'error',
        msg: `${mm.label} needs ${cwN(need)} plus ${cwN(m.clearanceEach)} clearance each side — ${cwN(needTotal)} in all — and the run allots ${cwN(mm.w)}.` });
    }
    if (spec && spec.dimensions) {
      issues.push({ level: 'info', msg: `${mm.label}: the spec sheet reads "${spec.dimensions}" — confirm the opening against it, the number here is what the run was drawn to.` });
    }
    // APP-001 / APP-002 / APP-003 — the data-status ladder, quoted so the
    // warning says WHY rather than only that it fired.
    const ds = m.dataStatus || 'Generic';
    if (ds === 'Generic') {
      issues.push({ level: 'warn',
        msg: `${mm.label} is a GENERIC appliance — estimating and layout only, never production-approved (APP-001). Select the model before this run is released.` });
    } else if (ds === 'Model Required') {
      issues.push({ level: 'error',
        msg: `${mm.label} is one the nominal size cannot be inferred from (APP-002). Load the exact manufacturer dimensions and check cutouts and utilities.` });
    } else if (ds === 'Typical Range') {
      issues.push({ level: 'warn',
        msg: `${mm.label} is drawn to a TYPICAL RANGE across manufacturers. Verify against the chosen model before fabrication.` });
    } else if (ds === 'Model Selected') {
      issues.push({ level: 'info',
        msg: `${mm.label}: model dimensions loaded. The installation document still has to be reviewed before fabrication (APP-003).` });
    }
  });

  if (remaining < 0) issues.push({ level: 'error', msg: `The run is over the wall by ${cwN(-remaining)}.` });
  else if (remaining > cwIn(0.25) && remaining < cwIn(3)) {
    issues.push({ level: 'warn', msg: `${cwN(remaining)} left over — too small for a cabinet and too big to ignore. Add a filler.` });
  }

  return { wallLen, measured, members, used, remaining, over: remaining < 0, issues, z0 };
}

// ============================================================================
// CASEWORK TYPES — designed once, built forty times
// ============================================================================

// Mirror the whole room, not just the drawing: run member order reverses, every
// obstruction measures from the other end, and handing flips. A mirrored unit
// that still hinges left is the classic multifamily field problem.
function cwMirrorCaseworkType(ct) {
  const m = cloneDeep(ct);
  (m.walls || []).forEach(w => {
    (w.obstructions || []).forEach(o => {
      o.fromLeft = cwNum(w.length) - cwNum(o.fromLeft) - cwNum(o.width);
    });
  });
  (m.runs || []).forEach(run => {
    const wall = (m.walls || []).find(w => w.id === run.wallId);
    const wallLen = wall ? cwNum(wall.length) : 0;
    const total = (run.members || []).reduce((a, x) => a + cwNum(x.width || 0), 0);
    run.members = (run.members || []).slice().reverse();
    run.members.forEach(x => { if (x.handing === 'L') x.handing = 'R'; else if (x.handing === 'R') x.handing = 'L'; });
    // The run keeps the same gap at the other end of the wall.
    if (wallLen > 0) run.startOffset = Math.max(0, wallLen - cwNum(run.startOffset) - total);
  });
  m.mirrored = true;
  return m;
}
// What a room instance is actually designed to. Mirroring is applied here, once,
// so nothing downstream has to remember to.
function cwEffectiveType(project, room) {
  const ct = cwCaseworkTypes(project).find(x => x.id === room.caseworkTypeId) || null;
  if (!ct) return null;
  return room.mirrored ? cwMirrorCaseworkType(ct) : ct;
}

// A stable identity for "the cabinet this member becomes", so re-issuing a type
// can find the instance it already created instead of making a second one.
function cwMemberKey(run, member) { return `${run.id}:${member.id}`; }

// What the cabinets in a room WOULD be if the type were applied right now.
// Nothing is written — this is the function the impact review reads, and the
// same function the apply step then writes from, so the preview cannot differ
// from the result.
function cwProjectedCabinets(project, room) {
  const ct = cwEffectiveType(project, room);
  if (!ct) return [];
  const out = [];
  (ct.runs || []).forEach(run => {
    (run.members || []).filter(m => m.kind === 'cabinet').forEach((m, i) => {
      const t = cwType(project, m.cabTypeId);
      // `*Set` says whether the RUN chose this dimension or the cabinet type did.
      // It matters: applying stamps a chosen dimension onto the cabinet, and
      // leaves an unchosen one null so it keeps inheriting — otherwise the first
      // apply would silently convert every cabinet into a set of overrides and a
      // later change to B2D would reach none of them.
      out.push({
        memberKey: cwMemberKey(run, m),
        runId: run.id, runName: run.name, wallId: run.wallId,
        typeId: m.cabTypeId,
        mark: `${t ? t.code : 'CAB'}-${String(i + 1).padStart(2, '0')}`,
        widthSet: m.width != null && m.width !== '',
        heightSet: m.height != null && m.height !== '',
        depthSet: m.depth != null && m.depth !== '',
        width: m.width != null && m.width !== '' ? cwNum(m.width) : (t ? cwNum(t.width) : 0),
        height: m.height != null && m.height !== '' ? cwNum(m.height) : (t ? cwNum(t.height) : 0),
        depth: m.depth != null && m.depth !== '' ? cwNum(m.depth) : (t ? cwNum(t.depth) : 0),
        handing: m.handing || 'Auto',
        label: m.label || '',
      });
    });
  });
  return out;
}

// The impact review. It answers the three questions someone asks before
// re-issuing a type — how many units, how many cabinets, and how many of those
// have already been cut — and it answers them BEFORE anything changes.
function cwTypeImpact(project, caseworkTypeId) {
  const rooms = cwRooms(project).filter(r => r.caseworkTypeId === caseworkTypeId);
  const items = cwItems(project);
  const units = [];
  let changed = 0, added = 0, removed = 0, producedTouched = 0, installedTouched = 0;
  rooms.forEach(room => {
    const projected = cwProjectedCabinets(project, room);
    const existing = items.filter(c => c.roomId === room.id);
    const byKey = {};
    existing.forEach(c => { byKey[c.memberKey || c.id] = c; });
    const rowChanges = [];
    projected.forEach(p => {
      const cur = byKey[p.memberKey];
      if (!cur) { added++; rowChanges.push({ kind: 'add', mark: p.mark, detail: 'new cabinet' }); return; }
      // Compare the RESOLVED size, not the stored field. A cabinet inheriting
      // its width from its type stores null, and comparing nulls would report
      // "no change" on exactly the case this review exists for.
      const curRes = cwResolve(project, cur);
      const diffs = [];
      if (cwRound(curRes.width) !== cwRound(p.width)) diffs.push(`width ${cwN(curRes.width)} → ${cwN(p.width)}`);
      if (cwRound(curRes.height) !== cwRound(p.height)) diffs.push(`height ${cwN(curRes.height)} → ${cwN(p.height)}`);
      if (cwRound(curRes.depth) !== cwRound(p.depth)) diffs.push(`depth ${cwN(curRes.depth)} → ${cwN(p.depth)}`);
      if (cur.typeId !== p.typeId) diffs.push('cabinet type');
      if (diffs.length) {
        changed++;
        rowChanges.push({ kind: 'change', mark: cur.mark, detail: diffs.join(', '), status: cur.status });
        if (CW_PRODUCED_STATUSES.indexOf(cur.status) >= 0) producedTouched++;
        if (cur.status === 'Installed') installedTouched++;
      }
    });
    const keys = projected.map(p => p.memberKey);
    existing.forEach(c => {
      if (c.memberKey && keys.indexOf(c.memberKey) < 0) {
        removed++;
        rowChanges.push({ kind: 'remove', mark: c.mark, detail: 'no longer in the type', status: c.status });
        if (CW_PRODUCED_STATUSES.indexOf(c.status) >= 0) producedTouched++;
      }
    });
    units.push({ room, changes: rowChanges,
      produced: existing.filter(c => CW_PRODUCED_STATUSES.indexOf(c.status) >= 0).length,
      installed: existing.filter(c => c.status === 'Installed').length,
      total: existing.length });
  });
  return { rooms: rooms.length, units, changed, added, removed, producedTouched, installedTouched,
           touched: units.filter(u => u.changes.length).length };
}

// ============================================================================
// BOM — every panel traceable both ways
// ----------------------------------------------------------------------------
// Down: Project -> Casework Type -> Unit -> Cabinet -> Panel -> Machining.
// Up:   a panel on the saw answers "what is this, and who is it for?".
// The part number carries the whole chain, which is why it is built here and
// not typed anywhere.
// ============================================================================
function cwCollectPanels(project, cabs) {
  const out = [];
  const rooms = cwRooms(project);
  const cts = cwCaseworkTypes(project);
  cabs.forEach(cab => {
    const room = rooms.find(r => r.id === cab.roomId) || null;
    const ct = room ? cts.find(x => x.id === room.caseworkTypeId) : null;
    const built = cwComputeParts(project, cab);
    built.panels.forEach(p => {
      const copy = Object.assign({}, p);
      copy.cabinetId = cab.id;
      copy.mark = cab.mark;
      copy.cabQty = Math.max(1, cwNum(cab.qty) || 1);
      copy.qty = copy.qty * copy.cabQty;
      copy.roomName = room ? room.name : '';
      copy.unitLabel = room ? (room.unit || room.name) : (cab.unit || '');
      copy.caseworkTypeCode = ct ? ct.code : '';
      copy.partNo = [ct ? ct.code : null, room ? (room.unit || room.name) : null, cab.mark, p.key]
        .filter(Boolean).join(' / ');
      out.push(copy);
    });
  });
  return out;
}
function cwBom(project, cabs) {
  const panels = cwCollectPanels(project, cabs);
  const byMaterial = {};
  const byBand = {};
  panels.forEach(p => {
    const k = p.materialId || p.materialName || 'unset';
    if (!byMaterial[k]) byMaterial[k] = { id: p.materialId, name: p.materialName, thickness: p.thickness, panels: [], qty: 0, area: 0, band: 0 };
    const g = byMaterial[k];
    g.panels.push(p);
    g.qty += p.qty;
    g.area += cwPanelArea(p);
    g.band += cwPanelBandLength(p);
    CW_EDGES.forEach(e => {
      const b = (p.edges || {})[e];
      if (!b) return;
      const len = (e === 'front' || e === 'back' ? p.length : p.width) * p.qty;
      byBand[b] = (byBand[b] || 0) + len;
    });
  });
  const groups = Object.keys(byMaterial).map(k => byMaterial[k])
    .sort((a, b) => b.area - a.area);
  return {
    panels, groups,
    bands: Object.keys(byBand).map(k => ({ name: k, mm: byBand[k] })).sort((a, b) => b.mm - a.mm),
    totalBand: panels.reduce((a, p) => a + cwPanelBandLength(p), 0),
    totalArea: panels.reduce((a, p) => a + cwPanelArea(p), 0),
    totalPanels: panels.reduce((a, p) => a + p.qty, 0),
  };
}

// ============================================================================
// NESTING
// ----------------------------------------------------------------------------
// A real optimiser: actual rectangles placed on actual sheets, honouring saw
// kerf, grain and rotation rules, with the layout drawn so a person can look at
// it. It is a HEURISTIC — first-fit-decreasing with shelf/guillotine packing —
// which means it produces a good proposal, not a proven optimum, and the UI
// says exactly that. Nothing here is a silent production decision: someone
// approves a nest before it is cut.
//
// The sheet's grain runs along its HEIGHT. A panel whose grain is 'Length'
// therefore has to stand its length up the sheet, and may not be turned.
// ============================================================================
function cwNestExpand(panels) {
  const items = [];
  panels.forEach(p => {
    for (let i = 0; i < p.qty; i++) {
      items.push({
        uid: `${p.cabinetId || 'x'}:${p.key}:${i}`,
        panel: p, copy: i + 1,
        L: p.length, W: p.width,
        grain: p.grain || 'None',
        group: p.grainGroup || null,
        area: p.length * p.width,
      });
    }
  });
  return items;
}
function cwNewSheet(w, h, remnantId) {
  return { w, h, shelves: [], cursorY: 0, remnantId: remnantId || null, placements: [] };
}
// Try to place one item on one sheet, mutating it. Returns true on success.
function cwPlace(sheet, item, kerf, allowRotate) {
  const opts = [];
  opts.push({ w: item.W, h: item.L, rotated: false });        // length up the sheet
  if (allowRotate) opts.push({ w: item.L, h: item.W, rotated: true });
  for (let s = 0; s < sheet.shelves.length; s++) {
    const sh = sheet.shelves[s];
    for (const o of opts) {
      if (o.h <= sh.h && sh.cursorX + o.w <= sheet.w) {
        sheet.placements.push({ item, x: sh.cursorX, y: sh.y, w: o.w, h: o.h, rotated: o.rotated });
        sh.cursorX += o.w + kerf;
        return true;
      }
    }
  }
  for (const o of opts) {
    if (sheet.cursorY + o.h <= sheet.h && o.w <= sheet.w) {
      const sh = { y: sheet.cursorY, h: o.h, cursorX: 0 };
      sheet.shelves.push(sh);
      sheet.placements.push({ item, x: 0, y: sh.y, w: o.w, h: o.h, rotated: o.rotated });
      sh.cursorX = o.w + kerf;
      sheet.cursorY += o.h + kerf;
      return true;
    }
  }
  return false;
}
function cwSheetClone(s) {
  return { w: s.w, h: s.h, remnantId: s.remnantId, cursorY: s.cursorY,
           shelves: s.shelves.map(x => ({ y: x.y, h: x.h, cursorX: x.cursorX })),
           placements: s.placements.slice() };
}
function cwSheetAdopt(target, src) {
  target.cursorY = src.cursorY; target.shelves = src.shelves; target.placements = src.placements;
}

function cwNest(panels, opts) {
  const o = Object.assign({ kerf: 3.2, mode: 'minSheets', sheetW: cwIn(48), sheetH: cwIn(96),
    trim: 0, minOffcutW: cwIn(6), minOffcutH: cwIn(6), remnants: [] }, opts || {});
  const usableW = cwNum(o.sheetW) - 2 * cwNum(o.trim);
  const usableH = cwNum(o.sheetH) - 2 * cwNum(o.trim);
  const kerf = cwNum(o.kerf);
  const grainMode = o.mode === 'grain';
  const items = cwNestExpand(panels);
  const notes = [];

  // Usable offcuts from earlier runs are consumed before a new sheet is opened.
  // That is the only way a remnant pile ever gets smaller.
  const sheets = (o.remnants || []).map(r => cwNewSheet(cwNum(r.w), cwNum(r.h), r.id));
  const remnantCount = sheets.length;

  // Grouped matches travel together. Splitting a bookmatched pair across two
  // sheets from two different bundles is exactly the failure a match mode
  // exists to prevent, so a group is placed as one batch or reported as split.
  const batches = [];
  const groups = {};
  items.forEach(it => {
    const grouped = it.group && (grainMode || cwMatchIsGrouped(it.panel.matchMode));
    if (grouped) {
      if (!groups[it.group]) { groups[it.group] = { group: it.group, items: [], area: 0 }; batches.push(groups[it.group]); }
      groups[it.group].items.push(it);
      groups[it.group].area += it.area;
    } else {
      batches.push({ group: null, items: [it], area: it.area });
    }
  });
  // First-fit DECREASING: biggest first. Shelf packing behaves far better when
  // the tall parts go down before the short ones.
  batches.forEach(b => b.items.sort((x, y) => Math.max(y.L, y.W) - Math.max(x.L, x.W) || y.area - x.area));
  batches.sort((a, b) => b.area - a.area);

  const unplaced = [];
  const split = [];
  batches.forEach(batch => {
    // A grained part NEVER turns, in either mode — that is not an optimisation
    // choice. Maintain-grain goes further and turns nothing at all, because a
    // shop cutting for veneer runs the whole sheet one way; that is the real
    // difference between the two modes and it is what makes the sheet counts
    // differ.
    const allowRotate = it => !grainMode && it.grain === 'None';
    if (batch.group && batch.items.length > 1) {
      // Try to land the whole group on one sheet.
      let landed = false;
      for (let s = 0; s < sheets.length && !landed; s++) {
        const trial = cwSheetClone(sheets[s]);
        let ok = true;
        for (const it of batch.items) { if (!cwPlace(trial, it, kerf, allowRotate(it))) { ok = false; break; } }
        if (ok) { cwSheetAdopt(sheets[s], trial); landed = true; }
      }
      if (!landed) {
        const fresh = cwNewSheet(usableW, usableH);
        let ok = true;
        for (const it of batch.items) { if (!cwPlace(fresh, it, kerf, allowRotate(it))) { ok = false; break; } }
        if (ok) { sheets.push(fresh); landed = true; }
      }
      if (landed) return;
      split.push(batch.group);
      notes.push(`${batch.group.split(':').pop()} — the matched group does not fit one sheet and has been split. The match will not read across the split.`);
    }
    batch.items.forEach(it => {
      let placed = false;
      for (let s = 0; s < sheets.length && !placed; s++) placed = cwPlace(sheets[s], it, kerf, allowRotate(it));
      if (!placed) {
        const fresh = cwNewSheet(usableW, usableH);
        placed = cwPlace(fresh, it, kerf, allowRotate(it));
        if (placed) sheets.push(fresh);
      }
      if (!placed) unplaced.push(it);
    });
  });

  // Offcuts: the strip left at the end of each shelf, and the strip left under
  // the last shelf. Anything above the minimum becomes a remnant the next run
  // can use.
  const offcuts = [];
  const live = sheets.filter(s => s.placements.length);
  live.forEach((s, i) => {
    s.shelves.forEach(sh => {
      const w = s.w - sh.cursorX;
      if (w >= cwNum(o.minOffcutW) && sh.h >= cwNum(o.minOffcutH)) {
        offcuts.push({ sheet: i, x: sh.cursorX, y: sh.y, w, h: sh.h });
      }
    });
    const hLeft = s.h - s.cursorY;
    if (hLeft >= cwNum(o.minOffcutH) && s.w >= cwNum(o.minOffcutW)) {
      offcuts.push({ sheet: i, x: 0, y: s.cursorY, w: s.w, h: hLeft });
    }
  });

  const partsArea = items.reduce((a, it) => a + it.area, 0);
  const consumed = live.reduce((a, s) => a + s.w * s.h, 0);
  const fullSheets = live.filter(s => !s.remnantId).length;
  const remnantsUsed = live.filter(s => s.remnantId).length;
  return {
    mode: o.mode, sheets: live, fullSheets, remnantsUsed, remnantOffered: remnantCount,
    offcuts, unplaced, split, notes,
    partsArea, consumed,
    wastePct: consumed > 0 ? (1 - partsArea / consumed) * 100 : 0,
    kerf, usableW, usableH,
  };
}

// ============================================================================
// DRAWINGS — every one of them generated from the numbers above
// ============================================================================
const CW_INK = 'var(--leon-black)';
const CW_BROWN = 'var(--leon-brown)';
const CW_LINE = 'var(--leon-line)';

// One front, drawn in its style. A rail height changed in CW_FRONT_STYLES
// redraws every door in the project — which is the reason styles are numbers
// and not pictures.
function CwFrontFace({ x, y, w, h, style, px, kind, handing, showPull }) {
  const s = cwFrontStyle(style);
  const out = [];
  const fill = kind === 'falseFront' ? '#faf7f2' : '#fff';
  out.push(<rect key="f" x={px(x)} y={px(y)} width={px(w)} height={px(h)} fill={fill} stroke={CW_INK} strokeWidth="1" />);
  if (s.kind === 'frame' || s.kind === 'glass') {
    const st = Math.min(s.stile, w / 3), rl = Math.min(s.rail, h / 3);
    out.push(<rect key="p" x={px(x + st)} y={px(y + rl)} width={px(w - 2 * st)} height={px(h - 2 * rl)}
      fill={s.kind === 'glass' ? '#eef4f7' : '#fbfaf8'} stroke={CW_INK} strokeWidth="0.7" />);
    if (s.kind === 'glass') {
      out.push(<line key="g1" x1={px(x + st)} y1={px(y + rl)} x2={px(x + w - st)} y2={px(y + h - rl)} stroke={CW_INK} strokeWidth="0.4" opacity="0.35" />);
    }
  } else if (s.kind === 'grooved') {
    const n = s.grooves;
    for (let i = 1; i <= n; i++) {
      const gx = x + (w * i) / (n + 1);
      out.push(<line key={`g${i}`} x1={px(gx)} y1={px(y + h * 0.03)} x2={px(gx)} y2={px(y + h * 0.97)}
        stroke={CW_BROWN} strokeWidth={s.round ? 1.1 : 0.8} opacity="0.7" />);
    }
  }
  if (showPull) {
    if (kind === 'door') {
      const side = handing === 'R' ? x + 24 : x + w - 24;         // pull opposite the hinges
      const cy = y + h * 0.12;
      out.push(<rect key="pl" x={px(side - 6)} y={px(cy)} width={px(12)} height={px(Math.min(120, h * 0.3))}
        rx={px(6)} fill={CW_BROWN} opacity="0.85" />);
    } else if (kind === 'drawer') {
      out.push(<rect key="pl" x={px(x + w / 2 - 60)} y={px(y + h / 2 - 6)} width={px(120)} height={px(12)}
        rx={px(6)} fill={CW_BROWN} opacity="0.85" />);
    }
  }
  return <g>{out}</g>;
}

// The cabinet elevation. Box outline, real reveals, real gaps, toe kick, pulls
// and dimensions — redrawn from `built`, never from a stored picture.
function CwElevation({ built, system, height, showDims, mark, handing }) {
  if (!built) return null;
  const L = built.layout, R = built.res;
  if (!(R.width > 0) || !(R.height > 0)) {
    return <div className="text-xs text-[var(--leon-black)]/40 p-4">Enter a size to draw this cabinet.</div>;
  }
  const pad = showDims ? 150 : 30;
  const totalW = R.width + pad * 2, totalH = R.height + pad * 2;
  const H = height || 320;
  const scale = H / totalH;
  const px = mm => mm * scale;
  const ox = pad, oy = pad;
  const style = R.frontStyle;

  return (
    <svg viewBox={`0 0 ${px(totalW)} ${px(totalH)}`} width="100%" height={H}
      style={{ maxWidth: px(totalW) }} role="img" aria-label={`Cabinet elevation ${mark || ''}`}>
      <rect x={px(ox)} y={px(oy)} width={px(R.width)} height={px(R.height)} fill="#fdfcfa" stroke={CW_INK} strokeWidth="1.6" />
      {L.blind > 0 && (
        <rect x={px(ox + R.width - L.blind)} y={px(oy)} width={px(L.blind)} height={px(R.height - L.toeH)}
          fill={CW_LINE} opacity="0.6" />
      )}
      {L.toeH > 0 && (
        <rect x={px(ox)} y={px(oy + R.height - L.toeH)} width={px(R.width)} height={px(L.toeH)}
          fill={CW_LINE} opacity="0.75" stroke={CW_INK} strokeWidth="0.6" />
      )}
      {L.cells.map(c => c.kind === 'opening' ? (
        <rect key={c.key} x={px(ox + c.x)} y={px(oy + c.y)} width={px(c.w)} height={px(c.h)}
          fill="var(--leon-cream)" stroke={CW_INK} strokeWidth="0.7" strokeDasharray="4 3" />
      ) : (
        <CwFrontFace key={c.key} x={ox + c.x} y={oy + c.y} w={c.w} h={c.h} style={style}
          kind={c.kind} handing={handing} showPull px={px} />
      ))}
      <line x1={px(ox - 40)} y1={px(oy + R.height)} x2={px(ox + R.width + 40)} y2={px(oy + R.height)}
        stroke={CW_INK} strokeWidth="1.6" />
      {showDims && (
        <g fontSize={Math.max(9, px(58))} fill={CW_BROWN} fontFamily="inherit">
          <line x1={px(ox)} y1={px(oy + R.height + 55)} x2={px(ox + R.width)} y2={px(oy + R.height + 55)}
            stroke={CW_BROWN} strokeWidth="0.9" markerStart="url(#cwtick)" markerEnd="url(#cwtick)" />
          <text x={px(ox + R.width / 2)} y={px(oy + R.height + 105)} textAnchor="middle">
            {fmtDim(R.width, system, { inchesOnly: true })}
          </text>
          <text x={px(ox - 45)} y={px(oy + R.height / 2)} textAnchor="middle"
            transform={`rotate(-90 ${px(ox - 45)} ${px(oy + R.height / 2)})`}>
            {fmtDim(R.height, system, { inchesOnly: true })}
          </text>
          {L.cells.filter(c => c.kind !== 'opening').map(c => (
            <text key={`d${c.key}`} x={px(ox + c.x + c.w / 2)} y={px(oy + c.y + c.h / 2 + 20)}
              textAnchor="middle" fill={CW_INK} opacity="0.5" fontSize={Math.max(7, px(44))}>
              {fmtDim(c.w, system, { inchesOnly: true })} × {fmtDim(c.h, system, { inchesOnly: true })}
            </text>
          ))}
          <text x={px(ox + R.width / 2)} y={px(oy - 45)} textAnchor="middle" fill={CW_INK} opacity="0.6">
            {mark || ''} · depth {fmtDim(R.depth, system, { inchesOnly: true })}
          </text>
        </g>
      )}
      <defs>
        <marker id="cwtick" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto">
          <line x1="3" y1="0" x2="3" y2="6" stroke={CW_BROWN} strokeWidth="0.9" />
        </marker>
      </defs>
    </svg>
  );
}

// The fronts of one member, resolved the same way the Cabinet Designer resolves
// them. A run elevation that draws blank boxes is a placement strip, not an
// elevation — 2020 draws the doors and drawers, and so does the shop drawing
// this module has to stand next to. Returns null for anything that is not a
// cabinet, so a filler or an appliance keeps its plain outline.
function cwMemberFronts(project, member, w, h) {
  if (!member || member.kind !== 'cabinet') return null;
  const type = cwType(project, member.cabTypeId);
  if (!type) return null;
  try {
    // A member overrides only what it disagrees with, which is the same rule
    // the schedule and the cut list read.
    const cab = cwMakeCabinet({ typeId: type.id, width: w, height: h,
      depth: member.depth != null ? member.depth : null });
    const res = cwResolve(project, cab);
    const con = cwConstruction(project, res.constructionId);
    return { layout: cwFrontLayout(res, con), res, con };
  } catch (e) { return null; }
}

// The run, drawn along its wall with the obstructions on it. This is the
// picture of the arithmetic above it — over-run shows as red past the wall end
// rather than as a number someone has to notice.
function CwRunElevation({ project, layout, wall, run, system, height }) {
  const wallLen = layout.wallLen;
  const wallH = cwNum(wall.height) || cwIn(96);
  const pad = 220;
  const spanW = Math.max(wallLen, layout.used + cwNum(run.startOffset)) + pad * 2;
  const spanH = wallH + pad * 2;
  const H = height || 260;
  const scale = H / spanH;
  const px = mm => mm * scale;
  const ox = pad, oy = pad;
  const floorY = oy + wallH;

  return (
    <svg viewBox={`0 0 ${px(spanW)} ${px(spanH)}`} width="100%" height={H} role="img" aria-label={`${run.name} on ${wall.name}`}>
      <rect x={px(ox)} y={px(oy)} width={px(wallLen)} height={px(wallH)} fill="#fdfcfa" stroke={CW_LINE} strokeWidth="1.5" />
      {(wall.obstructions || []).map(o => {
        const za = o.kind === 'Door' ? 0 : cwNum(o.sillHeight);
        const zb = za + cwNum(o.height);
        return (
          <g key={o.id}>
            <rect x={px(ox + cwNum(o.fromLeft))} y={px(floorY - zb)} width={px(cwNum(o.width))} height={px(zb - za)}
              fill="#eef4f7" stroke={CW_INK} strokeWidth="0.8" strokeDasharray="4 3" />
            <text x={px(ox + cwNum(o.fromLeft) + cwNum(o.width) / 2)} y={px(floorY - zb - 30)} textAnchor="middle"
              fontSize={Math.max(8, px(60))} fill={CW_INK} opacity="0.55">{o.label || o.kind}</text>
          </g>
        );
      })}
      {layout.members.map(mm => {
        const clash = layout.issues.some(i => i.level === 'error' && i.msg.indexOf(mm.label) === 0);
        const yTop = floorY - mm.z1;
        const isCab = mm.member.kind === 'cabinet';
        // The FRONTS, drawn the way the Cabinet Designer draws them — doors,
        // drawers, reveals, toe kick and pulls. Falling back to a labelled box
        // only where the member is not a cabinet or its type will not resolve.
        const F = isCab ? cwMemberFronts(project, mm.member, mm.w, mm.h) : null;
        return (
          <g key={mm.member.id}>
            <rect x={px(ox + mm.x)} y={px(yTop)} width={px(mm.w)} height={px(mm.h)}
              fill={mm.member.kind === 'appliance' ? '#eef4f7' : mm.member.kind === 'gap' ? 'none' : '#fdfcfa'}
              stroke={clash ? '#b83b3b' : CW_INK} strokeWidth={clash ? 2 : 1.2}
              strokeDasharray={mm.member.kind === 'gap' ? '5 4' : undefined} />
            {F && F.layout.toeH > 0 && (
              <rect x={px(ox + mm.x)} y={px(yTop + mm.h - F.layout.toeH)} width={px(mm.w)} height={px(F.layout.toeH)}
                fill={CW_LINE} opacity="0.7" stroke={CW_INK} strokeWidth="0.5" />
            )}
            {F && F.layout.cells.map(c => c.kind === 'opening' ? (
              <rect key={c.key} x={px(ox + mm.x + c.x)} y={px(yTop + c.y)} width={px(c.w)} height={px(c.h)}
                fill="var(--leon-cream)" stroke={CW_INK} strokeWidth="0.5" strokeDasharray="4 3" />
            ) : (
              <CwFrontFace key={c.key} x={ox + mm.x + c.x} y={yTop + c.y} w={c.w} h={c.h}
                style={F.res.frontStyle} kind={c.kind} handing={mm.member.handing} showPull px={px} />
            ))}
            {isCab && mm.type && !F && (
              <text x={px(ox + mm.x + mm.w / 2)} y={px(yTop + mm.h / 2)} textAnchor="middle"
                fontSize={Math.max(7, px(52))} fill={CW_INK} opacity="0.65">{mm.type.code}</text>
            )}
            {isCab && mm.type && (
              <text x={px(ox + mm.x + mm.w / 2)} y={px(floorY + 152)} textAnchor="middle"
                fontSize={Math.max(6, px(44))} fill={CW_INK} opacity="0.5">{mm.type.code}</text>
            )}
            <text x={px(ox + mm.x + mm.w / 2)} y={px(floorY + 78)} textAnchor="middle"
              fontSize={Math.max(7, px(50))} fill={CW_BROWN}>{fmtDim(mm.w, system, { inchesOnly: true })}</text>
          </g>
        );
      })}
      <line x1={px(ox - 40)} y1={px(floorY)} x2={px(ox + Math.max(wallLen, layout.used) + 40)} y2={px(floorY)}
        stroke={CW_INK} strokeWidth="1.8" />
      {layout.over && (
        <rect x={px(ox + wallLen)} y={px(floorY - 300)} width={px(-layout.remaining)} height={px(300)}
          fill="#fbe7e7" stroke="#b83b3b" strokeWidth="1.5" />
      )}
      <text x={px(ox + wallLen / 2)} y={px(oy - 60)} textAnchor="middle" fontSize={Math.max(9, px(66))} fill={CW_INK} opacity="0.7">
        {wall.name} — {fmtDim(wallLen, system, { inchesOnly: true })}{layout.measured ? ' (field measured)' : ' (nominal)'}
      </text>
    </svg>
  );
}

// The authoritative production view of a part: drawn to scale, with its grain
// direction, which edges are banded, where the holes go, and what it is called
// all the way back up the chain.
function CwPanelView({ panel, system, height, con }) {
  if (!panel) return null;
  const pad = 90;
  const spanW = panel.length + pad * 2, spanH = panel.width + pad * 2;
  const H = height || 220;
  const scale = Math.min(H / spanH, (H * 2.2) / spanW);
  const px = mm => mm * scale;
  const ox = pad, oy = pad;
  const bandColor = '#B08968';
  const e = panel.edges || {};
  return (
    <svg viewBox={`0 0 ${px(spanW)} ${px(spanH)}`} width="100%" height={H}
      style={{ maxWidth: px(spanW) }} role="img" aria-label={`Panel ${panel.name}`}>
      <rect x={px(ox)} y={px(oy)} width={px(panel.length)} height={px(panel.width)}
        fill="#fdfcfa" stroke={CW_INK} strokeWidth="1.2" />
      {/* Banded edges are drawn heavy on the edge they are on, so "which edges"
          is answered by looking rather than by reading four checkboxes. */}
      {e.front && <line x1={px(ox)} y1={px(oy + panel.width)} x2={px(ox + panel.length)} y2={px(oy + panel.width)} stroke={bandColor} strokeWidth="4" />}
      {e.back && <line x1={px(ox)} y1={px(oy)} x2={px(ox + panel.length)} y2={px(oy)} stroke={bandColor} strokeWidth="4" />}
      {e.left && <line x1={px(ox)} y1={px(oy)} x2={px(ox)} y2={px(oy + panel.width)} stroke={bandColor} strokeWidth="4" />}
      {e.right && <line x1={px(ox + panel.length)} y1={px(oy)} x2={px(ox + panel.length)} y2={px(oy + panel.width)} stroke={bandColor} strokeWidth="4" />}
      {/* Grain arrow — 'Length' runs along the long edge, which is how a saw
          operator reads it. 'None' draws nothing rather than an arrow that
          means "any way up", which is not the same claim. */}
      {panel.grain === 'Length' && (
        <g stroke={CW_INK} opacity="0.4" strokeWidth="1">
          <line x1={px(ox + panel.length * 0.25)} y1={px(oy + panel.width / 2)} x2={px(ox + panel.length * 0.75)} y2={px(oy + panel.width / 2)} markerEnd="url(#cwgrain)" />
        </g>
      )}
      {panel.grain === 'Width' && (
        <g stroke={CW_INK} opacity="0.4" strokeWidth="1">
          <line x1={px(ox + panel.length / 2)} y1={px(oy + panel.width * 0.75)} x2={px(ox + panel.length / 2)} y2={px(oy + panel.width * 0.25)} markerEnd="url(#cwgrain)" />
        </g>
      )}
      {(panel.ops || []).filter(op => op.x != null && op.y != null).map((op, i) => (
        <circle key={i} cx={px(ox + cwNum(op.y))} cy={px(oy + cwNum(op.x))} r={Math.max(2, px(cwNum(op.dia) / 2 || 6))}
          fill="none" stroke={CW_BROWN} strokeWidth="1.2" />
      ))}
      <text x={px(ox + panel.length / 2)} y={px(oy - 25)} textAnchor="middle" fontSize={Math.max(9, px(45))} fill={CW_BROWN}>
        {fmtDim(panel.length, system, { inchesOnly: true })}
      </text>
      <text x={px(ox - 25)} y={px(oy + panel.width / 2)} textAnchor="middle" fontSize={Math.max(9, px(45))} fill={CW_BROWN}
        transform={`rotate(-90 ${px(ox - 25)} ${px(oy + panel.width / 2)})`}>
        {fmtDim(panel.width, system, { inchesOnly: true })}
      </text>
      <defs>
        <marker id="cwgrain" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto">
          <path d="M0,1 L6,4 L0,7 z" fill={CW_INK} opacity="0.5" />
        </marker>
      </defs>
    </svg>
  );
}

// The nesting diagram. Every rectangle is a real placement from the optimiser
// above, labelled, so the proposal can be read and approved rather than trusted.
function CwNestView({ nest, sheet, index, system }) {
  const W = 520;
  const scale = W / sheet.w;
  const px = mm => mm * scale;
  const cuts = nest.offcuts.filter(o => o.sheet === index);
  return (
    <svg viewBox={`0 0 ${px(sheet.w)} ${px(sheet.h)}`} width="100%" height={Math.min(430, px(sheet.h))}
      role="img" aria-label={`Nest sheet ${index + 1}`}>
      <rect x="0" y="0" width={px(sheet.w)} height={px(sheet.h)} fill="#fdfcfa" stroke={CW_INK} strokeWidth="1.4" />
      {cuts.map((o, i) => (
        <rect key={`o${i}`} x={px(o.x)} y={px(o.y)} width={px(o.w)} height={px(o.h)}
          fill="#e7f3e9" stroke="#3a7d44" strokeWidth="0.8" strokeDasharray="4 3" />
      ))}
      {sheet.placements.map((p, i) => (
        <g key={i}>
          <rect x={px(p.x)} y={px(p.y)} width={px(p.w)} height={px(p.h)}
            fill={p.item.panel.group === 'Front' ? '#fbf1dd' : '#fff'} stroke={CW_INK} strokeWidth="0.9" />
          {px(p.w) > 44 && px(p.h) > 16 && (
            <text x={px(p.x + p.w / 2)} y={px(p.y + p.h / 2) + 3} textAnchor="middle" fontSize="8" fill={CW_INK} opacity="0.75">
              {p.item.panel.mark || ''} {p.item.panel.key}{p.rotated ? ' ↻' : ''}
            </text>
          )}
        </g>
      ))}
    </svg>
  );
}

// ---- small shared UI ------------------------------------------------------
// A dimension field that speaks the user's units both ways: it shows the stored
// millimetres formatted and accepts 3'-0", 36 1/2", 914mm or a bare number.
function CwDimField({ value, onChange, system, placeholder, w, disabled }) {
  const [draft, setDraft] = useState(null);
  const shown = draft !== null ? draft
    : (value === null || value === undefined || value === '' ? '' : fmtDim(cwNum(value), system, { inchesOnly: true }));
  return (
    <input type="text" value={shown} placeholder={placeholder} disabled={disabled}
      onChange={e => setDraft(e.target.value)}
      onBlur={e => {
        setDraft(null);
        const t = e.target.value.trim();
        if (!t) { onChange(null); return; }
        const mm = parseDim(t, system);
        if (mm !== null) onChange(mm);
      }}
      className={`${w || 'w-24'} px-2 py-1 text-sm border border-[var(--leon-line)] rounded bg-white focus:outline-none focus:border-[var(--leon-brown)] disabled:bg-[var(--leon-cream)]`} />
  );
}
function CwIssueList({ issues, limit }) {
  if (!issues || !issues.length) return null;
  const tone = l => l === 'error' ? 'bg-red-50 border-red-200 text-red-700'
    : l === 'warn' ? 'bg-amber-50 border-amber-200 text-amber-900'
    : 'bg-[var(--leon-cream)] border-[var(--leon-line)] text-[var(--leon-black)]/70';
  const shown = limit ? issues.slice(0, limit) : issues;
  return (
    <div className="space-y-1.5">
      {shown.map((i, k) => <div key={k} className={`rounded border text-xs px-2.5 py-1.5 ${tone(i.level)}`}>{i.msg}</div>)}
      {limit && issues.length > limit && (
        <div className="text-[11px] text-[var(--leon-black)]/45">…and {issues.length - limit} more.</div>
      )}
    </div>
  );
}
// The arithmetic, shown. Every derived number in this module can be traced to a
// line here — that is the difference between a tool a shop uses and a tool a
// shop re-checks by hand.
function CwMathList({ math, title }) {
  if (!math || !math.length) return null;
  return (
    <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]/60 p-3">
      <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 mb-1.5">{title || 'How these numbers were reached'}</div>
      <div className="space-y-1">
        {math.map((m, i) => (
          <div key={i} className="text-[11px] flex gap-2">
            <span className="font-semibold text-[var(--leon-black)]/70 w-36 shrink-0">{m.label}</span>
            <span className="text-[var(--leon-black)]/60 tabular-nums">{m.expr}</span>
          </div>
        ))}
      </div>
    </div>
  );
}
function CwStat({ label, value, tone }) {
  return (
    <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
      <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">{label}</div>
      <div className={`text-2xl font-bold ${tone === 'red' ? 'text-[#b83b3b]' : tone === 'green' ? 'text-[#3a7d44]' : 'text-[var(--leon-brown)]'}`}>{value}</div>
    </div>
  );
}
// A single named list inside the project's casework settings. One editor serves
// constructions, materials and hardware rather than three near-identical panels.
function CwLibPanel({ title, blurb, items, editable, onAdd, onRemove, columns, renderRow, addLabel, minWidth }) {
  return (
    <div className="space-y-3">
      <div className="flex items-start justify-between gap-3 flex-wrap">
        <div>
          <h3 className="font-bold">{title}</h3>
          <p className="text-sm text-[var(--leon-black)]/55 max-w-2xl">{blurb}</p>
        </div>
        {editable && onAdd && <Button size="sm" onClick={onAdd}>+ {addLabel || 'Add'}</Button>}
      </div>
      <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-x-auto">
        <table className="w-full text-xs" style={{ minWidth: minWidth || 760 }}>
          <thead>
            <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
              {columns.map(c => <th key={c} className="px-2 py-2">{c}</th>)}
              {editable && onRemove && <th className="px-2 py-2 w-8"></th>}
            </tr>
          </thead>
          <tbody>
            {items.map(it => (
              <tr key={it.id} className="border-b border-[var(--leon-line)]/60">
                {renderRow(it)}
                {editable && onRemove && (
                  <td className="px-2 py-1">
                    <button className="text-red-600" title="Remove"
                      onClick={() => { if (confirm(`Remove "${it.name || it.code}"?`)) onRemove(it.id); }}>✕</button>
                  </td>
                )}
              </tr>
            ))}
            {!items.length && <tr><td colSpan={columns.length + 1} className="px-3 py-5 text-center text-[var(--leon-black)]/40">Nothing here yet.</td></tr>}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// ---- writes ---------------------------------------------------------------
// Every collection is created on first write, never assumed. A project saved
// before this module existed has none of these arrays, and a guard here is
// cheaper than a crash in eleven places.
//
// ctx.updateProject and ctx.logAction both live inside App(). Calling a bare
// logAction() from this file fails silently INSIDE the draft callback and the
// whole write is lost with nothing on screen, which is why nothing below ever
// does.
function cwUpdate(ctx, project, fn, action) {
  ctx.updateProject(project.id, draft => {
    if (!Array.isArray(draft.caseworkItems)) draft.caseworkItems = [];
    if (!Array.isArray(draft.caseworkTypes)) draft.caseworkTypes = [];
    if (!Array.isArray(draft.caseworkRoomTypes)) draft.caseworkRoomTypes = [];
    if (!Array.isArray(draft.caseworkRooms)) draft.caseworkRooms = [];
    if (!Array.isArray(draft.caseworkFinishTags)) draft.caseworkFinishTags = [];
    if (!Array.isArray(draft.caseworkKeynotes)) draft.caseworkKeynotes = [];
    if (!Array.isArray(draft.caseworkSubmittals)) draft.caseworkSubmittals = [];
    fn(draft);
    if (action) ctx.logAction(draft, action);
  });
}

// ============================================================================
// THE MODULE
// ============================================================================
function CaseworkSoftware({ ctx }) {
  // A link can say which job and which screen, not only which tool.
  const [section, setSection] = useState(() =>
    (typeof swBootParam === 'function' && swBootParam('section')) || 'dashboard');
  const [projectId, setProjectId] = useState(() =>
    (typeof swBootParam === 'function' && swBootParam('project')) || '');
  const [system, setSystem] = useState('Imperial');
  const [focusCab, setFocusCab] = useState(null);
  const [focusRoom, setFocusRoom] = useState(null);

  // deptProjects is a FUNCTION that filters by the active department, not an
  // array. Treating it as one is quietly fatal — a function's .length is its
  // arity, so a truthiness guard passes and .find() then throws on the function
  // itself.
  // Real jobs, plus this person's own unassigned workspace. Work often
  // starts before there is a job to attach it to — and sometimes never
  // becomes one — so no tool should demand a project before it will open.
  const allForTools = typeof ctx.toolProjects === 'function' ? ctx.toolProjects() : (ctx.projects || []);
  const projects = ctx.deptProjects(allForTools);
  const scratchIds = new Set((ctx.scratchProjects || []).map(p => p.id));
  const project = projects.find(p => p.id === projectId) || null;
  const editable = ctx.canEdit('documents');
  // The casework LIBRARY is a separate right from using the software, exactly
  // as the door library is: it holds what every future cabinet is built to.
  const libEditable = editable && ctx.canEditCaseworkLibrary !== false;

  // Without a project this is a library browser, which is a legitimate thing to
  // want. Anything that reads or writes job data needs one.
  const needsProject = ['rooms', 'runs', 'planner', 'design3d', 'render', 'pano', 'casework',
                        'schedule', 'designer', 'types', 'construction', 'hardware', 'panels',
                        'nesting', 'sheets', 'tags', 'keynotes', 'submittals'].indexOf(section) >= 0;

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-4 flex-wrap">
        <div>
          <h2 className="text-xl font-bold">🪵 LEON Casework &amp; Millwork</h2>
          <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
            One parametric engine for every room — kitchens, vanities, closets, wardrobes, laundry, bars,
            reception desks and custom millwork. A cabinet is a record: the run, the elevation, the panel
            list, the edge banding and the nest all read it, so changing a width changes all of them.
          </p>
        </div>
        <div className="flex items-end gap-2 flex-wrap">
          <Field label="Project">
            <Select className="!w-56" value={scratchIds.has(projectId) ? '__scratch' : projectId}
              onChange={e => { const v = e.target.value; const id = v === '__scratch' && typeof ctx.myScratchProject === 'function' ? ctx.myScratchProject().id : v; setProjectId(id); }}>
              <option value="">— select a project —</option>
              {projects.filter(p => !scratchIds.has(p.id)).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
              <option value="__scratch">— Not linked to a job (my workspace) —</option>
            </Select>
          </Field>
          <Field label="Units">
            <Select className="!w-32" value={system} onChange={e => setSystem(e.target.value)}>
              <option>Imperial</option><option>Metric</option>
            </Select>
          </Field>
        </div>
      </div>


      <SoftwareRail swKey="casework" sections={CW_SW_SECTIONS} active={section}
        onSelect={setSection}
        status={project ? <>
          <span>{cwItems(project).length} cabinet{cwItems(project).length === 1 ? '' : 's'}</span>
          <span className="opacity-40">·</span>
          <span>{cwRooms(project).length} room{cwRooms(project).length === 1 ? '' : 's'}</span>
          <span className="opacity-40">·</span>
          <span>{cwCaseworkTypes(project).length} casework type{cwCaseworkTypes(project).length === 1 ? '' : 's'}</span>
          <span className="opacity-40">·</span>
          <span>{system}</span>
        </> : null}>
      {needsProject && !project ? (
        <div className="rounded-lg border border-dashed border-[var(--leon-line)] p-8 text-center">
          <div className="text-3xl mb-2">🪵</div>
          <div className="font-semibold mb-1">Pick a project</div>
          <div className="text-sm text-[var(--leon-black)]/55 max-w-lg mx-auto">
            Cabinets, rooms and the shop standards they are built to all belong to a job. The Global
            Library is shared and can be browsed without one.
          </div>
        </div>
      ) : (
        <>
          {section === 'dashboard' && <CwDashboard ctx={ctx} projects={projects} system={system}
            onOpen={(pid, s) => { setProjectId(pid); setSection(s); }} />}
          {section === 'rooms' && <CwRoomsPanel ctx={ctx} project={project} system={system} editable={editable}
            onOpenRun={r => { setFocusRoom(r); setSection('runs'); }} />}
          {section === 'runs' && <CwRunDesigner ctx={ctx} project={project} system={system} editable={editable}
            room={focusRoom} onRoom={setFocusRoom} />}
          {/* The 3D view is another way of looking at the same records — it derives every
              mesh from cwComputeParts, so it can never disagree with the cut list. It lives
              in its own file, which loads after this one; guard on it so a load-order change
              shows a named notice instead of blanking the tab. */}
          {section === 'design3d' && (typeof Cw3DStudio === 'function'
            ? <Cw3DStudio ctx={ctx} project={project} editable={editable} />
            : <div className="rounded-lg border border-dashed border-[var(--leon-line)] p-8 text-center text-sm text-[var(--leon-black)]/55">
                The 3D Studio (softwares/casework3d.jsx) did not load.
              </div>)}
          {(section === 'render' || section === 'pano') && (typeof LeonRenderPanel === 'function'
            ? <LeonRenderPanel ctx={ctx} project={project} room={null} mode={section} />
            : <EmptyState text="The render module (softwares/render.jsx) did not load." />)}
          {section === 'planner' && <CwPlanner ctx={ctx} project={project} system={system} editable={editable} />}
          {section === 'casework' && <CwCaseworkTypesPanel ctx={ctx} project={project} system={system} editable={editable} />}
          {section === 'schedule' && <CwSchedule ctx={ctx} project={project} system={system} editable={editable}
            onEdit={c => { setFocusCab(c); setSection('designer'); }} />}
          {section === 'designer' && <CwDesigner ctx={ctx} project={project} system={system} editable={editable}
            cab={focusCab} onPick={setFocusCab} />}
          {/* The library sections take the same capability Door Settings does:
              these are standards every future cabinet inherits, which is a
              narrower authority than drawing with them. */}
          {section === 'cabLibrary' && <CwLibraryModulesPanel ctx={ctx} system={system} />}
          {section === 'appliances' && <CwLibraryAppliancesPanel ctx={ctx} />}
          {section === 'components' && <CwLibraryComponentsPanel ctx={ctx} />}
          {section === 'rules' && <CwLibraryRulesPanel ctx={ctx} />}
          {section === 'types' && <CwCabinetTypesPanel ctx={ctx} project={project} system={system} editable={libEditable} />}
          {section === 'global' && <CwGlobalLibrary ctx={ctx} project={project} system={system} editable={libEditable} />}
          {section === 'construction' && <CwConstructionPanel ctx={ctx} project={project} system={system} editable={libEditable} />}
          {section === 'hardware' && <CwHardwarePanel ctx={ctx} project={project} system={system} editable={libEditable} />}
          {section === 'panels' && <CwPanelsPanel ctx={ctx} project={project} system={system} editable={editable} />}
          {section === 'nesting' && <CwNestingPanel ctx={ctx} project={project} system={system} editable={editable} />}
          {section === 'sheets' && <CwSheetsPanel ctx={ctx} project={project} system={system} />}
          {section === 'tags' && <CwFinishTagsPanel ctx={ctx} project={project} editable={editable} />}
          {section === 'keynotes' && <CwKeynotesPanel ctx={ctx} project={project} editable={editable} />}
          {section === 'submittals' && <CwSubmittalsPanel ctx={ctx} project={project} editable={editable} />}
        </>
      )}
      </SoftwareRail>
    </div>
  );
}

function CwDashboard({ ctx, projects, system, onOpen }) {
  const rows = projects.map(p => {
    const items = cwItems(p);
    const byStatus = {};
    items.forEach(c => { byStatus[c.status || 'Draft'] = (byStatus[c.status || 'Draft'] || 0) + 1; });
    return { p, items, rooms: cwRooms(p), cts: cwCaseworkTypes(p), types: cwProjectTypes(p).length, byStatus,
             qty: items.reduce((a, c) => a + (cwNum(c.qty) || 1), 0) };
  }).filter(r => r.items.length || r.rooms.length || r.cts.length || r.types);
  const totalCabs = rows.reduce((a, r) => a + r.qty, 0);
  const totalUnits = rows.reduce((a, r) => a + r.rooms.length, 0);
  return (
    <div className="space-y-4">
      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
        <CwStat label="Cabinets on the books" value={totalCabs} />
        <CwStat label="Rooms / units" value={totalUnits} />
        <CwStat label="Jobs with casework" value={rows.length} />
        <CwStat label="Standard library types" value={CW_STANDARD_TYPES.length} />
        <CwStat label="Front styles" value={CW_FRONT_STYLES.length} />
      </div>
      {!rows.length && <EmptyState text="No casework yet. Pick a project, set up a room and its walls, then lay out a run." />}
      <div className="grid gap-3 md:grid-cols-2">
        {rows.map(r => (
          <button key={r.p.id} onClick={() => onOpen(r.p.id, r.rooms.length ? 'runs' : 'rooms')}
            className="text-left rounded-lg border border-[var(--leon-line)] bg-white p-3 hover:border-[var(--leon-brown)]">
            <div className="font-bold">{r.p.name}</div>
            <div className="text-xs text-[var(--leon-black)]/50 mb-2">
              {r.items.length} cabinet{r.items.length === 1 ? '' : 's'} · {r.rooms.length} room{r.rooms.length === 1 ? '' : 's'} ·
              {' '}{r.cts.length} casework type{r.cts.length === 1 ? '' : 's'} · {r.types} project cabinet type{r.types === 1 ? '' : 's'}
            </div>
            <div className="flex flex-wrap gap-1">
              {Object.keys(r.byStatus).map(k => <Badge key={k}>{k} {r.byStatus[k]}</Badge>)}
            </div>
          </button>
        ))}
      </div>
      <div className="rounded-lg border border-[var(--leon-line)] bg-white p-4 text-sm text-[var(--leon-black)]/60 space-y-1.5">
        <div className="font-semibold text-[var(--leon-black)]">What this module does not do</div>
        <p>
          There is <b>no CNC output</b>: machining is stored as structured operations against a panel, and
          turning those into a program belongs to whoever owns the machine. There is <b>no AI</b> — the ✨
          controls write the request properly and hand it to Claude or ChatGPT; nothing here answers one.
          The nesting under <b>Nesting</b> is a real optimiser but a heuristic one, so it produces a
          proposal a person approves and never a silent production decision. <b>Render</b> and
          <b>360 Panorama</b> are real, but they draw the room this module models &mdash; they are not a
          photoreal studio, and a picture that disagreed with the shop drawings would be worse than none.
          The <b>Planner</b> places by catalog code with snap and collision checks; it is not a freehand
          CAD canvas, because a run has to stay an ordered list for the cut list to read it.
        </p>
      </div>
    </div>
  );
}

// ============================================================================
// ROOMS & WALLS
// ----------------------------------------------------------------------------
// A room is a physical place with a ceiling height and walls that carry the
// runs. It points at a Casework Type when it is one of forty identical units,
// and stands on its own when it is a one-off — the same object either way.
// ============================================================================
function CwRoomsPanel({ ctx, project, system, editable, onOpenRun }) {
  const rooms = cwRooms(project);
  const cts = cwCaseworkTypes(project);
  const scopes = (project.scopes || []);

  function addRoom() {
    cwUpdate(ctx, project, draft => {
      draft.caseworkRooms.push(cwMakeRoom({ name: `Room ${draft.caseworkRooms.length + 1}` }, ctx.currentUserName));
    }, `LEON Casework — room added.`);
  }
  function updRoom(id, fields) {
    cwUpdate(ctx, project, draft => {
      const r = draft.caseworkRooms.find(x => x.id === id);
      if (r) Object.assign(r, fields);
    }, null);
  }
  function removeRoom(id) {
    cwUpdate(ctx, project, draft => {
      const r = draft.caseworkRooms.find(x => x.id === id);
      draft.caseworkRooms = draft.caseworkRooms.filter(x => x.id !== id);
      // The cabinets go with it — leaving orphans behind is how a cut list ends
      // up with parts for a room nobody is building.
      draft.caseworkItems = draft.caseworkItems.filter(c => c.roomId !== id);
      if (r) ctx.logAction(draft, `LEON Casework — room "${r.name}" removed with its cabinets.`);
    });
  }
  // A wall is added where the design lives: to the Casework Type when the room
  // is built to one, to the room itself when it is a one-off. Adding it to the
  // room instead would shadow the type's walls entirely and quietly detach the
  // unit from the design it is supposed to share.
  function addWall(roomId) {
    cwUpdate(ctx, project, draft => {
      const r = draft.caseworkRooms.find(x => x.id === roomId);
      if (!r) return;
      const owner = r.caseworkTypeId ? draft.caseworkRoomTypes.find(x => x.id === r.caseworkTypeId) : r;
      if (!owner) return;
      if (!Array.isArray(owner.walls)) owner.walls = [];
      owner.walls.push(cwMakeWall({ name: `Wall ${String.fromCharCode(65 + owner.walls.length)}` }));
    }, 'LEON Casework — wall added.');
  }

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-3 flex-wrap">
        <div>
          <h3 className="font-bold">Rooms &amp; Walls</h3>
          <p className="text-sm text-[var(--leon-black)]/55 max-w-2xl">
            The wall length here is what every run on it is checked against. A <b>field measurement</b>
            {' '}always beats the nominal — enter it the day it is taken and every run redraws to it.
          </p>
        </div>
        {editable && <Button size="sm" onClick={addRoom}>+ Add room</Button>}
      </div>
      {!rooms.length && <EmptyState text="No rooms yet. Add one, give its walls their lengths, then lay out the runs." />}

      {rooms.map(room => {
        const ct = cts.find(x => x.id === room.caseworkTypeId) || null;
        const walls = room.walls && room.walls.length ? room.walls : (ct ? ct.walls : []);
        const fromType = !(room.walls && room.walls.length) && !!ct;
        return (
          <Collapsible key={room.id} id={`cwroom-${room.id}`} title={`${room.name}${room.unit ? ` — unit ${room.unit}` : ''}`}
            count={walls.length} right={<Badge>{ct ? `${ct.code}${room.mirrored ? ' mirrored' : ''}` : 'one-off'}</Badge>}>
            <div className="space-y-3">
              <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
                <Field label="Name"><TextInput value={room.name} disabled={!editable} onChange={e => updRoom(room.id, { name: e.target.value })} /></Field>
                <Field label="Room kind">
                  <Select value={room.kind} disabled={!editable} onChange={e => updRoom(room.id, { kind: e.target.value })}>
                    {CW_ROOM_KINDS.map(k => <option key={k}>{k}</option>)}
                  </Select>
                </Field>
                <Field label="Unit"><TextInput value={room.unit} disabled={!editable} onChange={e => updRoom(room.id, { unit: e.target.value })} placeholder="1204" /></Field>
                <Field label="Level"><TextInput value={room.level} disabled={!editable} onChange={e => updRoom(room.id, { level: e.target.value })} placeholder="12" /></Field>
                <Field label="Casework Type" hint="Points this room at a type designed once and built many times.">
                  <Select value={room.caseworkTypeId || ''} disabled={!editable} onChange={e => updRoom(room.id, { caseworkTypeId: e.target.value || null })}>
                    <option value="">— one-off, no type —</option>
                    {cts.map(t => <option key={t.id} value={t.id}>{t.code} · {t.name}</option>)}
                  </Select>
                </Field>
                <Field label="Scope">
                  <Select value={room.scopeId || ''} disabled={!editable} onChange={e => updRoom(room.id, { scopeId: e.target.value || null })}>
                    <option value="">— none —</option>
                    {scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
                  </Select>
                </Field>
                <Field label="Ceiling height">
                  <CwDimField value={room.ceilingHeight != null ? room.ceilingHeight : (ct ? ct.ceilingHeight : null)}
                    system={system} disabled={!editable} onChange={v => updRoom(room.id, { ceilingHeight: v })} />
                </Field>
                <Field label="Status">
                  <Select value={room.status} disabled={!editable} onChange={e => updRoom(room.id, { status: e.target.value })}>
                    {CW_STATUSES.map(s => <option key={s}>{s}</option>)}
                  </Select>
                </Field>
              </div>
              <label className="flex items-center gap-2 text-sm">
                <input type="checkbox" checked={!!room.mirrored} disabled={!editable}
                  onChange={e => updRoom(room.id, { mirrored: e.target.checked })} />
                <span><b>Mirror this unit.</b> Reverses the run order, measures every obstruction from the other end and flips the handing.</span>
              </label>

              <div className="flex items-center justify-between gap-2 pt-2 border-t border-[var(--leon-line)]">
                <div className="text-sm font-semibold">Walls{fromType ? ' — inherited from the Casework Type' : ''}</div>
                <div className="flex gap-2">
                  {editable && <Button size="sm" variant="ghost" onClick={() => addWall(room.id)}>+ Add wall</Button>}
                  <Button size="sm" variant="ghost" onClick={() => onOpenRun(room)}>Open the run designer →</Button>
                </div>
              </div>
              {fromType && (
                <p className="text-[11px] text-[var(--leon-black)]/50">
                  These come from {ct.code}. A field measurement below is stored on THIS unit and does not
                  change the type or any other unit built to it.
                </p>
              )}
              {!walls.length && <EmptyState text="No walls yet." />}
              {walls.map(w => (
                <CwWallEditor key={w.id} ctx={ctx} project={project} room={room} wall={w} system={system}
                  editable={editable} fromType={fromType} />
              ))}
              {editable && (
                <button onClick={() => { if (confirm(`Remove ${room.name} and its cabinets?`)) removeRoom(room.id); }}
                  className="text-xs font-semibold text-red-600">Remove this room</button>
              )}
            </div>
          </Collapsible>
        );
      })}
    </div>
  );
}

function CwWallEditor({ ctx, project, room, wall, system, editable, fromType }) {
  const measured = cwNum((room.fieldMeasure || {})[wall.id]);
  // A wall shown here belongs either to the room (a one-off) or to the Casework
  // Type it is built to. The edit has to land on whichever actually owns it —
  // writing to the room when the type owns the wall looks like it worked and
  // silently changes nothing, which is the worst of both.
  function withWall(draft, fn) {
    const owner = fromType
      ? draft.caseworkRoomTypes.find(x => x.id === room.caseworkTypeId)
      : draft.caseworkRooms.find(x => x.id === room.id);
    if (!owner || !Array.isArray(owner.walls)) return;
    const w = owner.walls.find(x => x.id === wall.id);
    if (w) fn(w, owner);
  }
  function setWall(fields) {
    cwUpdate(ctx, project, draft => withWall(draft, w => Object.assign(w, fields)), null);
  }
  // A field measurement is ALWAYS the unit's own, even when the wall is the
  // type's — that is the entire point of measuring unit 1204 rather than the
  // drawing.
  function setMeasure(v) {
    cwUpdate(ctx, project, draft => {
      const r = draft.caseworkRooms.find(x => x.id === room.id);
      if (!r) return;
      r.fieldMeasure = Object.assign({}, r.fieldMeasure || {});
      if (v === null) delete r.fieldMeasure[wall.id];
      else r.fieldMeasure[wall.id] = v;
    }, `LEON Casework — ${room.name} ${wall.name} field measurement ${v === null ? 'cleared' : `set to ${cwN(v)} mm`}.`);
  }
  function addObs() {
    cwUpdate(ctx, project, draft => withWall(draft, w => { w.obstructions = (w.obstructions || []).concat([cwMakeObstruction({})]); }),
      `LEON Casework — obstruction added to ${wall.name}${fromType ? ' on the casework type' : ''}.`);
  }
  function updObs(oid, fields) {
    cwUpdate(ctx, project, draft => withWall(draft, w => {
      const o = (w.obstructions || []).find(x => x.id === oid);
      if (o) Object.assign(o, fields);
    }), null);
  }
  function removeObs(oid) {
    cwUpdate(ctx, project, draft => withWall(draft, w => { w.obstructions = (w.obstructions || []).filter(x => x.id !== oid); }),
      'LEON Casework — obstruction removed.');
  }
  // The wall itself can still be edited when it belongs to the type — the edit
  // simply lands on the type, and the banner says so, because "greyed out with
  // no explanation" is how a drawing stays wrong.
  const canEditWall = editable;
  return (
    <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3 space-y-2">
      {fromType && (
        <div className="text-[11px] text-[var(--leon-black)]/55">
          This wall belongs to the casework type — changing its length or obstructions changes it for every
          unit built to it. The field measurement below is this unit's alone.
        </div>
      )}
      <div className="flex items-end gap-3 flex-wrap">
        <Field label="Wall">
          <TextInput className="!w-28" value={wall.name} disabled={!canEditWall} onChange={e => setWall({ name: e.target.value })} />
        </Field>
        <Field label="Nominal length">
          <CwDimField value={wall.length} system={system} disabled={!canEditWall} onChange={v => setWall({ length: v })} />
        </Field>
        <Field label="Field measured" hint="Beats the nominal everywhere.">
          <CwDimField value={measured || null} system={system} disabled={!editable} onChange={setMeasure} placeholder="not taken" />
        </Field>
        <Field label="Wall height">
          <CwDimField value={wall.height} system={system} disabled={!canEditWall} onChange={v => setWall({ height: v })} />
        </Field>
        {measured > 0 && Math.abs(measured - cwNum(wall.length)) > 3 && (
          <div className="text-[11px] text-amber-800 bg-amber-50 border border-amber-200 rounded px-2 py-1">
            {measured > cwNum(wall.length) ? 'Wider' : 'Narrower'} than drawn by {fmtDim(Math.abs(measured - cwNum(wall.length)), system, { inchesOnly: true })}.
          </div>
        )}
        {editable && <Button size="sm" variant="ghost" className="ml-auto" onClick={addObs}>+ Obstruction</Button>}
      </div>
      {!!(wall.obstructions || []).length && (
        <table className="w-full text-xs">
          <thead>
            <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">
              <th className="py-1">What</th><th className="py-1">Label</th><th className="py-1">From the left</th>
              <th className="py-1">Width</th><th className="py-1">Sill</th><th className="py-1">Height</th><th></th>
            </tr>
          </thead>
          <tbody>
            {(wall.obstructions || []).map(o => (
              <tr key={o.id} className="border-t border-[var(--leon-line)]/60">
                <td className="py-1 pr-2">
                  <select value={o.kind} disabled={!editable} onChange={e => updObs(o.id, { kind: e.target.value })}
                    className="px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                    {CW_OBSTRUCTION_KINDS.map(k => <option key={k}>{k}</option>)}
                  </select>
                </td>
                <td className="py-1 pr-2">
                  <input value={o.label} disabled={!editable} onChange={e => updObs(o.id, { label: e.target.value })}
                    className="w-28 px-1 py-0.5 border border-transparent hover:border-[var(--leon-line)] rounded bg-transparent" />
                </td>
                {['fromLeft', 'width', 'sillHeight', 'height'].map(f => (
                  <td key={f} className="py-1 pr-2">
                    <CwDimField value={o[f]} system={system} disabled={!editable} w="w-20" onChange={v => updObs(o.id, { [f]: v })} />
                  </td>
                ))}
                <td className="py-1">{editable && <button className="text-red-600" onClick={() => removeObs(o.id)}>✕</button>}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

// ============================================================================
// RUN DESIGNER — the running arithmetic
// ============================================================================

// Runs live wherever the design lives: on the Casework Type when the room is one
// of many built to it, on the room itself when it is a one-off. One accessor, so
// nothing downstream has to know which.
function cwRunHost(project, room) {
  const ct = room.caseworkTypeId ? cwCaseworkTypes(project).find(x => x.id === room.caseworkTypeId) : null;
  if (ct) return { kind: 'type', ct, walls: ct.walls || [], runs: ct.runs || [] };
  return { kind: 'room', ct: null, walls: room.walls || [], runs: room.runs || [] };
}
function cwMutateRuns(ctx, project, room, fn, action) {
  cwUpdate(ctx, project, draft => {
    if (room.caseworkTypeId) {
      const ct = draft.caseworkRoomTypes.find(x => x.id === room.caseworkTypeId);
      if (!ct) return;
      if (!Array.isArray(ct.runs)) ct.runs = [];
      fn(ct.runs, ct);
      // A design change bumps the type's revision. The impact review is what
      // decides whether it reaches the units, not this.
      ct.revision = cwNum(ct.revision) + 1;
    } else {
      const r = draft.caseworkRooms.find(x => x.id === room.id);
      if (!r) return;
      if (!Array.isArray(r.runs)) r.runs = [];
      fn(r.runs, r);
    }
  }, action);
}

// The wizard's own screen. It shows what it WILL make before it makes it —
// a wizard that just does it is a wizard nobody trusts the second time.
function CwCounterWizard({ ctx, project, room, system, editable, onClose }) {
  const [overhang, setOverhang] = useState(1.5);
  const [done, setDone] = useState(null);
  const plan = cwCounterPlan(project, ctx, room, { overhangIn: overhang });
  const total = plan.pieces.reduce((n, p) => n + p.lengthIn * p.depthIn, 0) / 144;

  return (
    <Modal open onClose={onClose} size="lg" title={`Generate countertops — ${room.name}`}>
      <div className="space-y-3">
        <p className="text-sm text-[var(--leon-black)]/65">
          The worktop over the base cabinets, drawn from the runs rather than measured again. It is created
          in <b>LEON Countertop</b> and lives there &mdash; this keeps no second copy, so a run that changes
          is re-run here rather than corrected in two places.
        </p>

        <div className="flex items-end gap-3 flex-wrap">
          <Field label="Front overhang" hint="Past the face of the box.">
            <TextInput className="!w-24" value={overhang}
              onChange={e => setOverhang(e.target.value)} disabled={!editable} />
          </Field>
          <div className="pb-2 text-xs text-[var(--leon-black)]/55">inches</div>
        </div>

        {plan.issues.map((m, i) => (
          <div key={i} className="rounded-md border border-[#d8b96a] bg-[#fdf6e3] px-3 py-2 text-xs">{m}</div>
        ))}

        {plan.pieces.length > 0 && (
          <div className="rounded-lg border border-[var(--leon-line)] overflow-hidden">
            <table className="w-full text-sm">
              <thead className="bg-[var(--leon-cream)] text-[11px] uppercase tracking-wide text-[var(--leon-black)]/55">
                <tr><th className="text-left px-2 py-1">Piece</th><th className="text-right px-2 py-1">Length</th>
                  <th className="text-right px-2 py-1">Depth</th><th className="text-right px-2 py-1">Area</th>
                  <th className="text-right px-2 py-1">Boxes</th></tr>
              </thead>
              <tbody>
                {plan.pieces.map(pc => (
                  <tr key={pc.runId} className="border-t border-[var(--leon-line)]">
                    <td className="px-2 py-1">{pc.name}</td>
                    <td className="px-2 py-1 text-right tabular-nums">{pc.lengthIn.toFixed(1)}&quot;</td>
                    <td className="px-2 py-1 text-right tabular-nums">{pc.depthIn.toFixed(1)}&quot;</td>
                    <td className="px-2 py-1 text-right tabular-nums">{(pc.lengthIn * pc.depthIn / 144).toFixed(1)} sq ft</td>
                    <td className="px-2 py-1 text-right tabular-nums">{pc.cabinetCount}</td>
                  </tr>
                ))}
                <tr className="border-t-2 border-[var(--leon-line)] font-bold">
                  <td className="px-2 py-1">{plan.pieces.length} piece(s)</td>
                  <td colSpan="2"></td>
                  <td className="px-2 py-1 text-right tabular-nums">{total.toFixed(1)} sq ft</td>
                  <td></td>
                </tr>
              </tbody>
            </table>
          </div>
        )}

        <p className="text-[11px] text-[var(--leon-black)]/50">
          A wall on this room carries a <b>length, not an angle</b>, so the walls are taken to turn 90&deg; in
          the order they are listed &mdash; which is what makes an L come out as an L. If the room is not
          square, drag the corners in LEON Countertop; the wizard will not invent a geometry it was not given.
          The wall side of each piece is marked as splash and the room side as finished edge. Re-running
          replaces only what the wizard generated; a counter drawn by hand there is left alone.
        </p>

        {done && (
          <div className="rounded-md border border-[#8fbf8f] bg-[#f2f9f2] px-3 py-2 text-sm">
            {done.made} created, {done.replaced} updated in LEON Countertop.
          </div>
        )}

        <div className="flex justify-end gap-2">
          <Button variant="ghost" onClick={onClose}>Close</Button>
          <Button disabled={!editable || !plan.pieces.length}
            onClick={() => setDone(cwWriteCounters(ctx, project, room, plan))}>
            {done ? 'Generate again' : `Generate ${plan.pieces.length} countertop piece(s)`}
          </Button>
        </div>
      </div>
    </Modal>
  );
}

// Crown, light rail, toe kick and scribe as RUNS, the way 2020 generates them,
// rather than as components somebody counts off the elevation. The length is
// never typed — it is the run's own cabinet extent, so a cabinet added tomorrow
// lengthens the crown with it.
function CwTrimRunPanel({ ctx, project, run, layout, system, editable, mutate }) {
  const con = cwConstruction(project, null);
  const kinds = CW_TRIM_KINDS.filter(k => k.tiers.indexOf(run.tier) >= 0);
  const runsOut = cwTrimRuns(project, layout, run, con);
  if (!kinds.length) return null;

  function setTrim(key, fields, line) {
    mutate(r => {
      r.trims = Object.assign({}, r.trims || {});
      r.trims[key] = Object.assign({}, r.trims[key] || {}, fields);
    }, line);
  }

  return (
    <div className="rounded-lg border border-[var(--leon-line)] p-2">
      <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/50 mb-1.5">
        Trim along this run
      </div>
      <div className="flex flex-wrap gap-x-5 gap-y-2">
        {kinds.map(k => {
          const t = (run.trims || {})[k.key] || {};
          const made = runsOut.find(x => x.key === k.key);
          return (
            <div key={k.key} className="min-w-[13rem]">
              <label className="flex items-center gap-1.5 text-sm" title={k.note}>
                <input type="checkbox" checked={!!t.on} disabled={!editable}
                  onChange={e => setTrim(k.key, { on: e.target.checked },
                    `LEON Casework — ${k.label.toLowerCase()} ${e.target.checked ? 'added to' : 'removed from'} ${run.name}.`)} />
                <span className={t.on ? 'font-semibold' : ''}>{k.label}</span>
              </label>
              {t.on && (
                <div className="mt-1 pl-5 space-y-1">
                  <TextInput className="!py-0.5 !text-xs" placeholder="Profile / product"
                    disabled={!editable} defaultValue={t.profile || ''}
                    onBlur={e => setTrim(k.key, { profile: e.target.value }, null)} />
                  {made && (
                    <div className="text-[11px] text-[var(--leon-black)]/60 tabular-nums">
                      <b>{fmtDim(made.lengthIn, system, { inchesOnly: true })}</b>
                      {' · '}{made.sticks} stick{made.sticks === 1 ? '' : 's'} of {fmtDim(made.stickIn, system, { inchesOnly: true })}
                      <div className="text-[10px] text-[var(--leon-black)]/40">{made.math}</div>
                    </div>
                  )}
                </div>
              )}
            </div>
          );
        })}
      </div>
      {!runsOut.length && (
        <p className="text-[11px] text-[var(--leon-black)]/45 mt-1">
          Nothing ticked. Trim is measured from the run itself, so it never needs a length typed in.
        </p>
      )}
    </div>
  );
}

function CwRunDesigner({ ctx, project, system, editable, room, onRoom }) {
  const rooms = cwRooms(project);
  const current = (room && rooms.find(r => r.id === room.id)) || rooms[0] || null;
  const [wallId, setWallId] = useState('');
  const [asBuilt, setAsBuilt] = useState(true);
  const [counterWiz, setCounterWiz] = useState(false);
  useEffect(() => { setWallId(''); }, [current && current.id]);

  if (!rooms.length) return <EmptyState text="No rooms on this project yet. Add one under Rooms & Walls first — a run has to sit on a wall of a known length." />;
  if (!current) return <EmptyState text="Pick a room." />;

  const host = cwRunHost(project, current);
  // What the unit is actually built to: mirroring applied, field measurements
  // applied. The editor below still writes to the master.
  const effType = cwEffectiveType(project, current);
  const showWalls = asBuilt && effType ? effType.walls : host.walls;
  const showRuns = asBuilt && effType ? effType.runs : host.runs;
  const wall = showWalls.find(w => w.id === wallId) || showWalls[0] || null;
  const runs = showRuns.filter(r => wall && r.wallId === wall.id);

  function addRun() {
    if (!wall) return;
    cwMutateRuns(ctx, project, current, list => {
      list.push(cwMakeRun({ wallId: wall.id, name: `Run ${list.length + 1}`, tier: 'Base' }));
    }, `LEON Casework — run added on ${wall.name}.`);
  }

  return (
    <div className="space-y-4">
      <div className="flex items-end gap-2 flex-wrap">
        <Field label="Room / unit">
          <Select className="!w-56" value={current.id} onChange={e => onRoom(rooms.find(r => r.id === e.target.value))}>
            {rooms.map(r => <option key={r.id} value={r.id}>{r.name}{r.unit ? ` — ${r.unit}` : ''}</option>)}
          </Select>
        </Field>
        <Field label="Wall">
          <Select className="!w-44" value={wall ? wall.id : ''} onChange={e => setWallId(e.target.value)}>
            {showWalls.map(w => <option key={w.id} value={w.id}>{w.name}</option>)}
          </Select>
        </Field>
        {current.mirrored && (
          <label className="flex items-center gap-1.5 text-xs pb-2">
            <input type="checkbox" checked={asBuilt} onChange={e => setAsBuilt(e.target.checked)} />
            Show as built in this unit (mirrored)
          </label>
        )}
        <div className="ml-auto flex items-end gap-2">
          <Button size="sm" variant="outline" onClick={() => setCounterWiz(true)}
            title="Generate the worktop over the base cabinets, into LEON Countertop">
            🪨 Generate countertops
          </Button>
          {editable && wall && <Button size="sm" onClick={addRun}>+ Add run on {wall.name}</Button>}
        </div>
      </div>

      {counterWiz && (
        <CwCounterWizard ctx={ctx} project={project} room={current} system={system}
          editable={editable} onClose={() => setCounterWiz(false)} />
      )}

      {host.kind === 'type' && (
        <div className="rounded-lg border border-[var(--leon-brown)]/40 bg-[var(--leon-cream)] p-3 text-sm">
          These runs belong to <b>{host.ct.code} — {host.ct.name}</b>, revision {host.ct.revision}, which
          {' '}{cwRooms(project).filter(r => r.caseworkTypeId === host.ct.id).length} room(s) are built to.
          Editing here changes the <b>master</b>. Nothing reaches the units until you run the impact review
          under <b>Casework Types</b> and apply it.
          {current.mirrored && asBuilt && <span className="block mt-1 text-[var(--leon-black)]/60">
            You are looking at the mirrored version. Edits still write to the master, un-mirrored.</span>}
        </div>
      )}

      {!wall && <EmptyState text="This room has no walls yet. Add one under Rooms & Walls." />}
      {wall && !runs.length && <EmptyState text={`No runs on ${wall.name} yet.`} />}

      {wall && runs.map(run => (
        <CwRunCard key={run.id} ctx={ctx} project={project} room={current} wall={wall} run={run}
          system={system} editable={editable && !(asBuilt && current.mirrored)} />
      ))}
      {wall && asBuilt && current.mirrored && (
        <p className="text-[11px] text-[var(--leon-black)]/50">
          Editing is off while the mirrored view is on, because a change made here would read as a change to
          this unit and would in fact change all of them. Untick “show as built” to edit the master.
        </p>
      )}
    </div>
  );
}

function CwRunCard({ ctx, project, room, wall, run, system, editable }) {
  const layout = cwRunLayout(project, ctx, room, wall, run);
  const types = cwAllTypes(project);
  const [adding, setAdding] = useState(false);

  function mutate(fn, action) {
    cwMutateRuns(ctx, project, room, list => {
      const r = list.find(x => x.id === run.id);
      if (r) fn(r);
    }, action);
  }
  function addMember(kind, cabTypeId) {
    mutate(r => {
      if (!Array.isArray(r.members)) r.members = [];
      const t = cabTypeId ? cwType(project, cabTypeId) : null;
      r.members.push(cwMakeMember({
        kind, cabTypeId: cabTypeId || null,
        width: kind === 'cabinet' ? null : kind === 'filler' ? cwIn(3) : kind === 'panel' ? cwIn(0.75) : cwIn(24),
        openingW: kind === 'appliance' ? cwIn(24) : null,
        openingH: kind === 'appliance' ? cwIn(34.5) : null,
        label: t ? t.name : '',
      }));
    }, `LEON Casework — ${kind} added to ${run.name}.`);
    setAdding(false);
  }
  function updMember(mid, fields) {
    mutate(r => { const m = (r.members || []).find(x => x.id === mid); if (m) Object.assign(m, fields); }, null);
  }
  function moveMember(mid, dir) {
    mutate(r => {
      const i = (r.members || []).findIndex(x => x.id === mid);
      const j = i + dir;
      if (i < 0 || j < 0 || j >= r.members.length) return;
      const tmp = r.members[i]; r.members[i] = r.members[j]; r.members[j] = tmp;
    }, `LEON Casework — ${run.name} order changed.`);
  }
  function removeMember(mid) {
    mutate(r => { r.members = (r.members || []).filter(x => x.id !== mid); }, `LEON Casework — member removed from ${run.name}.`);
  }
  // Countertop coordination: the run already knows its length and its depth, so
  // LEON Stone should not be asked to re-measure it. The piece is created there,
  // in that module's own cut list — this never keeps a second copy.
  function sendToStone() {
    if (typeof stoneMakePiece !== 'function') { alert('LEON Stone is not loaded in this session.'); return; }
    const baseCabs = layout.members.filter(m => m.member.kind === 'cabinet' && m.type && m.type.category !== 'Wall');
    if (!baseCabs.length) { alert('This run has no base cabinets, so there is no countertop to send.'); return; }
    const depth = Math.max.apply(null, baseCabs.map(m => cwNum(m.type.depth))) + cwIn(1.5);   // overhang
    ctx.updateProject(project.id, draft => {
      if (!Array.isArray(draft.stoneCutList)) draft.stoneCutList = [];
      draft.stoneCutList.push(stoneMakePiece({
        scopeId: room.scopeId || null,
        label: `${room.name} — ${wall.name} ${run.name}`,
        lengthMm: cwRound(layout.used), widthMm: cwRound(depth), qty: 1,
        notes: `From LEON Casework. Run length ${cwN(layout.used)} over ${baseCabs.length} base cabinet(s); depth is the deepest box ${cwN(depth - cwIn(1.5))} plus ${cwN(cwIn(1.5))} overhang. Re-send after any run change.`,
      }, ctx.currentUserName));
      ctx.logAction(draft, `LEON Casework — countertop piece sent to LEON Stone for ${room.name} ${wall.name}.`);
    });
  }

  const remainTone = layout.over ? 'text-[#b83b3b]' : layout.remaining < cwIn(3) && layout.remaining > 0 ? 'text-[#a67b1f]' : 'text-[#3a7d44]';

  return (
    <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3 space-y-3">
      <div className="flex items-center gap-2 flex-wrap">
        <input value={run.name} disabled={!editable} onChange={e => mutate(r => { r.name = e.target.value; }, null)}
          className="px-1 py-0.5 font-bold bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded w-40" />
        <Select className="!w-28" value={run.tier} disabled={!editable}
          onChange={e => mutate(r => { r.tier = e.target.value; r.zBottom = e.target.value === 'Wall' ? cwIn(54) : 0; }, null)}>
          {CW_TIERS.map(t => <option key={t}>{t}</option>)}
        </Select>
        <Field label="Starts at"><CwDimField value={run.startOffset} system={system} disabled={!editable} w="w-20"
          onChange={v => mutate(r => { r.startOffset = cwNum(v); }, null)} /></Field>
        <Field label="Bottom off floor"><CwDimField value={run.zBottom} system={system} disabled={!editable} w="w-20"
          onChange={v => mutate(r => { r.zBottom = cwNum(v); }, null)} /></Field>
        <div className="ml-auto flex gap-2">
          <Button size="sm" variant="ghost" onClick={sendToStone}>Send countertop to LEON Stone</Button>
          {editable && <Button size="sm" onClick={() => setAdding(true)}>+ Add to run</Button>}
        </div>
      </div>

      {/* The single most useful line in the module. */}
      <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)] px-3 py-2 flex items-center gap-6 flex-wrap text-sm">
        <span><span className="text-[var(--leon-black)]/50">Wall</span> <b className="tabular-nums">{fmtDim(layout.wallLen, system, { inchesOnly: true })}</b>
          {layout.measured && <span className="text-[10px] uppercase ml-1 text-[#3a7d44]">measured</span>}</span>
        <span><span className="text-[var(--leon-black)]/50">Used</span> <b className="tabular-nums">{fmtDim(layout.used, system, { inchesOnly: true })}</b></span>
        {cwNum(run.startOffset) > 0 && <span><span className="text-[var(--leon-black)]/50">Offset</span> <b className="tabular-nums">{fmtDim(run.startOffset, system, { inchesOnly: true })}</b></span>}
        <span className={`font-bold tabular-nums ${remainTone}`}>
          {layout.over ? `OVER BY ${fmtDim(-layout.remaining, system, { inchesOnly: true })}` : `${fmtDim(layout.remaining, system, { inchesOnly: true })} left`}
        </span>
        <span className="text-[11px] text-[var(--leon-black)]/45 tabular-nums">
          {cwN(layout.wallLen)} − {cwN(run.startOffset)} − {cwN(layout.used)} = {cwN(layout.remaining)} mm
        </span>
      </div>

      <div className="bg-[var(--leon-cream)]/50 rounded p-2">
        <CwRunElevation project={project} layout={layout} wall={wall} run={run} system={system} height={230} />
      </div>

      <CwTrimRunPanel ctx={ctx} project={project} run={run} layout={layout}
        system={system} editable={editable} mutate={mutate} />

      <div className="overflow-x-auto">
        <table className="w-full text-xs min-w-[820px]">
          <thead>
            <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
              <th className="px-2 py-1.5 w-8">#</th><th className="px-2 py-1.5">What</th><th className="px-2 py-1.5">Cabinet type</th>
              <th className="px-2 py-1.5">Label</th><th className="px-2 py-1.5">Width</th><th className="px-2 py-1.5">Height</th>
              <th className="px-2 py-1.5">Handing</th><th className="px-2 py-1.5">From the left</th><th className="px-2 py-1.5 w-20"></th>
            </tr>
          </thead>
          <tbody>
            {layout.members.map((mm, i) => {
              const m = mm.member;
              return (
                <tr key={m.id} className="border-b border-[var(--leon-line)]/60">
                  <td className="px-2 py-1 text-[var(--leon-black)]/40">{i + 1}</td>
                  <td className="px-2 py-1">
                    <select value={m.kind} disabled={!editable} onChange={e => updMember(m.id, { kind: e.target.value })}
                      className="px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                      {CW_MEMBER_KINDS.map(k => <option key={k.key} value={k.key}>{k.label}</option>)}
                    </select>
                  </td>
                  <td className="px-2 py-1">
                    {m.kind === 'cabinet' ? (
                      <select value={m.cabTypeId || ''} disabled={!editable} onChange={e => updMember(m.id, { cabTypeId: e.target.value || null })}
                        className="w-40 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                        <option value="">— pick a type —</option>
                        {types.map(t => <option key={t.id} value={t.id}>{t.code} · {t.name}</option>)}
                      </select>
                    ) : <span className="text-[var(--leon-black)]/30">—</span>}
                  </td>
                  <td className="px-2 py-1">
                    <input value={m.label} disabled={!editable} onChange={e => updMember(m.id, { label: e.target.value })}
                      className="w-32 px-1 py-0.5 bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                  </td>
                  <td className="px-2 py-1">
                    <CwDimField value={m.width} system={system} disabled={!editable} w="w-20"
                      placeholder={m.kind === 'cabinet' && mm.type ? fmtDim(mm.type.width, system, { inchesOnly: true })
                        : m.kind === 'appliance' ? fmtDim(mm.w, system, { inchesOnly: true }) : ''}
                      onChange={v => updMember(m.id, { width: v })} />
                  </td>
                  <td className="px-2 py-1">
                    <CwDimField value={m.height} system={system} disabled={!editable} w="w-20"
                      placeholder={mm.type ? fmtDim(mm.type.height, system, { inchesOnly: true }) : ''}
                      onChange={v => updMember(m.id, { height: v })} />
                  </td>
                  <td className="px-2 py-1">
                    <select value={m.handing} disabled={!editable} onChange={e => updMember(m.id, { handing: e.target.value })}
                      className="px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                      <option value="Auto">Auto</option><option value="L">L</option><option value="R">R</option>
                    </select>
                  </td>
                  <td className="px-2 py-1 tabular-nums text-[var(--leon-black)]/55">{fmtDim(mm.x, system, { inchesOnly: true })}</td>
                  <td className="px-2 py-1 whitespace-nowrap">
                    {editable && <>
                      <button onClick={() => moveMember(m.id, -1)} title="Move left" className="text-[var(--leon-brown)]">↑</button>
                      <button onClick={() => moveMember(m.id, 1)} title="Move right" className="ml-1 text-[var(--leon-brown)]">↓</button>
                      <button onClick={() => removeMember(m.id)} title="Remove" className="ml-1.5 text-red-600">✕</button>
                    </>}
                  </td>
                </tr>
              );
            })}
            {!layout.members.length && <tr><td colSpan={9} className="px-3 py-5 text-center text-[var(--leon-black)]/40">Nothing in this run yet.</td></tr>}
          </tbody>
        </table>
      </div>

      {/* Appliance openings, spelled out where they are set rather than buried. */}
      {layout.members.filter(m => m.member.kind === 'appliance').map(mm => (
        <CwApplianceRow key={mm.member.id} ctx={ctx} member={mm.member} system={system} editable={editable}
          onChange={f => updMember(mm.member.id, f)} />
      ))}

      <CwIssueList issues={layout.issues} />

      {editable && (
        <button onClick={() => { if (confirm(`Remove ${run.name}?`)) cwMutateRuns(ctx, project, room, list => {
          const i = list.findIndex(x => x.id === run.id); if (i >= 0) list.splice(i, 1);
        }, `LEON Casework — run "${run.name}" removed.`); }} className="text-xs font-semibold text-red-600">Remove this run</button>
      )}

      <Modal open={adding} onClose={() => setAdding(false)} wide title={`Add to ${run.name}`}
        footer={<Button variant="ghost" onClick={() => setAdding(false)}>Close</Button>}>
        <div className="space-y-3">
          <p className="text-sm text-[var(--leon-black)]/60">
            Whatever you add goes on the right-hand end of the run and can be moved from there.
          </p>
          <div className="flex gap-2 flex-wrap">
            {CW_MEMBER_KINDS.filter(k => k.key !== 'cabinet').map(k => (
              <Button key={k.key} size="sm" variant="outline" onClick={() => addMember(k.key)}>{k.label}</Button>
            ))}
          </div>
          <div className="text-xs font-semibold uppercase tracking-wide text-[var(--leon-black)]/45 pt-2">Cabinet modules</div>
          <div className="grid gap-2 sm:grid-cols-2 max-h-[380px] overflow-y-auto">
            {types.map(t => (
              <button key={t.id} onClick={() => addMember('cabinet', t.id)}
                className="rounded border border-[var(--leon-line)] p-2 text-left hover:border-[var(--leon-brown)]">
                <div className="text-xs font-bold">{t.code} · {t.name}</div>
                <div className="text-[10px] text-[var(--leon-black)]/50">
                  {fmtDim(t.width, system, { inchesOnly: true })} × {fmtDim(t.height, system, { inchesOnly: true })} × {fmtDim(t.depth, system, { inchesOnly: true })}
                  {t.global ? ' · library' : ' · project'}
                </div>
              </button>
            ))}
          </div>
        </div>
      </Modal>
    </div>
  );
}

// An appliance or fixture in a run references the app's OWN library — the same
// record the Shop Drawing Hub files spec sheets against. A second list of
// appliances here would be a second answer to the same question.
function CwApplianceRow({ ctx, member, system, editable, onChange }) {
  const isFixture = member.specKind === 'fixture';
  const lib = (isFixture ? ctx.fixtureLibrary : ctx.applianceLibrary) || [];
  return (
    <div className="rounded border border-[var(--leon-line)] bg-[var(--leon-cream)]/50 p-2.5 flex items-end gap-3 flex-wrap text-xs">
      <span className="font-semibold">{member.label || 'Appliance'}</span>
      <Field label="Kind">
        <Select className="!w-28 !text-xs" value={isFixture ? 'fixture' : 'appliance'} disabled={!editable}
          onChange={e => onChange({ specKind: e.target.value, specId: null })}>
          <option value="appliance">Appliance</option><option value="fixture">Fixture</option>
        </Select>
      </Field>
      <Field label="Specification">
        <Select className="!w-56 !text-xs" value={member.specId || ''} disabled={!editable}
          onChange={e => onChange({ specId: e.target.value || null })}>
          <option value="">— not linked —</option>
          {lib.filter(s => s.active !== false).map(s => (
            <option key={s.id} value={s.id}>
              {(isFixture ? s.fixtureType : s.applianceType)} — {s.manufacturer} {s.model || s.modelNumber}
            </option>
          ))}
        </Select>
      </Field>
      {/* THE TEMPLATE, then how far its numbers can be trusted. Picking one
          seeds the opening from the library and sets the status the workbook
          gives it — which for most of them is "the model decides", not a size
          that can be read off the nominal. */}
      <Field label="From the library">
        <Select className="!w-56 !text-xs" value={member.libTemplate || ''} disabled={!editable}
          onChange={e => {
            const t = (typeof CW_LIB_APPLIANCES !== 'undefined' ? CW_LIB_APPLIANCES : [])
              .find(x => `${x.category}|${x.subtype}|${x.nominal}` === e.target.value) || null;
            if (!t) { onChange({ libTemplate: '' }); return; }
            const n = Number(String(t.cutW).match(/[\d.]+/) ? String(t.cutW).match(/[\d.]+/)[0] : 0);
            onChange({
              libTemplate: e.target.value,
              applianceKind: t.category,
              dataStatus: t.status,
              openingW: n > 0 ? cwIn(n) : member.openingW,
            });
          }}>
          <option value="">— not from a template —</option>
          {(typeof CW_LIB_APPLIANCES !== 'undefined' ? CW_LIB_APPLIANCES : []).map(t => (
            <option key={`${t.category}|${t.subtype}|${t.nominal}`}
              value={`${t.category}|${t.subtype}|${t.nominal}`}>
              {t.category} {t.nominal}&Prime; — {t.status}
            </option>
          ))}
        </Select>
      </Field>
      <Field label="Data status"
        hint={typeof CW_DATA_STATUS_NOTE !== 'undefined' ? CW_DATA_STATUS_NOTE[member.dataStatus || 'Generic'] : ''}>
        <Select className="!w-44 !text-xs" value={member.dataStatus || 'Generic'} disabled={!editable}
          onChange={e => onChange({ dataStatus: e.target.value })}>
          {(typeof CW_DATA_STATUSES !== 'undefined' ? CW_DATA_STATUSES
            : ['Generic', 'Model Selected', 'Production Verified']).map(x => <option key={x}>{x}</option>)}
        </Select>
      </Field>
      <Field label="Opening W"><CwDimField value={member.openingW} system={system} disabled={!editable} w="w-20" onChange={v => onChange({ openingW: v })} /></Field>
      <Field label="Opening H"><CwDimField value={member.openingH} system={system} disabled={!editable} w="w-20" onChange={v => onChange({ openingH: v })} /></Field>
      <Field label="Clearance each side"><CwDimField value={member.clearanceEach} system={system} disabled={!editable} w="w-20" onChange={v => onChange({ clearanceEach: v })} /></Field>
      {!lib.length && <span className="text-[11px] text-[var(--leon-black)]/45">
        Nothing in the {isFixture ? 'fixture' : 'appliance'} library yet — add it there and it appears here.
      </span>}
    </div>
  );
}

// ============================================================================
// CASEWORK TYPES — designed once, assigned to forty units
// ----------------------------------------------------------------------------
// The impact review is the whole feature. Anyone can re-issue a drawing; the
// question that decides whether it is safe is "how many of these are already
// cut?", and it has to be answered BEFORE the change propagates, not in a
// change log afterwards.
// ============================================================================
function CwCaseworkTypesPanel({ ctx, project, system, editable }) {
  const cts = cwCaseworkTypes(project);
  const [impactOn, setImpactOn] = useState(null);

  function addType() {
    cwUpdate(ctx, project, draft => {
      const n = draft.caseworkRoomTypes.length;
      draft.caseworkRoomTypes.push(cwMakeCaseworkType({
        code: `K-${String.fromCharCode(65 + n)}`, name: `Kitchen Type ${String.fromCharCode(65 + n)}`,
      }, ctx.currentUserName));
    }, 'LEON Casework — casework type added.');
  }
  function upd(id, fields) {
    cwUpdate(ctx, project, draft => {
      const t = draft.caseworkRoomTypes.find(x => x.id === id);
      if (t) Object.assign(t, fields);
    }, null);
  }
  function addWall(id) {
    cwUpdate(ctx, project, draft => {
      const t = draft.caseworkRoomTypes.find(x => x.id === id);
      if (!t) return;
      if (!Array.isArray(t.walls)) t.walls = [];
      t.walls.push(cwMakeWall({ name: `Wall ${String.fromCharCode(65 + t.walls.length)}` }));
    }, 'LEON Casework — wall added to a casework type.');
  }

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-3 flex-wrap">
        <div>
          <h3 className="font-bold">Casework Types</h3>
          <p className="text-sm text-[var(--leon-black)]/55 max-w-2xl">
            A whole room, designed once. Point forty units at <b>K-A</b> and they share one design while each
            keeps its own field measurement, production status and overrides. <b>Mirror</b> is set on the unit,
            not here, because half a floor plate is usually the mirror of the other half.
          </p>
        </div>
        {editable && <Button size="sm" onClick={addType}>+ Add casework type</Button>}
      </div>
      {!cts.length && <EmptyState text="No casework types yet. Add one, give it walls, then lay its runs out in the Run Designer." />}

      {cts.map(ct => {
        const rooms = cwRooms(project).filter(r => r.caseworkTypeId === ct.id);
        const cabs = cwItems(project).filter(c => rooms.some(r => r.id === c.roomId));
        const produced = cabs.filter(c => CW_PRODUCED_STATUSES.indexOf(c.status) >= 0).length;
        return (
          <Collapsible key={ct.id} id={`cwct-${ct.id}`} title={`${ct.code} — ${ct.name}`}
            count={rooms.length} right={<span className="flex gap-1">
              <Badge tone="brown">rev {ct.revision}</Badge>
              {produced > 0 && <Badge tone="red">{produced} cut</Badge>}
            </span>}>
            <div className="space-y-3">
              <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
                <Field label="Code"><TextInput value={ct.code} disabled={!editable} onChange={e => upd(ct.id, { code: e.target.value })} /></Field>
                <Field label="Name"><TextInput value={ct.name} disabled={!editable} onChange={e => upd(ct.id, { name: e.target.value })} /></Field>
                <Field label="Room kind">
                  <Select value={ct.kind} disabled={!editable} onChange={e => upd(ct.id, { kind: e.target.value })}>
                    {CW_ROOM_KINDS.map(k => <option key={k}>{k}</option>)}
                  </Select>
                </Field>
                <Field label="Ceiling height">
                  <CwDimField value={ct.ceilingHeight} system={system} disabled={!editable} onChange={v => upd(ct.id, { ceilingHeight: v })} />
                </Field>
              </div>

              <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
                <div className="text-sm font-semibold mb-1.5">Walls in this type</div>
                <div className="flex flex-wrap gap-2">
                  {(ct.walls || []).map(w => (
                    <span key={w.id} className="rounded border border-[var(--leon-line)] px-2 py-1 text-xs">
                      <b>{w.name}</b> {fmtDim(w.length, system, { inchesOnly: true })} × {fmtDim(w.height, system, { inchesOnly: true })}
                      {' '}· {(ct.runs || []).filter(r => r.wallId === w.id).length} run(s)
                      {!!(w.obstructions || []).length && ` · ${w.obstructions.length} obstruction(s)`}
                    </span>
                  ))}
                  {!(ct.walls || []).length && <span className="text-xs text-[var(--leon-black)]/40">None yet.</span>}
                  {editable && <Button size="sm" variant="ghost" onClick={() => addWall(ct.id)}>+ Add wall</Button>}
                </div>
                <p className="text-[11px] text-[var(--leon-black)]/45 mt-2">
                  Wall lengths and obstructions on a type are edited on any room that uses it, under Rooms &amp; Walls.
                  Runs are laid out in the Run Designer.
                </p>
              </div>

              <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
                <div className="flex items-center justify-between gap-2 flex-wrap mb-2">
                  <div className="text-sm font-semibold">Units built to {ct.code} ({rooms.length})</div>
                  <Button size="sm" onClick={() => setImpactOn(ct.id)}>Review impact before applying</Button>
                </div>
                {!rooms.length && <div className="text-xs text-[var(--leon-black)]/40">No rooms point at this type yet. Assign one under Rooms &amp; Walls.</div>}
                <div className="flex flex-wrap gap-1.5">
                  {rooms.map(r => {
                    const rc = cwItems(project).filter(c => c.roomId === r.id);
                    const prod = rc.filter(c => CW_PRODUCED_STATUSES.indexOf(c.status) >= 0).length;
                    return (
                      <span key={r.id} className={`rounded-full border px-2.5 py-1 text-[11px] ${prod ? 'border-[#b83b3b]/40 bg-red-50' : 'border-[var(--leon-line)]'}`}>
                        {r.unit || r.name}{r.mirrored ? ' ⇄' : ''} · {rc.length} cab{prod ? ` · ${prod} cut` : ''}
                        {r.appliedRevision != null && r.appliedRevision !== ct.revision && <b className="ml-1 text-[#a67b1f]">rev {r.appliedRevision}</b>}
                      </span>
                    );
                  })}
                </div>
              </div>

              {editable && (
                <button onClick={() => { if (confirm(`Remove ${ct.code}? Rooms pointing at it become one-offs and keep their cabinets.`)) {
                  cwUpdate(ctx, project, draft => {
                    draft.caseworkRoomTypes = draft.caseworkRoomTypes.filter(x => x.id !== ct.id);
                    draft.caseworkRooms.forEach(r => { if (r.caseworkTypeId === ct.id) r.caseworkTypeId = null; });
                  }, `LEON Casework — casework type ${ct.code} removed.`);
                } }} className="text-xs font-semibold text-red-600">Remove this casework type</button>
              )}
            </div>
          </Collapsible>
        );
      })}

      <CwImpactModal ctx={ctx} project={project} system={system} editable={editable}
        caseworkTypeId={impactOn} onClose={() => setImpactOn(null)} />
    </div>
  );
}

// Apply a Casework Type to its rooms. The projection this writes from is the
// SAME function the review reads, so what was previewed is what happens.
function cwApplyCaseworkType(ctx, project, caseworkTypeId) {
  const impact = cwTypeImpact(project, caseworkTypeId);
  cwUpdate(ctx, project, draft => {
    const ct = draft.caseworkRoomTypes.find(x => x.id === caseworkTypeId);
    if (!ct) return;
    const rooms = draft.caseworkRooms.filter(r => r.caseworkTypeId === caseworkTypeId);
    rooms.forEach(room => {
      const projected = cwProjectedCabinets(project, room);
      const keys = projected.map(p => p.memberKey);
      projected.forEach(p => {
        const cur = draft.caseworkItems.find(c => c.roomId === room.id && c.memberKey === p.memberKey);
        if (!cur) {
          draft.caseworkItems.push(cwMakeCabinet({
            mark: p.mark, typeId: p.typeId, roomId: room.id, runId: p.runId, memberKey: p.memberKey,
            scopeId: room.scopeId || null, room: room.name, unit: room.unit, level: room.level,
            // A dimension is stamped on the cabinet ONLY when the run member set
            // one. Otherwise it stays null and keeps inheriting its cabinet type,
            // which is what makes a later change to that type still reach it.
            width: p.widthSet ? p.width : null,
            height: p.heightSet ? p.height : null,
            depth: p.depthSet ? p.depth : null,
            handing: p.handing,
          }, ctx.currentUserName));
        } else {
          cur.typeId = p.typeId;
          cur.width = p.widthSet ? p.width : null;
          cur.height = p.heightSet ? p.height : null;
          cur.depth = p.depthSet ? p.depth : null;
          cur.handing = p.handing;
          cur.room = room.name; cur.unit = room.unit; cur.level = room.level;
          cur.revision = cwNum(cur.revision) + 1;
          cur.history = (cur.history || []).concat([{
            id: uid('cwh'), date: todayISO(), by: ctx.currentUserName,
            action: `Re-issued from ${ct.code} rev ${ct.revision}.`,
          }]);
        }
      });
      // A cabinet no longer in the type: removed if it has not been cut, KEPT and
      // flagged if it has. Deleting a part that is already on a truck is not a
      // drawing change and this module will not pretend it is.
      draft.caseworkItems.forEach(c => {
        if (c.roomId !== room.id || !c.memberKey || keys.indexOf(c.memberKey) >= 0) return;
        if (CW_PRODUCED_STATUSES.indexOf(c.status) >= 0) {
          c.notes = `${c.notes ? c.notes + ' ' : ''}[${todayISO()}] Dropped from ${ct.code} rev ${ct.revision} but already ${c.status.toLowerCase()} — resolve on site.`;
        } else {
          c.__drop = true;
        }
      });
      draft.caseworkItems = draft.caseworkItems.filter(c => !c.__drop);
      room.appliedRevision = ct.revision;
    });
    ctx.logAction(draft, `LEON Casework — ${ct.code} rev ${ct.revision} applied to ${rooms.length} unit(s): ${impact.changed} cabinet(s) changed, ${impact.added} added, ${impact.removed} removed${impact.producedTouched ? `, ${impact.producedTouched} already produced` : ''}.`);
  });
}

function CwImpactModal({ ctx, project, system, editable, caseworkTypeId, onClose }) {
  if (!caseworkTypeId) return null;
  const ct = cwCaseworkTypes(project).find(x => x.id === caseworkTypeId);
  if (!ct) return null;
  const impact = cwTypeImpact(project, caseworkTypeId);
  const nothing = !impact.changed && !impact.added && !impact.removed;
  return (
    <Modal open wide onClose={onClose} title={`Impact of applying ${ct.code} rev ${ct.revision}`}
      footer={<>
        <Button variant="ghost" onClick={onClose}>Cancel</Button>
        <Button disabled={!editable || nothing}
          onClick={() => { cwApplyCaseworkType(ctx, project, caseworkTypeId); onClose(); }}>
          {nothing ? 'Nothing to apply' : `Apply to ${impact.touched} unit${impact.touched === 1 ? '' : 's'}`}
        </Button>
      </>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
          <CwStat label="Units affected" value={impact.touched} />
          <CwStat label="Cabinets changed" value={impact.changed + impact.added + impact.removed} />
          <CwStat label="Already produced" value={impact.producedTouched} tone={impact.producedTouched ? 'red' : undefined} />
          <CwStat label="Already installed" value={impact.installedTouched} tone={impact.installedTouched ? 'red' : undefined} />
        </div>
        {impact.producedTouched > 0 && (
          <div className="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800">
            <b>{impact.producedTouched} cabinet(s) in this change have already been cut or shipped.</b> Applying
            re-issues the drawing; it does not un-cut the parts. Anything already installed stays and is flagged
            in its notes rather than silently deleted.
          </div>
        )}
        {nothing && <div className="text-sm text-[var(--leon-black)]/55">Every unit already matches this type. Nothing would change.</div>}
        <div className="max-h-[380px] overflow-y-auto space-y-2">
          {impact.units.filter(u => u.changes.length).map(u => (
            <div key={u.room.id} className="rounded border border-[var(--leon-line)] p-2.5">
              <div className="text-sm font-bold mb-1">
                {u.room.unit || u.room.name}{u.room.mirrored ? ' ⇄ mirrored' : ''}
                <span className="ml-2 text-[11px] font-normal text-[var(--leon-black)]/50">
                  {u.total} cabinet(s){u.produced ? `, ${u.produced} already produced` : ''}
                </span>
              </div>
              <ul className="text-xs space-y-0.5">
                {u.changes.map((c, i) => (
                  <li key={i} className={c.kind === 'remove' ? 'text-red-700' : c.kind === 'add' ? 'text-[#3a7d44]' : ''}>
                    <b>{c.mark}</b> — {c.kind === 'add' ? 'added' : c.kind === 'remove' ? 'removed' : c.detail}
                    {c.status && CW_PRODUCED_STATUSES.indexOf(c.status) >= 0 && <span className="ml-1 uppercase text-[10px] text-red-700">{c.status}</span>}
                  </li>
                ))}
              </ul>
            </div>
          ))}
        </div>
      </div>
    </Modal>
  );
}

// ============================================================================
// CABINET SCHEDULE
// ----------------------------------------------------------------------------
// The schedule is not a report of the cabinets — it IS the cabinets, in a
// table. Editing a cell writes the record the designer edits and the cut list
// reads, which is the only way the three can never disagree.
// ============================================================================
function CwSchedule({ ctx, project, system, editable, onEdit }) {
  const [q, setQ] = useState('');
  const [sel, setSel] = useState([]);
  const [bulk, setBulk] = useState(false);
  const items = cwItems(project);
  const rooms = cwRooms(project);
  const types = cwAllTypes(project);

  const rows = items.map(c => {
    const built = cwComputeParts(project, c);
    return { cab: c, built, res: built.res, room: rooms.find(r => r.id === c.roomId) || null };
  }).filter(r => {
    if (!q.trim()) return true;
    const hay = `${r.cab.mark} ${r.cab.room} ${r.cab.unit} ${r.cab.level} ${r.res.type ? r.res.type.code : ''}`.toLowerCase();
    return hay.indexOf(q.trim().toLowerCase()) >= 0;
  });

  function setCab(id, fields) {
    cwUpdate(ctx, project, draft => {
      const c = draft.caseworkItems.find(x => x.id === id);
      if (!c) return;
      Object.assign(c, fields);
    }, null);
  }
  function addCab() {
    cwUpdate(ctx, project, draft => {
      const n = draft.caseworkItems.length + 1;
      draft.caseworkItems.push(cwMakeCabinet({ mark: `B-${String(n).padStart(2, '0')}`, typeId: 'cwt-b2d' }, ctx.currentUserName));
    }, 'LEON Casework — cabinet added.');
  }
  function removeCab(id) {
    cwUpdate(ctx, project, draft => {
      const c = draft.caseworkItems.find(x => x.id === id);
      draft.caseworkItems = draft.caseworkItems.filter(x => x.id !== id);
      if (c) ctx.logAction(draft, `LEON Casework — cabinet ${c.mark} removed.`);
    });
  }
  const flagged = rows.filter(r => r.built.issues.some(i => i.level === 'error')).length;

  return (
    <div className="space-y-3">
      <div className="flex items-center gap-2 flex-wrap">
        <TextInput className="!w-64" value={q} onChange={e => setQ(e.target.value)} placeholder="Search mark, room, unit…" />
        <span className="text-xs text-[var(--leon-black)]/50">
          {rows.length} cabinet{rows.length === 1 ? '' : 's'} · {rows.reduce((a, r) => a + (cwNum(r.cab.qty) || 1), 0)} to build
          {flagged ? ` · ${flagged} will not build as drawn` : ''}
        </span>
        <div className="ml-auto flex items-center gap-2">
          {!!sel.length && editable && <Button size="sm" variant="ghost" onClick={() => setBulk(true)}>Edit {sel.length} selected</Button>}
          {editable && <Button size="sm" onClick={addCab}>+ Add cabinet</Button>}
        </div>
      </div>

      <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-x-auto">
        <table className="w-full text-xs" style={{ minWidth: 1260 }}>
          <thead>
            <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
              <th className="px-2 py-2 w-8">
                <input type="checkbox" checked={!!rows.length && sel.length === rows.length}
                  onChange={e => setSel(e.target.checked ? rows.map(r => r.cab.id) : [])} />
              </th>
              {['Mark', 'Type', 'Unit', 'Room', 'Qty', 'Width', 'Height', 'Depth', 'Front', 'Fronts',
                'Fillers L/R', 'Panels', 'Hardware', 'Status', ''].map((h, i) => <th key={i} className="px-2 py-2">{h}</th>)}
            </tr>
          </thead>
          <tbody>
            {rows.map(r => {
              const c = r.cab, R = r.res, B = r.built;
              const bad = B.issues.some(i => i.level === 'error');
              const tint = f => cwOwn(R, f) ? 'bg-amber-50' : '';
              const title = f => cwOwn(R, f) ? 'Set on this cabinet, not inherited from its type' : 'Inherited from the cabinet type';
              return (
                <tr key={c.id} className={`border-b border-[var(--leon-line)]/60 ${bad ? 'bg-red-50/40' : ''}`}>
                  <td className="px-2 py-1">
                    <input type="checkbox" checked={sel.indexOf(c.id) >= 0}
                      onChange={e => setSel(e.target.checked ? sel.concat([c.id]) : sel.filter(x => x !== c.id))} />
                  </td>
                  <td className="px-2 py-1">
                    <input value={c.mark} disabled={!editable} onChange={e => setCab(c.id, { mark: e.target.value })}
                      className="w-20 px-1 py-0.5 font-bold bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                  </td>
                  <td className="px-2 py-1">
                    <select value={c.typeId || ''} disabled={!editable} onChange={e => setCab(c.id, { typeId: e.target.value || null })}
                      className="w-32 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                      <option value="">— none —</option>
                      {types.map(t => <option key={t.id} value={t.id}>{t.code} · {t.name}</option>)}
                    </select>
                  </td>
                  <td className="px-2 py-1 text-[var(--leon-black)]/60">{c.unit || (r.room ? r.room.unit : '') || '—'}</td>
                  <td className="px-2 py-1 text-[var(--leon-black)]/60">{c.room || (r.room ? r.room.name : '') || '—'}</td>
                  <td className="px-2 py-1">
                    <input value={c.qty} disabled={!editable} onChange={e => setCab(c.id, { qty: Number(e.target.value) || 1 })}
                      className="w-12 px-1 py-0.5 text-right bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                  </td>
                  {['width', 'height', 'depth'].map(f => (
                    <td key={f} className={`px-2 py-1 ${tint(f)}`} title={title(f)}>
                      <CwDimField value={R[f]} system={system} disabled={!editable} w="w-20" onChange={v => setCab(c.id, { [f]: v })} />
                    </td>
                  ))}
                  <td className={`px-2 py-1 ${tint('frontStyle')}`} title={title('frontStyle')}>
                    <select value={R.frontStyle} disabled={!editable} onChange={e => setCab(c.id, { frontStyle: e.target.value })}
                      className="w-28 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                      {CW_FRONT_STYLES.map(s => <option key={s.key} value={s.key}>{s.label}</option>)}
                    </select>
                  </td>
                  <td className="px-2 py-1 text-[var(--leon-black)]/60 whitespace-nowrap">
                    {B.doorCount ? `${B.doorCount} door` : ''}{B.doorCount && B.drawerCount ? ' · ' : ''}{B.drawerCount ? `${B.drawerCount} drawer` : ''}
                    {!B.doorCount && !B.drawerCount ? 'open' : ''}
                  </td>
                  <td className="px-2 py-1 whitespace-nowrap">
                    <CwDimField value={c.fillerLeft} system={system} disabled={!editable} w="w-14" onChange={v => setCab(c.id, { fillerLeft: cwNum(v) })} />
                    <CwDimField value={c.fillerRight} system={system} disabled={!editable} w="w-14" onChange={v => setCab(c.id, { fillerRight: cwNum(v) })} />
                  </td>
                  <td className="px-2 py-1 whitespace-nowrap text-[11px]">
                    <label className="mr-1"><input type="checkbox" checked={!!c.finishedEndLeft} disabled={!editable}
                      onChange={e => setCab(c.id, { finishedEndLeft: e.target.checked })} /> L</label>
                    <label><input type="checkbox" checked={!!c.finishedEndRight} disabled={!editable}
                      onChange={e => setCab(c.id, { finishedEndRight: e.target.checked })} /> R</label>
                  </td>
                  <td className="px-2 py-1 text-[var(--leon-black)]/60">{B.hardwareSet ? B.hardwareSet.code : '—'}</td>
                  <td className="px-2 py-1">
                    <select value={c.status} disabled={!editable} onChange={e => setCab(c.id, { status: e.target.value })}
                      className="w-28 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                      {CW_STATUSES.map(s => <option key={s}>{s}</option>)}
                    </select>
                  </td>
                  <td className="px-2 py-1 whitespace-nowrap">
                    <button onClick={() => onEdit(c)} title="Open in the designer" className="text-[var(--leon-brown)] font-semibold">✎</button>
                    {editable && <button onClick={() => { if (confirm(`Remove ${c.mark}?`)) removeCab(c.id); }}
                      className="ml-1.5 text-red-600" title="Remove">✕</button>}
                  </td>
                </tr>
              );
            })}
            {!rows.length && <tr><td colSpan={16} className="px-3 py-6 text-center text-[var(--leon-black)]/40">
              No cabinets yet. Lay out a run, or add one here directly.</td></tr>}
          </tbody>
        </table>
      </div>

      {rows.some(r => r.built.issues.length) && (
        <Collapsible id={`cw-issues-${project.id}`} title="What needs attention"
          count={rows.reduce((a, r) => a + r.built.issues.length, 0)}>
          <div className="space-y-2">
            {rows.filter(r => r.built.issues.length).map(r => (
              <div key={r.cab.id}>
                <div className="text-xs font-bold mb-1">{r.cab.mark}</div>
                <CwIssueList issues={r.built.issues} />
              </div>
            ))}
          </div>
        </Collapsible>
      )}

      <CwBulkEdit ctx={ctx} project={project} open={bulk} ids={sel} system={system}
        onClose={() => setBulk(false)} onDone={() => { setBulk(false); setSel([]); }} />
    </div>
  );
}

// The count is shown BEFORE anything is applied. "40 cabinets will be updated"
// is the moment to catch a mistake, not the change log afterwards.
function CwBulkEdit({ ctx, project, open, ids, system, onClose, onDone }) {
  const [fields, setFields] = useState({});
  useEffect(() => { if (open) setFields({}); }, [open]);
  if (!open) return null;
  const s = cwSettings(project);
  const set = (k, v) => setFields(f => {
    const next = Object.assign({}, f);
    if (v === '' || v === null) delete next[k]; else next[k] = v;
    return next;
  });
  const keys = Object.keys(fields);
  function apply() {
    cwUpdate(ctx, project, draft => {
      draft.caseworkItems.forEach(c => { if (ids.indexOf(c.id) >= 0) Object.assign(c, fields); });
    }, `LEON Casework — bulk change on ${ids.length} cabinet(s): ${keys.join(', ')}.`);
    onDone();
  }
  return (
    <Modal open={open} onClose={onClose} wide title={`Edit ${ids.length} cabinet${ids.length === 1 ? '' : 's'}`}
      footer={<>
        <Button variant="ghost" onClick={onClose}>Cancel</Button>
        <Button onClick={apply} disabled={!keys.length}>{keys.length ? `Update ${ids.length}` : 'Nothing to change'}</Button>
      </>}>
      <div className="space-y-3">
        <p className="text-sm text-[var(--leon-black)]/60">Only the fields set below are touched. Everything else on each cabinet is left as it is.</p>
        <div className="grid gap-3 md:grid-cols-2">
          <Field label="Cabinet type">
            <Select value={fields.typeId || ''} onChange={e => set('typeId', e.target.value)}>
              <option value="">— leave alone —</option>
              {cwAllTypes(project).map(t => <option key={t.id} value={t.id}>{t.code} · {t.name}</option>)}
            </Select>
          </Field>
          <Field label="Front style">
            <Select value={fields.frontStyle || ''} onChange={e => set('frontStyle', e.target.value)}>
              <option value="">— leave alone —</option>
              {CW_FRONT_STYLES.map(x => <option key={x.key} value={x.key}>{x.label}</option>)}
            </Select>
          </Field>
          <Field label="Construction">
            <Select value={fields.constructionId || ''} onChange={e => set('constructionId', e.target.value)}>
              <option value="">— leave alone —</option>
              {s.constructions.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
            </Select>
          </Field>
          <Field label="Hardware set">
            <Select value={fields.hardwareSetId || ''} onChange={e => set('hardwareSetId', e.target.value)}>
              <option value="">— leave alone —</option>
              {s.hardwareSets.map(h => <option key={h.id} value={h.id}>{h.code} · {h.name}</option>)}
            </Select>
          </Field>
          <Field label="Box material">
            <Select value={fields.boxMaterialId || ''} onChange={e => set('boxMaterialId', e.target.value)}>
              <option value="">— leave alone —</option>
              {s.materials.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
            </Select>
          </Field>
          <Field label="Front material">
            <Select value={fields.frontMaterialId || ''} onChange={e => set('frontMaterialId', e.target.value)}>
              <option value="">— leave alone —</option>
              {s.materials.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
            </Select>
          </Field>
          <Field label="Status">
            <Select value={fields.status || ''} onChange={e => set('status', e.target.value)}>
              <option value="">— leave alone —</option>
              {CW_STATUSES.map(x => <option key={x}>{x}</option>)}
            </Select>
          </Field>
        </div>
        {!!keys.length && (
          <div className="rounded-lg bg-[var(--leon-cream)] border border-[var(--leon-line)] p-3 text-sm">
            <b>{ids.length} cabinet{ids.length === 1 ? '' : 's'} will be updated.</b> Every panel size, edge-band
            length and sheet requirement downstream recalculates from the new values.
          </div>
        )}
      </div>
    </Modal>
  );
}

// ============================================================================
// CABINET DESIGNER
// ----------------------------------------------------------------------------
// Change the width here and watch every number on the page move: the elevation,
// the door and drawer front sizes, the carcass parts, the shelves, the
// edge-band total and the sheet area. Nothing on this screen is stored — it is
// all recomputed, which is why it cannot drift.
// ============================================================================
const CW_DESIGNER_STEPS = [
  { key: 'size', label: 'Size' },
  { key: 'front', label: 'Front' },
  { key: 'interior', label: 'Interior' },
  { key: 'materials', label: 'Materials & grain' },
  { key: 'hardware', label: 'Hardware' },
  { key: 'trim', label: 'Fillers & panels' },
  { key: 'panels', label: 'Panels' },
];

function CwDesigner({ ctx, project, system, editable, cab, onPick }) {
  const items = cwItems(project);
  const [selId, setSelId] = useState(cab ? cab.id : (items[0] ? items[0].id : null));
  const [step, setStep] = useState('size');
  const [panelKey, setPanelKey] = useState(null);
  useEffect(() => { if (cab) setSelId(cab.id); }, [cab && cab.id]);

  if (!items.length) return <EmptyState text="No cabinets on this project yet. Lay out a run, or add one from the Cabinet Schedule." />;
  const current = items.find(c => c.id === selId) || items[0];
  if (!current) return <EmptyState text="Pick a cabinet." />;

  const built = cwComputeParts(project, current);
  const R = built.res, con = built.con, L = built.layout;
  const s = cwSettings(project);
  const set = fields => cwUpdate(ctx, project, draft => {
    const c = draft.caseworkItems.find(x => x.id === current.id);
    if (!c) return;
    Object.assign(c, fields);
  }, `LEON Casework — ${current.mark}: ${Object.keys(fields).join(', ')} set in the designer.`);
  const setRows = rows => set({ rows: cloneDeep(rows) });
  const rows = cloneDeep(R.rows || []);
  const panel = built.panels.find(p => p.key === panelKey) || null;

  return (
    <div className="grid gap-4 lg:grid-cols-[1fr_380px] items-start">
      <div className="space-y-3">
        <div className="flex items-center gap-2 flex-wrap">
          <Select className="!w-56" value={current.id} onChange={e => { setSelId(e.target.value); onPick(items.find(x => x.id === e.target.value)); }}>
            {items.map(c => <option key={c.id} value={c.id}>{c.mark}{c.unit ? ` — ${c.unit}` : ''}</option>)}
          </Select>
          <Badge>{R.type ? R.type.code : 'no type'}</Badge>
          {!!R.ownFields.length && <Badge tone="yellow">{R.ownFields.length} field(s) overridden</Badge>}
        </div>

        <div className="flex gap-1 flex-wrap">
          {CW_DESIGNER_STEPS.map((x, i) => (
            <button key={x.key} onClick={() => setStep(x.key)}
              className={`px-2.5 py-1 rounded-full text-[11px] font-semibold border ${step === x.key ? 'bg-[var(--leon-brown)] text-white border-[var(--leon-brown)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown)]'}`}>
              {i + 1}. {x.label}
            </button>
          ))}
        </div>

        <div className="rounded-lg border border-[var(--leon-line)] bg-white p-4 space-y-3">
          {step === 'size' && (
            <>
              <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
                <Field label="Mark"><TextInput value={current.mark} disabled={!editable} onChange={e => set({ mark: e.target.value })} /></Field>
                <Field label="Width" hint={cwOwn(R, 'width') ? 'set here' : 'from the type'}>
                  <CwDimField value={R.width} system={system} disabled={!editable} onChange={v => set({ width: v })} /></Field>
                <Field label="Height" hint={cwOwn(R, 'height') ? 'set here' : 'from the type'}>
                  <CwDimField value={R.height} system={system} disabled={!editable} onChange={v => set({ height: v })} /></Field>
                <Field label="Depth" hint={cwOwn(R, 'depth') ? 'set here' : 'from the type'}>
                  <CwDimField value={R.depth} system={system} disabled={!editable} onChange={v => set({ depth: v })} /></Field>
                <Field label="Quantity">
                  <TextInput type="number" value={current.qty} disabled={!editable} onChange={e => set({ qty: Number(e.target.value) || 1 })} /></Field>
                <Field label="Construction">
                  <Select value={R.constructionId} disabled={!editable} onChange={e => set({ constructionId: e.target.value })}>
                    {s.constructions.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
                  </Select>
                </Field>
                <Field label="Toe kick">
                  <Select value={R.toeKick ? 'yes' : 'no'} disabled={!editable} onChange={e => set({ toeKick: e.target.value === 'yes' })}>
                    <option value="yes">Yes — {fmtDim(con.toeKickHeight, system, { inchesOnly: true })} high</option>
                    <option value="no">No — hangs or sits on a base</option>
                  </Select>
                </Field>
                <Field label="Status">
                  <Select value={current.status} disabled={!editable} onChange={e => set({ status: e.target.value })}>
                    {CW_STATUSES.map(x => <option key={x}>{x}</option>)}
                  </Select>
                </Field>
              </div>
              <CwMathList math={built.math} />
            </>
          )}

          {step === 'front' && (
            <>
              <Field label="Front style">
                <div className="grid gap-2 sm:grid-cols-3">
                  {CW_FRONT_STYLES.map(fs => (
                    <button key={fs.key} disabled={!editable} onClick={() => set({ frontStyle: fs.key })}
                      className={`rounded-lg border p-2 text-left ${R.frontStyle === fs.key ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown)]/50'}`}>
                      <div className="bg-white rounded border border-[var(--leon-line)] mb-1.5 h-20 grid place-items-center overflow-hidden">
                        <svg viewBox="0 0 60 90" width="42" height="66">
                          <CwFrontFace x={2} y={2} w={56} h={86} style={fs.key} px={v => v} kind="door" handing="L" />
                        </svg>
                      </div>
                      <div className="text-[11px] font-semibold">{fs.label}</div>
                    </button>
                  ))}
                </div>
              </Field>
              <Field label="Front description" hint="What goes on the drawing and the purchase order.">
                <TextInput value={R.doorStyle || ''} disabled={!editable} placeholder="Shaker — paint grade maple, 2¼ stile"
                  onChange={e => set({ doorStyle: e.target.value })} />
              </Field>

              <div className="pt-2 border-t border-[var(--leon-line)]">
                <div className="flex items-center justify-between mb-1.5">
                  <div className="text-sm font-semibold">Rows, top to bottom</div>
                  {editable && <Button size="sm" variant="ghost"
                    onClick={() => setRows(rows.concat([{ kind: 'doors', count: 1, weight: 1 }]))}>+ Add row</Button>}
                </div>
                <p className="text-[11px] text-[var(--leon-black)]/50 mb-2">
                  A row with a set height keeps it; the rest share what is left by weight — which is what makes
                  a drawer bank still work when the cabinet height changes.
                </p>
                <table className="w-full text-xs">
                  <thead><tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">
                    <th className="py-1">Kind</th><th className="py-1">Count</th><th className="py-1">Fixed height</th>
                    <th className="py-1">Weight</th><th></th></tr></thead>
                  <tbody>
                    {rows.map((row, i) => (
                      <tr key={i} className="border-t border-[var(--leon-line)]/60">
                        <td className="py-1 pr-2">
                          <select value={row.kind} disabled={!editable}
                            onChange={e => { rows[i].kind = e.target.value; setRows(rows); }}
                            className="px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                            {CW_ROW_KINDS.map(k => <option key={k.key} value={k.key}>{k.label}</option>)}
                          </select>
                        </td>
                        <td className="py-1 pr-2">
                          {(row.kind === 'doors' || row.kind === 'drawers') ? (
                            <input type="number" min="1" value={row.count || 1} disabled={!editable}
                              onChange={e => { rows[i].count = Number(e.target.value) || 1; setRows(rows); }}
                              className="w-14 px-1 py-0.5 border border-[var(--leon-line)] rounded" />
                          ) : <span className="text-[var(--leon-black)]/30">—</span>}
                        </td>
                        <td className="py-1 pr-2">
                          <CwDimField value={row.h} system={system} disabled={!editable} w="w-20" placeholder="share"
                            onChange={v => { rows[i].h = v; setRows(rows); }} />
                        </td>
                        <td className="py-1 pr-2">
                          <input type="number" step="0.1" value={row.weight != null ? row.weight : 1} disabled={!editable || row.h != null}
                            onChange={e => { rows[i].weight = Number(e.target.value) || 1; setRows(rows); }}
                            className="w-16 px-1 py-0.5 border border-[var(--leon-line)] rounded disabled:bg-[var(--leon-cream)]" />
                        </td>
                        <td className="py-1">
                          {editable && <>
                            <button className="text-[var(--leon-brown)]" onClick={() => { if (i > 0) { const t = rows[i - 1]; rows[i - 1] = rows[i]; rows[i] = t; setRows(rows); } }}>↑</button>
                            <button className="ml-1 text-[var(--leon-brown)]" onClick={() => { if (i < rows.length - 1) { const t = rows[i + 1]; rows[i + 1] = rows[i]; rows[i] = t; setRows(rows); } }}>↓</button>
                            <button className="ml-1.5 text-red-600" onClick={() => setRows(rows.filter((_, k) => k !== i))}>✕</button>
                          </>}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
              <CwMathList math={L.notes.concat(L.cells.filter(c => c.kind !== 'opening').map(c => ({ label: c.key, expr: c.math })))}
                title="Front sizes, from the opening" />
            </>
          )}

          {step === 'interior' && (
            <div className="grid grid-cols-2 gap-3">
              <Field label="Adjustable shelves">
                <TextInput type="number" min="0" value={R.shelfCount} disabled={!editable}
                  onChange={e => set({ shelfCount: Number(e.target.value) || 0 })} />
              </Field>
              <Field label="Handing" hint="Which side the hinges are on.">
                <Select value={current.handing} disabled={!editable} onChange={e => set({ handing: e.target.value })}>
                  <option value="Auto">Auto</option><option value="L">Left</option><option value="R">Right</option>
                </Select>
              </Field>
              <Field label="Notes" className="col-span-2">
                <TextArea rows="2" value={current.notes} disabled={!editable} onChange={e => set({ notes: e.target.value })} />
              </Field>
              <div className="col-span-2">
                <CwApplianceCutouts ctx={ctx} project={project} cab={current} system={system} editable={editable} />
              </div>
            </div>
          )}

          {step === 'materials' && (
            <>
              <div className="grid grid-cols-2 gap-3">
                <Field label="Carcass material">
                  <Select value={R.boxMaterialId} disabled={!editable} onChange={e => set({ boxMaterialId: e.target.value })}>
                    {s.materials.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
                  </Select>
                </Field>
                <Field label="Front material">
                  <Select value={R.frontMaterialId} disabled={!editable} onChange={e => set({ frontMaterialId: e.target.value })}>
                    {s.materials.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
                  </Select>
                </Field>
                <Field label="Edge banding" className="col-span-2" hint="Assigned per edge on each panel below; this is the default.">
                  <TextInput value={R.edgeBanding} disabled={!editable} onChange={e => set({ edgeBanding: e.target.value })} />
                </Field>
              </div>
              <CwFinishPicker cab={current} system={system} editable={editable} onChange={f => set({ finish: f })} />
              <div className="rounded-lg border border-[var(--leon-line)] p-3 text-xs space-y-1">
                <div className="font-semibold text-sm">Grain and veneer</div>
                <p className="text-[var(--leon-black)]/60">
                  Grain follows the material: a material marked <b>grained</b> in Construction makes every visible
                  panel from it run its length, and puts this cabinet's fronts into one <b>Continuous</b> match
                  group. A group stays together through nesting rather than being optimised apart — an upper and
                  a lower pantry door have to come off one sequence.
                </p>
                <div className="pt-1">
                  {[['Front material', cwMaterial(project, R.frontMaterialId)], ['Carcass material', cwMaterial(project, R.boxMaterialId)]].map(([k, m]) => (
                    <div key={k} className="flex justify-between">
                      <span className="text-[var(--leon-black)]/50">{k}</span>
                      <span className="font-semibold">{m ? (m.grained ? 'grained — direction enforced' : 'no grain — free to rotate in the nest') : 'not set'}</span>
                    </div>
                  ))}
                </div>
              </div>
            </>
          )}

          {step === 'hardware' && (
            <CwCabinetHardware ctx={ctx} project={project} cab={current} built={built} editable={editable} onSet={set} />
          )}

          {step === 'trim' && (
            <div className="grid grid-cols-2 gap-3">
              {[['fillerLeft', 'Filler — left'], ['fillerRight', 'Filler — right'],
                ['scribeLeft', 'Scribe allowance — left'], ['scribeRight', 'Scribe allowance — right']].map(([f, lbl]) => (
                <Field key={f} label={lbl}>
                  <CwDimField value={current[f]} system={system} disabled={!editable} onChange={v => set({ [f]: cwNum(v) })} />
                </Field>
              ))}
              <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={!!current.finishedEndLeft}
                disabled={!editable} onChange={e => set({ finishedEndLeft: e.target.checked })} /> Finished end panel — left</label>
              <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={!!current.finishedEndRight}
                disabled={!editable} onChange={e => set({ finishedEndRight: e.target.checked })} /> Finished end panel — right</label>
              <p className="col-span-2 text-[11px] text-[var(--leon-black)]/55">
                A filler is a real part and appears in the cut list at its own size. A scribe allowance is cut
                onto the filler and trimmed to the wall on site — it is not a separate piece.
                A finished end panel is sized to the box depth plus the front thickness so it finishes flush
                with the door.
              </p>
            </div>
          )}

          {step === 'panels' && (
            <CwCabinetPanels project={project} built={built} system={system} editable={editable}
              cab={current} ctx={ctx} onOpen={setPanelKey} />
          )}
        </div>
      </div>

      {/* The live drawing, always on screen. */}
      <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3 space-y-3 lg:sticky lg:top-4">
        <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">{current.mark} — elevation</div>
        <div className="bg-[var(--leon-cream)] rounded p-2 grid place-items-center">
          <CwElevation built={built} system={system} height={300} showDims mark={current.mark}
            handing={current.handing === 'Auto' ? 'L' : current.handing} />
        </div>
        <div className="text-xs space-y-1 border-t border-[var(--leon-line)] pt-2">
          {[['Overall', `${fmtDim(R.width, system, { inchesOnly: true })} × ${fmtDim(R.height, system, { inchesOnly: true })} × ${fmtDim(R.depth, system, { inchesOnly: true })}`],
            ['Interior', `${fmtDim(built.interiorW, system, { inchesOnly: true })} × ${fmtDim(built.interiorH, system, { inchesOnly: true })}`],
            ['Panels', `${built.panels.reduce((a, p) => a + p.qty, 0)} parts, ${cwRound(built.area)} m²`],
            ['Edge band', `${cwRound(built.bandTotal / 1000)} m`],
            ['Construction', con.name]].map(([k, v]) => (
            <div key={k} className="flex justify-between gap-2">
              <span className="text-[var(--leon-black)]/50">{k}</span>
              <span className="font-semibold tabular-nums text-right">{v}</span>
            </div>
          ))}
        </div>
        <CwIssueList issues={built.issues} limit={4} />
        {panel && (
          <div className="border-t border-[var(--leon-line)] pt-2">
            <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 mb-1">Panel — {panel.name}</div>
            <div className="bg-[var(--leon-cream)] rounded p-2"><CwPanelView panel={panel} system={system} height={150} con={con} /></div>
          </div>
        )}
      </div>
    </div>
  );
}

// Appliance and fixture cutouts on a cabinet. The check that matters is whether
// the bowl actually fits inside the box — a countertop drawing will happily show
// a sink the cabinet cannot take.
function CwApplianceCutouts({ ctx, project, cab, system, editable }) {
  const cuts = cab.applianceCutouts || [];
  const fixtures = ctx.fixtureLibrary || [];
  function mutate(fn, action) {
    cwUpdate(ctx, project, draft => {
      const c = draft.caseworkItems.find(x => x.id === cab.id);
      if (!c) return;
      if (!Array.isArray(c.applianceCutouts)) c.applianceCutouts = [];
      fn(c);
    }, action);
  }
  return (
    <div className="rounded-lg border border-[var(--leon-line)] p-3 space-y-2">
      <div className="flex items-center justify-between">
        <div className="text-sm font-semibold">Cutouts</div>
        {editable && <Button size="sm" variant="ghost"
          onClick={() => mutate(c => c.applianceCutouts.push({ id: uid('cwcut'), kind: 'Sink cutout', label: '', w: cwIn(30), h: cwIn(18), specId: null, note: '' }),
            `LEON Casework — cutout added to ${cab.mark}.`)}>+ Add cutout</Button>}
      </div>
      {!cuts.length && <div className="text-xs text-[var(--leon-black)]/40">None.</div>}
      {cuts.map(c => (
        <div key={c.id} className="flex items-end gap-2 flex-wrap text-xs">
          <Field label="Label"><TextInput className="!w-32 !text-xs" value={c.label} disabled={!editable}
            onChange={e => mutate(x => { const t = x.applianceCutouts.find(y => y.id === c.id); if (t) t.label = e.target.value; }, null)} /></Field>
          <Field label="Fixture spec">
            <Select className="!w-52 !text-xs" value={c.specId || ''} disabled={!editable}
              onChange={e => mutate(x => { const t = x.applianceCutouts.find(y => y.id === c.id); if (t) t.specId = e.target.value || null; }, null)}>
              <option value="">— not linked —</option>
              {fixtures.filter(f => f.active !== false).map(f => <option key={f.id} value={f.id}>{f.fixtureType} — {f.manufacturer} {f.model}</option>)}
            </Select>
          </Field>
          <Field label="Width"><CwDimField value={c.w} system={system} disabled={!editable} w="w-20"
            onChange={v => mutate(x => { const t = x.applianceCutouts.find(y => y.id === c.id); if (t) t.w = cwNum(v); }, null)} /></Field>
          <Field label="Depth"><CwDimField value={c.h} system={system} disabled={!editable} w="w-20"
            onChange={v => mutate(x => { const t = x.applianceCutouts.find(y => y.id === c.id); if (t) t.h = cwNum(v); }, null)} /></Field>
          {editable && <button className="text-red-600 pb-2"
            onClick={() => mutate(x => { x.applianceCutouts = x.applianceCutouts.filter(y => y.id !== c.id); }, `LEON Casework — cutout removed from ${cab.mark}.`)}>✕</button>}
        </div>
      ))}
      {!fixtures.length && <p className="text-[11px] text-[var(--leon-black)]/45">
        The fixture library is empty. Sinks and faucets are filed there once, in the Shop Drawing Hub, and are
        referenced here — this module does not keep a second list of them.
      </p>}
    </div>
  );
}

// The finish comes from the supplier catalog, never a parallel material list —
// the photo, the code and the supplier are already on file there.
function CwFinishPicker({ cab, system, editable, onChange }) {
  const [open, setOpen] = useState(false);
  const [q, setQ] = useState('');
  const pool = useMemo(() => {
    const all = (typeof supplierCatalog === 'function') ? supplierCatalog() : [];
    return all.filter(r => /laminate|melamine|veneer|acrylic|mdf|panel|quartz/i.test(r.cat || ''));
  }, []);
  const shown = useMemo(() => {
    const t = q.trim().toLowerCase();
    const list = t ? pool.filter(r => `${r.name} ${r.code} ${r.cat} ${r.supLabel}`.toLowerCase().indexOf(t) >= 0) : pool;
    return list.slice(0, 60);
  }, [q, pool]);
  const f = cab.finish;
  return (
    <div className="rounded-lg border border-[var(--leon-line)] p-3">
      <div className="flex items-center gap-3">
        {f && f.img && <img src={f.img} alt="" className="w-12 h-12 object-cover rounded border border-[var(--leon-line)]" />}
        <div className="min-w-0">
          <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Finish</div>
          <div className="text-sm font-semibold truncate">{f ? f.name : 'Not chosen'}</div>
          {f && <div className="text-[11px] text-[var(--leon-black)]/50 truncate">{f.supLabel} · {f.code} · {f.cat}</div>}
        </div>
        {editable && <div className="ml-auto flex gap-2">
          <Button size="sm" variant="ghost" onClick={() => setOpen(true)}>{f ? 'Change' : 'Choose'}</Button>
          {f && <Button size="sm" variant="ghost" onClick={() => onChange(null)}>Clear</Button>}
        </div>}
      </div>
      <Modal open={open} wide onClose={() => setOpen(false)} title="Choose a finish"
        footer={<Button variant="ghost" onClick={() => setOpen(false)}>Close</Button>}>
        <div className="space-y-3">
          <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Search the supplier catalog…" />
          <p className="text-xs text-[var(--leon-black)]/55">
            {pool.length} panel and surface finishes are in the supplier catalog. Choosing one stores a
            reference — the supplier's own record stays the source.
          </p>
          <div className="grid gap-2 sm:grid-cols-3 max-h-[420px] overflow-y-auto">
            {shown.map(r => (
              <button key={`${r.sup}:${r.id}`} onClick={() => { onChange(makeSupplierFinishRef(r)); setOpen(false); }}
                className="rounded border border-[var(--leon-line)] p-2 text-left hover:border-[var(--leon-brown)]">
                {r.img && <img src={r.img} alt="" className="w-full h-16 object-cover rounded mb-1" />}
                <div className="text-[11px] font-semibold truncate">{r.name}</div>
                <div className="text-[10px] text-[var(--leon-black)]/45 truncate">{r.supLabel} · {r.code}</div>
              </button>
            ))}
          </div>
          {!shown.length && <EmptyState text="Nothing matches." />}
        </div>
      </Modal>
    </div>
  );
}

// Hardware on this cabinet, and what that hardware does to the geometry.
function CwCabinetHardware({ ctx, project, cab, built, editable, onSet }) {
  const s = cwSettings(project);
  const set = built.hardwareSet;
  const items = s.hardware;
  const ops = [];
  built.panels.forEach(p => (p.ops || []).forEach(o => ops.push({ panel: p, op: o })));
  const unknown = ops.filter(x => x.op.unknown);
  return (
    <div className="space-y-3">
      <Field label="Hardware set" hint="A set fills in every item at once, and its numbers drive the geometry below.">
        <Select value={cab.hardwareSetId || ''} disabled={!editable} onChange={e => onSet({ hardwareSetId: e.target.value || null })}>
          <option value="">— none —</option>
          {s.hardwareSets.map(h => <option key={h.id} value={h.id}>{h.code} · {h.name}</option>)}
        </Select>
      </Field>
      {set ? (
        <div className="rounded border border-[var(--leon-line)] divide-y divide-[var(--leon-line)]">
          {(set.lines || []).map((l, i) => {
            const it = items.find(h => h.id === l.itemId);
            return (
              <div key={i} className="px-3 py-1.5 text-sm flex items-center gap-2">
                <span className="font-semibold w-10">{l.qty}×</span>
                <span>{it ? it.name : 'Item not in the project hardware list'}</span>
                <span className="text-[11px] text-[var(--leon-black)]/45">per {l.per || 'cabinet'}</span>
                <span className="ml-auto text-xs">
                  {it && cwDrillingKnown(it)
                    ? <span className="text-[#3a7d44]">drilling data loaded</span>
                    : <span className="text-amber-700">manufacturer drilling data not loaded</span>}
                </span>
              </div>
            );
          })}
          {!(set.lines || []).length && <div className="px-3 py-2 text-xs text-[var(--leon-black)]/45">This set has no items yet.</div>}
        </div>
      ) : (
        <p className="text-sm text-[var(--leon-black)]/55">
          No set assigned, so no bore positions can be derived. Build sets under <b>Hardware</b> — Imundex's
          hinges, runners and handles are already in the supplier catalog and can be pulled in there.
        </p>
      )}

      <div className="rounded-lg border border-[var(--leon-line)] p-3">
        <div className="text-sm font-semibold mb-1.5">Machining this hardware produces</div>
        <p className="text-[11px] text-[var(--leon-black)]/55 mb-2">
          Structured operations against a panel, with coordinates. <b>Not machine code</b> — turning these into
          a program belongs to whoever owns the machine, and a file written from a guessed pattern is worse
          than no file.
        </p>
        {!ops.length && <div className="text-xs text-[var(--leon-black)]/40">Nothing yet.</div>}
        <div className="max-h-56 overflow-y-auto">
          {ops.map((x, i) => (
            <div key={i} className={`text-xs py-1 border-b border-[var(--leon-line)]/50 ${x.op.unknown ? 'text-amber-800' : ''}`}>
              <b>{x.panel.name}</b> — {x.op.kind}
              {x.op.x != null && <span className="tabular-nums"> at ({cwN(x.op.x)}, {cwN(x.op.y)})
                {x.op.dia ? ` ⌀${cwN(x.op.dia)}` : ''}</span>}
              {x.op.note && <span className="text-[var(--leon-black)]/55"> — {x.op.note}</span>}
            </div>
          ))}
        </div>
        {!!unknown.length && (
          <div className="mt-2 rounded border border-amber-200 bg-amber-50 p-2 text-[11px] text-amber-900">
            {unknown.length} operation(s) have no drilling data. Fill the cup diameter, setback, runner length or
            handle centres in on the hardware item and they resolve to real coordinates.
          </div>
        )}
      </div>
    </div>
  );
}

// The panel list for one cabinet — the bridge from the elevation to the saw, and
// where a per-edge banding decision is actually made.
function CwCabinetPanels({ ctx, project, built, system, editable, cab, onOpen }) {
  const [openKey, setOpenKey] = useState(null);
  function setOverride(key, field, mm) {
    cwUpdate(ctx, project, draft => {
      const c = draft.caseworkItems.find(x => x.id === cab.id);
      if (!c) return;
      c.overrides = Object.assign({}, c.overrides || {});
      const o = Object.assign({}, c.overrides[key] || {});
      if (mm === null) delete o[field]; else o[field] = mm;
      if (!Object.keys(o).length) delete c.overrides[key]; else c.overrides[key] = o;
    }, `LEON Casework — ${cab.mark} ${key} cut size forced.`);
  }
  return (
    <div className="space-y-2">
      <p className="text-xs text-[var(--leon-black)]/55">
        Every part, computed. Click a row to draw it. A forced cut size is tinted, and the number the engine
        produced is kept beside it — a shop override should never be able to look like the arithmetic.
      </p>
      <div className="overflow-x-auto">
        <table className="w-full text-xs" style={{ minWidth: 900 }}>
          <thead>
            <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
              <th className="px-2 py-1.5">Part</th><th className="px-2 py-1.5">Group</th><th className="px-2 py-1.5">Material</th>
              <th className="px-2 py-1.5">Thk</th><th className="px-2 py-1.5">Length</th><th className="px-2 py-1.5">Width</th>
              <th className="px-2 py-1.5">Qty</th><th className="px-2 py-1.5">Grain</th>
              <th className="px-2 py-1.5">Banded edges</th>
            </tr>
          </thead>
          <tbody>
            {built.panels.map(p => (
              <React.Fragment key={p.key}>
                <tr className={`border-b border-[var(--leon-line)]/60 cursor-pointer hover:bg-[var(--leon-cream)]/50 ${p.overridden ? 'bg-amber-50' : ''}`}
                  onClick={() => { setOpenKey(openKey === p.key ? null : p.key); onOpen(p.key); }}>
                  <td className="px-2 py-1 font-semibold">{p.name}</td>
                  <td className="px-2 py-1 text-[var(--leon-black)]/55">{p.group}</td>
                  <td className="px-2 py-1 text-[var(--leon-black)]/55">{p.materialName}</td>
                  <td className="px-2 py-1 tabular-nums">{fmtDim(p.thickness, system, { inchesOnly: true })}</td>
                  <td className="px-2 py-1 tabular-nums font-semibold">{fmtDim(p.length, system, { inchesOnly: true })}</td>
                  <td className="px-2 py-1 tabular-nums font-semibold">{fmtDim(p.width, system, { inchesOnly: true })}</td>
                  <td className="px-2 py-1 tabular-nums">{p.qty}</td>
                  <td className="px-2 py-1">{p.grain === 'None' ? <span className="text-[var(--leon-black)]/30">free</span> : p.grain}</td>
                  <td className="px-2 py-1">
                    {CW_EDGES.filter(e => (p.edges || {})[e]).map(e => e[0].toUpperCase()).join(' ') || <span className="text-[var(--leon-black)]/30">none</span>}
                  </td>
                </tr>
                {openKey === p.key && (
                  <tr className="bg-[var(--leon-cream)]/40">
                    <td colSpan={9} className="px-3 py-3">
                      <div className="grid gap-3 lg:grid-cols-[260px_1fr]">
                        <div className="bg-white rounded border border-[var(--leon-line)] p-2">
                          <CwPanelView panel={p} system={system} height={150} con={built.con} />
                        </div>
                        <div className="space-y-2">
                          <div className="text-[11px] text-[var(--leon-black)]/60"><b>How it was sized:</b> {p.math}</div>
                          {p.note && <div className="text-[11px] text-[var(--leon-black)]/55">{p.note}</div>}
                          <div className="text-[11px]"><b>Part number:</b> {p.partNo || `${cab.mark} / ${p.key}`}</div>
                          <div className="flex gap-3 flex-wrap items-end">
                            {CW_EDGES.map(e => (
                              <label key={e} className="text-[11px] flex items-center gap-1">
                                <input type="checkbox" checked={!!(p.edges || {})[e]} disabled
                                  readOnly />
                                {CW_EDGE_LABELS[e]}
                              </label>
                            ))}
                            <span className="text-[11px] text-[var(--leon-black)]/50">
                              band {cwRound(cwPanelBandLength(p) / 1000)} m — assigned by the part's own rule
                              (a visible edge is banded; an open cabinet's shelves get all four).
                            </span>
                          </div>
                          {editable && (
                            <div className="flex gap-3 items-end">
                              <Field label="Force length"><CwDimField value={(cab.overrides || {})[p.key] ? (cab.overrides[p.key].length != null ? cab.overrides[p.key].length : null) : null}
                                system={system} w="w-24" placeholder={fmtDim(p.baseLength, system, { inchesOnly: true })}
                                onChange={v => setOverride(p.key, 'length', v)} /></Field>
                              <Field label="Force width"><CwDimField value={(cab.overrides || {})[p.key] ? (cab.overrides[p.key].width != null ? cab.overrides[p.key].width : null) : null}
                                system={system} w="w-24" placeholder={fmtDim(p.baseWidth, system, { inchesOnly: true })}
                                onChange={v => setOverride(p.key, 'width', v)} /></Field>
                              <span className="text-[11px] text-[var(--leon-black)]/45 pb-2">Leave blank to follow the engine.</span>
                            </div>
                          )}
                          {!!(p.ops || []).length && (
                            <div className="text-[11px] text-[var(--leon-black)]/60">
                              <b>Machining:</b> {(p.ops || []).map((o, i) => (
                                <span key={i} className={o.unknown ? 'text-amber-800' : ''}>{i ? ' · ' : ' '}{o.kind}{o.x != null ? ` (${cwN(o.x)}, ${cwN(o.y)})` : ''}</span>
                              ))}
                            </div>
                          )}
                        </div>
                      </div>
                    </td>
                  </tr>
                )}
              </React.Fragment>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// ============================================================================
// CABINET TYPES — the project's own, plus what it took from the library
// ============================================================================
function CwCabinetTypesPanel({ ctx, project, system, editable }) {
  const own = cwProjectTypes(project);
  const s = cwSettings(project);
  function importType(t) {
    cwUpdate(ctx, project, draft => {
      draft.caseworkTypes.push(cwMakeType(Object.assign({}, t, { id: uid('cwtype'), global: false }), ctx.currentUserName));
    }, `LEON Casework — cabinet type "${t.name}" imported from the standard library.`);
  }
  function upd(id, fields) {
    cwUpdate(ctx, project, draft => {
      const t = draft.caseworkTypes.find(x => x.id === id);
      if (t) Object.assign(t, fields);
    }, null);
  }
  return (
    <div className="space-y-4">
      <div>
        <h3 className="font-bold">Project Cabinet Types</h3>
        <p className="text-sm text-[var(--leon-black)]/55 max-w-2xl">
          A type is the module standard; a mark is the cabinet. Forty marks can point at <b>B2D</b>, and
          changing B2D reaches all forty — except any mark that set its own value, which stays as it is and is
          tinted in the schedule so an inherited number is never mistaken for a chosen one.
        </p>
      </div>
      {!own.length && <EmptyState text="No project types yet. Import from the standard library below — the library types can also be used directly, but a project type is editable." />}
      <div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
        {own.map(t => {
          const used = cwItems(project).filter(c => c.typeId === t.id);
          const demo = cwComputeParts(project, cwDemoCabinet(t.id, t.code));
          return (
            <div key={t.id} className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
              <div className="bg-[var(--leon-cream)] rounded mb-2 grid place-items-center h-40 overflow-hidden">
                <CwElevation built={demo} system={system} height={140} handing="L" />
              </div>
              <div className="flex items-center gap-2 mb-2">
                <input value={t.code} disabled={!editable} onChange={e => upd(t.id, { code: e.target.value })}
                  className="w-20 px-1 py-0.5 font-bold bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                <input value={t.name} disabled={!editable} onChange={e => upd(t.id, { name: e.target.value })}
                  className="flex-1 min-w-0 px-1 py-0.5 text-sm bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                <Badge>{used.length}</Badge>
              </div>
              <div className="grid grid-cols-3 gap-2 text-xs">
                {['width', 'height', 'depth'].map(f => (
                  <Field key={f} label={f[0].toUpperCase() + f.slice(1)}>
                    <CwDimField value={t[f]} system={system} disabled={!editable} w="w-full" onChange={v => upd(t.id, { [f]: v })} />
                  </Field>
                ))}
                <Field label="Category">
                  <Select className="!text-xs" value={t.category} disabled={!editable} onChange={e => upd(t.id, { category: e.target.value })}>
                    {CW_CATEGORIES.map(c => <option key={c}>{c}</option>)}
                  </Select>
                </Field>
                <Field label="Shelves">
                  <TextInput className="!text-xs" type="number" value={t.shelfCount} disabled={!editable}
                    onChange={e => upd(t.id, { shelfCount: Number(e.target.value) || 0 })} />
                </Field>
                <Field label="Front">
                  <Select className="!text-xs" value={t.frontStyle} disabled={!editable} onChange={e => upd(t.id, { frontStyle: e.target.value })}>
                    {CW_FRONT_STYLES.map(x => <option key={x.key} value={x.key}>{x.label}</option>)}
                  </Select>
                </Field>
              </div>
              {editable && (
                <button onClick={() => { if (confirm(`Remove ${t.code}? ${used.length} cabinet(s) use it and will fall back to their own values.`)) {
                  cwUpdate(ctx, project, draft => { draft.caseworkTypes = draft.caseworkTypes.filter(x => x.id !== t.id); },
                    `LEON Casework — cabinet type ${t.code} removed.`);
                } }} className="mt-2 text-[11px] font-semibold text-red-600">Remove</button>
              )}
            </div>
          );
        })}
      </div>
      <div className="pt-2 border-t border-[var(--leon-line)]">
        <h4 className="font-bold text-sm mb-2">Import from the standard library</h4>
        <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
          {CW_STANDARD_TYPES.map(t => (
            <button key={t.id} disabled={!editable} onClick={() => importType(t)}
              className="rounded-lg border border-[var(--leon-line)] p-2.5 text-left hover:border-[var(--leon-brown)] disabled:opacity-50">
              <div className="font-semibold text-sm">{t.code} · {t.name}</div>
              <div className="text-[11px] text-[var(--leon-black)]/50">
                {fmtDim(t.width, system, { inchesOnly: true })} × {fmtDim(t.height, system, { inchesOnly: true })} × {fmtDim(t.depth, system, { inchesOnly: true })}
              </div>
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

function CwGlobalLibrary({ ctx, project, system }) {
  const [cat, setCat] = useState('All');
  const list = CW_STANDARD_TYPES.filter(t => cat === 'All' || t.category === cat);
  return (
    <div className="space-y-3">
      <div className="flex items-end justify-between gap-3 flex-wrap">
        <div>
          <h3 className="font-bold">Standard Cabinet Library</h3>
          <p className="text-sm text-[var(--leon-black)]/55 max-w-2xl">
            {CW_STANDARD_TYPES.length} real starting points on North American standards — base 24" deep ×
            34½" high, wall 12" deep at 30/36/42", tall 24" deep at 84/90/96", vanity 21" deep, widths on the
            3" module. Every number is editable once a type is imported onto a job.
          </p>
        </div>
        <Select className="!w-40" value={cat} onChange={e => setCat(e.target.value)}>
          <option>All</option>{CW_CATEGORIES.map(c => <option key={c}>{c}</option>)}
        </Select>
      </div>
      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
        {list.map(t => {
          const demo = project ? cwComputeParts(project, cwDemoCabinet(t.id, t.code)) : null;
          return (
            <div key={t.id} className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
              <div className="bg-[var(--leon-cream)] rounded mb-2 grid place-items-center h-40 overflow-hidden">
                {demo ? <CwElevation built={demo} system={system} height={140} handing="L" />
                      : <span className="text-[11px] text-[var(--leon-black)]/40 px-3 text-center">Pick a project to draw this against its construction standard.</span>}
              </div>
              <div className="font-semibold text-sm leading-tight">{t.code} · {t.name}</div>
              <div className="text-[11px] text-[var(--leon-black)]/50">
                {fmtDim(t.width, system, { inchesOnly: true })} × {fmtDim(t.height, system, { inchesOnly: true })} × {fmtDim(t.depth, system, { inchesOnly: true })}
                {' '}· {t.shelfCount} shelf{t.shelfCount === 1 ? '' : 'ves'}
              </div>
              {t.notes && <p className="text-[11px] text-[var(--leon-black)]/45 mt-1">{t.notes}</p>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ============================================================================
// CONSTRUCTION — the numbers every derived size comes out of
// ============================================================================
function CwConstructionPanel({ ctx, project, system, editable }) {
  const s = cwSettings(project);
  const mut = (fn, action) => cwSetSettings(ctx, project, fn, action);
  const conFields = [
    ['panelThickness', 'Carcass thickness'], ['doorThickness', 'Front thickness'],
    ['backThickness', 'Back thickness'], ['backInset', 'Back inset'], ['backGrooveDepth', 'Back groove'],
    ['reveal', 'Reveal (frameless)'], ['gap', 'Gap between fronts'],
    ['ffStile', 'Face frame stile'], ['ffRail', 'Face frame rail'], ['overlay', 'Overlay per edge'],
    ['hingeBoreSetback', 'Hinge cup setback'], ['hingeBoreDia', 'Hinge cup ⌀'],
    ['shelfSideClearance', 'Shelf side clearance'], ['shelfDepthSetback', 'Shelf depth setback'],
    ['drawerSideClearance', 'Drawer clearance per side'], ['drawerBoxThickness', 'Drawer box thickness'],
    ['drawerBottomThickness', 'Drawer bottom thickness'], ['drawerBottomGroove', 'Drawer bottom groove'],
    ['drawerBoxHeightDeduction', 'Front over box height'], ['slideTakeUp', 'Slide take-up (depth)'],
    ['toeKickHeight', 'Toe kick height'], ['toeKickSetback', 'Toe kick setback'],
    ['stretcherWidth', 'Top stretcher width'], ['nailerWidth', 'Nailer width'],
    ['edgeBandThickness', 'Edge band thickness'], ['scribeAllowance', 'Default scribe allowance'],
  ];
  return (
    <div className="space-y-5">
      <div>
        <h3 className="font-bold">Construction standards</h3>
        <p className="text-sm text-[var(--leon-black)]/55 max-w-3xl">
          Every derived part size in this module comes out of these numbers — not one of them is hard-coded.
          A 32 mm frameless shop and a face-frame shop do not stack up the same way and neither is wrong,
          so the rules live here as data. <b>They are saved on this job</b>, so tuning a standard for one client
          never silently re-cuts work on another.
        </p>
      </div>

      {s.constructions.map(c => (
        <Collapsible key={c.id} id={`cwcon-${c.id}`} title={c.name} right={<Badge tone="brown">{c.kind}</Badge>}>
          <div className="space-y-3">
            <div className="flex gap-3 flex-wrap">
              <Field label="Name"><TextInput className="!w-64" value={c.name} disabled={!editable}
                onChange={e => mut(x => { const t = x.constructions.find(y => y.id === c.id); if (t) t.name = e.target.value; }, null)} /></Field>
              <Field label="Kind">
                <Select className="!w-40" value={c.kind} disabled={!editable}
                  onChange={e => mut(x => { const t = x.constructions.find(y => y.id === c.id); if (t) t.kind = e.target.value; },
                    `LEON Casework — ${c.name} switched to ${e.target.value}.`)}>
                  <option>Frameless</option><option>Face Frame</option>
                </Select>
              </Field>
            </div>
            <div className="grid gap-2 sm:grid-cols-3 lg:grid-cols-4">
              {conFields.map(([f, lbl]) => (
                <Field key={f} label={lbl}>
                  <CwDimField value={c[f]} system={system} disabled={!editable} w="w-full"
                    onChange={v => mut(x => { const t = x.constructions.find(y => y.id === c.id); if (t) t[f] = cwNum(v); },
                      `LEON Casework — ${c.name}: ${lbl} set to ${cwN(v)} mm.`)} />
                </Field>
              ))}
            </div>
            <label className="flex items-center gap-2 text-sm">
              <input type="checkbox" checked={c.backCaptured !== false} disabled={!editable}
                onChange={e => mut(x => { const t = x.constructions.find(y => y.id === c.id); if (t) t.backCaptured = e.target.checked; }, null)} />
              Back captured in a groove (unticked = applied to the back of the carcass)
            </label>
            <Field label="Notes"><TextArea rows="2" value={c.notes} disabled={!editable}
              onChange={e => mut(x => { const t = x.constructions.find(y => y.id === c.id); if (t) t.notes = e.target.value; }, null)} /></Field>
          </div>
        </Collapsible>
      ))}
      {editable && <Button size="sm" onClick={() => mut(x => x.constructions.push(cwMakeConstruction({})), 'LEON Casework — construction standard added.')}>+ Add construction standard</Button>}

      <CwLibPanel title="Sheet materials" editable={editable} items={s.materials} minWidth={880}
        blurb="What the parts are cut from. The cut list groups by these and the nest reads each one's own sheet size, so a 5×5 Baltic birch sheet is never averaged into a 4×8 melamine sheet. Marking a material grained is what enforces grain direction downstream."
        addLabel="Add material"
        onAdd={() => mut(x => x.materials.push(cwMakeMaterial({})), 'LEON Casework — material added.')}
        onRemove={id => mut(x => { x.materials = x.materials.filter(m => m.id !== id); }, 'LEON Casework — material removed.')}
        columns={['Material', 'Use', 'Thickness', 'Sheet width', 'Sheet height', 'Grained']}
        renderRow={m => [
          <td key="n" className="px-2 py-1">
            <input value={m.name} disabled={!editable}
              onChange={e => mut(x => { const t = x.materials.find(y => y.id === m.id); if (t) t.name = e.target.value; }, null)}
              className="w-56 px-1 py-0.5 font-semibold bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" /></td>,
          <td key="k" className="px-2 py-1">
            <select value={m.kind} disabled={!editable}
              onChange={e => mut(x => { const t = x.materials.find(y => y.id === m.id); if (t) t.kind = e.target.value; }, null)}
              className="px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
              {['Panel', 'Front', 'Drawer box', 'Back', 'Edge band'].map(k => <option key={k}>{k}</option>)}
            </select></td>,
          ...['thickness', 'sheetW', 'sheetH'].map(f => (
            <td key={f} className="px-2 py-1">
              <CwDimField value={m[f]} system={system} disabled={!editable} w="w-24"
                onChange={v => mut(x => { const t = x.materials.find(y => y.id === m.id); if (t) t[f] = cwNum(v); }, null)} />
            </td>
          )),
          <td key="g" className="px-2 py-1">
            <input type="checkbox" checked={!!m.grained} disabled={!editable}
              onChange={e => mut(x => { const t = x.materials.find(y => y.id === m.id); if (t) t.grained = e.target.checked; },
                `LEON Casework — ${m.name} marked ${e.target.checked ? 'grained' : 'not grained'}.`)} /></td>,
        ]} />

      <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3 space-y-2">
        <div className="font-semibold text-sm">Edge banding default</div>
        <Field label="Band" hint="Assigned per edge on each panel; this is what a new edge takes.">
          <TextInput className="!w-80" value={s.defaultBand} disabled={!editable}
            onChange={e => mut(x => { x.defaultBand = e.target.value; }, null)} />
        </Field>
      </div>
    </div>
  );
}

// ============================================================================
// HARDWARE
// ============================================================================
function CwHardwarePanel({ ctx, project, system, editable }) {
  const s = cwSettings(project);
  const [importing, setImporting] = useState(false);
  const [q, setQ] = useState('');
  const mut = (fn, action) => cwSetSettings(ctx, project, fn, action);
  const catalog = useMemo(() => {
    const all = (typeof supplierCatalog === 'function') ? supplierCatalog() : [];
    return all.filter(r => CW_HARDWARE_CATS.test(r.cat || ''));
  }, []);
  const shown = catalog.filter(r => !q.trim() || `${r.name} ${r.code} ${r.cat} ${r.supLabel}`.toLowerCase().indexOf(q.trim().toLowerCase()) >= 0);
  function guessKind(cat) {
    if (/hinge/i.test(cat)) return 'Hinge';
    if (/slide|drawer/i.test(cat)) return 'Slide';
    if (/handle|knob|pull/i.test(cat)) return 'Handle';
    return 'Other';
  }
  function addFromCatalog(r) {
    mut(x => {
      if (x.hardware.some(h => h.productNumber === r.code && r.code)) return;
      x.hardware.push(cwMakeHardwareItem({
        name: r.name, category: r.cat || 'Other', kind: guessKind(r.cat || ''),
        productNumber: r.code, manufacturer: r.supLabel || '', img: r.img,
        supplierKey: r.sup, supplierId: r.id,
      }));
    }, `LEON Casework — "${r.name}" added from the supplier catalog.`);
  }
  const drillFields = {
    Hinge: [['cupDia', 'Cup ⌀'], ['cupSetback', 'Cup setback'], ['plateOffset', 'Plate offset']],
    Slide: [['slideLength', 'Length'], ['slideSideClearance', 'Clearance per side'], ['slideSetback', 'Setback']],
    Handle: [['boreCentres', 'Hole centres']],
    Other: [],
  };
  return (
    <div className="space-y-5">
      <CwLibPanel title="Hardware on this job" editable={editable} items={s.hardware} minWidth={1020}
        blurb="Pulled from the supplier catalog rather than retyped — Imundex alone ships 76 hinges, 42 runners and 87 handles with photos and codes already on file. The drilling numbers are what create the machining: a hinge makes the cup bore, a runner sets the drawer box, a handle sets the hole centres. Left blank, the engine says so instead of inventing a pattern."
        addLabel="Add from the catalog"
        onAdd={() => setImporting(true)}
        onRemove={id => mut(x => { x.hardware = x.hardware.filter(h => h.id !== id); }, 'LEON Casework — hardware item removed.')}
        columns={['Item', 'Kind', 'Product no.', 'Supplier', 'Drilling data', 'Numbers']}
        renderRow={h => [
          <td key="n" className="px-2 py-1">
            <div className="flex items-center gap-2">
              {h.img && <img src={h.img} alt="" className="w-8 h-8 object-contain bg-[var(--leon-cream)] rounded" />}
              <span className="font-semibold">{h.name}</span>
            </div></td>,
          <td key="k" className="px-2 py-1">
            <select value={h.kind} disabled={!editable}
              onChange={e => mut(x => { const t = x.hardware.find(y => y.id === h.id); if (t) t.kind = e.target.value; }, null)}
              className="px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
              {['Hinge', 'Slide', 'Handle', 'Other'].map(k => <option key={k}>{k}</option>)}
            </select></td>,
          <td key="p" className="px-2 py-1">{h.productNumber || '—'}</td>,
          <td key="m" className="px-2 py-1 text-[var(--leon-black)]/55">{h.manufacturer || '—'}</td>,
          <td key="d" className="px-2 py-1">
            {cwDrillingKnown(h) ? <Badge tone="green">loaded</Badge> : <Badge tone="yellow">not loaded</Badge>}</td>,
          <td key="f" className="px-2 py-1">
            <div className="flex gap-1.5 flex-wrap">
              {(drillFields[h.kind] || []).map(([f, lbl]) => (
                <label key={f} className="text-[10px] text-[var(--leon-black)]/55">
                  {lbl}
                  <CwDimField value={h[f]} system={system} disabled={!editable} w="w-16"
                    onChange={v => mut(x => { const t = x.hardware.find(y => y.id === h.id); if (t) t[f] = v; },
                      `LEON Casework — ${h.name} ${lbl.toLowerCase()} set.`)} />
                </label>
              ))}
              {!(drillFields[h.kind] || []).length && <span className="text-[11px] text-[var(--leon-black)]/35">no geometry</span>}
            </div></td>,
        ]} />

      <CwHardwareSets ctx={ctx} project={project} editable={editable} />

      <Modal open={importing} onClose={() => setImporting(false)} wide title="Add hardware from the supplier catalog"
        footer={<Button onClick={() => setImporting(false)}>Done</Button>}>
        <div className="space-y-3">
          <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Search hinge, runner, handle…" />
          <p className="text-xs text-[var(--leon-black)]/55">
            {catalog.length} cabinet hardware items are in the supplier catalog. Adding one copies its details
            here; the catalog record stays the source. The drilling numbers are not in the catalog and are
            entered once, on the item.
          </p>
          <div className="grid gap-2 sm:grid-cols-2 max-h-[420px] overflow-y-auto">
            {shown.slice(0, 100).map(r => {
              const have = s.hardware.some(x => x.productNumber === r.code && r.code);
              return (
                <button key={`${r.sup}:${r.id}`} disabled={have || !editable} onClick={() => addFromCatalog(r)}
                  className={`flex items-center gap-2 rounded border p-2 text-left ${have ? 'opacity-45 border-[var(--leon-line)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown)]'}`}>
                  {r.img && <img src={r.img} alt="" className="w-10 h-10 object-contain bg-[var(--leon-cream)] rounded" />}
                  <span className="min-w-0">
                    <span className="block text-xs font-semibold truncate">{r.name}</span>
                    <span className="block text-[10px] text-[var(--leon-black)]/45">{r.supLabel} · {r.cat} · {r.code}</span>
                  </span>
                  {have && <span className="ml-auto text-[10px] uppercase text-[var(--leon-black)]/40">added</span>}
                </button>
              );
            })}
          </div>
          {!catalog.length && <EmptyState text="No cabinet hardware in the supplier catalog yet." />}
        </div>
      </Modal>
    </div>
  );
}

function CwHardwareSets({ ctx, project, editable }) {
  const s = cwSettings(project);
  const mut = (fn, action) => cwSetSettings(ctx, project, fn, action);
  return (
    <div className="space-y-2">
      <div className="flex items-center justify-between gap-3">
        <div>
          <h3 className="font-bold">Hardware sets</h3>
          <p className="text-sm text-[var(--leon-black)]/55">
            A set is a recipe. Assigning HW-02 to sixty drawer banks fills in the runners, the pulls and the
            counts on all sixty — and the runner in it is what sets those sixty drawer boxes' width and depth.
          </p>
        </div>
        {editable && <Button size="sm" onClick={() => mut(x => x.hardwareSets.push(cwMakeHardwareSet({
          code: `HW-${String(x.hardwareSets.length + 1).padStart(2, '0')}` })), 'LEON Casework — hardware set added.')}>+ Add set</Button>}
      </div>
      {!s.hardwareSets.length && <EmptyState text="No hardware sets yet." />}
      {s.hardwareSets.map(set => (
        <Collapsible key={set.id} id={`cwhwset-${set.id}`} title={`${set.code} — ${set.name}`} count={(set.lines || []).length}>
          <div className="space-y-2">
            <div className="grid grid-cols-2 gap-3">
              <Field label="Code"><TextInput value={set.code} disabled={!editable}
                onChange={e => mut(x => { const t = x.hardwareSets.find(y => y.id === set.id); if (t) t.code = e.target.value; }, null)} /></Field>
              <Field label="Name"><TextInput value={set.name} disabled={!editable}
                onChange={e => mut(x => { const t = x.hardwareSets.find(y => y.id === set.id); if (t) t.name = e.target.value; }, null)} /></Field>
            </div>
            {(set.lines || []).map((l, i) => (
              <div key={i} className="flex items-center gap-2">
                <input type="number" value={l.qty} disabled={!editable}
                  onChange={e => mut(x => { const t = x.hardwareSets.find(y => y.id === set.id); if (t) t.lines[i].qty = Number(e.target.value) || 1; }, null)}
                  className="w-14 px-1 py-1 text-sm border border-[var(--leon-line)] rounded" />
                <select value={l.itemId || ''} disabled={!editable}
                  onChange={e => mut(x => { const t = x.hardwareSets.find(y => y.id === set.id); if (t) t.lines[i].itemId = e.target.value; }, null)}
                  className="flex-1 px-2 py-1 text-sm border border-[var(--leon-line)] rounded bg-white">
                  <option value="">— pick an item —</option>
                  {s.hardware.map(h => <option key={h.id} value={h.id}>{h.name} ({h.kind})</option>)}
                </select>
                <select value={l.per || 'cabinet'} disabled={!editable}
                  onChange={e => mut(x => { const t = x.hardwareSets.find(y => y.id === set.id); if (t) t.lines[i].per = e.target.value; }, null)}
                  className="px-2 py-1 text-sm border border-[var(--leon-line)] rounded bg-white">
                  <option value="cabinet">per cabinet</option><option value="door">per door</option><option value="drawer">per drawer</option>
                </select>
                {editable && <button className="text-red-600"
                  onClick={() => mut(x => { const t = x.hardwareSets.find(y => y.id === set.id); if (t) t.lines = t.lines.filter((_, k) => k !== i); }, null)}>✕</button>}
              </div>
            ))}
            {editable && (
              <div className="flex gap-2">
                <button className="text-xs font-semibold text-[var(--leon-brown)]"
                  onClick={() => mut(x => { const t = x.hardwareSets.find(y => y.id === set.id); if (t) t.lines = (t.lines || []).concat([{ itemId: '', qty: 1, per: 'cabinet' }]); }, null)}>+ Add item</button>
                <button className="text-xs font-semibold text-red-600 ml-auto"
                  onClick={() => { if (confirm(`Remove ${set.code}?`)) mut(x => { x.hardwareSets = x.hardwareSets.filter(y => y.id !== set.id); }, `LEON Casework — hardware set ${set.code} removed.`); }}>Remove set</button>
              </div>
            )}
          </div>
        </Collapsible>
      ))}
    </div>
  );
}

// ============================================================================
// PANELS & BOM
// ============================================================================
// Every trim run on the job, gathered. Trim is bought in sticks and ordered as
// linear feet, so it belongs beside the panel BOM rather than only on the run
// it was ticked on — otherwise somebody counts it off the elevations again,
// which is the exact work this was built to remove.
function cwAllTrimRuns(project, ctx, roomFilter) {
  const con = cwConstruction(project, null);
  const out = [];
  cwRooms(project).forEach(room => {
    if (roomFilter && roomFilter !== 'all' && room.id !== roomFilter) return;
    const ct = cwCaseworkTypes(project).find(t => t.id === room.caseworkTypeId);
    if (!ct) return;
    (ct.walls || []).forEach(wall => {
      (ct.runs || []).filter(r => r.wallId === wall.id).forEach(run => {
        const layout = cwRunLayout(project, ctx, room, wall, run);
        cwTrimRuns(project, layout, run, con).forEach(t => {
          out.push(Object.assign({}, t, {
            roomName: room.unit || room.name, wallName: wall.name, runName: run.name,
          }));
        });
      });
    });
  });
  return out;
}
function CwTrimSchedule({ project, ctx, system, scope }) {
  const rows = cwAllTrimRuns(project, ctx, scope);
  if (!rows.length) return null;
  const byKind = {};
  rows.forEach(r => {
    const k = `${r.key}|${r.profile || ''}`;
    if (!byKind[k]) byKind[k] = { label: r.label, profile: r.profile, lengthIn: 0, sticks: 0, stickIn: r.stickIn };
    byKind[k].lengthIn += r.lengthIn; byKind[k].sticks += r.sticks;
  });
  const totals = Object.keys(byKind).map(k => byKind[k]);
  return (
    <Collapsible id="cw-trim-sched" title={`Trim runs (${rows.length})`}
      right={<span className="text-xs text-[var(--leon-black)]/50">
        {(totals.reduce((n, t) => n + t.lengthIn, 0) / 12).toFixed(1)} lin ft
      </span>}>
      <div className="overflow-x-auto">
        <table className="w-full text-sm">
          <thead className="bg-[var(--leon-cream)] text-[11px] uppercase tracking-wide text-[var(--leon-black)]/55">
            <tr><th className="text-left px-2 py-1">Trim</th><th className="text-left px-2 py-1">Profile</th>
              <th className="text-left px-2 py-1">Where</th><th className="text-right px-2 py-1">Length</th>
              <th className="text-right px-2 py-1">Sticks</th></tr>
          </thead>
          <tbody>
            {rows.map((r, i) => (
              <tr key={i} className="border-t border-[var(--leon-line)]">
                <td className="px-2 py-1">{r.label}</td>
                <td className="px-2 py-1 text-[var(--leon-black)]/60">{r.profile || <span className="opacity-40">not specified</span>}</td>
                <td className="px-2 py-1 text-[var(--leon-black)]/60">{r.roomName} · {r.wallName} · {r.runName}</td>
                <td className="px-2 py-1 text-right tabular-nums">{fmtDim(r.lengthIn, system, { inchesOnly: true })}</td>
                <td className="px-2 py-1 text-right tabular-nums">{r.sticks}</td>
              </tr>
            ))}
            {totals.map((t, i) => (
              <tr key={`t${i}`} className="border-t-2 border-[var(--leon-line)] font-bold bg-[var(--leon-cream)]/40">
                <td className="px-2 py-1">{t.label}</td>
                <td className="px-2 py-1 font-normal text-[var(--leon-black)]/60">{t.profile || '—'}</td>
                <td className="px-2 py-1 font-normal text-[11px] text-[var(--leon-black)]/45">to order</td>
                <td className="px-2 py-1 text-right tabular-nums">{(t.lengthIn / 12).toFixed(1)} lin ft</td>
                <td className="px-2 py-1 text-right tabular-nums">{t.sticks}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <p className="text-[11px] text-[var(--leon-black)]/45 mt-1.5">
        Measured from the runs themselves, never typed &mdash; a cabinet added tomorrow lengthens the crown
        with it. Sticks are rounded up per run, because that is how trim is cut: the offcut from one run
        does not start the next one across the room.
      </p>
    </Collapsible>
  );
}

function CwPanelsPanel({ ctx, project, system, editable }) {
  const [scope, setScope] = useState('all');
  const rooms = cwRooms(project);
  const all = cwItems(project);
  const cabs = scope === 'all' ? all : all.filter(c => c.roomId === scope);
  const bom = useMemo(() => cwBom(project, cabs), [project, scope, all.length, JSON.stringify(cabs.map(c => [c.id, c.width, c.height, c.depth, c.qty, c.typeId, c.rows]))]);
  const [openPanel, setOpenPanel] = useState(null);

  function exportCsv() {
    const cols = [{ key: 'partNo', label: 'Part no.' }, { key: 'name', label: 'Part' }, { key: 'mark', label: 'Cabinet' },
      { key: 'unitLabel', label: 'Unit' }, { key: 'group', label: 'Group' }, { key: 'materialName', label: 'Material' },
      { key: 'thickness', label: 'Thickness mm' }, { key: 'length', label: 'Length mm' }, { key: 'width', label: 'Width mm' },
      { key: 'qty', label: 'Qty' }, { key: 'grain', label: 'Grain' }, { key: 'match', label: 'Match' },
      { key: 'edges', label: 'Banded edges' }, { key: 'band', label: 'Band mm' }, { key: 'math', label: 'How it was sized' }];
    const rows = bom.panels.map(p => Object.assign({}, p, {
      match: p.matchMode,
      edges: CW_EDGES.filter(e => (p.edges || {})[e]).map(e => CW_EDGE_LABELS[e]).join(' + '),
      band: Math.round(cwPanelBandLength(p)),
    }));
    downloadCsv(`casework-bom-${project.name || project.id}`, cols, rows);
  }

  return (
    <div className="space-y-4">
      <div className="flex items-end gap-2 flex-wrap">
        <Field label="Show">
          <Select className="!w-56" value={scope} onChange={e => setScope(e.target.value)}>
            <option value="all">Whole project</option>
            {rooms.map(r => <option key={r.id} value={r.id}>{r.unit || r.name}</option>)}
          </Select>
        </Field>
        <div className="ml-auto flex gap-2">
          <Button size="sm" variant="ghost" onClick={exportCsv}>Export CSV</Button>
        </div>
      </div>

      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
        <CwStat label="Cabinets" value={cabs.reduce((a, c) => a + (cwNum(c.qty) || 1), 0)} />
        <CwStat label="Panels" value={bom.totalPanels} />
        <CwStat label="Panel area" value={`${cwRound(bom.totalArea)} m²`} />
        <CwStat label="Edge band" value={`${cwRound(bom.totalBand / 1000)} m`} />
      </div>

      <CwTrimSchedule project={project} ctx={ctx} system={system} scope={scope} />

      {!cabs.length && <EmptyState text="No cabinets in this selection." />}

      {!!bom.bands.length && (
        <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
          <div className="font-semibold text-sm mb-1.5">Edge banding, by band</div>
          <p className="text-[11px] text-[var(--leon-black)]/55 mb-2">
            Per edge, not one project total: each panel carries its own four edges and they roll up here, by
            material below, and by cabinet in the designer.
          </p>
          <div className="flex flex-wrap gap-2">
            {bom.bands.map(b => (
              <span key={b.name} className="rounded border border-[var(--leon-line)] px-2.5 py-1 text-xs">
                <b>{b.name}</b> — {cwRound(b.mm / 1000)} m
              </span>
            ))}
          </div>
        </div>
      )}

      {bom.groups.map(g => (
        <Collapsible key={g.id || g.name} id={`cwbom-${g.id || g.name}`}
          title={`${g.name} — ${fmtDim(g.thickness, system, { inchesOnly: true })}`} count={g.qty}
          right={<span className="text-xs text-[var(--leon-black)]/55">{cwRound(g.area)} m² · band {cwRound(g.band / 1000)} m</span>}>
          <div className="overflow-x-auto">
            <table className="w-full text-xs" style={{ minWidth: 1000 }}>
              <thead>
                <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
                  <th className="px-2 py-1.5">Part no.</th><th className="px-2 py-1.5">Part</th>
                  <th className="px-2 py-1.5">Length</th><th className="px-2 py-1.5">Width</th><th className="px-2 py-1.5">Qty</th>
                  <th className="px-2 py-1.5">Grain</th><th className="px-2 py-1.5">Match</th>
                  <th className="px-2 py-1.5">Edges</th><th className="px-2 py-1.5">Band</th>
                </tr>
              </thead>
              <tbody>
                {g.panels.map((p, i) => (
                  <tr key={`${p.cabinetId}-${p.key}-${i}`}
                    className={`border-b border-[var(--leon-line)]/60 cursor-pointer hover:bg-[var(--leon-cream)]/50 ${p.overridden ? 'bg-amber-50' : ''}`}
                    onClick={() => setOpenPanel(p)}>
                    <td className="px-2 py-1 text-[var(--leon-black)]/55">{p.partNo}</td>
                    <td className="px-2 py-1 font-semibold">{p.name}</td>
                    <td className="px-2 py-1 tabular-nums">{fmtDim(p.length, system, { inchesOnly: true })}</td>
                    <td className="px-2 py-1 tabular-nums">{fmtDim(p.width, system, { inchesOnly: true })}</td>
                    <td className="px-2 py-1 tabular-nums">{p.qty}</td>
                    <td className="px-2 py-1">{p.grain === 'None' ? <span className="text-[var(--leon-black)]/30">free</span> : p.grain}</td>
                    <td className="px-2 py-1">{cwMatchIsGrouped(p.matchMode) ? <Badge tone="brown">{p.matchMode}</Badge> : <span className="text-[var(--leon-black)]/40">{p.matchMode}</span>}</td>
                    <td className="px-2 py-1">{CW_EDGES.filter(e => (p.edges || {})[e]).map(e => e[0].toUpperCase()).join(' ') || '—'}</td>
                    <td className="px-2 py-1 tabular-nums">{cwRound(cwPanelBandLength(p) / 1000)} m</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </Collapsible>
      ))}

      <Modal open={!!openPanel} wide onClose={() => setOpenPanel(null)} title={openPanel ? openPanel.name : ''}
        footer={<Button variant="ghost" onClick={() => setOpenPanel(null)}>Close</Button>}>
        {openPanel && (
          <div className="space-y-3">
            <div className="bg-[var(--leon-cream)] rounded p-3"><CwPanelView panel={openPanel} system={system} height={200} /></div>
            <table className="w-full text-sm">
              <tbody>
                {[['Part number', openPanel.partNo],
                  ['Traceability', `${openPanel.caseworkTypeCode || 'one-off'} → ${openPanel.unitLabel || '—'} → ${openPanel.mark} → ${openPanel.key}`],
                  ['Material', `${openPanel.materialName} · ${fmtDim(openPanel.thickness, system, { inchesOnly: true })}`],
                  ['Size', `${fmtDim(openPanel.length, system, { inchesOnly: true })} × ${fmtDim(openPanel.width, system, { inchesOnly: true })} × ${openPanel.qty} off`],
                  ['Grain', `${openPanel.grain}${cwMatchIsGrouped(openPanel.matchMode) ? ` · ${openPanel.matchMode} match, kept together in the nest` : ''}`],
                  ['Banded edges', CW_EDGES.filter(e => (openPanel.edges || {})[e]).map(e => `${CW_EDGE_LABELS[e]} — ${openPanel.edges[e]}`).join('; ') || 'none'],
                  ['Band length', `${cwRound(cwPanelBandLength(openPanel) / 1000)} m`],
                  ['How it was sized', openPanel.math]].map(([k, v]) => (
                  <tr key={k} className="border-b border-[var(--leon-line)]/60">
                    <td className="py-1.5 text-[var(--leon-black)]/50 w-40 align-top">{k}</td>
                    <td className="py-1.5">{v}</td>
                  </tr>
                ))}
              </tbody>
            </table>
            {!!(openPanel.ops || []).length && (
              <div>
                <div className="text-sm font-semibold mb-1">Machining</div>
                {(openPanel.ops || []).map((o, i) => (
                  <div key={i} className={`text-xs py-0.5 ${o.unknown ? 'text-amber-800' : ''}`}>
                    {o.kind}{o.x != null ? ` at (${cwN(o.x)}, ${cwN(o.y)})${o.dia ? ` ⌀${cwN(o.dia)}` : ''}` : ''}
                    {o.note ? ` — ${o.note}` : ''}
                  </div>
                ))}
                <p className="text-[11px] text-[var(--leon-black)]/45 mt-1">
                  Structured operations, not machine code. This module does not emit a CNC program.
                </p>
              </div>
            )}
          </div>
        )}
      </Modal>
    </div>
  );
}

// ============================================================================
// NESTING
// ============================================================================
function CwNestingPanel({ ctx, project, system, editable }) {
  const s = cwSettings(project);
  const rooms = cwRooms(project);
  const all = cwItems(project);
  const [scope, setScope] = useState('all');
  const [matId, setMatId] = useState(s.materials[0] ? s.materials[0].id : '');
  const [mode, setMode] = useState(s.nest.mode || 'minSheets');
  const cabs = scope === 'all' ? all : all.filter(c => c.roomId === scope);
  const mat = cwMaterial(project, matId);
  const bom = useMemo(() => cwBom(project, cabs), [project, scope, all.length,
    JSON.stringify(cabs.map(c => [c.id, c.width, c.height, c.depth, c.qty, c.typeId, c.rows]))]);
  const panels = bom.panels.filter(p => p.materialId === matId);
  const remnants = (s.panelRemnants || []).filter(r => r.materialId === matId);

  const opts = {
    kerf: s.nest.kerf, trim: s.nest.trim,
    sheetW: mat ? mat.sheetW : cwIn(48), sheetH: mat ? mat.sheetH : cwIn(96),
    minOffcutW: s.nest.minOffcutW, minOffcutH: s.nest.minOffcutH,
    remnants: remnants.map(r => ({ id: r.id, w: r.w, h: r.h })),
  };
  // Both modes are computed every time so the comparison is real rather than a
  // claim — a shop deciding between them wants the two sheet counts side by side.
  const nestMin = useMemo(() => cwNest(panels, Object.assign({}, opts, { mode: 'minSheets' })),
    [JSON.stringify(panels.map(p => [p.key, p.cabinetId, p.length, p.width, p.qty, p.grain, p.grainGroup])), JSON.stringify(opts)]);
  const nestGrain = useMemo(() => cwNest(panels, Object.assign({}, opts, { mode: 'grain' })),
    [JSON.stringify(panels.map(p => [p.key, p.cabinetId, p.length, p.width, p.qty, p.grain, p.grainGroup])), JSON.stringify(opts)]);
  const nest = mode === 'grain' ? nestGrain : nestMin;

  const mut = (fn, action) => cwSetSettings(ctx, project, fn, action);
  function saveOffcuts() {
    const keep = nest.offcuts.map(o => ({ id: uid('cwrem'), materialId: matId, w: cwRound(o.w), h: cwRound(o.h),
      fromDate: todayISO(), note: `From a ${mode === 'grain' ? 'maintain-grain' : 'minimum-sheets'} nest of ${project.name}.` }));
    if (!keep.length) { alert('This nest leaves nothing above the minimum offcut size.'); return; }
    mut(x => { x.panelRemnants = (x.panelRemnants || []).concat(keep); },
      `LEON Casework — ${keep.length} offcut(s) kept as panel remnants.`);
  }

  return (
    <div className="space-y-4">
      <div>
        <h3 className="font-bold">Sheet nesting</h3>
        <p className="text-sm text-[var(--leon-black)]/55 max-w-3xl">
          Real rectangles placed on real sheets, honouring saw kerf, grain and rotation. It is a
          <b> heuristic</b> — first-fit-decreasing with shelf packing — so it produces a <b>good proposal a
          person approves</b>, not a proven optimum and never a silent production decision. Compare the two
          modes below before you cut anything.
        </p>
      </div>

      <div className="flex items-end gap-2 flex-wrap">
        <Field label="Show">
          <Select className="!w-48" value={scope} onChange={e => setScope(e.target.value)}>
            <option value="all">Whole project</option>
            {rooms.map(r => <option key={r.id} value={r.id}>{r.unit || r.name}</option>)}
          </Select>
        </Field>
        <Field label="Material">
          <Select className="!w-64" value={matId} onChange={e => setMatId(e.target.value)}>
            {s.materials.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
          </Select>
        </Field>
        <Field label="Saw kerf"><CwDimField value={s.nest.kerf} system={system} disabled={!editable} w="w-20"
          onChange={v => mut(x => { x.nest.kerf = cwNum(v); }, `LEON Casework — saw kerf set to ${cwN(v)} mm.`)} /></Field>
        <Field label="Sheet trim"><CwDimField value={s.nest.trim} system={system} disabled={!editable} w="w-20"
          onChange={v => mut(x => { x.nest.trim = cwNum(v); }, null)} /></Field>
        <Field label="Min offcut W"><CwDimField value={s.nest.minOffcutW} system={system} disabled={!editable} w="w-20"
          onChange={v => mut(x => { x.nest.minOffcutW = cwNum(v); }, null)} /></Field>
        <Field label="Min offcut H"><CwDimField value={s.nest.minOffcutH} system={system} disabled={!editable} w="w-20"
          onChange={v => mut(x => { x.nest.minOffcutH = cwNum(v); }, null)} /></Field>
      </div>

      {!mat && <EmptyState text="Pick a material." />}
      {mat && !panels.length && <EmptyState text={`No parts on ${mat.name} in this selection.`} />}

      {mat && !!panels.length && (
        <>
          <div className="grid gap-3 sm:grid-cols-2">
            {[['minSheets', 'Minimum sheets', nestMin,
               'Ungrained parts are turned freely to close gaps. A grained part still keeps its direction — this mode buys sheets, not grain.'],
              ['grain', 'Maintain grain', nestGrain,
               'Nothing is turned at all, and every matched group lands on one sheet. Usually costs a sheet or two; it is what makes the veneer read.']]
              .map(([k, label, n, note]) => (
              <button key={k} onClick={() => setMode(k)}
                className={`text-left rounded-lg border p-3 ${mode === k ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] bg-white hover:border-[var(--leon-brown)]/50'}`}>
                <div className="flex items-baseline gap-2">
                  <span className="font-bold">{label}</span>
                  {mode === k && <Badge tone="brown">shown</Badge>}
                </div>
                <div className="text-2xl font-bold text-[var(--leon-brown)] tabular-nums">
                  {n.fullSheets} sheet{n.fullSheets === 1 ? '' : 's'}
                  {n.remnantsUsed ? <span className="text-sm font-semibold text-[#3a7d44]"> + {n.remnantsUsed} remnant{n.remnantsUsed === 1 ? '' : 's'}</span> : null}
                </div>
                <div className="text-xs text-[var(--leon-black)]/55">{cwRound(n.wastePct)}% waste · {cwRound(n.partsArea / 1e6)} m² of parts</div>
                <div className="text-[11px] text-[var(--leon-black)]/50 mt-1">{note}</div>
                {!!n.split.length && <div className="text-[11px] text-amber-800 mt-1">{n.split.length} matched group(s) had to be split.</div>}
                {!!n.unplaced.length && <div className="text-[11px] text-red-700 mt-1">{n.unplaced.length} part(s) do not fit a sheet at all.</div>}
              </button>
            ))}
          </div>

          {!!nest.notes.length && <CwIssueList issues={nest.notes.map(n => ({ level: 'warn', msg: n }))} />}
          {!!nest.unplaced.length && (
            <CwIssueList issues={nest.unplaced.map(u => ({ level: 'error',
              msg: `${u.panel.mark} ${u.panel.key} is ${cwN(u.L)} × ${cwN(u.W)} and does not fit a ${cwN(opts.sheetW)} × ${cwN(opts.sheetH)} sheet. It has to be made from two pieces or from a different material.` }))} />
          )}

          <div className="flex items-center gap-2 flex-wrap">
            <span className="text-sm text-[var(--leon-black)]/60">
              {remnants.length} remnant{remnants.length === 1 ? '' : 's'} of {mat.name} on the shelf
              {nest.remnantsUsed ? `, ${nest.remnantsUsed} used by this nest` : ''}.
              Remnants are always tried before a new sheet is opened.
            </span>
            {editable && <Button size="sm" variant="ghost" onClick={saveOffcuts}>
              Keep this nest's {nest.offcuts.length} usable offcut{nest.offcuts.length === 1 ? '' : 's'}
            </Button>}
            {editable && !!remnants.length && <Button size="sm" variant="ghost"
              onClick={() => { if (confirm('Clear every remnant of this material?')) mut(x => { x.panelRemnants = (x.panelRemnants || []).filter(r => r.materialId !== matId); }, 'LEON Casework — panel remnants cleared.'); }}>
              Clear remnants</Button>}
          </div>

          <div className="grid gap-4 md:grid-cols-2">
            {nest.sheets.map((sh, i) => (
              <div key={i} className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
                <div className="flex items-center justify-between mb-1.5">
                  <div className="text-sm font-bold">
                    {sh.remnantId ? 'Remnant' : `Sheet ${i + 1}`}
                    <span className="ml-2 text-[11px] font-normal text-[var(--leon-black)]/50">
                      {fmtDim(sh.w, system, { inchesOnly: true })} × {fmtDim(sh.h, system, { inchesOnly: true })}
                    </span>
                  </div>
                  <Badge>{sh.placements.length} part{sh.placements.length === 1 ? '' : 's'}</Badge>
                </div>
                <CwNestView nest={nest} sheet={sh} index={i} system={system} />
                <div className="text-[10px] text-[var(--leon-black)]/45 mt-1">
                  Green dashed areas are offcuts above the minimum. ↻ marks a part that has been turned.
                </div>
              </div>
            ))}
          </div>
        </>
      )}
    </div>
  );
}

// ============================================================================
// THE SUBMITTAL HALF
// Read out of the 55 India Rev 03 set. Everything below models something that
// was, until now, text on a slide: the architect's finish tags, their keynotes
// with our answers, and the submittal record with its revision log.
// ============================================================================

// The three shared structures are filtered by DISCIPLINE, so LEON Casework and
// LEON Doors each show only their own. They are stored together because they
// are one idea; they are shown apart because a door submittal and a casework
// submittal are two packages, issued and approved separately. A record with no
// discipline is casework, which is what everything saved before this was.
function cwDisc(list, discipline) {
  const want = discipline || 'casework';
  return (list || []).filter(x => (x.discipline || 'casework') === want);
}
function cwFinishTags(project, discipline) { return cwDisc(project && project.caseworkFinishTags, discipline); }
function cwKeynotes(project, discipline) { return cwDisc(project && project.caseworkKeynotes, discipline); }
function cwSubmittals(project, discipline) { return cwDisc(project && project.caseworkSubmittals, discipline); }
// What the panels call themselves, so one component serves both tools without
// a door screen saying "casework".
const CW_DISCIPLINE_LABEL = { casework: 'Casework', doors: 'Door' };
function cwDiscLabel(d) { return CW_DISCIPLINE_LABEL[d || 'casework'] || 'Casework'; }
// A tag that has been re-specified resolves to whatever replaced it, however
// many times it has moved. The chain is walked rather than followed once,
// because WD-2 -> WD-3 -> WD-3A is a normal thing for a job to do, and it is
// bounded so a circular supersede cannot hang the page.
function cwResolveTag(project, tagId) {
  const all = cwFinishTags(project);
  let t = all.find(x => x.id === tagId) || null;
  for (let hops = 0; t && t.supersededById && hops < 20; hops++) {
    const nxt = all.find(x => x.id === t.supersededById);
    if (!nxt) break;
    t = nxt;
  }
  return t;
}
function cwTagByCode(project, code) {
  const c = String(code || '').trim().toUpperCase();
  return cwFinishTags(project).find(t => t.code === c) || null;
}

// ---- finish tags -----------------------------------------------------------

function CwFinishTagsPanel({ ctx, project, editable, discipline }) {
  const tags = cwFinishTags(project, discipline);
  const [adding, setAdding] = useState(false);
  const [editId, setEditId] = useState(null);
  const [q, setQ] = useState('');
  const live = tags.filter(t => t.active !== false && !t.supersededById);
  const retired = tags.filter(t => t.active === false || t.supersededById);
  const needle = q.trim().toLowerCase();
  const match = t => !needle || `${t.code} ${t.name} ${t.appliesTo}`.toLowerCase().includes(needle);

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-3 flex-wrap">
        <div>
          <h3 className="font-bold">🏷️ Finish tags</h3>
          <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
            The architect&rsquo;s code for a material &mdash; <b>WD-2</b>, <b>MW-4</b>, <b>CT-1</b>.
            Every callout on a drawing points at one of these, so re-specifying a material is one edit
            here rather than a hunt through the set. Point a tag at a real supplier finish and the
            drawing can be checked against what will actually be ordered.
          </p>
        </div>
        <div className="flex items-end gap-2">
          <Field label="Find">
            <TextInput className="!w-44" value={q} onChange={e => setQ(e.target.value)} placeholder="Code or name" />
          </Field>
          {editable && <Button size="sm" onClick={() => setAdding(true)}>+ Tag</Button>}
        </div>
      </div>

      {!live.length && !retired.length && (
        <EmptyState text="No finish tags yet. Add the architect's schedule codes and every callout can reference them." />
      )}

      {!!live.filter(match).length && (
        <div className="overflow-x-auto rounded-lg border border-[var(--leon-line)]">
          <table className="w-full text-sm">
            <thead className="bg-[var(--leon-cream)]">
              <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/50">
                <th className="px-3 py-2">Tag</th>
                <th className="px-3 py-2">What it is</th>
                <th className="px-3 py-2">Applies to</th>
                <th className="px-3 py-2">Specified as</th>
                <th className="px-3 py-2 w-20"></th>
              </tr>
            </thead>
            <tbody>
              {live.filter(match).map(t => {
                const fam = cwTagFamilyFor(t.code);
                return (
                  <tr key={t.id} className="border-t border-[var(--leon-line)]">
                    <td className="px-3 py-2">
                      <span className="font-mono font-semibold">[{t.code}]</span>
                      {fam && <div className="text-[10px] text-[var(--leon-black)]/40">{fam.label}</div>}
                    </td>
                    <td className="px-3 py-2">
                      <div className="font-medium">{t.name || <span className="opacity-40">not named</span>}</div>
                      {t.description && <div className="text-[11px] text-[var(--leon-black)]/50">{t.description}</div>}
                    </td>
                    <td className="px-3 py-2 text-[var(--leon-black)]/60">{t.appliesTo || '—'}</td>
                    <td className="px-3 py-2">
                      {t.finishRef ? (
                        <span className="inline-flex items-center gap-1.5">
                          {t.finishRef.img && <img src={t.finishRef.img} alt="" className="w-7 h-7 rounded object-cover border border-[var(--leon-line)]" />}
                          <span className="leading-tight">
                            <span className="block text-xs font-semibold">{t.finishRef.name}</span>
                            <span className="block text-[10px] text-[var(--leon-black)]/45">
                              {t.finishRef.code}{t.finishRef.supLabel ? ` · ${t.finishRef.supLabel}` : ''}
                            </span>
                          </span>
                        </span>
                      ) : (
                        // Said plainly rather than left blank: a tag with no
                        // product behind it is exactly the callout nobody can
                        // check, which is the thing this screen exists to stop.
                        <span className="text-[11px] text-[var(--leon-brown)]">not yet specified</span>
                      )}
                    </td>
                    <td className="px-3 py-2 text-right">
                      {editable && (
                        <button onClick={() => setEditId(t.id)} className="text-xs font-semibold text-[var(--leon-brown)]">Edit</button>
                      )}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}

      {!!retired.length && (
        <Collapsible id={`cw-tags-retired-${project.id}`} title="Re-specified and retired" count={retired.length}>
          <div className="space-y-1.5">
            {retired.map(t => {
              const to = t.supersededById ? cwResolveTag(project, t.id) : null;
              return (
                <div key={t.id} className="flex items-center gap-2 text-xs px-2 py-1.5 rounded border border-[var(--leon-line)]">
                  <span className="font-mono font-semibold opacity-60">[{t.code}]</span>
                  <span className="opacity-60">{t.name}</span>
                  {to && to.id !== t.id && (
                    <span className="ml-auto text-[var(--leon-brown)] font-semibold">
                      now [{to.code}]{t.supersededDate ? ` · ${fmtDate(t.supersededDate)}` : ''}
                    </span>
                  )}
                </div>
              );
            })}
          </div>
          <p className="text-[11px] text-[var(--leon-black)]/50 mt-2">
            A retired code is kept, not deleted &mdash; every issued revision and every reviewer comment
            still quotes it, so it has to stay resolvable.
          </p>
        </Collapsible>
      )}

      <CwFinishTagModal ctx={ctx} project={project} discipline={discipline} open={adding || !!editId}
        tag={editId ? tags.find(t => t.id === editId) : null}
        onClose={() => { setAdding(false); setEditId(null); }} />
    </div>
  );
}

function CwFinishTagModal({ ctx, project, open, tag, onClose, discipline }) {
  const blank = { code: '', name: '', description: '', appliesTo: '' };
  const [form, setForm] = useState(blank);
  const [picking, setPicking] = useState(false);
  const [ref, setRef] = useState(null);
  const [superseding, setSuperseding] = useState(false);
  const [newCode, setNewCode] = useState('');
  useEffect(() => {
    if (!tag) setSuperseding(false);
    if (!open) return;
    setForm(tag ? { code: tag.code, name: tag.name, description: tag.description, appliesTo: tag.appliesTo } : blank);
    setRef(tag ? tag.finishRef : null);
    setPicking(false); setSuperseding(false); setNewCode('');
  }, [open, tag && tag.id]);
  const set = f => setForm(o => ({ ...o, ...f }));
  const dup = !tag && !!cwTagByCode(project, form.code);
  // `superseding` is component state and the modal is NOT remounted between
  // opens, so it survives a close — and the reset effect runs AFTER the render
  // that reopened it. Reopening on "+ Tag" therefore rendered the supersede
  // panel with no tag behind it and read `tag.code` off null, which blanked the
  // whole app. Both the panel and its button are gated on the tag actually
  // being there rather than on the flag alone.
  const canSupersede = superseding && !!tag;

  function save() {
    const code = form.code.trim().toUpperCase();
    if (!code) return;
    if (tag) {
      cwUpdate(ctx, project, draft => {
        const t = draft.caseworkFinishTags.find(x => x.id === tag.id);
        if (t) Object.assign(t, form, { code, finishRef: ref });
      }, `LEON Casework — finish tag [${code}] updated.`);
    } else {
      cwUpdate(ctx, project, draft => {
        draft.caseworkFinishTags.push(makeCwFinishTag({ ...form, code, finishRef: ref, discipline }, ctx.currentUserName));
      }, `LEON Casework — finish tag [${code}] added.`);
    }
    onClose();
  }

  // Re-specifying is not an edit. The old code stays, pointing at the new one,
  // because it is still quoted in every issued revision.
  function supersede() {
    const code = newCode.trim().toUpperCase();
    if (!code || !tag) return;
    cwUpdate(ctx, project, draft => {
      const fresh = makeCwFinishTag({ ...form, code, finishRef: ref, discipline }, ctx.currentUserName);
      draft.caseworkFinishTags.push(fresh);
      const old = draft.caseworkFinishTags.find(x => x.id === tag.id);
      if (old) { old.supersededById = fresh.id; old.supersededDate = todayISO(); }
    }, `LEON Casework — finish tag [${tag.code}] re-specified as [${code}].`);
    onClose();
  }

  return (
    <Modal open={open} onClose={onClose} wide title={tag ? `Finish tag [${tag.code}]` : 'New finish tag'}
      footer={
        <div className="flex items-center gap-2 w-full">
          {tag && !canSupersede && (
            <Button variant="ghost" onClick={() => { setSuperseding(true); setNewCode(''); }}>Re-specify…</Button>
          )}
          <div className="ml-auto flex gap-2">
            <Button variant="ghost" onClick={onClose}>Cancel</Button>
            {canSupersede
              ? <Button onClick={supersede} disabled={!newCode.trim()}>Supersede</Button>
              : <Button onClick={save} disabled={!form.code.trim() || dup}>{tag ? 'Save' : 'Add tag'}</Button>}
          </div>
        </div>
      }>
      <div className="space-y-3">
        {canSupersede ? (
          <div className="rounded-lg border border-[var(--leon-brown)] bg-[var(--leon-cream)] p-3 space-y-2">
            <p className="text-sm">
              <b>[{tag.code}]</b> stays on the job and points at the new code. It has to: every issued
              revision and every reviewer comment quotes it by name.
            </p>
            <Field label="New code" hint="What the architect has re-specified it as.">
              <TextInput className="!w-40 font-mono" value={newCode} placeholder="WD-3"
                onChange={e => setNewCode(e.target.value.toUpperCase())} />
            </Field>
          </div>
        ) : (
          <>
            <div className="grid sm:grid-cols-2 gap-3">
              <Field label="Code" hint="The architect's, exactly as they write it.">
                <TextInput className="font-mono" value={form.code} placeholder="WD-2"
                  onChange={e => set({ code: e.target.value.toUpperCase() })} />
              </Field>
              <Field label="Applies to" hint="Cabinet door, drawer box, shelf, panel…">
                <TextInput value={form.appliesTo} onChange={e => set({ appliesTo: e.target.value })} />
              </Field>
            </div>
            {dup && <p className="text-xs text-red-600">[{form.code.trim().toUpperCase()}] already exists on this job.</p>}
            <Field label="What it is">
              <TextInput value={form.name} onChange={e => set({ name: e.target.value })}
                placeholder="Rift white oak, horizontal grain" />
            </Field>
            <Field label="Notes">
              <TextInput value={form.description} onChange={e => set({ description: e.target.value })} />
            </Field>
          </>
        )}

        <Field label="Specified as" hint="A real catalogue product, so a callout can be checked against what gets ordered.">
          {ref ? (
            <div className="flex items-center gap-2 rounded border border-[var(--leon-line)] bg-white px-2 py-1.5">
              {ref.img && <img src={ref.img} alt="" className="w-9 h-9 rounded object-cover" />}
              <span className="leading-tight">
                <span className="block text-xs font-semibold">{ref.name}</span>
                <span className="block text-[10px] text-[var(--leon-black)]/45">{ref.code} · {ref.supLabel}</span>
              </span>
              <button onClick={() => setRef(null)} className="ml-auto text-[11px] font-semibold text-[var(--leon-brown)]">Change</button>
            </div>
          ) : (
            <button onClick={() => setPicking(true)} className="text-xs font-semibold text-[var(--leon-brown)]">
              + Pick a supplier finish
            </button>
          )}
        </Field>

        {picking && (
          <CwFinishPickerInline ctx={ctx} onClose={() => setPicking(false)}
            onPick={r => { setRef(makeSupplierFinishRef(r)); setPicking(false); }} />
        )}
      </div>
    </Modal>
  );
}

// The same vendor -> construction -> search the Selection Hub uses, inline.
// Deliberately not a second catalogue browser: one way to find a finish.
function CwFinishPickerInline({ ctx, onPick, onClose }) {
  // A separator that cannot occur inside a supplier key or a category name.
  // A space would split "Solid Surface" down the middle.
  const SEP = '\u0000';
  const [pick, setPick] = useState('');
  const [q, setQ] = useState('');
  const groups = useMemo(() => supplierGroups(), []);
  const [sup, cat] = pick ? pick.split(SEP) : ['', ''];
  const results = useMemo(() => (cat ? searchSupplierFinishes(sup, cat, q, 36) : []), [sup, cat, q]);
  return (
    <div className="rounded-lg border border-[var(--leon-brown)] bg-white p-2 space-y-2">
      <div className="flex items-center gap-2">
        <Select value={pick} onChange={e => { setPick(e.target.value); setQ(''); }} className="!py-1 !text-xs !w-64">
          <option value="">Vendor &amp; construction…</option>
          {groups.map(g => (
            <optgroup key={g.key} label={supplierDisplayName(g.key, ctx.vendors)}>
              {g.cats.map(c => <option key={c.sup + c.cat} value={c.sup + SEP + c.cat}>{c.cat} ({c.count})</option>)}
            </optgroup>
          ))}
        </Select>
        <TextInput className="!py-1 !text-xs flex-1" value={q} disabled={!cat}
          placeholder={cat ? 'Name or supplier code…' : 'Pick a vendor & category first'}
          onChange={e => setQ(e.target.value)} />
        <IconBtn title="Close" onClick={onClose}>✕</IconBtn>
      </div>
      {cat && (
        results.length ? (
          <div className="grid grid-cols-4 sm:grid-cols-6 gap-1.5 max-h-56 overflow-y-auto pr-1">
            {results.map(r => (
              <button key={r.id} onClick={() => onPick(r)}
                className="text-left rounded border border-[var(--leon-line)] p-1 hover:border-[var(--leon-brown)]">
                {r.img
                  ? <img src={r.img} alt="" loading="lazy" className="w-full h-12 object-cover rounded-sm" />
                  : <span className="block w-full h-12 rounded-sm bg-[var(--leon-cream)]" />}
                <span className="block text-[9px] font-semibold truncate mt-0.5">{r.name}</span>
              </button>
            ))}
          </div>
        ) : <p className="text-xs text-[var(--leon-black)]/45">Nothing matches.</p>
      )}
    </div>
  );
}

// ---- keynotes --------------------------------------------------------------

const CW_KIND_TONE = {
  commitment: { cls: 'bg-emerald-50 text-emerald-800 border-emerald-200', tone: 'good' },
  exclusion:  { cls: 'bg-red-50 text-red-800 border-red-200', tone: 'bad' },
  question:   { cls: 'bg-amber-50 text-amber-900 border-amber-200', tone: 'warn' },
  noted:      { cls: 'bg-[var(--leon-cream)] text-[var(--leon-black)]/70 border-[var(--leon-line)]', tone: 'neutral' },
};
function CwKindChip({ kind }) {
  const k = cwKeynoteAnswerKind(kind) || cwKeynoteAnswerKind('noted');
  const t = CW_KIND_TONE[k.key] || CW_KIND_TONE.noted;
  return (
    <span className={`inline-flex items-center gap-1 text-[10px] font-bold uppercase tracking-wide px-1.5 py-0.5 rounded border ${t.cls}`}>
      <span aria-hidden="true">{k.icon}</span>{k.label}
    </span>
  );
}

function CwKeynotesPanel({ ctx, project, editable, discipline }) {
  const notes = cwKeynotes(project, discipline).filter(k => k.active !== false);
  const [editId, setEditId] = useState(null);
  const [adding, setAdding] = useState(false);
  const [kind, setKind] = useState('');
  const shown = kind ? notes.filter(k => (k.answerKind || 'noted') === kind) : notes;
  const byKind = k => notes.filter(x => (x.answerKind || 'noted') === k).length;
  const unanswered = notes.filter(k => !String(k.answer || '').trim());

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-3 flex-wrap">
        <div>
          <h3 className="font-bold">📌 Keynotes &amp; our answers</h3>
          <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
            The architect issues <b>FN32</b>; we answer with <b>A.FN32</b> and say which pages answer it.
            Every answer is one of four things, and only two of them have consequences after approval:
            a <b>commitment</b> is a dimension we then owe, and an <b>exclusion</b> is what a back-charge
            argument turns on. Carrying both as prose is how one gets forgotten.
          </p>
        </div>
        {editable && <Button size="sm" onClick={() => setAdding(true)}>+ Keynote</Button>}
      </div>

      {!!notes.length && (
        <div className="flex items-center gap-1.5 flex-wrap">
          <button onClick={() => setKind('')}
            className={`text-xs font-semibold px-2 py-1 rounded border ${!kind ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)]'}`}>
            All {notes.length}
          </button>
          {CW_KEYNOTE_ANSWER_KINDS.map(k => {
            const n = byKind(k.key);
            if (!n) return null;
            return (
              <button key={k.key} onClick={() => setKind(kind === k.key ? '' : k.key)} title={k.hint}
                className={`text-xs font-semibold px-2 py-1 rounded border ${kind === k.key ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)]'}`}>
                {k.label} {n}
              </button>
            );
          })}
          {!!unanswered.length && (
            <span className="text-xs text-[var(--leon-brown)] font-semibold ml-1">
              {unanswered.length} not answered yet
            </span>
          )}
        </div>
      )}

      {!notes.length && (
        <EmptyState text="No keynotes yet. Add the architect's FN notes and record what we answered against each." />
      )}

      <div className="space-y-2">
        {shown.map(k => {
          const types = (project.caseworkTypes || []).filter(t => (k.caseworkTypeIds || []).indexOf(t.id) >= 0);
          return (
            <div key={k.id} className="rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden">
              <div className="flex items-start gap-3 px-3 py-2 bg-[var(--leon-cream)] border-b border-[var(--leon-line)]">
                <span className="font-mono font-bold text-sm">{k.code}</span>
                <span className="text-sm flex-1 min-w-0">{k.text || <span className="opacity-40">no text</span>}</span>
                {k.source && <span className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 whitespace-nowrap">{k.source}</span>}
                {editable && (
                  <button onClick={() => setEditId(k.id)} className="text-xs font-semibold text-[var(--leon-brown)] whitespace-nowrap">Edit</button>
                )}
              </div>
              <div className="px-3 py-2 flex items-start gap-3">
                <span className="font-mono font-bold text-sm text-[var(--leon-brown)] whitespace-nowrap">
                  {cwKeynoteAnswerCode(k)}
                </span>
                <div className="flex-1 min-w-0">
                  {String(k.answer || '').trim()
                    ? <p className="text-sm">{k.answer}</p>
                    : <p className="text-sm text-[var(--leon-brown)]">Not answered yet.</p>}
                  <div className="flex items-center gap-2 flex-wrap mt-1.5">
                    <CwKindChip kind={k.answerKind} />
                    {!!(k.pageRefs || []).length && (
                      <span className="text-[11px] text-[var(--leon-black)]/50">
                        Answered on {k.pageRefs.length === 1 ? 'page' : 'pages'} {k.pageRefs.join(', ')}
                      </span>
                    )}
                    {!!types.length && (
                      <span className="text-[11px] text-[var(--leon-black)]/50">
                        · {types.map(t => t.code).join(', ')}
                      </span>
                    )}
                  </div>
                </div>
              </div>
            </div>
          );
        })}
      </div>

      {!!notes.filter(k => k.answerKind === 'exclusion').length && (
        <div className="rounded-lg border border-red-200 bg-red-50 p-3">
          <p className="text-sm font-bold text-red-800 mb-1">
            {notes.filter(k => k.answerKind === 'exclusion').length} exclusion{notes.filter(k => k.answerKind === 'exclusion').length === 1 ? '' : 's'} on this job
          </p>
          <p className="text-xs text-red-900/80">
            These are the things we have said in writing that we are not doing. Worth reading before
            anyone agrees to a variation on site &mdash; an exclusion nobody remembered is how a
            back-charge starts.
          </p>
        </div>
      )}

      <CwKeynoteModal ctx={ctx} project={project} discipline={discipline} open={adding || !!editId}
        note={editId ? notes.find(k => k.id === editId) : null}
        onClose={() => { setAdding(false); setEditId(null); }} />
    </div>
  );
}

function CwKeynoteModal({ ctx, project, open, note, onClose, discipline }) {
  const blank = { code: '', text: '', source: '', answer: '', answerKind: 'noted', pageRefs: [], caseworkTypeIds: [] };
  const [form, setForm] = useState(blank);
  const [pages, setPages] = useState('');
  useEffect(() => {
    if (!open) return;
    setForm(note ? {
      code: note.code, text: note.text, source: note.source, answer: note.answer,
      answerKind: note.answerKind || 'noted',
      pageRefs: (note.pageRefs || []).slice(), caseworkTypeIds: (note.caseworkTypeIds || []).slice(),
    } : blank);
    setPages(note ? (note.pageRefs || []).join(', ') : '');
  }, [open, note && note.id]);
  const set = f => setForm(o => ({ ...o, ...f }));
  const types = project.caseworkTypes || [];

  function save() {
    const code = form.code.trim().toUpperCase();
    if (!code) return;
    // Page references are typed as people write them — "10, 11-14, 48" — and
    // stored as the tokens they wrote. Expanding a range would invent pages the
    // author did not name.
    const refs = pages.split(/[,;]/).map(x => x.trim()).filter(Boolean);
    const body = { ...form, code, pageRefs: refs,
      answeredDate: String(form.answer || '').trim() ? todayISO() : null,
      answeredBy: String(form.answer || '').trim() ? ctx.currentUserName : '' };
    if (note) {
      cwUpdate(ctx, project, draft => {
        const k = draft.caseworkKeynotes.find(x => x.id === note.id);
        if (k) Object.assign(k, body);
      }, `LEON Casework — keynote ${code} answered as ${(cwKeynoteAnswerKind(form.answerKind) || {}).label || form.answerKind}.`);
    } else {
      cwUpdate(ctx, project, draft => {
        draft.caseworkKeynotes.push(makeCwKeynote(Object.assign({ discipline }, body), ctx.currentUserName));
      }, `LEON Casework — keynote ${code} added.`);
    }
    onClose();
  }

  return (
    <Modal open={open} onClose={onClose} wide title={note ? `Keynote ${note.code}` : 'New keynote'}
      footer={
        <>
          <Button variant="ghost" onClick={onClose}>Cancel</Button>
          <Button onClick={save} disabled={!form.code.trim()}>{note ? 'Save' : 'Add keynote'}</Button>
        </>
      }>
      <div className="space-y-3">
        <div className="grid sm:grid-cols-3 gap-3">
          <Field label="Keynote" hint="The architect's number.">
            <TextInput className="font-mono" value={form.code} placeholder="FN32"
              onChange={e => set({ code: e.target.value.toUpperCase() })} />
          </Field>
          <Field label="Issued by" className="sm:col-span-2" hint="The practice that wrote it.">
            <TextInput value={form.source} onChange={e => set({ source: e.target.value })} placeholder="Hacin" />
          </Field>
        </div>
        <Field label="What they wrote">
          <TextArea rows={2} value={form.text} onChange={e => set({ text: e.target.value })} />
        </Field>

        <div className="rounded-lg border border-[var(--leon-brown)] bg-[var(--leon-cream)] p-3 space-y-3">
          <p className="text-xs font-bold uppercase tracking-wide text-[var(--leon-brown)]">
            Our answer &mdash; {form.code ? `A.${form.code}` : 'A.FN…'}
          </p>
          <Field label="What we answered">
            <TextArea rows={3} value={form.answer} onChange={e => set({ answer: e.target.value })} />
          </Field>
          <Field label="What kind of answer is it?">
            <div className="grid sm:grid-cols-2 gap-1.5">
              {CW_KEYNOTE_ANSWER_KINDS.map(k => (
                <button key={k.key} type="button" onClick={() => set({ answerKind: k.key })}
                  className={`text-left rounded border px-2 py-1.5 ${form.answerKind === k.key ? 'border-[var(--leon-brown)] bg-white' : 'border-[var(--leon-line)]'}`}>
                  <span className="text-xs font-bold">{k.icon} {k.label}</span>
                  <span className="block text-[10px] text-[var(--leon-black)]/55 leading-tight">{k.hint}</span>
                </button>
              ))}
            </div>
          </Field>
          <Field label="Answered on pages" hint="As you would write it on the sheet — 10, 11-14, 48.">
            <TextInput value={pages} onChange={e => setPages(e.target.value)} placeholder="11, 12, 13" />
          </Field>
        </div>

        {!!types.length && (
          <Field label="Applies to which kitchen types" hint="Leave all clear if it applies to the whole set.">
            <div className="flex flex-wrap gap-1.5">
              {types.map(t => {
                const on = form.caseworkTypeIds.indexOf(t.id) >= 0;
                return (
                  <button key={t.id} type="button"
                    onClick={() => set({ caseworkTypeIds: on
                      ? form.caseworkTypeIds.filter(x => x !== t.id)
                      : form.caseworkTypeIds.concat([t.id]) })}
                    className={`text-xs font-semibold px-2 py-1 rounded border ${on ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]' : 'border-[var(--leon-line)]'}`}>
                    {t.code}
                  </button>
                );
              })}
            </div>
          </Field>
        )}
      </div>
    </Modal>
  );
}

// ---- the submittal record --------------------------------------------------

function CwSubmittalsPanel({ ctx, project, editable, discipline }) {
  const subs = cwSubmittals(project, discipline);
  const [openId, setOpenId] = useState(null);
  const [adding, setAdding] = useState(false);
  const open = subs.find(s => s.id === openId) || null;

  if (open) {
    return <CwSubmittalDetail ctx={ctx} project={project} sub={open} editable={editable}
      onBack={() => setOpenId(null)} />;
  }

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-3 flex-wrap">
        <div>
          <h3 className="font-bold">📑 {cwDiscLabel(discipline)} submittals</h3>
          <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
            The approval page and the revision log &mdash; who signed, with what outcome, and every
            comment they made with what we answered. The four outcomes are the Hub&rsquo;s own submittal
            statuses, so a {cwDiscLabel(discipline).toLowerCase()} submittal and a shop-drawing thread mean the
            same thing.
          </p>
        </div>
        {editable && <Button size="sm" onClick={() => setAdding(true)}>+ Submittal</Button>}
      </div>

      {!subs.length && <EmptyState text="No casework submittals recorded yet." />}

      <div className="space-y-2">
        {subs.slice().sort((a, b) => String(b.date || '').localeCompare(String(a.date || ''))).map(s => {
          const open2 = (s.log || []).filter(l => !l.resolved).length;
          return (
            <button key={s.id} onClick={() => setOpenId(s.id)}
              className="w-full text-left rounded-lg border border-[var(--leon-line)] bg-white px-3 py-2.5 hover:border-[var(--leon-brown)]">
              <div className="flex items-center gap-3 flex-wrap">
                <span className="font-bold">{s.book || 'Casework'}</span>
                <span className="font-mono text-sm text-[var(--leon-brown)]">{s.revision || `Submittal ${s.number}`}</span>
                <Badge tone={s.issued ? 'good' : 'neutral'}>{s.issued ? 'Issued' : 'Draft'}</Badge>
                {s.reviews && s.reviews.architect && s.reviews.architect.outcome && (
                  <Badge tone={s.reviews.architect.outcome === 'Approved' ? 'good'
                    : s.reviews.architect.outcome === 'Rejected' ? 'bad' : 'warn'}>
                    {s.reviews.architect.outcome}
                  </Badge>
                )}
                <span className="ml-auto text-xs text-[var(--leon-black)]/50">
                  {s.date ? fmtDate(s.date) : '—'}
                  {open2 ? ` · ${open2} comment${open2 === 1 ? '' : 's'} open` : ''}
                </span>
              </div>
            </button>
          );
        })}
      </div>

      <CwSubmittalModal ctx={ctx} project={project} discipline={discipline} open={adding} onClose={() => setAdding(false)} />
    </div>
  );
}

function CwSubmittalModal({ ctx, project, open, onClose, discipline }) {
  const [form, setForm] = useState({ number: '', revision: '', book: '', date: todayISO() });
  useEffect(() => {
    if (!open) return;
    const n = cwSubmittals(project).length + 1;
    const num = String(n).padStart(2, '0');
    setForm({ number: num, revision: `REV${num}`, book: '', date: todayISO() });
  }, [open]);
  const set = f => setForm(o => ({ ...o, ...f }));
  return (
    <Modal open={open} onClose={onClose} title="New casework submittal"
      footer={
        <>
          <Button variant="ghost" onClick={onClose}>Cancel</Button>
          <Button onClick={() => {
            cwUpdate(ctx, project, draft => {
              draft.caseworkSubmittals.push(makeCwSubmittal({
                discipline,
                ...form, projectName: project.name, location: project.address || '',
                preparedById: ctx.currentUserId, preparedBy: ctx.currentUserName,
              }, ctx.currentUserName));
            }, `LEON Casework — submittal ${form.revision || form.number} started.`);
            onClose();
          }}>Create</Button>
        </>
      }>
      <div className="space-y-3">
        <div className="grid sm:grid-cols-2 gap-3">
          <Field label="Submittal number"><TextInput value={form.number} onChange={e => set({ number: e.target.value })} /></Field>
          <Field label="Revision"><TextInput className="font-mono" value={form.revision} onChange={e => set({ revision: e.target.value.toUpperCase() })} /></Field>
        </div>
        <Field label="Book" hint="One book per unit family, because they revise independently — Artist Unit, Condo Unit.">
          <TextInput value={form.book} onChange={e => set({ book: e.target.value })} placeholder="Condo Unit" />
        </Field>
        <Field label="Date"><TextInput type="date" value={form.date} onChange={e => set({ date: e.target.value })} /></Field>
      </div>
    </Modal>
  );
}

function CwSubmittalDetail({ ctx, project, sub, editable, onBack }) {
  const [tab, setTab] = useState('approval');
  const write = (fn, action) => cwUpdate(ctx, project, draft => {
    const s = draft.caseworkSubmittals.find(x => x.id === sub.id);
    if (s) fn(s);
  }, action);

  return (
    <div className="space-y-4">
      <button onClick={onBack} className="text-sm font-semibold text-[var(--leon-brown)]">&larr; All submittals</button>

      <div className="flex items-start justify-between gap-3 flex-wrap">
        <div>
          <h3 className="font-bold text-lg">
            {sub.book || 'Casework'} &middot; <span className="font-mono text-[var(--leon-brown)]">{sub.revision}</span>
          </h3>
          <p className="text-sm text-[var(--leon-black)]/55">
            Submittal {sub.number} &middot; {sub.date ? fmtDate(sub.date) : '—'}
            {sub.preparedBy ? ` · prepared by ${sub.preparedBy}` : ''}
          </p>
        </div>
        {editable && !sub.issued && (
          <Button size="sm" onClick={() => write(s => { s.issued = true; s.issuedDate = todayISO(); },
            `LEON Casework — submittal ${sub.revision} issued.`)}>Mark issued</Button>
        )}
        {sub.issued && <Badge tone="good">Issued {sub.issuedDate ? fmtDate(sub.issuedDate) : ''}</Badge>}
      </div>

      <div className="flex gap-1 border-b border-[var(--leon-line)] flex-wrap">
        {[{ k: 'approval', l: 'Approval page', i: '✍️' },
          { k: 'log', l: 'Revision log', i: '🕘' },
          { k: 'keynotes', l: 'Keynotes', i: '📌' }].map(t => (
          <button key={t.k} onClick={() => setTab(t.k)}
            className={`subtab-btn px-3 py-1.5 text-[12px] font-semibold border-b-2 whitespace-nowrap ${tab === t.k ? 'border-[var(--leon-brown)] text-[var(--leon-brown)]' : 'border-transparent text-[var(--leon-black)]/50'}`}>
            <span aria-hidden="true" className="mr-1.5 opacity-80">{t.i}</span>{t.l}
          </button>
        ))}
      </div>

      {tab === 'approval' && (
        <div className="space-y-4">
          <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden">
            <div className="px-4 py-2.5 bg-[var(--leon-black)] text-white">
              <p className="text-[10px] uppercase tracking-[0.25em] opacity-70">Casework shop drawing submittal</p>
              <p className="font-bold tracking-[0.15em] text-sm">A P P R O V A L&nbsp; P A G E</p>
            </div>
            <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-3 p-4 text-sm">
              <div><dt className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Project</dt>
                <dd className="font-medium">{sub.projectName || project.name}</dd></div>
              <div><dt className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Location</dt>
                <dd className="font-medium">{sub.location || project.address || '—'}</dd></div>
              <div><dt className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Date</dt>
                <dd className="font-medium">{sub.date ? fmtDate(sub.date) : '—'}</dd></div>
              <div><dt className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Submittal number</dt>
                <dd className="font-medium font-mono">{sub.number}</dd></div>
              <div><dt className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Submitted by</dt>
                <dd className="font-medium">{sub.submittedBy}</dd></div>
              <div><dt className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Prepared by</dt>
                <dd className="font-medium">{sub.preparedBy || '—'}</dd></div>
            </div>
            <div className="px-4 pb-4">
              <p className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 mb-1">Submittal contents</p>
              <ul className="text-sm list-disc pl-5 space-y-0.5 text-[var(--leon-black)]/75">
                {(sub.contents || []).map((c, i) => <li key={i}>{c}</li>)}
              </ul>
            </div>
          </div>

          <div className="grid md:grid-cols-3 gap-3">
            {CW_SUBMITTAL_REVIEWERS.map(r => {
              const rv = (sub.reviews || {})[r.key] || {};
              return (
                <div key={r.key} className="rounded-lg border border-[var(--leon-line)] bg-white p-3 space-y-2">
                  <p className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 font-bold">{r.label}</p>
                  {r.outcomes && (
                    <Field label="Outcome">
                      <Select value={rv.outcome || ''} disabled={!editable}
                        onChange={e => write(s => { s.reviews[r.key].outcome = e.target.value; },
                          `LEON Casework — ${sub.revision} ${r.label}: ${e.target.value || 'cleared'}.`)}>
                        <option value="">— awaiting —</option>
                        {CW_SUBMITTAL_OUTCOMES.map(o => <option key={o} value={o}>{o}</option>)}
                      </Select>
                    </Field>
                  )}
                  <Field label="Name">
                    <TextInput value={rv.name || ''} disabled={!editable}
                      onChange={e => write(s => { s.reviews[r.key].name = e.target.value; })} />
                  </Field>
                  <Field label="Date">
                    <TextInput type="date" value={rv.date || ''} disabled={!editable}
                      onChange={e => write(s => { s.reviews[r.key].date = e.target.value; })} />
                  </Field>
                  {/* A signature is a wet mark on a returned sheet. The Hub
                      records that it happened and who by; it does not pretend
                      to hold the signature itself. */}
                  <p className="text-[10px] text-[var(--leon-black)]/40">
                    Signature is on the returned sheet &mdash; file it under the scope&rsquo;s documents.
                  </p>
                </div>
              );
            })}
          </div>

          <Field label="Reviewer comments">
            <TextArea rows={3} value={sub.reviewerComments || ''} disabled={!editable}
              onChange={e => write(s => { s.reviewerComments = e.target.value; })} />
          </Field>
        </div>
      )}

      {tab === 'log' && <CwSubmittalLog ctx={ctx} project={project} sub={sub} editable={editable} write={write} />}

      {tab === 'keynotes' && (
        <div className="space-y-2">
          <p className="text-sm text-[var(--leon-black)]/60">
            The keynotes as they stand today. They are held on the job, not copied onto each submittal,
            so an answer corrected once is corrected everywhere it was issued.
          </p>
          {!cwKeynotes(project).length && <EmptyState text="No keynotes recorded on this job yet." />}
          {cwKeynotes(project).filter(k => k.active !== false).map(k => (
            <div key={k.id} className="rounded border border-[var(--leon-line)] bg-white px-3 py-2">
              <div className="flex items-start gap-2 flex-wrap">
                <span className="font-mono font-bold text-xs">{k.code}</span>
                <span className="text-xs flex-1 min-w-0">{k.text}</span>
                <CwKindChip kind={k.answerKind} />
              </div>
              {k.answer && (
                <p className="text-xs text-[var(--leon-black)]/70 mt-1">
                  <span className="font-mono font-bold text-[var(--leon-brown)]">{cwKeynoteAnswerCode(k)}</span>{' '}
                  {k.answer}
                  {!!(k.pageRefs || []).length && (
                    <span className="text-[var(--leon-black)]/45"> (p. {k.pageRefs.join(', ')})</span>
                  )}
                </p>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function CwSubmittalLog({ ctx, project, sub, editable, write }) {
  const [adding, setAdding] = useState(false);
  const [form, setForm] = useState({ rev: '', ref: '', comment: '', response: '', pages: '' });
  const log = sub.log || [];
  // Grouped by the revision the comment came in on, newest first — which is how
  // the source document reads and how a reviewer looks for their own comment.
  const revs = Array.from(new Set(log.map(l => l.rev || '—'))).sort().reverse();
  const set = f => setForm(o => ({ ...o, ...f }));

  return (
    <div className="space-y-3">
      <div className="flex items-start justify-between gap-3 flex-wrap">
        <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
          Every comment the reviewer made, at the revision they made it, and what we answered. Numbered
          the way they number them, because that is the number they quote back.
        </p>
        {editable && (
          <Button size="sm" variant="ghost" onClick={() => {
            setForm({ rev: sub.revision || '', ref: '', comment: '', response: '', pages: '' });
            setAdding(true);
          }}>+ Comment</Button>
        )}
      </div>

      {!log.length && <EmptyState text="No comments logged against this submittal yet." />}

      {revs.map(r => (
        <Collapsible key={r} id={`cw-log-${sub.id}-${r}`} defaultOpen title={r}
          count={log.filter(l => (l.rev || '—') === r).length}>
          <div className="space-y-2">
            {log.filter(l => (l.rev || '—') === r).map(l => (
              <div key={l.id} className="rounded border border-[var(--leon-line)] bg-white overflow-hidden">
                <div className="flex items-start gap-2 px-3 py-1.5 bg-[var(--leon-cream)]">
                  <span className="font-mono font-bold text-xs">{l.ref || '—'}.</span>
                  <span className="text-xs flex-1 min-w-0">{l.comment}</span>
                  {editable && (
                    <label className="flex items-center gap-1 text-[10px] whitespace-nowrap">
                      <input type="checkbox" checked={!!l.resolved}
                        onChange={e => write(s => {
                          const row = (s.log || []).find(x => x.id === l.id);
                          if (row) row.resolved = e.target.checked;
                        })} />
                      done
                    </label>
                  )}
                </div>
                <div className="px-3 py-1.5 flex items-start gap-2">
                  <span className="font-mono font-bold text-xs text-[var(--leon-brown)]">A.{l.ref || '—'}</span>
                  <span className="text-xs flex-1 min-w-0">
                    {l.response || <span className="text-[var(--leon-brown)]">Not answered yet.</span>}
                    {!!(l.pageRefs || []).length && (
                      <span className="text-[var(--leon-black)]/45"> (p. {l.pageRefs.join(', ')})</span>
                    )}
                  </span>
                </div>
              </div>
            ))}
          </div>
        </Collapsible>
      ))}

      <Modal open={adding} onClose={() => setAdding(false)} wide title="Log a reviewer comment"
        footer={
          <>
            <Button variant="ghost" onClick={() => setAdding(false)}>Cancel</Button>
            <Button disabled={!form.comment.trim()} onClick={() => {
              write(s => {
                s.log = (s.log || []).concat([makeCwSubmittalLogEntry({
                  rev: form.rev.trim().toUpperCase(), ref: form.ref.trim(),
                  comment: form.comment, response: form.response, date: todayISO(),
                  pageRefs: form.pages.split(/[,;]/).map(x => x.trim()).filter(Boolean),
                })]);
              }, `LEON Casework — comment ${form.ref || ''} logged against ${sub.revision}.`);
              setAdding(false);
            }}>Log it</Button>
          </>
        }>
        <div className="space-y-3">
          <div className="grid sm:grid-cols-2 gap-3">
            <Field label="Came in at" hint="The revision the reviewer was marking up.">
              <TextInput className="font-mono" value={form.rev} onChange={e => set({ rev: e.target.value.toUpperCase() })} placeholder="REV00" />
            </Field>
            <Field label="Their number" hint="Answered back as A.8.">
              <TextInput className="font-mono" value={form.ref} onChange={e => set({ ref: e.target.value })} placeholder="8" />
            </Field>
          </div>
          <Field label="What they said"><TextArea rows={2} value={form.comment} onChange={e => set({ comment: e.target.value })} /></Field>
          <Field label="What we answered"><TextArea rows={2} value={form.response} onChange={e => set({ response: e.target.value })} /></Field>
          <Field label="Answered on pages"><TextInput value={form.pages} onChange={e => set({ pages: e.target.value })} placeholder="28, 29, 40, 41" /></Field>
        </div>
      </Modal>
    </div>
  );
}

// ============================================================================
// SCALED SHEET OUTPUT
// The 55 India casework set is 166 PowerPoint slides carrying pasted
// screenshots. The elevations in this module were already vector — but they are
// PREVIEWS: each one fits itself to a pixel height, which means it has no
// scale, no dimension strings and no sheet around it. A reviewer cannot put a
// rule on a preview.
//
// What follows turns the same geometry into a drawing: at a STATED scale, with
// running dimensions, inside a titled sheet. Nothing here re-derives anything —
// it reads `cwRunLayout`, the same function the run designer and the cut list
// read, so a sheet can never disagree with the parts it is made of.
// ============================================================================

// Real drafting scales. The number is the DENOMINATOR: at 1:20, twenty
// millimetres of cabinet is one millimetre of paper. The imperial pair are the
// two scales a US casework drawing is actually issued at, expressed the same
// way so one code path serves both.
// The scales and paper sizes moved to data.jsx when LEON Doors started drawing
// sheets too — one table, so the two modules cannot drift apart. Kept under the
// old names here so the ten call sites below did not have to change.
const CW_SHEET_SCALES = SHEET_SCALES;
const CW_SHEET_SIZES = SHEET_SIZES;

// A dimension string: extension lines, a witness line with ticks, and the
// figure sitting on it. Drawn in PAPER millimetres, because a dimension line is
// a property of the sheet and not of the cabinet — at 1:50 the text must not
// shrink with the drawing or it becomes unreadable, which is the single most
// common fault in a generated drawing.
function CwDim({ x1, x2, y, text, vertical, flip }) {
  const tick = 1.4;
  if (vertical) {
    const yy1 = Math.min(x1, x2), yy2 = Math.max(x1, x2);
    return (
      <g stroke={CW_INK} strokeWidth="0.18" fill="none">
        <line x1={y} y1={yy1} x2={y} y2={yy2} />
        <line x1={y - tick} y1={yy1} x2={y + tick} y2={yy1} />
        <line x1={y - tick} y1={yy2} x2={y + tick} y2={yy2} />
        <text x={y - 1.2} y={(yy1 + yy2) / 2} fontSize="2.6" fill={CW_INK} stroke="none"
          textAnchor="middle" dominantBaseline="middle"
          transform={`rotate(-90 ${y - 1.2} ${(yy1 + yy2) / 2})`}>{text}</text>
      </g>
    );
  }
  const xx1 = Math.min(x1, x2), xx2 = Math.max(x1, x2);
  return (
    <g stroke={CW_INK} strokeWidth="0.18" fill="none">
      <line x1={xx1} y1={y} x2={xx2} y2={y} />
      <line x1={xx1} y1={y - tick} x2={xx1} y2={y + tick} />
      <line x1={xx2} y1={y - tick} x2={xx2} y2={y + tick} />
      <text x={(xx1 + xx2) / 2} y={y + (flip ? 3.4 : -1.2)} fontSize="2.6" fill={CW_INK} stroke="none"
        textAnchor="middle">{text}</text>
    </g>
  );
}

// One wall elevation, at scale, on a sheet. Everything inside is in PAPER
// millimetres: model millimetres divided by the scale denominator.
function CwSheetElevation({ project, ctx, room, wall, runs, system, denom, originX, originY, width }) {
  const s = mm => mm / denom;                      // model mm -> paper mm
  const wallLen = cwWallLength(room, wall);
  const wallH = cwNum(wall.height) || cwIn(96);
  const floorY = originY + s(wallH);
  const layouts = runs.map(r => ({ run: r, layout: cwRunLayout(project, ctx, room, wall, r) }));
  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });

  // Dimension bands stack downward: one per run, then the overall wall.
  let band = floorY + 9;

  return (
    <g>
      {/* The wall, and the floor line it stands on. */}
      <rect x={originX} y={originY} width={s(wallLen)} height={s(wallH)}
        fill="#ffffff" stroke={CW_INK} strokeWidth="0.35" />
      <line x1={originX - 4} y1={floorY} x2={originX + s(wallLen) + 4} y2={floorY}
        stroke={CW_INK} strokeWidth="0.6" />

      {/* Openings, dashed, with their own sill and head. */}
      {(wall.obstructions || []).map(o => {
        const za = o.kind === 'Door' ? 0 : cwNum(o.sillHeight);
        const zb = za + cwNum(o.height);
        return (
          <g key={o.id}>
            <rect x={originX + s(cwNum(o.fromLeft))} y={floorY - s(zb)}
              width={s(cwNum(o.width))} height={s(zb - za)}
              fill="none" stroke={CW_INK} strokeWidth="0.25" strokeDasharray="1.6 1.2" />
            <text x={originX + s(cwNum(o.fromLeft) + cwNum(o.width) / 2)} y={floorY - s(zb) - 1.4}
              fontSize="2.4" fill={CW_INK} textAnchor="middle" opacity="0.7">{o.label || o.kind}</text>
          </g>
        );
      })}

      {/* The cabinets. A member is drawn from its own x and z, so what is on the
          sheet is what the run designer holds — not a second arrangement. */}
      {layouts.map(({ run, layout }) => (
        <g key={run.id}>
          {layout.members.map(m => (
            <g key={m.member.id}>
              <rect x={originX + s(m.x)} y={floorY - s(m.z1)} width={s(m.w)} height={s(m.h)}
                fill="#fdfcfa" stroke={CW_INK} strokeWidth="0.3" />
              <text x={originX + s(m.x + m.w / 2)} y={floorY - s(m.z0) - 1.6}
                fontSize="2.2" fill={CW_INK} textAnchor="middle" opacity="0.75">
                {m.member.label || (m.type ? m.type.code : '')}
              </text>
            </g>
          ))}
        </g>
      ))}

      {/* Running dimensions, one band per run: every member, left to right. */}
      {layouts.map(({ run, layout }) => {
        const y = (band += 7);
        return (
          <g key={'d' + run.id}>
            <text x={originX - 3} y={y} fontSize="2.4" fill={CW_INK} textAnchor="end"
              dominantBaseline="middle" opacity="0.6">{run.name}</text>
            {layout.members.map(m => (
              <CwDim key={m.member.id} x1={originX + s(m.x)} x2={originX + s(m.x + m.w)}
                y={y} text={fmt(m.w)} />
            ))}
          </g>
        );
      })}

      {/* The overall, which is the one a joiner checks first. */}
      <CwDim x1={originX} x2={originX + s(wallLen)} y={band + 8} text={fmt(wallLen)} />
      {/* And the wall height, up the left. */}
      <CwDim vertical x1={originY} x2={floorY} y={originX - 8} text={fmt(wallH)} />
    </g>
  );
}

// The sheet itself: a real paper size, a border, a title block, and the views
// laid on it. The SVG is sized in MILLIMETRES, so what prints is at the scale
// the title block claims — that is the whole difference between this and the
// previews elsewhere in the module.
function CwSheet({ project, ctx, room, wall, runs, system, denom, size, sheetNo, sheetTitle, rev, qtyNote }) {
  const S = CW_SHEET_SIZES.find(x => x.key === size) || CW_SHEET_SIZES[0];
  const margin = 10;
  const tbH = 26;                                   // title block height
  const tbW = 92;
  const co = ctx.companyProfile || {};
  const drawnBy = ctx.currentUserName || '';
  // The drawing sits above the title block, left-aligned, with room beneath for
  // the dimension bands.
  const originX = margin + 16;
  const originY = margin + 14;

  return (
    <svg width={`${S.w}mm`} height={`${S.h}mm`} viewBox={`0 0 ${S.w} ${S.h}`}
      style={{ background: '#fff', maxWidth: '100%', height: 'auto' }}
      role="img" aria-label={`${sheetTitle} — ${wall.name}`}>
      {/* Sheet border */}
      <rect x="0" y="0" width={S.w} height={S.h} fill="#ffffff" />
      <rect x={margin / 2} y={margin / 2} width={S.w - margin} height={S.h - margin}
        fill="none" stroke={CW_INK} strokeWidth="0.5" />

      <CwSheetElevation project={project} ctx={ctx} room={room} wall={wall} runs={runs}
        system={system} denom={denom} originX={originX} originY={originY} />

      {/* Title block, bottom right, the way every sheet in the trade carries it */}
      <g>
        <rect x={S.w - margin / 2 - tbW} y={S.h - margin / 2 - tbH} width={tbW} height={tbH}
          fill="#ffffff" stroke={CW_INK} strokeWidth="0.5" />
        <line x1={S.w - margin / 2 - tbW} y1={S.h - margin / 2 - tbH + 8}
          x2={S.w - margin / 2} y2={S.h - margin / 2 - tbH + 8} stroke={CW_INK} strokeWidth="0.3" />
        <text x={S.w - margin / 2 - tbW + 3} y={S.h - margin / 2 - tbH + 5.5}
          fontSize="4" fill={CW_INK} fontWeight="bold" letterSpacing="1.2">
          {(co.name || 'LEON INTEGRA').toUpperCase()}
        </text>
        {[
          ['PROJECT', project.name],
          ['ROOM / TYPE', `${room.name}${wall ? ' — ' + wall.name : ''}`],
          ['TITLE', sheetTitle],
          ['QUANTITY', qtyNote || '—'],
        ].map(([k, v], i) => (
          <g key={k}>
            <text x={S.w - margin / 2 - tbW + 3} y={S.h - margin / 2 - tbH + 12.5 + i * 4.2}
              fontSize="1.9" fill={CW_INK} opacity="0.5">{k}</text>
            <text x={S.w - margin / 2 - tbW + 20} y={S.h - margin / 2 - tbH + 12.5 + i * 4.2}
              fontSize="2.6" fill={CW_INK}>{String(v || '').slice(0, 42)}</text>
          </g>
        ))}
        {/* Scale, sheet number, revision and who drew it — the four a reviewer
            checks before reading anything else. */}
        <line x1={S.w - margin / 2 - 30} y1={S.h - margin / 2 - tbH + 8}
          x2={S.w - margin / 2 - 30} y2={S.h - margin / 2} stroke={CW_INK} strokeWidth="0.3" />
        {[['SCALE', (CW_SHEET_SCALES.find(x => x.denom === denom) || {}).label || `1:${denom}`],
          ['SHEET', sheetNo || '—'],
          ['REV', rev || '—'],
          ['DRAWN', drawnBy]].map(([k, v], i) => (
          <g key={k}>
            <text x={S.w - margin / 2 - 27} y={S.h - margin / 2 - tbH + 12 + i * 4.2}
              fontSize="1.9" fill={CW_INK} opacity="0.5">{k}</text>
            <text x={S.w - margin / 2 - 14} y={S.h - margin / 2 - tbH + 12 + i * 4.2}
              fontSize="2.4" fill={CW_INK}>{String(v || '').slice(0, 16)}</text>
          </g>
        ))}
      </g>
    </svg>
  );
}


// The cabinet schedule that sits on the sheet — 2020 Design calls it the Items
// List, and the reason it belongs ON the drawing rather than beside it is that
// a shop reads the mark off the elevation and then needs the size without
// opening anything else. Drawn in paper millimetres, like the dimensions.
function CwSheetSchedule({ project, ctx, room, walls, runsByWall, system, x, y, width }) {
  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
  const rows = [];
  walls.forEach(w => {
    (runsByWall[w.id] || []).forEach(run => {
      const layout = cwRunLayout(project, ctx, room, w, run);
      layout.members.forEach(m => {
        rows.push({
          mark: m.member.label || (m.type ? m.type.code : m.member.kind),
          wall: w.name,
          w: m.w, h: m.h,
          d: m.type ? cwNum(m.type.depth) : cwNum(m.member.depth),
          kind: m.type ? m.type.category : (m.member.kind || ''),
        });
      });
    });
  });
  const rowH = 4.2, headH = 5;
  const cols = [
    { k: 'mark', label: 'MARK', w: 0.20 },
    { k: 'wall', label: 'WALL', w: 0.16 },
    { k: 'kind', label: 'TYPE', w: 0.18 },
    { k: 'w', label: 'WIDTH', w: 0.15, dim: true },
    { k: 'h', label: 'HEIGHT', w: 0.15, dim: true },
    { k: 'd', label: 'DEPTH', w: 0.16, dim: true },
  ];
  let cx = x;
  const colX = cols.map(c => { const at = cx; cx += c.w * width; return at; });

  return (
    <g>
      <text x={x} y={y - 2} fontSize="3" fill={CW_INK} fontWeight="bold" letterSpacing="0.6">CABINET SCHEDULE</text>
      <rect x={x} y={y} width={width} height={headH} fill="#f4efe8" stroke={CW_INK} strokeWidth="0.2" />
      {cols.map((c, i) => (
        <text key={c.k} x={colX[i] + 1.2} y={y + 3.4} fontSize="2.1" fill={CW_INK} opacity="0.65">{c.label}</text>
      ))}
      {rows.map((r, i) => (
        <g key={i}>
          <rect x={x} y={y + headH + i * rowH} width={width} height={rowH}
            fill={i % 2 ? '#faf8f5' : '#ffffff'} stroke={CW_INK} strokeWidth="0.12" />
          {cols.map((c, ci) => (
            <text key={c.k} x={colX[ci] + 1.2} y={y + headH + i * rowH + 2.9} fontSize="2.3" fill={CW_INK}>
              {c.dim ? (r[c.k] ? fmt(r[c.k]) : '—') : String(r[c.k] || '—').slice(0, 18)}
            </text>
          ))}
        </g>
      ))}
      {!rows.length && (
        <text x={x + 2} y={y + headH + 3} fontSize="2.3" fill={CW_INK} opacity="0.5">No cabinets in this type yet.</text>
      )}
    </g>
  );
}

// A DRAWING LAYOUT: every wall of a casework type on one sheet, stacked, with
// the schedule beneath. 2020 Design's idea — a composed sheet generated from
// the design rather than assembled beside it — which is the direct answer to a
// set built by pasting screenshots into slides.
//
// Deliberately NO plan view. A plan needs to know how the walls turn, and the
// model does not store that: the 3D view infers it by assuming every wall turns
// ninety degrees from the last. That is fine for a box-shaped room and wrong for
// anything else, and a plan that is quietly wrong is worse than no plan. If wall
// angles are ever stored, this is where the plan goes.
// How much sheet the layout actually needs, in paper millimetres. Split out so
// the panel can say "this will not fit" BEFORE anything is printed — a drawing
// that silently runs off the sheet is the one fault a drafting tool must not
// have, and stacking two 96" walls at 1:20 fills an A3 exactly.
function cwLayoutHeightMm(walls, runsByWall, denom, scheduleRows) {
  const drawn = walls.filter(w => (runsByWall[w.id] || []).length);
  const bands = drawn.reduce((a, w) => a + ((cwNum(w.height) || cwIn(96)) / denom + 26), 0);
  return 14 + bands + 8 + 5 + scheduleRows * 4.2 + 6;
}
function cwLayoutRowCount(project, ctx, room, walls, runsByWall) {
  let n = 0;
  walls.forEach(w => (runsByWall[w.id] || []).forEach(run => {
    n += cwRunLayout(project, ctx, room, w, run).members.length;
  }));
  return n;
}
function CwSheetLayout({ project, ctx, room, walls, runsByWall, system, denom, size,
                        sheetNo, sheetTitle, rev, qtyNote }) {
  const S = CW_SHEET_SIZES.find(x => x.key === size) || CW_SHEET_SIZES[0];
  const margin = 10;
  const tbH = 26, tbW = 92;
  const co = ctx.companyProfile || {};
  const drawnBy = ctx.currentUserName || '';
  const drawn = walls.filter(w => (runsByWall[w.id] || []).length);
  // Each elevation gets a band tall enough for the wall plus its dimension
  // stack. Walls with no runs are skipped rather than printed empty — a blank
  // rectangle on a shop drawing reads as "nothing here", which may be untrue.
  const bandH = (w) => (cwNum(w.height) || cwIn(96)) / denom + 26;
  let cursorY = margin + 14;

  return (
    <svg width={`${S.w}mm`} height={`${S.h}mm`} viewBox={`0 0 ${S.w} ${S.h}`}
      style={{ background: '#fff', maxWidth: '100%', height: 'auto' }}
      role="img" aria-label={sheetTitle}>
      <rect x="0" y="0" width={S.w} height={S.h} fill="#ffffff" />
      <rect x={margin / 2} y={margin / 2} width={S.w - margin} height={S.h - margin}
        fill="none" stroke={CW_INK} strokeWidth="0.5" />

      {drawn.map(w => {
        const at = cursorY;
        cursorY += bandH(w);
        return (
          <g key={w.id}>
            <text x={margin + 16} y={at - 3} fontSize="3" fill={CW_INK} fontWeight="bold" letterSpacing="0.6">
              {w.name.toUpperCase()} — ELEVATION
            </text>
            <CwSheetElevation project={project} ctx={ctx} room={room} wall={w}
              runs={runsByWall[w.id] || []} system={system} denom={denom}
              originX={margin + 16} originY={at} />
          </g>
        );
      })}

      <CwSheetSchedule project={project} ctx={ctx} room={room} walls={drawn} runsByWall={runsByWall}
        system={system} x={margin + 16} y={cursorY + 8} width={Math.min(150, S.w - margin - 32)} />

      {!drawn.length && (
        <text x={S.w / 2} y={S.h / 2} fontSize="4" fill={CW_INK} opacity="0.4" textAnchor="middle">
          No runs on any wall of this type yet.
        </text>
      )}

      <g>
        <rect x={S.w - margin / 2 - tbW} y={S.h - margin / 2 - tbH} width={tbW} height={tbH}
          fill="#ffffff" stroke={CW_INK} strokeWidth="0.5" />
        <line x1={S.w - margin / 2 - tbW} y1={S.h - margin / 2 - tbH + 8}
          x2={S.w - margin / 2} y2={S.h - margin / 2 - tbH + 8} stroke={CW_INK} strokeWidth="0.3" />
        <text x={S.w - margin / 2 - tbW + 3} y={S.h - margin / 2 - tbH + 5.5}
          fontSize="4" fill={CW_INK} fontWeight="bold" letterSpacing="1.2">
          {(co.name || 'LEON INTEGRA').toUpperCase()}
        </text>
        {[['PROJECT', project.name], ['TYPE', room.name], ['TITLE', sheetTitle], ['QUANTITY', qtyNote || '—']].map(([k, v], i) => (
          <g key={k}>
            <text x={S.w - margin / 2 - tbW + 3} y={S.h - margin / 2 - tbH + 12.5 + i * 4.2}
              fontSize="1.9" fill={CW_INK} opacity="0.5">{k}</text>
            <text x={S.w - margin / 2 - tbW + 20} y={S.h - margin / 2 - tbH + 12.5 + i * 4.2}
              fontSize="2.6" fill={CW_INK}>{String(v || '').slice(0, 42)}</text>
          </g>
        ))}
        <line x1={S.w - margin / 2 - 30} y1={S.h - margin / 2 - tbH + 8}
          x2={S.w - margin / 2 - 30} y2={S.h - margin / 2} stroke={CW_INK} strokeWidth="0.3" />
        {[['SCALE', (CW_SHEET_SCALES.find(x => x.denom === denom) || {}).label || `1:${denom}`],
          ['SHEET', sheetNo || '—'], ['REV', rev || '—'], ['DRAWN', drawnBy]].map(([k, v], i) => (
          <g key={k}>
            <text x={S.w - margin / 2 - 27} y={S.h - margin / 2 - tbH + 12 + i * 4.2}
              fontSize="1.9" fill={CW_INK} opacity="0.5">{k}</text>
            <text x={S.w - margin / 2 - 14} y={S.h - margin / 2 - tbH + 12 + i * 4.2}
              fontSize="2.4" fill={CW_INK}>{String(v || '').slice(0, 16)}</text>
          </g>
        ))}
      </g>
    </svg>
  );
}

// ── The Sheets section ──────────────────────────────────────────────────────
// 2020 Design calls this a Drawing Layout: a composed sheet, generated from the
// design rather than assembled beside it. Pick the room, the wall and the
// scale; the sheet is drawn from the same `cwRunLayout` the cut list reads.
function CwSheetsPanel({ ctx, project, system }) {
  // Walls and runs live on the CASEWORK TYPE, never on the room — a room is a
  // physical instance that points at a type and stores only what it disagrees
  // with. Driving the sheet off the type is therefore the only correct reading;
  // an earlier version fell back to `room.walls`, which exists on older stored
  // rooms, carries no runs, and produced a confident "no runs on Wall A" when
  // the real answer was that the room is not linked to a type at all.
  const types = cwCaseworkTypes(project);
  const [ctId, setCtId] = useState(types[0] ? types[0].id : '');
  const [wallId, setWallId] = useState('');
  const [scaleKey, setScaleKey] = useState('1:20');
  const [sizeKey, setSizeKey] = useState('A3');
  // One elevation per sheet, or every wall of the type on one sheet with the
  // schedule under it — 2020 Design's Drawing Layout.
  const [mode, setMode] = useState('layout');
  const ct = types.find(t => t.id === ctId) || types[0] || null;
  const walls = (ct && ct.walls) || [];
  const wall = walls.find(w => w.id === wallId) || walls[0] || null;
  const runs = (wall && ct) ? (ct.runs || []).filter(r => r.wallId === wall.id) : [];
  // The rooms this type is built in — what the sheet is actually for, and the
  // quantity a shop reads off it.
  const rooms = ct ? cwRooms(project).filter(r => r.caseworkTypeId === ct.id) : [];
  const room = rooms[0] || { name: ct ? ct.name : '', unit: '' };
  const denom = (CW_SHEET_SCALES.find(s => s.key === scaleKey) || {}).denom || 20;
  // Every wall's runs, resolved once — the layout needs all of them and the
  // single-wall sheet needs one of them.
  const runsByWall = {};
  walls.forEach(w => { runsByWall[w.id] = ct ? (ct.runs || []).filter(r => r.wallId === w.id) : []; });
  const qtyNote = rooms.length ? `${rooms.length} off — ${rooms.map(r => r.unit || r.name).join(', ')}` : '';
  // Does the layout fit the chosen sheet? Answered before printing, not after.
  const sheetDef = CW_SHEET_SIZES.find(x => x.key === sizeKey) || CW_SHEET_SIZES[0];
  const scheduleRows = ct ? cwLayoutRowCount(project, ctx, room, walls, runsByWall) : 0;
  const needH = cwLayoutHeightMm(walls, runsByWall, denom, scheduleRows);
  const availH = sheetDef.h - 10 - 26 - 6;      // sheet less margin, title block, breathing room
  const overflow = needH - availH;
  const ref = useRef(null);

  if (!types.length) {
    return <EmptyState text="No casework types on this job yet. A sheet is drawn from a type's walls and the runs on them — start under Casework Types." />;
  }

  return (
    <div className="space-y-3">
      <div>
        <h3 className="font-bold">📄 Sheets</h3>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
          A wall elevation at a stated scale, dimensioned, in a titled sheet &mdash; drawn from the same
          run layout the cut list reads, so the drawing and the parts cannot disagree. The SVG is sized
          in millimetres, so what prints is at the scale the title block claims.
        </p>
      </div>

      <div className="flex items-end gap-3 flex-wrap">
        <Field label="Casework type" hint={rooms.length ? `Built in ${rooms.length} room${rooms.length === 1 ? '' : 's'}` : 'Not yet used in any room'}>
          <Select className="!w-52" value={ct ? ct.id : ''} onChange={e => { setCtId(e.target.value); setWallId(''); }}>
            {types.map(t => <option key={t.id} value={t.id}>{t.code} — {t.name}</option>)}
          </Select>
        </Field>
        <Field label="Wall">
          <Select className="!w-40" value={wall ? wall.id : ''} onChange={e => setWallId(e.target.value)}>
            {walls.map(w => <option key={w.id} value={w.id}>{w.name}</option>)}
          </Select>
        </Field>
        <Field label="Scale">
          <Select className="!w-40" value={scaleKey} onChange={e => setScaleKey(e.target.value)}>
            {CW_SHEET_SCALES.map(s => <option key={s.key} value={s.key}>{s.label}</option>)}
          </Select>
        </Field>
        <Field label="Sheet size">
          <Select className="!w-48" value={sizeKey} onChange={e => setSizeKey(e.target.value)}>
            {CW_SHEET_SIZES.map(s => <option key={s.key} value={s.key}>{s.label}</option>)}
          </Select>
        </Field>
        <Field label="Sheet">
          <Select className="!w-52" value={mode} onChange={e => setMode(e.target.value)}>
            <option value="layout">Layout — every wall + schedule</option>
            <option value="single">One wall per sheet</option>
          </Select>
        </Field>
        <div className="ml-auto flex items-end gap-1.5">
          <IconAction icon="🖨" title="Print this sheet"
            onClick={() => printRegion(ref.current, { title: `${project.name} — ${room ? room.name : ''}`, heading: 'Casework elevation' })} />
        </div>
      </div>

      {mode === 'layout' && overflow > 0 && (
        <div className="rounded-lg border border-[#e6d9b8] bg-[#fbf1dd] px-3 py-2 text-[12px] text-[#7a5c14]">
          <b>This layout needs about {Math.round(needH)} mm and the sheet is {Math.round(availH)} mm.</b>{' '}
          The elevations below the fold will run off the paper. Choose a larger sheet, a smaller scale, or
          use <b>One wall per sheet</b>. Nothing is cropped silently &mdash; the drawing is still shown so you can
          see what does not fit.
        </div>
      )}

      {!walls.length ? (
        <EmptyState text="This casework type has no walls yet. Add one under Rooms & Walls." />
      ) : mode === 'single' && !runs.length ? (
        <EmptyState text={`No runs on ${wall ? wall.name : 'this wall'} yet. The sheet draws the runs, so there is nothing to show until one exists.`} />
      ) : (
        <div ref={ref} data-print-region="Casework drawing"
          className="overflow-auto rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]/40 p-3">
          {mode === 'layout' ? (
            <CwSheetLayout project={project} ctx={ctx} room={room} walls={walls}
              runsByWall={runsByWall} system={system} denom={denom} size={sizeKey}
              sheetTitle={`${ct ? ct.code : ''} — elevations & schedule`}
              sheetNo={`CW-${ct ? ct.code : '01'}`}
              qtyNote={qtyNote} rev={ct ? `R${ct.revision || 1}` : '—'} />
          ) : (
            <CwSheet project={project} ctx={ctx} room={room} wall={wall} runs={runs} system={system}
              denom={denom} size={sizeKey} sheetTitle={`${wall.name} elevation`}
              sheetNo={`CW-${String(walls.indexOf(wall) + 1).padStart(2, '0')}`}
              qtyNote={qtyNote} rev={ct ? `R${ct.revision || 1}` : '—'} />
          )}
        </div>
      )}

      <p className="text-[11px] text-[var(--leon-black)]/45 max-w-3xl">
        A browser prints through the page, so the paper size here sets the DRAWING size &mdash; choose the
        matching paper in the print dialog and turn scaling off, or the scale in the title block stops
        being true. That is a limitation of printing from a browser, not of the drawing.
      </p>
    </div>
  );
}
