// ============================================================================
// LEON Stone — countertop slab management
// ----------------------------------------------------------------------------
// A slab is a PHYSICAL OBJECT, not a quantity. Two slabs of the same colour are
// not interchangeable: they carry different veining, they came off different
// blocks, and a kitchen cut from two lots looks like a mistake. That is the one
// idea the whole module is built on — every slab is its own record with its own
// photo, batch and lot, and nothing here ever collapses them into a count.
//
// Everything in this file is prefixed stone/Stone/STONE_ per the module
// contract, because all the app's source files share one global scope.
// ============================================================================

// ── Vocabularies ────────────────────────────────────────────────────────────

// The full life of a slab, in the order it normally travels. 'On Order' sits
// first because a slab is worth recording before it lands — that is what makes
// the "additional required" figure on a project mean anything.
const STONE_STATUSES = [
  'On Order', 'Available', 'Reserved', 'Allocated', 'Approved for Production',
  'Cutting', 'Partially Consumed', 'Consumed', 'Remnant Created', 'Damaged',
];
// Statuses that mean the slab is no longer stock that can be sold or cut again.
const STONE_DEAD_STATUSES = ['Consumed', 'Damaged'];
// Statuses that mean a saw has touched it — the basis for the waste figure.
const STONE_CUT_STATUSES = ['Cutting', 'Partially Consumed', 'Consumed', 'Remnant Created'];
const STONE_PRODUCTION_STATUSES = ['Approved for Production', 'Cutting'];

const STONE_STATUS_TONE = {
  'On Order': 'blue',
  'Available': 'green',
  'Reserved': 'yellow',
  'Allocated': 'brown',
  'Approved for Production': 'brown',
  'Cutting': 'yellow',
  'Partially Consumed': 'yellow',
  'Consumed': 'neutral',
  'Remnant Created': 'blue',
  'Damaged': 'red',
};

const STONE_REMNANT_STATUSES = ['Available', 'Reserved', 'Allocated', 'Consumed', 'Scrap'];
const STONE_REMNANT_TONE = {
  'Available': 'green', 'Reserved': 'yellow', 'Allocated': 'brown', 'Consumed': 'neutral', 'Scrap': 'red',
};

// Vein direction decides how a piece may be laid out on a slab, so it is a
// first-class field and not a note. 'Directionless' is genuinely different from
// 'Random': a directionless material (most engineered quartz) can be rotated
// freely, a random-movement natural stone cannot be rotated without the seam
// showing.
const STONE_VEIN_DIRECTIONS = ['Horizontal', 'Vertical', 'Diagonal', 'Random', 'Directionless', 'Custom'];
// What a PIECE may demand of the grain, which is a different question from what
// a slab HAS. '' means the piece does not care; 'Directionless' means it is
// being cut from a material with no grain to keep. A slab's 'Random' and
// 'Custom' are not offered here: neither is a direction a piece can be cut to.
const STONE_PIECE_VEIN_OPTIONS = ['', 'Horizontal', 'Vertical', 'Diagonal', 'Directionless'];

const STONE_MATERIALS = ['Quartz', 'Quartzite', 'Granite', 'Marble', 'Porcelain', 'Sintered Stone', 'Dolomite', 'Onyx', 'Soapstone', 'Travertine', 'Limestone', 'Solid Surface'];
const STONE_FINISHES = ['Polished', 'Honed', 'Leathered', 'Brushed', 'Flamed', 'Satin', 'Matte', 'Textured'];
const STONE_PATTERNS = ['Bookmatch', 'Sidematch', 'Continuous Vein', 'Uniform', 'Speckled', 'Movement', 'None'];
const STONE_EDGE_PROFILES = ['Eased', 'Bullnose', 'Half Bullnose', 'Ogee', 'Bevel', 'Mitered', 'Waterfall', 'Laminated / Built-up'];
const STONE_PIECE_STATUSES = ['Planned', 'Assigned', 'Cut', 'Installed'];

const STONE_SUBTABS = [
  { key: 'dashboard', label: 'Dashboard', icon: '📊' },
  { key: 'slabs', label: 'All Slabs', icon: '🪨' },
  { key: 'remnants', label: 'Remnants', icon: '🧩' },
  { key: 'allocation', label: 'Allocation', icon: '🎯' },
  { key: 'costing', label: 'Costing', icon: '💲' },
  { key: 'cutlist', label: 'Cut List', icon: '✂️' },
  { key: 'layout', label: 'Slab Layout', icon: '📐' },
];

// Nominal slab used only when a material has no slab on record yet, so the
// "how many slabs do I need" figure still has a basis instead of showing a
// dash. 3200 × 1600 is the common jumbo footprint.
const STONE_NOMINAL_LENGTH_MM = 3200;
const STONE_NOMINAL_WIDTH_MM = 1600;

const STONE_SQFT_PER_M2 = 10.7639104;
// Saw kerf plus a trim allowance on each cut edge. A remnant that is exactly
// the size of the piece is not actually a match.
const STONE_DEFAULT_KERF_MM = 12;
// Nobody yields 100% of a slab. This is the default assumption behind the
// "slabs required" figure and it is exposed as an input, because it is an
// assumption and not a measurement.
const STONE_DEFAULT_YIELD = 0.75;

// ── Pure helpers ────────────────────────────────────────────────────────────

function stoneNum(v) {
  const n = Number(v);
  return isFinite(n) ? n : 0;
}
// Dimensions are stored in millimetres (contract rule 3); area is stored and
// computed in square metres so it never depends on which unit the user typed.
function stoneAreaM2(lengthMm, widthMm) {
  return (stoneNum(lengthMm) * stoneNum(widthMm)) / 1e6;
}
function stoneSqft(m2) { return stoneNum(m2) * STONE_SQFT_PER_M2; }
function stoneFmtArea(m2) {
  if (!isFinite(m2)) return '—';
  return `${m2.toFixed(2)} m² · ${stoneSqft(m2).toFixed(1)} ft²`;
}
function stoneFmtAreaShort(m2) {
  if (!isFinite(m2)) return '—';
  return `${m2.toFixed(1)} m²`;
}
// Shoelace. Points are millimetres relative to the remnant's own bounding box.
function stonePolygonAreaM2(points) {
  if (!points || points.length < 3) return 0;
  let sum = 0;
  for (let i = 0; i < points.length; i++) {
    const a = points[i], b = points[(i + 1) % points.length];
    sum += stoneNum(a.x) * stoneNum(b.y) - stoneNum(b.x) * stoneNum(a.y);
  }
  return Math.abs(sum) / 2 / 1e6;
}
// "3000,1400" per line. Anything unreadable is dropped rather than guessed at.
function stoneParsePoints(text) {
  if (!text) return [];
  return String(text).split(/[\n;]+/).map(line => {
    const m = line.trim().match(/^(-?[\d.]+)\s*[, ]\s*(-?[\d.]+)$/);
    return m ? { x: parseFloat(m[1]), y: parseFloat(m[2]) } : null;
  }).filter(Boolean);
}
function stonePointsBounds(points) {
  if (!points || !points.length) return { lengthMm: 0, widthMm: 0 };
  const xs = points.map(p => stoneNum(p.x)), ys = points.map(p => stoneNum(p.y));
  return { lengthMm: Math.max.apply(null, xs) - Math.min.apply(null, xs), widthMm: Math.max.apply(null, ys) - Math.min.apply(null, ys) };
}

// Landed cost is ALWAYS recomputed from its parts on read. The stored
// `landedCost` field exists because the record schema calls for it, but a
// stored total that disagrees with its own components is worse than no total,
// so nothing in the UI reads the stored value.
function stoneLandedCost(slab) {
  return stoneNum(slab.unitCost) + stoneNum(slab.freightCost) + stoneNum(slab.tariffCost) + stoneNum(slab.handlingCost);
}
function stoneSlabArea(slab) {
  // Prefer the stored area (it may have been entered directly for an offcut of
  // unusual shape), fall back to length × width.
  const stored = stoneNum(slab.area);
  return stored > 0 ? stored : stoneAreaM2(slab.lengthMm, slab.widthMm);
}
function stoneCostPerM2(slab) {
  const a = stoneSlabArea(slab);
  return a > 0 ? stoneLandedCost(slab) / a : null;
}
function stoneCostPerSqft(slab) {
  const a = stoneSqft(stoneSlabArea(slab));
  return a > 0 ? stoneLandedCost(slab) / a : null;
}
function stoneRemnantArea(r) {
  const stored = stoneNum(r.area);
  if (stored > 0) return stored;
  if (r.shape === 'Irregular' && r.points && r.points.length >= 3) return stonePolygonAreaM2(r.points);
  return stoneAreaM2(r.lengthMm, r.widthMm);
}

// SL-QUARTZ-00125. The sequence runs per material, so the number reads as
// "the 125th quartz slab" rather than as a meaningless global counter.
function stoneMaterialToken(material) {
  const t = String(material || 'STONE').toUpperCase().replace(/[^A-Z0-9]/g, '');
  return t.slice(0, 10) || 'STONE';
}
function stoneNextSlabId(slabs, material) {
  const token = stoneMaterialToken(material);
  const re = new RegExp('^SL-' + token + '-(\\d+)$');
  let max = 0;
  (slabs || []).forEach(s => {
    const m = re.exec(s.slabId || '');
    if (m) max = Math.max(max, parseInt(m[1], 10));
  });
  return `SL-${token}-${String(max + 1).padStart(5, '0')}`;
}
function stoneNextRemnantId(remnants, material) {
  const token = stoneMaterialToken(material);
  const re = new RegExp('^RM-' + token + '-(\\d+)$');
  let max = 0;
  (remnants || []).forEach(r => {
    const m = re.exec(r.remnantId || '');
    if (m) max = Math.max(max, parseInt(m[1], 10));
  });
  return `RM-${token}-${String(max + 1).padStart(4, '0')}`;
}

// One history shape for slabs and remnants alike, so an audit trail reads the
// same whichever record you opened. Built inline rather than through app.jsx's
// logAction, which is a closure over App() state and is not on ctx.
function stoneHistoryEntry(user, action, note) {
  return { id: uid('sthist'), date: todayISO(), user: user || 'Unknown', action, note: note || '' };
}
function stonePushHistory(record, user, action, note) {
  record.history = [stoneHistoryEntry(user, action, note)].concat(record.history || []);
  return record;
}

function stoneMakeSlab(data, createdBy, existingSlabs) {
  const lengthMm = stoneNum(data.lengthMm);
  const widthMm = stoneNum(data.widthMm);
  const slab = {
    id: uid('slab'),
    slabId: (data.slabId || '').trim() || stoneNextSlabId(existingSlabs, data.material),
    material: data.material || '', manufacturer: data.manufacturer || '', collection: data.collection || '',
    colour: data.colour || '',
    // Batch and lot are why this module exists. A batch is the production run;
    // a lot is the bundle cut from one block. Matching lot is the strongest
    // guarantee two slabs will look like each other.
    batch: data.batch || '', lot: data.lot || '',
    lengthMm, widthMm, thicknessMm: stoneNum(data.thicknessMm),
    area: stoneNum(data.area) > 0 ? stoneNum(data.area) : stoneAreaM2(lengthMm, widthMm),
    finish: data.finish || '', pattern: data.pattern || '',
    veinDirection: data.veinDirection || 'Directionless',
    photoUrl: data.photoUrl || null, photoName: data.photoName || '',
    // References into the app's existing records — never a copy of them.
    warehouseId: data.warehouseId || null, rack: data.rack || '',
    vendorId: data.vendorId || null, poRef: data.poRef || '',
    containerId: data.containerId || null, countryOfOrigin: data.countryOfOrigin || '',
    unitCost: stoneNum(data.unitCost), freightCost: stoneNum(data.freightCost),
    tariffCost: stoneNum(data.tariffCost), handlingCost: stoneNum(data.handlingCost),
    landedCost: 0,
    status: data.status || 'Available',
    projectId: data.projectId || null, scopeId: data.scopeId || null,
    notes: data.notes || '',
    createdBy: createdBy || '', createdDate: todayISO(),
    history: [],
  };
  slab.landedCost = stoneLandedCost(slab);
  stonePushHistory(slab, createdBy, `Slab recorded as ${slab.status}`);
  return slab;
}

function stoneMakeRemnant(slab, data, createdBy, existingRemnants) {
  const lengthMm = stoneNum(data.lengthMm);
  const widthMm = stoneNum(data.widthMm);
  const points = data.shape === 'Irregular' ? (data.points || []) : [];
  const area = points.length >= 3 ? stonePolygonAreaM2(points) : stoneAreaM2(lengthMm, widthMm);
  const parentArea = stoneSlabArea(slab);
  const remnant = {
    id: uid('rmn'),
    remnantId: (data.remnantId || '').trim() || stoneNextRemnantId(existingRemnants, slab.material),
    // The remnant points at its parent; it never copies the parent's identity
    // fields for editing. They are denormalised here only so a remnant can be
    // searched on material/batch/lot without walking back to a slab that may
    // by then be fully consumed.
    parentSlabId: slab.id, parentSlabRef: slab.slabId,
    material: slab.material, manufacturer: slab.manufacturer, collection: slab.collection,
    colour: slab.colour, batch: slab.batch, lot: slab.lot, finish: slab.finish,
    veinDirection: slab.veinDirection,
    thicknessMm: stoneNum(slab.thicknessMm),
    // Bounding dimensions. For an irregular remnant these are the box it fits
    // in — NOT usable area, which is why area is carried separately.
    lengthMm, widthMm,
    shape: data.shape === 'Irregular' ? 'Irregular' : 'Rectangle',
    points,
    area,
    // Cost basis is the parent's landed cost pro-rated by area. That is the
    // only defensible number: the offcut cost what its share of the slab cost.
    costBasis: parentArea > 0 ? (stoneLandedCost(slab) * area) / parentArea : 0,
    warehouseId: data.warehouseId || slab.warehouseId || null,
    rack: data.rack || '',
    status: data.status || 'Available',
    projectId: null, scopeId: null,
    photoUrl: data.photoUrl || null, photoName: data.photoName || '',
    notes: data.notes || '',
    createdBy: createdBy || '', createdDate: todayISO(),
    history: [],
  };
  stonePushHistory(remnant, createdBy, `Remnant created from ${slab.slabId || 'slab'}`);
  return remnant;
}

function stoneMakePiece(data, createdBy) {
  return {
    id: uid('stonepc'),
    scopeId: data.scopeId || null,
    label: data.label || '', material: data.material || '',
    thicknessMm: stoneNum(data.thicknessMm),
    lengthMm: stoneNum(data.lengthMm), widthMm: stoneNum(data.widthMm),
    qty: Math.max(1, stoneNum(data.qty) || 1),
    // The grain this piece REQUIRES, which is not the same question as the
    // grain a slab HAS: '' means the piece does not care, 'Directionless' means
    // it is being cut from a material with no grain to keep. The layout canvas
    // warns when a piece is rotated against this. Older pieces have no field —
    // every read falls back to '' (no requirement), never to a guess.
    veinDirection: data.veinDirection || '',
    edgeProfile: data.edgeProfile || '', notes: data.notes || '',
    slabId: null, remnantId: null,
    status: 'Planned',
    createdBy: createdBy || '', createdDate: todayISO(),
  };
}
function stonePieceArea(piece) {
  return stoneAreaM2(piece.lengthMm, piece.widthMm) * Math.max(1, stoneNum(piece.qty) || 1);
}

// Will this remnant take this piece? Rectangles only, both orientations, with
// kerf added on each axis. For an irregular remnant this can only ever say the
// piece fits the BOUNDING BOX — the real answer needs a nesting layout, which
// is not built (see the Cut List tab).
function stoneRemnantFit(remnant, needLmm, needWmm, kerfMm) {
  const kerf = stoneNum(kerfMm);
  const nl = stoneNum(needLmm) + kerf, nw = stoneNum(needWmm) + kerf;
  const L = stoneNum(remnant.lengthMm), W = stoneNum(remnant.widthMm);
  if (nl <= 0 || nw <= 0) return { fits: false, rotated: false };
  if (nl <= L && nw <= W) return { fits: true, rotated: false };
  if (nw <= L && nl <= W) return { fits: true, rotated: true };
  return { fits: false, rotated: false };
}
// Same batch AND lot is the strongest match; same batch next; same material
// last. Within a tier the tightest remnant wins, so a big offcut is not spent
// on a small piece.
function stoneRemnantMatchRank(remnant, want) {
  if (want.batch && want.lot && remnant.batch === want.batch && remnant.lot === want.lot) return 3;
  if (want.batch && remnant.batch === want.batch) return 2;
  return 1;
}
function stoneRankLabel(rank) {
  return rank === 3 ? 'Same batch & lot' : rank === 2 ? 'Same batch' : 'Same material';
}

// Slab photos persist to localStorage with everything else, so a 4000px phone
// photo cannot be stored as it arrives. 720px keeps the veining readable —
// which is the only reason the photo is there — at a fraction of the bytes.
function stoneShrinkImage(dataUrl, maxWidth) {
  return new Promise(resolve => {
    const im = new Image();
    im.onload = () => {
      const scale = Math.min(1, (maxWidth || 720) / im.width);
      const w = Math.round(im.width * scale), h = Math.round(im.height * scale);
      const c = document.createElement('canvas');
      c.width = w; c.height = h;
      c.getContext('2d').drawImage(im, 0, 0, w, h);
      // A stone photo is a photograph, never line art — JPEG every time, and
      // 0.72 is where the veining still reads.
      const url = c.toDataURL('image/jpeg', 0.72);
      resolve({ url, w, h, bytes: Math.round(url.length * 0.75) });
    };
    im.onerror = () => resolve({ url: dataUrl, w: 0, h: 0, bytes: Math.round(dataUrl.length * 0.75) });
    im.src = dataUrl;
  });
}
function stoneFmtBytes(b) {
  if (!b) return '—';
  if (b < 1024) return `${b} B`;
  if (b < 1024 * 1024) return `${Math.round(b / 1024)} KB`;
  return `${(b / 1024 / 1024).toFixed(1)} MB`;
}

// Name lookups against the app's own records. Every one of these returns a
// plain dash rather than inventing a placeholder record.
function stoneWarehouseName(ctx, id) {
  const w = (ctx.warehouses || []).find(x => x.id === id);
  return w ? w.name : '—';
}
function stoneVendorName(ctx, id) {
  const v = (ctx.vendors || []).find(x => x.id === id);
  return v ? v.name : '—';
}
function stoneContainerLabel(ctx, id) {
  const c = (ctx.exportContainers || []).find(x => x.id === id);
  return c ? (c.containerNumber || 'Container') : '—';
}
function stoneProject(ctx, id) {
  return (ctx.projects || []).find(p => p.id === id) || null;
}
function stoneProjectName(ctx, id) {
  const p = stoneProject(ctx, id);
  return p ? p.name : '—';
}
function stoneScope(ctx, projectId, scopeId) {
  const p = stoneProject(ctx, projectId);
  if (!p) return null;
  return (p.scopes || []).find(s => s.id === scopeId) || null;
}
function stoneScopeName(ctx, projectId, scopeId) {
  const s = stoneScope(ctx, projectId, scopeId);
  return s ? s.name : '—';
}
// Pickers offer the department the user is currently looking at; lookups
// always run against the full list, so a slab allocated to the other
// department's job never renders as "—".
function stonePickerProjects(ctx) {
  const list = ctx.projects || [];
  return typeof ctx.deptProjects === 'function' ? ctx.deptProjects(list) : list;
}

// ── Small shared UI ─────────────────────────────────────────────────────────

// The direction the veining runs, drawn rather than spelled out — on a list of
// forty slabs an arrow is read at a glance and a word is not.
function StoneVeinArrow({ direction, size }) {
  const s = size || 18;
  const stroke = 'var(--leon-brown)';
  const common = { stroke, strokeWidth: 2, strokeLinecap: 'round', fill: 'none' };
  let art = null;
  if (direction === 'Horizontal') art = <g><line x1="3" y1="12" x2="21" y2="12" {...common} /><polyline points="16,7 21,12 16,17" {...common} /></g>;
  else if (direction === 'Vertical') art = <g><line x1="12" y1="3" x2="12" y2="21" {...common} /><polyline points="7,16 12,21 17,16" {...common} /></g>;
  else if (direction === 'Diagonal') art = <g><line x1="4" y1="20" x2="20" y2="4" {...common} /><polyline points="13,4 20,4 20,11" {...common} /></g>;
  else if (direction === 'Random') art = <path d="M3 15 C 7 6, 11 20, 15 10 S 21 14, 21 9" {...common} />;
  else if (direction === 'Directionless') art = <g><circle cx="12" cy="12" r="7" {...common} /><circle cx="12" cy="12" r="1.8" fill={stroke} stroke="none" /></g>;
  else art = <g><line x1="12" y1="4" x2="12" y2="20" {...common} /><line x1="4" y1="12" x2="20" y2="12" {...common} /><line x1="6" y1="6" x2="18" y2="18" {...common} /></g>;
  return (
    <span title={`Vein direction — ${direction || 'not set'}`} className="inline-flex items-center shrink-0">
      <svg width={s} height={s} viewBox="0 0 24 24" aria-hidden="true">{art}</svg>
      <span className="sr-only">Vein direction {direction || 'not set'}</span>
    </span>
  );
}

// The photo of the actual slab, drawn at the aspect ratio of its REAL
// dimensions and carrying a scale bar. A slab photo without a scale is just a
// swatch — this is what lets someone look at the veining and judge where a
// 2400mm run of counter can come out of it.
function StoneScaledPhoto({ slab, width, showVein }) {
  const w = width || 360;
  const L = stoneNum(slab.lengthMm), W = stoneNum(slab.widthMm);
  const ratio = L > 0 && W > 0 ? W / L : 0.5;
  const h = Math.max(60, Math.round(w * ratio));
  const mmPerPx = L > 0 ? L / w : 0;
  // Largest round bar that still sits inside about half the photo's width.
  const barMm = [2000, 1000, 500, 250, 100].find(mm => mmPerPx > 0 && mm / mmPerPx <= w * 0.5) || null;
  const barPx = barMm && mmPerPx ? barMm / mmPerPx : 0;
  return (
    <div style={{ width: w }}>
      <div className="relative rounded-md overflow-hidden border border-[var(--leon-line)] bg-[var(--leon-cream)]" style={{ width: w, height: h }}>
        {slab.photoUrl
          ? <img src={slab.photoUrl} alt={`Slab ${slab.slabId}`} className="w-full h-full object-cover" />
          : <div className="w-full h-full flex items-center justify-center text-[var(--leon-black)]/25 text-xs italic">No slab photo</div>}
        {showVein !== false && slab.veinDirection && (
          <span className="absolute top-1.5 left-1.5 bg-white/85 rounded px-1 py-0.5 flex items-center">
            <StoneVeinArrow direction={slab.veinDirection} size={16} />
          </span>
        )}
        {barMm ? (
          <div className="absolute bottom-1.5 left-1.5 bg-white/85 rounded px-1.5 py-0.5">
            <div className="h-[3px] bg-[var(--leon-black)]" style={{ width: barPx }} />
            <div className="text-[9px] font-semibold text-[var(--leon-black)] leading-tight mt-0.5">
              {barMm >= 1000 ? `${barMm / 1000} m` : `${barMm} mm`}
            </div>
          </div>
        ) : null}
      </div>
      <p className="text-[11px] text-[var(--leon-black)]/50 mt-1">
        {L > 0 && W > 0
          ? <>Shown to scale — {fmtDim(L, 'Metric')} × {fmtDim(W, 'Metric')} · {stoneFmtArea(stoneSlabArea(slab))}</>
          : <>Enter the slab's real length and width to scale this photo.</>}
      </p>
    </div>
  );
}

// Irregular remnants are drawn from their own point list. Nothing is nested
// into this outline — it records the shape and gives the true area.
function StoneRemnantShape({ remnant, width }) {
  const w = width || 200;
  const L = stoneNum(remnant.lengthMm), W = stoneNum(remnant.widthMm);
  const h = L > 0 ? Math.max(40, Math.round(w * (W / L))) : 100;
  if (remnant.shape !== 'Irregular' || !remnant.points || remnant.points.length < 3) {
    return <div className="rounded border border-[var(--leon-brown-light)] bg-[var(--leon-cream)]" style={{ width: w, height: h }} />;
  }
  const scale = L > 0 ? w / L : 1;
  const pts = remnant.points.map(p => `${(stoneNum(p.x) * scale).toFixed(1)},${(stoneNum(p.y) * scale).toFixed(1)}`).join(' ');
  return (
    <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} className="rounded border border-[var(--leon-line)] bg-white">
      <polygon points={pts} fill="var(--leon-cream)" stroke="var(--leon-brown)" strokeWidth="1.5" />
    </svg>
  );
}

function StoneKpi({ label, value, sub, tone }) {
  const toneCls = tone === 'brown' ? 'bg-[var(--leon-brown)] text-white border-[var(--leon-brown)]' : 'bg-white border-[var(--leon-line)]';
  return (
    <div className={`rounded-lg border p-3 ${toneCls}`}>
      <div className={`text-[10px] uppercase tracking-wide ${tone === 'brown' ? 'opacity-70' : 'text-[var(--leon-black)]/45'}`}>{label}</div>
      <div className="text-xl font-bold leading-tight mt-0.5">{value}</div>
      {sub ? <div className={`text-[11px] mt-0.5 ${tone === 'brown' ? 'opacity-75' : 'text-[var(--leon-black)]/50'}`}>{sub}</div> : null}
    </div>
  );
}

function StoneSlabStatusBadge({ status }) {
  return <Badge tone={STONE_STATUS_TONE[status] || 'neutral'}>{status}</Badge>;
}
function StoneRemnantStatusBadge({ status }) {
  return <Badge tone={STONE_REMNANT_TONE[status] || 'neutral'}>{status}</Badge>;
}

function StoneThumb({ slab, size, onClick }) {
  const s = size || 40;
  const common = 'rounded border border-[var(--leon-line)] object-cover shrink-0';
  if (slab.photoUrl) {
    return <button type="button" onClick={onClick} title={slab.slabId}><img src={slab.photoUrl} alt="" loading="lazy" className={common} style={{ width: s, height: s }} /></button>;
  }
  return (
    <button type="button" onClick={onClick} title={slab.slabId}
      className={`${common} bg-[var(--leon-cream)] text-[10px] font-bold text-[var(--leon-black)]/35 flex items-center justify-center`}
      style={{ width: s, height: s }}>
      {stoneMaterialToken(slab.material).slice(0, 2)}
    </button>
  );
}

// A photo field that downscales on the way in and says what it did. Not
// FileField, because that one stores whatever it is handed — a slab photo has
// to be shrunk before it reaches localStorage.
function StonePhotoField({ url, name, onChange, editable }) {
  const inputRef = useRef(null);
  const [note, setNote] = useState('');
  async function onPick(e) {
    const f = e.target.files[0];
    e.target.value = '';
    if (!f) return;
    if (!/^image\//.test(f.type)) { setNote('That is not an image — photograph the slab as JPG or PNG.'); return; }
    const raw = await readFileAsDataURL(f);
    const shrunk = await stoneShrinkImage(raw, 720);
    onChange(shrunk.url, f.name);
    setNote(`Stored at ${shrunk.w}×${shrunk.h}, about ${stoneFmtBytes(shrunk.bytes)}. Slab photos are downscaled because they are saved in this browser with the rest of the app's data.`);
  }
  return (
    <div>
      <div className="flex items-center gap-2">
        {url
          ? <img src={url} alt="" className="w-16 h-16 object-cover rounded border border-[var(--leon-line)]" />
          : <div className="w-16 h-16 rounded border border-dashed border-[var(--leon-line)] bg-[var(--leon-cream)] flex items-center justify-center text-[var(--leon-black)]/25">🪨</div>}
        <div className="text-xs">
          <p className="font-semibold">{name || 'No photo of this slab'}</p>
          {editable && (
            <div className="flex gap-2 mt-1">
              <button type="button" className="text-[11px] font-semibold text-[var(--leon-brown)]" onClick={() => inputRef.current.click()}>{url ? 'Replace photo' : '+ Add photo'}</button>
              {url && <button type="button" className="text-[11px] font-semibold text-[var(--leon-black)]/40" onClick={() => { onChange(null, ''); setNote(''); }}>Remove</button>}
            </div>
          )}
          <input ref={inputRef} type="file" accept="image/*" className="hidden" onChange={onPick} />
        </div>
      </div>
      {note ? <p className="text-[11px] text-[var(--leon-black)]/50 mt-1.5">{note}</p> : null}
    </div>
  );
}

function StoneHistoryList({ history }) {
  if (!history || !history.length) return <p className="text-xs text-[var(--leon-black)]/40 italic">Nothing recorded yet.</p>;
  return (
    <div className="space-y-1">
      {history.map(h => (
        <p key={h.id} className="text-xs text-[var(--leon-black)]/60">
          {fmtDate(h.date)} — <span className="font-semibold">{h.user}</span>: {h.action}{h.note ? ` — ${h.note}` : ''}
        </p>
      ))}
    </div>
  );
}

// ── Entry point ─────────────────────────────────────────────────────────────

// LEON Countertop hosts BOTH halves of the job: the drawing and quoting side
// (countertops.jsx) and the slab side (this file). Drawing a kitchen produces
// the pieces; the slab side consumes them. They were two tiles and one job,
// which is the wrong way round, so this shell owns one tab bar and the drawing
// sections come first — you draw and price before you buy and cut.
function StoneSoftware({ ctx }) {
  const ctSections = (typeof CT_SECTIONS !== 'undefined' && typeof CtSection === 'function') ? CT_SECTIONS : [];
  // The old bar ran the two halves together with a "|" between them. In the
  // rail they become named groups, which says the same thing more plainly and
  // in the order the work actually happens: you draw and price before you buy
  // and cut. `group` is the only field added — the section objects themselves
  // are untouched, so countertops.jsx keeps ownership of its own half.
  const railSections = ctSections.map(t => ({ ...t, group: 'Draw & price' }))
    .concat(STONE_SUBTABS.map(t => ({ ...t, group: 'Stock & cut' })));
  // A link can say which job and which screen, not only which tool.
  const [sub, setSub] = useState(() =>
    (typeof swBootParam === 'function' && swBootParam('section'))
    || (ctSections.length ? ctSections[0].key : 'dashboard'));
  const [ctProjectId, setCtProjectId] = useState(() =>
    (typeof swBootParam === 'function' && swBootParam('project')) || '');
  // Slabs are physical stock, so the right to change them is the Inventory
  // module's right. There is no separate 'stone' module key and inventing one
  // would leave it ungoverned by the editable permission matrix.
  const editable = ctx.canEdit('warehouse');
  const slabs = ctx.slabs || [];
  const remnants = ctx.remnants || [];

  const counts = {
    slabs: slabs.length,
    remnants: remnants.filter(r => r.status === 'Available').length,
  };

  return (
    <div data-print-region="LEON Countertop">
      <div className="mb-5">
        <h1 className="text-2xl font-bold">LEON Countertop</h1>
        <p className="text-sm text-[var(--leon-black)]/50">
          Draw it, price it, then cut it. The quote and the drawing produce the pieces; the slab side
          holds the stock they come out of — every physical slab its own record, with its own photo,
          batch, lot and veining, because two slabs of the same colour are not interchangeable.
        </p>
      </div>

      {!editable && (
        <div className="mb-3">
          <LockedNotice label="You can read the slab inventory but not change it. Editing slabs follows your Inventory permission." />
        </div>
      )}

      <HubTools title="LEON Countertop" heading="LEON Countertop" />
      <SoftwareRail swKey="countertop" sections={railSections} active={sub}
        onSelect={setSub}
        status={<>
          <span>{counts.slabs} slab{counts.slabs === 1 ? '' : 's'} on record</span>
          <span className="opacity-40">·</span>
          <span>{counts.remnants} remnant{counts.remnants === 1 ? '' : 's'} available</span>
        </>}>

      {/* The drawing and quoting sections live in countertops.jsx, which loads
          after this file. Guarded, so a load-order change shows a named notice
          rather than an empty tab bar.
          The PROJECT PICKER belongs here, not in the section: the host owns the
          tab bar, so a quote and its drawing must stay on the same job when you
          move between them. Without it those sections asked for a project and
          gave you no way to choose one. */}
      {ctSections.some(t => t.key === sub) && (
        <div>
          <div className="flex items-end gap-3 flex-wrap mb-4">
            <Field label="Project">
              <Select className="!w-72" value={ctProjectId} onChange={e => {
                if (e.target.value === '__scratch') {
                  const sp = ctx.myScratchProject ? ctx.myScratchProject() : null;
                  setCtProjectId(sp ? sp.id : '');
                } else setCtProjectId(e.target.value);
              }}>
                <option value="">— select a project —</option>
                {/* STANDALONE. CounterGo lets a quote exist before it belongs
                    to anyone — most of LEON's own 254 quotes are Standalone —
                    and being forced to file a measure against a job before you
                    can draw it is how it ends up on the wrong one. The scratch
                    workspace already exists for exactly this, and the work can
                    be moved onto a real job later without being retyped. */}
                <option value="__scratch">— Standalone — not on a job yet —</option>
                {(ctx.toolProjects ? ctx.toolProjects() : ctx.deptProjects(ctx.projects || [])).map(p => (
                  <option key={p.id} value={p.id}>{p.name}{p.isScratch ? ' (unassigned)' : ''}</option>
                ))}
              </Select>
            </Field>
            <p className="text-[12px] text-[var(--leon-black)]/50 max-w-md">
              A quote can be <b>standalone</b> — drawn and priced before it belongs to a job, and moved
              onto one later. The price list is company-wide and needs no job at all.
            </p>
          </div>
          <CtSection ctx={ctx} sectionKey={sub} projectId={ctProjectId}
            onProject={setCtProjectId} onSection={setSub} />
        </div>
      )}

      {sub === 'dashboard' && <StoneDashboard ctx={ctx} />}
      {sub === 'slabs' && <StoneAllSlabs ctx={ctx} editable={editable} />}
      {sub === 'remnants' && <StoneRemnants ctx={ctx} editable={editable} />}
      {sub === 'allocation' && <StoneAllocation ctx={ctx} editable={editable} />}
      {sub === 'costing' && <StoneCosting ctx={ctx} />}
      {sub === 'cutlist' && <StoneCutList ctx={ctx} editable={editable} />}
      {sub === 'layout' && <StoneSlabLayout ctx={ctx} editable={editable} />}
      </SoftwareRail>
    </div>
  );
}

// ── Shared computation: cut list, requirement, waste ────────────────────────

// The cut list lives on the project (project.stoneCutList), not in a global
// collection — it is per-project data, and the contract forbids new top-level
// state. Every read goes through here so nothing assumes the field exists.
function stoneProjectPieces(project) {
  return (project && project.stoneCutList) || [];
}
function stoneAllPieces(ctx) {
  const out = [];
  (ctx.projects || []).forEach(p => stoneProjectPieces(p).forEach(pc => out.push({ piece: pc, project: p })));
  return out;
}

// What a project needs of one material, against what the warehouse actually
// holds. Required slabs is an ESTIMATE from total piece area and a yield
// assumption — not a nesting result, which this module does not compute.
function stoneMaterialRequirement(ctx, project, material, yieldFactor) {
  const slabs = ctx.slabs || [];
  const pieces = stoneProjectPieces(project).filter(p => p.material === material);
  const pieceArea = pieces.reduce((n, p) => n + stonePieceArea(p), 0);

  const sameMaterial = slabs.filter(s => s.material === material);
  const basisSlabs = sameMaterial.filter(s => stoneSlabArea(s) > 0);
  const basisArea = basisSlabs.length
    ? basisSlabs.reduce((n, s) => n + stoneSlabArea(s), 0) / basisSlabs.length
    : stoneAreaM2(STONE_NOMINAL_LENGTH_MM, STONE_NOMINAL_WIDTH_MM);
  const basisFromStock = basisSlabs.length > 0;

  const y = yieldFactor > 0 ? yieldFactor : STONE_DEFAULT_YIELD;
  const usablePerSlab = basisArea * y;
  const requiredSlabs = usablePerSlab > 0 ? Math.ceil(pieceArea / usablePerSlab) : 0;

  const allocated = sameMaterial.filter(s => s.projectId === project.id && STONE_DEAD_STATUSES.indexOf(s.status) === -1);
  const available = sameMaterial.filter(s => s.status === 'Available' && !s.projectId);

  return {
    material, pieces, pieceArea, basisArea, basisFromStock, usablePerSlab,
    requiredSlabs,
    allocatedSlabs: allocated.length, allocatedArea: allocated.reduce((n, s) => n + stoneSlabArea(s), 0),
    availableSlabs: available.length, availableArea: available.reduce((n, s) => n + stoneSlabArea(s), 0),
    additionalRequired: Math.max(0, requiredSlabs - allocated.length - available.length),
  };
}

// Waste is measured only against slabs a saw has actually touched. A slab
// sitting in the rack is not waste, and counting it as such would make the
// figure meaningless.
function stoneWasteSummary(ctx) {
  const slabs = ctx.slabs || [];
  const remnants = ctx.remnants || [];
  const cut = slabs.filter(s => STONE_CUT_STATUSES.indexOf(s.status) !== -1);
  const cutIds = {};
  cut.forEach(s => { cutIds[s.id] = true; });
  const gross = cut.reduce((n, s) => n + stoneSlabArea(s), 0);
  const piecesArea = stoneAllPieces(ctx).reduce((n, row) => (
    row.piece.slabId && cutIds[row.piece.slabId] ? n + stonePieceArea(row.piece) : n
  ), 0);
  // A remnant still on the shelf is recovered material, not waste. One that has
  // been scrapped or consumed no longer offsets anything.
  const recovered = remnants.reduce((n, r) => (
    cutIds[r.parentSlabId] && ['Available', 'Reserved', 'Allocated'].indexOf(r.status) !== -1 ? n + stoneRemnantArea(r) : n
  ), 0);
  const waste = Math.max(0, gross - piecesArea - recovered);
  return {
    slabsCut: cut.length, gross, piecesArea, recovered, waste,
    pct: gross > 0 ? (waste / gross) * 100 : null,
  };
}

// ── Dashboard ───────────────────────────────────────────────────────────────

function StoneDashboard({ ctx }) {
  const slabs = ctx.slabs || [];
  const remnants = ctx.remnants || [];

  const byStatus = {};
  STONE_STATUSES.forEach(s => { byStatus[s] = slabs.filter(x => x.status === s); });
  const available = byStatus['Available'] || [];
  const allocated = (byStatus['Allocated'] || []).concat(byStatus['Reserved'] || []);
  const inProduction = slabs.filter(s => STONE_PRODUCTION_STATUSES.indexOf(s.status) !== -1);
  const consumed = slabs.filter(s => s.status === 'Consumed' || s.status === 'Partially Consumed');
  const remnantsAvailable = remnants.filter(r => r.status === 'Available');

  // Everything still standing as stock — a consumed or damaged slab is not
  // inventory and a scrapped remnant is not either.
  const liveSlabs = slabs.filter(s => STONE_DEAD_STATUSES.indexOf(s.status) === -1);
  const liveRemnants = remnants.filter(r => r.status !== 'Consumed' && r.status !== 'Scrap');
  const totalArea = liveSlabs.reduce((n, s) => n + stoneSlabArea(s), 0) + liveRemnants.reduce((n, r) => n + stoneRemnantArea(r), 0);
  const totalValue = liveSlabs.reduce((n, s) => n + stoneLandedCost(s), 0) + liveRemnants.reduce((n, r) => n + stoneNum(r.costBasis), 0);

  const waste = stoneWasteSummary(ctx);

  // Variance: what the cut lists say the jobs need, against what has actually
  // been put aside for them. Positive means over-committed slabs; negative
  // means the jobs are short.
  const variance = useMemo(() => {
    let required = 0, allocatedCount = 0, jobs = 0;
    (ctx.projects || []).forEach(p => {
      const pieces = stoneProjectPieces(p);
      if (!pieces.length) return;
      jobs += 1;
      const materials = Array.from(new Set(pieces.map(x => x.material).filter(Boolean)));
      materials.forEach(m => {
        const r = stoneMaterialRequirement(ctx, p, m, STONE_DEFAULT_YIELD);
        required += r.requiredSlabs;
        allocatedCount += r.allocatedSlabs;
      });
    });
    return { required, allocated: allocatedCount, jobs, delta: allocatedCount - required };
  }, [ctx.projects, ctx.slabs]);

  const materials = Array.from(new Set(slabs.map(s => s.material).filter(Boolean))).sort();

  return (
    <div className="space-y-4">
      <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
        <StoneKpi label="Available" value={available.length} sub={`${stoneFmtAreaShort(available.reduce((n, s) => n + stoneSlabArea(s), 0))} on the rack`} />
        <StoneKpi label="Reserved / Allocated" value={allocated.length} sub="Put aside for a job" />
        <StoneKpi label="In production" value={inProduction.length} sub="Approved to cut or cutting" />
        <StoneKpi label="Consumed" value={consumed.length} sub="Fully or partly cut out" />
      </div>

      <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
        <StoneKpi label="Remnants available" value={remnantsAvailable.length}
          sub={`${stoneFmtAreaShort(remnantsAvailable.reduce((n, r) => n + stoneRemnantArea(r), 0))} recoverable`} />
        <StoneKpi label="Total inventory area" value={stoneFmtAreaShort(totalArea)} sub={`${stoneSqft(totalArea).toFixed(0)} ft² of slab and offcut`} />
        <StoneKpi label="Waste"
          value={waste.pct === null ? '—' : `${waste.pct.toFixed(1)}%`}
          sub={waste.pct === null ? 'No slab has been cut yet' : `${stoneFmtAreaShort(waste.waste)} lost of ${stoneFmtAreaShort(waste.gross)} cut`} />
        <StoneKpi label="Slab variance" tone="brown"
          value={variance.delta === 0 ? 'Balanced' : (variance.delta > 0 ? `+${variance.delta}` : String(variance.delta))}
          sub={variance.jobs ? `${variance.allocated} put aside vs ${variance.required} needed across ${variance.jobs} job${variance.jobs === 1 ? '' : 's'}` : 'No cut list entered yet'} />
      </div>

      {ctx.canSeeFin && (
        <div className="rounded-lg border border-[var(--leon-line)] bg-white p-4 flex flex-wrap gap-6">
          <div>
            <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Landed value on hand</div>
            <div className="text-2xl font-bold">{fmtMoney(totalValue)}</div>
          </div>
          <div>
            <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Average landed cost / ft²</div>
            <div className="text-lg font-semibold">
              {totalArea > 0 ? fmtMoney(totalValue / stoneSqft(totalArea)) : '—'}
            </div>
          </div>
          <div>
            <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Value tied up in remnants</div>
            <div className="text-lg font-semibold">{fmtMoney(liveRemnants.reduce((n, r) => n + stoneNum(r.costBasis), 0))}</div>
          </div>
        </div>
      )}

      <Collapsible title="Where the slabs are" id="stone-dash-status" defaultOpen count={slabs.length}>
        {!slabs.length ? <EmptyState text="No slabs recorded yet. Add the first one under All Slabs." /> : (
          <div className="overflow-x-auto">
            <table className="w-full text-xs">
              <thead className="bg-[var(--leon-cream)]">
                <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                  <th className="px-3 py-2">Status</th><th className="px-3 py-2">Slabs</th><th className="px-3 py-2">Area</th>
                  {ctx.canSeeFin && <th className="px-3 py-2">Landed value</th>}
                </tr>
              </thead>
              <tbody>
                {STONE_STATUSES.filter(s => byStatus[s].length).map(s => (
                  <tr key={s} className="border-t border-[var(--leon-line)]">
                    <td className="px-3 py-2"><StoneSlabStatusBadge status={s} /></td>
                    <td className="px-3 py-2 font-semibold">{byStatus[s].length}</td>
                    <td className="px-3 py-2">{stoneFmtArea(byStatus[s].reduce((n, x) => n + stoneSlabArea(x), 0))}</td>
                    {ctx.canSeeFin && <td className="px-3 py-2">{fmtMoney(byStatus[s].reduce((n, x) => n + stoneLandedCost(x), 0))}</td>}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </Collapsible>

      <Collapsible title="By material" id="stone-dash-material" count={materials.length}>
        {!materials.length ? <EmptyState text="Nothing to summarise yet." /> : (
          <div className="overflow-x-auto">
            <table className="w-full text-xs">
              <thead className="bg-[var(--leon-cream)]">
                <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                  <th className="px-3 py-2">Material</th><th className="px-3 py-2">Slabs</th><th className="px-3 py-2">Available</th>
                  <th className="px-3 py-2">Remnants</th><th className="px-3 py-2">Area on hand</th>
                </tr>
              </thead>
              <tbody>
                {materials.map(m => {
                  const ms = liveSlabs.filter(s => s.material === m);
                  const mr = liveRemnants.filter(r => r.material === m);
                  return (
                    <tr key={m} className="border-t border-[var(--leon-line)]">
                      <td className="px-3 py-2 font-semibold">{m}</td>
                      <td className="px-3 py-2">{ms.length}</td>
                      <td className="px-3 py-2">{ms.filter(s => s.status === 'Available').length}</td>
                      <td className="px-3 py-2">{mr.filter(r => r.status === 'Available').length}</td>
                      <td className="px-3 py-2">{stoneFmtArea(ms.reduce((n, s) => n + stoneSlabArea(s), 0) + mr.reduce((n, r) => n + stoneRemnantArea(r), 0))}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </Collapsible>

      <Collapsible title="How the waste figure is worked out" id="stone-dash-waste">
        <div className="text-xs text-[var(--leon-black)]/70 space-y-1.5">
          <p>Waste is measured only across the {waste.slabsCut} slab{waste.slabsCut === 1 ? '' : 's'} that have actually been cut. A slab standing in the rack is stock, not waste.</p>
          <p>Gross area cut: <b>{stoneFmtArea(waste.gross)}</b> · pieces taken off those slabs: <b>{stoneFmtArea(waste.piecesArea)}</b> · recovered as usable remnant: <b>{stoneFmtArea(waste.recovered)}</b> · unaccounted: <b>{stoneFmtArea(waste.waste)}</b>.</p>
          <p className="text-[var(--leon-black)]/50">Pieces are counted from the Cut List, so the figure is only as good as the cut list entered against those slabs. There is no automatic yield measurement here — that would need the nesting layout, which is not built.</p>
        </div>
      </Collapsible>
    </div>
  );
}

// ── All Slabs ───────────────────────────────────────────────────────────────

function StoneAllSlabs({ ctx, editable }) {
  const [search, setSearch] = useState('');
  const [statusFilter, setStatusFilter] = useState('All');
  const [materialFilter, setMaterialFilter] = useState('All');
  const [warehouseFilter, setWarehouseFilter] = useState('All');
  const [editing, setEditing] = useState(null);   // slab being edited, or 'new'
  const [detail, setDetail] = useState(null);
  const [allocating, setAllocating] = useState(null);
  const [remnantFrom, setRemnantFrom] = useState(null);

  const slabs = ctx.slabs || [];
  const materials = ['All'].concat(Array.from(new Set(slabs.map(s => s.material).filter(Boolean))).sort());

  const filtered = slabs.filter(s => {
    if (statusFilter !== 'All' && s.status !== statusFilter) return false;
    if (materialFilter !== 'All' && s.material !== materialFilter) return false;
    if (warehouseFilter !== 'All' && (s.warehouseId || '') !== warehouseFilter) return false;
    if (!search) return true;
    const q = search.toLowerCase();
    return [s.slabId, s.material, s.colour, s.collection, s.manufacturer, s.batch, s.lot, s.rack, s.poRef]
      .some(v => String(v || '').toLowerCase().includes(q));
  });

  return (
    <div>
      <div className="flex flex-wrap items-end gap-2 mb-3">
        <TextInput placeholder="Search slab ID, colour, batch, lot…" value={search} onChange={e => setSearch(e.target.value)} className="!w-64" />
        <Select value={statusFilter} onChange={e => setStatusFilter(e.target.value)} className="!w-auto">
          <option value="All">All statuses</option>
          {STONE_STATUSES.map(s => <option key={s}>{s}</option>)}
        </Select>
        <Select value={materialFilter} onChange={e => setMaterialFilter(e.target.value)} className="!w-auto">
          {materials.map(m => <option key={m} value={m}>{m === 'All' ? 'All materials' : m}</option>)}
        </Select>
        <Select value={warehouseFilter} onChange={e => setWarehouseFilter(e.target.value)} className="!w-auto">
          <option value="All">All warehouses</option>
          {(ctx.warehouses || []).map(w => <option key={w.id} value={w.id}>{w.name}</option>)}
        </Select>
        <span className="ml-auto" />
        {editable && <Button onClick={() => setEditing('new')}>+ New Slab</Button>}
      </div>

      <p className="text-xs text-[var(--leon-black)]/45 mb-2">
        Showing {filtered.length} of {slabs.length} slab{slabs.length === 1 ? '' : 's'}.
      </p>

      {!filtered.length ? <EmptyState text={slabs.length ? 'No slab matches those filters.' : 'No slabs recorded yet.'} /> : (
        <div className="overflow-x-auto bg-white border border-[var(--leon-line)] rounded-xl">
          <table className="w-full text-xs">
            <thead className="bg-[var(--leon-cream)]">
              <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                <th className="px-3 py-2 w-12"></th>
                <th className="px-3 py-2">Slab ID</th>
                <th className="px-3 py-2">Material / Colour</th>
                <th className="px-3 py-2">Batch · Lot</th>
                <th className="px-3 py-2">Size</th>
                <th className="px-3 py-2">Area</th>
                <th className="px-3 py-2">Vein</th>
                <th className="px-3 py-2">Location</th>
                <th className="px-3 py-2">Job</th>
                <th className="px-3 py-2">Status</th>
                {ctx.canSeeFin && <th className="px-3 py-2">Landed</th>}
                <th className="px-3 py-2"></th>
              </tr>
            </thead>
            <tbody>
              {filtered.map(s => (
                <tr key={s.id} className="border-t border-[var(--leon-line)]">
                  <td className="px-3 py-1.5"><StoneThumb slab={s} onClick={() => setDetail(s)} /></td>
                  <td className="px-3 py-2 font-semibold cursor-pointer hover:underline" onClick={() => setDetail(s)}>{s.slabId}</td>
                  <td className="px-3 py-2">
                    <div className="font-semibold">{s.colour || '—'}</div>
                    <div className="text-[var(--leon-black)]/45">{[s.material, s.collection].filter(Boolean).join(' · ') || '—'}</div>
                  </td>
                  <td className="px-3 py-2">{s.batch || '—'}{s.lot ? ` · ${s.lot}` : ''}</td>
                  <td className="px-3 py-2 whitespace-nowrap">{fmtDim(s.lengthMm, 'Metric')} × {fmtDim(s.widthMm, 'Metric')}<div className="text-[var(--leon-black)]/45">{fmtDim(s.thicknessMm, 'Metric')} thick</div></td>
                  <td className="px-3 py-2">{stoneFmtAreaShort(stoneSlabArea(s))}</td>
                  <td className="px-3 py-2"><StoneVeinArrow direction={s.veinDirection} /></td>
                  <td className="px-3 py-2">{stoneWarehouseName(ctx, s.warehouseId)}{s.rack ? <div className="text-[var(--leon-black)]/45">Rack {s.rack}</div> : null}</td>
                  <td className="px-3 py-2">
                    {s.projectId
                      ? <><div className="font-semibold">{stoneProjectName(ctx, s.projectId)}</div><div className="text-[var(--leon-black)]/45">{stoneScopeName(ctx, s.projectId, s.scopeId)}</div></>
                      : <span className="text-[var(--leon-black)]/30">—</span>}
                  </td>
                  <td className="px-3 py-2"><StoneSlabStatusBadge status={s.status} /></td>
                  {ctx.canSeeFin && <td className="px-3 py-2 font-semibold">{fmtMoney(stoneLandedCost(s))}</td>}
                  <td className="px-3 py-2 whitespace-nowrap">
                    {editable && <Button size="sm" variant="ghost" onClick={() => setEditing(s)}>Edit</Button>}
                    {editable && <Button size="sm" variant="ghost" onClick={() => setAllocating(s)}>Allocate</Button>}
                    {editable && <Button size="sm" variant="ghost" onClick={() => setRemnantFrom(s)}>Remnant</Button>}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      <StoneSlabModal ctx={ctx} open={!!editing} slab={editing === 'new' ? null : editing} onClose={() => setEditing(null)} />
      <StoneSlabDetailModal ctx={ctx} slab={detail} editable={editable} onClose={() => setDetail(null)}
        onAllocate={s => { setDetail(null); setAllocating(s); }}
        onRemnant={s => { setDetail(null); setRemnantFrom(s); }} />
      <StoneAllocateModal ctx={ctx} slab={allocating} onClose={() => setAllocating(null)} />
      <StoneCreateRemnantModal ctx={ctx} slab={remnantFrom} onClose={() => setRemnantFrom(null)} />
    </div>
  );
}

function StoneSlabModal({ ctx, open, slab, onClose }) {
  const blank = {
    slabId: '', material: STONE_MATERIALS[0], manufacturer: '', collection: '', colour: '', batch: '', lot: '',
    lengthMm: '', widthMm: '', thicknessMm: '', finish: STONE_FINISHES[0], pattern: '',
    veinDirection: 'Directionless', photoUrl: null, photoName: '',
    warehouseId: '', rack: '', vendorId: '', poRef: '', containerId: '', countryOfOrigin: '',
    unitCost: '', freightCost: '', tariffCost: '', handlingCost: '',
    status: 'Available', notes: '',
  };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open) return;
    if (slab) {
      setForm({
        slabId: slab.slabId || '', material: slab.material || '', manufacturer: slab.manufacturer || '',
        collection: slab.collection || '', colour: slab.colour || '', batch: slab.batch || '', lot: slab.lot || '',
        lengthMm: slab.lengthMm || '', widthMm: slab.widthMm || '', thicknessMm: slab.thicknessMm || '',
        finish: slab.finish || '', pattern: slab.pattern || '', veinDirection: slab.veinDirection || 'Directionless',
        photoUrl: slab.photoUrl || null, photoName: slab.photoName || '',
        warehouseId: slab.warehouseId || '', rack: slab.rack || '', vendorId: slab.vendorId || '',
        poRef: slab.poRef || '', containerId: slab.containerId || '', countryOfOrigin: slab.countryOfOrigin || '',
        unitCost: slab.unitCost || '', freightCost: slab.freightCost || '', tariffCost: slab.tariffCost || '',
        handlingCost: slab.handlingCost || '', status: slab.status || 'Available', notes: slab.notes || '',
      });
    } else {
      setForm(Object.assign({}, blank, { warehouseId: (ctx.warehouses || []).length ? ctx.warehouses[0].id : '' }));
    }
  }, [open, slab]);

  const set = (k, v) => setForm(f => Object.assign({}, f, { [k]: v }));
  const preview = Object.assign({}, form, {
    lengthMm: stoneNum(form.lengthMm), widthMm: stoneNum(form.widthMm), area: 0,
  });
  const valid = form.material && stoneNum(form.lengthMm) > 0 && stoneNum(form.widthMm) > 0;

  function submit() {
    if (!valid) return;
    if (slab) {
      ctx.setSlabs(prev => prev.map(s => {
        if (s.id !== slab.id) return s;
        const next = Object.assign({}, s, form, {
          lengthMm: stoneNum(form.lengthMm), widthMm: stoneNum(form.widthMm), thicknessMm: stoneNum(form.thicknessMm),
          unitCost: stoneNum(form.unitCost), freightCost: stoneNum(form.freightCost),
          tariffCost: stoneNum(form.tariffCost), handlingCost: stoneNum(form.handlingCost),
          warehouseId: form.warehouseId || null, vendorId: form.vendorId || null, containerId: form.containerId || null,
        });
        next.area = stoneAreaM2(next.lengthMm, next.widthMm);
        next.landedCost = stoneLandedCost(next);
        stonePushHistory(next, ctx.currentUserName, 'Slab record edited');
        if (s.status !== next.status) stonePushHistory(next, ctx.currentUserName, `Status changed from ${s.status} to ${next.status}`);
        return next;
      }));
    } else {
      ctx.setSlabs(prev => prev.concat([stoneMakeSlab(form, ctx.currentUserName, prev)]));
    }
    onClose();
  }

  return (
    <Modal open={open} onClose={onClose} wide title={slab ? `Edit — ${slab.slabId}` : 'New slab'}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit} disabled={!valid}>{slab ? 'Save' : 'Add slab'}</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-3 gap-3">
          <Field label="Slab ID" hint={slab ? '' : 'Left blank, one is generated per material'}>
            <TextInput value={form.slabId} onChange={e => set('slabId', e.target.value)} placeholder={stoneNextSlabId(ctx.slabs, form.material)} />
          </Field>
          <Field label="Material">
            <Select value={form.material} onChange={e => set('material', e.target.value)}>
              {(STONE_MATERIALS.indexOf(form.material) === -1 && form.material ? [form.material] : []).concat(STONE_MATERIALS).map(m => <option key={m}>{m}</option>)}
            </Select>
          </Field>
          <Field label="Status">
            <Select value={form.status} onChange={e => set('status', e.target.value)}>{STONE_STATUSES.map(s => <option key={s}>{s}</option>)}</Select>
          </Field>
        </div>

        <div className="grid grid-cols-3 gap-3">
          <Field label="Manufacturer"><TextInput value={form.manufacturer} onChange={e => set('manufacturer', e.target.value)} /></Field>
          <Field label="Collection"><TextInput value={form.collection} onChange={e => set('collection', e.target.value)} /></Field>
          <Field label="Colour"><TextInput value={form.colour} onChange={e => set('colour', e.target.value)} /></Field>
        </div>

        <div className="grid grid-cols-2 gap-3">
          <Field label="Batch" hint="The production run"><TextInput value={form.batch} onChange={e => set('batch', e.target.value)} /></Field>
          <Field label="Lot" hint="The bundle cut from one block — the strongest match"><TextInput value={form.lot} onChange={e => set('lot', e.target.value)} /></Field>
        </div>

        <div className="grid grid-cols-3 gap-3">
          <Field label="Length (mm)"><TextInput type="number" value={form.lengthMm} onChange={e => set('lengthMm', e.target.value)} /></Field>
          <Field label="Width (mm)"><TextInput type="number" value={form.widthMm} onChange={e => set('widthMm', e.target.value)} /></Field>
          <Field label="Thickness (mm)"><TextInput type="number" value={form.thicknessMm} onChange={e => set('thicknessMm', e.target.value)} /></Field>
        </div>
        <p className="text-[11px] text-[var(--leon-black)]/50 -mt-1">
          Area: <b>{stoneFmtArea(stoneAreaM2(form.lengthMm, form.widthMm))}</b>. Dimensions are stored in millimetres throughout this app.
        </p>

        <div className="grid grid-cols-3 gap-3">
          <Field label="Finish">
            <Select value={form.finish} onChange={e => set('finish', e.target.value)}>
              <option value="">—</option>
              {STONE_FINISHES.map(f => <option key={f}>{f}</option>)}
            </Select>
          </Field>
          <Field label="Pattern">
            <Select value={form.pattern} onChange={e => set('pattern', e.target.value)}>
              <option value="">—</option>
              {STONE_PATTERNS.map(p => <option key={p}>{p}</option>)}
            </Select>
          </Field>
          <Field label="Vein direction">
            <Select value={form.veinDirection} onChange={e => set('veinDirection', e.target.value)}>
              {STONE_VEIN_DIRECTIONS.map(v => <option key={v}>{v}</option>)}
            </Select>
          </Field>
        </div>
        <div className="flex items-center gap-2 text-xs text-[var(--leon-black)]/60">
          <StoneVeinArrow direction={form.veinDirection} size={22} />
          <span>
            {form.veinDirection === 'Directionless' ? 'Rotates freely — no grain to keep.' :
             form.veinDirection === 'Random' ? 'Movement runs every way; pieces cannot be rotated without the seam showing.' :
             form.veinDirection === 'Custom' ? 'Describe the veining in the notes so the shop knows how to lay it out.' :
             `Veining runs ${form.veinDirection.toLowerCase()} along the slab.`}
          </span>
        </div>

        <Field label="Photo of this slab" hint="Scaled against the real dimensions above, so the veining can be judged against a run of counter">
          <StonePhotoField url={form.photoUrl} name={form.photoName} editable
            onChange={(url, name) => setForm(f => Object.assign({}, f, { photoUrl: url, photoName: name }))} />
        </Field>
        {form.photoUrl && stoneNum(form.lengthMm) > 0 && (
          <div className="pt-1"><StoneScaledPhoto slab={preview} width={320} /></div>
        )}

        <div className="grid grid-cols-2 gap-3">
          <Field label="Warehouse">
            <Select value={form.warehouseId} onChange={e => set('warehouseId', e.target.value)}>
              <option value="">—</option>
              {(ctx.warehouses || []).map(w => <option key={w.id} value={w.id}>{w.name}</option>)}
            </Select>
          </Field>
          <Field label="Rack / bay"><TextInput value={form.rack} onChange={e => set('rack', e.target.value)} /></Field>
        </div>

        <div className="grid grid-cols-2 gap-3">
          <Field label="Vendor" hint="The supplier record this slab was bought from">
            <Select value={form.vendorId} onChange={e => set('vendorId', e.target.value)}>
              <option value="">—</option>
              {(ctx.vendors || []).map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
            </Select>
          </Field>
          <Field label="PO reference"><TextInput value={form.poRef} onChange={e => set('poRef', e.target.value)} /></Field>
        </div>
        <div className="grid grid-cols-2 gap-3">
          <Field label="Container" hint="The export container it shipped in">
            <Select value={form.containerId} onChange={e => set('containerId', e.target.value)}>
              <option value="">—</option>
              {(ctx.exportContainers || []).map(c => <option key={c.id} value={c.id}>{c.containerNumber || c.id}</option>)}
            </Select>
          </Field>
          <Field label="Country of origin"><TextInput value={form.countryOfOrigin} onChange={e => set('countryOfOrigin', e.target.value)} /></Field>
        </div>

        {ctx.canSeeFin && (
          <>
            <div className="grid grid-cols-4 gap-3">
              <Field label="Unit cost"><TextInput type="number" value={form.unitCost} onChange={e => set('unitCost', e.target.value)} /></Field>
              <Field label="Freight"><TextInput type="number" value={form.freightCost} onChange={e => set('freightCost', e.target.value)} /></Field>
              <Field label="Tariff / duty"><TextInput type="number" value={form.tariffCost} onChange={e => set('tariffCost', e.target.value)} /></Field>
              <Field label="Handling"><TextInput type="number" value={form.handlingCost} onChange={e => set('handlingCost', e.target.value)} /></Field>
            </div>
            <p className="text-[11px] text-[var(--leon-black)]/50 -mt-1">
              Landed cost <b>{fmtMoney(stoneLandedCost({ unitCost: form.unitCost, freightCost: form.freightCost, tariffCost: form.tariffCost, handlingCost: form.handlingCost }))}</b>
              {stoneAreaM2(form.lengthMm, form.widthMm) > 0 && <>
                {' '}· {fmtMoney(stoneLandedCost({ unitCost: form.unitCost, freightCost: form.freightCost, tariffCost: form.tariffCost, handlingCost: form.handlingCost }) / stoneSqft(stoneAreaM2(form.lengthMm, form.widthMm)))} per ft²
              </>}
            </p>
          </>
        )}

        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => set('notes', e.target.value)} /></Field>
      </div>
    </Modal>
  );
}

function StoneSlabDetailModal({ ctx, slab: opened, onClose, editable, onAllocate, onRemnant }) {
  if (!opened) return null;
  // The caller holds the slab object it was opened with, which goes stale the
  // moment a status is changed from in here. Everything below reads the live
  // record so the modal shows what was just saved.
  const slab = (ctx.slabs || []).find(s => s.id === opened.id) || opened;
  const remnants = (ctx.remnants || []).filter(r => r.parentSlabId === slab.id);
  const pieces = stoneAllPieces(ctx).filter(row => row.piece.slabId === slab.id);
  function setStatus(next) {
    ctx.setSlabs(prev => prev.map(s => {
      if (s.id !== slab.id) return s;
      const copy = Object.assign({}, s, { status: next });
      stonePushHistory(copy, ctx.currentUserName, `Status changed from ${s.status} to ${next}`);
      return copy;
    }));
  }
  function release() {
    ctx.setSlabs(prev => prev.map(s => {
      if (s.id !== slab.id) return s;
      const copy = Object.assign({}, s, { status: 'Available', projectId: null, scopeId: null });
      stonePushHistory(copy, ctx.currentUserName, `Released from ${stoneProjectName(ctx, s.projectId)} back to available stock`);
      return copy;
    }));
    onClose();
  }
  return (
    <Modal open={!!slab} onClose={onClose} wide title={`Slab — ${slab.slabId}`}
      footer={<Button variant="ghost" onClick={onClose}>Close</Button>}>
      <div className="space-y-4">
        <div className="flex flex-wrap gap-4">
          <StoneScaledPhoto slab={slab} width={300} />
          <div className="flex-1 min-w-[240px] space-y-1.5 text-xs">
            <div className="flex items-center gap-2 mb-1"><StoneSlabStatusBadge status={slab.status} /><StoneVeinArrow direction={slab.veinDirection} size={20} /><span className="text-[var(--leon-black)]/55">{slab.veinDirection}</span></div>
            <p><span className="text-[var(--leon-black)]/45">Colour</span> <b>{slab.colour || '—'}</b></p>
            <p><span className="text-[var(--leon-black)]/45">Material / collection</span> <b>{[slab.material, slab.collection].filter(Boolean).join(' · ') || '—'}</b></p>
            <p><span className="text-[var(--leon-black)]/45">Manufacturer</span> <b>{slab.manufacturer || '—'}</b></p>
            <p><span className="text-[var(--leon-black)]/45">Batch · Lot</span> <b>{slab.batch || '—'}{slab.lot ? ` · ${slab.lot}` : ''}</b></p>
            <p><span className="text-[var(--leon-black)]/45">Finish / pattern</span> <b>{[slab.finish, slab.pattern].filter(Boolean).join(' · ') || '—'}</b></p>
            <p><span className="text-[var(--leon-black)]/45">Location</span> <b>{stoneWarehouseName(ctx, slab.warehouseId)}{slab.rack ? ` · rack ${slab.rack}` : ''}</b></p>
            <p><span className="text-[var(--leon-black)]/45">Vendor · PO</span> <b>{stoneVendorName(ctx, slab.vendorId)}{slab.poRef ? ` · ${slab.poRef}` : ''}</b></p>
            <p><span className="text-[var(--leon-black)]/45">Container · origin</span> <b>{stoneContainerLabel(ctx, slab.containerId)}{slab.countryOfOrigin ? ` · ${slab.countryOfOrigin}` : ''}</b></p>
            <p><span className="text-[var(--leon-black)]/45">Job</span> <b>{slab.projectId ? `${stoneProjectName(ctx, slab.projectId)} — ${stoneScopeName(ctx, slab.projectId, slab.scopeId)}` : 'Not allocated'}</b></p>
            {slab.notes ? <p className="text-[var(--leon-black)]/60 pt-1">{slab.notes}</p> : null}
          </div>
        </div>

        {ctx.canSeeFin && (
          <div className="rounded-lg bg-[var(--leon-cream)] p-3 text-xs flex flex-wrap gap-5">
            <span><span className="text-[var(--leon-black)]/45">Unit</span> <b>{fmtMoney(slab.unitCost)}</b></span>
            <span><span className="text-[var(--leon-black)]/45">Freight</span> <b>{fmtMoney(slab.freightCost)}</b></span>
            <span><span className="text-[var(--leon-black)]/45">Tariff</span> <b>{fmtMoney(slab.tariffCost)}</b></span>
            <span><span className="text-[var(--leon-black)]/45">Handling</span> <b>{fmtMoney(slab.handlingCost)}</b></span>
            <span><span className="text-[var(--leon-black)]/45">Landed</span> <b>{fmtMoney(stoneLandedCost(slab))}</b></span>
            <span><span className="text-[var(--leon-black)]/45">Per ft²</span> <b>{stoneCostPerSqft(slab) === null ? '—' : fmtMoney(stoneCostPerSqft(slab))}</b></span>
            <span><span className="text-[var(--leon-black)]/45">Per m²</span> <b>{stoneCostPerM2(slab) === null ? '—' : fmtMoney(stoneCostPerM2(slab))}</b></span>
          </div>
        )}

        {editable && (
          <div className="flex flex-wrap items-end gap-2">
            <Field label="Move to status">
              <Select value={slab.status} onChange={e => setStatus(e.target.value)} className="!w-auto">
                {STONE_STATUSES.map(s => <option key={s}>{s}</option>)}
              </Select>
            </Field>
            <Button variant="outline" size="sm" onClick={() => onAllocate && onAllocate(slab)}>Allocate to a job</Button>
            <Button variant="outline" size="sm" onClick={() => onRemnant && onRemnant(slab)}>Create a remnant</Button>
            {slab.projectId && <Button variant="ghost" size="sm" onClick={release}>Release allocation</Button>}
          </div>
        )}

        <div>
          <p className="text-xs font-semibold mb-1">Remnants off this slab ({remnants.length})</p>
          {!remnants.length ? <p className="text-xs text-[var(--leon-black)]/40 italic">None recorded.</p> : (
            <div className="space-y-1">
              {remnants.map(r => (
                <p key={r.id} className="text-xs">
                  <b>{r.remnantId}</b> — {fmtDim(r.lengthMm, 'Metric')} × {fmtDim(r.widthMm, 'Metric')} · {stoneFmtAreaShort(stoneRemnantArea(r))} · {r.shape} · <StoneRemnantStatusBadge status={r.status} />
                </p>
              ))}
            </div>
          )}
        </div>

        <div>
          <p className="text-xs font-semibold mb-1">Cut list pieces assigned to this slab ({pieces.length})</p>
          {!pieces.length ? <p className="text-xs text-[var(--leon-black)]/40 italic">None assigned.</p> : (
            <div className="space-y-1">
              {pieces.map(row => (
                <p key={row.piece.id} className="text-xs">
                  <b>{row.piece.label || 'Piece'}</b> ×{row.piece.qty} — {fmtDim(row.piece.lengthMm, 'Metric')} × {fmtDim(row.piece.widthMm, 'Metric')} · {row.project.name} · {row.piece.status}
                </p>
              ))}
            </div>
          )}
        </div>

        <div>
          <p className="text-xs font-semibold mb-1">History</p>
          <StoneHistoryList history={slab.history} />
        </div>
      </div>
    </Modal>
  );
}

// Allocating puts a slab aside for a job. It deliberately does NOT mark the
// slab consumed — a slab is only consumed once it has actually been cut, and
// conflating the two is how stone inventories stop matching the rack.
function StoneAllocateModal({ ctx, slab, onClose }) {
  const [projectId, setProjectId] = useState('');
  const [scopeId, setScopeId] = useState('');
  const [mode, setMode] = useState('Allocated');
  const [note, setNote] = useState('');
  useEffect(() => {
    if (!slab) return;
    setProjectId(slab.projectId || '');
    setScopeId(slab.scopeId || '');
    setMode(slab.status === 'Reserved' ? 'Reserved' : 'Allocated');
    setNote('');
  }, [slab]);
  if (!slab) return null;

  const project = stoneProject(ctx, projectId);
  const scopes = project ? (project.scopes || []) : [];

  function submit() {
    if (!projectId) return;
    ctx.setSlabs(prev => prev.map(s => {
      if (s.id !== slab.id) return s;
      const copy = Object.assign({}, s, { projectId, scopeId: scopeId || null, status: mode });
      stonePushHistory(copy, ctx.currentUserName,
        `${mode === 'Reserved' ? 'Reserved' : 'Allocated'} to ${stoneProjectName(ctx, projectId)}${scopeId ? ` — ${stoneScopeName(ctx, projectId, scopeId)}` : ''}`,
        note);
      return copy;
    }));
    onClose();
  }

  return (
    <Modal open={!!slab} onClose={onClose} title={`Allocate — ${slab.slabId}`}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit} disabled={!projectId}>Allocate</Button></>}>
      <div className="space-y-3">
        <Field label="Project">
          <Select value={projectId} onChange={e => { setProjectId(e.target.value); setScopeId(''); }}>
            <option value="">Select a project…</option>
            {stonePickerProjects(ctx).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
        </Field>
        <Field label="Scope" hint={project && !scopes.length ? 'This project has no scopes yet' : ''}>
          <Select value={scopeId} onChange={e => setScopeId(e.target.value)} disabled={!project}>
            <option value="">Whole project (no scope)</option>
            {scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
          </Select>
        </Field>
        <Field label="Put aside as">
          <Select value={mode} onChange={e => setMode(e.target.value)}>
            <option value="Reserved">Reserved — held while the job is confirmed</option>
            <option value="Allocated">Allocated — committed to this job</option>
          </Select>
        </Field>
        <Field label="Note"><TextInput value={note} onChange={e => setNote(e.target.value)} placeholder="Why this slab, e.g. matches the island lot" /></Field>
        <p className="text-[11px] text-[var(--leon-black)]/50">
          This puts the slab aside and records who did it. It does not mark the slab cut or consumed — that happens when the saw runs.
        </p>
      </div>
    </Modal>
  );
}

// ── Remnants ────────────────────────────────────────────────────────────────

function StoneCreateRemnantModal({ ctx, slab, onClose }) {
  const blank = { remnantId: '', shape: 'Rectangle', lengthMm: '', widthMm: '', pointsText: '', warehouseId: '', rack: '', status: 'Available', notes: '', parentStatus: 'Remnant Created', photoUrl: null, photoName: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!slab) return;
    setForm(Object.assign({}, blank, { warehouseId: slab.warehouseId || '', rack: slab.rack || '' }));
  }, [slab]);
  if (!slab) return null;

  const set = (k, v) => setForm(f => Object.assign({}, f, { [k]: v }));
  const points = form.shape === 'Irregular' ? stoneParsePoints(form.pointsText) : [];
  const bounds = points.length >= 3 ? stonePointsBounds(points) : null;
  const effLength = bounds ? bounds.lengthMm : stoneNum(form.lengthMm);
  const effWidth = bounds ? bounds.widthMm : stoneNum(form.widthMm);
  const area = points.length >= 3 ? stonePolygonAreaM2(points) : stoneAreaM2(effLength, effWidth);
  const parentArea = stoneSlabArea(slab);
  const valid = effLength > 0 && effWidth > 0 && area > 0 && area <= parentArea;

  function submit() {
    if (!valid) return;
    const draft = Object.assign({}, form, { lengthMm: effLength, widthMm: effWidth, points });
    ctx.setRemnants(prev => prev.concat([stoneMakeRemnant(slab, draft, ctx.currentUserName, prev)]));
    ctx.setSlabs(prev => prev.map(s => {
      if (s.id !== slab.id) return s;
      const copy = Object.assign({}, s, { status: form.parentStatus });
      stonePushHistory(copy, ctx.currentUserName, `Remnant taken off this slab (${stoneFmtAreaShort(area)}); slab moved to ${form.parentStatus}`);
      return copy;
    }));
    onClose();
  }

  return (
    <Modal open={!!slab} onClose={onClose} wide title={`Create a remnant from ${slab.slabId}`}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit} disabled={!valid}>Create remnant</Button></>}>
      <div className="space-y-3">
        <p className="text-xs text-[var(--leon-black)]/60">
          The remnant inherits <b>{slab.material}</b> {slab.colour ? <>· <b>{slab.colour}</b> </> : null}
          · batch <b>{slab.batch || '—'}</b> · lot <b>{slab.lot || '—'}</b> · {fmtDim(slab.thicknessMm, 'Metric')} thick, from the parent slab.
          Those are not re-typed here — a remnant that disagrees with its own slab is worse than no remnant record.
        </p>

        <div className="grid grid-cols-2 gap-3">
          <Field label="Remnant ID" hint="Generated if left blank"><TextInput value={form.remnantId} onChange={e => set('remnantId', e.target.value)} placeholder={stoneNextRemnantId(ctx.remnants, slab.material)} /></Field>
          <Field label="Shape">
            <Select value={form.shape} onChange={e => set('shape', e.target.value)}>
              <option value="Rectangle">Rectangle</option>
              <option value="Irregular">Irregular — record the outline</option>
            </Select>
          </Field>
        </div>

        {form.shape === 'Rectangle' ? (
          <div className="grid grid-cols-2 gap-3">
            <Field label="Length (mm)"><TextInput type="number" value={form.lengthMm} onChange={e => set('lengthMm', e.target.value)} /></Field>
            <Field label="Width (mm)"><TextInput type="number" value={form.widthMm} onChange={e => set('widthMm', e.target.value)} /></Field>
          </div>
        ) : (
          <>
            <Field label="Outline points" hint="One x,y pair per line, in millimetres, walking the outline in order. Most offcuts are an L or a wedge — four to six points.">
              <TextArea rows={5} value={form.pointsText} onChange={e => set('pointsText', e.target.value)} placeholder={'0,0\n2400,0\n2400,600\n900,600\n900,1400\n0,1400'} />
            </Field>
            <div className="flex items-start gap-4">
              <StoneRemnantShape remnant={{ shape: 'Irregular', points, lengthMm: effLength, widthMm: effWidth }} width={220} />
              <div className="text-xs text-[var(--leon-black)]/60 space-y-1">
                <p>{points.length} readable point{points.length === 1 ? '' : 's'}{points.length && points.length < 3 ? ' — at least three are needed' : ''}.</p>
                <p>Bounding box <b>{fmtDim(effLength, 'Metric')} × {fmtDim(effWidth, 'Metric')}</b></p>
                <p>True area <b>{stoneFmtArea(area)}</b></p>
                <p className="text-[var(--leon-black)]/45">The outline is recorded so the true area and shape are known. Nothing is laid out inside it — there is no nesting engine here.</p>
              </div>
            </div>
          </>
        )}

        <p className="text-[11px] text-[var(--leon-black)]/60">
          Area <b>{stoneFmtArea(area)}</b> of the parent slab's {stoneFmtArea(parentArea)}
          {area > parentArea ? <span className="text-[var(--leon-red)]"> — larger than the slab it came from.</span> : null}
          {ctx.canSeeFin && parentArea > 0 ? <> · cost basis <b>{fmtMoney((stoneLandedCost(slab) * area) / parentArea)}</b>, pro-rated by area from the parent's landed cost.</> : null}
        </p>

        <div className="grid grid-cols-3 gap-3">
          <Field label="Warehouse">
            <Select value={form.warehouseId} onChange={e => set('warehouseId', e.target.value)}>
              <option value="">—</option>
              {(ctx.warehouses || []).map(w => <option key={w.id} value={w.id}>{w.name}</option>)}
            </Select>
          </Field>
          <Field label="Rack / bay"><TextInput value={form.rack} onChange={e => set('rack', e.target.value)} /></Field>
          <Field label="Remnant status">
            <Select value={form.status} onChange={e => set('status', e.target.value)}>{STONE_REMNANT_STATUSES.map(s => <option key={s}>{s}</option>)}</Select>
          </Field>
        </div>

        <Field label="Parent slab becomes" hint="Partially Consumed if more is still to come off it; Consumed if this was the last of it">
          <Select value={form.parentStatus} onChange={e => set('parentStatus', e.target.value)}>
            <option value="Remnant Created">Remnant Created</option>
            <option value="Partially Consumed">Partially Consumed</option>
            <option value="Consumed">Consumed</option>
          </Select>
        </Field>

        <Field label="Photo of the offcut">
          <StonePhotoField url={form.photoUrl} name={form.photoName} editable
            onChange={(url, name) => setForm(f => Object.assign({}, f, { photoUrl: url, photoName: name }))} />
        </Field>

        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => set('notes', e.target.value)} /></Field>
      </div>
    </Modal>
  );
}

// The headline feature. Before anyone orders a slab, this asks whether the yard
// already holds something the piece can come out of — and it answers with the
// same-batch offcuts first, because a match that does not match is no use.
function StoneRemnantSearchPanel({ ctx, editable, defaults }) {
  const [need, setNeed] = useState(Object.assign({
    material: '', lengthMm: '', widthMm: '', thicknessMm: '', batch: '', lot: '',
    kerfMm: STONE_DEFAULT_KERF_MM, includeSlabs: true,
  }, defaults || {}));
  const [assigning, setAssigning] = useState(null);

  const set = (k, v) => setNeed(n => Object.assign({}, n, { [k]: v }));
  const remnants = ctx.remnants || [];
  const slabs = ctx.slabs || [];
  const searched = stoneNum(need.lengthMm) > 0 && stoneNum(need.widthMm) > 0;

  const matches = useMemo(() => {
    if (!searched) return [];
    const out = [];
    remnants.forEach(r => {
      if (r.status !== 'Available' && r.status !== 'Reserved') return;
      if (need.material && r.material !== need.material) return;
      if (stoneNum(need.thicknessMm) > 0 && stoneNum(r.thicknessMm) !== stoneNum(need.thicknessMm)) return;
      const fit = stoneRemnantFit(r, need.lengthMm, need.widthMm, need.kerfMm);
      if (!fit.fits) return;
      const area = stoneRemnantArea(r);
      out.push({
        kind: 'remnant', record: r, fit,
        rank: stoneRemnantMatchRank(r, need),
        leftover: area - stoneAreaM2(need.lengthMm, need.widthMm),
        area,
      });
    });
    // Best batch match first; within a tier the tightest offcut, so a big
    // remnant is not spent on a small piece.
    out.sort((a, b) => (b.rank - a.rank) || (a.leftover - b.leftover));
    return out;
  }, [remnants, need, searched]);

  const slabMatches = useMemo(() => {
    if (!searched || !need.includeSlabs) return [];
    const out = [];
    slabs.forEach(s => {
      if (s.status !== 'Available') return;
      if (need.material && s.material !== need.material) return;
      if (stoneNum(need.thicknessMm) > 0 && stoneNum(s.thicknessMm) !== stoneNum(need.thicknessMm)) return;
      const fit = stoneRemnantFit(s, need.lengthMm, need.widthMm, need.kerfMm);
      if (!fit.fits) return;
      out.push({ kind: 'slab', record: s, fit, rank: stoneRemnantMatchRank(s, need), area: stoneSlabArea(s) });
    });
    out.sort((a, b) => (b.rank - a.rank) || (a.area - b.area));
    return out;
  }, [slabs, need, searched]);

  const materials = Array.from(new Set(remnants.concat(slabs).map(x => x.material).filter(Boolean))).sort();

  return (
    <div className="rounded-lg border border-[var(--leon-brown-light)] bg-white overflow-hidden mb-4">
      <div className="px-4 py-2.5 bg-[var(--leon-cream)] border-b border-[var(--leon-line)]">
        <p className="text-sm font-bold">Check the remnants before ordering a slab</p>
        <p className="text-[11px] text-[var(--leon-black)]/55">Enter the piece you need. Offcuts from the same batch and lot are listed first, because they are the only ones guaranteed to match.</p>
      </div>
      <div className="p-4 space-y-3">
        <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
          <Field label="Material">
            <Select value={need.material} onChange={e => set('material', e.target.value)}>
              <option value="">Any material</option>
              {materials.map(m => <option key={m}>{m}</option>)}
            </Select>
          </Field>
          <Field label="Piece length (mm)"><TextInput type="number" value={need.lengthMm} onChange={e => set('lengthMm', e.target.value)} /></Field>
          <Field label="Piece width (mm)"><TextInput type="number" value={need.widthMm} onChange={e => set('widthMm', e.target.value)} /></Field>
          <Field label="Thickness (mm)" hint="Blank matches any"><TextInput type="number" value={need.thicknessMm} onChange={e => set('thicknessMm', e.target.value)} /></Field>
        </div>
        <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
          <Field label="Batch to match" hint="Prioritised, not required"><TextInput value={need.batch} onChange={e => set('batch', e.target.value)} /></Field>
          <Field label="Lot to match"><TextInput value={need.lot} onChange={e => set('lot', e.target.value)} /></Field>
          <Field label="Kerf + trim (mm)" hint="Added to both dimensions"><TextInput type="number" value={need.kerfMm} onChange={e => set('kerfMm', e.target.value)} /></Field>
          <Field label="Also show whole slabs">
            <Select value={need.includeSlabs ? 'yes' : 'no'} onChange={e => set('includeSlabs', e.target.value === 'yes')}>
              <option value="yes">Yes — after the remnants</option>
              <option value="no">No — remnants only</option>
            </Select>
          </Field>
        </div>

        {!searched ? (
          <EmptyState text="Enter the length and width of the piece you need." />
        ) : (
          <>
            <div>
              <p className="text-xs font-semibold mb-1.5">
                {matches.length ? `${matches.length} remnant${matches.length === 1 ? '' : 's'} could take this piece` : 'No remnant in stock takes this piece'}
              </p>
              {!matches.length ? (
                <p className="text-xs text-[var(--leon-black)]/55">Nothing on the shelf fits — this piece needs a slab.</p>
              ) : (
                <div className="overflow-x-auto border border-[var(--leon-line)] rounded-lg">
                  <table className="w-full text-xs">
                    <thead className="bg-[var(--leon-cream)]">
                      <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                        <th className="px-3 py-2">Match</th><th className="px-3 py-2">Remnant</th><th className="px-3 py-2">Material / colour</th>
                        <th className="px-3 py-2">Batch · Lot</th><th className="px-3 py-2">Size</th><th className="px-3 py-2">Fit</th>
                        <th className="px-3 py-2">Left over</th><th className="px-3 py-2">Where</th>
                        {ctx.canSeeFin && <th className="px-3 py-2">Cost basis</th>}
                        <th className="px-3 py-2"></th>
                      </tr>
                    </thead>
                    <tbody>
                      {matches.map(m => (
                        <tr key={m.record.id} className="border-t border-[var(--leon-line)]">
                          <td className="px-3 py-2"><Badge tone={m.rank === 3 ? 'green' : m.rank === 2 ? 'yellow' : 'neutral'}>{stoneRankLabel(m.rank)}</Badge></td>
                          <td className="px-3 py-2 font-semibold">{m.record.remnantId}<div className="text-[var(--leon-black)]/40 font-normal">{m.record.shape}</div></td>
                          <td className="px-3 py-2">{m.record.material}{m.record.colour ? <div className="text-[var(--leon-black)]/45">{m.record.colour}</div> : null}</td>
                          <td className="px-3 py-2">{m.record.batch || '—'}{m.record.lot ? ` · ${m.record.lot}` : ''}</td>
                          <td className="px-3 py-2 whitespace-nowrap">{fmtDim(m.record.lengthMm, 'Metric')} × {fmtDim(m.record.widthMm, 'Metric')}</td>
                          <td className="px-3 py-2">{m.fit.rotated ? 'Rotated 90°' : 'As laid'}{m.record.shape === 'Irregular' ? <div className="text-[var(--leon-yellow)]">bounding box only</div> : null}</td>
                          <td className="px-3 py-2">{stoneFmtAreaShort(Math.max(0, m.leftover))}</td>
                          <td className="px-3 py-2">{stoneWarehouseName(ctx, m.record.warehouseId)}{m.record.rack ? ` · ${m.record.rack}` : ''}</td>
                          {ctx.canSeeFin && <td className="px-3 py-2">{fmtMoney(m.record.costBasis)}</td>}
                          <td className="px-3 py-2">{editable && m.record.status === 'Available' && <Button size="sm" variant="ghost" onClick={() => setAssigning(m.record)}>Reserve</Button>}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              )}
              {matches.some(m => m.record.shape === 'Irregular') && (
                <p className="text-[11px] text-[var(--leon-yellow)] mt-1.5">
                  An irregular remnant is only checked against its bounding box. Whether the piece actually fits inside the outline needs a layout, which this module does not compute — look at the shape before committing it.
                </p>
              )}
            </div>

            {need.includeSlabs && (
              <div>
                <p className="text-xs font-semibold mb-1.5">{slabMatches.length ? `${slabMatches.length} whole slab${slabMatches.length === 1 ? '' : 's'} would also take it` : 'No whole slab in stock takes this piece either'}</p>
                {slabMatches.length ? (
                  <div className="space-y-1">
                    {slabMatches.slice(0, 8).map(m => (
                      <p key={m.record.id} className="text-xs">
                        <b>{m.record.slabId}</b> — {m.record.colour || m.record.material} · batch {m.record.batch || '—'}{m.record.lot ? ` · lot ${m.record.lot}` : ''} · {fmtDim(m.record.lengthMm, 'Metric')} × {fmtDim(m.record.widthMm, 'Metric')} · {stoneWarehouseName(ctx, m.record.warehouseId)}
                        {' '}<Badge tone={m.rank === 3 ? 'green' : m.rank === 2 ? 'yellow' : 'neutral'}>{stoneRankLabel(m.rank)}</Badge>
                      </p>
                    ))}
                    {slabMatches.length > 8 && <p className="text-[11px] text-[var(--leon-black)]/45">…and {slabMatches.length - 8} more.</p>}
                  </div>
                ) : null}
              </div>
            )}
          </>
        )}
      </div>
      <StoneReserveRemnantModal ctx={ctx} remnant={assigning} onClose={() => setAssigning(null)} />
    </div>
  );
}

function StoneReserveRemnantModal({ ctx, remnant, onClose }) {
  const [projectId, setProjectId] = useState('');
  const [scopeId, setScopeId] = useState('');
  const [status, setStatus] = useState('Reserved');
  useEffect(() => {
    if (!remnant) return;
    setProjectId(remnant.projectId || '');
    setScopeId(remnant.scopeId || '');
    setStatus(remnant.status === 'Allocated' ? 'Allocated' : 'Reserved');
  }, [remnant]);
  if (!remnant) return null;
  const project = stoneProject(ctx, projectId);

  function submit() {
    if (!projectId) return;
    ctx.setRemnants(prev => prev.map(r => {
      if (r.id !== remnant.id) return r;
      const copy = Object.assign({}, r, { projectId, scopeId: scopeId || null, status });
      stonePushHistory(copy, ctx.currentUserName, `${status} for ${stoneProjectName(ctx, projectId)}${scopeId ? ` — ${stoneScopeName(ctx, projectId, scopeId)}` : ''}`);
      return copy;
    }));
    onClose();
  }
  return (
    <Modal open={!!remnant} onClose={onClose} title={`Put aside — ${remnant.remnantId}`}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={submit} disabled={!projectId}>Save</Button></>}>
      <div className="space-y-3">
        <Field label="Project">
          <Select value={projectId} onChange={e => { setProjectId(e.target.value); setScopeId(''); }}>
            <option value="">Select a project…</option>
            {stonePickerProjects(ctx).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
        </Field>
        <Field label="Scope">
          <Select value={scopeId} onChange={e => setScopeId(e.target.value)} disabled={!project}>
            <option value="">Whole project (no scope)</option>
            {(project ? project.scopes || [] : []).map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
          </Select>
        </Field>
        <Field label="Status">
          <Select value={status} onChange={e => setStatus(e.target.value)}>
            <option value="Reserved">Reserved</option>
            <option value="Allocated">Allocated</option>
          </Select>
        </Field>
      </div>
    </Modal>
  );
}

function StoneRemnants({ ctx, editable }) {
  const [search, setSearch] = useState('');
  const [statusFilter, setStatusFilter] = useState('Available');
  const [detailId, setDetailId] = useState(null);
  const remnants = ctx.remnants || [];
  // Held by id, not by object — a status changed from the list behind the modal
  // would otherwise leave the open record showing its old state.
  const detail = detailId ? remnants.find(r => r.id === detailId) || null : null;

  const filtered = remnants.filter(r => {
    if (statusFilter !== 'All' && r.status !== statusFilter) return false;
    if (!search) return true;
    const q = search.toLowerCase();
    return [r.remnantId, r.material, r.colour, r.batch, r.lot, r.parentSlabRef, r.rack].some(v => String(v || '').toLowerCase().includes(q));
  });

  function setStatus(id, next) {
    ctx.setRemnants(prev => prev.map(r => {
      if (r.id !== id) return r;
      const copy = Object.assign({}, r, { status: next });
      if (next === 'Available') { copy.projectId = null; copy.scopeId = null; }
      stonePushHistory(copy, ctx.currentUserName, `Status changed from ${r.status} to ${next}`);
      return copy;
    }));
  }

  return (
    <div>
      <StoneRemnantSearchPanel ctx={ctx} editable={editable} />

      <div className="flex flex-wrap items-end gap-2 mb-3">
        <TextInput placeholder="Search remnant ID, colour, batch, parent slab…" value={search} onChange={e => setSearch(e.target.value)} className="!w-72" />
        <Select value={statusFilter} onChange={e => setStatusFilter(e.target.value)} className="!w-auto">
          <option value="All">All statuses</option>
          {STONE_REMNANT_STATUSES.map(s => <option key={s}>{s}</option>)}
        </Select>
        <span className="text-xs text-[var(--leon-black)]/45 ml-2">
          {filtered.length} of {remnants.length} · {stoneFmtArea(filtered.reduce((n, r) => n + stoneRemnantArea(r), 0))}
        </span>
      </div>

      <p className="text-[11px] text-[var(--leon-black)]/50 mb-2">
        A remnant is created from the slab it came off — open a slab under All Slabs and use <b>Remnant</b>. That is what keeps its batch, lot and cost basis honest.
      </p>

      {!filtered.length ? <EmptyState text={remnants.length ? 'No remnant matches those filters.' : 'No remnants recorded yet.'} /> : (
        <div className="overflow-x-auto bg-white border border-[var(--leon-line)] rounded-xl">
          <table className="w-full text-xs">
            <thead className="bg-[var(--leon-cream)]">
              <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                <th className="px-3 py-2">Remnant</th><th className="px-3 py-2">From slab</th><th className="px-3 py-2">Material / colour</th>
                <th className="px-3 py-2">Batch · Lot</th><th className="px-3 py-2">Shape</th><th className="px-3 py-2">Bounding size</th>
                <th className="px-3 py-2">Usable area</th><th className="px-3 py-2">Where</th><th className="px-3 py-2">Job</th>
                {ctx.canSeeFin && <th className="px-3 py-2">Cost basis</th>}
                <th className="px-3 py-2">Status</th><th className="px-3 py-2"></th>
              </tr>
            </thead>
            <tbody>
              {filtered.map(r => (
                <tr key={r.id} className="border-t border-[var(--leon-line)]">
                  <td className="px-3 py-2 font-semibold cursor-pointer hover:underline" onClick={() => setDetailId(r.id)}>{r.remnantId}</td>
                  <td className="px-3 py-2">{r.parentSlabRef || '—'}</td>
                  <td className="px-3 py-2">{r.material}{r.colour ? <div className="text-[var(--leon-black)]/45">{r.colour}</div> : null}</td>
                  <td className="px-3 py-2">{r.batch || '—'}{r.lot ? ` · ${r.lot}` : ''}</td>
                  <td className="px-3 py-2">{r.shape}</td>
                  <td className="px-3 py-2 whitespace-nowrap">{fmtDim(r.lengthMm, 'Metric')} × {fmtDim(r.widthMm, 'Metric')}<div className="text-[var(--leon-black)]/45">{fmtDim(r.thicknessMm, 'Metric')} thick</div></td>
                  <td className="px-3 py-2">{stoneFmtAreaShort(stoneRemnantArea(r))}</td>
                  <td className="px-3 py-2">{stoneWarehouseName(ctx, r.warehouseId)}{r.rack ? <div className="text-[var(--leon-black)]/45">Rack {r.rack}</div> : null}</td>
                  <td className="px-3 py-2">{r.projectId ? stoneProjectName(ctx, r.projectId) : <span className="text-[var(--leon-black)]/30">—</span>}</td>
                  {ctx.canSeeFin && <td className="px-3 py-2">{fmtMoney(r.costBasis)}</td>}
                  <td className="px-3 py-2"><StoneRemnantStatusBadge status={r.status} /></td>
                  <td className="px-3 py-2">
                    {editable && (
                      <Select value={r.status} onChange={e => setStatus(r.id, e.target.value)} className="!w-auto !py-1 !text-[11px]">
                        {STONE_REMNANT_STATUSES.map(s => <option key={s}>{s}</option>)}
                      </Select>
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      <Modal open={!!detail} onClose={() => setDetailId(null)} wide title={detail ? `Remnant — ${detail.remnantId}` : ''}
        footer={<Button variant="ghost" onClick={() => setDetailId(null)}>Close</Button>}>
        {detail && (
          <div className="space-y-3">
            <div className="flex flex-wrap gap-4">
              {detail.photoUrl
                ? <img src={detail.photoUrl} alt="" className="rounded border border-[var(--leon-line)] object-cover" style={{ width: 220 }} />
                : <StoneRemnantShape remnant={detail} width={220} />}
              <div className="flex-1 min-w-[220px] text-xs space-y-1.5">
                <div className="flex items-center gap-2"><StoneRemnantStatusBadge status={detail.status} /><StoneVeinArrow direction={detail.veinDirection} size={18} /></div>
                <p><span className="text-[var(--leon-black)]/45">Parent slab</span> <b>{detail.parentSlabRef || '—'}</b></p>
                <p><span className="text-[var(--leon-black)]/45">Material · colour</span> <b>{[detail.material, detail.colour].filter(Boolean).join(' · ')}</b></p>
                <p><span className="text-[var(--leon-black)]/45">Batch · lot</span> <b>{detail.batch || '—'}{detail.lot ? ` · ${detail.lot}` : ''}</b></p>
                <p><span className="text-[var(--leon-black)]/45">Bounding box</span> <b>{fmtDim(detail.lengthMm, 'Metric')} × {fmtDim(detail.widthMm, 'Metric')}</b></p>
                <p><span className="text-[var(--leon-black)]/45">Usable area</span> <b>{stoneFmtArea(stoneRemnantArea(detail))}</b></p>
                <p><span className="text-[var(--leon-black)]/45">Where</span> <b>{stoneWarehouseName(ctx, detail.warehouseId)}{detail.rack ? ` · rack ${detail.rack}` : ''}</b></p>
                {ctx.canSeeFin && <p><span className="text-[var(--leon-black)]/45">Cost basis</span> <b>{fmtMoney(detail.costBasis)}</b> <span className="text-[var(--leon-black)]/40">pro-rated by area from the parent slab</span></p>}
                {detail.notes ? <p className="text-[var(--leon-black)]/60">{detail.notes}</p> : null}
              </div>
            </div>
            <div>
              <p className="text-xs font-semibold mb-1">History</p>
              <StoneHistoryList history={detail.history} />
            </div>
          </div>
        )}
      </Modal>
    </div>
  );
}

// ── Allocation ──────────────────────────────────────────────────────────────

function StoneAllocation({ ctx, editable }) {
  const [projectId, setProjectId] = useState('');
  const [yieldPct, setYieldPct] = useState(Math.round(STONE_DEFAULT_YIELD * 100));
  const [allocating, setAllocating] = useState(null);

  const projects = stonePickerProjects(ctx);
  const project = stoneProject(ctx, projectId);
  const slabs = ctx.slabs || [];
  const remnants = ctx.remnants || [];

  const onJob = project ? slabs.filter(s => s.projectId === project.id) : [];
  const remnantsOnJob = project ? remnants.filter(r => r.projectId === project.id) : [];

  // Materials this job touches: whatever the cut list calls for, plus anything
  // already put aside for it.
  const materials = useMemo(() => {
    if (!project) return [];
    const set = {};
    stoneProjectPieces(project).forEach(p => { if (p.material) set[p.material] = true; });
    onJob.forEach(s => { if (s.material) set[s.material] = true; });
    return Object.keys(set).sort();
  }, [project, ctx.slabs]);

  function release(slab) {
    ctx.setSlabs(prev => prev.map(s => {
      if (s.id !== slab.id) return s;
      const copy = Object.assign({}, s, { status: 'Available', projectId: null, scopeId: null });
      stonePushHistory(copy, ctx.currentUserName, `Released from ${stoneProjectName(ctx, s.projectId)} back to available stock`);
      return copy;
    }));
  }

  return (
    <div className="space-y-4">
      <div className="flex flex-wrap items-end gap-3">
        <Field label="Project" className="min-w-[260px]">
          <Select value={projectId} onChange={e => setProjectId(e.target.value)}>
            <option value="">Select a project…</option>
            {projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
        </Field>
        <Field label="Assumed cutting yield (%)" hint="How much of a slab actually becomes finished pieces">
          <TextInput type="number" min="10" max="100" value={yieldPct} onChange={e => setYieldPct(e.target.value)} className="!w-28" />
        </Field>
      </div>

      {!project ? <EmptyState text="Pick a project to see what it needs and what has been put aside for it." /> : (
        <>
          <Collapsible title="Material requirement" id="stone-alloc-req" defaultOpen count={materials.length}>
            {!materials.length ? (
              <EmptyState text="This project has no stone cut list and no slabs put aside. Add pieces under Cut List and the requirement appears here." />
            ) : (
              <>
                <div className="overflow-x-auto">
                  <table className="w-full text-xs">
                    <thead className="bg-[var(--leon-cream)]">
                      <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                        <th className="px-3 py-2">Material</th><th className="px-3 py-2">Piece area</th><th className="px-3 py-2">Slabs required</th>
                        <th className="px-3 py-2">Already allocated</th><th className="px-3 py-2">Available in warehouse</th><th className="px-3 py-2">Additional required</th>
                      </tr>
                    </thead>
                    <tbody>
                      {materials.map(m => {
                        const r = stoneMaterialRequirement(ctx, project, m, stoneNum(yieldPct) / 100);
                        return (
                          <tr key={m} className="border-t border-[var(--leon-line)]">
                            <td className="px-3 py-2 font-semibold">{m}</td>
                            <td className="px-3 py-2">{stoneFmtArea(r.pieceArea)}<div className="text-[var(--leon-black)]/45">{r.pieces.length} cut-list line{r.pieces.length === 1 ? '' : 's'}</div></td>
                            <td className="px-3 py-2 font-semibold">{r.requiredSlabs}
                              <div className="text-[var(--leon-black)]/45 font-normal">
                                {stoneFmtAreaShort(r.usablePerSlab)} usable per slab{r.basisFromStock ? '' : ' (nominal 3200 × 1600 — no slab of this material on record)'}
                              </div>
                            </td>
                            <td className="px-3 py-2">{r.allocatedSlabs}<div className="text-[var(--leon-black)]/45">{stoneFmtAreaShort(r.allocatedArea)}</div></td>
                            <td className="px-3 py-2">{r.availableSlabs}<div className="text-[var(--leon-black)]/45">{stoneFmtAreaShort(r.availableArea)}</div></td>
                            <td className="px-3 py-2">
                              {r.additionalRequired > 0
                                ? <Badge tone="red">{r.additionalRequired} to buy</Badge>
                                : <Badge tone="green">Covered</Badge>}
                            </td>
                          </tr>
                        );
                      })}
                    </tbody>
                  </table>
                </div>
                <p className="text-[11px] text-[var(--leon-black)]/50 mt-2">
                  Slabs required is an estimate: total piece area divided by the usable area of a slab at the yield above, rounded up.
                  It is not a nesting result — the real number depends on where the pieces actually fall on each slab, and there is no layout engine here.
                </p>
              </>
            )}
          </Collapsible>

          <Collapsible title="Slabs put aside for this job" id="stone-alloc-slabs" defaultOpen count={onJob.length}>
            {!onJob.length ? <EmptyState text="No slab is allocated to this project yet." /> : (
              <div className="overflow-x-auto">
                <table className="w-full text-xs">
                  <thead className="bg-[var(--leon-cream)]">
                    <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                      <th className="px-3 py-2 w-12"></th><th className="px-3 py-2">Slab</th><th className="px-3 py-2">Material / colour</th>
                      <th className="px-3 py-2">Batch · Lot</th><th className="px-3 py-2">Scope</th><th className="px-3 py-2">Area</th>
                      <th className="px-3 py-2">Status</th>{ctx.canSeeFin && <th className="px-3 py-2">Landed</th>}<th className="px-3 py-2"></th>
                    </tr>
                  </thead>
                  <tbody>
                    {onJob.map(s => (
                      <tr key={s.id} className="border-t border-[var(--leon-line)]">
                        <td className="px-3 py-1.5"><StoneThumb slab={s} size={32} /></td>
                        <td className="px-3 py-2 font-semibold">{s.slabId}</td>
                        <td className="px-3 py-2">{[s.material, s.colour].filter(Boolean).join(' · ')}</td>
                        <td className="px-3 py-2">{s.batch || '—'}{s.lot ? ` · ${s.lot}` : ''}</td>
                        <td className="px-3 py-2">{s.scopeId ? stoneScopeName(ctx, project.id, s.scopeId) : <span className="text-[var(--leon-black)]/30">Whole project</span>}</td>
                        <td className="px-3 py-2">{stoneFmtAreaShort(stoneSlabArea(s))}</td>
                        <td className="px-3 py-2"><StoneSlabStatusBadge status={s.status} /></td>
                        {ctx.canSeeFin && <td className="px-3 py-2">{fmtMoney(stoneLandedCost(s))}</td>}
                        <td className="px-3 py-2 whitespace-nowrap">
                          {editable && <Button size="sm" variant="ghost" onClick={() => setAllocating(s)}>Change</Button>}
                          {editable && <Button size="sm" variant="ghost" onClick={() => release(s)}>Release</Button>}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            )}
            {remnantsOnJob.length ? (
              <div className="mt-3">
                <p className="text-xs font-semibold mb-1">Remnants put aside ({remnantsOnJob.length})</p>
                {remnantsOnJob.map(r => (
                  <p key={r.id} className="text-xs">
                    <b>{r.remnantId}</b> — {[r.material, r.colour].filter(Boolean).join(' · ')} · {fmtDim(r.lengthMm, 'Metric')} × {fmtDim(r.widthMm, 'Metric')} · <StoneRemnantStatusBadge status={r.status} />
                  </p>
                ))}
              </div>
            ) : null}
          </Collapsible>

          <Collapsible title="Allocate more stock to this job" id="stone-alloc-pool">
            <StoneAvailablePool ctx={ctx} project={project} editable={editable} onAllocate={setAllocating} />
          </Collapsible>

          <Collapsible title="Check the remnants first" id="stone-alloc-remnants">
            <StoneRemnantSearchPanel ctx={ctx} editable={editable} />
          </Collapsible>
        </>
      )}

      <StoneAllocateModal ctx={ctx} slab={allocating} onClose={() => setAllocating(null)} />
    </div>
  );
}

function StoneAvailablePool({ ctx, project, editable, onAllocate }) {
  const [material, setMaterial] = useState('All');
  const slabs = (ctx.slabs || []).filter(s => s.status === 'Available' && !s.projectId);
  const materials = ['All'].concat(Array.from(new Set(slabs.map(s => s.material).filter(Boolean))).sort());
  const list = slabs.filter(s => material === 'All' || s.material === material);
  return (
    <div>
      <div className="flex items-end gap-2 mb-2">
        <Select value={material} onChange={e => setMaterial(e.target.value)} className="!w-auto">
          {materials.map(m => <option key={m} value={m}>{m === 'All' ? 'All materials' : m}</option>)}
        </Select>
        <span className="text-xs text-[var(--leon-black)]/45">{list.length} unallocated slab{list.length === 1 ? '' : 's'} available</span>
      </div>
      {!list.length ? <EmptyState text="Nothing unallocated in stock." /> : (
        <div className="overflow-x-auto">
          <table className="w-full text-xs">
            <thead className="bg-[var(--leon-cream)]">
              <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                <th className="px-3 py-2 w-12"></th><th className="px-3 py-2">Slab</th><th className="px-3 py-2">Material / colour</th>
                <th className="px-3 py-2">Batch · Lot</th><th className="px-3 py-2">Size</th><th className="px-3 py-2">Vein</th>
                <th className="px-3 py-2">Where</th><th className="px-3 py-2"></th>
              </tr>
            </thead>
            <tbody>
              {list.map(s => (
                <tr key={s.id} className="border-t border-[var(--leon-line)]">
                  <td className="px-3 py-1.5"><StoneThumb slab={s} size={32} /></td>
                  <td className="px-3 py-2 font-semibold">{s.slabId}</td>
                  <td className="px-3 py-2">{[s.material, s.colour].filter(Boolean).join(' · ')}</td>
                  <td className="px-3 py-2">{s.batch || '—'}{s.lot ? ` · ${s.lot}` : ''}</td>
                  <td className="px-3 py-2 whitespace-nowrap">{fmtDim(s.lengthMm, 'Metric')} × {fmtDim(s.widthMm, 'Metric')}</td>
                  <td className="px-3 py-2"><StoneVeinArrow direction={s.veinDirection} /></td>
                  <td className="px-3 py-2">{stoneWarehouseName(ctx, s.warehouseId)}{s.rack ? ` · ${s.rack}` : ''}</td>
                  <td className="px-3 py-2">{editable && <Button size="sm" variant="ghost" onClick={() => onAllocate(s)}>Allocate</Button>}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

// ── Costing ─────────────────────────────────────────────────────────────────

function StoneCosting({ ctx }) {
  const [materialFilter, setMaterialFilter] = useState('All');
  if (!ctx.canSeeFin) {
    return <LockedNotice label="Slab costing is financial information and is restricted to the roles that can see job costs." />;
  }
  const slabs = ctx.slabs || [];
  const remnants = ctx.remnants || [];
  const materials = ['All'].concat(Array.from(new Set(slabs.map(s => s.material).filter(Boolean))).sort());
  const list = slabs.filter(s => materialFilter === 'All' || s.material === materialFilter);

  const totals = list.reduce((acc, s) => {
    acc.unit += stoneNum(s.unitCost); acc.freight += stoneNum(s.freightCost);
    acc.tariff += stoneNum(s.tariffCost); acc.handling += stoneNum(s.handlingCost);
    acc.landed += stoneLandedCost(s); acc.area += stoneSlabArea(s);
    return acc;
  }, { unit: 0, freight: 0, tariff: 0, handling: 0, landed: 0, area: 0 });

  // The variance worth showing here is how far landed cost sits above the
  // invoice price. That uplift is what gets forgotten when a slab is quoted off
  // the supplier's sheet.
  const uplift = totals.unit > 0 ? ((totals.landed - totals.unit) / totals.unit) * 100 : null;

  return (
    <div className="space-y-4">
      <div className="flex items-end gap-2">
        <Select value={materialFilter} onChange={e => setMaterialFilter(e.target.value)} className="!w-auto">
          {materials.map(m => <option key={m} value={m}>{m === 'All' ? 'All materials' : m}</option>)}
        </Select>
        <span className="text-xs text-[var(--leon-black)]/45">{list.length} slab{list.length === 1 ? '' : 's'}</span>
      </div>

      <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
        <StoneKpi label="Invoice cost" value={fmtMoney(totals.unit)} sub="What the supplier billed" />
        <StoneKpi label="Freight + tariff + handling" value={fmtMoney(totals.freight + totals.tariff + totals.handling)} sub="What it cost to land it" />
        <StoneKpi label="Landed cost" value={fmtMoney(totals.landed)} tone="brown" sub={totals.area > 0 ? `${fmtMoney(totals.landed / stoneSqft(totals.area))} per ft² · ${fmtMoney(totals.landed / totals.area)} per m²` : ''} />
        <StoneKpi label="Landed vs invoice" value={uplift === null ? '—' : `+${uplift.toFixed(1)}%`} sub="The uplift a quote off the supplier's sheet misses" />
      </div>

      <Collapsible title="Cost per slab" id="stone-cost-slabs" defaultOpen count={list.length}>
        {!list.length ? <EmptyState text="No slabs to cost." /> : (
          <div className="overflow-x-auto">
            <table className="w-full text-xs">
              <thead className="bg-[var(--leon-cream)]">
                <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                  <th className="px-3 py-2">Slab</th><th className="px-3 py-2">Material / colour</th><th className="px-3 py-2">Area</th>
                  <th className="px-3 py-2">Unit</th><th className="px-3 py-2">Freight</th><th className="px-3 py-2">Tariff</th>
                  <th className="px-3 py-2">Handling</th><th className="px-3 py-2">Landed</th>
                  <th className="px-3 py-2">$/ft²</th><th className="px-3 py-2">$/m²</th><th className="px-3 py-2">Job</th>
                </tr>
              </thead>
              <tbody>
                {list.map(s => (
                  <tr key={s.id} className="border-t border-[var(--leon-line)]">
                    <td className="px-3 py-2 font-semibold">{s.slabId}</td>
                    <td className="px-3 py-2">{[s.material, s.colour].filter(Boolean).join(' · ')}</td>
                    <td className="px-3 py-2">{stoneFmtAreaShort(stoneSlabArea(s))}</td>
                    <td className="px-3 py-2">{fmtMoney(s.unitCost)}</td>
                    <td className="px-3 py-2">{fmtMoney(s.freightCost)}</td>
                    <td className="px-3 py-2">{fmtMoney(s.tariffCost)}</td>
                    <td className="px-3 py-2">{fmtMoney(s.handlingCost)}</td>
                    <td className="px-3 py-2 font-semibold">{fmtMoney(stoneLandedCost(s))}</td>
                    <td className="px-3 py-2">{stoneCostPerSqft(s) === null ? '—' : fmtMoney(stoneCostPerSqft(s))}</td>
                    <td className="px-3 py-2">{stoneCostPerM2(s) === null ? '—' : fmtMoney(stoneCostPerM2(s))}</td>
                    <td className="px-3 py-2">{s.projectId ? stoneProjectName(ctx, s.projectId) : <span className="text-[var(--leon-black)]/30">—</span>}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
        <p className="text-[11px] text-[var(--leon-black)]/50 mt-2">
          Landed cost is unit + freight + tariff + handling, recomputed from those four every time it is shown — never read back from a stored total that could have drifted.
        </p>
      </Collapsible>

      <Collapsible title="Value held in remnants" id="stone-cost-remnants" count={remnants.length}>
        {!remnants.length ? <EmptyState text="No remnants recorded." /> : (
          <div className="overflow-x-auto">
            <table className="w-full text-xs">
              <thead className="bg-[var(--leon-cream)]">
                <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                  <th className="px-3 py-2">Remnant</th><th className="px-3 py-2">From</th><th className="px-3 py-2">Material</th>
                  <th className="px-3 py-2">Area</th><th className="px-3 py-2">Cost basis</th><th className="px-3 py-2">$/ft²</th><th className="px-3 py-2">Status</th>
                </tr>
              </thead>
              <tbody>
                {remnants.map(r => {
                  const a = stoneRemnantArea(r);
                  return (
                    <tr key={r.id} className="border-t border-[var(--leon-line)]">
                      <td className="px-3 py-2 font-semibold">{r.remnantId}</td>
                      <td className="px-3 py-2">{r.parentSlabRef || '—'}</td>
                      <td className="px-3 py-2">{[r.material, r.colour].filter(Boolean).join(' · ')}</td>
                      <td className="px-3 py-2">{stoneFmtAreaShort(a)}</td>
                      <td className="px-3 py-2">{fmtMoney(r.costBasis)}</td>
                      <td className="px-3 py-2">{a > 0 ? fmtMoney(stoneNum(r.costBasis) / stoneSqft(a)) : '—'}</td>
                      <td className="px-3 py-2"><StoneRemnantStatusBadge status={r.status} /></td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
        <p className="text-[11px] text-[var(--leon-black)]/50 mt-2">
          A remnant's cost basis is its share of the parent slab's landed cost, by area. Using it on the next job is what turns that number from a write-off into recovered material.
        </p>
      </Collapsible>
    </div>
  );
}

// ── Cut List ────────────────────────────────────────────────────────────────

function StoneCutList({ ctx, editable }) {
  const [projectId, setProjectId] = useState('');
  const [adding, setAdding] = useState(false);
  const [editingPiece, setEditingPiece] = useState(null);

  const projects = stonePickerProjects(ctx);
  const project = stoneProject(ctx, projectId);
  // The cut list writes onto the project record. If the host app has not wired
  // updateProject onto ctx, say so instead of throwing on the first click.
  const canWrite = editable && typeof ctx.updateProject === 'function';

  const pieces = stoneProjectPieces(project);
  const scopes = project ? (project.scopes || []) : [];

  function mutatePieces(fn, logLine) {
    if (typeof ctx.updateProject !== 'function') return;
    ctx.updateProject(project.id, draft => {
      draft.stoneCutList = fn(draft.stoneCutList || []);
      // ctx.logAction, never a bare logAction — that one is declared inside
      // App() and is invisible here, so calling it throws inside the draft
      // callback and the whole mutation is lost without anything on screen.
      if (typeof ctx.logAction === 'function') ctx.logAction(draft, logLine);
    });
  }
  function addPiece(data) { mutatePieces(list => list.concat([stoneMakePiece(data, ctx.currentUserName)]), `LEON Stone — cut list piece added: ${data.label || 'piece'}`); }
  function updatePiece(id, fields, line) { mutatePieces(list => list.map(p => (p.id === id ? Object.assign({}, p, fields) : p)), line); }
  function removePiece(p) { mutatePieces(list => list.filter(x => x.id !== p.id), `LEON Stone — cut list piece removed: ${p.label || 'piece'}`); }

  const byScope = {};
  pieces.forEach(p => {
    const k = p.scopeId || '__none';
    if (!byScope[k]) byScope[k] = [];
    byScope[k].push(p);
  });

  const totalArea = pieces.reduce((n, p) => n + stonePieceArea(p), 0);
  const assigned = pieces.filter(p => p.slabId || p.remnantId).length;

  const slabPool = project ? (ctx.slabs || []).filter(s => !s.projectId || s.projectId === project.id) : [];
  const remnantPool = project ? (ctx.remnants || []).filter(r => !r.projectId || r.projectId === project.id) : [];

  return (
    <div className="space-y-4">
      <div className="rounded-lg border border-dashed border-[var(--leon-line)] bg-[var(--leon-cream)] px-4 py-3 text-xs text-[var(--leon-black)]/70">
        This tab records the pieces a job needs and which slab or remnant each one comes out of, so the yield and waste figures have something real behind them.
        To place those pieces on a slab by hand — against its photograph, with the grain, kerf and offcuts checked — open <b>Slab Layout</b>.
        <b> There is still no auto-nesting optimiser</b> anywhere in this module, and none is faked: where a piece falls is a fabricator's judgement, so a person puts it there.
      </div>

      <div className="flex flex-wrap items-end gap-3">
        <Field label="Project" className="min-w-[260px]">
          <Select value={projectId} onChange={e => setProjectId(e.target.value)}>
            <option value="">Select a project…</option>
            {projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
        </Field>
        <span className="ml-auto" />
        {project && canWrite && <Button onClick={() => setAdding(true)}>+ Add piece</Button>}
      </div>

      {!project ? <EmptyState text="Pick a project to see or build its cut list." /> : (
        <>
          {editable && !canWrite && (
            <LockedNotice label="The cut list is saved onto the project record, and this build has not exposed project editing to the software modules — so it is read-only here." />
          )}

          <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
            <StoneKpi label="Pieces" value={pieces.reduce((n, p) => n + Math.max(1, stoneNum(p.qty) || 1), 0)} sub={`${pieces.length} line${pieces.length === 1 ? '' : 's'}`} />
            <StoneKpi label="Finished area" value={stoneFmtAreaShort(totalArea)} sub={`${stoneSqft(totalArea).toFixed(0)} ft²`} />
            <StoneKpi label="Assigned to stock" value={`${assigned} / ${pieces.length}`} sub="Pieces with a slab or remnant against them" />
            <StoneKpi label="Cut" value={pieces.filter(p => p.status === 'Cut' || p.status === 'Installed').length} sub="Recorded as cut or installed" />
          </div>

          {!pieces.length ? <EmptyState text="No pieces on this project's cut list yet." /> : (
            Object.keys(byScope).map(k => {
              const scope = scopes.find(s => s.id === k);
              const rows = byScope[k];
              return (
                <Collapsible key={k} title={scope ? scope.name : 'No scope assigned'} id={`stone-cut-${k}`} defaultOpen count={rows.length}>
                  <div className="overflow-x-auto">
                    <table className="w-full text-xs">
                      <thead className="bg-[var(--leon-cream)]">
                        <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                          <th className="px-3 py-2">Piece</th><th className="px-3 py-2">Material</th><th className="px-3 py-2">Size</th>
                          <th className="px-3 py-2">Qty</th><th className="px-3 py-2">Area</th><th className="px-3 py-2">Edge</th>
                          <th className="px-3 py-2">Cut from</th><th className="px-3 py-2">Status</th><th className="px-3 py-2"></th>
                        </tr>
                      </thead>
                      <tbody>
                        {rows.map(p => {
                          const slab = (ctx.slabs || []).find(s => s.id === p.slabId);
                          const remnant = (ctx.remnants || []).find(r => r.id === p.remnantId);
                          return (
                            <tr key={p.id} className="border-t border-[var(--leon-line)]">
                              <td className="px-3 py-2 font-semibold">{p.label || 'Piece'}{p.notes ? <div className="text-[var(--leon-black)]/45 font-normal">{p.notes}</div> : null}</td>
                              <td className="px-3 py-2">{p.material || '—'}<div className="text-[var(--leon-black)]/45">{p.thicknessMm ? fmtDim(p.thicknessMm, 'Metric') : ''}</div></td>
                              <td className="px-3 py-2 whitespace-nowrap">{fmtDim(p.lengthMm, 'Metric')} × {fmtDim(p.widthMm, 'Metric')}</td>
                              <td className="px-3 py-2">{p.qty}</td>
                              <td className="px-3 py-2">{stoneFmtAreaShort(stonePieceArea(p))}</td>
                              <td className="px-3 py-2">{p.edgeProfile || '—'}</td>
                              <td className="px-3 py-2">
                                {canWrite ? (
                                  <Select
                                    value={p.slabId ? `slab:${p.slabId}` : p.remnantId ? `rmn:${p.remnantId}` : ''}
                                    onChange={e => {
                                      const v = e.target.value;
                                      const fields = v.indexOf('slab:') === 0
                                        ? { slabId: v.slice(5), remnantId: null, status: p.status === 'Planned' ? 'Assigned' : p.status }
                                        : v.indexOf('rmn:') === 0
                                          ? { slabId: null, remnantId: v.slice(4), status: p.status === 'Planned' ? 'Assigned' : p.status }
                                          : { slabId: null, remnantId: null, status: 'Planned' };
                                      updatePiece(p.id, fields, `LEON Stone — "${p.label || 'piece'}" cut source set to ${v || 'unassigned'}`);
                                    }}
                                    className="!w-auto !py-1 !text-[11px]">
                                    <option value="">Not assigned</option>
                                    <optgroup label="Remnants">
                                      {remnantPool.filter(r => !p.material || r.material === p.material).map(r => (
                                        <option key={r.id} value={`rmn:${r.id}`}>{r.remnantId} — {fmtDim(r.lengthMm, 'Metric')} × {fmtDim(r.widthMm, 'Metric')}</option>
                                      ))}
                                    </optgroup>
                                    <optgroup label="Slabs">
                                      {slabPool.filter(s => !p.material || s.material === p.material).map(s => (
                                        <option key={s.id} value={`slab:${s.id}`}>{s.slabId} — {s.colour || s.material}</option>
                                      ))}
                                    </optgroup>
                                  </Select>
                                ) : (
                                  <span>{slab ? slab.slabId : remnant ? remnant.remnantId : '—'}</span>
                                )}
                              </td>
                              <td className="px-3 py-2">
                                {canWrite ? (
                                  <Select value={p.status} onChange={e => updatePiece(p.id, { status: e.target.value }, `LEON Stone — "${p.label || 'piece'}" moved to ${e.target.value}`)} className="!w-auto !py-1 !text-[11px]">
                                    {STONE_PIECE_STATUSES.map(s => <option key={s}>{s}</option>)}
                                  </Select>
                                ) : <Badge tone={p.status === 'Installed' ? 'green' : p.status === 'Cut' ? 'blue' : p.status === 'Assigned' ? 'yellow' : 'neutral'}>{p.status}</Badge>}
                              </td>
                              <td className="px-3 py-2 whitespace-nowrap">
                                {canWrite && <Button size="sm" variant="ghost" onClick={() => setEditingPiece(p)}>Edit</Button>}
                                {canWrite && <Button size="sm" variant="ghost" onClick={() => removePiece(p)}>Remove</Button>}
                              </td>
                            </tr>
                          );
                        })}
                      </tbody>
                    </table>
                  </div>
                </Collapsible>
              );
            })
          )}

          <Collapsible title="Find a remnant for a piece on this list" id="stone-cut-search">
            <StoneRemnantSearchPanel ctx={ctx} editable={editable} />
          </Collapsible>
        </>
      )}

      <StonePieceModal ctx={ctx} open={adding || !!editingPiece} piece={editingPiece} scopes={scopes}
        onClose={() => { setAdding(false); setEditingPiece(null); }}
        onSave={(data) => {
          if (editingPiece) {
            updatePiece(editingPiece.id, {
              scopeId: data.scopeId || null, label: data.label, material: data.material,
              thicknessMm: stoneNum(data.thicknessMm), lengthMm: stoneNum(data.lengthMm), widthMm: stoneNum(data.widthMm),
              qty: Math.max(1, stoneNum(data.qty) || 1), edgeProfile: data.edgeProfile,
              veinDirection: data.veinDirection || '', notes: data.notes,
            }, `LEON Stone — cut list piece edited: ${data.label || 'piece'}`);
          } else {
            addPiece(data);
          }
          setAdding(false); setEditingPiece(null);
        }} />
    </div>
  );
}

function StonePieceModal({ ctx, open, piece, scopes, onClose, onSave }) {
  const blank = { scopeId: '', label: '', material: '', thicknessMm: '', lengthMm: '', widthMm: '', qty: 1, edgeProfile: '', veinDirection: '', notes: '' };
  const [form, setForm] = useState(blank);
  useEffect(() => {
    if (!open) return;
    setForm(piece ? {
      scopeId: piece.scopeId || '', label: piece.label || '', material: piece.material || '',
      thicknessMm: piece.thicknessMm || '', lengthMm: piece.lengthMm || '', widthMm: piece.widthMm || '',
      qty: piece.qty || 1, edgeProfile: piece.edgeProfile || '', veinDirection: piece.veinDirection || '', notes: piece.notes || '',
    } : blank);
  }, [open, piece]);
  const set = (k, v) => setForm(f => Object.assign({}, f, { [k]: v }));
  const valid = stoneNum(form.lengthMm) > 0 && stoneNum(form.widthMm) > 0 && !!form.material;
  const materials = Array.from(new Set((ctx.slabs || []).map(s => s.material).filter(Boolean))).sort();

  return (
    <Modal open={open} onClose={onClose} wide title={piece ? 'Edit piece' : 'Add a piece to the cut list'}
      footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button onClick={() => onSave(form)} disabled={!valid}>{piece ? 'Save' : 'Add'}</Button></>}>
      <div className="space-y-3">
        <div className="grid grid-cols-2 gap-3">
          <Field label="Piece"><TextInput value={form.label} onChange={e => set('label', e.target.value)} placeholder="Island top, sink run, backsplash…" /></Field>
          <Field label="Scope">
            <Select value={form.scopeId} onChange={e => set('scopeId', e.target.value)}>
              <option value="">No scope</option>
              {scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
            </Select>
          </Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Material">
            <Select value={form.material} onChange={e => set('material', e.target.value)}>
              <option value="">Select…</option>
              {Array.from(new Set(materials.concat(STONE_MATERIALS))).map(m => <option key={m}>{m}</option>)}
            </Select>
          </Field>
          <Field label="Thickness (mm)"><TextInput type="number" value={form.thicknessMm} onChange={e => set('thicknessMm', e.target.value)} /></Field>
          <Field label="Quantity"><TextInput type="number" min="1" value={form.qty} onChange={e => set('qty', e.target.value)} /></Field>
        </div>
        <div className="grid grid-cols-3 gap-3">
          <Field label="Length (mm)"><TextInput type="number" value={form.lengthMm} onChange={e => set('lengthMm', e.target.value)} /></Field>
          <Field label="Width (mm)"><TextInput type="number" value={form.widthMm} onChange={e => set('widthMm', e.target.value)} /></Field>
          <Field label="Edge profile">
            <Select value={form.edgeProfile} onChange={e => set('edgeProfile', e.target.value)}>
              <option value="">—</option>
              {STONE_EDGE_PROFILES.map(x => <option key={x}>{x}</option>)}
            </Select>
          </Field>
        </div>
        <Field label="Grain required on this piece" hint="Which way the veining must run once this piece is finished. The Slab Layout canvas warns when a piece is rotated against it — that is what makes it a requirement rather than a note.">
          <Select value={form.veinDirection} onChange={e => set('veinDirection', e.target.value)} className="!w-auto">
            {STONE_PIECE_VEIN_OPTIONS.map(v => <option key={v || 'none'} value={v}>{v || 'No requirement'}</option>)}
          </Select>
        </Field>
        <p className="text-[11px] text-[var(--leon-black)]/50 -mt-1">
          Finished size <b>{stoneFmtArea(stoneAreaM2(form.lengthMm, form.widthMm) * Math.max(1, stoneNum(form.qty) || 1))}</b> for {Math.max(1, stoneNum(form.qty) || 1)} piece{Math.max(1, stoneNum(form.qty) || 1) === 1 ? '' : 's'}. Cutting allowance is added when a remnant is searched, not stored here.
        </p>
        <Field label="Notes"><TextArea rows={2} value={form.notes} onChange={e => set('notes', e.target.value)} /></Field>
      </div>
    </Modal>
  );
}

// ── Slab layout canvas ──────────────────────────────────────────────────────
// Where a piece falls on a slab is a fabricator's judgement about veining,
// defects and grain — so this is a HAND placement surface, not an optimiser.
// Pieces are dragged, rotated and nudged by a person looking at the slab's own
// photograph, which is the whole reason the photograph is stored to scale.
//
// Nothing new is invented to hold a layout: it is saved ON the slab (or the
// remnant) it belongs to, the pieces ARE the project's existing cut-list
// records, and an offcut becomes a remnant through stoneMakeRemnant — the same
// path the Create-a-remnant modal uses. There is no second set of records here.

// Clearance kept between a piece and the edge of the usable area. Separate from
// kerf, which is the gap between two pieces: the saw needs somewhere to run at
// the perimeter too, and an edge that has been trimmed is not the same thing.
const STONE_LAYOUT_EDGE_CLEARANCE_MM = 10;
// The smallest offcut worth racking as a remnant. A default, not a rule — it is
// an input, because every shop draws that line somewhere different.
const STONE_LAYOUT_MIN_REMNANT_MM = 300;
// The grid the free-area measurement rasterises onto. 25 mm is finer than any
// offcut anyone would keep, and coarse enough to stay instant while dragging.
const STONE_LAYOUT_GRID_MM = 25;
const STONE_LAYOUT_NUDGE_MM = 1;
const STONE_LAYOUT_NUDGE_COARSE_MM = 10;
// Vein directions that resolve to an ANGLE. 'Random', 'Custom' and
// 'Directionless' deliberately have none — see stoneVeinVerdict.
const STONE_VEIN_ANGLE = { Horizontal: 0, Vertical: 90, Diagonal: 45 };
const STONE_VEIN_TOLERANCE_DEG = 5;

// ---- Geometry: oriented rectangles -----------------------------------------
// A placed piece is a rectangle that may sit at any angle, so every check here
// is an oriented-box check. Axis-aligned maths would silently pass a rotated
// piece that overlaps its neighbour.
function stoneDeg2Rad(d) { return (stoneNum(d) * Math.PI) / 180; }
function stoneNormAngle180(a) { let x = stoneNum(a) % 180; if (x < 0) x += 180; return x; }
function stoneNormAngle360(a) { let x = stoneNum(a) % 360; if (x < 0) x += 360; return x; }

function stoneObb(cx, cy, w, h, rotDeg) {
  return { cx: stoneNum(cx), cy: stoneNum(cy), w: stoneNum(w), h: stoneNum(h), rot: stoneNum(rotDeg) };
}
function stoneObbInflate(o, m) {
  return { cx: o.cx, cy: o.cy, w: o.w + 2 * stoneNum(m), h: o.h + 2 * stoneNum(m), rot: o.rot };
}
function stoneObbCorners(o) {
  const c = Math.cos(stoneDeg2Rad(o.rot)), s = Math.sin(stoneDeg2Rad(o.rot));
  const hw = o.w / 2, hh = o.h / 2;
  return [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]].map(p => ({
    x: o.cx + p[0] * c - p[1] * s,
    y: o.cy + p[0] * s + p[1] * c,
  }));
}
function stoneProjectOnAxis(corners, ax, ay) {
  let min = Infinity, max = -Infinity;
  corners.forEach(p => {
    const v = p.x * ax + p.y * ay;
    if (v < min) min = v;
    if (v > max) max = v;
  });
  return { min, max };
}
// Separating-axis test. Two rectangles miss each other only if some edge normal
// of one of them separates them; four axes settle it for two boxes.
function stoneObbOverlap(a, b) {
  const ca = stoneObbCorners(a), cb = stoneObbCorners(b);
  const angles = [a.rot, a.rot + 90, b.rot, b.rot + 90];
  for (let i = 0; i < angles.length; i++) {
    const r = stoneDeg2Rad(angles[i]);
    const ax = Math.cos(r), ay = Math.sin(r);
    const pa = stoneProjectOnAxis(ca, ax, ay), pb = stoneProjectOnAxis(cb, ax, ay);
    if (pa.max <= pb.min + 1e-6 || pb.max <= pa.min + 1e-6) return false;
  }
  return true;
}
function stonePointInObb(px, py, o) {
  const r = stoneDeg2Rad(-o.rot);
  const dx = px - o.cx, dy = py - o.cy;
  const lx = dx * Math.cos(r) - dy * Math.sin(r);
  const ly = dx * Math.sin(r) + dy * Math.cos(r);
  return Math.abs(lx) <= o.w / 2 + 1e-6 && Math.abs(ly) <= o.h / 2 + 1e-6;
}

// ---- Vein direction --------------------------------------------------------
// The piece states the grain it needs; the slab states the grain it has. What
// the veining does ON A PIECE is the slab's angle minus however far the piece
// has been turned — which is exactly why rotating a piece can break it.
function stoneVeinVerdict(surfaceVein, pieceVein, rotationDeg) {
  const need = pieceVein || '';
  if (!need || need === 'Directionless') return { state: 'none', text: 'This piece does not require a grain direction.' };
  if (!surfaceVein || surfaceVein === 'Directionless') {
    return { state: 'none', text: 'This material has no grain to keep — the piece can sit any way up.' };
  }
  if (surfaceVein === 'Random') {
    // The module's own position (see STONE_VEIN_DIRECTIONS): random movement
    // cannot be rotated without the seam showing.
    const r = stoneNormAngle360(rotationDeg);
    const off = Math.min(r, 360 - r) > STONE_VEIN_TOLERANCE_DEG;
    return off
      ? { state: 'bad', text: 'Random movement — turning this piece will show at the seam. Lay it as the slab lies.' }
      : { state: 'ok', text: 'Laid square to the slab, as random movement requires.' };
  }
  if (surfaceVein === 'Custom' || need === 'Random' || need === 'Custom') {
    return { state: 'unknown', text: 'The veining here is described in words rather than an angle — check this piece by eye.' };
  }
  const resolved = stoneNormAngle180(stoneNum(STONE_VEIN_ANGLE[surfaceVein]) - stoneNum(rotationDeg));
  const want = stoneNum(STONE_VEIN_ANGLE[need]);
  let diff = Math.abs(resolved - want);
  if (diff > 90) diff = 180 - diff;
  if (diff <= STONE_VEIN_TOLERANCE_DEG) {
    return { state: 'ok', text: `Grain runs ${need.toLowerCase()} on this piece, as required.`, resolved };
  }
  return { state: 'bad', text: `This piece needs the grain ${need.toLowerCase()}; as rotated, the slab's veining crosses it at ${Math.round(resolved)}°.`, resolved };
}
// The first quarter-turn that does not break the grain, used as the starting
// angle when a piece is first dropped. It is a starting angle, nothing more.
function stoneVeinPreferredRotation(surfaceVein, pieceVein) {
  const tries = [0, 90, 180, 270];
  for (let i = 0; i < tries.length; i++) {
    if (stoneVeinVerdict(surfaceVein, pieceVein, tries[i]).state !== 'bad') return tries[i];
  }
  return 0;
}

// ---- The layout record -----------------------------------------------------
// A slab is cut once, so it carries ONE layout. Placements are stored, never a
// rendered picture: x and y are the piece's CENTRE in millimetres from the
// surface's top-left origin, and rotation is clockwise degrees about that
// centre. The corner reading the shop wants is derived from those on the way
// out (see the placement table) rather than stored twice.
function stoneMakeLayout(surfaceKind, surfaceId, projectId, createdBy) {
  return {
    id: uid('stlay'),
    surfaceKind, surfaceId,
    projectId: projectId || null,
    kerfMm: STONE_DEFAULT_KERF_MM,
    edgeClearanceMm: STONE_LAYOUT_EDGE_CLEARANCE_MM,
    // The slab record carries no trim field, so a margin entered here is the
    // one the layout was planned to. Zero means the drawn rectangle is the
    // nominal size straight off the slab record.
    trimMm: 0,
    minRemnantMm: STONE_LAYOUT_MIN_REMNANT_MM,
    placements: [],
    status: 'Draft',
    remnantIds: [],
    createdBy: createdBy || '', createdDate: todayISO(),
    updatedBy: createdBy || '', updatedDate: todayISO(),
  };
}
function stoneMakePlacement(piece, copy, xMm, yMm, rotationDeg) {
  return {
    id: uid('stpl'),
    pieceId: piece.id,
    copy: Math.max(0, stoneNum(copy)),
    xMm: Math.round(stoneNum(xMm)),
    yMm: Math.round(stoneNum(yMm)),
    rotationDeg: stoneNum(rotationDeg) || 0,
  };
}
// A slab may declare its own trimmed edge one day; until it does the layout's
// own figure is used, and the UI says which of the two it is showing.
function stoneLayoutTrimMm(record, layout) {
  const own = stoneNum(record && record.edgeTrimMm);
  return own > 0 ? own : Math.max(0, stoneNum(layout && layout.trimMm));
}
function stoneLayoutUsable(dims, trimMm) {
  const t = Math.max(0, stoneNum(trimMm));
  return {
    x0: t, y0: t,
    x1: Math.max(t, stoneNum(dims.lengthMm) - t),
    y1: Math.max(t, stoneNum(dims.widthMm) - t),
  };
}

// Everything the canvas and the figures need, recomputed from the placements
// on every change. Nothing derived is stored.
function stoneLayoutEvaluate(layout, pieces, dims, surfaceVein, record) {
  // The trim is resolved from the RECORD first and the layout second, exactly
  // as the panel reports it — the check and the caption must never disagree
  // about how big the usable area is.
  const usable = stoneLayoutUsable(dims, stoneLayoutTrimMm(record, layout));
  const kerf = Math.max(0, stoneNum(layout.kerfMm));
  const clear = Math.max(0, stoneNum(layout.edgeClearanceMm));
  const byId = {};
  (pieces || []).forEach(p => { byId[p.id] = p; });

  const items = (layout.placements || []).map(pl => {
    const piece = byId[pl.pieceId] || null;
    const w = piece ? stoneNum(piece.lengthMm) : 0;
    const h = piece ? stoneNum(piece.widthMm) : 0;
    return {
      placement: pl,
      piece,
      orphan: !piece,
      box: stoneObb(pl.xMm, pl.yMm, w, h, pl.rotationDeg),
      area: stoneAreaM2(w, h),
      vein: piece ? stoneVeinVerdict(surfaceVein, piece.veinDirection, pl.rotationDeg) : { state: 'none', text: '' },
      outOfBounds: false,
      overlaps: [],
    };
  });

  items.forEach(it => {
    if (it.orphan) return;
    // Edge clearance is part of the footprint for the bounds check: a piece
    // that reaches the trim line has nowhere for the blade to run.
    const corners = stoneObbCorners(stoneObbInflate(it.box, clear));
    it.outOfBounds = !corners.every(p =>
      p.x >= usable.x0 - 0.5 && p.x <= usable.x1 + 0.5 &&
      p.y >= usable.y0 - 0.5 && p.y <= usable.y1 + 0.5);
  });
  // Two pieces closer than the kerf are touching once the blade has been
  // through, so each is grown by half the kerf and the pair tested for
  // intersection. That flags a gap smaller than the kerf, not just an overlap.
  for (let i = 0; i < items.length; i++) {
    for (let j = i + 1; j < items.length; j++) {
      if (items[i].orphan || items[j].orphan) continue;
      if (stoneObbOverlap(stoneObbInflate(items[i].box, kerf / 2), stoneObbInflate(items[j].box, kerf / 2))) {
        items[i].overlaps.push(items[j].placement.id);
        items[j].overlaps.push(items[i].placement.id);
      }
    }
  }

  const placedArea = items.reduce((n, it) => n + (it.orphan ? 0 : it.area), 0);
  const fullArea = stoneAreaM2(dims.lengthMm, dims.widthMm);
  const usableArea = stoneAreaM2(usable.x1 - usable.x0, usable.y1 - usable.y0);
  return {
    usable, kerf, clear, items,
    outOfBounds: items.filter(i => i.outOfBounds).length,
    overlapping: items.filter(i => i.overlaps.length).length,
    veinConflicts: items.filter(i => i.vein.state === 'bad').length,
    veinUnknown: items.filter(i => i.vein.state === 'unknown').length,
    orphans: items.filter(i => i.orphan).length,
    placedArea, fullArea, usableArea,
    remainingArea: Math.max(0, usableArea - placedArea),
    yieldPct: fullArea > 0 ? (placedArea / fullArea) * 100 : null,
  };
}
function stoneLayoutErrors(evaln) {
  return evaln.outOfBounds + evaln.overlapping + evaln.orphans;
}

// ---- What is left over -----------------------------------------------------
// A MEASUREMENT of the free area, not a nesting result. The usable rectangle is
// rasterised, every placed piece plus its kerf is struck out, and the largest
// clear rectangle is taken, recorded, struck out and the scan repeated. That is
// the only defensible way to answer "what is big enough to rack" without
// pretending to have optimised anything.
function stoneLargestFreeRect(grid, cols, rows) {
  const h = new Array(cols).fill(0);
  let best = null;
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) h[c] = grid[r * cols + c] ? 0 : h[c] + 1;
    const stack = [];
    for (let c = 0; c <= cols; c++) {
      const cur = c === cols ? 0 : h[c];
      while (stack.length && h[stack[stack.length - 1]] >= cur) {
        const top = stack.pop();
        const height = h[top];
        const left = stack.length ? stack[stack.length - 1] + 1 : 0;
        const width = c - left;
        if (height > 0 && width > 0) {
          const area = width * height;
          if (!best || area > best.area) best = { area, c0: left, c1: c - 1, r0: r - height + 1, r1: r };
        }
      }
      stack.push(c);
    }
  }
  return best;
}
function stoneLayoutFreeRects(evaln, minMm, maxOut) {
  const u = evaln.usable;
  const cell = STONE_LAYOUT_GRID_MM;
  const cols = Math.floor((u.x1 - u.x0) / cell);
  const rows = Math.floor((u.y1 - u.y0) / cell);
  if (cols < 1 || rows < 1) return [];
  const grid = new Uint8Array(cols * rows);
  // Half the cell is added to the pad so a cell that merely clips a piece is
  // struck out — an offcut must not be reported over the top of a piece.
  const pad = evaln.kerf / 2 + cell / 2;
  const boxes = evaln.items.filter(i => !i.orphan).map(i => stoneObbInflate(i.box, pad));
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const px = u.x0 + (c + 0.5) * cell, py = u.y0 + (r + 0.5) * cell;
      for (let k = 0; k < boxes.length; k++) {
        if (stonePointInObb(px, py, boxes[k])) { grid[r * cols + c] = 1; break; }
      }
    }
  }
  const minCells = Math.max(1, Math.ceil(stoneNum(minMm) / cell));
  const out = [];
  const limit = maxOut || 6;
  for (let n = 0; n < limit; n++) {
    const best = stoneLargestFreeRect(grid, cols, rows);
    if (!best) break;
    const wCells = best.c1 - best.c0 + 1, hCells = best.r1 - best.r0 + 1;
    if (wCells < minCells || hCells < minCells) break;
    out.push({
      xMm: Math.round(u.x0 + best.c0 * cell), yMm: Math.round(u.y0 + best.r0 * cell),
      lengthMm: wCells * cell, widthMm: hCells * cell,
      area: stoneAreaM2(wCells * cell, hCells * cell),
    });
    for (let r = best.r0; r <= best.r1; r++) {
      for (let c = best.c0; c <= best.c1; c++) grid[r * cols + c] = 1;
    }
  }
  return out;
}

// ---- Suggest a starting position -------------------------------------------
// The FIRST free spot, scanning from the surface's origin corner — the plain
// bottom-left rule and nothing more. It is offered as a place to start from,
// never as an optimised nest, and the button says exactly that.
function stoneSuggestPlacement(piece, rotationDeg, evaln) {
  const u = evaln.usable;
  const w = stoneNum(piece.lengthMm), h = stoneNum(piece.widthMm);
  if (w <= 0 || h <= 0) return null;
  const step = STONE_LAYOUT_GRID_MM;
  const others = evaln.items.filter(i => !i.orphan).map(i => stoneObbInflate(i.box, evaln.kerf / 2));
  for (let cy = u.y0; cy <= u.y1; cy += step) {
    for (let cx = u.x0; cx <= u.x1; cx += step) {
      const box = stoneObb(cx, cy, w, h, rotationDeg);
      const corners = stoneObbCorners(stoneObbInflate(box, evaln.clear));
      const inside = corners.every(p => p.x >= u.x0 && p.x <= u.x1 && p.y >= u.y0 && p.y <= u.y1);
      if (!inside) continue;
      const self = stoneObbInflate(box, evaln.kerf / 2);
      let clash = false;
      for (let k = 0; k < others.length; k++) {
        if (stoneObbOverlap(self, others[k])) { clash = true; break; }
      }
      if (!clash) return { xMm: Math.round(cx), yMm: Math.round(cy) };
    }
  }
  return null;
}

function stonePieceCopyLabel(piece, copy) {
  const qty = Math.max(1, stoneNum(piece.qty) || 1);
  const base = piece.label || 'Piece';
  return qty > 1 ? `${base} ${stoneNum(copy) + 1}/${qty}` : base;
}
function stoneLayoutSurfaceLabel(surface) {
  const r = surface.record;
  return surface.kind === 'slab' ? (r.slabId || 'Slab') : (r.remnantId || 'Remnant');
}

// ---- The canvas ------------------------------------------------------------
// Drawn as SVG in MILLIMETRES: the viewBox is the surface itself, so every
// coordinate on screen is the coordinate that gets stored, with no pixel
// arithmetic in between. It also means the drawing prints as vector art through
// the app's own print path rather than as a picture of the screen.
function StoneLayoutCanvas({ surface, dims, layout, evaln, freeRects, selectedId, onSelect, onMove, editable, showPhoto, showOffcuts }) {
  const svgRef = useRef(null);
  const dragRef = useRef(null);
  const moveRef = useRef(onMove);
  moveRef.current = onMove;

  useEffect(() => {
    function onPointerMove(e) {
      const d = dragRef.current;
      if (!d) return;
      moveRef.current(d.id, d.ox + (e.clientX - d.sx) * d.scale, d.oy + (e.clientY - d.sy) * d.scale);
    }
    function onPointerUp() { dragRef.current = null; }
    window.addEventListener('pointermove', onPointerMove);
    window.addEventListener('pointerup', onPointerUp);
    return () => {
      window.removeEventListener('pointermove', onPointerMove);
      window.removeEventListener('pointerup', onPointerUp);
    };
  }, []);

  const L = Math.max(1, stoneNum(dims.lengthMm));
  const W = Math.max(1, stoneNum(dims.widthMm));
  const u = evaln.usable;
  const rec = surface.record;
  const fontMm = Math.max(24, Math.round(L / 55));
  const hair = Math.max(2, Math.round(L / 900));

  function startDrag(e, pl) {
    e.stopPropagation();
    onSelect(pl.id);
    if (!editable) return;
    // preventDefault below stops the browser handing focus to the keyboard
    // wrapper on its own, which would leave the arrow keys and Delete doing
    // nothing after a drag. Focus is therefore moved explicitly.
    const host = svgRef.current && svgRef.current.closest('[data-stone-layout-keys]');
    if (host && host.focus) host.focus();
    e.preventDefault();
    const box = svgRef.current ? svgRef.current.getBoundingClientRect() : null;
    const scale = box && box.width > 0 ? L / box.width : 1;
    dragRef.current = { id: pl.id, sx: e.clientX, sy: e.clientY, ox: stoneNum(pl.xMm), oy: stoneNum(pl.yMm), scale };
  }

  // Gridlines every 500 mm — enough to read a position off the drawing without
  // measuring it, few enough not to fight the photograph behind them.
  const gridStep = 500;
  const vLines = [];
  for (let x = gridStep; x < L; x += gridStep) vLines.push(x);
  const hLines = [];
  for (let y = gridStep; y < W; y += gridStep) hLines.push(y);

  // Scale bar: the largest round length that still sits inside a third of the
  // slab, drawn in the same millimetres as everything else.
  const barMm = [2000, 1000, 500, 250, 100].filter(mm => mm <= L / 3)[0] || Math.round(L / 4);

  function veinMarks(it) {
    const sv = rec.veinDirection;
    if (!sv || STONE_VEIN_ANGLE[sv] === undefined) return null;
    const local = stoneDeg2Rad(stoneNum(STONE_VEIN_ANGLE[sv]) - stoneNum(it.placement.rotationDeg));
    const len = Math.min(it.box.w, it.box.h) * 0.5;
    const dx = (Math.cos(local) * len) / 2, dy = (Math.sin(local) * len) / 2;
    const col = it.vein.state === 'bad' ? '#b83b3b' : '#161311';
    const headLen = len * 0.22;
    const a1 = local + Math.PI * 0.82, a2 = local - Math.PI * 0.82;
    return (
      <g stroke={col} strokeWidth={hair * 1.6} strokeLinecap="round" fill="none" opacity="0.85" style={{ pointerEvents: 'none' }}>
        <line x1={-dx} y1={-dy} x2={dx} y2={dy} />
        <line x1={dx} y1={dy} x2={dx + Math.cos(a1) * headLen} y2={dy + Math.sin(a1) * headLen} />
        <line x1={dx} y1={dy} x2={dx + Math.cos(a2) * headLen} y2={dy + Math.sin(a2) * headLen} />
      </g>
    );
  }

  return (
    <svg ref={svgRef} viewBox={`0 0 ${L} ${W}`}
      className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]"
      style={{ display: 'block', width: '100%', height: 'auto', touchAction: 'none' }}
      onPointerDown={() => onSelect(null)}>
      {showPhoto && rec.photoUrl
        ? <image href={rec.photoUrl} x="0" y="0" width={L} height={W} preserveAspectRatio="none" />
        : <rect x="0" y="0" width={L} height={W} fill="#f4efe7" />}

      {vLines.map(x => <line key={`v${x}`} x1={x} y1="0" x2={x} y2={W} stroke="#161311" strokeWidth={hair * 0.5} opacity="0.10" />)}
      {hLines.map(y => <line key={`h${y}`} x1="0" y1={y} x2={L} y2={y} stroke="#161311" strokeWidth={hair * 0.5} opacity="0.10" />)}

      {/* The full slab, then the usable area inside it. When no trim is set the
          two are the same rectangle, and the panel above says so. */}
      <rect x="0" y="0" width={L} height={W} fill="none" stroke="#161311" strokeWidth={hair * 2} />
      {(u.x0 > 0 || u.y0 > 0) && (
        <rect x={u.x0} y={u.y0} width={Math.max(0, u.x1 - u.x0)} height={Math.max(0, u.y1 - u.y0)}
          fill="none" stroke="#6b4a34" strokeWidth={hair * 1.4} strokeDasharray={`${hair * 8} ${hair * 6}`} />
      )}

      {/* An irregular remnant's real outline, for reference. Bounds are still
          checked against the bounding box — the panel says so plainly. */}
      {surface.kind === 'remnant' && rec.shape === 'Irregular' && rec.points && rec.points.length >= 3 && (
        <polygon points={rec.points.map(p => `${stoneNum(p.x)},${stoneNum(p.y)}`).join(' ')}
          fill="none" stroke="#a67b1f" strokeWidth={hair * 2} strokeDasharray={`${hair * 10} ${hair * 5}`} />
      )}

      {showOffcuts && (freeRects || []).map((r, i) => (
        <g key={`fr${i}`} style={{ pointerEvents: 'none' }}>
          <rect x={r.xMm} y={r.yMm} width={r.lengthMm} height={r.widthMm}
            fill="rgba(58,125,68,0.10)" stroke="#3a7d44" strokeWidth={hair} strokeDasharray={`${hair * 6} ${hair * 5}`} />
          <text x={r.xMm + r.lengthMm / 2} y={r.yMm + r.widthMm / 2} textAnchor="middle"
            fontSize={fontMm * 0.8} fill="#3a7d44">{Math.round(r.lengthMm)} × {Math.round(r.widthMm)}</text>
        </g>
      ))}

      {evaln.items.map(it => {
        if (it.orphan) return null;
        const pl = it.placement, b = it.box;
        const hard = it.outOfBounds || it.overlaps.length > 0;
        const sel = selectedId === pl.id;
        const fill = hard ? 'rgba(184,59,59,0.30)' : it.vein.state === 'bad' ? 'rgba(166,123,31,0.28)' : 'rgba(255,255,255,0.55)';
        const stroke = hard ? '#b83b3b' : it.vein.state === 'bad' ? '#a67b1f' : '#161311';
        return (
          <g key={pl.id} transform={`translate(${b.cx} ${b.cy}) rotate(${b.rot})`}
            style={{ cursor: editable ? 'move' : 'pointer' }}
            onPointerDown={e => startDrag(e, pl)}>
            <rect x={-b.w / 2} y={-b.h / 2} width={b.w} height={b.h}
              fill={fill} stroke={stroke} strokeWidth={sel ? hair * 4 : hair * 2} />
            {veinMarks(it)}
            <text x="0" y={-fontMm * 0.15} textAnchor="middle" fontSize={fontMm} fontWeight="bold" fill="#161311" style={{ pointerEvents: 'none' }}>
              {stonePieceCopyLabel(it.piece, pl.copy)}
            </text>
            <text x="0" y={fontMm * 1.05} textAnchor="middle" fontSize={fontMm * 0.8} fill="#58504a" style={{ pointerEvents: 'none' }}>
              {Math.round(b.w)} × {Math.round(b.h)}{b.rot ? ` · ${Math.round(b.rot)}°` : ''}
            </text>
          </g>
        );
      })}

      {/* The slab's own grain, drawn once so the reference direction is on the
          sheet the shop is handed. */}
      {STONE_VEIN_ANGLE[rec.veinDirection] !== undefined && (
        <g transform={`translate(${L * 0.06} ${W * 0.07})`} style={{ pointerEvents: 'none' }}>
          <g transform={`rotate(${STONE_VEIN_ANGLE[rec.veinDirection]})`} stroke="#6b4a34" strokeWidth={hair * 2} strokeLinecap="round" fill="none">
            <line x1={-L * 0.035} y1="0" x2={L * 0.035} y2="0" />
            <line x1={L * 0.035} y1="0" x2={L * 0.022} y2={-L * 0.012} />
            <line x1={L * 0.035} y1="0" x2={L * 0.022} y2={L * 0.012} />
          </g>
          <text x="0" y={W * 0.05} textAnchor="middle" fontSize={fontMm * 0.75} fill="#6b4a34">Slab grain — {rec.veinDirection}</text>
        </g>
      )}

      <g transform={`translate(${L * 0.03} ${W - W * 0.05})`} style={{ pointerEvents: 'none' }}>
        <line x1="0" y1="0" x2={barMm} y2="0" stroke="#161311" strokeWidth={hair * 2.5} />
        <line x1="0" y1={-fontMm * 0.3} x2="0" y2={fontMm * 0.3} stroke="#161311" strokeWidth={hair * 2} />
        <line x1={barMm} y1={-fontMm * 0.3} x2={barMm} y2={fontMm * 0.3} stroke="#161311" strokeWidth={hair * 2} />
        <text x={barMm / 2} y={-fontMm * 0.6} textAnchor="middle" fontSize={fontMm * 0.8} fill="#161311">
          {barMm >= 1000 ? `${barMm / 1000} m` : `${barMm} mm`}
        </text>
      </g>
    </svg>
  );
}

// ---- The tab ---------------------------------------------------------------

function StoneSlabLayout({ ctx, editable }) {
  const [projectId, setProjectId] = useState('');
  const [surfaceKey, setSurfaceKey] = useState('');
  const [includeStock, setIncludeStock] = useState(false);
  const [sameMaterialOnly, setSameMaterialOnly] = useState(true);
  const [draft, setDraft] = useState(null);
  const [dirty, setDirty] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  const [showPhoto, setShowPhoto] = useState(true);
  const [showOffcuts, setShowOffcuts] = useState(true);
  const [offcuts, setOffcuts] = useState([]);
  const [offering, setOffering] = useState(false);

  const projects = stonePickerProjects(ctx);
  const project = stoneProject(ctx, projectId);
  const canAssign = typeof ctx.updateProject === 'function';

  // Only stone this job can actually be cut from: what has been put aside for
  // it, plus unallocated stock when explicitly asked for.
  const surfaces = useMemo(() => {
    const out = [];
    (ctx.slabs || []).forEach(s => {
      if (STONE_DEAD_STATUSES.indexOf(s.status) !== -1) return;
      const mine = !!(project && s.projectId === project.id);
      if (!mine && !(includeStock && s.status === 'Available' && !s.projectId)) return;
      out.push({ kind: 'slab', id: s.id, key: `slab:${s.id}`, record: s, mine });
    });
    (ctx.remnants || []).forEach(r => {
      if (r.status === 'Consumed' || r.status === 'Scrap') return;
      const mine = !!(project && r.projectId === project.id);
      if (!mine && !(includeStock && r.status === 'Available' && !r.projectId)) return;
      out.push({ kind: 'remnant', id: r.id, key: `rmn:${r.id}`, record: r, mine });
    });
    return out;
  }, [ctx.slabs, ctx.remnants, project, includeStock]);

  const surface = surfaces.find(s => s.key === surfaceKey) || null;

  // The working copy. Dragging a piece must not write to localStorage on every
  // mouse move, so the canvas edits a draft and Save commits it.
  useEffect(() => {
    if (!surface) { setDraft(null); setDirty(false); setSelectedId(null); return; }
    const stored = surface.record.layout;
    setDraft(stored ? cloneDeep(stored) : stoneMakeLayout(surface.kind, surface.id, projectId, ctx.currentUserName));
    setDirty(false);
    setSelectedId(null);
  }, [surfaceKey]);

  const allPieces = stoneProjectPieces(project);
  const dims = surface
    ? { lengthMm: stoneNum(surface.record.lengthMm), widthMm: stoneNum(surface.record.widthMm) }
    : { lengthMm: 0, widthMm: 0 };
  const surfaceVein = surface ? surface.record.veinDirection : '';

  const evaln = useMemo(() => {
    if (!draft || !surface) return null;
    return stoneLayoutEvaluate(draft, allPieces, dims, surfaceVein, surface.record);
  }, [draft, allPieces, surface, dims.lengthMm, dims.widthMm, surfaceVein]);

  // Measuring the free area rasterises the slab, so it is debounced rather than
  // run on every pixel of a drag.
  useEffect(() => {
    if (!evaln || !draft) { setOffcuts([]); return; }
    const t = setTimeout(() => setOffcuts(stoneLayoutFreeRects(evaln, draft.minRemnantMm, 6)), 160);
    return () => clearTimeout(t);
  }, [evaln, draft && draft.minRemnantMm]);

  function editDraft(fn) {
    setDraft(d => {
      if (!d) return d;
      const next = Object.assign({}, d);
      fn(next);
      return next;
    });
    setDirty(true);
  }
  function setPlacement(id, fields) {
    editDraft(d => {
      d.placements = (d.placements || []).map(p => (p.id === id ? Object.assign({}, p, fields) : p));
    });
  }
  function movePlacement(id, xMm, yMm) {
    setPlacement(id, { xMm: Math.round(xMm), yMm: Math.round(yMm) });
  }
  function removePlacement(id) {
    editDraft(d => { d.placements = (d.placements || []).filter(p => p.id !== id); });
    setSelectedId(s => (s === id ? null : s));
  }
  function placePiece(piece, copy) {
    if (!evaln) return;
    const rot = stoneVeinPreferredRotation(surfaceVein, piece.veinDirection);
    const spot = stoneSuggestPlacement(piece, rot, evaln);
    const at = spot || { xMm: Math.round(evaln.usable.x0 + stoneNum(piece.lengthMm) / 2), yMm: Math.round(evaln.usable.y0 + stoneNum(piece.widthMm) / 2) };
    const pl = stoneMakePlacement(piece, copy, at.xMm, at.yMm, rot);
    editDraft(d => { d.placements = (d.placements || []).concat([pl]); });
    setSelectedId(pl.id);
    return !!spot;
  }
  function suggestFor(item) {
    if (!evaln || !item.piece) return;
    // The piece being moved must not block itself, so it is taken out of the
    // occupancy before the scan.
    const without = Object.assign({}, draft, { placements: (draft.placements || []).filter(p => p.id !== item.placement.id) });
    const trial = stoneLayoutEvaluate(without, allPieces, dims, surfaceVein, surface.record);
    const spot = stoneSuggestPlacement(item.piece, stoneNum(item.placement.rotationDeg), trial);
    if (spot) movePlacement(item.placement.id, spot.xMm, spot.yMm);
  }
  function setPieceVein(pieceId, value) {
    if (!canAssign || !project) return;
    ctx.updateProject(project.id, d => {
      d.stoneCutList = (d.stoneCutList || []).map(p => (p.id === pieceId ? Object.assign({}, p, { veinDirection: value }) : p));
      if (typeof ctx.logAction === 'function') {
        ctx.logAction(d, `LEON Stone — grain requirement set to ${value || 'none'} on a cut-list piece`);
      }
    });
  }

  const selected = evaln ? evaln.items.find(i => i.placement.id === selectedId) || null : null;

  function onKeyDown(e) {
    if (!editable || !selected) return;
    const step = e.shiftKey ? STONE_LAYOUT_NUDGE_COARSE_MM : STONE_LAYOUT_NUDGE_MM;
    const pl = selected.placement;
    if (e.key === 'ArrowLeft') { e.preventDefault(); movePlacement(pl.id, stoneNum(pl.xMm) - step, stoneNum(pl.yMm)); }
    else if (e.key === 'ArrowRight') { e.preventDefault(); movePlacement(pl.id, stoneNum(pl.xMm) + step, stoneNum(pl.yMm)); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); movePlacement(pl.id, stoneNum(pl.xMm), stoneNum(pl.yMm) - step); }
    else if (e.key === 'ArrowDown') { e.preventDefault(); movePlacement(pl.id, stoneNum(pl.xMm), stoneNum(pl.yMm) + step); }
    else if (e.key === '[') { e.preventDefault(); setPlacement(pl.id, { rotationDeg: stoneNormAngle360(stoneNum(pl.rotationDeg) - 1) }); }
    else if (e.key === ']') { e.preventDefault(); setPlacement(pl.id, { rotationDeg: stoneNormAngle360(stoneNum(pl.rotationDeg) + 1) }); }
    else if (e.key === 'r' || e.key === 'R') { e.preventDefault(); setPlacement(pl.id, { rotationDeg: stoneNormAngle360(stoneNum(pl.rotationDeg) + 90) }); }
    else if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); removePlacement(pl.id); }
  }

  // ---- Saving --------------------------------------------------------------
  function commitLayout(next, historyLine) {
    if (surface.kind === 'slab') {
      ctx.setSlabs(prev => prev.map(s => {
        if (s.id !== surface.id) return s;
        const copy = Object.assign({}, s, { layout: next });
        stonePushHistory(copy, ctx.currentUserName, historyLine);
        return copy;
      }));
    } else {
      ctx.setRemnants(prev => prev.map(r => {
        if (r.id !== surface.id) return r;
        const copy = Object.assign({}, r, { layout: next });
        stonePushHistory(copy, ctx.currentUserName, historyLine);
        return copy;
      }));
    }
  }
  function save(markComplete) {
    if (!draft || !surface || !evaln) return;
    const next = Object.assign({}, draft, {
      projectId: projectId || draft.projectId || null,
      status: markComplete ? 'Complete' : 'Draft',
      updatedBy: ctx.currentUserName, updatedDate: todayISO(),
    });
    const yieldTxt = evaln.yieldPct === null ? '—' : `${evaln.yieldPct.toFixed(1)}%`;
    commitLayout(next, markComplete
      ? `Cutting layout marked complete — ${next.placements.length} piece${next.placements.length === 1 ? '' : 's'}, ${yieldTxt} yield`
      : `Cutting layout saved — ${next.placements.length} piece${next.placements.length === 1 ? '' : 's'}, ${yieldTxt} yield`);

    // The pieces laid out are the job's own cut-list lines, so saving records
    // the source against them through the SAME fields the Cut List tab edits —
    // not a second link that could disagree with it.
    const placedIds = {};
    evaln.items.forEach(it => { if (!it.orphan) placedIds[it.piece.id] = true; });
    const ids = Object.keys(placedIds);
    if (ids.length && canAssign && project) {
      ctx.updateProject(project.id, d => {
        d.stoneCutList = (d.stoneCutList || []).map(p => {
          if (!placedIds[p.id]) return p;
          return Object.assign({}, p, {
            slabId: surface.kind === 'slab' ? surface.id : null,
            remnantId: surface.kind === 'remnant' ? surface.id : null,
            status: p.status === 'Planned' ? 'Assigned' : p.status,
          });
        });
        if (typeof ctx.logAction === 'function') {
          ctx.logAction(d, `LEON Stone — ${ids.length} cut-list piece${ids.length === 1 ? '' : 's'} laid out on ${stoneLayoutSurfaceLabel(surface)}`);
        }
      });
    }
    setDraft(next);
    setDirty(false);
    if (markComplete && offcuts.length) setOffering(true);
  }
  function onRemnantsCreated(ids) {
    const next = Object.assign({}, draft, { remnantIds: (draft.remnantIds || []).concat(ids) });
    commitLayout(next, `${ids.length} offcut${ids.length === 1 ? '' : 's'} racked as remnant${ids.length === 1 ? '' : 's'} from this layout`);
    setDraft(next);
    setOffering(false);
  }

  const hardErrors = evaln ? stoneLayoutErrors(evaln) : 0;
  const placedKeys = {};
  if (draft) (draft.placements || []).forEach(p => { placedKeys[`${p.pieceId}:${p.copy}`] = true; });
  const listPieces = sameMaterialOnly && surface && surface.record.material
    ? allPieces.filter(p => !p.material || p.material === surface.record.material)
    : allPieces;
  const unplaced = [];
  listPieces.forEach(p => {
    const qty = Math.max(1, stoneNum(p.qty) || 1);
    for (let i = 0; i < qty; i++) {
      if (!placedKeys[`${p.id}:${i}`]) unplaced.push({ piece: p, copy: i });
    }
  });

  return (
    <div className="space-y-4">
      <div className="rounded-lg border border-dashed border-[var(--leon-line)] bg-[var(--leon-cream)] px-4 py-3 text-xs text-[var(--leon-black)]/70 space-y-1.5">
        <p>
          <b>Pieces are placed by hand, on purpose.</b> Where a piece falls on a slab is a judgement about veining, defects and grain,
          made by someone looking at the slab's own photograph — which is why the photo sits behind the layout at the slab's real scale.
        </p>
        <p>
          <b>There is no auto-nesting optimiser here, and none is faked.</b> <i>Suggest a start point</i> drops a piece in the first free spot
          scanning from the slab's origin corner — one plain rule, offered as somewhere to begin, not as an optimised nest. Move it.
        </p>
        <p className="text-[var(--leon-black)]/55">
          No CNC or CAM output and no DXF export: this writes a cutting plan for a person to read, and the app has nothing behind it that could drive a machine.
        </p>
      </div>

      <div className="flex flex-wrap items-end gap-3">
        <Field label="Project" className="min-w-[240px]">
          <Select value={projectId} onChange={e => { setProjectId(e.target.value); setSurfaceKey(''); }}>
            <option value="">Select a project…</option>
            {projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
        </Field>
        <Field label="Slab or remnant" className="min-w-[280px]" hint={surfaces.length ? '' : 'Nothing is put aside for this job yet — allocate a slab under Allocation, or tick the box below.'}>
          <Select value={surfaceKey} onChange={e => setSurfaceKey(e.target.value)} disabled={!project}>
            <option value="">Select…</option>
            <optgroup label="Slabs">
              {surfaces.filter(s => s.kind === 'slab').map(s => (
                <option key={s.key} value={s.key}>
                  {s.record.slabId} — {[s.record.material, s.record.colour].filter(Boolean).join(' · ')}{s.mine ? '' : ' (unallocated stock)'}
                </option>
              ))}
            </optgroup>
            <optgroup label="Remnants">
              {surfaces.filter(s => s.kind === 'remnant').map(s => (
                <option key={s.key} value={s.key}>
                  {s.record.remnantId} — {[s.record.material, s.record.colour].filter(Boolean).join(' · ')}{s.mine ? '' : ' (unallocated stock)'}
                </option>
              ))}
            </optgroup>
          </Select>
        </Field>
        <label className="flex items-center gap-1.5 text-xs text-[var(--leon-black)]/60 pb-2">
          <input type="checkbox" checked={includeStock} onChange={e => setIncludeStock(e.target.checked)} />
          Also offer unallocated stock
        </label>
      </div>

      {!project ? <EmptyState text="Pick a project — the pieces come from its cut list." />
        : !surface ? <EmptyState text="Pick the slab or remnant you are laying out." />
        : !draft || !evaln ? <EmptyState text="Loading the layout…" />
        : (
          <>
            {(stoneNum(dims.lengthMm) <= 0 || stoneNum(dims.widthMm) <= 0) && (
              <div className="rounded-lg border border-[var(--leon-red)] bg-[#fbe7e7] px-4 py-3 text-xs">
                This {surface.kind} has no length and width on record, so it cannot be drawn to scale. Enter its real dimensions first.
              </div>
            )}

            <div data-print-region className="rounded-xl border border-[var(--leon-line)] bg-white overflow-hidden">
              <div className="px-4 py-2.5 bg-[var(--leon-cream)] border-b border-[var(--leon-line)] flex flex-wrap items-center gap-2">
                <div className="min-w-0">
                  <p className="text-sm font-bold">
                    Cutting layout — {stoneLayoutSurfaceLabel(surface)}
                    <span className="ml-2 font-normal text-[var(--leon-black)]/55">
                      {[surface.record.material, surface.record.colour].filter(Boolean).join(' · ')}
                      {surface.record.batch ? ` · batch ${surface.record.batch}` : ''}{surface.record.lot ? ` · lot ${surface.record.lot}` : ''}
                    </span>
                  </p>
                  <p className="text-[11px] text-[var(--leon-black)]/55">
                    {fmtDim(dims.lengthMm, 'Metric')} × {fmtDim(dims.widthMm, 'Metric')} · {stoneFmtArea(evaln.fullArea)} ·
                    {' '}{draft.status === 'Complete' ? 'Marked complete' : 'Draft'}{dirty ? ' · unsaved changes' : ''}
                  </p>
                </div>
                <span className="ml-auto flex items-center gap-2">
                  {/* The shop needs this on paper. Same print path as the rest of
                      the app — the drawing is SVG, so it prints as line art. */}
                  <DocActions title={`Cutting layout — ${stoneLayoutSurfaceLabel(surface)}`}
                    heading={`Cutting layout — ${stoneLayoutSurfaceLabel(surface)}`}
                    lines={[
                      project.name,
                      [surface.record.material, surface.record.colour].filter(Boolean).join(' · '),
                      `${Math.round(stoneNum(dims.lengthMm))} × ${Math.round(stoneNum(dims.widthMm))} mm`,
                      `Kerf ${Math.round(stoneNum(draft.kerfMm))} mm · edge clearance ${Math.round(stoneNum(draft.edgeClearanceMm))} mm`,
                    ]} />
                </span>
              </div>

              <div className="p-4 space-y-3">
                {!surface.record.photoUrl && (
                  <p className="text-[11px] text-[var(--leon-yellow)] bg-[#fbf1dd] rounded px-2.5 py-1.5">
                    No photograph of this {surface.kind} is on record. Without one you cannot see the veining, so this layout is a <b>yield estimate, not a cutting plan</b> —
                    photograph the {surface.kind} and add it under {surface.kind === 'slab' ? 'All Slabs' : 'Remnants'}.
                  </p>
                )}
                {surface.kind === 'remnant' && surface.record.shape === 'Irregular' && (
                  <p className="text-[11px] text-[var(--leon-yellow)] bg-[#fbf1dd] rounded px-2.5 py-1.5">
                    This remnant is irregular. Its recorded outline is drawn dashed for reference, but bounds are only checked against the bounding box — check by eye that nothing falls outside the outline.
                  </p>
                )}

                <div className="flex flex-wrap gap-4">
                  <div className="flex-1 min-w-[320px]">
                    <div tabIndex={0} data-stone-layout-keys onKeyDown={onKeyDown} className="outline-none rounded-lg focus:ring-2 focus:ring-[var(--leon-brown-light)]">
                      <StoneLayoutCanvas surface={surface} dims={dims} layout={draft} evaln={evaln}
                        freeRects={offcuts} selectedId={selectedId} onSelect={setSelectedId}
                        onMove={movePlacement} editable={editable} showPhoto={showPhoto} showOffcuts={showOffcuts} />
                    </div>
                    <p className="text-[11px] text-[var(--leon-black)]/45 mt-1.5">
                      Drag a piece to move it. Click one, then arrow keys nudge it {STONE_LAYOUT_NUDGE_MM} mm ({STONE_LAYOUT_NUDGE_COARSE_MM} mm with Shift),
                      <b> R</b> turns it 90°, <b>[</b> and <b>]</b> turn it 1°, Delete removes it.
                    </p>
                    <div className="flex flex-wrap gap-3 mt-1.5">
                      <label className="flex items-center gap-1.5 text-[11px] text-[var(--leon-black)]/60">
                        <input type="checkbox" checked={showPhoto} onChange={e => setShowPhoto(e.target.checked)} /> Show the slab photo
                      </label>
                      <label className="flex items-center gap-1.5 text-[11px] text-[var(--leon-black)]/60">
                        <input type="checkbox" checked={showOffcuts} onChange={e => setShowOffcuts(e.target.checked)} /> Show the offcuts left
                      </label>
                    </div>
                  </div>

                  <div className="w-full md:w-[300px] shrink-0 space-y-3">
                    <div className="rounded-lg border border-[var(--leon-line)] p-3">
                      <p className="text-xs font-bold mb-1.5">Where it stands</p>
                      <div className="grid grid-cols-2 gap-2 text-[11px]">
                        <div><span className="text-[var(--leon-black)]/45 block">Area used</span><b>{stoneFmtAreaShort(evaln.placedArea)}</b></div>
                        <div><span className="text-[var(--leon-black)]/45 block">Usable left</span><b>{stoneFmtAreaShort(evaln.remainingArea)}</b></div>
                        <div><span className="text-[var(--leon-black)]/45 block">Yield</span><b className="text-sm">{evaln.yieldPct === null ? '—' : `${evaln.yieldPct.toFixed(1)}%`}</b></div>
                        <div><span className="text-[var(--leon-black)]/45 block">Pieces placed</span><b>{evaln.items.length - evaln.orphans}</b></div>
                      </div>
                      <p className="text-[10px] text-[var(--leon-black)]/45 mt-1.5">
                        Yield is finished piece area over the {stoneFmtAreaShort(evaln.fullArea)} of the whole {surface.kind}.
                        Usable area {stoneFmtAreaShort(evaln.usableArea)}
                        {stoneLayoutTrimMm(surface.record, draft) > 0
                          ? ` — after a ${Math.round(stoneLayoutTrimMm(surface.record, draft))} mm trim on every edge.`
                          : ' — no trim is set, so the rectangle drawn is the nominal size straight off the record.'}
                      </p>
                    </div>

                    <div className={`rounded-lg border p-3 ${hardErrors ? 'border-[var(--leon-red)] bg-[#fbe7e7]' : 'border-[var(--leon-line)]'}`}>
                      <p className="text-xs font-bold mb-1">{hardErrors ? `${hardErrors} piece${hardErrors === 1 ? '' : 's'} cannot be cut as laid` : 'Nothing is clashing'}</p>
                      <ul className="text-[11px] space-y-0.5">
                        {evaln.outOfBounds > 0 && <li>❌ {evaln.outOfBounds} off the usable area (edge clearance {Math.round(evaln.clear)} mm).</li>}
                        {evaln.overlapping > 0 && <li>❌ {evaln.overlapping} overlapping, or closer than the {Math.round(evaln.kerf)} mm kerf.</li>}
                        {evaln.orphans > 0 && <li>❌ {evaln.orphans} placed piece{evaln.orphans === 1 ? '' : 's'} no longer on the cut list — remove them from the table below.</li>}
                        {evaln.veinConflicts > 0 && <li>⚠️ {evaln.veinConflicts} rotated against the grain the piece requires.</li>}
                        {evaln.veinUnknown > 0 && <li>⚠️ {evaln.veinUnknown} whose veining is described in words — check by eye.</li>}
                        {!hardErrors && !evaln.veinConflicts && !evaln.veinUnknown && <li className="text-[var(--leon-black)]/50">No bounds, kerf or grain problems.</li>}
                      </ul>
                    </div>

                    <div className="rounded-lg border border-[var(--leon-line)] p-3 space-y-2">
                      <p className="text-xs font-bold">Cutting allowances</p>
                      <Field label="Saw kerf + trim (mm)" hint="Two pieces closer than this are touching once the blade has been through — that is not a valid cut.">
                        <TextInput type="number" min="0" value={draft.kerfMm} disabled={!editable}
                          onChange={e => editDraft(d => { d.kerfMm = stoneNum(e.target.value); })} />
                      </Field>
                      <Field label="Edge clearance (mm)" hint="Kept between a piece and the edge of the usable area.">
                        <TextInput type="number" min="0" value={draft.edgeClearanceMm} disabled={!editable}
                          onChange={e => editDraft(d => { d.edgeClearanceMm = stoneNum(e.target.value); })} />
                      </Field>
                      <Field label="Edge trim on the slab (mm)" hint={stoneNum(surface.record.edgeTrimMm) > 0 ? 'Taken from the slab record.' : 'The slab record carries no trim figure, so this is the margin this layout was planned to.'}>
                        <TextInput type="number" min="0" value={draft.trimMm} disabled={!editable || stoneNum(surface.record.edgeTrimMm) > 0}
                          onChange={e => editDraft(d => { d.trimMm = stoneNum(e.target.value); })} />
                      </Field>
                      <Field label="Smallest offcut worth racking (mm)" hint="Free rectangles smaller than this on either side are not offered as remnants.">
                        <TextInput type="number" min="0" value={draft.minRemnantMm} disabled={!editable}
                          onChange={e => editDraft(d => { d.minRemnantMm = stoneNum(e.target.value); })} />
                      </Field>
                    </div>
                  </div>
                </div>

                {selected && (
                  <div className="rounded-lg border border-[var(--leon-brown-light)] bg-[var(--leon-cream)] p-3">
                    <div className="flex flex-wrap items-end gap-3">
                      <div className="min-w-[160px]">
                        <p className="text-xs font-bold">{stonePieceCopyLabel(selected.piece, selected.placement.copy)}</p>
                        <p className="text-[11px] text-[var(--leon-black)]/55">
                          {fmtDim(selected.piece.lengthMm, 'Metric')} × {fmtDim(selected.piece.widthMm, 'Metric')} · {stoneFmtAreaShort(selected.area)}
                          {selected.piece.edgeProfile ? ` · ${selected.piece.edgeProfile}` : ''}
                        </p>
                      </div>
                      <Field label="X — centre (mm)"><TextInput type="number" className="!w-28" value={selected.placement.xMm} disabled={!editable}
                        onChange={e => setPlacement(selected.placement.id, { xMm: Math.round(stoneNum(e.target.value)) })} /></Field>
                      <Field label="Y — centre (mm)"><TextInput type="number" className="!w-28" value={selected.placement.yMm} disabled={!editable}
                        onChange={e => setPlacement(selected.placement.id, { yMm: Math.round(stoneNum(e.target.value)) })} /></Field>
                      <Field label="Rotation (°)"><TextInput type="number" className="!w-24" value={selected.placement.rotationDeg} disabled={!editable}
                        onChange={e => setPlacement(selected.placement.id, { rotationDeg: stoneNum(e.target.value) })} /></Field>
                      {editable && (
                        <div className="flex flex-wrap gap-1.5 pb-0.5">
                          <Button size="sm" variant="outline" onClick={() => setPlacement(selected.placement.id, { rotationDeg: stoneNormAngle360(stoneNum(selected.placement.rotationDeg) - 90) })}>↺ 90°</Button>
                          <Button size="sm" variant="outline" onClick={() => setPlacement(selected.placement.id, { rotationDeg: stoneNormAngle360(stoneNum(selected.placement.rotationDeg) + 90) })}>↻ 90°</Button>
                          <Button size="sm" variant="outline" onClick={() => setPlacement(selected.placement.id, { rotationDeg: 0 })}>Square up</Button>
                          <Button size="sm" variant="ghost" onClick={() => suggestFor(selected)} title="Drops it in the first free spot scanning from the slab's origin corner. A starting point, not an optimised nest.">Suggest a start point</Button>
                          <Button size="sm" variant="ghost" onClick={() => removePlacement(selected.placement.id)}>Take off the slab</Button>
                        </div>
                      )}
                    </div>
                    <p className={`text-[11px] mt-2 ${selected.vein.state === 'bad' ? 'text-[var(--leon-red)] font-semibold' : selected.vein.state === 'unknown' ? 'text-[var(--leon-yellow)]' : 'text-[var(--leon-black)]/55'}`}>
                      Grain — {selected.vein.text}
                    </p>
                    {(selected.outOfBounds || selected.overlaps.length > 0) && (
                      <p className="text-[11px] text-[var(--leon-red)] font-semibold mt-1">
                        {selected.outOfBounds ? 'This piece is off the usable area. ' : ''}
                        {selected.overlaps.length ? `It is within the kerf of ${selected.overlaps.length} other piece${selected.overlaps.length === 1 ? '' : 's'}.` : ''}
                      </p>
                    )}
                  </div>
                )}

                <div className="flex flex-wrap items-center gap-2">
                  {editable && <Button onClick={() => save(false)} disabled={!dirty}>Save layout</Button>}
                  {editable && (
                    <Button variant="black" onClick={() => save(true)} disabled={hardErrors > 0}
                      title={hardErrors > 0 ? 'Clear the bounds and kerf errors first — a layout that cannot be cut cannot be marked complete.' : ''}>
                      Mark the layout complete
                    </Button>
                  )}
                  {!!offcuts.length && editable && (
                    <Button variant="outline" onClick={() => setOffering(true)}>Rack the offcuts as remnants ({offcuts.length})</Button>
                  )}
                  {hardErrors > 0 && <span className="text-[11px] text-[var(--leon-red)]">A layout with a piece off the slab or inside another's kerf cannot be marked complete.</span>}
                  {!canAssign && <span className="text-[11px] text-[var(--leon-black)]/45">Saving records the layout on the {surface.kind}; this build has not exposed project editing, so the cut-list pieces will not be marked assigned.</span>}
                </div>
              </div>

              {/* The drawing is for the wall; this table is what a shop actually
                  reads a position off, and it is what survives on paper. */}
              <div className="border-t border-[var(--leon-line)] px-4 py-3">
                <p className="text-xs font-bold mb-1.5">Placements ({evaln.items.length})</p>
                {!evaln.items.length ? <p className="text-xs text-[var(--leon-black)]/40 italic">Nothing placed yet.</p> : (
                  <div className="overflow-x-auto">
                    <table className="w-full text-xs">
                      <thead className="bg-[var(--leon-cream)]">
                        <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                          <th className="px-3 py-2">Piece</th><th className="px-3 py-2">Size (mm)</th>
                          <th className="px-3 py-2">Centre X · Y</th><th className="px-3 py-2">Corner X · Y</th>
                          <th className="px-3 py-2">Rotation</th><th className="px-3 py-2">Grain</th>
                          <th className="px-3 py-2">Check</th><th className="px-3 py-2"></th>
                        </tr>
                      </thead>
                      <tbody>
                        {evaln.items.map(it => {
                          if (it.orphan) {
                            return (
                              <tr key={it.placement.id} className="border-t border-[var(--leon-line)]">
                                <td className="px-3 py-2 font-semibold text-[var(--leon-red)]">Piece no longer on the cut list</td>
                                <td className="px-3 py-2" colSpan={5}>—</td>
                                <td className="px-3 py-2"><Badge tone="red">Orphan</Badge></td>
                                <td className="px-3 py-2">{editable && <Button size="sm" variant="ghost" onClick={() => removePlacement(it.placement.id)}>Remove</Button>}</td>
                              </tr>
                            );
                          }
                          const cs = stoneObbCorners(it.box);
                          const x0 = Math.round(Math.min.apply(null, cs.map(p => p.x)));
                          const y0 = Math.round(Math.min.apply(null, cs.map(p => p.y)));
                          return (
                            <tr key={it.placement.id} className={`border-t border-[var(--leon-line)] ${selectedId === it.placement.id ? 'bg-[var(--leon-cream)]' : ''}`}>
                              <td className="px-3 py-2 font-semibold cursor-pointer hover:underline" onClick={() => setSelectedId(it.placement.id)}>
                                {stonePieceCopyLabel(it.piece, it.placement.copy)}
                              </td>
                              <td className="px-3 py-2 whitespace-nowrap">{Math.round(stoneNum(it.piece.lengthMm))} × {Math.round(stoneNum(it.piece.widthMm))}</td>
                              <td className="px-3 py-2 whitespace-nowrap">{Math.round(stoneNum(it.placement.xMm))} · {Math.round(stoneNum(it.placement.yMm))}</td>
                              <td className="px-3 py-2 whitespace-nowrap">{x0} · {y0}</td>
                              <td className="px-3 py-2">{Math.round(stoneNum(it.placement.rotationDeg))}°</td>
                              <td className="px-3 py-2">
                                {it.piece.veinDirection
                                  ? <span className={it.vein.state === 'bad' ? 'text-[var(--leon-red)] font-semibold' : ''}>{it.piece.veinDirection}</span>
                                  : <span className="text-[var(--leon-black)]/30">No requirement</span>}
                              </td>
                              <td className="px-3 py-2">
                                {it.outOfBounds ? <Badge tone="red">Off the slab</Badge>
                                  : it.overlaps.length ? <Badge tone="red">Inside the kerf</Badge>
                                  : it.vein.state === 'bad' ? <Badge tone="yellow">Against the grain</Badge>
                                  : it.vein.state === 'unknown' ? <Badge tone="yellow">Check by eye</Badge>
                                  : <Badge tone="green">OK</Badge>}
                              </td>
                              <td className="px-3 py-2">{editable && <Button size="sm" variant="ghost" onClick={() => removePlacement(it.placement.id)}>Remove</Button>}</td>
                            </tr>
                          );
                        })}
                      </tbody>
                    </table>
                  </div>
                )}
                <p className="text-[11px] text-[var(--leon-black)]/45 mt-2">
                  X and Y are millimetres from the {surface.kind}'s top-left corner. Centre is the point the piece turns about; corner is the top-left of the space it occupies once rotated.
                </p>
              </div>
            </div>

            <Collapsible title="Pieces still to place" id="stone-layout-unplaced" defaultOpen count={unplaced.length}>
              <div className="flex flex-wrap items-center gap-3 mb-2">
                <label className="flex items-center gap-1.5 text-[11px] text-[var(--leon-black)]/60">
                  <input type="checkbox" checked={sameMaterialOnly} onChange={e => setSameMaterialOnly(e.target.checked)} />
                  Only pieces in {surface.record.material || 'this material'}
                </label>
                <span className="text-[11px] text-[var(--leon-black)]/45">
                  {unplaced.length} of {listPieces.reduce((n, p) => n + Math.max(1, stoneNum(p.qty) || 1), 0)} still to place on this {surface.kind}.
                </span>
              </div>
              {!unplaced.length ? <EmptyState text={listPieces.length ? 'Every piece on this list is on the slab.' : 'This project has no cut-list pieces yet — add them under Cut List.'} /> : (
                <div className="overflow-x-auto">
                  <table className="w-full text-xs">
                    <thead className="bg-[var(--leon-cream)]">
                      <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                        <th className="px-3 py-2">Piece</th><th className="px-3 py-2">Material</th><th className="px-3 py-2">Size (mm)</th>
                        <th className="px-3 py-2">Area</th><th className="px-3 py-2">Grain required</th><th className="px-3 py-2">Cut from</th><th className="px-3 py-2"></th>
                      </tr>
                    </thead>
                    <tbody>
                      {unplaced.map(row => {
                        const mismatch = !!(surface.record.material && row.piece.material && row.piece.material !== surface.record.material);
                        const fits = stoneRemnantFit({ lengthMm: evaln.usable.x1 - evaln.usable.x0, widthMm: evaln.usable.y1 - evaln.usable.y0 },
                          row.piece.lengthMm, row.piece.widthMm, stoneNum(draft.kerfMm) + 2 * stoneNum(draft.edgeClearanceMm));
                        return (
                          <tr key={`${row.piece.id}:${row.copy}`} className="border-t border-[var(--leon-line)]">
                            <td className="px-3 py-2 font-semibold">{stonePieceCopyLabel(row.piece, row.copy)}</td>
                            <td className="px-3 py-2">
                              {row.piece.material || '—'}
                              {mismatch && <div className="text-[var(--leon-red)]">not this {surface.kind}'s material</div>}
                            </td>
                            <td className="px-3 py-2 whitespace-nowrap">{Math.round(stoneNum(row.piece.lengthMm))} × {Math.round(stoneNum(row.piece.widthMm))}</td>
                            <td className="px-3 py-2">{stoneFmtAreaShort(stoneAreaM2(row.piece.lengthMm, row.piece.widthMm))}</td>
                            <td className="px-3 py-2">
                              {canAssign && editable ? (
                                <Select value={row.piece.veinDirection || ''} onChange={e => setPieceVein(row.piece.id, e.target.value)} className="!w-auto !py-1 !text-[11px]">
                                  {STONE_PIECE_VEIN_OPTIONS.map(v => <option key={v || 'none'} value={v}>{v || 'No requirement'}</option>)}
                                </Select>
                              ) : <span>{row.piece.veinDirection || 'No requirement'}</span>}
                            </td>
                            <td className="px-3 py-2">
                              {!fits.fits ? <Badge tone="red">Too big for this {surface.kind}</Badge>
                                : fits.rotated ? <Badge tone="yellow">Only if turned 90°</Badge>
                                : <Badge tone="green">Fits</Badge>}
                            </td>
                            <td className="px-3 py-2">
                              {editable && <Button size="sm" variant="ghost" disabled={!fits.fits}
                                title="Drops it in the first free spot scanning from the origin corner — a starting point, not an optimised nest."
                                onClick={() => placePiece(row.piece, row.copy)}>Place</Button>}
                            </td>
                          </tr>
                        );
                      })}
                    </tbody>
                  </table>
                </div>
              )}
              <p className="text-[11px] text-[var(--leon-black)]/45 mt-2">
                <b>Fits</b> only asks whether the piece is smaller than the usable area — it says nothing about whether there is still room once the other pieces are down. That is what the drawing is for.
              </p>
            </Collapsible>

            <Collapsible title="What is left over" id="stone-layout-offcuts" count={offcuts.length}>
              {!offcuts.length ? (
                <EmptyState text={`No clear rectangle at least ${Math.round(stoneNum(draft.minRemnantMm))} mm on both sides is left.`} />
              ) : (
                <>
                  <div className="overflow-x-auto">
                    <table className="w-full text-xs">
                      <thead className="bg-[var(--leon-cream)]">
                        <tr className="text-left text-[var(--leon-black)]/50 uppercase">
                          <th className="px-3 py-2">Offcut</th><th className="px-3 py-2">Size (mm)</th><th className="px-3 py-2">Position X · Y</th><th className="px-3 py-2">Area</th>
                        </tr>
                      </thead>
                      <tbody>
                        {offcuts.map((r, i) => (
                          <tr key={i} className="border-t border-[var(--leon-line)]">
                            <td className="px-3 py-2 font-semibold">#{i + 1}</td>
                            <td className="px-3 py-2 whitespace-nowrap">{r.lengthMm} × {r.widthMm}</td>
                            <td className="px-3 py-2 whitespace-nowrap">{r.xMm} · {r.yMm}</td>
                            <td className="px-3 py-2">{stoneFmtArea(r.area)}</td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                  <p className="text-[11px] text-[var(--leon-black)]/50 mt-2">
                    These are a <b>measurement</b> of the clear area left, taken by striking out every placed piece plus its kerf on a {STONE_LAYOUT_GRID_MM} mm grid and reading the largest clear rectangles off what remains.
                    Nothing has been optimised — move a piece and they change.
                  </p>
                  {editable && <div className="mt-2"><Button variant="outline" size="sm" onClick={() => setOffering(true)}>Rack these as remnants…</Button></div>}
                </>
              )}
            </Collapsible>
          </>
        )}

      <StoneLayoutOffcutModal ctx={ctx} open={offering} surface={surface} rects={offcuts}
        onClose={() => setOffering(false)} onCreated={onRemnantsCreated} />
    </div>
  );
}

// Offcuts become remnants through stoneMakeRemnant — the same factory the
// Create-a-remnant modal uses, so the batch, lot, thickness and pro-rated cost
// basis come off the parent slab exactly as they always have. A remnant laid
// out on is still recorded against ITS parent slab, because that is where the
// lineage and the cost basis honestly come from.
function StoneLayoutOffcutModal({ ctx, open, surface, rects, onClose, onCreated }) {
  const [picked, setPicked] = useState({});
  const [rack, setRack] = useState('');
  const [parentStatus, setParentStatus] = useState('Remnant Created');
  useEffect(() => {
    if (!open || !surface) return;
    const init = {};
    (rects || []).forEach((r, i) => { init[i] = true; });
    setPicked(init);
    setRack(surface.record.rack || '');
    setParentStatus('Remnant Created');
  }, [open, surface, rects]);
  if (!open || !surface) return null;

  const parentSlab = surface.kind === 'slab'
    ? surface.record
    : (ctx.slabs || []).find(s => s.id === surface.record.parentSlabId) || null;
  const chosen = (rects || []).filter((r, i) => picked[i]);

  function submit() {
    if (!parentSlab || !chosen.length) return;
    const created = [];
    ctx.setRemnants(prev => {
      let out = prev.slice();
      chosen.forEach(r => {
        const made = stoneMakeRemnant(parentSlab, {
          shape: 'Rectangle', lengthMm: r.lengthMm, widthMm: r.widthMm,
          warehouseId: surface.record.warehouseId || parentSlab.warehouseId || null,
          rack, status: 'Available',
          notes: `Offcut measured from the cutting layout of ${stoneLayoutSurfaceLabel(surface)} — ${r.lengthMm} × ${r.widthMm} mm at ${r.xMm}, ${r.yMm}.`,
        }, ctx.currentUserName, out);
        out = out.concat([made]);
        created.push(made.id);
      });
      return out;
    });
    if (surface.kind === 'slab') {
      ctx.setSlabs(prev => prev.map(s => {
        if (s.id !== surface.id) return s;
        const copy = Object.assign({}, s, { status: parentStatus });
        stonePushHistory(copy, ctx.currentUserName, `${chosen.length} offcut${chosen.length === 1 ? '' : 's'} racked from the cutting layout; slab moved to ${parentStatus}`);
        return copy;
      }));
    }
    onCreated(created);
  }

  return (
    <Modal open={open} onClose={onClose} wide title="Rack the offcuts as remnants"
      footer={<><Button variant="ghost" onClick={onClose}>Not now</Button>
        <Button onClick={submit} disabled={!parentSlab || !chosen.length}>Create {chosen.length} remnant{chosen.length === 1 ? '' : 's'}</Button></>}>
      <div className="space-y-3">
        {!parentSlab ? (
          <p className="text-xs text-[var(--leon-red)]">
            The parent slab for this remnant is no longer on record, so a new remnant cannot inherit its batch, lot or cost basis. Record the offcut from the slab it came off instead.
          </p>
        ) : (
          <>
            <p className="text-xs text-[var(--leon-black)]/60">
              Each one becomes a remnant of <b>{parentSlab.slabId}</b> — inheriting {parentSlab.material}
              {parentSlab.colour ? <> · <b>{parentSlab.colour}</b></> : null} · batch <b>{parentSlab.batch || '—'}</b> · lot <b>{parentSlab.lot || '—'}</b> · {fmtDim(parentSlab.thicknessMm, 'Metric')} thick,
              with its cost basis pro-rated by area. Nothing is retyped.
            </p>
            <div className="space-y-1">
              {(rects || []).map((r, i) => (
                <label key={i} className="flex items-center gap-2 text-xs border border-[var(--leon-line)] rounded px-2.5 py-1.5">
                  <input type="checkbox" checked={!!picked[i]} onChange={e => setPicked(p => Object.assign({}, p, { [i]: e.target.checked }))} />
                  <b>#{i + 1}</b> — {r.lengthMm} × {r.widthMm} mm · {stoneFmtArea(r.area)}
                  <span className="text-[var(--leon-black)]/45">at {r.xMm}, {r.yMm}</span>
                  {ctx.canSeeFin && stoneSlabArea(parentSlab) > 0 && (
                    <span className="ml-auto text-[var(--leon-black)]/55">{fmtMoney((stoneLandedCost(parentSlab) * r.area) / stoneSlabArea(parentSlab))}</span>
                  )}
                </label>
              ))}
            </div>
            <div className="grid grid-cols-2 gap-3">
              <Field label="Rack / bay"><TextInput value={rack} onChange={e => setRack(e.target.value)} /></Field>
              {surface.kind === 'slab' && (
                <Field label="Parent slab becomes" hint="Partially Consumed if more is still to come off it; Consumed if this was the last of it">
                  <Select value={parentStatus} onChange={e => setParentStatus(e.target.value)}>
                    <option value="Remnant Created">Remnant Created</option>
                    <option value="Partially Consumed">Partially Consumed</option>
                    <option value="Consumed">Consumed</option>
                  </Select>
                </Field>
              )}
            </div>
            <p className="text-[11px] text-[var(--leon-black)]/50">
              These rectangles are what the layout leaves clear. Look at the slab photo before racking one — a clear rectangle over a defect or a colour change is not a usable offcut, and only a person can tell.
            </p>
          </>
        )}
      </div>
    </Modal>
  );
}
