// ═══════════════════════════════════════════════════ LEON Doors
// A door is a database object. The schedule, the elevation, the hardware,
// production and the submittal all read ONE record, so changing a width in the
// schedule and changing it in the designer are the same act — there is no
// second copy to fall out of step.
//
// Drawings are GENERATED from the parameters every time they are shown. Nothing
// here stores a door as a picture, which is what makes a frame change redraw
// forty elevations instead of invalidating them.

// Two groups, because the rail was one flat run of twelve and half of them are
// settings a person opens once a year. The job's own work sits at the top; the
// libraries and standards the shop works to sit under Door Settings.
const DOOR_SW_SECTIONS = [
  { key: 'dashboard', label: 'Dashboard', icon: '📊', group: 'This job' },
  { key: 'schedule', label: 'Door Schedule', icon: '📋', group: 'This job' },
  { key: 'designer', label: 'Door Designer', icon: '🚪', group: 'This job' },
  { key: 'sheets', label: 'Shop Drawing', icon: '📐', group: 'This job' },
  { key: 'tags', label: 'Finish Tags', icon: '🏷️', group: 'This job' },
  { key: 'keynotes', label: 'Keynotes', icon: '📌', group: 'This job' },
  { key: 'submittals', label: 'Submittals', icon: '📑', group: 'This job' },
  { key: 'package', label: 'Submittal Package', icon: '📦', group: 'This job' },
  { key: 'models', label: 'LEON Models', icon: '📖', group: 'Door Settings' },
  { key: 'types', label: 'Door Types', icon: '🅰️', group: 'Door Settings' },
  { key: 'global', label: 'Global Library', icon: '🌍', group: 'Door Settings' },
  { key: 'frames', label: 'Frames', icon: '🖼️', group: 'Door Settings' },
  { key: 'trims', label: 'Trims', icon: '📏', group: 'Door Settings' },
  { key: 'rules', label: 'Opening Rules', icon: '📐', group: 'Door Settings' },
  { key: 'designs', label: 'Leaf Designs', icon: '🎨', group: 'Door Settings' },
  { key: 'hardware', label: 'Hardware', icon: '🔩', group: 'Door Settings' },
];

// ── Parametric elevation ──────────────────────────────────────────────────
// Drawn from the leaf, the design and the frame. Everything is in millimetres
// and scaled at the last moment, so the geometry never has to know how big the
// picture on screen is.
function DoorElevation({ sizes, design, frame, handing, liteKind, liteW, liteH, liteSill,
                         system, height, showDims, mark }) {
  const d = design || makeDoorDesign();
  const S = sizes;
  if (!S || !(S.leaf.w > 0) || !(S.leaf.h > 0)) {
    return <div className="text-xs text-[var(--leon-black)]/40 p-4">Enter a size to draw this door.</div>;
  }
  const pair = handing === 'PAIR' || handing === 'PAIR_UNEQUAL';
  const jamb = frame ? qnum(frame.jambWidth) : 50.8;
  const head = frame ? qnum(frame.headWidth) : 50.8;
  const pad = 90;                                   // room for dimension lines
  const totalW = S.frame.w + pad * 2;
  const totalH = S.frame.h + pad * 2;
  const H = height || 340;
  const scale = H / totalH;
  const px = mm => mm * scale;
  const ox = pad, oy = pad;                          // frame origin inside the viewBox

  const leafW = pair ? (S.frame.w - jamb * 2 - qnum((frame || {}).astragal || 0)) / 2 : S.frame.w - jamb * 2;
  const leafH = S.frame.h - head;
  const leaves = pair
    ? [{ x: ox + jamb, w: leafW }, { x: ox + jamb + leafW, w: leafW }]
    : [{ x: ox + jamb, w: leafW }];

  const BROWN = 'var(--leon-brown)';
  const LINE = 'var(--leon-line)';
  const INK = 'var(--leon-black)';

  function leafFace(l, i) {
    const parts = [];
    const y = oy + head;
    parts.push(<rect key={`f${i}`} x={px(l.x)} y={px(y)} width={px(l.w)} height={px(leafH)}
      fill="#fff" stroke={INK} strokeWidth="1.2" />);
    // Shaker: rails and stiles bound a recessed panel field.
    if (d.kind === 'Shaker' || d.kind === 'Wood Shaker' || d.panelRows > 0) {
      const st = qnum(d.stile), rt = qnum(d.railTop), rb = qnum(d.railBottom), rm = qnum(d.railMid);
      const rows = Math.max(1, d.panelRows || 1);
      const innerH = leafH - rt - rb - rm * (rows - 1);
      const panelH = innerH / rows;
      for (let r = 0; r < rows; r++) {
        const py = y + rt + r * (panelH + rm);
        parts.push(<rect key={`p${i}-${r}`} x={px(l.x + st)} y={px(py)}
          width={px(l.w - st * 2)} height={px(panelH)}
          fill="none" stroke={INK} strokeWidth="0.8" />);
      }
    }
    // Grooves are cut lines, not panels — a single stroke each.
    if (d.grooveCount > 0) {
      const n = d.grooveCount;
      const vertical = (d.grooveOrientation || 'Vertical') === 'Vertical';
      for (let g = 1; g <= n; g++) {
        const t = g / (n + 1);
        if (vertical) {
          const gx = l.x + l.w * t;
          parts.push(<line key={`g${i}-${g}`} x1={px(gx)} y1={px(y + leafH * 0.06)}
            x2={px(gx)} y2={px(y + leafH * 0.94)} stroke={BROWN} strokeWidth="1.4" />);
        } else {
          const gy = y + leafH * t;
          parts.push(<line key={`g${i}-${g}`} x1={px(l.x + l.w * 0.06)} y1={px(gy)}
            x2={px(l.x + l.w * 0.94)} y2={px(gy)} stroke={BROWN} strokeWidth="1.4" />);
        }
      }
    }
    // Vision panel / louver. The DOOR's own answer wins; where it has none, the
    // design's does — which is what makes "Louvered" a leaf style you pick
    // rather than something re-entered on every door.
    if ((!liteKind || liteKind === 'None') && design && design.liteKind) {
      liteKind = design.liteKind;
      if (!qnum(liteW)) liteW = design.liteW;
      if (!qnum(liteH)) liteH = design.liteH;
      if (!qnum(liteSill)) liteSill = design.liteSill;
    }
    if (liteKind && liteKind !== 'None') {
      let lw, lh, ly;
      if (liteKind === 'Full Lite') { lw = l.w * 0.72; lh = leafH * 0.82; ly = y + leafH * 0.09; }
      else if (liteKind === 'Half Lite') { lw = l.w * 0.72; lh = leafH * 0.4; ly = y + leafH * 0.08; }
      else if (liteKind === 'Narrow Lite') { lw = Math.min(l.w * 0.22, 152); lh = leafH * 0.62; ly = y + leafH * 0.16; }
      else if (liteKind === 'Louver') { lw = l.w * 0.7; lh = leafH * 0.3; ly = y + leafH * 0.62; }
      else { lw = qnum(liteW) || l.w * 0.5; lh = qnum(liteH) || leafH * 0.3; ly = y + leafH - (qnum(liteSill) || leafH * 0.5) - lh; }
      const lx = l.x + (l.w - lw) / 2;
      parts.push(<rect key={`l${i}`} x={px(lx)} y={px(ly)} width={px(lw)} height={px(lh)}
        fill="#eef4f7" stroke={INK} strokeWidth="1" />);
      if (liteKind === 'Louver') {
        const blades = 7;
        for (let b = 1; b < blades; b++) {
          const by = ly + (lh / blades) * b;
          parts.push(<line key={`lv${i}-${b}`} x1={px(lx)} y1={px(by)} x2={px(lx + lw)} y2={px(by)}
            stroke={INK} strokeWidth="0.6" />);
        }
      }
    }
    // Lever, at its real height above finished floor.
    const leverY = y + leafH - 1016;                 // 40" AFF, the usual
    const hingeLeft = !pair && (handing === 'RH' || handing === 'RHR');
    const lx2 = hingeLeft ? l.x + l.w - 70 : l.x + 70;
    if (leverY > y) {
      parts.push(<circle key={`h${i}`} cx={px(lx2)} cy={px(leverY)} r={Math.max(2, px(28))}
        fill={BROWN} opacity="0.85" />);
    }
    return parts;
  }

  return (
    <svg viewBox={`0 0 ${px(totalW)} ${px(totalH)}`} width="100%" height={H}
      style={{ maxWidth: px(totalW) }} role="img" aria-label={`Door elevation ${mark || ''}`}>
      {/* frame */}
      <rect x={px(ox)} y={px(oy)} width={px(S.frame.w)} height={px(S.frame.h)}
        fill="none" stroke={INK} strokeWidth="2" />
      <rect x={px(ox)} y={px(oy)} width={px(S.frame.w)} height={px(head)} fill={LINE} opacity="0.55" />
      <rect x={px(ox)} y={px(oy + head)} width={px(jamb)} height={px(S.frame.h - head)} fill={LINE} opacity="0.55" />
      <rect x={px(ox + S.frame.w - jamb)} y={px(oy + head)} width={px(jamb)} height={px(S.frame.h - head)} fill={LINE} opacity="0.55" />
      {leaves.map(leafFace)}
      {/* floor line */}
      <line x1={px(ox - 30)} y1={px(oy + S.frame.h)} x2={px(ox + S.frame.w + 30)} y2={px(oy + S.frame.h)}
        stroke={INK} strokeWidth="1.6" />
      {showDims && (
        <g fontSize={Math.max(8, px(90))} fill={INK} fontFamily="inherit">
          {/* leaf width, under the door */}
          <line x1={px(ox + jamb)} y1={px(oy + S.frame.h + 42)} x2={px(ox + S.frame.w - jamb)} y2={px(oy + S.frame.h + 42)}
            stroke={BROWN} strokeWidth="0.9" markerStart="url(#dtick)" markerEnd="url(#dtick)" />
          <text x={px(ox + S.frame.w / 2)} y={px(oy + S.frame.h + 78)} textAnchor="middle" fill={BROWN}>
            {fmtDim(pair ? leafW : S.leaf.w, system, { inchesOnly: true })}{pair ? ' ea.' : ''}
          </text>
          {/* leaf height, at the left */}
          <text x={px(ox - 34)} y={px(oy + S.frame.h / 2)} textAnchor="middle" fill={BROWN}
            transform={`rotate(-90 ${px(ox - 34)} ${px(oy + S.frame.h / 2)})`}>
            {fmtDim(S.leaf.h, system, { inchesOnly: true })}
          </text>
          {/* rough opening, over the head */}
          <text x={px(ox + S.frame.w / 2)} y={px(oy - 30)} textAnchor="middle" fill={INK} opacity="0.6">
            RO {fmtDim(S.ro.w, system, { inchesOnly: true })} × {fmtDim(S.ro.h, system, { inchesOnly: true })}
          </text>
        </g>
      )}
      <defs>
        <marker id="dtick" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto">
          <line x1="3" y1="0" x2="3" y2="6" stroke="var(--leon-brown)" strokeWidth="0.9" />
        </marker>
      </defs>
    </svg>
  );
}

// ── Plan view ─────────────────────────────────────────────────────────────
// Wall, frame, leaf and swing arc. The arc is what tells an installer the
// handing at a glance, which is why handing is picked from these rather than
// from four letters.
function DoorPlan({ sizes, handing, wallThickness, system, size }) {
  const S = sizes;
  if (!S || !(S.frame.w > 0)) return null;
  const H = size || 150;
  const wall = qnum(wallThickness) || 133.35;
  const span = S.ro.w * 1.5;
  const scale = H / span;
  const px = mm => mm * scale;
  const cx = span / 2;
  const openW = S.frame.w;
  const leafLen = (handing === 'PAIR' || handing === 'PAIR_UNEQUAL') ? openW / 2 : openW;
  const INK = 'var(--leon-black)';
  const BROWN = 'var(--leon-brown)';
  const wallY = span * 0.32;
  const leftEdge = cx - openW / 2, rightEdge = cx + openW / 2;
  const hingeRight = handing === 'RH' || handing === 'RHR';
  const swingDown = handing === 'RHR' || handing === 'LHR';

  function leaf(hingeX, dir, key) {
    const endX = hingeX + dir * leafLen;
    const endY = swingDown ? wallY + wall / 2 + leafLen : wallY + wall / 2 - leafLen;
    return (
      <g key={key}>
        <line x1={px(hingeX)} y1={px(wallY + wall / 2)} x2={px(hingeX)} y2={px(endY)}
          stroke={INK} strokeWidth="2.4" />
        <path d={`M ${px(endX)} ${px(wallY + wall / 2)} A ${px(leafLen)} ${px(leafLen)} 0 0 ${(dir > 0) === swingDown ? 1 : 0} ${px(hingeX)} ${px(endY)}`}
          fill="none" stroke={BROWN} strokeWidth="1" strokeDasharray="3 3" />
      </g>
    );
  }
  return (
    <svg viewBox={`0 0 ${px(span)} ${px(span * 0.8)}`} width="100%" height={H} role="img" aria-label="Door plan">
      <rect x="0" y={px(wallY)} width={px(leftEdge)} height={px(wall)} fill="var(--leon-line)" stroke={INK} strokeWidth="0.8" />
      <rect x={px(rightEdge)} y={px(wallY)} width={px(span - rightEdge)} height={px(wall)} fill="var(--leon-line)" stroke={INK} strokeWidth="0.8" />
      {(handing === 'PAIR' || handing === 'PAIR_UNEQUAL')
        ? [leaf(leftEdge, 1, 'a'), leaf(rightEdge, -1, 'b')]
        : [leaf(hingeRight ? rightEdge : leftEdge, hingeRight ? -1 : 1, 'a')]}
      <text x={px(cx)} y={px(wallY - 18)} textAnchor="middle" fontSize={Math.max(8, px(70))} fill={INK} opacity="0.6">
        {fmtDim(S.ro.w, system, { inchesOnly: true })} RO
      </text>
    </svg>
  );
}

// ── Shared resolution helpers ─────────────────────────────────────────────
// Every screen in this module asks the same question — "what is this door,
// really?" — so it is answered once here: type first, then the door's own
// overrides, then the rule that sizes it.
function doorCtxLib(ctx) { return ctx.doorLibrary || makeDoorLibrary(); }
function doorTypesFor(ctx, project) {
  const lib = doorCtxLib(ctx);
  return [...(project ? project.doorTypes || [] : []), ...(lib.types || [])];
}
function doorResolve(ctx, project, door) {
  const type = doorTypesFor(ctx, project).find(t => t.id === door.typeId) || null;
  const r = resolveDoor(door, type);
  const lib = doorCtxLib(ctx);
  const frame = (lib.frames || []).find(f => f.id === r.frameId) || null;
  const rule = (lib.rules || []).find(x => x.id === (r.openingRuleId || (frame && frame.openingRuleId))) || null;
  const designRaw = (lib.designs || []).find(x => x.id === r.designId) || null;
  // The library profile is the starting point; the DOOR may differ from it in
  // material, either size or the joint, so the resolved trim is the profile
  // with this door's overrides on top. Null on a door field means "follow the
  // profile", which is why each is tested rather than spread blindly.
  // The DESIGN is the standard; a door that carries a different number of
  // panels or grooves, or a different moulding width, overrides it here. Each
  // is tested rather than spread, because null means "follow the design".
  const designBase = designRaw;
  const dOv = {};
  if (qnum(r.panelRows) > 0 || r.panelRows === 0) dOv.panelRows = qnum(r.panelRows);
  if (qnum(r.panelCols) > 0 || r.panelCols === 0) dOv.panelCols = qnum(r.panelCols);
  if (qnum(r.panelProfile) > 0) dOv.panelProfile = qnum(r.panelProfile);
  if (qnum(r.grooveCount) > 0 || r.grooveCount === 0) dOv.grooveCount = qnum(r.grooveCount);
  if (qnum(r.grooveWidth) > 0) dOv.grooveWidth = qnum(r.grooveWidth);
  if (r.grooveOrientation) dOv.grooveOrientation = r.grooveOrientation;
  const design = designBase ? { ...designBase, ...dOv }
    : (Object.keys(dOv).length ? { ...makeDoorDesign({ name: 'Custom' }), ...dOv } : null);

  const trimBase = (lib.trims || []).find(x => x.id === r.trimProfileId) || null;
  const trimOv = {};
  if (r.trimFinishRef) trimOv.finishRef = r.trimFinishRef;
  if (qnum(r.trimWidth) > 0) trimOv.width = qnum(r.trimWidth);
  if (qnum(r.trimThickness) > 0) trimOv.thickness = qnum(r.trimThickness);
  if (r.trimJoint) trimOv.joint = r.trimJoint;
  const trim = trimBase
    ? { ...trimBase, ...trimOv }
    : (Object.keys(trimOv).length
        ? { id: null, name: r.trimType || 'Trim', design: r.trimType || 'Square / Flat',
            width: qnum(r.trimSize) || 63.5, thickness: 19, reveal: 6.35,
            joint: 'Mitered', ...trimOv }
        : null);
  const set = (lib.hardwareSets || []).find(x => x.id === r.hardwareSetId) || null;
  const v = validateDoor(r, rule);
  return { type, resolved: r, frame, rule, design, trim, hardwareSet: set, sizes: v.sizes, issues: v.issues, ok: v.ok };
}


// ============================================================================
// SHEET OUTPUT
// The door module could hold everything about a door and then not issue it. The
// elevations elsewhere in here are PREVIEWS — each fits itself to a pixel
// height, so there is no scale to measure against and no sheet around it.
//
// This draws the same doors at a STATED scale on real paper. Nothing is
// re-derived: it reads `doorResolve`, the same function the schedule and the
// designer read, so a sheet and the schedule cannot disagree.
//
// A door is drawn as THREE nested outlines — rough opening, frame, leaf —
// because that is how the 55 India schedule reads and why: three trades read
// three different dimensions off it. The framer sets out the RO, the shop
// builds the frame, the door is the leaf.
// ============================================================================
const DOOR_INK = '#161311';

// A dimension string, drawn in PAPER millimetres rather than model ones. A
// dimension belongs to the sheet, not to the door: let the text scale with the
// drawing and at 1:50 it is unreadable, which is the commonest fault in a
// generated drawing.
function DoorDim({ x1, x2, y, text, vertical, size, tone }) {
  const tick = 1.3, fs = size || 2.4;
  const ink = tone || DOOR_INK;
  if (vertical) {
    const a = Math.min(x1, x2), b = Math.max(x1, x2);
    return (
      <g stroke={ink} strokeWidth="0.18" fill="none">
        <line x1={y} y1={a} x2={y} y2={b} />
        <line x1={y - tick} y1={a} x2={y + tick} y2={a} />
        <line x1={y - tick} y1={b} x2={y + tick} y2={b} />
        <text x={y - 1.1} y={(a + b) / 2} fontSize={fs} fill={ink} stroke="none"
          fontFamily={DOOR_FONT} letterSpacing="0.2"
          textAnchor="middle" dominantBaseline="middle"
          transform={`rotate(-90 ${y - 1.1} ${(a + b) / 2})`}>{text}</text>
      </g>
    );
  }
  const a = Math.min(x1, x2), b = Math.max(x1, x2);
  return (
    <g stroke={ink} strokeWidth="0.18" fill="none">
      <line x1={a} y1={y} x2={b} y2={y} />
      <line x1={a} y1={y - tick} x2={a} y2={y + tick} />
      <line x1={b} y1={y - tick} x2={b} y2={y + tick} />
      <text x={(a + b) / 2} y={y - 1.1} fontSize={fs} fill={ink} stroke="none"
        fontFamily={DOOR_FONT} letterSpacing="0.2" textAnchor="middle">{text}</text>
    </g>
  );
}

// One door at scale. `ox`/`oy` are the paper-millimetre coordinates of the
// bottom-left of its ROUGH OPENING, so a caller can lay several across a sheet.
function DoorSheetElevation({ res, door, denom, ox, oy, system }) {
  const S = res.sizes;
  if (!S || !(S.leaf.w > 0) || !(S.leaf.h > 0)) return null;
  const s = mm => mm / denom;
  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
  const design = res.design;
  const pair = res.resolved.handing === 'PAIR' || res.resolved.handing === 'PAIR_UNEQUAL';

  const roW = s(S.ro.w), roH = s(S.ro.h);
  const frW = s(S.frame.w), frH = s(S.frame.h);
  const lfW = s(S.leaf.w * (pair ? 2 : 1)), lfH = s(S.leaf.h);
  // Everything is centred on the opening and sits on one floor line.
  const cx = ox + roW / 2;
  const floor = oy;
  const box = (w, h) => ({ x: cx - w / 2, y: floor - h, w, h });
  const ro = box(roW, roH), fr = box(frW, frH), lf = box(lfW, lfH);

  const leaves = pair ? [
    { x: lf.x, w: lf.w / 2 }, { x: lf.x + lf.w / 2, w: lf.w / 2 },
  ] : [{ x: lf.x, w: lf.w }];

  return (
    <g>
      {/* Rough opening — dashed, because it is a hole in the wall and not a
          thing anyone builds. */}
      <rect x={ro.x} y={ro.y} width={ro.w} height={ro.h} fill="none"
        stroke={DOOR_INK} strokeWidth="0.22" strokeDasharray="1.6 1.2" />
      {/* Frame */}
      <rect x={fr.x} y={fr.y} width={fr.w} height={fr.h} fill="#ffffff"
        stroke={DOOR_INK} strokeWidth="0.35" />
      {/* Leaf, or two of them */}
      {!openingOnly && leaves.map((l, i) => (
        <g key={i}>
          <rect x={l.x} y={lf.y} width={l.w} height={lf.h} fill="#fdfcfa"
            stroke={DOOR_INK} strokeWidth="0.3" />
          {/* The design, at scale: a shaker's rails and stiles, or grooves. */}
          {design && design.panelRows > 0 && (() => {
            const st = s(qnum(design.stile)), rt = s(qnum(design.railTop)), rb = s(qnum(design.railBottom));
            const rows = design.panelRows, mid = s(qnum(design.railMid) || 0);
            const inW = l.w - st * 2;
            const avail = lf.h - rt - rb - mid * (rows - 1);
            const ph = avail / rows;
            return Array.from({ length: rows }, (_, r) => (
              <rect key={r} x={l.x + st} y={lf.y + rt + r * (ph + mid)} width={inW} height={ph}
                fill="none" stroke={DOOR_INK} strokeWidth="0.18" />
            ));
          })()}
          {design && design.grooveCount > 0 && Array.from({ length: design.grooveCount }, (_, g) => {
            const horiz = design.grooveOrientation === 'Horizontal';
            const t = (g + 1) / (design.grooveCount + 1);
            return horiz
              ? <line key={g} x1={l.x + l.w * 0.06} y1={lf.y + lf.h * t} x2={l.x + l.w * 0.94} y2={lf.y + lf.h * t}
                  stroke={DOOR_INK} strokeWidth="0.18" />
              : <line key={g} x1={l.x + l.w * t} y1={lf.y + lf.h * 0.05} x2={l.x + l.w * t} y2={lf.y + lf.h * 0.95}
                  stroke={DOOR_INK} strokeWidth="0.18" />;
          })}
          {/* A vision panel or louver. The DOOR's own answer wins; where it has
              none the DESIGN's does — the same precedence DoorElevation uses,
              which is what makes "Louvered" a leaf style rather than something
              re-entered per door. Without this a louvered leaf drew as a flat
              slab on the sheet while drawing correctly everywhere else. */}
          {(() => {
            const lk = res.resolved.liteKind && res.resolved.liteKind !== 'None'
              ? res.resolved.liteKind : (design && design.liteKind);
            if (!lk || lk === 'None') return null;
            let lw, lh, ly;
            if (lk === 'Full Lite') { lw = l.w * 0.72; lh = lf.h * 0.82; ly = lf.y + lf.h * 0.09; }
            else if (lk === 'Half Lite') { lw = l.w * 0.72; lh = lf.h * 0.4; ly = lf.y + lf.h * 0.08; }
            else if (lk === 'Narrow Lite') { lw = Math.min(l.w * 0.22, s(152)); lh = lf.h * 0.62; ly = lf.y + lf.h * 0.16; }
            else if (lk === 'Louver') { lw = l.w * 0.7; lh = lf.h * 0.3; ly = lf.y + lf.h * 0.62; }
            else { lw = l.w * 0.5; lh = lf.h * 0.3; ly = lf.y + lf.h * 0.5; }
            const lx = l.x + (l.w - lw) / 2;
            const blades = 7;
            return (
              <g>
                <rect x={lx} y={ly} width={lw} height={lh} fill="#eef4f7"
                  stroke={DOOR_INK} strokeWidth="0.22" />
                {lk === 'Louver' && Array.from({ length: blades - 1 }, (_, b) => (
                  <line key={b} x1={lx} y1={ly + (lh / blades) * (b + 1)}
                    x2={lx + lw} y2={ly + (lh / blades) * (b + 1)}
                    stroke={DOOR_INK} strokeWidth="0.14" />
                ))}
              </g>
            );
          })()}
          {/* The lever, at its real height above finished floor. */}
          <circle cx={l.x + (i === 0 && !pair && /L/.test(res.resolved.handing || '') ? l.w * 0.12 : l.w * 0.88)}
            cy={floor - s(1020)} r="0.5" fill={DOOR_INK} />
        </g>
      ))}
      {/* Floor line, run past the opening on both sides. */}
      <line x1={ro.x - 3} y1={floor} x2={ro.x + ro.w + 3} y2={floor}
        stroke={DOOR_INK} strokeWidth="0.5" />

      {/* Three widths, stacked below: leaf, frame, rough opening — the order a
          drawing is read in, nearest the thing it measures first. */}
      <DoorDim x1={lf.x} x2={lf.x + lf.w} y={floor + 5} text={pair ? `2 @ ${fmt(S.leaf.w)}` : fmt(S.leaf.w)} />
      <DoorDim x1={fr.x} x2={fr.x + fr.w} y={floor + 9.5} text={fmt(S.frame.w)} />
      <DoorDim x1={ro.x} x2={ro.x + ro.w} y={floor + 14} text={fmt(S.ro.w)} />
      {/* And three heights up the left. */}
      <DoorDim vertical x1={lf.y} x2={floor} y={ro.x - 4} text={fmt(S.leaf.h)} />
      <DoorDim vertical x1={fr.y} x2={floor} y={ro.x - 8} text={fmt(S.frame.h)} />
      <DoorDim vertical x1={ro.y} x2={floor} y={ro.x - 12} text={fmt(S.ro.h)} />

      {/* What this door IS, under its dimensions. */}
      <text x={cx} y={floor + 20} fontSize="3.2" fontWeight="bold" fill={DOOR_INK} textAnchor="middle">
        {res.resolved.mark || '—'}
      </text>
      <text x={cx} y={floor + 24} fontSize="2.2" fill={DOOR_INK} textAnchor="middle" opacity="0.65">
        {[res.resolved.operation, res.resolved.handing, res.resolved.fireRating && res.resolved.fireRating !== 'None'
          ? `${res.resolved.fireRating}${res.resolved.ratingState === 'Requested' ? ' REQ' : ''}` : null]
          .filter(Boolean).join(' · ')}
      </text>
      <text x={cx} y={floor + 27.5} fontSize="2.2" fill={DOOR_INK} textAnchor="middle" opacity="0.5">
        {[res.resolved.location || res.resolved.room, res.resolved.hwSetCode].filter(Boolean).join(' · ')}
      </text>
    </g>
  );
}


// ============================================================================
// THE SHOP DRAWING
// One page per DOOR, laid out the way a door shop drawing actually is: the
// front elevation with its three sets of dimensions, a horizontal section
// through the jamb showing the wall, the frame and the trim, a vertical section
// through the head and sill, a plan swing diagram, and a data panel down the
// right with the logo and everything the fabricator is told.
//
// Line weight carries meaning here, as it does on a real drawing: CUT material
// is heavy and poched, things seen in elevation are medium, and detail and
// dimensions are thin. A drawing where everything is one weight is the thing
// that reads as immature.
// ============================================================================
const DW = { cut: 0.6, outline: 0.35, detail: 0.2, thin: 0.14 };
// The sheet is branded: LEON's own face, LEON's own palette. The font is the
// one the app ships (`fonts/CenturyGothicLeon.ttf`, declared in styles.css), so
// SVG text picks it up on screen and in print without embedding anything.
const DOOR_FONT = "'Century Gothic Leon', 'Century Gothic', Questrial, sans-serif";
const DOOR_BROWN = '#6b4a34';
const DOOR_CREAM = '#f7f3ee';
const DOOR_LINE = '#e5ded4';
const DOOR_RED = '#b83b3b';
// The dimension a label belongs to, said in words rather than left to be
// guessed — the client's own set writes "40\" ROUGH OPENING" beside the line,
// and a bare figure on a drawing is how the wrong one gets built to.
const DOOR_POCHE = 'rgba(22,19,17,0.13)';   // cut material
const DOOR_GLASS = '#eef4f7';

// Cross-hatch for a cut wall, drawn once and referenced — a pattern beats
// hundreds of individual lines in a 6-page document.
function DoorSheetDefs() {
  return (
    <defs>
      <pattern id="dHatch" width="1.6" height="1.6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
        <line x1="0" y1="0" x2="0" y2="1.6" stroke={DOOR_INK} strokeWidth="0.12" opacity="0.5" />
      </pattern>
      <pattern id="dCore" width="1.1" height="1.1" patternUnits="userSpaceOnUse">
        <circle cx="0.55" cy="0.55" r="0.18" fill={DOOR_INK} opacity="0.28" />
      </pattern>
      {/* Finger-joint stock: staggered interlocking fingers, which is what the
          core of a wrapped jamb and a door stile actually is. */}
      <pattern id="dFinger" width="3.2" height="2.2" patternUnits="userSpaceOnUse">
        <rect width="3.2" height="2.2" fill="#fdfaf4" />
        <path d="M0 0 L0.8 0 L0.8 1.1 L1.6 1.1 L1.6 0 L2.4 0 L2.4 1.1 L3.2 1.1"
          fill="none" stroke={DOOR_INK} strokeWidth="0.1" opacity="0.45" />
        <path d="M0 2.2 L0.8 2.2 L0.8 1.1 L1.6 1.1 L1.6 2.2 L2.4 2.2 L2.4 1.1 L3.2 1.1"
          fill="none" stroke={DOOR_INK} strokeWidth="0.1" opacity="0.45" />
      </pattern>
      {/* Timber, cut: grain lines with the odd figure. Every wooden member in
          a joinery section is drawn this way — it is what separates a jamb
          from a stud at a glance, and the reference detail uses nothing else. */}
      <pattern id="dGrain" width="6" height="3.4" patternUnits="userSpaceOnUse">
        <rect width="6" height="3.4" fill="#fdf6e9" />
        <path d="M0 0.5 Q1.5 0.2 3 0.6 T6 0.4" fill="none" stroke={DOOR_INK} strokeWidth="0.09" opacity="0.5" />
        <path d="M0 1.5 Q2 1.9 3.6 1.3 T6 1.6" fill="none" stroke={DOOR_INK} strokeWidth="0.09" opacity="0.42" />
        <path d="M0 2.6 Q1.8 2.2 3.2 2.8 T6 2.5" fill="none" stroke={DOOR_INK} strokeWidth="0.09" opacity="0.5" />
      </pattern>
      {/* Honeycomb, for a leaf cut through its core. */}
      <pattern id="dHoney" width="2.4" height="2.1" patternUnits="userSpaceOnUse">
        <path d="M0.6 0 L1.8 0 L2.4 1.05 L1.8 2.1 L0.6 2.1 L0 1.05 Z"
          fill="none" stroke={DOOR_INK} strokeWidth="0.09" opacity="0.4" />
      </pattern>
    </defs>
  );
}

// A titled panel with a hairline box — every part of the page sits in one, so
// the sheet reads as a set of views rather than a scatter of drawings.
function DoorViewBox({ x, y, w, h, title, scaleNote, children }) {
  return (
    <g>
      <rect x={x} y={y} width={w} height={h} fill="none" stroke={DOOR_INK}
        strokeWidth={DW.thin} opacity="0.45" />
      <text x={x + 1.5} y={y + 3.6} fontSize="2.6" fontWeight="bold" fill={DOOR_BROWN}
        fontFamily={DOOR_FONT} letterSpacing="0.9">{title}</text>
      {scaleNote && (
        <text x={x + w - 1.5} y={y + 3.6} fontSize="2" fill={DOOR_INK} opacity="0.5"
          fontFamily={DOOR_FONT} textAnchor="end">{scaleNote}</text>
      )}
      <line x1={x} y1={y + 5} x2={x + w} y2={y + 5} stroke={DOOR_INK} strokeWidth={DW.thin} opacity="0.35" />
      {children}
    </g>
  );
}

// ── Front elevation ────────────────────────────────────────────────────────
// The leaf as it is seen, inside its frame, inside its rough opening, with the
// three sets of dimensions three trades read and the HANDLE HEIGHT called out —
// the installer sets the lock out from that figure, so it belongs on the sheet.
function DoorViewElevation({ res, x, y, w, h, denom, system, openingOnly }) {
  const S = res.sizes, d = res.resolved, design = res.design;
  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
  const s = mm => mm / denom;
  const pair = d.handing === 'PAIR' || d.handing === 'PAIR_UNEQUAL';
  const leafW = S.leaf.w * (pair ? 2 : 1);
  // CENTRED IN THE BOX, both ways. It used to sit a fixed 18 mm off the bottom
  // and 4 mm right of centre, so on a taller box the drawing hugged the floor
  // with dead space above it. The drawn group is the rough opening plus its own
  // dimension bands — 12 mm above, 18 mm below — and that is what is centred.
  const bandTop = 12, bandBot = 23;   // three width bands at 5, 12 and 19
  const drawnH = s(S.ro.h) + bandTop + bandBot;
  const floor = y + (h - drawnH) / 2 + bandTop + s(S.ro.h);
  const cx = x + w / 2;
  const box = (bw, bh) => ({ x: cx - s(bw) / 2, y: floor - s(bh), w: s(bw), h: s(bh) });
  const ro = box(S.ro.w, S.ro.h), fr = box(S.frame.w, S.frame.h), lf = box(leafW, S.leaf.h);
  const hh = qnum(res.rule && res.rule.handleHeight) || 914.4;
  // The door's own backset wins over the rule's standard, which is what lets a
  // single opening take a lock supplied at something else.
  const backset = qnum(d.handleBackset) || qnum(res.rule && res.rule.handleBackset) || 60;
  const leaves = pair ? [{ x: lf.x, w: lf.w / 2 }, { x: lf.x + lf.w / 2, w: lf.w / 2 }]
                      : [{ x: lf.x, w: lf.w }];
  const lite = (d.liteKind && d.liteKind !== 'None') ? d.liteKind : (design && design.liteKind);

  return (
    <g>
      {/* NO WALL BEHIND THE ELEVATION. It was hatched on both sides and over
          the head, which reads as the door buried in a block rather than as an
          elevation of the door. An elevation shows the thing; the wall belongs
          in the sections. */}
      <rect x={ro.x} y={ro.y} width={ro.w} height={ro.h} fill="#fff"
        stroke={DOOR_INK} strokeWidth={DW.detail} strokeDasharray="1.6 1.2" />
      {/* The trim, drawn round the frame — it is what the client sees and it
          was only ever in the sections. */}
      {!openingOnly && (() => {
        const trimW = qnum(res.trim && res.trim.width) || qnum(d.trimSize) || 0;
        const trimT = qnum(res.trim && res.trim.thickness) || 19;
        const trimName = String((res.trim && (res.trim.design || res.trim.name)) || d.trimType || 'TRIM').toUpperCase();
        const tw2 = s(trimW);
        if (!(tw2 > 0)) return null;
        return (
          <g>
            <rect x={fr.x - tw2} y={fr.y - tw2} width={fr.w + tw2 * 2} height={fr.h + tw2}
              fill="none" stroke={DOOR_INK} strokeWidth={DW.detail} />
            {/* HOW THE CORNERS ARE JOINED, drawn as they are cut. A mitre is a
                45° line across the corner; a butt joint is the head running
                square over the legs. Both were being drawn as the square line,
                so a mitred casing and a butted one looked identical. */}
            {(() => {
              const mitred = !/butt/i.test(String((res.trim && res.trim.joint) || 'Mitered'));
              const L = fr.x - tw2, R = fr.x + fr.w + tw2, T = fr.y - tw2;
              return (
                <g stroke={DOOR_INK} strokeWidth={DW.detail} fill="none">
                  {mitred ? (
                    <>
                      {/* OUTER corner to INNER corner. It was drawn the other
                          way round, which is the mitre reversed — the cut runs
                          from the outside of the casing into the opening. */}
                      <line x1={L} y1={T} x2={L + tw2} y2={T + tw2} />
                      <line x1={R} y1={T} x2={R - tw2} y2={T + tw2} />
                    </>
                  ) : (
                    <>
                      <line x1={L} y1={T + tw2} x2={L + tw2} y2={T + tw2} />
                      <line x1={R} y1={T + tw2} x2={R - tw2} y2={T + tw2} />
                      <line x1={L + tw2} y1={T + tw2} x2={L + tw2} y2={fr.y + fr.h} />
                      <line x1={R - tw2} y1={T + tw2} x2={R - tw2} y2={fr.y + fr.h} />
                    </>
                  )}
                </g>
              );
            })()}
            {/* The trim's own SIZE, dimensioned rather than labelled — it is
                material being bought and cut, so its face width belongs on a
                dimension line like every other width on the sheet. The
                thickness is called out beside it because a casing is a
                two-number product. */}
            {/* CLAMPED to the view's own top. The scale ladder is coarse, so
                on some doors this dimension still landed above the box and ran
                through the view's title. A clamp is honest here — the leaders
                still point at the trim, they just do not climb out of the
                frame to do it. */}
            <DoorDim x1={fr.x - tw2} x2={fr.x} y={Math.max(y + 2.6, fr.y - tw2 - 2)}
              text={fmt(trimW)} size={1.7} />
            <DoorDim x1={fr.x + fr.w} x2={fr.x + fr.w + tw2} y={Math.max(y + 2.6, fr.y - tw2 - 2)}
              text={fmt(trimW)} size={1.7} />
            <DoorDim vertical x1={fr.y - tw2} x2={fr.y} y={fr.x + fr.w + tw2 + 2.5}
              text={fmt(trimW)} size={1.7} />
            {/* NO TRIM NAME HERE. The elevation carries its SIZE, which is
                what gets built to; what the profile is called is in the
                Finishes & Selections footer, where the rest of what was chosen
                already lives. Saying it twice crowded the elevation. */}
          </g>
        );
      })()}
      <rect x={fr.x} y={fr.y} width={fr.w} height={fr.h} fill="#ffffff"
        stroke={DOOR_INK} strokeWidth={DW.outline} />

      {!openingOnly && leaves.map((l, i) => (
        <g key={i}>
          <rect x={l.x} y={lf.y} width={l.w} height={lf.h} fill="#fdfcfa"
            stroke={DOOR_INK} strokeWidth={DW.outline} />
          {design && design.panelRows > 0 && (() => {
            // ROWS AND COLUMNS. `panelCols` has been on the design all along and
            // was never drawn, so a 2×2 door came out as two full-width panels.
            // Each panel is drawn twice: the opening, and the moulding round it
            // at its own face width — a panelled door is a count AND a profile.
            const st = s(qnum(design.stile)), rt = s(qnum(design.railTop)), rb = s(qnum(design.railBottom));
            const mid = s(qnum(design.railMid) || 0);
            const rows = Math.max(1, qnum(design.panelRows));
            const cols = Math.max(1, qnum(design.panelCols) || 1);
            const prof = s(qnum(design.panelProfile) || 0);
            const fieldW = l.w - st * 2;
            const pw = (fieldW - mid * (cols - 1)) / cols;
            const ph = (lf.h - rt - rb - mid * (rows - 1)) / rows;
            const out = [];
            for (let rr = 0; rr < rows; rr++) for (let cc = 0; cc < cols; cc++) {
              const px = l.x + st + cc * (pw + mid), py = lf.y + rt + rr * (ph + mid);
              out.push(
                <g key={`${rr}-${cc}`}>
                  <rect x={px} y={py} width={pw} height={ph}
                    fill="none" stroke={DOOR_INK} strokeWidth={DW.detail} />
                  {prof > 0.2 && pw > prof * 2.4 && ph > prof * 2.4 && (
                    <rect x={px + prof} y={py + prof} width={pw - prof * 2} height={ph - prof * 2}
                      fill="none" stroke={DOOR_INK} strokeWidth={DW.thin} />
                  )}
                </g>
              );
            }
            return out;
          })()}
          {design && design.grooveCount > 0 && Array.from({ length: design.grooveCount }, (_, g) => {
            const t = (g + 1) / (design.grooveCount + 1);
            return design.grooveOrientation === 'Horizontal'
              ? <line key={g} x1={l.x + l.w * 0.06} y1={lf.y + lf.h * t} x2={l.x + l.w * 0.94} y2={lf.y + lf.h * t}
                  stroke={DOOR_INK} strokeWidth={DW.detail} />
              : <line key={g} x1={l.x + l.w * t} y1={lf.y + lf.h * 0.05} x2={l.x + l.w * t} y2={lf.y + lf.h * 0.95}
                  stroke={DOOR_INK} strokeWidth={DW.detail} />;
          })}
          {lite && lite !== 'None' && (() => {
            // A given size WINS over the proportional default: a louver the
            // shop has to cut needs the real figure, not a fraction of a leaf.
            const gw = qnum(d.liteW), gh = qnum(d.liteH), gs = qnum(d.liteSill);
            let lw, lh, ly;
            if (gw > 0 && gh > 0) {
              lw = s(gw); lh = s(gh);
              ly = gs > 0 ? lf.y + lf.h - s(gs) - lh : lf.y + (lf.h - lh) / 2;
            }
            else if (lite === 'Full Lite') { lw = l.w * 0.72; lh = lf.h * 0.82; ly = lf.y + lf.h * 0.09; }
            else if (lite === 'Half Lite') { lw = l.w * 0.72; lh = lf.h * 0.4; ly = lf.y + lf.h * 0.08; }
            else if (lite === 'Narrow Lite') { lw = Math.min(l.w * 0.22, s(152)); lh = lf.h * 0.62; ly = lf.y + lf.h * 0.16; }
            else if (lite === 'Louver') { lw = l.w * 0.7; lh = lf.h * 0.3; ly = lf.y + lf.h * 0.62; }
            else { lw = l.w * 0.5; lh = lf.h * 0.3; ly = lf.y + lf.h * 0.5; }
            const lx = l.x + (l.w - lw) / 2;
            return (
              <g>
                <rect x={lx} y={ly} width={lw} height={lh} fill={DOOR_GLASS}
                  stroke={DOOR_INK} strokeWidth={DW.detail} />
                {lite === 'Louver' && Array.from({ length: 6 }, (_, b) => (
                  <line key={b} x1={lx} y1={ly + (lh / 7) * (b + 1)} x2={lx + lw} y2={ly + (lh / 7) * (b + 1)}
                    stroke={DOOR_INK} strokeWidth={DW.thin} />
                ))}
                {i === 0 && qnum(d.liteW) > 0 && qnum(d.liteH) > 0 && (
                  <g>
                    <DoorDim x1={lx} x2={lx + lw} y={ly - 1.5} text={fmt(qnum(d.liteW))} size={1.7} />
                    <DoorDim vertical x1={ly} x2={ly + lh} y={lx - 2.5} text={fmt(qnum(d.liteH))} size={1.7} />
                    <text x={lx + lw / 2} y={ly + lh + 3} fontSize="1.6" fill={DOOR_INK}
                      fontFamily={DOOR_FONT} textAnchor="middle" opacity="0.65">
                      {String(lite).toUpperCase()}
                    </text>
                  </g>
                )}
              </g>
            );
          })()}
          {/* The DESIGN's own dimensions. A shaker is a stile width and three
              rail heights; a groove is a distance in from the edge and a width.
              Those numbers are already on the design record and drive the
              drawing — a shop that can see the door but not the setting-out
              still has to ring up and ask. Only on the first leaf of a pair:
              both are identical and dimensioning twice is noise. */}
          {i === 0 && design && design.panelRows > 0 && (() => {
            const st = s(qnum(design.stile)), rt = s(qnum(design.railTop));
            const rb = s(qnum(design.railBottom)), mid = s(qnum(design.railMid) || 0);
            return (
              <g>
                {/* INSIDE THE LEAF. These sat above it and to its right, in the
                    same band as the frame, the rough opening and the trim — so
                    the setting-out ran through the dimensions of everything
                    around the door. A leaf's own dimensions belong on the leaf. */}
                <DoorDim x1={l.x} x2={l.x + st} y={lf.y + rt + 4} text={fmt(qnum(design.stile))} size={1.6} />
                <DoorDim x1={l.x + l.w - st} x2={l.x + l.w} y={lf.y + rt + 4}
                  text={fmt(qnum(design.stile))} size={1.6} />
                <DoorDim vertical x1={lf.y} x2={lf.y + rt} y={l.x + st + 4}
                  text={fmt(qnum(design.railTop))} size={1.6} />
                <DoorDim vertical x1={lf.y + lf.h - rb} x2={lf.y + lf.h} y={l.x + st + 4}
                  text={fmt(qnum(design.railBottom))} size={1.6} />
                {mid > 0 && design.panelRows > 1 && (
                  <text x={l.x + l.w / 2} y={lf.y + lf.h / 2} fontSize="1.6" fill={DOOR_INK}
                    fontFamily={DOOR_FONT} textAnchor="middle" opacity="0.7">
                    MID RAIL {fmt(qnum(design.railMid))}
                  </text>
                )}
              </g>
            );
          })()}
          {i === 0 && design && design.grooveCount > 0 && (() => {
            const horiz = design.grooveOrientation === 'Horizontal';
            const gw = qnum(design.grooveWidth);
            return (
              <g>
                {/* Inside the leaf, for the same reason. */}
                {Array.from({ length: design.grooveCount }, (_, g) => {
                  const t = (g + 1) / (design.grooveCount + 1);
                  return horiz
                    ? <DoorDim key={g} vertical x1={lf.y} x2={lf.y + lf.h * t} y={l.x + 5 + g * 4}
                        text={fmt(qnum(S.leaf.h) * t)} size={1.5} />
                    : <DoorDim key={g} x1={l.x} x2={l.x + l.w * t} y={lf.y + 5 + g * 4}
                        text={fmt(qnum(S.leaf.w) * t)} size={1.5} />;
                })}
                <text x={l.x + l.w / 2} y={lf.y + lf.h - 3} fontSize="1.6"
                  fill={DOOR_INK} fontFamily={DOOR_FONT} textAnchor="middle" opacity="0.7">
                  {design.grooveCount} × {fmt(gw)} {horiz ? 'HORIZONTAL' : 'VERTICAL'} GROOVE
                </text>
              </g>
            );
          })()}
          {/* Lever and rose at the REAL height and the REAL backset. It used
              to sit at a fraction of the leaf width, which looks about right
              and is not a position anyone can machine to: a handle is located
              by its height off the floor and the distance in from the lock
              edge, and the leaf is bored to both. */}
          {(() => {
            // Which edge is the lock edge: the far edge from the hinges. On a
            // pair the two leaves meet in the middle, so both lock edges are
            // the inner ones.
            const lockRight = leaves.length === 2 ? (i === 0) : !/L/.test(d.handing || '');
            const bs = s(backset);
            const hx = lockRight ? l.x + l.w - bs : l.x + bs;
            const hy = floor - s(hh);
            const dir = lockRight ? -1 : 1;
            return (
              <g>
                <circle cx={hx} cy={hy} r="0.9" fill="#fff" stroke={DOOR_INK} strokeWidth={DW.detail} />
                <line x1={hx} y1={hy} x2={hx + dir * 3.2} y2={hy}
                  stroke={DOOR_INK} strokeWidth={DW.outline} />
                {i === 0 && backset > 0 && (
                  <DoorDim x1={lockRight ? l.x + l.w : l.x} x2={hx} y={hy - 3}
                    text={`${fmt(backset)} BACKSET`} size={1.7} tone={DOOR_BROWN} />
                )}
              </g>
            );
          })()}
        </g>
      ))}

      <line x1={ro.x - 6} y1={floor} x2={ro.x + ro.w + 6} y2={floor} stroke={DOOR_INK} strokeWidth={DW.cut} />
      <text x={ro.x + ro.w + 6.5} y={floor + 2.4} fontSize="1.9" fill={DOOR_INK} opacity="0.55">FFL</text>

      {/* Widths below, heights up the left, and the handle height called out
          against the leaf where the installer reads it. */}
      {/* Every dimension says WHAT it is, the way the client's own sheet
          writes them — "40\" ROUGH OPENING" rather than a bare 40. A figure
          with no noun is how the wrong one gets built to. */}
      {!openingOnly && (
        <DoorDim x1={lf.x} x2={lf.x + lf.w} y={floor + 5}
          text={`${pair ? `2 @ ${fmt(S.leaf.w)}` : fmt(S.leaf.w)} LEAF WIDTH`} size={2.1} />
      )}
      {/* The bands stand further off, and further apart. At 5 mm they sat on
          the trim and on each other, so three widths read as one tangle of
          lines. The trim's own face width is what they have to clear. */}
      <DoorDim x1={fr.x} x2={fr.x + fr.w} y={floor + 12}
        text={`${fmt(S.frame.w)} FRAME WIDTH`} size={2.1} />
      <DoorDim x1={ro.x} x2={ro.x + ro.w} y={floor + 19}
        text={`${fmt(S.ro.w)} ROUGH OPENING`} size={2.1} tone={DOOR_BROWN} />
      {/* The three height bands, spread as far as the box allows. At 1:10 the
          drawing is wide enough that a fixed 18 mm offset put the outermost
          one off the left edge of the sheet — so the spacing is scaled to the
          room actually left of the opening, and compresses rather than
          escaping the paper. */}
      {(() => {
        const room = Math.max(6, ro.x - x - 3);
        const k = Math.min(1, room / 18);
        const at = n => ro.x - n * k;
        return (
          <g>
            {!openingOnly && (
              <DoorDim vertical x1={lf.y} x2={floor} y={at(5)}
                text={`${fmt(S.leaf.h)} LEAF HEIGHT`} size={2.1} />
            )}
            <DoorDim vertical x1={fr.y} x2={floor} y={at(11)}
              text={`${fmt(S.frame.h)} FRAME HEIGHT`} size={2.1} />
            <DoorDim vertical x1={ro.y} x2={floor} y={at(18)}
              text={`${fmt(S.ro.h)} ROUGH OPENING HEIGHT`} size={2.1} tone={DOOR_BROWN} />
          </g>
        );
      })()}
      {/* OUTSIDE the trim, not between the leaf and it. At lf.x + lf.w + 4 the
          witness line ran down the casing and the text sat on the trim's own
          dimension. */}
      {!openingOnly && (
        <DoorDim vertical x1={floor - s(hh)} x2={floor}
          y={fr.x + fr.w + s(qnum(res.trim && res.trim.width) || qnum(d.trimSize) || 0) + 6}
          text={`${fmt(hh)} HANDLE HEIGHT`} size={2} tone={DOOR_BROWN} />
      )}
      {!openingOnly && qnum(d.leafUndercut) > 0 && (
        <text x={lf.x + lf.w / 2} y={floor - 1.2} fontSize="1.7" fill={DOOR_BROWN}
          fontFamily={DOOR_FONT} textAnchor="middle">
          {fmt(qnum(d.leafUndercut))} AIR-FLOW UNDERCUT
        </text>
      )}
    </g>
  );
}

// ── Horizontal section through the jamb ────────────────────────────────────
// Cut through the wall at handle height and looked at from above: the wall with
// its finishes, the frame profile in it, the trim either side, and the leaf
// with its core. This is the view that answers "what is the wall thickness and
// how does the trim land on it", which is why it is on every door sheet.
function DoorViewJamb({ res, x, y, w, h, denom, system }) {
  const S = res.sizes, d = res.resolved, frame = res.frame;
  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
  const sc = mm => mm / denom;
  const wallT = qnum(d.wallThickness) || 124;          // 4 7/8" is LEON's usual
  const jamb = qnum(frame && frame.jambWidth) || 50.8;
  const depth = qnum(frame && frame.frameDepth) || wallT;
  // The trim comes from the library now: its face width, its thickness and its
  // reveal are the profile's, not two loose numbers on the door.
  const trim = res.trim || null;
  const casing = qnum(trim && trim.width) || qnum(d.trimSize) || qnum(frame && frame.casing) || 63.5;
  const trimT = qnum(trim && trim.thickness) || 19;
  const integralCasing = !!(frame && frame.integralCasing);
  const trimmed = integralCasing || !!trim
    || ((d.trimType || '') !== 'None' && (d.trimType || '') !== 'Plaster-In / Trimless');
  const stop = qnum(frame && frame.stop) || 15.9;
  const th = qnum(S.thickness) || 44.45;
  const jambT = qnum(d.jambThickness) || 19;

  const midY = y + h / 2 + 2;
  const wallH = sc(wallT);
  // A SHORT run of wall, not half the view. This is a detail: what is worth
  // looking at is the jamb, the stop, the trim and how the leaf meets them —
  // and a long blank wall running off to the left is the part of the drawing
  // carrying no information. 150 mm of wall is enough to say "wall continues".
  const wallRun = sc(150);
  const ww = w - 12;
  // 16 mm of margin, not 6: the wall-thickness dimension is drawn to the LEFT
  // of the wall and its rotated text ran off the edge of the sheet at 6.
  const wx = x + 16;
  const openX = wx + wallRun;

  return (
    <g>
      {/* Wall, cut — hatched and heavy, with a break line where it runs on. */}
      <rect x={wx} y={midY - wallH / 2} width={openX - wx} height={wallH}
        fill="url(#dHatch)" stroke={DOOR_INK} strokeWidth={DW.cut} />
      <path d={`M ${wx} ${midY - wallH / 2} l -1.6 ${wallH * 0.25} l 3.2 ${wallH * 0.25}
        l -3.2 ${wallH * 0.25} l 1.6 ${wallH * 0.25}`}
        fill="none" stroke={DOOR_INK} strokeWidth={DW.thin} />
      {/* Frame in the opening, cut. ITS DEPTH IS THE WALL'S THICKNESS — a jamb
          is made to suit the wall it sits in, so the wall figure drives it
          rather than the frame profile's own nominal depth. The trims then add
          their thickness on top of each wall face, outside this. */}
      {(() => {
        // A WRAPPED JAMB IS NOT A SOLID BLOCK. It is a core, a skin wrapped over
        // it and a face on the skin — An Cuong's UHT20 is finger-joint core,
        // moisture-resistant MDF skin, melamine face — and a section drawn as
        // one poché tells the shop nothing about what to make. Where the frame
        // record carries no build-up the old solid fill is still what is drawn,
        // because inventing layers would be worse than saying nothing.
        const jx = openX, jw = sc(jamb);
        const skin = sc(qnum(frame && frame.skinThickness) || 0);
        const core = String((frame && frame.coreMaterial) || '');
        const built = skin > 0.25 && !!core && core !== 'None';
        const coreFill = /finger/i.test(core) ? 'url(#dFinger)'
          : /steel|alumin/i.test(core) ? DOOR_INK
          : /timber|wood|lvl/i.test(core) ? 'url(#dGrain)' : DOOR_POCHE;
        // A timber jamb is drawn with grain, the way a joinery section is: it
        // is what separates a wooden member from a metal one at a glance.
        const timber = !/steel|alumin/i.test(String((frame && frame.kind) || '') + core);
        const bodyFill = timber ? 'url(#dGrain)' : DOOR_POCHE;
        return (
          <g>
            {/* the skin, as the outer body */}
            <rect x={jx} y={midY - wallH / 2} width={jw} height={wallH}
              fill={bodyFill} stroke={DOOR_INK} strokeWidth={DW.cut} />
            {built && (
              <g>
                {/* the core inside it */}
                <rect x={jx + skin} y={midY - wallH / 2 + skin}
                  width={Math.max(0.2, jw - skin * 2)} height={Math.max(0.2, wallH - skin * 2)}
                  fill={coreFill} stroke={DOOR_INK} strokeWidth={DW.thin}
                  opacity={/steel|alumin/i.test(core) ? 0.5 : 1} />
                {/* and the face, on the two wall faces and the reveal */}
                <line x1={jx} y1={midY - wallH / 2} x2={jx + jw} y2={midY - wallH / 2}
                  stroke={DOOR_BROWN} strokeWidth={DW.outline} />
                <line x1={jx} y1={midY + wallH / 2} x2={jx + jw} y2={midY + wallH / 2}
                  stroke={DOOR_BROWN} strokeWidth={DW.outline} />
              </g>
            )}
            <text x={jx + jw / 2} y={midY} fontSize="1.5" fill={DOOR_INK}
              fontFamily={DOOR_FONT} textAnchor="middle" opacity="0.55"
              transform={`rotate(-90 ${jx + jw / 2} ${midY})`}>
              JAMB DEPTH = WALL {fmt(wallT)}
            </text>
            {built && (
              <g>
                <line x1={jx + jw} y1={midY - wallH / 2 + skin / 2}
                  x2={jx + jw + 5} y2={midY - wallH / 2 - 2} stroke={DOOR_INK} strokeWidth={DW.thin} />
                <text x={jx + jw + 5.5} y={midY - wallH / 2 - 2.2} fontSize="1.5" fill={DOOR_INK}
                  fontFamily={DOOR_FONT} opacity="0.7">
                  {String(frame.skinMaterial || 'SKIN').toUpperCase()} {fmt(qnum(frame.skinThickness))}
                </text>
                <line x1={jx + jw / 2} y1={midY + wallH / 2 - skin - 1}
                  x2={jx + jw + 5} y2={midY + wallH / 2 + 3} stroke={DOOR_INK} strokeWidth={DW.thin} />
                <text x={jx + jw + 5.5} y={midY + wallH / 2 + 3.2} fontSize="1.5" fill={DOOR_INK}
                  fontFamily={DOOR_FONT} opacity="0.7">
                  {core.toUpperCase()} CORE
                  {frame.faceMaterial ? ` · ${String(frame.faceMaterial).toUpperCase()} FACE` : ''}
                </text>
              </g>
            )}
          </g>
        );
      })()}
      {/* Stop on the frame. */}
      {(() => {
        const sx = openX + sc(jamb) * 0.35, sy = midY - wallH / 2 + sc(depth) * 0.35;
        const sw2 = sc(stop), sh2 = sc(stop) * 1.6;
        const sealed = !!(frame && frame.sealGroove) || ((d.gasketType || 'None') !== 'None');
        return (
          <g>
            {/* A planted stop: its own member fixed to the jamb, with the
                groove the seal sits in cut into its face. Drawn as a separate
                piece because that is what it is — the reference detail shows
                the rebate and the seal in it, not a lump on the jamb. */}
            <rect x={sx} y={sy} width={sw2} height={sh2}
              fill="url(#dGrain)" stroke={DOOR_INK} strokeWidth={DW.detail} />
            <line x1={sx} y1={sy} x2={sx + sw2} y2={sy}
              stroke={DOOR_INK} strokeWidth={DW.outline} />
            {/* The bulb seal, set INTO the stop — it is a groove and a gasket,
                not a strip stuck on, which is what An Cuong's section shows and
                what the shop machines for. */}
            {/* THE GASKET, as a circle in the gap BETWEEN the leaf and the
                jamb — which is where it actually is and how a section draws a
                bulb seal. It used to sit on the stop's own face, which is where
                the groove is but not where the seal closes. */}
            {sealed && (() => {
              const gx = openX + sc(jamb), gy = midY - sc(th) / 2;
              const gr = Math.max(0.6, sc(qnum(stop) * 0.5) || 0.9);
              return (
                <g>
                  <circle cx={gx} cy={gy} r={gr}
                    fill={DOOR_BROWN} opacity="0.5" stroke={DOOR_INK} strokeWidth={DW.thin} />
                  <line x1={gx} y1={gy - gr} x2={gx + 5} y2={gy - 6}
                    stroke={DOOR_INK} strokeWidth={DW.thin} />
                  <text x={gx + 5.5} y={gy - 6.2} fontSize="1.5" fill={DOOR_INK}
                    fontFamily={DOOR_FONT} opacity="0.7">
                    {String(d.gasketType && d.gasketType !== 'None' ? d.gasketType : 'Seal').toUpperCase()}
                  </text>
                </g>
              );
            })()}
          </g>
        );
      })()}
      {/* Trim / casing both faces — what the client sees, and the one part of
          the assembly the record could not describe until now. */}
      {trimmed && (
        <g>
          {/* An INTEGRAL casing is moulded as part of the jamb and returns onto
              the wall in one piece — there is no joint to draw and nothing
              separate to order. A separate trim is applied to the jamb and gets
              its own outline. The record says which, so the section can too. */}
          {/* Both casings, drawn straight and identical — a plain section
              through the trim on each wall face. The tongue-and-groove notch
              that was here came off on the client's instruction: it is not how
              theirs is made, and a joint drawn wrong is worse than one left
              out. */}
          {(() => {
            // AN L, SITTING INTO THE JAMB. The long arm runs across the wall
            // face; the short arm turns the corner and returns down the jamb's
            // own edge, into the opening. That is the section, and it is why
            // the casing holds a line where the wall face and the jamb meet.
            const tt = sc(trimT), cw2 = sc(casing);
            // THE LEG INTO THE JAMB IS THIN — a 1/4" tongue, not the casing's
            // own 3/4". It was drawn at the full trim thickness, which made the
            // return look like a second piece of casing rather than the lip it
            // is. `trimLegThickness` on the profile so it can be changed.
            const legT = sc(qnum(trim && trim.legThickness) || 6.35);
            // REAL SET-OUT, not a fraction of something else. The return is
            // 1 1/2" long and sits 1/2" in from the frame's own face — both
            // figures the shop machines to. It has been half the reveal and
            // then half the jamb's depth; neither was a number anyone gave.
            const ret = sc(qnum(trim && trim.legLength) || 38.1);
            const off = sc(qnum(trim && trim.legOffset) || 12.7);
            const leg = flip => {
              const sgn = flip ? 1 : -1;
              const yOut = flip ? midY + wallH / 2 + tt : midY - wallH / 2 - tt;   // outer face
              const yIn = yOut - sgn * tt;                                          // inner face
              const yRet = yIn - sgn * ret;                                         // end of the return
              return (
                <path d={`M ${openX - cw2} ${yOut}
                          L ${openX + off + legT} ${yOut}
                          L ${openX + off + legT} ${yRet}
                          L ${openX + off} ${yRet}
                          L ${openX + off} ${yIn}
                          L ${openX - cw2} ${yIn} Z`}
                  fill="url(#dGrain)" stroke={DOOR_INK}
                  strokeWidth={integralCasing ? DW.cut : DW.detail} />
              );
            };
            return <g>{leg(false)}{leg(true)}</g>;
          })()}
          {integralCasing && (
            <text x={openX - sc(casing) / 2} y={midY + wallH / 2 + sc(trimT) + 3} fontSize="1.5"
              fill={DOOR_BROWN} fontFamily={DOOR_FONT} textAnchor="middle">
              CASING INTEGRAL WITH JAMB — ONE PIECE
            </text>
          )}
          <text x={openX - sc(casing) / 2} y={midY - wallH / 2 - sc(trimT) - 3.2} fontSize="1.7"
            fill={DOOR_INK} fontFamily={DOOR_FONT} textAnchor="middle" opacity="0.75">
            {/* The SIZE and the JOINT, which is what gets cut. What the
                profile is CALLED belongs in the Finishes & Selections footer
                with the rest of what was chosen — saying it here as well only
                crowded the detail. */}
            TRIM {fmt(casing)} × {fmt(trimT)}
            {trim && trim.joint ? ` · ${String(trim.joint).toUpperCase()}` : ''}
          </text>
          <DoorDim vertical x1={midY - wallH / 2 - sc(trimT)} x2={midY - wallH / 2}
            y={openX - sc(casing) - 2.5} text={fmt(trimT)} size={1.5} />
          {/* No reveal call-out here on the client's instruction — the two
              casings are drawn identically and the detail says the size and
              the joint, which is what gets cut. */}
        </g>
      )}
      {/* The leaf, cut, with its core shown as a distinct fill. */}
      {(() => {
        const lx = openX + sc(jamb);
        const lw = Math.min(sc(350), x + w - 16 - lx);   // a stub of leaf, then a break
        return (
          <g>
            {(() => {
              // The leaf cut through, the way An Cuong's cutaway reads: a
              // finger-joint stile at the lock/hinge edge, the core between the
              // two skins, and the face on each skin. The core FILL follows the
              // core actually specified rather than one generic dot pattern.
              const core = String(d.coreType || '');
              const fill = /honey/i.test(core) ? 'url(#dHoney)'
                : /tubular/i.test(core) ? 'url(#dCore)'
                : /hSemi|semi/i.test(core) ? 'url(#dFinger)'
                : DOOR_POCHE;
              const skinT = Math.max(0.35, sc(6));          // the MDF skin either face
              const stileW = Math.min(lw * 0.5, sc(40));    // the edge stile
              return (
                <g>
                  <rect x={lx} y={midY - sc(th) / 2} width={lw} height={sc(th)}
                    fill={fill} stroke={DOOR_INK} strokeWidth={DW.cut} />
                  {/* the two skins */}
                  <rect x={lx} y={midY - sc(th) / 2} width={lw} height={skinT}
                    fill={DOOR_POCHE} stroke={DOOR_INK} strokeWidth={DW.thin} />
                  <rect x={lx} y={midY + sc(th) / 2 - skinT} width={lw} height={skinT}
                    fill={DOOR_POCHE} stroke={DOOR_INK} strokeWidth={DW.thin} />
                  {/* the finger-joint stile the lock is fixed into */}
                  <rect x={lx} y={midY - sc(th) / 2} width={stileW} height={sc(th)}
                    fill="url(#dFinger)" stroke={DOOR_INK} strokeWidth={DW.detail} />
                  <line x1={lx + stileW} y1={midY - sc(th) / 2 - 1.5} x2={lx + stileW} y2={midY - sc(th) / 2}
                    stroke={DOOR_INK} strokeWidth={DW.thin} />
                  <text x={lx + stileW + 1} y={midY - sc(th) / 2 - 2} fontSize="1.5" fill={DOOR_INK}
                    fontFamily={DOOR_FONT} opacity="0.7">FINGER-JOINT STILE</text>
                  {/* No hinge here on the client's instruction — the jamb
                      detail is about how the leaf meets the frame, and the
                      hinges are scheduled on the hardware lines and drawn on
                      the elevation. */}
                </g>
              );
            })()}
            <path d={`M ${lx + lw} ${midY - sc(th) / 2} l 1.6 ${sc(th) * 0.25}
              l -3.2 ${sc(th) * 0.25} l 3.2 ${sc(th) * 0.25} l -1.6 ${sc(th) * 0.25}`}
              fill="none" stroke={DOOR_INK} strokeWidth={DW.thin} />
            <text x={lx + lw / 2} y={midY - sc(th) / 2 - 1.4} fontSize="1.9"
              fill={DOOR_INK} textAnchor="middle" opacity="0.6">
              {doorCoreLabel(d.coreType).toUpperCase()}
            </text>
          </g>
        );
      })()}

      {/* WALL THICKNESS IS MEASURED ACROSS THE WALL, NOT ALONG IT. This was
          drawn horizontally over the wall's run and labelled with the
          thickness — two different dimensions with one figure on them, which
          is worse than leaving it off. The wall's depth is the vertical
          extent of the cut, so that is where the dimension goes. */}
      <DoorDim vertical x1={midY - wallH / 2} x2={midY + wallH / 2} y={wx - 3}
        text={`${fmt(wallT)} WALL THK`} size={1.9} tone={DOOR_BROWN} />
      <DoorDim vertical x1={midY - wallH / 2} x2={midY - wallH / 2 + sc(jambT)} y={openX + sc(jamb) + 3}
        text={`${fmt(jambT)} JAMB THK`} size={1.7} />
      {/* BELOW the wall, not above it — above, it sat on the trim's own label
          and the two ran through each other. */}
      <DoorDim x1={openX} x2={openX + sc(jamb)} y={midY + wallH / 2 + 13}
        text={`${fmt(jamb)} JAMB W`} size={1.9} />
      <DoorDim vertical x1={midY - sc(th) / 2} x2={midY + sc(th) / 2} y={x + w - 4}
        text={`${fmt(th)} LEAF THK`} size={1.9} tone={DOOR_BROWN} />
      {/* Wall + a trim on each face — what the assembly actually measures
          across, which is the figure that has to clear the reveal. */}
      {trimmed && (
        <DoorDim vertical x1={midY - wallH / 2 - sc(trimT)} x2={midY + wallH / 2 + sc(trimT)}
          y={wx - 9} text={`${fmt(wallT + trimT * 2)} OVERALL`} size={1.7} />
      )}
    </g>
  );
}

// ── Vertical section: head and sill ────────────────────────────────────────
function DoorViewHeadSill({ res, x, y, w, h, denom, system }) {
  const S = res.sizes, d = res.resolved, frame = res.frame;
  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
  const sc = mm => mm / denom;
  const headW = qnum(frame && frame.headWidth) || 50.8;
  const wallT = qnum(d.wallThickness) || 124;
  const undercut = qnum(S.undercut) || 0;
  const cx = x + w / 2;
  const wallW = sc(wallT);

  const headY = y + 10, sillY = y + h - 16;
  return (
    <g>
      {/* Head: wall above, frame head cut, leaf below it. */}
      <rect x={cx - wallW / 2} y={headY - 8} width={wallW} height={8}
        fill="url(#dHatch)" stroke={DOOR_INK} strokeWidth={DW.cut} />
      <rect x={cx - wallW / 2} y={headY} width={wallW} height={sc(headW)}
        fill={DOOR_POCHE} stroke={DOOR_INK} strokeWidth={DW.cut} />
      <rect x={cx - sc(qnum(S.thickness) || 44) / 2} y={headY + sc(headW)}
        width={sc(qnum(S.thickness) || 44)} height={10}
        fill="url(#dCore)" stroke={DOOR_INK} strokeWidth={DW.outline} />
      <text x={cx + wallW / 2 + 2} y={headY + sc(headW) / 2 + 1} fontSize="1.9"
        fill={DOOR_INK} opacity="0.6">HEAD</text>

      {/* Sill: leaf, the undercut gap, then the floor. */}
      <rect x={cx - sc(qnum(S.thickness) || 44) / 2} y={sillY - 10}
        width={sc(qnum(S.thickness) || 44)} height={10}
        fill="url(#dCore)" stroke={DOOR_INK} strokeWidth={DW.outline} />
      <line x1={cx - wallW / 2 - 3} y1={sillY + sc(undercut)} x2={cx + wallW / 2 + 3} y2={sillY + sc(undercut)}
        stroke={DOOR_INK} strokeWidth={DW.cut} />
      <text x={cx + wallW / 2 + 2} y={sillY + sc(undercut) + 3} fontSize="1.9"
        fill={DOOR_INK} opacity="0.6">FFL</text>
      {undercut > 0 && (
        <DoorDim vertical x1={sillY} x2={sillY + sc(undercut)} y={cx - wallW / 2 - 5}
          text={`U/C ${fmt(undercut)}`} size={1.8} />
      )}
    </g>
  );
}

// ── Plan: the opening diagram for THIS door ────────────────────────────────
// Which way it swings, how far, and into what. A schedule can say "RHR" and a
// person can still hang it backwards; a swing arc cannot be misread.
// Drawn at the SAME scale as the head/plan beside it, so the two read as one
// pair rather than as two different doors.
function DoorViewSwing({ res, x, y, w, h, denom, system }) {
  const S = res.sizes, d = res.resolved;
  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
  const hand = d.handing || 'RH';
  const pair = doorIsPair(d.operation, hand);
  const left = /LH|LHR/.test(hand);
  // CENTRED in its own box, both ways. The arc sweeps up from the opening and
  // the person stands below it, so the whole group is that tall; sitting it on
  // a fixed 10 mm from the bottom left it low and off-centre in the frame.
  const cx = x + w / 2;
  // The leaf at the sheet's own scale, capped so a wide pair still fits.
  // TRUE scale — the leaf here is the same size as the leaf in the elevation
  // above it. The old cap at a fraction of this box's size made the swing
  // whatever size looked comfortable, which is exactly what stops two views
  // being comparable. The fallback only catches a box too small to hold it.
  const trueHalf = (qnum(S.leaf.w) || 900) / (denom || 20);
  const half = trueHalf <= Math.min(w, h) * 0.46 ? trueHalf : Math.min(w, h) * 0.46;
  const toScale = half === trueHalf;
  // The group runs from the IN caption above the arc down to the person's
  // caption below; centre THAT, not the opening line.
  const above = half * 0.95 + 6;                  // arc + the IN captions
  const shoulder = Math.max(2.6, 460 / (denom || 20)) / 2;
  const below = shoulder * 2.5 + 3.4 + 9;         // the figure and its captions
  const wallY = y + (h - (above + below)) / 2 + above;
  const base = wallY;
  const openW = half * (pair ? 2 : 1);

  const leafArc = (hx, dir) => {
    const tip = { x: hx + dir * half * 0.72, y: wallY - half * 0.72 };
    return (
      <g>
        {/* leaf at 45°, and the arc it sweeps */}
        <line x1={hx} y1={wallY} x2={tip.x} y2={tip.y} stroke={DOOR_INK} strokeWidth={DW.outline} />
        <path d={`M ${hx + dir * half} ${wallY} A ${half} ${half} 0 0 ${dir > 0 ? 0 : 1} ${tip.x} ${tip.y}`}
          fill="none" stroke={DOOR_INK} strokeWidth={DW.thin} strokeDasharray="1.4 1.2" />
      </g>
    );
  };
  return (
    <g>
      {/* Built to match the client's own handing chart: INSIDE tinted above
          the opening line, OUTSIDE plain below it, the leaf shown part-open at
          45°, the arc arrowed in the direction it travels, and a person stood
          OUTSIDE with a hand on the lever. It reads as "stand here, pull this,
          it goes that way" rather than as a piece of geometry. */}
      {/* The inside, tinted — the side the leaf sweeps into. */}
      <rect x={x + 2} y={y + 4} width={w - 4} height={wallY - (y + 4)}
        fill={DOOR_INK} opacity="0.09" />

      {/* The wall either side of the opening, as their chart draws it: a plain
          outlined bar, not hatch — this is a handing diagram, not a section. */}
      <rect x={x + 3} y={wallY - 1.6} width={cx - openW / 2 - (x + 3)} height="3.2"
        fill="#fff" stroke={DOOR_INK} strokeWidth={DW.outline} />
      <rect x={cx + openW / 2} y={wallY - 1.6} width={x + w - 3 - (cx + openW / 2)} height="3.2"
        fill="#fff" stroke={DOOR_INK} strokeWidth={DW.outline} />

      {/* WHAT THE DOOR ACTUALLY DOES. This drew an arc whatever the operation
          was, so changing a door to sliding, pocket or bifold changed the
          caption and nothing else. Each now draws its own movement. */}
      {(() => {
        const opn = d.operation || 'Swing';
        const L = cx - openW / 2, R = cx + openW / 2;
        if (opn === 'Pocket') {
          // The leaf runs into the wall it is parked in, so the pocket is
          // drawn as the cavity it needs — which is the thing that has to be
          // built and the reason this view matters on a pocket door.
          const dir = left ? -1 : 1;
          return (
            <g>
              <rect x={dir < 0 ? L - openW : R} y={wallY - 1.6} width={openW} height="3.2"
                fill="none" stroke={DOOR_INK} strokeWidth={DW.thin} strokeDasharray="1.6 1.2" />
              <line x1={dir < 0 ? L - openW * 0.9 : R + openW * 0.1} y1={wallY}
                x2={dir < 0 ? L - openW * 0.1 : R + openW * 0.9} y2={wallY}
                stroke={DOOR_INK} strokeWidth={DW.cut} />
              <path d={`M ${dir < 0 ? L + 3 : R - 3} ${wallY - 4} L ${dir < 0 ? L - 1 : R + 1} ${wallY - 4}
                        l 2 -1.4 m -2 1.4 l 2 1.4`} fill="none" stroke={DOOR_BROWN} strokeWidth={DW.outline} />
              <text x={dir < 0 ? L - openW / 2 : R + openW / 2} y={wallY - 6} fontSize="1.8"
                fill={DOOR_INK} fontFamily={DOOR_FONT} textAnchor="middle" opacity="0.7">POCKET CAVITY</text>
            </g>
          );
        }
        if (opn === 'Sliding' || opn === 'Barn') {
          const dir = left ? -1 : 1;
          return (
            <g>
              {/* the track, and the leaf parked on the wall beside the opening */}
              <line x1={L - openW * (dir < 0 ? 1.1 : 0.1)} y1={wallY - 5}
                x2={R + openW * (dir < 0 ? 0.1 : 1.1)} y2={wallY - 5}
                stroke={DOOR_INK} strokeWidth={DW.detail} />
              <rect x={dir < 0 ? L - openW : R} y={wallY - 3.6} width={openW} height="3.2"
                fill="#fff" stroke={DOOR_INK} strokeWidth={DW.outline} />
              <path d={`M ${cx - openW * 0.2} ${wallY - 8} L ${cx + dir * openW * 0.4} ${wallY - 8}`}
                stroke={DOOR_BROWN} strokeWidth={DW.outline} fill="none" />
              <path d={`M ${cx + dir * openW * 0.4} ${wallY - 8} l ${-dir * 2} -1.4 m ${dir * 2} 1.4 l ${-dir * 2} 1.4`}
                stroke={DOOR_BROWN} strokeWidth={DW.outline} fill="none" />
              <text x={cx} y={wallY - 10.5} fontSize="1.8" fill={DOOR_INK}
                fontFamily={DOOR_FONT} textAnchor="middle" opacity="0.7">
                {opn === 'Barn' ? 'BARN TRACK' : 'SLIDES ON TRACK'}
              </text>
            </g>
          );
        }
        if (opn === 'Bypass') {
          return (
            <g>
              <rect x={L} y={wallY - 4.4} width={openW / 2} height="3"
                fill="#fff" stroke={DOOR_INK} strokeWidth={DW.outline} />
              <rect x={cx} y={wallY - 1.2} width={openW / 2} height="3"
                fill="#fff" stroke={DOOR_INK} strokeWidth={DW.outline} />
              <text x={cx} y={wallY - 7} fontSize="1.8" fill={DOOR_INK}
                fontFamily={DOOR_FONT} textAnchor="middle" opacity="0.7">BYPASS — TWO TRACKS</text>
            </g>
          );
        }
        if (opn === 'Bifold') {
          // Folded to 90°: two panels per leaf, hinged at the jamb.
          const fold = (hx, dir) => {
            const q = openW / 4;
            return (
              <g stroke={DOOR_INK} strokeWidth={DW.outline} fill="none">
                <line x1={hx} y1={wallY} x2={hx + dir * q * 0.72} y2={wallY - q * 0.72} />
                <line x1={hx + dir * q * 0.72} y1={wallY - q * 0.72}
                  x2={hx + dir * q * 1.44} y2={wallY} />
              </g>
            );
          };
          return <g>{fold(L, 1)}{fold(R, -1)}
            <text x={cx} y={wallY - openW / 5} fontSize="1.8" fill={DOOR_INK}
              fontFamily={DOOR_FONT} textAnchor="middle" opacity="0.7">BIFOLD</text></g>;
        }
        if (opn === 'Fixed') {
          return (
            <g>
              <rect x={L} y={wallY - 1.6} width={openW} height="3.2"
                fill="url(#dHatch)" stroke={DOOR_INK} strokeWidth={DW.cut} />
              <text x={cx} y={wallY - 5} fontSize="1.8" fill={DOOR_INK}
                fontFamily={DOOR_FONT} textAnchor="middle" opacity="0.7">FIXED — DOES NOT OPEN</text>
            </g>
          );
        }
        // Swing, Double Swing and Pivot all sweep an arc.
        return pair
          ? <g>{leafArc(L, 1)}{leafArc(R, -1)}</g>
          : leafArc(left ? L : R, left ? 1 : -1);
      })()}

      {/* The person, OUTSIDE, hand closed on the lever — only where there is a
          leaf to pull. A figure reaching for a fixed panel is nonsense. */}
      {(d.operation || 'Swing') !== 'Fixed' && (() => {
        const dir = pair ? -1 : (left ? 1 : -1);
        const hingeX = pair ? cx + openW / 2 : (left ? cx - openW / 2 : cx + openW / 2);
        // Where the lever ends up on the part-open leaf: along the leaf line at
        // 45°, at the far end from the hinge.
        const lx2 = hingeX + dir * half * 0.72, ly2 = wallY - half * 0.72;
        const sh = Math.max(2.8, 460 / (denom || 20)) / 2;
        const px = hingeX + dir * half * 0.30, py = wallY + sh * 1.7 + 2;
        return (
          <g>
            {/* shoulders and head, seen from above */}
            <ellipse cx={px} cy={py} rx={sh} ry={sh * 0.52} fill="#fff"
              stroke={DOOR_INK} strokeWidth={DW.outline} />
            <circle cx={px} cy={py} r={sh * 0.44} fill="#fff" stroke={DOOR_INK} strokeWidth={DW.outline} />
            {/* the arm, reaching up to the lever on the swung leaf */}
            <path d={`M ${px + dir * sh * 0.55} ${py - sh * 0.3}
                      Q ${px + dir * sh * 1.1} ${(py + ly2) / 2} ${lx2} ${ly2}`}
              fill="none" stroke={DOOR_INK} strokeWidth={DW.outline} strokeLinecap="round" />
            {/* the hand, closed on it */}
            <circle cx={lx2} cy={ly2} r={Math.max(0.8, sh * 0.3)}
              fill="#fff" stroke={DOOR_INK} strokeWidth={DW.outline} />
          </g>
        );
      })()}

      {/* INSIDE / OUTSIDE, set the way their chart sets them. */}
      <text x={x + 4} y={y + 9} fontSize="2.8" fill={DOOR_INK} fontFamily={DOOR_FONT}>Inside</text>
      <text x={x + w - 4} y={y + h - 8} fontSize="2.8" fill={DOOR_INK}
        fontFamily={DOOR_FONT} textAnchor="end">Outside</text>
      <text x={x + 4} y={y + h - 8} fontSize="2" fill={DOOR_BROWN} fontFamily={DOOR_FONT}
        fontWeight="bold" letterSpacing="0.5">
        {doorSlides(d.operation)
          ? `${left ? 'SLIDES LEFT' : 'SLIDES RIGHT'} · ${String(d.operation).toUpperCase()}`
          : `${left ? 'LEFT HAND' : 'RIGHT HAND'} ${pair
              ? `PAIR — ${String(d.pairSwing || 'Inward').toUpperCase()}`
              : (/R$/.test(hand) ? 'OUTSWING' : 'INSWING')}`}
      </text>
      <text x={x + 4} y={y + h - 5} fontSize="1.7" fill={DOOR_INK}
        fontFamily={DOOR_FONT} opacity="0.6">
        {d.toRoom ? `into ${d.toRoom}` : 'opens inward'}
        {d.fromRoom ? ` · from ${d.fromRoom}` : ''}
      </text>

      {/* CLEAR OPENING — the leaf width less what the hinge side and the stop
          take, which is the figure someone measures a fridge against. It is
          NOT the frame width, which is what this used to print. */}
      <DoorDim x1={cx - openW / 2} x2={cx + openW / 2} y={wallY - 5}
        text={`${fmt(Math.max(0, qnum(S.leaf.w) * (pair ? 2 : 1)
          - (qnum(res.rule && res.rule.clearOpeningDeduction) || 38.1)))} CLEAR OPENING`} size={1.9} />
      <text x={cx} y={y + h - 1.5} fontSize="2" fill={DOOR_INK} textAnchor="middle" opacity="0.6">
        {hand} · {d.operation || 'Swing'}
      </text>
    </g>
  );
}

// ── The data panel down the right ──────────────────────────────────────────
// Everything the fabricator is told that is not a dimension: the mark, where it
// goes, what it is made of, its finishes with their swatches, its hardware and
// its rating. The LEON mark sits at the top, as it does on the issued set.
function DoorViewData({ res, ctx, project, x, y, w, h, system, sheetNo, rev }) {
  const d = res.resolved, S = res.sizes;
  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
  const co = ctx.companyProfile || {};
  const rows = [
    ['Door code', d.mark || '—'],
    ['Type', res.type ? res.type.name : (d.typeName || '—')],
    ['Location', [d.location, d.room].filter(Boolean).join(' · ') || '—'],
    ['Operation', `${d.operation || 'Swing'} · ${d.handing || '—'}`],
    ['Leaf', `${fmt(S.leaf.w)} × ${fmt(S.leaf.h)} × ${fmt(S.thickness)}`],
    ['Frame', `${fmt(S.frame.w)} × ${fmt(S.frame.h)}`],
    ['Rough opening', `${fmt(S.ro.w)} × ${fmt(S.ro.h)}`],
    ...(d.modelCode ? [['LEON model', d.modelCode]] : []),
    ['Design', res.design ? res.design.name : '—'],
    ['Setting out', res.design
      ? (res.design.panelRows > 0
          ? `Stile ${fmt(qnum(res.design.stile))} · rails ${fmt(qnum(res.design.railTop))}/${fmt(qnum(res.design.railBottom))}`
          : res.design.grooveCount > 0
            ? `${res.design.grooveCount} × ${fmt(qnum(res.design.grooveWidth))} ${String(res.design.grooveOrientation || '').toLowerCase()}`
            : 'Flat — no setting out')
      : '—'],
    ['Core', doorCoreLabel(d.coreType)],
    ['Frame profile', res.frame ? res.frame.name : '—'],
    ['Trim', `${d.trimType || '—'}${qnum(d.trimSize) ? ` · ${fmt(d.trimSize)}` : ''}`],
    ['Gasket', d.gasketType || '—'],
    ['Hinges', (h => `${h.count} × ${h.name}`)(doorHinges(res))],
    ['Handle height', fmt(qnum(res.rule && res.rule.handleHeight) || 914.4)],
    ['Wall', `${d.wallKind || '—'} · ${fmt(d.wallThickness)}`],
    ['Finishes', 'see the strip below'],
    // Only when the architect actually gave a set code. LEON hand-picks the
    // hardware line by line, so an empty "Hardware set —" row is a question
    // nobody asked.
    ...(d.hwSetCode ? [['Hardware set', d.hwSetCode]] : []),
    ['Fire rating', d.fireRating && d.fireRating !== 'None'
      ? `${d.fireRating}${d.ratingState === 'Requested' ? ' (REQUESTED)' : ''}` : 'None'],
    ['Supplied as', d.installationType || '—'],
    ['Qty', String(d.qty || 1)],
    ['Sheet', `${sheetNo || 'D-01'} · ${rev || 'R1'} · ${fmtDate(todayISO())}`],
  ];
  // The three finishes, each NAMED for what it is. A fabricator reads a swatch
  // faster than a code, and an unlabelled row of swatches is how the hardware
  // plating ends up on the leaf.


  return (
    <g>
      <rect x={x} y={y} width={w} height={h} fill="#ffffff" stroke={DOOR_INK}
        strokeWidth={DW.thin} opacity="0.9" />
      {/* LEON's OWN lockup — the lion over LEON COLLECTION — referenced as the
          real SVG rather than set as type. It is the file the brand ships, and
          no font reproduces the E or the lion. */}
      <rect x={x} y={y} width={w} height={40} fill={DOOR_CREAM} />
      <image href="logo/leon-official.svg" x={x + w / 2 - 15} y={y + 2} width="30" height="36"
        preserveAspectRatio="xMidYMid meet" />
      <line x1={x} y1={y + 40} x2={x + w} y2={y + 40} stroke={DOOR_BROWN} strokeWidth="0.5" />
      <text x={x + 2} y={y + 45} fontSize="2.6" fill={DOOR_INK} fontFamily={DOOR_FONT} fontWeight="bold">
        {String(project.name || '').slice(0, 28)}
      </text>
      <text x={x + 2} y={y + 48.6} fontSize="1.9" fill={DOOR_INK} fontFamily={DOOR_FONT} opacity="0.55">
        {project.projectNumber || ''}
      </text>
      <line x1={x} y1={y + 50.5} x2={x + w} y2={y + 50.5} stroke={DOOR_LINE} strokeWidth={DW.thin} />

      {rows.map(([k, v], i) => (
        <g key={k}>
          {/* Pitch 5.4, not 4.4: at 4.4 a row's VALUE ended 0.2 mm above the
              next row's LABEL, so every pair in the panel was touching. */}
          <text x={x + 2} y={y + 55 + i * 5.4} fontSize="1.7" fill={DOOR_BROWN}
            fontFamily={DOOR_FONT} opacity="0.75" letterSpacing="0.3">
            {k.toUpperCase()}
          </text>
          <text x={x + 2} y={y + 58.2 + i * 5.4} fontSize="2.2" fill={DOOR_INK} fontFamily={DOOR_FONT}>
            {String(v).slice(0, 30)}
          </text>
        </g>
      ))}

      {/* The hardware is NOT here. It is in the Finishes & Selections strip
          along the bottom, with the swatches, because that is where the client
          asked for what was chosen to be shown. */}
    </g>
  );
}

// ── Side elevation ─────────────────────────────────────────────────────────
// Redrawn against the client's own: the door seen EDGE-ON, not cut open. A thin
// upright — the frame head as a block at the top, the leaf running the full
// height below it, the lock at 36", the air-flow gap at the foot — with the
// leaf height and the frame height carried down the right and the leaf
// thickness across the bottom. It is a slim view on purpose; the make-up lives
// in the sections, and this one answers "how tall, how thick, where is the
// lock, how much air under it".
function DoorViewSideSection({ res, x, y, w, h, system, denom }) {
  const S = res.sizes, d = res.resolved, frame = res.frame;
  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
  const th = qnum(S.thickness) || 44.45;
  const headW = qnum(frame && frame.headWidth) || 50.8;
  const air = qnum(d.leafUndercut);
  const hh = qnum(res.rule && res.rule.handleHeight) || 914.4;
  const hinges = Math.max(0, doorHinges(res).count);
  const wallT = qnum(d.wallThickness) || 124;
  const jambT = qnum(d.jambThickness) || 19;
  const trimW = qnum(res.trim && res.trim.width) || qnum(d.trimSize) || 0;
  const trimT2 = qnum(res.trim && res.trim.thickness) || 19;

  // SAME SCALE as the other two elevations. It was fitting itself to its own
  // box, so the three sat side by side at three different sizes and could not
  // be read against each other — which is the whole point of putting them in a
  // row. `denom` is the sheet's, so a 84" leaf is 84" tall in all three.
  const vs = mm => (denom ? mm / denom : (mm / (qnum(S.frame.h) || 2184)) * (h - 20));
  const totalH = vs(qnum(S.frame.h) || 2184) + vs(qnum(d.leafUndercut));
  // CENTRED, like the other two elevations. It was pinned 14 mm off the bottom
  // of its box, so on a tall A2 band it sat low with dead space above it.
  // The drawn group is the wall above the head, the door, and the dimension
  // band under the floor.
  const sideTop = 17, sideBot = 12;
  const bot = y + (h - (totalH + sideTop + sideBot)) / 2 + sideTop + totalH;
  // +17, not +6: the WALL and OVERALL dimensions are drawn 9 and 13 mm above
  // the head, so starting the drawing at the top of the box put them inside
  // the title bar.
  const top = Math.max(y + sideTop, bot - totalH);
  // Everything across this view is a REAL dimension at the view's own scale:
  // the leaf edge-on, and the wall it sits in. A minimum keeps a thin leaf
  // drawable at 1:20 without making it a lie — it is a line weight, not a size.
  const tw = Math.max(1.4, vs(qnum(S.thickness) || 44.45));
  const wallHalf = Math.max(tw * 1.6, vs(wallT) / 2);
  const cx = x + w / 2;                            // centred across, too

  // Edge-on you see the head jamb's MATERIAL THICKNESS, not its face width.
  // It was drawn at `headWidth` and dimensioned at `jambThickness` — two
  // different numbers, so the dimension measured nothing that was drawn.
  const headTop = top, headH = Math.max(0.8, vs(jambT));
  const leafTop = headTop + headH;
  const leafH = vs(qnum(S.leaf.h) || 2032);
  const leafBot = leafTop + leafH;
  const floor = leafBot + vs(air);

  return (
    <g>
      {/* The wall above the head, cut, so the view says what the door is fixed
          into — and the head jamb and its trim, which the client asked for. */}
      {/* THE WALL, AT ITS REAL THICKNESS — and the head jamb spanning exactly
          the same width, because a jamb is made to suit the wall it sits in.
          The trims then stand proud of each wall FACE by their own thickness,
          so the assembly measures wall + two trims across. This was drawn at
          arbitrary paper widths before, so the jamb and the wall did not agree
          with each other or with the head/plan detail. */}
      <rect x={cx - wallHalf} y={headTop - 7} width={wallHalf * 2} height="7"
        fill="url(#dHatch)" stroke={DOOR_INK} strokeWidth={DW.cut} />
      <text x={cx + wallHalf + 2} y={headTop - 3} fontSize="1.7" fill={DOOR_INK}
        fontFamily={DOOR_FONT} opacity="0.6">WALL</text>
      {/* Frame head, seen on edge — the full depth of the wall. */}
      <rect x={cx - wallHalf} y={headTop} width={wallHalf * 2} height={headH}
        fill={DOOR_CREAM} stroke={DOOR_INK} strokeWidth={DW.cut} />
      <text x={cx + wallHalf + 2} y={headTop + headH / 2 + 0.8} fontSize="1.7"
        fill={DOOR_INK} fontFamily={DOOR_FONT} opacity="0.65">HEAD JAMB {fmt(jambT)}</text>
      {/* THE TRIM ON THE WALL. Edge-on you see the casing standing proud of
          the wall face on BOTH sides, running the full height of the opening
          and returning across the head — not just a stub at the head, which is
          all this used to draw. It is the part the client sees. */}
      {trimW > 0 && (() => {
        const tt = Math.max(0.7, vs(trimT2));          // it stands this proud
        const inner = wallHalf * 0.55, outer = wallHalf;  // frame face → wall face
        return (
          <g>
            {/* THE SAME L AS THE PLAN DETAIL. Edge-on the casing shows its
                face standing proud of the wall and its thin lip turning in
                against the jamb — this used to draw two flat bands with no
                return, so the section and this view disagreed about the same
                piece of timber. */}
            {[-1, 1].map(side => {
              const legT = Math.max(0.35, vs(qnum(res.trim && res.trim.legThickness) || 6.35));
              // The face sits OUTSIDE the wall; the lip turns IN and lands ON
              // THE JAMB, set 1/2" in from the frame's own face — it was drawn
              // hard against the wall face, which reads as sitting in the wall.
              const legOff = Math.max(0.3, vs(qnum(res.trim && res.trim.legOffset) || 12.7));
              const faceX = side < 0 ? cx - outer - tt : cx + outer;   // proud of the wall
              const lipX = side < 0 ? cx - outer + legOff : cx + outer - legOff - legT;
              return (
                <g key={side}>
                  {/* the face, down the wall, full height of the opening */}
                  <rect x={faceX} y={headTop - tt} width={tt} height={floor - headTop + tt}
                    fill="url(#dGrain)" stroke={DOOR_INK} strokeWidth={DW.thin} />
                  {/* THE LIP IS BEHIND THE CASING FROM HERE — this view looks
                      straight along it — so it is drawn as a HIDDEN line, which
                      is how a drawing shows something it cannot see, at a width
                      that can be read: at 1:10 a 1/4" lip is 0.6 mm of paper,
                      which is why solid it looked like nothing at all. */}
                  <rect x={lipX} y={headTop - tt} width={Math.max(1.1, legT)} height={floor - headTop + tt}
                    fill="none" stroke={DOOR_INK} strokeWidth={DW.thin} strokeDasharray="1.4 1" />
                  {side < 0 && (
                    <g>
                      <line x1={lipX + Math.max(1.1, legT)} y1={floor - (floor - headTop) * 0.62}
                        x2={lipX + 9} y2={floor - (floor - headTop) * 0.62 - 4}
                        stroke={DOOR_INK} strokeWidth={DW.thin} />
                      <text x={lipX + 9.5} y={floor - (floor - headTop) * 0.62 - 4.2} fontSize="1.5"
                        fill={DOOR_INK} fontFamily={DOOR_FONT} opacity="0.7">
                        TRIM LIP INTO JAMB {fmt(qnum(res.trim && res.trim.legLength) || 38.1)}
                      </text>
                    </g>
                  )}
                  {/* and the head, where the casing turns the corner */}
                  <rect x={side < 0 ? faceX : cx + inner} y={headTop - tt}
                    width={outer - inner + tt} height={tt}
                    fill="url(#dGrain)" stroke={DOOR_INK} strokeWidth={DW.thin} />
                </g>
              );
            })}
            <line x1={cx - outer - tt} y1={floor - (floor - headTop) * 0.34}
              x2={cx - outer - tt - 5} y2={floor - (floor - headTop) * 0.34}
              stroke={DOOR_INK} strokeWidth={DW.thin} />
            <text x={cx - outer - tt - 5.5} y={floor - (floor - headTop) * 0.34 - 0.8}
              fontSize="1.6" fill={DOOR_INK} fontFamily={DOOR_FONT} textAnchor="end" opacity="0.7">
              TRIM {fmt(trimW)} × {fmt(trimT2)}
            </text>
          </g>
        );
      })()}

      {/* The leaf on edge: two faces and the core between, full height. */}
      <rect x={cx - tw / 2} y={leafTop} width={tw} height={leafH}
        fill="#fdfcfa" stroke={DOOR_INK} strokeWidth={DW.outline} />
      <line x1={cx - tw / 2 + 0.5} y1={leafTop} x2={cx - tw / 2 + 0.5} y2={leafBot}
        stroke={DOOR_INK} strokeWidth={DW.thin} />
      <line x1={cx + tw / 2 - 0.5} y1={leafTop} x2={cx + tw / 2 - 0.5} y2={leafBot}
        stroke={DOOR_INK} strokeWidth={DW.thin} />

      {/* NO HINGES HERE. They are on the hanging edge, which this view looks
          straight down — so they were being drawn on an edge the view does not
          actually show, and they cluttered the one thing it is for: the four
          thicknesses and the heights. The hinges are scheduled in the hardware
          lines and drawn on the elevation. */}

      {/* The lock, at the handle height — the pin lock on their sheet. */}
      {/* CENTRED IN THE LEAF. The lock body sits in the middle of the leaf's
          thickness — it was drawn hanging off the face, which is where the
          escutcheon is, not the case. */}
      <rect x={cx - Math.max(0.7, tw * 0.34)} y={floor - vs(hh) - 3}
        width={Math.max(1.4, tw * 0.68)} height="6"
        fill={DOOR_CREAM} stroke={DOOR_INK} strokeWidth={DW.detail} />
      <circle cx={cx} cy={floor - vs(hh)} r="0.6" fill={DOOR_INK} opacity="0.7" />
      <line x1={cx + tw / 2 + 1} y1={floor - vs(hh)} x2={cx + w * 0.16} y2={floor - vs(hh)}
        stroke={DOOR_INK} strokeWidth={DW.thin} />
      <text x={cx + w * 0.17} y={floor - vs(hh) - 0.8} fontSize="1.8" fill={DOOR_INK}
        fontFamily={DOOR_FONT}>LOCK</text>

      {/* Finished floor, and the air gap the room breathes through. */}
      <line x1={cx - w * 0.10} y1={floor} x2={cx + w * 0.16} y2={floor}
        stroke={DOOR_INK} strokeWidth={DW.cut} />
      <text x={cx - w * 0.10} y={floor + 3.2} fontSize="1.8" fill={DOOR_INK}
        fontFamily={DOOR_FONT} opacity="0.6">FFL</text>
      {air > 0 && (
        <g>
          <DoorDim vertical x1={leafBot} x2={floor} y={cx - w * 0.12}
            text={fmt(air)} size={1.7} tone={DOOR_BROWN} />
          <text x={cx - w * 0.12 - 3} y={(leafBot + floor) / 2 + 6} fontSize="1.6"
            fill={DOOR_BROWN} fontFamily={DOOR_FONT} textAnchor="middle">AIR FLOW</text>
        </g>
      )}

      {/* Heights down the right, as their sheet carries them. */}
      <DoorDim vertical x1={leafTop} x2={leafBot} y={x + w - 12}
        text={`${fmt(S.leaf.h)} LEAF HEIGHT`} size={1.9} />
      <DoorDim vertical x1={headTop} x2={floor} y={x + w - 4}
        text={`${fmt(S.frame.h)} FRAME HEIGHT`} size={1.9} />
      <DoorDim vertical x1={floor - vs(hh)} x2={floor} y={cx + w * 0.22}
        text={`${fmt(hh)} HANDLE HEIGHT`} size={1.8} tone={DOOR_BROWN} />
      {/* The three thicknesses this view exists to state, across the foot and
          the head. A side elevation that does not carry them is a picture. */}
      <DoorDim x1={cx - tw / 2} x2={cx + tw / 2} y={floor + 8}
        text={`${fmt(th)} LEAF THK`} size={1.9} tone={DOOR_BROWN} />
      <DoorDim x1={cx - wallHalf} x2={cx + wallHalf} y={headTop - 9}
        text={`${fmt(wallT)} WALL = JAMB DEPTH`} size={1.8} tone={DOOR_BROWN} />
      {trimW > 0 && (
        <DoorDim x1={cx - wallHalf - Math.max(0.7, vs(trimT2))} x2={cx + wallHalf + Math.max(0.7, vs(trimT2))}
          y={headTop - 13} text={`${fmt(wallT + trimT2 * 2)} OVERALL WITH TRIMS`} size={1.7} />
      )}
      <DoorDim vertical x1={headTop} x2={headTop + vs(jambT)} y={cx - wallHalf - 4}
        text={`${fmt(jambT)} JAMB THK`} size={1.7} />
    </g>
  );
}

// ── The finishes footer ────────────────────────────────────────────────────
// The swatches, big enough to judge, along the bottom of the sheet with what
// each one is FOR. They were in the side panel at 8 mm square, which is a
// thumbnail rather than a sample — and an unlabelled swatch is how the hardware
// plating ends up specified on the leaf.
// A trim profile drawn as its own SECTION — the wall behind it, the casing
// standing proud of it, and the shape the design actually names. This is the
// "trim detail from the library" a footer swatch should carry, since a moulding
// has a profile rather than a photograph.
function DoorTrimSectionSwatch({ t, x, y, s }) {
  const wmm = qnum(t.width) || 63.5, tmm = qnum(t.thickness) || 19;
  const design = String(t.design || '').toLowerCase();
  // Fit the section into the square with a margin, keeping width:thickness true.
  const pad = s * 0.14, iw = s - pad * 2, ih = s - pad * 2;
  const k = Math.min(iw / wmm, ih / (tmm * 2.4));       // thickness reads bigger
  const bw = wmm * k, bt = Math.max(1.2, tmm * k * 2.4);
  const bx = x + (s - bw) / 2, by = y + s * 0.62 - bt;
  // The face, described by the design name.
  let face = null;
  if (design.includes('ogee') || design.includes('colonial')) {
    face = <path d={`M ${bx} ${by + bt} L ${bx} ${by + bt * 0.45}
      Q ${bx + bw * 0.22} ${by} ${bx + bw * 0.5} ${by + bt * 0.3}
      Q ${bx + bw * 0.78} ${by + bt * 0.62} ${bx + bw} ${by + bt * 0.1}
      L ${bx + bw} ${by + bt} Z`} fill={DOOR_POCHE} stroke={DOOR_INK} strokeWidth={DW.detail} />;
  } else if (design.includes('bullnose')) {
    face = <path d={`M ${bx} ${by + bt} L ${bx} ${by + bt * 0.5}
      A ${bw / 2} ${bt * 0.5} 0 0 1 ${bx + bw} ${by + bt * 0.5} L ${bx + bw} ${by + bt} Z`}
      fill={DOOR_POCHE} stroke={DOOR_INK} strokeWidth={DW.detail} />;
  } else if (design.includes('chamfer')) {
    face = <path d={`M ${bx} ${by + bt} L ${bx} ${by + bt * 0.35} L ${bx + bw * 0.28} ${by}
      L ${bx + bw} ${by} L ${bx + bw} ${by + bt} Z`}
      fill={DOOR_POCHE} stroke={DOOR_INK} strokeWidth={DW.detail} />;
  } else if (design.includes('step')) {
    face = <path d={`M ${bx} ${by + bt} L ${bx} ${by + bt * 0.55} L ${bx + bw * 0.34} ${by + bt * 0.55}
      L ${bx + bw * 0.34} ${by} L ${bx + bw} ${by} L ${bx + bw} ${by + bt} Z`}
      fill={DOOR_POCHE} stroke={DOOR_INK} strokeWidth={DW.detail} />;
  } else {
    face = <rect x={bx} y={by} width={bw} height={bt} fill={DOOR_POCHE}
      stroke={DOOR_INK} strokeWidth={DW.detail} />;
  }
  return (
    <g>
      <rect x={x} y={y} width={s} height={s} fill="#fff" />
      {/* the wall the casing sits on */}
      <rect x={x + pad * 0.4} y={y + s * 0.62} width={s - pad * 0.8} height={s * 0.2}
        fill="url(#dHatch)" stroke={DOOR_INK} strokeWidth={DW.thin} />
      {face}
      <text x={x + s / 2} y={y + s - 1} fontSize="1.5" fill={DOOR_INK}
        fontFamily={DOOR_FONT} textAnchor="middle" opacity="0.55">SECTION</text>
    </g>
  );
}

// The hinges on a door are the HARDWARE LINE, not a separate field. The line
// carries the product and the quantity, so the schedule and the drawing read it
// and there is only ever one answer. `hingeCount` is still read for a door
// saved before the hardware lines existed.
function doorHinges(res) {
  const d = res.resolved || {};
  const line = (Array.isArray(d.hardware) ? d.hardware : []).find(l => l.slot === 'hinges');
  const qty = line ? (qnum(line.qty) || 0) : 0;
  const name = line && line.ref ? line.ref.name : (d.hingeType || '');
  return { count: qty || qnum(d.hingeCount) || 3, name: name || 'Not selected', chosen: !!line };
}

function DoorViewFinishStrip({ res, ctx, x, y, w, h, system }) {
  const d = res.resolved;
  const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
  // ONLY what was chosen for this opening. This used to fall back to the
  // hardware set's recipe when nothing had been picked — so a door showed
  // ironmongery on its shop drawing that nobody had actually specified, chosen
  // for it by whichever set its lock function pointed at. A drawing must show
  // what was selected, and say nothing when nothing has been.
  const lib = typeof doorCtxLib === 'function' ? doorCtxLib(ctx) : {};
  // In slot order, so the strip reads the same way on every sheet, and only
  // the lines actually answered — a line ruled out N/A is left off, and a line
  // nobody has got to yet says nothing rather than guessing.
  const hw = (Array.isArray(d.hardware) ? d.hardware : [])
    .map(l => {
      const sl = l.slot ? doorSlot(l.slot) : null;
      const order = sl ? DOOR_HARDWARE_SLOTS.indexOf(sl) : 99;
      // A supplier finish is the current shape; an itemId is a line saved
      // before the catalog link and is still read so nothing vanishes.
      if (l.ref) return { name: l.ref.name, cat: (sl && sl.label) || l.ref.cat || '',
                          qty: l.qty || 1, img: l.ref.img || '', order };
      const it = (lib.hardware || []).find(h => h.id === l.itemId);
      return it ? { name: it.name, cat: (sl && sl.label) || it.category, qty: l.qty || 1,
                    img: it.img || '', order } : null;
    }).filter(Boolean).sort((a, b) => a.order - b.order);
  // The trim is a SELECTION like the others — it is bought as a profile from
  // the library, it is what the client sees round the opening, and it was the
  // one part of the assembly with no place in this strip. Its picture is the
  // profile's own if it has one, and its size travels with it.
  const trim = res.trim || null;
  const trimRef = trim
    ? { name: trim.name || trim.design || 'Trim', img: trim.img || trim.imageUrl || '',
        category: [trim.design, trim.material].filter(Boolean).join(' · ') || 'Casing',
        // A trim profile has no photograph — it has a SECTION. Drawing it is
        // both more honest and more useful than an empty swatch: the shape IS
        // what is being specified.
        section: trim }
    : (qnum(d.trimSize) > 0
        ? { name: d.trimType || 'Trim', img: '', category: 'Casing',
            section: { design: d.trimType, width: qnum(d.trimSize), thickness: 19, reveal: 6.35 } }
        : null);
  const trimSub = trim
    ? `${fmt(qnum(trim.width))} × ${fmt(qnum(trim.thickness))}`
    : (qnum(d.trimSize) > 0 ? fmt(qnum(d.trimSize)) : 'Trim');
  const picks = [
    { label: 'DOOR FACE', sub: 'Leaf both sides', ref: d.leafFinishRef },
    { label: 'CORE / EDGE', sub: doorCoreLabel(d.coreType), ref: d.coreFinishRef },
    { label: 'TRIM / CASING', sub: trimSub, ref: trimRef },
    { label: 'HARDWARE', sub: 'Ironmongery finish', ref: d.hardwareFinishRef },
  ];
  // The swatches take the left; the hardware chosen takes the right, because
  // both are "what was selected" and the client asked for them together.
  const hwW = hw.length ? Math.min(w * 0.42, 132) : 0;
  const cw = (w - hwW) / picks.length;
  return (
    <g>
      <rect x={x} y={y} width={w} height={h} fill="#fff" stroke={DOOR_LINE} strokeWidth={DW.detail} />
      <rect x={x} y={y} width={w} height={6} fill={DOOR_CREAM} />
      <text x={x + 2} y={y + 4} fontSize="2.6" fill={DOOR_INK} fontFamily={DOOR_FONT}
        fontWeight="bold" letterSpacing="0.8">FINISHES &amp; SELECTIONS</text>
      <text x={x + w - 2} y={y + 4} fontSize="1.9" fill={DOOR_INK} opacity="0.45"
        fontFamily={DOOR_FONT} textAnchor="end">from the LEON selection library</text>
      {hw.length > 0 && (
        <g>
          <line x1={x + w - hwW} y1={y + 6} x2={x + w - hwW} y2={y + h}
            stroke={DOOR_LINE} strokeWidth={DW.thin} />
          <text x={x + w - hwW + 3} y={y + 11} fontSize="2" fill={DOOR_BROWN}
            fontFamily={DOOR_FONT} fontWeight="bold" letterSpacing="0.4">HARDWARE SELECTED</text>
          {hw.slice(0, 8).map((l, i) => {
            const col = i % 2, row = Math.floor(i / 2);
            const lx = x + w - hwW + 3 + col * (hwW / 2), ly = y + 12 + row * 7.2;
            const th = 5.6;
            return (
              <g key={i}>
                {l.img
                  ? <image href={l.img} x={lx} y={ly} width={th} height={th}
                      preserveAspectRatio="xMidYMid slice" />
                  : <rect x={lx} y={ly} width={th} height={th} fill={DOOR_CREAM} />}
                <rect x={lx} y={ly} width={th} height={th} fill="none"
                  stroke={DOOR_INK} strokeWidth={DW.thin} />
                <text x={lx + th + 1.5} y={ly + 2.6} fontSize="1.8" fill={DOOR_INK} fontFamily={DOOR_FONT}>
                  {l.qty} × {String(l.name).slice(0, 24)}
                </text>
                <text x={lx + th + 1.5} y={ly + 5.2} fontSize="1.5" fill={DOOR_INK}
                  fontFamily={DOOR_FONT} opacity="0.45">
                  {String(l.cat || '').slice(0, 24)}
                </text>
              </g>
            );
          })}
        </g>
      )}
      {picks.map((p, i) => {
        const px = x + i * cw, sw = Math.min(h - 12, 22);
        return (
          <g key={p.label}>
            {i > 0 && <line x1={px} y1={y + 6} x2={px} y2={y + h} stroke={DOOR_LINE} strokeWidth={DW.thin} />}
            {p.ref && p.ref.img
              ? <image href={p.ref.img} x={px + 3} y={y + 9} width={sw} height={sw}
                  preserveAspectRatio="xMidYMid slice" />
              : p.ref && p.ref.section
                ? <DoorTrimSectionSwatch t={p.ref.section} x={px + 3} y={y + 9} s={sw} />
                : <rect x={px + 3} y={y + 9} width={sw} height={sw} fill={DOOR_CREAM} />}
            <rect x={px + 3} y={y + 9} width={sw} height={sw} fill="none"
              stroke={DOOR_INK} strokeWidth={DW.detail} />
            <text x={px + sw + 7} y={y + 13} fontSize="2" fill={DOOR_BROWN} fontFamily={DOOR_FONT}
              fontWeight="bold" letterSpacing="0.4">{p.label}</text>
            <text x={px + sw + 7} y={y + 17.5} fontSize="2.3" fill={DOOR_INK} fontFamily={DOOR_FONT}>
              {p.ref ? String(p.ref.name || '').slice(0, 22) : 'Not selected'}
            </text>
            <text x={px + sw + 7} y={y + 21.5} fontSize="1.8" fill={DOOR_INK}
              fontFamily={DOOR_FONT} opacity="0.5">
              {p.ref ? String(p.ref.category || p.sub).slice(0, 26) : p.sub}
            </text>
            <text x={px + sw + 7} y={y + 25.5} fontSize="1.7" fill={DOOR_INK}
              fontFamily={DOOR_FONT} opacity="0.4">
              APPLIES TO {p.label}
            </text>
          </g>
        );
      })}
    </g>
  );
}

// Fit an object of `mm` across a box of `paper` millimetres, and report the
// scale that achieved it. A generated sheet that leaves its drawing tiny in a
// big frame is the thing that reads as unfinished — and the scale is still
// TRUE, because it is stated rather than assumed.
function doorFitDenom(mm, paper, floorDenom) {
  if (!(mm > 0) || !(paper > 0)) return floorDenom || 20;
  const raw = mm / paper;
  const ladder = SHEET_SCALES.map(x => x.denom).sort((a, b) => a - b);
  return ladder.find(d => d >= raw) || ladder[ladder.length - 1];
}

function DoorShopDrawingPage({ project, ctx, res, size, denom, system, sheetNo, rev, autoFit }) {
  const S = sheetSize(size);
  const m = 7, panelW = 74, gap = 2.5;
  const drawW = S.w - m * 2 - panelW - gap;
  const top = m + 2;
  // The strip grows with the sheet. On A2 there is room to show the finishes at
  // a size someone can actually judge them at, which is the only reason a
  // swatch is on a drawing at all; on A3 it stays at the 42 mm that just fits.
  const footH = Math.max(42, Math.round(S.h * 0.135));
  const bodyH = S.h - m * 2 - 4 - footH - gap;

  // The client's own reading order: the hole, the door in it, the door on
  // edge, the door in the wall, then how it swings. The three elevations get
  // the tall band; the plan and the operation share the one beneath.
  // The bottom row carries two DETAILS now, not two thumbnails, so it needs
  // real height — at 0.66 the swing box was 77 mm tall and the view came out
  // at 1:48, smaller than the elevations above it rather than zoomed in.
  // 0.70. Measured, not guessed: on A2 this is the share at which the
  // elevations reach 1:10 instead of 1:20 — twice the size on the paper — and
  // the bottom row still gets ~98 mm, more than it had on the whole A3 sheet.
  // A 7'0" door's rough opening is 2235 mm, which needs 223.5 mm of drawing
  // height at 1:10; 0.70 gave 220 and fell back to 1:20 by 3 mm.
  // The scale ladder is coarse, so a band share either clears a step or does
  // not; 0.66 was still landing on 1:20 with 30 mm of the box unused.
  // The elevation band takes what the DRAWING needs, not a fixed share. Try the
  // scale ladder from the largest down and stop at the first one whose door —
  // rough opening plus its 30 mm of dimension bands — fits in what is left once
  // the two details below have the ~78 mm they need. Hand-tuning a fraction
  // against one door's height was chasing a moving target: a taller door, a
  // different sheet or a bigger footer all moved it.
  const roNeed = (qnum(res.sizes.ro.h) || 2200) + 0;   // + 35 of bands, below
  const botMin = 78;
  const elevH = (() => {
    const room = bodyH - gap - botMin;
    const ladder = SHEET_SCALES.map(x => x.denom).sort((a, b) => a - b);
    const want = ladder.map(dn => roNeed / dn + 35).filter(hh => hh <= room);
    const best = want.length ? Math.max(...want) : bodyH * 0.6;
    return Math.min(room, Math.max(bodyH * 0.5, best));
  })();
  const botH = bodyH - elevH - gap;
  const colW = (drawW - gap * 2) / 3;
  const halfW = (drawW - gap) / 2;
  const roH = qnum(res.sizes.ro.h) || 2200;
  // 1.45, not 1.25. The factor is the room left for the dimension bands, and
  // at 1.25 the trim dimension across the head ran up into the view's own
  // title bar — measured, not guessed: "DOOR & COMPONENTS" and 2 1/2" shared
  // 5.8 mm of the same line.
  // An explicit BUDGET, not a multiplier: the box has to hold the drawing plus
  // the dimension bands, which are ~12 mm above the head and ~18 mm below the
  // floor, plus the 6 mm title bar. A multiplier hid that and left the trim
  // dimension running up into the title.
  // 30, not 36 — a touch more of the box goes to the drawing now that it is
  // centred rather than pinned to the floor.
  const fitDenom = autoFit ? doorFitDenom(roH, elevH - 30) : denom;
  // The three ELEVATIONS share one scale, so they can be read against each
  // other. Views 4 and 5 are a DETAIL and a DIAGRAM: at the elevation's scale
  // the section is a sliver and the swing is a thumbnail, and neither shows
  // the thing it exists to show. Each takes the largest scale off the ladder
  // that fits its own box, and states it in its header — which is exactly how
  // a detail is drawn on a real sheet.
  const wallT4 = qnum(res.resolved.wallThickness) || 124;
  const trimW4 = qnum(res.trim && res.trim.width) || qnum(res.resolved.trimSize) || 0;
  const detailDenom = doorFitDenom((wallT4 + trimW4 * 0.6) * 2.1, botH - 18);
  // A scale has to fit BOTH dimensions of its box, so take whichever is the
  // tighter of the two. Sizing on width alone is what let the swing pick a
  // scale its own box was far too short for.
  const leafW5 = qnum(res.sizes.leaf.w) || 900;
  const pair5 = /PAIR/.test(res.resolved.handing || '');
  const swingDenom = Math.max(
    doorFitDenom(leafW5 * (pair5 ? 3.0 : 2.0), halfW - 10),   // opening plus the arc
    doorFitDenom(leafW5 * 1.0 + 780, botH - 14)               // the arc, then the person below it
  );

  const V = ({ x, y, w, h, n, title, note, children }) => (
    <g>
      <rect x={x} y={y} width={w} height={h} fill="#fff" stroke={DOOR_LINE} strokeWidth={DW.detail} />
      <rect x={x} y={y} width={w} height={6} fill={DOOR_CREAM} />
      <circle cx={x + 4} cy={y + 3} r="2.1" fill={DOOR_BROWN} />
      <text x={x + 4} y={y + 3.8} fontSize="2.4" fill="#fff" fontFamily={DOOR_FONT}
        textAnchor="middle" fontWeight="bold">{n}</text>
      <text x={x + 8} y={y + 4} fontSize="2.4" fill={DOOR_INK} fontFamily={DOOR_FONT}
        fontWeight="bold" letterSpacing="0.7">{title}</text>
      {/* THE SCALE, on every view, stated not implied. The views are no longer
          all at one scale — the two details are enlarged — so a reader has to
          be told which one they are looking at, on the view itself. */}
      {note && <text x={x + w - 2} y={y + 4} fontSize="2.1" fill={DOOR_BROWN}
        fontFamily={DOOR_FONT} textAnchor="end" fontWeight="bold" letterSpacing="0.4">{note}</text>}
      {children}
    </g>
  );

  return (
    <svg width={`${S.w}mm`} height={`${S.h}mm`} viewBox={`0 0 ${S.w} ${S.h}`}
      style={{ background: '#fff', maxWidth: '100%', height: 'auto' }}
      role="img" aria-label={`Shop drawing — ${res.resolved.mark}`}>
      <DoorSheetDefs />
      <rect x="0" y="0" width={S.w} height={S.h} fill="#ffffff" />
      <rect x={m / 2} y={m / 2} width={S.w - m} height={S.h - m}
        fill="none" stroke={DOOR_BROWN} strokeWidth={DW.outline} />

      <V x={m} y={top} w={colW} h={elevH} n="1" title="ROUGH OPENING" note={`SCALE ${sheetScaleLabel(fitDenom)}`}>
        <DoorViewElevation res={res} x={m} y={top + 6} w={colW} h={elevH - 6}
          denom={fitDenom} system={system} openingOnly />
      </V>
      <V x={m + colW + gap} y={top} w={colW} h={elevH} n="2" title="DOOR &amp; COMPONENTS" note={`SCALE ${sheetScaleLabel(fitDenom)}`}>
        <DoorViewElevation res={res} x={m + colW + gap} y={top + 6} w={colW} h={elevH - 6}
          denom={fitDenom} system={system} />
      </V>
      <V x={m + (colW + gap) * 2} y={top} w={colW} h={elevH} n="3" title="SIDE ELEVATION"
        note={`SCALE ${sheetScaleLabel(fitDenom)}`}>
        <DoorViewSideSection res={res} x={m + (colW + gap) * 2} y={top + 6} w={colW} h={elevH - 6}
          denom={fitDenom} system={system} />
      </V>

      <V x={m} y={top + elevH + gap} w={halfW} h={botH} n="4" title="HEAD / PLAN — JAMB DETAIL" note={`DETAIL — SCALE ${sheetScaleLabel(detailDenom)}`}>
        {/* The SAME scale as the elevations above. It used to be drawn three
            times larger as a detail, which is ordinary drafting but means the
            plan and the elevation cannot be read against each other — and the
            plan's width IS the elevation's width, so at one scale they line
            up, which is the whole use of stacking them. */}
        <DoorViewJamb res={res} x={m} y={top + elevH + gap + 6} w={halfW} h={botH - 6}
          denom={detailDenom} system={system} />
      </V>
      <V x={m + halfW + gap} y={top + elevH + gap} w={halfW} h={botH} n="5" title="DOOR OPERATION" note={`DIAGRAM — SCALE ${sheetScaleLabel(swingDenom)}`}>
        <DoorViewSwing res={res} x={m + halfW + gap} y={top + elevH + gap + 6} w={halfW} h={botH - 6}
          denom={swingDenom} system={system} />
      </V>

      <DoorViewData res={res} ctx={ctx} project={project} sheetNo={sheetNo} rev={rev}
        x={S.w - m - panelW} y={top} w={panelW} h={bodyH} system={system} />
      <DoorViewFinishStrip res={res} ctx={ctx} system={system}
        x={m} y={top + bodyH + gap} w={S.w - m * 2} h={footH} />
    </svg>
  );
}

// ── Getting the sheet OUT: PDF and print ───────────────────────────────────
// Neither goes through the app's document exporter, and that is the whole
// point. `exportPdf` reduces a region to headings, tables and lines of text —
// right for a report, wrong for a drawing: every rotated dimension came out
// horizontal and landed on top of the next one, which is exactly what the
// client saw. A drawing has to leave as a DRAWING.
//
// So the SVG is rasterised at print resolution and placed whole. It keeps every
// rotation, hatch and line weight because nothing re-interprets it.
function doorSheetToPng(svgEl, sheet, dpi) {
  return new Promise((resolve, reject) => {
    // The stylesheet is not inside the SVG, so anything inherited has to be
    // stated on the clone before it leaves the document.
    const clone = svgEl.cloneNode(true);
    clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    clone.setAttribute('width', sheet.w);
    clone.setAttribute('height', sheet.h);
    clone.style.background = '#ffffff';
    const xml = new XMLSerializer().serializeToString(clone);
    const svgUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(xml);
    const px = (dpi || 200) / 25.4;                    // mm -> pixels
    const cw = Math.round(sheet.w * px), ch = Math.round(sheet.h * px);
    const img = new Image();
    img.onload = () => {
      const c = document.createElement('canvas');
      c.width = cw; c.height = ch;
      const g = c.getContext('2d');
      g.fillStyle = '#ffffff'; g.fillRect(0, 0, cw, ch);
      g.drawImage(img, 0, 0, cw, ch);
      resolve(c.toDataURL('image/png'));
    };
    img.onerror = () => reject(new Error('The sheet could not be rasterised.'));
    img.src = svgUrl;
  });
}

async function doorSheetSavePdf(svgEl, sizeKey, filename) {
  const sheet = sheetSize(sizeKey);
  if (!window.jspdf || !window.jspdf.jsPDF) { alert('The PDF library did not load.'); return; }
  const png = await doorSheetToPng(svgEl, sheet, 200);
  const pdf = new window.jspdf.jsPDF({
    orientation: sheet.w >= sheet.h ? 'landscape' : 'portrait',
    unit: 'mm', format: [sheet.w, sheet.h],
    compress: true,
  });
  // Without the compression argument jsPDF embeds the raster raw: the same A3
  // sheet came out at 30 MB against ~1 MB deflated. Nobody emails a 30 MB door.
  pdf.addImage(png, 'PNG', 0, 0, sheet.w, sheet.h, undefined, 'MEDIUM');
  pdf.save(filename);                                   // downloads on its own
}

// Printing takes the same raster, for the same reason, on a page sized to the
// sheet so the scale in the title block stays true.
async function doorSheetPrint(svgEl, sizeKey, title) {
  const sheet = sheetSize(sizeKey);
  const png = await doorSheetToPng(svgEl, sheet, 200);
  const holder = document.createElement('div');
  holder.id = 'leon-door-print';
  holder.innerHTML = `<img src="${png}" style="width:${sheet.w}mm;height:${sheet.h}mm;display:block" alt="">`;
  const style = document.createElement('style');
  style.id = 'leon-door-print-style';
  style.textContent = `@page { size: ${sheet.w}mm ${sheet.h}mm; margin: 0 }
    @media print { body > *:not(#leon-door-print) { display: none !important }
      #leon-door-print { position: absolute; inset: 0; margin: 0 } }
    #leon-door-print { display: none }
    @media print { #leon-door-print { display: block } }`;
  document.body.appendChild(style);
  document.body.appendChild(holder);
  const done = () => {
    holder.remove(); style.remove();
    window.removeEventListener('afterprint', done);
  };
  window.addEventListener('afterprint', done);
  window.print();
  // Safari does not always fire afterprint; clean up regardless.
  setTimeout(done, 60000);
}

// ── The Shop Drawing section ────────────────────────────────────────────────
// ── The submittal package ──────────────────────────────────────────────────
// A submittal is not a folder of files. It is ONE document, in a stated order,
// with a cover that says what is in it — that is what gets issued, what the
// architect marks up, and what comes back. The Hub could produce every page of
// one and had no way to bind them.
//
// This builds it: a cover, the door schedule, one shop drawing per door, the
// finish tags and the keynote answers. It comes out as a single PDF and files
// a RECORD of what was issued against the scope, so the job carries the fact
// even where the file itself is too large to keep in a browser.
function DoorPackagePanel({ ctx, project, system, editable }) {
  const doors = (project && project.doors) || [];
  const scopes = (project && project.scopes) || [];
  const [scopeId, setScopeId] = useState(scopes[0] ? scopes[0].id : '');
  const [picked, setPicked] = useState(() => doors.map(d => d.id));
  const [sizeKey, setSizeKey] = useState('A2');
  const [include, setInclude] = useState({ schedule: true, drawings: true, tags: true, keynotes: true });
  const [rev, setRev] = useState('REV00');
  const [busy, setBusy] = useState(false);
  const [result, setResult] = useState(null);
  const holderRef = useRef(null);

  const chosen = doors.filter(d => picked.indexOf(d.id) >= 0);
  const sheet = sheetSize(sizeKey);
  const scope = scopes.find(s => s.id === scopeId) || null;

  function toggle(id) {
    setPicked(p => p.indexOf(id) >= 0 ? p.filter(x => x !== id) : [...p, id]);
  }

  async function build(fileIt) {
    if (busy) return;
    setBusy(true); setResult(null);
    try {
      const pages = [];
      if (include.schedule) {
        pages.push({
          kind: 'table', title: 'Door schedule',
          columns: DOOR_SCHEDULE_COLUMNS.map(c => c.label),
          rows: chosen.map(d => {
            const r = doorResolve(ctx, project, d);
            const fmt = mm => fmtDim(mm, system, { inchesOnly: true });
            return DOOR_SCHEDULE_COLUMNS.map(c => {
              if (c.key === 'typeName') return r.type ? r.type.name : '';
              if (c.key === 'frameName') return r.frame ? r.frame.name : '';
              if (c.key === 'hardwareSet') return r.hardwareSet ? r.hardwareSet.code : '';
              if (c.key === 'frameW') return fmt(r.sizes.frame.w);
              if (c.key === 'frameH') return fmt(r.sizes.frame.h);
              if (c.key === 'roW') return fmt(r.sizes.ro.w);
              if (c.key === 'roH') return fmt(r.sizes.ro.h);
              if (c.dim) return fmt(qnum(r.resolved[c.key]));
              return String(r.resolved[c.key] === undefined ? (d[c.key] || '') : (r.resolved[c.key] || ''));
            });
          }),
        });
      }
      if (include.drawings) {
        // The sheets are rendered into a hidden holder one at a time and read
        // straight out of the DOM — the same component the Shop Drawing screen
        // uses, so the package cannot differ from what was reviewed on screen.
        for (let i = 0; i < chosen.length; i++) {
          const svg = holderRef.current && holderRef.current.querySelectorAll('svg')[i];
          if (svg) pages.push({ kind: 'drawing', svg, wMm: sheet.w, hMm: sheet.h,
            title: `${chosen[i].mark || 'Door'} — shop drawing` });
        }
      }
      if (include.tags && typeof cwFinishTags === 'function') {
        const tags = cwFinishTags(project, 'doors') || [];
        if (tags.length) pages.push({ kind: 'table', title: 'Finish tags',
          columns: ['Code', 'Description', 'Supplier finish', 'Superseded by'],
          rows: tags.map(t => [t.code, t.description || '', (t.finishRef && t.finishRef.name) || '', t.supersededBy || '']) });
      }
      if (include.keynotes && typeof cwKeynotes === 'function') {
        const kns = cwKeynotes(project, 'doors') || [];
        if (kns.length) pages.push({ kind: 'table', title: 'Keynotes and responses',
          columns: ['No.', 'Keynote', 'Our response', 'Type'],
          rows: kns.map(k => [k.code, k.text || '', k.response || '', k.commitment || '']) });
      }

      const built = await leonBuildPackage(pages, {
        title: 'Door Shop Drawing Submittal',
        project: project.name, scope: scope ? scope.name : '',
        revision: rev, date: todayISO(), preparedBy: ctx.currentUserName,
        number: project.doorSubmittalNo || '',
        note: 'Issued by LEON Integra. Dimensions in inches unless noted. Every drawing in this package '
          + 'is generated from the door records — the schedule and the drawings cannot disagree.',
      });
      const saved = leonSavePackage(built);
      if (fileIt && scope && ctx.addDocument) {
        ctx.addDocument(project.id, scope.id, {
          name: built.fileName, type: 'Shop Drawing',
          date: todayISO(), uploadedBy: ctx.currentUserName,
          notes: `Submittal package ${rev} — ${built.pageCount} pages, ${built.sizeMb} MB. `
            + built.manifest.map(x => x.title).join('; ')
            + (saved ? ' The PDF was downloaded; attach it here to keep it with the job.'
                     : ' The browser would not save the file — build it again from LEON Doors.'),
        });
      }
      setResult({ ...built, saved, filed: !!(fileIt && scope) });
    } catch (e) {
      setResult({ error: e.message || String(e) });
    }
    setBusy(false);
  }

  if (!doors.length) return <EmptyState text="No doors on this job yet. A package is built from them." />;

  return (
    <div className="space-y-3">
      <div>
        <h3 className="font-bold">📦 Submittal package</h3>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
          One document, in order, with a cover that says what is in it &mdash; the schedule, a shop drawing
          for every door, the finish tags and the keynote answers. Every page is generated from the door
          records, so the schedule and the drawings cannot disagree.
        </p>
      </div>

      <div className="grid lg:grid-cols-2 gap-3">
        <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3 space-y-3">
          <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55">
            What goes in
          </div>
          <div className="space-y-1.5">
            {[['schedule', 'Door schedule'], ['drawings', `Shop drawings — one page per door`],
              ['tags', 'Finish tags'], ['keynotes', 'Keynotes and our responses']].map(([k, label]) => (
              <label key={k} className="flex items-center gap-2 text-sm">
                <input type="checkbox" checked={!!include[k]}
                  onChange={e => setInclude(v => ({ ...v, [k]: e.target.checked }))} />
                {label}
              </label>
            ))}
          </div>
          <div className="grid sm:grid-cols-3 gap-2">
            <Field label="Revision">
              <TextInput value={rev} onChange={e => setRev(e.target.value)} />
            </Field>
            <Field label="Sheet size">
              <Select value={sizeKey} onChange={e => setSizeKey(e.target.value)}>
                {SHEET_SIZES.map(x => <option key={x.key} value={x.key}>{x.label}</option>)}
              </Select>
            </Field>
            <Field label="File against">
              <Select value={scopeId} onChange={e => setScopeId(e.target.value)}>
                <option value="">— do not file —</option>
                {scopes.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
              </Select>
            </Field>
          </div>
        </div>

        <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
          <div className="flex items-baseline gap-2 mb-2">
            <span className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55">
              Doors in it
            </span>
            <span className="text-[11px] text-[var(--leon-black)]/45">{chosen.length} of {doors.length}</span>
            <button onClick={() => setPicked(doors.map(d => d.id))}
              className="ml-auto text-[11px] font-semibold text-[var(--leon-brown)]">All</button>
            <button onClick={() => setPicked([])} className="text-[11px] font-semibold text-[var(--leon-brown)]">None</button>
          </div>
          <div className="flex flex-wrap gap-1 max-h-40 overflow-y-auto">
            {doors.map(d => (
              <button key={d.id} onClick={() => toggle(d.id)}
                className={`px-1.5 py-0.5 rounded border text-[11px] font-mono ${picked.indexOf(d.id) >= 0
                  ? 'bg-[var(--leon-brown)] text-white border-[var(--leon-brown)]'
                  : 'border-[var(--leon-line)] hover:border-[var(--leon-brown)]'}`}>
                {d.mark || '—'}
              </button>
            ))}
          </div>
        </div>
      </div>

      <div className="flex items-center gap-2 flex-wrap">
        <Button disabled={busy || !chosen.length} onClick={() => build(true)}>
          {busy ? 'Building…' : '📦 Build and file the package'}
        </Button>
        <Button variant="outline" disabled={busy || !chosen.length} onClick={() => build(false)}>
          Build without filing
        </Button>
        <span className="text-[11px] text-[var(--leon-black)]/45">
          {chosen.length} drawing{chosen.length === 1 ? '' : 's'} at {sheet.label}
          {scope ? ` · filed against ${scope.name}` : ' · not filed'}
        </span>
      </div>

      {result && (
        result.error ? (
          <div className="rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
            The package could not be built: {result.error}
          </div>
        ) : (
          <div className="rounded border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-900">
            <b>{result.fileName}</b> — {result.pageCount} pages, {result.sizeMb} MB.
            {result.saved ? ' Downloaded.' : ' The browser would not save it — try again from a normal window.'}
            {result.filed ? ' Filed against the scope, with what was in it recorded.' : ''}
          </div>
        )
      )}

      {/* The sheets, rendered off-screen so the builder can read them. They are
          the SAME component the Shop Drawing screen renders, which is what
          makes the package match what was reviewed. */}
      <div ref={holderRef} aria-hidden="true"
        style={{ position: 'absolute', left: -100000, top: 0, width: 1, height: 1, overflow: 'hidden' }}>
        {include.drawings && chosen.map(d => {
          const r = doorResolve(ctx, project, d);
          if (!r.sizes || !(r.sizes.leaf.w > 0)) return null;
          return <DoorShopDrawingPage key={d.id} project={project} ctx={ctx} res={r} size={sizeKey}
            denom={20} autoFit system={system} sheetNo={d.mark} rev={rev} />;
        })}
      </div>
    </div>
  );
}

function DoorSheetsPanel({ ctx, project, system }) {
  const doors = (project && project.doors) || [];
  // 'auto' is the default and the honest one: the drawing fills whatever paper
  // is chosen and STATES the scale it landed on. The named scales stay for when
  // a sheet has to be issued at a particular one.
  const [scaleKey, setScaleKey] = useState('auto');
  const [sizeKey, setSizeKey] = useState('A2');    // LEON's standard sheet
  const [idx, setIdx] = useState(0);
  const ref = useRef(null);
  const denom = sheetDenom(scaleKey);

  const resolved = doors.map(d => doorResolve(ctx, project, d))
    .filter(r => r.sizes && r.sizes.leaf.w > 0);
  const at = Math.min(idx, Math.max(0, resolved.length - 1));
  const res = resolved[at];

  if (!doors.length) {
    return <EmptyState text="No doors on this job yet. A shop drawing is generated from the door schedule, so add or import doors first." />;
  }
  return (
    <div className="space-y-3">
      <div>
        <h3 className="font-bold">Shop Drawing</h3>
        <p className="text-sm text-[var(--leon-black)]/55 max-w-3xl">
          <b>One page per door.</b> Front elevation with the leaf, frame and rough opening dimensioned
          and the handle height called out; a jamb section through the wall showing the frame, the trim
          and the wall thickness; head and sill; the swing diagram for this door; and a data panel with
          the selections. Drawn at a stated scale from the same <code>doorResolve</code> the schedule
          reads, so the drawing and the schedule cannot disagree.
        </p>
      </div>

      <div className="flex items-end gap-3 flex-wrap">
        <Field label="Door">
          <Select className="!w-56" value={String(at)} onChange={e => setIdx(Number(e.target.value))}>
            {resolved.map((r, i) => (
              <option key={r.resolved.id} value={i}>
                {r.resolved.mark || `Door ${i + 1}`}{r.resolved.location ? ` — ${r.resolved.location}` : ''}
              </option>
            ))}
          </Select>
        </Field>
        <Field label="Scale">
          <Select className="!w-40" value={scaleKey} onChange={e => setScaleKey(e.target.value)}>
            <option value="auto">Fit to the sheet</option>
            {SHEET_SCALES.map(x => <option key={x.key} value={x.key}>{x.label}</option>)}
          </Select>
        </Field>
        <Field label="Sheet size">
          <Select className="!w-48" value={sizeKey} onChange={e => setSizeKey(e.target.value)}>
            {SHEET_SIZES.map(x => <option key={x.key} value={x.key}>{x.label}</option>)}
          </Select>
        </Field>
        <div className="flex items-end gap-1">
          <Button size="sm" variant="ghost" disabled={at === 0} onClick={() => setIdx(at - 1)}>&larr;</Button>
          <span className="text-xs text-[var(--leon-black)]/50 pb-2">{at + 1} of {resolved.length}</span>
          <Button size="sm" variant="ghost" disabled={at >= resolved.length - 1} onClick={() => setIdx(at + 1)}>&rarr;</Button>
        </div>
        <div className="ml-auto flex items-end gap-1.5">
          <IconAction icon="🖨" title="Print this sheet at its paper size"
            onClick={() => {
              const svg = ref.current && ref.current.querySelector('svg');
              if (svg) doorSheetPrint(svg, sizeKey, project.name);
            }} />
          <IconAction icon="📄" title="Save this sheet as a PDF"
            onClick={async () => {
              const svg = ref.current && ref.current.querySelector('svg');
              if (!svg) return;
              const mark = (res && res.resolved.mark) || 'door';
              try {
                await doorSheetSavePdf(svg, sizeKey, `${project.name} — ${mark} — shop drawing.pdf`);
              } catch (e) { alert(e.message || 'The PDF could not be written.'); }
            }} />
        </div>
      </div>

      {res && <DoorIssueList issues={res.issues} />}

      <div ref={ref} data-print-region="Door shop drawing"
        className="overflow-auto rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]/40 p-3">
        {/* autoFit was never passed, so every sheet used the dropdown's own
            default of 1:20 and the fitting code inside had never once run —
            which is why a bigger sheet never produced a bigger drawing.
            A comment cannot sit inside `{cond && ( <X/> )}` — that is one
            expression, not JSX children. */}
        {res && (
          <DoorShopDrawingPage project={project} ctx={ctx} res={res} size={sizeKey} denom={denom}
            autoFit={scaleKey === 'auto'}
            system={system} sheetNo={`D-${String(at + 1).padStart(2, '0')}`}
            rev={project.doorRevision || 'R1'} />
        )}
      </div>

      <p className="text-[11px] text-[var(--leon-black)]/45 max-w-3xl">
        The three elevations share ONE scale, so they can be read against each other; the jamb detail
        and the operation diagram are enlarged, as details are. Every view states its own scale in its
        header. A browser prints
        through the page, so the paper size here sets the DRAWING size &mdash; choose the matching paper
        and turn scaling off, or the scale in the title block stops being true.
      </p>
    </div>
  );
}

// ── Picking a supplier finish for a door ───────────────────────────────────
// The same catalog the Selection Hub and the quote read, so a door, a
// quotation and the order placed from them cannot describe three different
// products. Stores the ~196-byte reference, never a copy of the record.
function DoorFinishPicker({ label, hint, value, onChange, editable }) {
  const [open, setOpen] = useState(false);
  const [sup, setSup] = useState('');
  const [q, setQ] = useState('');
  const groups = typeof supplierGroups === 'function' ? supplierGroups() : [];
  const hits = open && typeof searchSupplierFinishes === 'function'
    ? searchSupplierFinishes(sup || null, null, q, 24) : [];
  return (
    <Field label={label} hint={hint}>
      <div className="flex items-center gap-2">
        {value && value.img && (
          <img src={value.img} alt="" className="w-8 h-8 rounded object-cover border border-[var(--leon-line)]" />
        )}
        <span className="text-xs min-w-0 flex-1 truncate">
          {value ? (value.name || '—') : <span className="text-[var(--leon-black)]/35">Not selected</span>}
        </span>
        {editable && (
          <>
            <button type="button" onClick={() => setOpen(true)}
              className="text-[11px] font-semibold text-[var(--leon-brown)] whitespace-nowrap">
              {value ? 'Change' : 'Choose'}
            </button>
            {value && (
              <button type="button" onClick={() => onChange(null)}
                className="text-[11px] text-[var(--leon-black)]/35 hover:text-[var(--leon-red)]">clear</button>
            )}
          </>
        )}
      </div>
      <Modal open={open} onClose={() => setOpen(false)} size="lg" title={`Choose — ${label}`}>
        <div className="space-y-3">
          <div className="flex items-end gap-2 flex-wrap">
            <Field label="Supplier">
              <Select className="!w-56" value={sup} onChange={e => setSup(e.target.value)}>
                <option value="">Every supplier</option>
                {groups.map(g => <option key={g.key} value={g.key}>{g.label} ({g.count})</option>)}
              </Select>
            </Field>
            <Field label="Search" className="flex-1">
              <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Name, colour or code" />
            </Field>
          </div>
          <div className="grid gap-2 max-h-[52vh] overflow-y-auto"
            style={{ gridTemplateColumns: 'repeat(auto-fill,minmax(120px,1fr))' }}>
            {hits.map(r => (
              <button key={r.sup + r.id} type="button"
                onClick={() => { onChange(makeSupplierFinishRef(r)); setOpen(false); }}
                className="text-left rounded-lg border border-[var(--leon-line)] overflow-hidden hover:border-[var(--leon-brown)]">
                {r.img
                  ? <img src={r.img} alt="" loading="lazy" className="w-full h-16 object-cover" />
                  : <div className="w-full h-16 bg-[var(--leon-cream)]" />}
                <div className="p-1.5">
                  <div className="text-[11px] font-semibold leading-tight truncate">{r.name}</div>
                  <div className="text-[10px] text-[var(--leon-black)]/45 truncate">{r.cat || ''}</div>
                </div>
              </button>
            ))}
            {!hits.length && (
              <p className="text-xs text-[var(--leon-black)]/45 col-span-full py-6 text-center">
                Nothing matches. Try another supplier, or clear the search.
              </p>
            )}
          </div>
        </div>
      </Modal>
    </Field>
  );
}

// ── The hardware actually on this door ─────────────────────────────────────
// Every line is answered from SUPPLIER FINISHES — the same catalog the
// Selection Hub, the quotation and the order all read, so a door, a quote and
// the purchase order cannot describe three different products. LEON's hardware
// is entered there, which is why there is no second hardware library to keep in
// step with it.
//
// A line is answered one of three ways, and the third is the point: a chosen
// model, N/A (ruled out, and left off the drawing), or nothing yet. A tick list
// cannot tell "this door has no closer" from "nobody has got to it".
function DoorHardwarePicker({ ctx, value, na, onChange, onNa, editable }) {
  const list = Array.isArray(value) ? value : [];
  const naList = Array.isArray(na) ? na : [];
  const [pick, setPick] = useState(null);              // the slot being filled
  const [sup, setSup] = useState('');
  const [q, setQ] = useState('');
  const lib = doorCtxLib(ctx);                          // legacy items, read only

  const lineFor = k => list.find(l => l.slot === k) || null;
  // A line saved before the catalog link carried an itemId into the old door
  // hardware library. Read it so nothing already specified disappears.
  const shown = l => {
    if (!l) return null;
    if (l.ref) return { name: l.ref.name, img: l.ref.img, sub: l.ref.cat || l.ref.supplierName || '' };
    const it = (lib.hardware || []).find(h => h.id === l.itemId);
    return it ? { name: it.name, img: it.img, sub: it.manufacturer || it.category || '' } : null;
  };

  function setLine(k, ref, qty) {
    const next = list.filter(l => l.slot !== k);
    if (ref) next.push({ slot: k, ref, qty: qty || 1 });
    onNa(naList.filter(x => x !== k));                 // choosing a model un-rules-it-out
    onChange(next);
  }
  function setNa(k, on) {
    if (on) { onChange(list.filter(l => l.slot !== k)); onNa(naList.concat(naList.indexOf(k) >= 0 ? [] : [k])); }
    else onNa(naList.filter(x => x !== k));
  }

  const slot = pick ? doorSlot(pick) : null;
  const groups = typeof supplierGroups === 'function' ? supplierGroups() : [];
  // Seed the search with the line's own name, so opening "Door Closer" already
  // shows closers rather than the whole catalog.
  const term = q || (slot ? String(slot.label).split(/[/(]/)[0].trim() : '');
  const hits = pick && typeof searchSupplierFinishes === 'function'
    ? searchSupplierFinishes(sup || null, null, term, 36) : [];

  const answered = DOOR_HARDWARE_SLOTS.filter(sl => lineFor(sl.key) || naList.indexOf(sl.key) >= 0).length;

  return (
    <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-hidden">
      <div className="flex items-baseline gap-2 px-3 py-2 bg-[var(--leon-cream)]">
        <span className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55">
          Hardware on this door
        </span>
        <span className="text-[11px] text-[var(--leon-black)]/45">
          {answered} of {DOOR_HARDWARE_SLOTS.length} lines answered
        </span>
      </div>
      <p className="px-3 pt-2 text-[11px] text-[var(--leon-black)]/50">
        Every line a LEON door can carry, each picked from <b>Supplier Finishes</b> &mdash; the same
        catalog the selections and the order read. The picture goes on the shop drawing. Mark a line
        <b> N/A</b> and it is left off the drawing and the schedule; a line left blank has not been
        answered yet, which is not the same thing.
      </p>
      <div className="divide-y divide-[var(--leon-line)] mt-2">
        {DOOR_HARDWARE_SLOTS.map(sl => {
          const line = lineFor(sl.key);
          const it = shown(line);
          const isNa = naList.indexOf(sl.key) >= 0;
          return (
            <div key={sl.key} className={`flex items-center gap-2.5 px-3 py-1.5 ${isNa ? 'opacity-45' : ''}`}>
              <div className="w-9 h-9 rounded border border-[var(--leon-line)] bg-[var(--leon-cream)]/60 shrink-0 overflow-hidden flex items-center justify-center">
                {it && it.img
                  ? <img src={it.img} alt="" className="w-full h-full object-cover" />
                  : <span className="text-[9px] text-[var(--leon-black)]/30">{isNa ? 'N/A' : '—'}</span>}
              </div>
              <div className="min-w-0 flex-1">
                <div className="text-xs font-semibold truncate">{sl.label}</div>
                <div className="text-[11px] text-[var(--leon-black)]/50 truncate">
                  {isNa ? 'Not applicable to this door'
                    : it ? `${it.name}${it.sub ? ` · ${it.sub}` : ''}`
                    : (sl.note || 'Not chosen yet')}
                </div>
              </div>
              {line && !isNa && (
                <input type="number" min="1" value={line.qty} disabled={!editable}
                  title="How many on this door"
                  onChange={e => setLine(sl.key, line.ref, Math.max(1, Number(e.target.value) || 1))}
                  className="w-12 px-1 py-0.5 text-[11px] border border-[var(--leon-line)] rounded text-right" />
              )}
              {editable && (
                <>
                  <button onClick={() => { setPick(sl.key); setQ(''); setSup(''); }}
                    className="text-[11px] font-semibold text-[var(--leon-brown)] whitespace-nowrap">
                    {it ? 'Change' : 'Choose…'}
                  </button>
                  <label className="flex items-center gap-1 text-[11px] text-[var(--leon-black)]/55 whitespace-nowrap"
                    title="Rule this line out — it is then left off the shop drawing">
                    <input type="checkbox" checked={isNa} onChange={e => setNa(sl.key, e.target.checked)} />
                    N/A
                  </label>
                </>
              )}
            </div>
          );
        })}
      </div>

      <Modal open={!!pick} onClose={() => setPick(null)} size="lg"
        title={slot ? `${slot.label} — from Supplier Finishes` : ''}>
        <div className="space-y-3">
          <div className="flex items-end gap-2 flex-wrap">
            <Field label="Supplier">
              <Select className="!w-56" value={sup} onChange={e => setSup(e.target.value)}>
                <option value="">Every supplier</option>
                {groups.map(g => <option key={g.key} value={g.key}>{g.label} ({g.count})</option>)}
              </Select>
            </Field>
            <Field label="Search" className="flex-1"
              hint={slot && !q ? `Showing “${term}” — clear or retype to search the whole catalog.` : ''}>
              <TextInput value={q} onChange={e => setQ(e.target.value)}
                placeholder={slot ? slot.label : 'Name, model or code'} />
            </Field>
          </div>
          <div className="grid gap-2 max-h-[52vh] overflow-y-auto"
            style={{ gridTemplateColumns: 'repeat(auto-fill,minmax(120px,1fr))' }}>
            {hits.map(rr => (
              <button key={rr.sup + rr.id} type="button"
                onClick={() => { setLine(pick, makeSupplierFinishRef(rr), (lineFor(pick) || {}).qty || slot.qty || 1); setPick(null); }}
                className="text-left rounded-lg border border-[var(--leon-line)] overflow-hidden hover:border-[var(--leon-brown)]">
                {rr.img
                  ? <img src={rr.img} alt="" loading="lazy" className="w-full h-16 object-cover" />
                  : <div className="w-full h-16 bg-[var(--leon-cream)]" />}
                <div className="p-1.5">
                  <div className="text-[11px] font-semibold leading-tight truncate">{rr.name}</div>
                  <div className="text-[10px] text-[var(--leon-black)]/45 truncate">{rr.cat || ''}</div>
                </div>
              </button>
            ))}
            {!hits.length && (
              <p className="text-xs text-[var(--leon-black)]/45 col-span-full py-6 text-center">
                Nothing matches. LEON's hardware is entered under <b>LEON Collection → Supplier
                Finishes</b>; anything added there is offered here straight away.
              </p>
            )}
          </div>
          {lineFor(pick) && (
            <button onClick={() => { setLine(pick, null); setPick(null); }}
              className="text-xs font-semibold text-[var(--leon-brown)]">Clear this line</button>
          )}
        </div>
      </Modal>
    </div>
  );
}

function DoorIssueList({ issues }) {
  if (!issues || !issues.length) return null;
  const tone = l => l === 'error' ? 'bg-red-50 border-red-200 text-red-700'
    : l === 'warn' ? 'bg-amber-50 border-amber-200 text-amber-900'
    : 'bg-[var(--leon-cream)] border-[var(--leon-line)] text-[var(--leon-black)]/70';
  return (
    <div className="space-y-1.5">
      {issues.map((i, k) => (
        <div key={k} className={`rounded border text-xs px-2.5 py-1.5 ${tone(i.level)}`}>{i.msg}</div>
      ))}
    </div>
  );
}

// A dimension field that speaks the user's units both ways: it shows the stored
// millimetres formatted, and accepts 3'-0", 36 1/2", 914mm or a bare number.
function DimField({ value, onChange, system, placeholder, w, disabled }) {
  const [draft, setDraft] = useState(null);
  const shown = draft !== null ? draft : (value === null || value === undefined ? '' : fmtDim(value, system, { inchesOnly: true }));
  return (
    <input type="text" value={shown} placeholder={placeholder} disabled={disabled}
      onChange={e => setDraft(e.target.value)}
      onBlur={e => {
        setDraft(null);
        const t = e.target.value.trim();
        if (!t) { onChange(null); return; }
        const mm = parseDim(t, system);
        if (mm !== null) onChange(mm);
      }}
      className={`${w || 'w-24'} px-2 py-1 text-sm border border-[var(--leon-line)] rounded bg-white focus:outline-none focus:border-[var(--leon-brown)] disabled:bg-[var(--leon-cream)]`} />
  );
}

// ── The module ────────────────────────────────────────────────────────────
function DoorSoftware({ ctx }) {
  const [section, setSection] = useState('dashboard');
  const [projectId, setProjectId] = useState('');
  const [system, setSystem] = useState('Imperial');
  const [editingDoor, setEditingDoor] = useState(null);

  // Real jobs, plus this person's own unassigned workspace. Work often

  // starts before there is a job to attach it to — and sometimes never

  // becomes one — so no tool should demand a project before it will open.

  const allForTools = typeof ctx.toolProjects === 'function' ? ctx.toolProjects() : (ctx.projects || []);

  const projects = ctx.deptProjects(allForTools);

  const scratchIds = new Set((ctx.scratchProjects || []).map(p => p.id));
  const project = projects.find(p => p.id === projectId) || null;
  const editable = ctx.canEdit('softwares');
  // The door LIBRARY is a separate right from using the software: it holds the
  // standards every future door inherits, and it is persisted globally (state
  // `doorLibrary`), so an edit here is a change to the company's own doors and
  // not to the drawing that happens to be open.
  const libEditable = editable && ctx.canEditDoorLibrary !== false;

  // A door module without a project selected is a library browser, which is a
  // legitimate thing to want — so the project picker never blocks the libraries.
  const needsProject = ['schedule', 'designer', 'types', 'sheets', 'package', 'tags', 'keynotes', 'submittals'].includes(section);

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-4 flex-wrap">
        <div>
          <h2 className="text-xl font-bold">🚪 LEON Doors</h2>
          <p className="text-sm text-[var(--leon-black)]/60 max-w-2xl">
            Every door is one record. The schedule, the elevation, the hardware and production all read
            it, so a width changed in the schedule is the same width the drawing redraws from.
          </p>
        </div>
        <div className="flex items-end gap-2 flex-wrap">
          <Field label="Project">
            <Select className="!w-56" value={scratchIds.has(projectId) ? '__scratch' : projectId}
              onChange={e => { const v = e.target.value; const id = v === '__scratch' && typeof ctx.myScratchProject === 'function' ? ctx.myScratchProject().id : v; setProjectId(id); }}>
              <option value="">— select a project —</option>
              {projects.filter(p => !scratchIds.has(p.id)).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
              <option value="__scratch">— Not linked to a job (my workspace) —</option>
            </Select>
          </Field>
          <Field label="Units">
            <Select className="!w-32" value={system} onChange={e => setSystem(e.target.value)}>
              {DOOR_UNIT_SYSTEMS.map(u => <option key={u}>{u}</option>)}
            </Select>
          </Field>
        </div>
      </div>


      <SoftwareRail swKey="doors" sections={DOOR_SW_SECTIONS} active={section}
        onSelect={setSection}
        status={project ? <>
          <span>{(project.doors || []).length} door{(project.doors || []).length === 1 ? '' : 's'}</span>
          <span className="opacity-40">·</span>
          <span>{(project.doors || []).reduce((a, d) => a + (Number(d.qty) || 1), 0)} leaf total</span>
          <span className="opacity-40">·</span>
          <span>{system}</span>
        </> : null}>
      {needsProject && !project ? (
        <div className="rounded-lg border border-dashed border-[var(--leon-line)] p-8 text-center">
          <div className="text-3xl mb-2">🚪</div>
          <div className="font-semibold mb-1">Pick a project</div>
          <div className="text-sm text-[var(--leon-black)]/55">
            Door records belong to a job. The libraries above are shared and can be browsed without one.
          </div>
        </div>
      ) : (
        <>
          {section === 'dashboard' && <DoorDashboard ctx={ctx} projects={projects} system={system} onOpen={(pid, s) => { setProjectId(pid); setSection(s); }} />}
          {section === 'schedule' && <DoorSchedule ctx={ctx} project={project} system={system} editable={editable} onEdit={setEditingDoor} />}
          {section === 'designer' && <DoorDesigner ctx={ctx} project={project} system={system} editable={editable} door={editingDoor} onDone={() => setEditingDoor(null)} />}
          {/* DOOR SETTINGS is a narrower authority than the tool. These are
              company standards every future door inherits — the opening rules
              the shop builds to, the frames, the trims, the hardware — so
              changing one is not the same act as drawing a door with them.
              `libEditable` is the capability; the panels stay READABLE to
              anyone who can open the software, because a coordinator needs to
              look up what a rule says without being able to move it. */}
          {section === 'models' && <DoorModelsPanel ctx={ctx} />}
          {section === 'types' && <DoorTypesPanel ctx={ctx} project={project} system={system} editable={libEditable} />}
          {section === 'global' && <DoorGlobalLibrary ctx={ctx} system={system} editable={libEditable} />}
          {section === 'frames' && <DoorFramesPanel ctx={ctx} system={system} editable={libEditable} />}
          {section === 'trims' && <DoorTrimsPanel ctx={ctx} system={system} editable={libEditable} />}
          {section === 'rules' && <DoorRulesPanel ctx={ctx} system={system} editable={libEditable} />}
          {section === 'designs' && <DoorDesignsPanel ctx={ctx} system={system} editable={libEditable} />}
          {section === 'hardware' && <DoorHardwarePanel ctx={ctx} editable={libEditable} />}
          {section === 'sheets' && <DoorSheetsPanel ctx={ctx} project={project} system={system} />}
          {/* Defined in casework.jsx, which loads AFTER this file. A top-level
              function declaration there is a global by the time anything
              renders, but the guard turns a load-order change into a named
              notice instead of a blank tab — the pattern OfficeHome uses. */}
          {section === 'tags' && (typeof CwFinishTagsPanel === 'function'
            ? <CwFinishTagsPanel ctx={ctx} project={project} editable={editable} discipline="doors" />
            : <EmptyState text="Finish tags are unavailable — the casework module did not load." />)}
          {section === 'keynotes' && (typeof CwKeynotesPanel === 'function'
            ? <CwKeynotesPanel ctx={ctx} project={project} editable={editable} discipline="doors" />
            : <EmptyState text="Keynotes are unavailable — the casework module did not load." />)}
          {section === 'package' && <DoorPackagePanel ctx={ctx} project={project} system={system} editable={editable} />}
          {section === 'submittals' && (typeof CwSubmittalsPanel === 'function'
            ? <CwSubmittalsPanel ctx={ctx} project={project} editable={editable} discipline="doors" />
            : <EmptyState text="Submittals are unavailable — the casework module did not load." />)}
        </>
      )}
      </SoftwareRail>
    </div>
  );
}

function DoorDashboard({ ctx, projects, system, onOpen }) {
  const rows = projects.map(p => {
    const doors = p.doors || [];
    const byStatus = {};
    doors.forEach(d => { byStatus[d.status || 'Draft'] = (byStatus[d.status || 'Draft'] || 0) + 1; });
    return { p, doors, types: (p.doorTypes || []).length, byStatus,
             qty: doors.reduce((a, d) => a + (Number(d.qty) || 1), 0) };
  }).filter(r => r.doors.length || r.types);
  const lib = doorCtxLib(ctx);
  const total = rows.reduce((a, r) => a + r.qty, 0);
  return (
    <div className="space-y-4">
      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
        {[['Doors on the books', total], ['Projects with doors', rows.length],
          ['Global types', (lib.types || []).length], ['Frames', (lib.frames || []).length],
          ['Opening rules', (lib.rules || []).length]].map(([k, v]) => (
          <div key={k} className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
            <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">{k}</div>
            <div className="text-2xl font-bold text-[var(--leon-brown)]">{v}</div>
          </div>
        ))}
      </div>
      {!rows.length && <EmptyState text="No doors yet. Pick a project and open the Door Designer, or start from the Global Library." />}
      <div className="grid gap-3 md:grid-cols-2">
        {rows.map(r => (
          <button key={r.p.id} onClick={() => onOpen(r.p.id, 'schedule')}
            className="text-left rounded-lg border border-[var(--leon-line)] bg-white p-3 hover:border-[var(--leon-brown)]">
            <div className="font-bold">{r.p.name}</div>
            <div className="text-xs text-[var(--leon-black)]/50 mb-2">
              {r.doors.length} mark{r.doors.length === 1 ? '' : 's'} · {r.qty} leaf total · {r.types} project type{r.types === 1 ? '' : 's'}
            </div>
            <div className="flex flex-wrap gap-1">
              {Object.entries(r.byStatus).map(([k, v]) => <Badge key={k}>{k} {v}</Badge>)}
            </div>
          </button>
        ))}
      </div>
    </div>
  );
}

// ── Door Schedule ─────────────────────────────────────────────────────────
// The schedule is not a report of the doors — it IS the doors, in a table.
// Editing a cell here writes the same record the designer edits, which is the
// only way the two can never disagree.
const DOOR_SCHEDULE_COLUMNS = [
  { key: 'mark', label: 'Mark', w: 'w-20' },
  { key: 'typeName', label: 'Type', w: 'w-32', derived: true },
  { key: 'location', label: 'Location', w: 'w-36' },
  { key: 'fromRoom', label: 'From', w: 'w-28' },
  { key: 'toRoom', label: 'To', w: 'w-28' },
  { key: 'qty', label: 'Qty', w: 'w-14' },
  { key: 'leafW', label: 'Leaf W', dim: true },
  { key: 'leafH', label: 'Leaf H', dim: true },
  { key: 'leafThickness', label: 'Thk', dim: true },
  { key: 'frameName', label: 'Frame', derived: true },
  // The FRAME OVERALL was missing, and it is the dimension the shop actually
  // builds to — the client's own schedule carries leaf, frame and rough opening
  // as three separate pairs, because three different trades read them.
  { key: 'frameW', label: 'Frame W', dim: true, derived: true },
  { key: 'frameH', label: 'Frame H', dim: true, derived: true },
  { key: 'roW', label: 'RO W', dim: true, derived: true },
  { key: 'roH', label: 'RO H', dim: true, derived: true },
  { key: 'handing', label: 'Handing' },
  { key: 'hardwareSet', label: 'HW Set', derived: true },
  { key: 'hwSetCode', label: 'HW Code', w: 'w-20' },
  { key: 'leafCode', label: 'Leaf Code', w: 'w-20' },
  { key: 'installationType', label: 'Supplied as', w: 'w-24' },
  { key: 'fireRating', label: 'Fire' },
  { key: 'status', label: 'Status' },
];

function DoorSchedule({ ctx, project, system, editable, onEdit }) {
  const [q, setQ] = useState('');
  const [sel, setSel] = useState([]);
  const [bulk, setBulk] = useState(false);
  const [adding, setAdding] = useState(false);
  const [importing, setImporting] = useState(false);
  const doors = project.doors || [];
  const rows = doors
    .map(d => ({ door: d, ...doorResolve(ctx, project, d) }))
    .filter(r => {
      if (!q.trim()) return true;
      const hay = `${r.door.mark} ${r.door.location} ${r.door.fromRoom} ${r.door.toRoom} ${(r.type || {}).code || ''}`.toLowerCase();
      return hay.includes(q.trim().toLowerCase());
    });

  function setDoor(id, fields) {
    ctx.updateProject(project.id, draft => {
      const d = (draft.doors || []).find(x => x.id === id);
      if (!d) return;
      Object.assign(d, fields);
      ctx.logAction(draft, `Door ${d.mark || d.id}: ${Object.keys(fields).join(', ')} changed.`);
    });
  }
  function addDoor() {
    const n = doors.length + 1;
    ctx.updateProject(project.id, draft => {
      if (!Array.isArray(draft.doors)) draft.doors = [];
      draft.doors.push(makeDoor({ mark: `D-${String(100 + n)}` }, ctx.currentUserName));
      ctx.logAction(draft, `Added door D-${String(100 + n)}.`);
    });
    setAdding(false);
  }
  function removeDoor(id) {
    ctx.updateProject(project.id, draft => {
      const d = (draft.doors || []).find(x => x.id === id);
      draft.doors = (draft.doors || []).filter(x => x.id !== id);
      if (d) ctx.logAction(draft, `Removed door ${d.mark}.`);
    });
  }

  const flagged = rows.filter(r => !r.ok).length;

  return (
    <div className="space-y-3">
      <div className="flex items-center gap-2 flex-wrap">
        <TextInput className="!w-64" value={q} onChange={e => setQ(e.target.value)} placeholder="Search mark, location, room…" />
        <span className="text-xs text-[var(--leon-black)]/50">
          {rows.length} mark{rows.length === 1 ? '' : 's'} · {rows.reduce((a, r) => a + (Number(r.door.qty) || 1), 0)} leaves
          {flagged ? ` · ${flagged} need attention` : ''}
        </span>
        <div className="ml-auto flex items-center gap-2">
          {!!sel.length && editable && <Button size="sm" variant="ghost" onClick={() => setBulk(true)}>Edit {sel.length} selected</Button>}
          {editable && <Button size="sm" variant="ghost" onClick={() => setImporting(true)}>📋 From a schedule</Button>}
          {editable && <Button size="sm" onClick={addDoor}>+ Add door</Button>}
        </div>
      </div>

      <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-x-auto">
        <table className="w-full text-xs min-w-[1180px]">
          <thead>
            <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
              <th className="px-2 py-2 w-8">
                <input type="checkbox" checked={!!rows.length && sel.length === rows.length}
                  onChange={e => setSel(e.target.checked ? rows.map(r => r.door.id) : [])} />
              </th>
              {DOOR_SCHEDULE_COLUMNS.map(c => <th key={c.key} className="px-2 py-2">{c.label}</th>)}
              <th className="px-2 py-2 w-8"></th>
            </tr>
          </thead>
          <tbody>
            {rows.map(r => {
              const d = r.door, R = r.resolved;
              const own = f => R.ownFields.includes(f);
              // Sizing from the rough opening makes the leaf a derived figure.
              const fromRo = R.sizeMethod === 'ro';
              return (
                <tr key={d.id} className={`border-b border-[var(--leon-line)]/60 ${!r.ok ? 'bg-red-50/40' : ''}`}>
                  <td className="px-2 py-1">
                    <input type="checkbox" checked={sel.includes(d.id)}
                      onChange={e => setSel(e.target.checked ? [...sel, d.id] : sel.filter(x => x !== d.id))} />
                  </td>
                  <td className="px-2 py-1">
                    <input value={d.mark} disabled={!editable} onChange={e => setDoor(d.id, { mark: e.target.value })}
                      className="w-20 px-1 py-0.5 font-bold bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                  </td>
                  <td className="px-2 py-1">
                    <select value={d.typeId || ''} disabled={!editable} onChange={e => setDoor(d.id, { typeId: e.target.value || null })}
                      className="w-32 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                      <option value="">— none —</option>
                      {doorTypesFor(ctx, project).map(t => <option key={t.id} value={t.id}>{t.code} · {t.name}</option>)}
                    </select>
                  </td>
                  {['location', 'fromRoom', 'toRoom'].map(f => (
                    <td key={f} className="px-2 py-1">
                      <input value={d[f] || ''} disabled={!editable} onChange={e => setDoor(d.id, { [f]: e.target.value })}
                        className="w-28 px-1 py-0.5 bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                    </td>
                  ))}
                  <td className="px-2 py-1">
                    <input value={d.qty} disabled={!editable} onChange={e => setDoor(d.id, { qty: Number(e.target.value) || 1 })}
                      className="w-12 px-1 py-0.5 text-right bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                  </td>
                  {/* Sizes: typing here overrides the type for this door only, and the
                      cell is tinted so an override is never invisible. */}
                  {/* When a door is sized from the ROUGH OPENING the leaf is
                      DERIVED, and the stored leafW/leafH are whatever the type
                      last said — so printing them here put a 32" leaf in the
                      same row as a 35 1/4" frame computed from the opening. A
                      schedule that contradicts itself is how a door gets built
                      wrong, so a derived leaf is shown as the computed figure
                      and is not editable; edit the rough opening instead. */}
                  {[['leafW', R.leafW, r.sizes.leaf.w], ['leafH', R.leafH, r.sizes.leaf.h],
                    ['leafThickness', R.leafThickness, R.leafThickness]].map(([f, v, derived]) => (
                    <td key={f}
                      className={`px-2 py-1 ${fromRo && f !== 'leafThickness' ? 'text-[var(--leon-black)]/70 italic' : (own(f) ? 'bg-amber-50' : '')}`}
                      title={fromRo && f !== 'leafThickness'
                        ? 'Derived from the rough opening — change the RO to change it'
                        : (own(f) ? 'Set on this door, not inherited from its type' : 'Inherited from the door type')}>
                      {fromRo && f !== 'leafThickness'
                        ? <span className="whitespace-nowrap">{fmtDim(derived, system, { inchesOnly: true })}</span>
                        : <DimField value={v} system={system} disabled={!editable} w="w-20"
                            onChange={mm => setDoor(d.id, { [f]: mm })} />}
                    </td>
                  ))}
                  <td className="px-2 py-1 text-[var(--leon-black)]/60">{r.frame ? r.frame.name : '—'}</td>
                  <td className="px-2 py-1 whitespace-nowrap">{fmtDim(r.sizes.frame.w, system, { inchesOnly: true })}</td>
                  <td className="px-2 py-1 whitespace-nowrap">{fmtDim(r.sizes.frame.h, system, { inchesOnly: true })}</td>
                  <td className="px-2 py-1 text-[var(--leon-brown)] font-semibold whitespace-nowrap">{fmtDim(r.sizes.ro.w, system, { inchesOnly: true })}</td>
                  <td className="px-2 py-1 text-[var(--leon-brown)] font-semibold whitespace-nowrap">{fmtDim(r.sizes.ro.h, system, { inchesOnly: true })}</td>
                  <td className={`px-2 py-1 ${own('handing') ? 'bg-amber-50' : ''}`}>
                    <select value={R.handing} disabled={!editable} onChange={e => setDoor(d.id, { handing: e.target.value })}
                      className="w-24 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                      {DOOR_HANDINGS.map(h => <option key={h.key} value={h.key}>{h.key}</option>)}
                    </select>
                  </td>
                  <td className="px-2 py-1 text-[var(--leon-black)]/60">{r.hardwareSet ? r.hardwareSet.code : '—'}</td>
                  {/* The ARCHITECT's label for the hardware set (U1B, U1C) — what
                      the reviewer and the hardware submittal both cite — kept
                      beside the Hub's own linked set rather than instead of it. */}
                  <td className="px-2 py-1">
                    <input value={d.hwSetCode || ''} disabled={!editable} placeholder="U1B"
                      onChange={e => setDoor(d.id, { hwSetCode: e.target.value })}
                      className="w-16 px-1 py-0.5 bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                  </td>
                  <td className="px-2 py-1">
                    <input value={d.leafCode || ''} disabled={!editable} placeholder="A"
                      onChange={e => setDoor(d.id, { leafCode: e.target.value.toUpperCase() })}
                      className="w-14 px-1 py-0.5 bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                  </td>
                  {/* Pre-hung or slab changes the price, the packing and what the
                      installer expects off the truck. */}
                  <td className="px-2 py-1">
                    <select value={d.installationType || 'Pre-Hung'} disabled={!editable}
                      onChange={e => setDoor(d.id, { installationType: e.target.value })}
                      className="w-24 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                      {DOOR_INSTALLATION_TYPES.map(t => <option key={t}>{t}</option>)}
                    </select>
                  </td>
                  <td className="px-2 py-1">
                    <span className={R.fireRating && R.fireRating !== 'None' ? 'font-semibold' : 'text-[var(--leon-black)]/35'}>
                      {R.fireRating || 'None'}
                    </span>
                    {R.fireRating && R.fireRating !== 'None' && R.ratingState !== 'Certified' &&
                      <span className="ml-1 text-[9px] uppercase text-amber-700" title="Requested, not certified — no document on file">req</span>}
                  </td>
                  <td className="px-2 py-1">
                    <select value={d.status} disabled={!editable} onChange={e => setDoor(d.id, { status: e.target.value })}
                      className="w-32 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
                      {DOOR_STATUSES.map(s => <option key={s}>{s}</option>)}
                    </select>
                  </td>
                  <td className="px-2 py-1 whitespace-nowrap">
                    <button onClick={() => onEdit(d)} title="Open in the designer"
                      className="text-[var(--leon-brown)] font-semibold">✎</button>
                    {editable && <button onClick={() => { if (confirm(`Remove ${d.mark}?`)) removeDoor(d.id); }}
                      className="ml-1.5 text-red-600" title="Remove">✕</button>}
                  </td>
                </tr>
              );
            })}
            {!rows.length && <tr><td colSpan={18} className="px-3 py-6 text-center text-[var(--leon-black)]/40">No doors on this project yet.</td></tr>}
          </tbody>
        </table>
      </div>

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

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

// Bulk edit — §32. The count is shown BEFORE anything is applied, because
// "40 doors will be updated" is the moment to catch a mistake, not after.
function DoorBulkEdit({ ctx, project, open, onClose, ids, system, onDone }) {
  const [fields, setFields] = useState({});
  useEffect(() => { if (open) setFields({}); }, [open]);
  if (!open) return null;
  const lib = doorCtxLib(ctx);
  const set = (k, v) => setFields(f => (v === '' || v === null ? (({ [k]: _, ...rest }) => rest)(f) : { ...f, [k]: v }));
  const keys = Object.keys(fields);
  function apply() {
    ctx.updateProject(project.id, draft => {
      (draft.doors || []).forEach(d => { if (ids.includes(d.id)) Object.assign(d, fields); });
      ctx.logAction(draft, `Bulk change on ${ids.length} door${ids.length === 1 ? '' : 's'}: ${keys.join(', ')}.`);
    });
    onDone();
  }
  return (
    <Modal open={open} onClose={onClose} title={`Edit ${ids.length} door${ids.length === 1 ? '' : 's'}`}
      footer={<>
        <Button variant="ghost" onClick={onClose}>Cancel</Button>
        <Button onClick={apply} disabled={!keys.length}>
          {keys.length ? `Update ${ids.length} door${ids.length === 1 ? '' : 's'}` : 'Nothing to change'}
        </Button>
      </>}>
      <div className="space-y-3">
        <p className="text-sm text-[var(--leon-black)]/60">
          Only the fields you set below are touched. Everything else on each door is left exactly as it is.
        </p>
        <div className="grid gap-3 md:grid-cols-2">
          <Field label="Door type">
            <Select value={fields.typeId || ''} onChange={e => set('typeId', e.target.value)}>
              <option value="">— leave alone —</option>
              {doorTypesFor(ctx, project).map(t => <option key={t.id} value={t.id}>{t.code} · {t.name}</option>)}
            </Select>
          </Field>
          <Field label="Hardware set">
            <Select value={fields.hardwareSetId || ''} onChange={e => set('hardwareSetId', e.target.value)}>
              <option value="">— leave alone —</option>
              {(lib.hardwareSets || []).map(h => <option key={h.id} value={h.id}>{h.code} · {h.name}</option>)}
            </Select>
          </Field>
          <Field label="Frame">
            <Select value={fields.frameId || ''} onChange={e => set('frameId', e.target.value)}>
              <option value="">— leave alone —</option>
              {(lib.frames || []).map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
            </Select>
          </Field>
          <Field label="Handing">
            <Select value={fields.handing || ''} onChange={e => set('handing', e.target.value)}>
              <option value="">— leave alone —</option>
              {DOOR_HANDINGS.map(h => <option key={h.key} value={h.key}>{h.key} — {h.label}</option>)}
            </Select>
          </Field>
          <Field label="Fire rating">
            <Select value={fields.fireRating || ''} onChange={e => set('fireRating', e.target.value)}>
              <option value="">— leave alone —</option>
              {DOOR_FIRE_RATINGS.map(f => <option key={f}>{f}</option>)}
            </Select>
          </Field>
          <Field label="Status">
            <Select value={fields.status || ''} onChange={e => set('status', e.target.value)}>
              <option value="">— leave alone —</option>
              {DOOR_STATUSES.map(s => <option key={s}>{s}</option>)}
            </Select>
          </Field>
        </div>
        {!!keys.length && (
          <div className="rounded-lg bg-[var(--leon-cream)] border border-[var(--leon-line)] p-3 text-sm">
            <b>{ids.length} door{ids.length === 1 ? '' : 's'} will be updated.</b> Rough openings recalculate
            from the new frame or rule where those changed.
          </div>
        )}
      </div>
    </Modal>
  );
}

// ── Door Designer ─────────────────────────────────────────────────────────
// A guided configurator, not a CAD window. The live preview is always on
// screen, because the whole point is that someone who understands doors but has
// never used CAD can get a correct door in a handful of choices.
const DOOR_DESIGNER_STEPS = [
  { key: 'opening', label: 'Opening' },
  { key: 'type', label: 'Type' },
  { key: 'design', label: 'Design' },
  // Finish follows Design because they are one conversation: what shape the
  // leaf is, then what it is made of and faced in. It used to sit three steps
  // later, which is why the core and the louver ended up asked in both.
  { key: 'finish', label: 'Finish' },
  { key: 'frame', label: 'Frame' },
  { key: 'handing', label: 'Handing' },
  { key: 'hardware', label: 'Hardware' },
  { key: 'ratings', label: 'Ratings' },
  { key: 'review', label: 'Review' },
];

function DoorDesigner({ ctx, project, system, editable, door, onDone }) {
  const doors = project.doors || [];
  const [selId, setSelId] = useState(door ? door.id : (doors[0] ? doors[0].id : null));
  const [step, setStep] = useState('opening');
  const [advanced, setAdvanced] = useState(false);
  useEffect(() => { if (door) setSelId(door.id); }, [door && door.id]);

  const current = doors.find(d => d.id === selId) || null;
  if (!doors.length) {
    return <EmptyState text="No doors on this project yet. Add one from the Door Schedule, or start from a Global Library template under Door Types." />;
  }
  if (!current) return <EmptyState text="Pick a door." />;

  const r = doorResolve(ctx, project, current);
  const lib = doorCtxLib(ctx);
  const set = fields => ctx.updateProject(project.id, draft => {
    const d = (draft.doors || []).find(x => x.id === current.id);
    if (!d) return;
    Object.assign(d, fields);
    ctx.logAction(draft, `Door ${d.mark}: ${Object.keys(fields).join(', ')} set in the designer.`);
  });
  const R = r.resolved;
  const own = f => R.ownFields.includes(f);

  return (
    <div className="grid gap-4 lg:grid-cols-[1fr_380px] items-start">
      <div className="space-y-3">
        <div className="flex items-center gap-2 flex-wrap">
          <Select className="!w-48" value={selId} onChange={e => setSelId(e.target.value)}>
            {doors.map(d => <option key={d.id} value={d.id}>{d.mark || 'Unmarked'}</option>)}
          </Select>
          <label className="flex items-center gap-1.5 text-xs ml-auto">
            <input type="checkbox" checked={advanced} onChange={e => setAdvanced(e.target.checked)} />
            Advanced
          </label>
        </div>

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

        <div className="rounded-lg border border-[var(--leon-line)] bg-white p-4 space-y-3">
          {step === 'opening' && (
            <>
              <p className="text-sm text-[var(--leon-black)]/60">
                How would you like to define this opening? Enter either end — the other two are calculated
                from the frame's own rule, and they stay in step.
              </p>
              <div className="flex gap-2">
                {[['leaf', 'Door leaf size'], ['ro', 'Rough opening']].map(([k, l]) => (
                  <button key={k} disabled={!editable} onClick={() => set({ sizeMethod: k })}
                    className={`flex-1 rounded-lg border p-3 text-left ${R.sizeMethod === k ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)]'}`}>
                    <div className="font-semibold text-sm">{l}</div>
                    <div className="text-[11px] text-[var(--leon-black)]/50">
                      {k === 'leaf' ? 'I know the door size' : 'I measured the opening'}
                    </div>
                  </button>
                ))}
              </div>
              <div className="grid grid-cols-2 gap-3">
                {R.sizeMethod === 'ro' ? (
                  <>
                    <Field label="Rough opening width"><DimField value={R.roW} system={system} disabled={!editable} onChange={v => set({ roW: v })} /></Field>
                    <Field label="Rough opening height"><DimField value={R.roH} system={system} disabled={!editable} onChange={v => set({ roH: v })} /></Field>
                  </>
                ) : (
                  <>
                    <Field label="Leaf width"><DimField value={R.leafW} system={system} disabled={!editable} onChange={v => set({ leafW: v })} /></Field>
                    <Field label="Leaf height"><DimField value={R.leafH} system={system} disabled={!editable} onChange={v => set({ leafH: v })} /></Field>
                  </>
                )}
                <Field label="Leaf thickness"><DimField value={R.leafThickness} system={system} disabled={!editable} onChange={v => set({ leafThickness: v })} /></Field>
                <Field label="Quantity">
                  <TextInput type="number" value={current.qty} disabled={!editable}
                    onChange={e => set({ qty: Number(e.target.value) || 1 })} />
                </Field>
              </div>
            </>
          )}

          {step === 'type' && (
            <>
              <Field label="Door type" hint="A type carries the standard; this door can still differ.">
                <Select value={current.typeId || ''} disabled={!editable} onChange={e => set({ typeId: e.target.value || null })}>
                  <option value="">— none —</option>
                  {doorTypesFor(ctx, project).map(t => <option key={t.id} value={t.id}>{t.code} · {t.name}{t.global ? ' (global)' : ''}</option>)}
                </Select>
              </Field>
              <div className="grid grid-cols-2 gap-3">
                <Field label="Mark"><TextInput value={current.mark} disabled={!editable} onChange={e => set({ mark: e.target.value })} /></Field>
                <Field label="Location"><TextInput value={current.location || ''} disabled={!editable} onChange={e => set({ location: e.target.value })} /></Field>
                <Field label="From room"><TextInput value={current.fromRoom || ''} disabled={!editable} onChange={e => set({ fromRoom: e.target.value })} /></Field>
                <Field label="To room"><TextInput value={current.toRoom || ''} disabled={!editable} onChange={e => set({ toRoom: e.target.value })} /></Field>
              </div>
            </>
          )}

          {step === 'design' && (
            <Field label="LEON Collection model"
              hint="The published catalog reference. It names what was specified; the shape below still decides what is drawn.">
              <Select value={R.modelCode || ''} disabled={!editable}
                onChange={e => set({ modelCode: e.target.value || null })}>
                <option value="">— not from the catalog —</option>
                {leonDoorModelsByStyle().map(g => (
                  <optgroup key={g.style} label={g.style}>
                    {g.items.map(m => <option key={m.code} value={m.code}>{m.code} — {m.desc.slice(0, 60)}</option>)}
                  </optgroup>
                ))}
              </Select>
            </Field>
          )}
          {step === 'design' && R.modelCode && leonDoorModel(R.modelCode) && (
            <p className="text-xs text-[var(--leon-black)]/60 -mt-1">
              <b>{R.modelCode}</b> · {leonDoorModel(R.modelCode).style} — {leonDoorModel(R.modelCode).desc}
            </p>
          )}
          {step === 'design' && (
            <div className="grid gap-2 sm:grid-cols-3">
              {(lib.designs || []).map(d => (
                <button key={d.id} disabled={!editable} onClick={() => set({ designId: d.id })}
                  className={`rounded-lg border p-2 text-left ${R.designId === d.id ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown)]/50'}`}>
                  <div className="bg-white rounded border border-[var(--leon-line)] mb-1.5 grid place-items-center h-24 overflow-hidden">
                    <DoorElevation sizes={r.sizes} design={d} frame={r.frame} handing={R.handing}
                      liteKind="None" system={system} height={90} />
                  </div>
                  <div className="text-[11px] font-semibold leading-tight">{d.name}</div>
                </button>
              ))}
            </div>
          )}
          {step === 'design' && (
            <div className="grid gap-3 sm:grid-cols-3 pt-3 border-t border-[var(--leon-line)]">
              {/* The CORE is not here — it is what the leaf is made of, which is
                  the Finish step's question, and asking it in both places is how
                  the two answers came to disagree. Design decides the shape:
                  the panel layout, and whether there is a hole in it. */}
              {/* A louver or a vision panel is a hole with a SIZE. Drawn
                  without one it tells the shop it is there, not how big. */}
              <Field label="Louver / glass">
                <Select value={R.liteKind || 'None'} disabled={!editable}
                  onChange={e => set({ liteKind: e.target.value })}>
                  {LITE_KINDS.map(k => <option key={k}>{k}</option>)}
                </Select>
              </Field>
              {R.liteKind && R.liteKind !== 'None' ? (
                <>
                  <Field label="Louver / glass width">
                    <DimField value={R.liteW} system={system} disabled={!editable} onChange={v => set({ liteW: v })} />
                  </Field>
                  <Field label="Louver / glass height">
                    <DimField value={R.liteH} system={system} disabled={!editable} onChange={v => set({ liteH: v })} />
                  </Field>
                  <Field label="Sill height" hint="Bottom of the opening above the leaf foot.">
                    <DimField value={R.liteSill} system={system} disabled={!editable} onChange={v => set({ liteSill: v })} />
                  </Field>
                </>
              ) : <div />}
              {/* A CUSTOM LAYOUT on this door. The design is the standard;
                  this is where a door that carries a different number of panels
                  or grooves — or a wider moulding round them — says so. Blank
                  follows the design, which is not the same as zero. */}
              <div className="sm:col-span-3 rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]/40 p-3">
                <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55 mb-2">
                  Custom panels &amp; grooves
                  {r.design && (
                    <span className="ml-2 font-normal normal-case tracking-normal text-[var(--leon-black)]/45">
                      {r.design.name} carries {qnum(r.design.panelRows) || 0} × {qnum(r.design.panelCols) || 1} panels
                      {qnum(r.design.grooveCount) > 0 ? ` and ${qnum(r.design.grooveCount)} grooves` : ''}
                    </span>
                  )}
                </div>
                <div className="grid gap-3 sm:grid-cols-3">
                  <Field label="Panels down" hint="Rows. Blank follows the design.">
                    <TextInput type="number" min="0" value={R.panelRows === null || R.panelRows === undefined ? '' : R.panelRows}
                      disabled={!editable} placeholder={String(qnum(r.design && r.design.panelRows) || 0)}
                      onChange={e => set({ panelRows: e.target.value === '' ? null : Number(e.target.value) })} />
                  </Field>
                  <Field label="Panels across" hint="Columns.">
                    <TextInput type="number" min="0" value={R.panelCols === null || R.panelCols === undefined ? '' : R.panelCols}
                      disabled={!editable} placeholder={String(qnum(r.design && r.design.panelCols) || 1)}
                      onChange={e => set({ panelCols: e.target.value === '' ? null : Number(e.target.value) })} />
                  </Field>
                  <Field label="Panel profile width" hint="The moulding round each panel.">
                    <DimField value={R.panelProfile} system={system} disabled={!editable}
                      placeholder={fmtDim(qnum(r.design && r.design.panelProfile) || 19, system, { inchesOnly: true })}
                      onChange={v => set({ panelProfile: v })} />
                  </Field>
                  <Field label="Grooves" hint="How many. Blank follows the design.">
                    <TextInput type="number" min="0" value={R.grooveCount === null || R.grooveCount === undefined ? '' : R.grooveCount}
                      disabled={!editable} placeholder={String(qnum(r.design && r.design.grooveCount) || 0)}
                      onChange={e => set({ grooveCount: e.target.value === '' ? null : Number(e.target.value) })} />
                  </Field>
                  <Field label="Groove width">
                    <DimField value={R.grooveWidth} system={system} disabled={!editable}
                      placeholder={fmtDim(qnum(r.design && r.design.grooveWidth) || 12.7, system, { inchesOnly: true })}
                      onChange={v => set({ grooveWidth: v })} />
                  </Field>
                  <Field label="Groove direction">
                    <Select value={R.grooveOrientation || ''} disabled={!editable}
                      onChange={e => set({ grooveOrientation: e.target.value || null })}>
                      <option value="">— follows the design —</option>
                      <option>Vertical</option>
                      <option>Horizontal</option>
                    </Select>
                  </Field>
                </div>
              </div>

              {/* The air path. NOT the rule's undercut, which is about how the
                  leaf clears its frame. */}
              <Field label="Air-flow undercut" hint="Finished floor to the bottom of the LEAF.">
                <DimField value={R.leafUndercut} system={system} disabled={!editable}
                  onChange={v => set({ leafUndercut: v })} />
              </Field>
            </div>
          )}

          {step === 'frame' && (
            <>
              {/* THE WALL COMES FIRST. A jamb is sized to the wall it sits in —
                  asking which frame before knowing what it has to span is the
                  wrong way round, and it was buried under "advanced" besides. */}
              <div className="rounded-lg border border-[var(--leon-brown)]/35 bg-[var(--leon-cream)]/50 p-3">
                <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55 mb-2">
                  1 · The wall this door sits in
                </div>
                <div className="grid gap-3 sm:grid-cols-3">
                  <Field label="Wall thickness" hint="Everything below is sized to this.">
                    <DimField value={R.wallThickness} system={system} disabled={!editable}
                      onChange={v => set({ wallThickness: v })} />
                  </Field>
                  <Field label="Wall type">
                    <Select value={R.wallKind} disabled={!editable} onChange={e => set({ wallKind: e.target.value })}>
                      {WALL_KINDS.map(w => <option key={w}>{w}</option>)}
                    </Select>
                  </Field>
                  <Field label="Jamb thickness" hint="The jamb's own material, drawn in section.">
                    <DimField value={R.jambThickness} system={system} disabled={!editable}
                      onChange={v => set({ jambThickness: v })} />
                  </Field>
                </div>
              </div>

              <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55 pt-1">
                2 · The jamb / frame type
                {qnum(R.wallThickness) > 0 && (
                  <span className="ml-2 font-normal normal-case tracking-normal text-[var(--leon-black)]/45">
                    for a {fmtDim(qnum(R.wallThickness), system, { inchesOnly: true })} wall
                  </span>
                )}
              </div>
              <div className="grid gap-2 sm:grid-cols-2">
                {(lib.frames || []).map(f => (
                  <button key={f.id} disabled={!editable} onClick={() => set({ frameId: f.id, openingRuleId: f.openingRuleId })}
                    className={`rounded-lg border p-2.5 text-left ${R.frameId === f.id ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown)]/50'}`}>
                    <div className="font-semibold text-sm">{f.name}</div>
                    <div className="text-[11px] text-[var(--leon-black)]/50">
                      {f.kind} · jamb {fmtDim(f.jambWidth, system, { inchesOnly: true })} · depth {fmtDim(f.frameDepth, system, { inchesOnly: true })}
                    </div>
                  </button>
                ))}
              </div>
              {advanced && (
                <div className="grid grid-cols-2 gap-3 pt-2 border-t border-[var(--leon-line)]">
                  <Field label="Opening rule" hint="Normally comes with the frame.">
                    <Select value={R.openingRuleId || ''} disabled={!editable} onChange={e => set({ openingRuleId: e.target.value || null })}>
                      <option value="">— from the frame —</option>
                      {(lib.rules || []).map(x => <option key={x.id} value={x.id}>{x.name}</option>)}
                    </Select>
                  </Field>
                </div>
              )}
              {/* The trim is NOT advanced — it is what the client sees, and it
                  is drawn on the front elevation and both sections. */}
              <div className="grid gap-3 sm:grid-cols-2 pt-3 border-t border-[var(--leon-line)]">
                <Field label="Gasket" hint="Every LEON door has one.">
                  <Select value={R.gasketType} disabled={!editable} onChange={e => set({ gasketType: e.target.value })}>
                    {DOOR_GASKET_TYPES.map(g => <option key={g}>{g}</option>)}
                  </Select>
                </Field>
              </div>

              {/* THE TRIM ON THIS DOOR. The library profile is the starting
                  point; these four are what is actually being bought and cut
                  for this opening, and any of them may differ from it. Blank
                  follows the profile — which is not the same as none. */}
              {(() => {
                const tp = (lib.trims || []).find(t => t.id === R.trimProfileId) || null;
                const dim = v => v > 0 ? fmtDim(v, system, { inchesOnly: true }) : '—';
                return (
                  <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]/40 p-3">
                    <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55 mb-2">
                      Trim on this door
                      <span className="ml-2 font-normal normal-case tracking-normal text-[var(--leon-black)]/45">
                        design, size and how the corners are joined
                      </span>
                    </div>
                    <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
                      <div className="lg:col-span-1 sm:col-span-2">
                        <DoorFinishPicker label="Trim design"
                          hint="From Supplier Finishes — the catalog entry IS the design."
                          value={R.trimFinishRef} editable={editable}
                          onChange={v => set({ trimFinishRef: v })} />
                      </div>
                      <Field label="Width">
                        <Select value={String(qnum(R.trimWidth) || '')} disabled={!editable}
                          onChange={e => set({ trimWidth: e.target.value ? Number(e.target.value) : null })}>
                          <option value="">— not set —</option>
                          {TRIM_WIDTHS.map(v => <option key={v} value={String(v)}>{dim(v)}</option>)}
                        </Select>
                      </Field>
                      <Field label="Thickness">
                        <Select value={String(qnum(R.trimThickness) || '')} disabled={!editable}
                          onChange={e => set({ trimThickness: e.target.value ? Number(e.target.value) : null })}>
                          <option value="">— not set —</option>
                          {TRIM_THICKNESSES.map(v => <option key={v} value={String(v)}>{dim(v)}</option>)}
                        </Select>
                      </Field>
                      <Field label="Installation" hint="How the head meets the legs.">
                        <Select value={R.trimJoint || ''} disabled={!editable}
                          onChange={e => set({ trimJoint: e.target.value || null })}>
                          <option value="">— not set —</option>
                          {TRIM_JOINTS.map(j => <option key={j}>{j === 'Mitered' ? 'Mitered (45°)' : 'Butt joint'}</option>)}
                        </Select>
                      </Field>
                    </div>
                  </div>
                );
              })()}
            </>
          )}

          {/* HOW IT MOVES comes first — a sliding door has no handing worth
              picking, and until now there was no field for this at all, so
              every door in the app was a swing whatever it really was. */}
          {step === 'handing' && (
            <div className="rounded-lg border border-[var(--leon-brown)]/35 bg-[var(--leon-cream)]/50 p-3 mb-3">
              <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55 mb-2">
                1 · How the door moves
              </div>
              <div className="grid gap-2 sm:grid-cols-3 lg:grid-cols-5">
                {DOOR_OPERATIONS.map(op => (
                  <button key={op} disabled={!editable}
                    onClick={() => set(Object.assign({ operation: op },
                      // Picking a pair operation while a single handing is set
                      // would leave the two contradicting each other on the
                      // schedule, so the handing follows the choice.
                      DOOR_PAIR_OPERATIONS.indexOf(op) >= 0 && !doorIsPair(null, R.handing) ? { handing: 'PAIR' } : {},
                      DOOR_PAIR_OPERATIONS.indexOf(op) < 0 && doorIsPair(null, R.handing) ? { handing: 'RH' } : {}))}
                    className={`rounded-lg border px-2 py-1.5 text-left text-xs ${R.operation === op
                      ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)] font-semibold'
                      : 'border-[var(--leon-line)] hover:border-[var(--leon-brown)]/50'}`}>
                    {op}
                  </button>
                ))}
              </div>
              {doorSlides(R.operation) && (
                <p className="text-[11px] text-[var(--leon-black)]/55 mt-2">
                  A {String(R.operation).toLowerCase()} door does not swing, so handing below only says which
                  side it parks on.
                </p>
              )}
              {doorIsPair(R.operation, R.handing) && (
                <div className="grid gap-3 sm:grid-cols-2 mt-3 pt-3 border-t border-[var(--leon-line)]">
                  <Field label="Which way the pair swings">
                    <Select value={R.pairSwing || 'Inward'} disabled={!editable}
                      onChange={e => set({ pairSwing: e.target.value })}>
                      {DOOR_PAIR_SWINGS.map(x => <option key={x}>{x}</option>)}
                    </Select>
                  </Field>
                  <Field label="Active leaf" hint="The one that opens day to day; the other is bolted.">
                    <Select value={R.activeLeaf || 'Right'} disabled={!editable}
                      onChange={e => set({ activeLeaf: e.target.value })}>
                      {DOOR_ACTIVE_LEAF.map(x => <option key={x}>{x}</option>)}
                    </Select>
                  </Field>
                </div>
              )}
            </div>
          )}
          {step === 'handing' && (
            <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55">
              2 · Handing
            </div>
          )}
          {step === 'handing' && (
            <div className="grid gap-2 sm:grid-cols-3">
              {DOOR_HANDINGS.map(h => (
                <button key={h.key} disabled={!editable} onClick={() => set({ handing: h.key })}
                  className={`rounded-lg border p-2 text-left ${R.handing === h.key ? 'border-[var(--leon-brown)] bg-[var(--leon-cream)]' : 'border-[var(--leon-line)] hover:border-[var(--leon-brown)]/50'}`}>
                  <div className="bg-white rounded border border-[var(--leon-line)] mb-1.5 h-20 grid place-items-center overflow-hidden">
                    <DoorPlan sizes={r.sizes} handing={h.key} wallThickness={R.wallThickness} system={system} size={74} />
                  </div>
                  <div className="text-[11px] font-bold">{h.key}</div>
                  <div className="text-[10px] text-[var(--leon-black)]/50 leading-tight">{h.label}</div>
                </button>
              ))}
            </div>
          )}

          {step === 'hardware' && (
            <>
              {/* Hand-picked, line by line, from Supplier Finishes. The
                  hardware-SET block that used to sit under this is gone on the
                  client's instruction: a set is a recipe, and a recipe that
                  fills a door in is hardware nobody read. */}
              <DoorHardwarePicker ctx={ctx} value={R.hardware} na={R.hardwareNa} editable={editable}
                onChange={v => set({ hardware: v })} onNa={v => set({ hardwareNa: v })} />

              <div className="grid gap-3 sm:grid-cols-3">
                <Field label="Handle backset"
                  hint={`From the lock edge of the leaf to the centre of the spindle. Blank follows the opening rule (${fmtDim(qnum(r.rule && r.rule.handleBackset) || 60, system, { inchesOnly: true })}).`}>
                  <DimField value={R.handleBackset} system={system} disabled={!editable}
                    placeholder={fmtDim(qnum(r.rule && r.rule.handleBackset) || 60, system, { inchesOnly: true })}
                    onChange={v => set({ handleBackset: v })} />
                </Field>
                {/* NO HINGE TYPE OR COUNT HERE. The hinge is chosen on the
                    Hardware line above, from the catalog, and its quantity is
                    that line's quantity — asking again below produced a second
                    answer to the same question. The drawing reads the line. */}
                <DoorFinishPicker label="Hardware finish" hint="What the ironmongery is plated in."
                  value={R.hardwareFinishRef} editable={editable}
                  onChange={v => set({ hardwareFinishRef: v })} />
              </div>
            </>
          )}

          {step === 'finish' && (
            <div className="space-y-4">
              {/* WHAT THE LEAF IS MADE OF AND FACED IN — asked once, here.
                  The core used to be on the Design step and the louver / glass
                  materials in both, which is how a door came to carry two
                  answers to one question. */}
              <div className="grid grid-cols-2 gap-3">
                <Field label="Core" hint="What is inside the leaf — drawn in the side section and the plan.">
                  <Select value={R.coreType} disabled={!editable} onChange={e => set({ coreType: e.target.value })}>
                    {DOOR_CORE_TYPES.map(c => <option key={c.key} value={c.key}>{c.label}</option>)}
                  </Select>
                </Field>
                <Field label="Finish notes">
                  <TextInput value={current.finishNotes || ''} disabled={!editable}
                    onChange={e => set({ finishNotes: e.target.value })} />
                </Field>
                <DoorFinishPicker label="Door finish" hint="What the leaf is faced in."
                  value={R.leafFinishRef} editable={editable}
                  onChange={v => set({ leafFinishRef: v })} />
                <DoorFinishPicker label="Core / edge finish" hint="What the edge and core are faced in."
                  value={R.coreFinishRef} editable={editable}
                  onChange={v => set({ coreFinishRef: v })} />
              </div>

              {/* The hole in the leaf, if there is one. Its KIND and SIZE were
                  set on Design; what fills it is a material, so it is asked
                  here. N/A is an answer — it says the question was considered,
                  which a blank does not. */}
              {(() => {
                const lite = (R.liteKind && R.liteKind !== 'None')
                  ? R.liteKind : ((r.design && r.design.liteKind) || 'None');
                const has = !!lite && lite !== 'None';
                const louver = /louver/i.test(lite);
                const naOpt = <option value="N/A">N/A — this door has none</option>;
                return (
                  <div className="border-t border-[var(--leon-line)] pt-3">
                    <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/55 mb-2">
                      Louver &amp; glass
                      <span className="ml-2 font-normal normal-case tracking-normal text-[var(--leon-black)]/45">
                        {has
                          ? `${lite}${qnum(R.liteW) > 0 && qnum(R.liteH) > 0
                              ? ` · ${fmtDim(qnum(R.liteW), system, { inchesOnly: true })} × ${fmtDim(qnum(R.liteH), system, { inchesOnly: true })}` : ''}`
                          : 'none on this door — set one on the Design step if it needs one'}
                      </span>
                    </div>
                    <div className="grid grid-cols-2 gap-3">
                      <Field label="Louver material" hint={louver ? 'What the blades are made of.' : 'Only if the leaf has a louver.'}>
                        <Select value={R.louverMaterial || (has && louver ? '' : 'N/A')} disabled={!editable}
                          onChange={e => set({ louverMaterial: e.target.value || null })}>
                          <option value="">— not specified —</option>
                          {naOpt}
                          {DOOR_LOUVER_MATERIALS.map(m => <option key={m}>{m}</option>)}
                        </Select>
                      </Field>
                      <Field label="Glass type" hint={!louver && has ? 'What the panel is glazed in.' : 'Only if the leaf has a vision panel.'}>
                        <Select value={R.glassType || (has && !louver ? '' : 'N/A')} disabled={!editable}
                          onChange={e => set({ glassType: e.target.value || null })}>
                          <option value="">— not specified —</option>
                          {naOpt}
                          {DOOR_GLASS_TYPES.map(g => <option key={g}>{g}</option>)}
                        </Select>
                      </Field>
                    </div>
                  </div>
                );
              })()}
            </div>
          )}

          {step === 'ratings' && (
            <div className="grid grid-cols-2 gap-3">
              <Field label="Fire rating">
                <Select value={R.fireRating || 'None'} disabled={!editable} onChange={e => set({ fireRating: e.target.value })}>
                  {DOOR_FIRE_RATINGS.map(f => <option key={f}>{f}</option>)}
                </Select>
              </Field>
              <Field label="Rating state" hint="Requested is not certified.">
                <Select value={R.ratingState || 'Requested'} disabled={!editable} onChange={e => set({ ratingState: e.target.value })}>
                  {DOOR_RATING_STATES.map(s => <option key={s}>{s}</option>)}
                </Select>
              </Field>
              <Field label="Acoustic rating"><TextInput value={R.acousticRating || ''} disabled={!editable}
                placeholder="STC 45" onChange={e => set({ acousticRating: e.target.value })} /></Field>
              <Field label="Certification document" className="col-span-2">
                <FileField name={current.ratingDocName} url={current.ratingDocUrl} editable={editable}
                  projectId={project.id} label={`${current.mark} fire rating`}
                  onChange={(f, u) => set({ ratingDocName: f, ratingDocUrl: u })} />
              </Field>
            </div>
          )}

          {step === 'review' && (
            <div className="space-y-3">
              <DoorIssueList issues={r.issues} />
              <table className="w-full text-sm">
                <tbody>
                  {[
                    ['Mark', current.mark], ['Type', r.type ? `${r.type.code} · ${r.type.name}` : '—'],
                    ['Leaf', `${fmtDim(r.sizes.leaf.w, system)} × ${fmtDim(r.sizes.leaf.h, system)} × ${fmtDim(R.leafThickness, system, { inchesOnly: true })}`],
                    ['Frame overall', `${fmtDim(r.sizes.frame.w, system)} × ${fmtDim(r.sizes.frame.h, system)}`],
                    ['Rough opening', `${fmtDim(r.sizes.ro.w, system)} × ${fmtDim(r.sizes.ro.h, system)}`],
                    ['Frame', r.frame ? r.frame.name : '—'], ['Opening rule', r.rule ? r.rule.name : '—'],
                    ['Handing', R.handing], ['Hardware set', r.hardwareSet ? r.hardwareSet.code : '—'],
                    ['Fire rating', `${R.fireRating || 'None'}${R.fireRating && R.fireRating !== 'None' ? ` (${R.ratingState})` : ''}`],
                    ['Undercut', fmtDim(r.sizes.undercut, system, { inchesOnly: true })],
                  ].map(([k, v]) => (
                    <tr key={k} className="border-b border-[var(--leon-line)]/60">
                      <td className="py-1.5 text-[var(--leon-black)]/50 w-40">{k}</td>
                      <td className="py-1.5 font-medium">{v}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
              {!!R.ownFields.length && (
                <div className="text-xs text-amber-800 bg-amber-50 border border-amber-200 rounded p-2.5">
                  This door differs from its type on: <b>{R.ownFields.join(', ')}</b>.
                </div>
              )}
            </div>
          )}
        </div>
      </div>

      {/* The live preview, always on screen */}
      <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3 space-y-3 lg:sticky lg:top-4">
        <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">{current.mark || 'Door'} — elevation</div>
        <div className="bg-[var(--leon-cream)] rounded p-2 grid place-items-center">
          <DoorElevation sizes={r.sizes} design={r.design} frame={r.frame} handing={R.handing}
            liteKind={R.liteKind} liteW={R.liteW} liteH={R.liteH} liteSill={R.liteSillHeight}
            system={system} height={300} showDims mark={current.mark} />
        </div>
        <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">Plan</div>
        <div className="bg-[var(--leon-cream)] rounded p-2 grid place-items-center">
          <DoorPlan sizes={r.sizes} handing={R.handing} wallThickness={R.wallThickness} system={system} size={130} />
        </div>
        {/* The live size relationship — §6 of the brief */}
        <div className="text-xs space-y-1 border-t border-[var(--leon-line)] pt-2">
          {[['Leaf', r.sizes.leaf], ['Frame overall', r.sizes.frame], ['Rough opening', r.sizes.ro]].map(([k, v]) => (
            <div key={k} className="flex justify-between">
              <span className="text-[var(--leon-black)]/50">{k}</span>
              <span className="font-semibold tabular-nums">{fmtDim(v.w, system)} × {fmtDim(v.h, system)}</span>
            </div>
          ))}
          {!!r.sizes.overridden.length && (
            <div className="text-[11px] text-amber-800 pt-1">Manual override on {r.sizes.overridden.join(', ')}</div>
          )}
        </div>
      </div>
    </div>
  );
}

// ── Libraries ─────────────────────────────────────────────────────────────
// Every library edits the same shape: a named list inside ctx.doorLibrary. One
// small editor serves all of them rather than five near-identical panels.
function doorLibSet(ctx, key, fn) {
  ctx.setDoorLibrary(prev => {
    const lib = { ...makeDoorLibrary(), ...(prev || {}) };
    lib[key] = fn([...(lib[key] || [])]);
    return lib;
  });
}

function DoorLibPanel({ title, blurb, items, editable, onAdd, onRemove, columns, renderRow, addLabel }) {
  return (
    <div className="space-y-3">
      <div className="flex items-start justify-between gap-3 flex-wrap">
        <div>
          <h3 className="font-bold">{title}</h3>
          <p className="text-sm text-[var(--leon-black)]/55 max-w-2xl">{blurb}</p>
        </div>
        {editable && onAdd && <Button size="sm" onClick={onAdd}>+ {addLabel || 'Add'}</Button>}
      </div>
      <div className="rounded-lg border border-[var(--leon-line)] bg-white overflow-x-auto">
        <table className="w-full text-xs min-w-[760px]">
          <thead>
            <tr className="text-left text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45 border-b border-[var(--leon-line)]">
              {columns.map(c => <th key={c} className="px-2 py-2">{c}</th>)}
              {editable && onRemove && <th className="px-2 py-2 w-8"></th>}
            </tr>
          </thead>
          <tbody>
            {items.map(it => (
              <tr key={it.id} className="border-b border-[var(--leon-line)]/60">
                {renderRow(it)}
                {editable && onRemove && (
                  <td className="px-2 py-1">
                    <button className="text-red-600" title="Remove"
                      onClick={() => { if (confirm(`Remove "${it.name || it.code}"?`)) onRemove(it.id); }}>✕</button>
                  </td>
                )}
              </tr>
            ))}
            {!items.length && <tr><td colSpan={columns.length + 1} className="px-3 py-5 text-center text-[var(--leon-black)]/40">Nothing here yet.</td></tr>}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function DoorRulesPanel({ ctx, system, editable }) {
  const rules = (doorCtxLib(ctx).rules) || [];
  const upd = (id, f) => doorLibSet(ctx, 'rules', list => list.map(x => x.id === id ? { ...x, ...f } : x));
  const cell = (r, k) => (
    <td key={k} className="px-2 py-1">
      <DimField value={r[k]} system={system} disabled={!editable} w="w-20" onChange={v => upd(r.id, { [k]: v })} />
    </td>
  );
  return (
    <DoorLibPanel title="Opening Rules" editable={editable} items={rules}
      blurb="How a frame system turns a leaf into a frame and a frame into a rough opening — per jamb, head and sill. There is deliberately no universal formula: a hollow metal frame and a split jamb do not stack up the same way, so the numbers live here and the engine reads them."
      addLabel="Add rule"
      onAdd={() => doorLibSet(ctx, 'rules', l => [...l, makeOpeningRule({ name: 'New rule' })])}
      onRemove={id => doorLibSet(ctx, 'rules', l => l.filter(x => x.id !== id))}
      columns={['Rule', 'Leaf→Frame jamb ea.', 'Head', 'Sill', 'Frame→RO jamb ea.', 'Head', 'Sill', 'Undercut', 'Handle height', 'Handle backset', 'Clear opening less', 'Astragal', 'Max leaf W', 'Max leaf H']}
      renderRow={r => [
        <td key="n" className="px-2 py-1">
          <input value={r.name} disabled={!editable} onChange={e => upd(r.id, { name: e.target.value })}
            className="w-48 px-1 py-0.5 font-semibold bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
        </td>,
        cell(r, 'leafToFrameJambEach'), cell(r, 'leafToFrameHead'), cell(r, 'leafToFrameSill'),
        cell(r, 'frameToRoJambEach'), cell(r, 'frameToRoHead'), cell(r, 'frameToRoSill'),
        // Handle height sits with the clearances because it is the same kind of
        // fact: a standard the shop works to, and one the drawing calls out.
        // Handle height AND backset sit with the clearances because they are the
        // same kind of fact: a standard the shop works to, and one the drawing
        // calls out. A height alone does not locate a handle.
        cell(r, 'undercut'), cell(r, 'handleHeight'), cell(r, 'handleBackset'),
        // What comes OFF the leaf width to give the usable clear opening —
        // about 1 1/2", and it moves with the hinge, so it is edited here.
        cell(r, 'clearOpeningDeduction'),
        cell(r, 'astragal'), cell(r, 'maxLeafWidth'), cell(r, 'maxLeafHeight'),
      ]} />
  );
}

// ── LEON Collection door models ────────────────────────────────────────────
// The product reference catalog, read straight from LEON_Door_Catalog.pdf. It
// is a LIST, not a library: nothing here is edited in the app, because it is
// LEON's own published catalog and the file is the source. A door cites a model
// by its code, which is what a schedule and a submittal quote.
function DoorModelsPanel({ ctx }) {
  const [q, setQ] = useState('');
  const [style, setStyle] = useState('');
  const all = typeof LEON_DOOR_MODELS !== 'undefined' ? LEON_DOOR_MODELS : [];
  const needle = q.trim().toLowerCase();
  const hits = all.filter(m =>
    (!style || m.style === style) &&
    (!needle || [m.code, m.style, m.desc].some(v => String(v).toLowerCase().includes(needle))));
  const styles = typeof LEON_DOOR_MODEL_STYLES !== 'undefined' ? LEON_DOOR_MODEL_STYLES : [];

  return (
    <div className="space-y-3">
      <div>
        <h3 className="font-bold">📖 LEON Collection — Door Models</h3>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
          Single Home Entrance Doors, the published product reference. A door cites a model by its
          code, which is what the schedule and the submittal quote.
        </p>
        <p className="text-[11px] text-[var(--leon-black)]/45 max-w-3xl mt-1">
          These are catalog references: a code, a style and a description. <b>They carry no dimensions
          and no photograph</b>, so they name what was specified and do not drive the drawing — the leaf
          design, the frame and the sizes are still set on the door. The catalog file is the source and
          is not edited here.
        </p>
      </div>
      <div className="flex items-end gap-3 flex-wrap">
        <Field label="Style">
          <Select className="!w-56" value={style} onChange={e => setStyle(e.target.value)}>
            <option value="">Every style</option>
            {styles.map(x => <option key={x}>{x}</option>)}
          </Select>
        </Field>
        <Field label="Search" className="flex-1">
          <TextInput value={q} onChange={e => setQ(e.target.value)} placeholder="Code, style or description" />
        </Field>
        <span className="text-xs text-[var(--leon-black)]/45 pb-2">{hits.length} of {all.length}</span>
      </div>
      {!all.length ? (
        <EmptyState text="The door model catalog did not load. Check the doors/leon-door-models.js script tag." />
      ) : (
        <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
          {hits.map(m => (
            <div key={m.code} className="rounded-lg border border-[var(--leon-line)] bg-white p-2.5">
              <div className="flex items-baseline gap-2">
                <span className="font-semibold text-sm">{m.code}</span>
                <span className="text-[11px] text-[var(--leon-black)]/45">{m.style}</span>
              </div>
              <p className="text-xs text-[var(--leon-black)]/65 mt-1">{m.desc}</p>
            </div>
          ))}
          {!hits.length && <p className="text-xs text-[var(--leon-black)]/45 col-span-full py-6 text-center">
            Nothing matches.</p>}
        </div>
      )}
    </div>
  );
}

function DoorTrimsPanel({ ctx, system, editable }) {
  const lib = doorCtxLib(ctx);
  const upd = (id, f) => doorLibSet(ctx, 'trims', l => l.map(x => x.id === id ? { ...x, ...f } : x));
  const trims = lib.trims || [];
  return (
    <div className="space-y-3">
      <div>
        <h3 className="font-bold">📏 Trim Library</h3>
        <p className="text-sm text-[var(--leon-black)]/60 max-w-3xl">
          A trim is specified in four answers &mdash; its <b>design</b>, its <b>width</b>, its
          <b> thickness</b> and how its corners are <b>joined</b>. Those four are what the shop cuts to.
        </p>
        <p className="text-[11px] text-[var(--leon-black)]/45 max-w-3xl mt-1">
          <b>A door no longer points at a profile here.</b> Each door carries its own trim &mdash; design
          from Supplier Finishes, width, thickness and installation &mdash; set on the designer's Frame
          step. This library is where standard profiles are recorded and compared; it does not drive a
          drawing on its own.
        </p>
      </div>
      {editable && (
        <Button size="sm" onClick={() => doorLibSet(ctx, 'trims', l => [...l, makeTrimProfile({ name: 'New trim' })])}>
          + Add trim
        </Button>
      )}
      {!trims.length && <EmptyState text="No trim profiles yet." />}
      <div className="grid gap-3 lg:grid-cols-2">
        {trims.map(t => (
          <div key={t.id} className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
            <div className="flex items-start gap-3">
              {/* The design's own picture. A moulding has a SECTION rather than
                  a photograph, so the library draws the profile it names — and
                  a real photograph can be uploaded over it when there is one. */}
              <div className="w-24 shrink-0">
                <div className="rounded border border-[var(--leon-line)] overflow-hidden bg-white">
                  {(t.finishRef && t.finishRef.img) || t.img
                    ? <Photo src={(t.finishRef && t.finishRef.img) || t.img} alt={t.design || 'Trim'}
                        className="w-full h-20 object-cover" />
                    : <svg viewBox="0 0 40 40" className="w-full h-20">
                        <DoorSheetDefs />
                        <DoorTrimSectionSwatch t={t} x={0} y={0} s={40} />
                      </svg>}
                </div>
                {editable && (
                  <ImagePicker value={t.img} label="profile photo"
                    onChange={v => upd(t.id, { img: v })} className="mt-1" />
                )}
              </div>
              <div className="min-w-0 flex-1 space-y-2">
                <input value={t.name} disabled={!editable} onChange={e => upd(t.id, { name: e.target.value })}
                  className="w-full px-1 py-0.5 font-semibold bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                <div className="grid grid-cols-2 gap-2">
                  {/* WHAT IT IS comes from the catalog, not a typed name — the
                      same Supplier Finishes the door finish and the hardware are
                      picked from, so the trim on the drawing and the trim on the
                      order are one product. */}
                  <div className="col-span-2">
                    <DoorFinishPicker label="1 · Trim design" hint="From Supplier Finishes."
                      value={t.finishRef} editable={editable}
                      onChange={v => upd(t.id, { finishRef: v })} />
                  </div>
                  <Field label="2 · Width" hint="The face you see — standard sizes only.">
                    <Select value={String(qnum(t.width) || '')} disabled={!editable}
                      onChange={e => upd(t.id, { width: Number(e.target.value) })}>
                      {!TRIM_WIDTHS.some(x => Math.abs(x - qnum(t.width)) < 0.01) && qnum(t.width) > 0 && (
                        <option value={String(qnum(t.width))}>
                          {fmtDim(qnum(t.width), system, { inchesOnly: true })} (as saved)
                        </option>
                      )}
                      {TRIM_WIDTHS.map(v => (
                        <option key={v} value={String(v)}>{fmtDim(v, system, { inchesOnly: true })}</option>
                      ))}
                    </Select>
                  </Field>
                  <Field label="3 · Thickness" hint="How far it stands off the wall.">
                    <Select value={String(qnum(t.thickness) || '')} disabled={!editable}
                      onChange={e => upd(t.id, { thickness: Number(e.target.value) })}>
                      {!TRIM_THICKNESSES.some(x => Math.abs(x - qnum(t.thickness)) < 0.01) && qnum(t.thickness) > 0 && (
                        <option value={String(qnum(t.thickness))}>
                          {fmtDim(qnum(t.thickness), system, { inchesOnly: true })} (as saved)
                        </option>
                      )}
                      {TRIM_THICKNESSES.map(v => (
                        <option key={v} value={String(v)}>{fmtDim(v, system, { inchesOnly: true })}</option>
                      ))}
                    </Select>
                  </Field>
                  <Field label="4 · Joining point" hint="How the head meets the legs.">
                    <Select value={t.joint || 'Mitered'} disabled={!editable}
                      onChange={e => upd(t.id, { joint: e.target.value })}>
                      {TRIM_JOINTS.map(x => <option key={x}>{x}</option>)}
                    </Select>
                  </Field>
                </div>
                <details className="text-xs">
                  <summary className="cursor-pointer text-[var(--leon-black)]/50">Section profile, reveal and notes</summary>
                  <div className="grid grid-cols-3 gap-2 pt-2">
                    <Field label="Section profile" hint="Only what the drawn section looks like.">
                      <Select value={t.design} disabled={!editable}
                        onChange={e => upd(t.id, { design: e.target.value })}>
                        {TRIM_DESIGNS.map(x => <option key={x}>{x}</option>)}
                      </Select>
                    </Field>
                    <Field label="Reveal">
                      <DimField value={t.reveal} system={system} disabled={!editable}
                        onChange={v => upd(t.id, { reveal: v })} />
                    </Field>
                    {/* A JSX attribute string cannot backslash-escape a quote —
                        the inch mark has to come through an expression. */}
                    <Field label="Return lip" hint={'How thick the leg is — 1/4" as standard.'}>
                      <DimField value={t.legThickness} system={system} disabled={!editable}
                        onChange={v => upd(t.id, { legThickness: v })} />
                    </Field>
                    <Field label="Lip length" hint={'How far it runs into the jamb — 1 1/2".'}>
                      <DimField value={t.legLength} system={system} disabled={!editable}
                        onChange={v => upd(t.id, { legLength: v })} />
                    </Field>
                    <Field label="Lip offset" hint={'How far in from the frame it sits — 1/2".'}>
                      <DimField value={t.legOffset} system={system} disabled={!editable}
                        onChange={v => upd(t.id, { legOffset: v })} />
                    </Field>
                    <Field label="Material">
                      <TextInput value={t.material || ''} disabled={!editable}
                        onChange={e => upd(t.id, { material: e.target.value })} />
                    </Field>
                    <Field label="Finish">
                      <TextInput value={t.finish || ''} disabled={!editable}
                        onChange={e => upd(t.id, { finish: e.target.value })} />
                    </Field>
                  </div>
                </details>
              </div>
            </div>
            {editable && (
              <button onClick={() => { if (confirm(`Remove ${t.name}?`)) doorLibSet(ctx, 'trims', l => l.filter(x => x.id !== t.id)); }}
                className="mt-2 text-[11px] text-[var(--leon-black)]/35 hover:text-[var(--leon-red)]">Remove</button>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

function DoorFramesPanel({ ctx, system, editable }) {
  const lib = doorCtxLib(ctx);
  const upd = (id, f) => doorLibSet(ctx, 'frames', l => l.map(x => x.id === id ? { ...x, ...f } : x));
  return (
    <DoorLibPanel title="Frame Library" editable={editable} items={lib.frames || []}
      blurb="The frame profiles we build to, the opening rule each one carries, and how each is BUILT — a wrapped jamb is a core, a skin over it and a face on the skin, and the jamb section on the shop drawing draws those layers from these fields. Picking a frame in the designer picks its rule too."
      addLabel="Add frame"
      onAdd={() => doorLibSet(ctx, 'frames', l => [...l, makeFrameProfile({ name: 'New frame' })])}
      onRemove={id => doorLibSet(ctx, 'frames', l => l.filter(x => x.id !== id))}
      columns={['Frame', 'Kind', 'Jamb', 'Head', 'Depth', 'Stop', 'Wall min', 'Wall max',
        'Core', 'Skin', 'Skin thk', 'Face', 'Casing', 'Seal', 'Opening rule']}
      renderRow={f => [
        <td key="n" className="px-2 py-1">
          <input value={f.name} disabled={!editable} onChange={e => upd(f.id, { name: e.target.value })}
            className="w-44 px-1 py-0.5 font-semibold bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
        </td>,
        <td key="k" className="px-2 py-1">
          <select value={f.kind} disabled={!editable} onChange={e => upd(f.id, { kind: e.target.value })}
            className="w-40 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
            {FRAME_KINDS.map(k => <option key={k}>{k}</option>)}
          </select>
        </td>,
        ...['jambWidth', 'headWidth', 'frameDepth', 'stop', 'wallMin', 'wallMax'].map(k => (
          <td key={k} className="px-2 py-1">
            <DimField value={f[k]} system={system} disabled={!editable} w="w-20" onChange={v => upd(f.id, { [k]: v })} />
          </td>
        )),
        // HOW IT IS BUILT. A wrapped jamb is a core, a skin over it and a face
        // on the skin — the section draws those layers rather than one solid
        // poché, and it can only do that if the record carries them. A JSX
        // comment cannot sit here: this is an ARRAY of cells, not JSX children.
        <td key="cm" className="px-2 py-1">
          <select value={f.coreMaterial || ''} disabled={!editable} onChange={e => upd(f.id, { coreMaterial: e.target.value })}
            className="w-36 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
            <option value="">— not given —</option>
            {JAMB_CORE_MATERIALS.map(k => <option key={k}>{k}</option>)}
          </select>
        </td>,
        <td key="sm" className="px-2 py-1">
          <select value={f.skinMaterial || ''} disabled={!editable} onChange={e => upd(f.id, { skinMaterial: e.target.value })}
            className="w-40 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
            <option value="">— not given —</option>
            {JAMB_SKIN_MATERIALS.map(k => <option key={k}>{k}</option>)}
          </select>
        </td>,
        <td key="st" className="px-2 py-1">
          <DimField value={f.skinThickness} system={system} disabled={!editable} w="w-20"
            onChange={v => upd(f.id, { skinThickness: v })} />
        </td>,
        <td key="fm" className="px-2 py-1">
          <select value={f.faceMaterial || ''} disabled={!editable} onChange={e => upd(f.id, { faceMaterial: e.target.value })}
            className="w-36 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
            <option value="">— not given —</option>
            {JAMB_FACE_MATERIALS.map(k => <option key={k}>{k}</option>)}
          </select>
        </td>,
        <td key="ic" className="px-2 py-1">
          <label className="flex items-center gap-1 text-[11px] whitespace-nowrap"
            title="The casing is moulded as part of the jamb and returns onto the wall in one piece.">
            <input type="checkbox" checked={!!f.integralCasing} disabled={!editable}
              onChange={e => upd(f.id, { integralCasing: e.target.checked })} />
            integral
          </label>
        </td>,
        <td key="sg" className="px-2 py-1">
          <label className="flex items-center gap-1 text-[11px] whitespace-nowrap"
            title="The stop carries a groove for a bulb seal.">
            <input type="checkbox" checked={!!f.sealGroove} disabled={!editable}
              onChange={e => upd(f.id, { sealGroove: e.target.checked })} />
            in stop
          </label>
        </td>,
        <td key="r" className="px-2 py-1">
          <select value={f.openingRuleId || ''} disabled={!editable} onChange={e => upd(f.id, { openingRuleId: e.target.value || null })}
            className="w-44 px-1 py-0.5 text-xs border border-[var(--leon-line)] rounded bg-white">
            <option value="">— none —</option>
            {(lib.rules || []).map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
          </select>
        </td>,
      ]} />
  );
}

function DoorDesignsPanel({ ctx, system, editable }) {
  const lib = doorCtxLib(ctx);
  const upd = (id, f) => doorLibSet(ctx, 'designs', l => l.map(x => x.id === id ? { ...x, ...f } : x));
  const demo = { leaf: { w: 914.4, h: 2032 }, frame: { w: 1016, h: 2083 }, ro: { w: 1067, h: 2134 }, overridden: [] };
  return (
    <div className="space-y-3">
      <div>
        <h3 className="font-bold">Leaf Designs</h3>
        <p className="text-sm text-[var(--leon-black)]/55 max-w-2xl">
          Parametric, not pictures. The elevation is drawn from these numbers, so changing a groove count
          or a rail height redraws every door that uses the design.
        </p>
      </div>
      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
        {(lib.designs || []).map(d => (
          <div key={d.id} className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
            <div className="bg-[var(--leon-cream)] rounded mb-2 grid place-items-center h-40 overflow-hidden">
              <DoorElevation sizes={demo} design={d} frame={null} handing="RH" liteKind="None" system={system} height={150} />
            </div>
            <input value={d.name} disabled={!editable} onChange={e => upd(d.id, { name: e.target.value })}
              className="w-full px-1 py-0.5 font-semibold text-sm bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
            <div className="text-[11px] text-[var(--leon-black)]/45 mb-2">{d.kind}</div>
            <div className="grid grid-cols-2 gap-2 text-[11px]">
              <label className="flex items-center gap-1">Grooves
                <input type="number" value={d.grooveCount} disabled={!editable}
                  onChange={e => upd(d.id, { grooveCount: Number(e.target.value) || 0 })}
                  className="w-12 px-1 py-0.5 border border-[var(--leon-line)] rounded" /></label>
              <select value={d.grooveOrientation} disabled={!editable} onChange={e => upd(d.id, { grooveOrientation: e.target.value })}
                className="px-1 py-0.5 border border-[var(--leon-line)] rounded bg-white">
                <option>Vertical</option><option>Horizontal</option>
              </select>
              <label className="flex items-center gap-1">Groove W
                <DimField value={d.grooveWidth} system={system} disabled={!editable} w="w-16"
                  onChange={v => upd(d.id, { grooveWidth: v })} /></label>
              {/* Panels are a grid, not a stack — `panelCols` was on the record
                  and had no field and was never drawn, so a 2×2 door came out
                  as two full-width panels. And a panelled door is a count AND a
                  profile: the moulding round each panel is what frames it. */}
              <label className="flex items-center gap-1">Panels ↓
                <input type="number" min="0" value={d.panelRows} disabled={!editable}
                  onChange={e => upd(d.id, { panelRows: Number(e.target.value) || 0 })}
                  className="w-12 px-1 py-0.5 border border-[var(--leon-line)] rounded" /></label>
              <label className="flex items-center gap-1">Panels →
                <input type="number" min="0" value={d.panelCols || 0} disabled={!editable}
                  onChange={e => upd(d.id, { panelCols: Number(e.target.value) || 0 })}
                  className="w-12 px-1 py-0.5 border border-[var(--leon-line)] rounded" /></label>
              <label className="flex items-center gap-1">Panel profile
                <DimField value={d.panelProfile} system={system} disabled={!editable} w="w-16"
                  onChange={v => upd(d.id, { panelProfile: v })} /></label>
              <label className="flex items-center gap-1">Stile
                <DimField value={d.stile} system={system} disabled={!editable} w="w-16" onChange={v => upd(d.id, { stile: v })} /></label>
            </div>
            {/* A vision panel or louver belonging to the LEAF STYLE rather than
                to each door — "Louvered" is what the leaf is. A door may still
                set its own, and its answer wins. */}
            <label className="flex items-center gap-1 mt-2 text-[11px]">Lite / louver
              <select value={d.liteKind || ''} disabled={!editable}
                onChange={e => upd(d.id, { liteKind: e.target.value || null })}
                className="px-1 py-0.5 border border-[var(--leon-line)] rounded bg-white flex-1">
                <option value="">Set on the door</option>
                {LITE_KINDS.map(k => <option key={k} value={k}>{k}</option>)}
              </select>
            </label>
          </div>
        ))}
      </div>
      {editable && <Button size="sm" onClick={() => doorLibSet(ctx, 'designs', l => [...l, makeDoorDesign({ name: 'New design' })])}>+ Add design</Button>}
    </div>
  );
}

// Hardware pulls from the supplier catalog rather than being retyped — An
// Cuong's door hardware is already in there with photos and specs.
function DoorHardwarePanel({ ctx, editable }) {
  const lib = doorCtxLib(ctx);
  const [importing, setImporting] = useState(false);
  const [q, setQ] = useState('');
  const catalog = useMemo(() => {
    const all = (typeof supplierCatalog === 'function') ? supplierCatalog() : [];
    return all.filter(r => /Door Hardware/i.test(r.cat || ''));
  }, []);
  const shown = catalog.filter(r => !q.trim() || `${r.name} ${r.code} ${r.type}`.toLowerCase().includes(q.trim().toLowerCase()));

  function addFromCatalog(r) {
    doorLibSet(ctx, 'hardware', l => l.some(x => x.productNumber === r.code) ? l : [...l, makeHardwareItem({
      name: r.name, category: r.type || 'Other', productNumber: r.code,
      manufacturer: 'An Cuong', img: r.img, notes: r.formats || '', supplierKey: 'ancuong', supplierId: r.id,
    })]);
  }
  return (
    <div className="space-y-4">
      <DoorLibPanel title="Hardware Library" editable={editable} items={lib.hardware || []}
        blurb="What goes on a door. Pull from the supplier catalog rather than retyping — the specs and photos are already there."
        addLabel="Add item"
        onAdd={() => setImporting(true)}
        onRemove={id => doorLibSet(ctx, 'hardware', l => l.filter(x => x.id !== id))}
        columns={['Item', 'Category', 'Product no.', 'Manufacturer', 'Notes']}
        renderRow={h => [
          <td key="n" className="px-2 py-1">
            <div className="flex items-center gap-2">
              {h.img && <img src={h.img} alt="" className="w-8 h-8 object-contain bg-[var(--leon-cream)] rounded" />}
              <span className="font-semibold">{h.name}</span>
            </div>
          </td>,
          <td key="c" className="px-2 py-1">{h.category}</td>,
          <td key="p" className="px-2 py-1">{h.productNumber}</td>,
          <td key="m" className="px-2 py-1 text-[var(--leon-black)]/55">{h.manufacturer}</td>,
          <td key="s" className="px-2 py-1 text-[var(--leon-black)]/45 max-w-[280px] truncate">{h.notes}</td>,
        ]} />

      <DoorHardwareSets ctx={ctx} editable={editable} />

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

function DoorHardwareSets({ ctx, editable }) {
  const lib = doorCtxLib(ctx);
  const sets = lib.hardwareSets || [];
  const upd = (id, f) => doorLibSet(ctx, 'hardwareSets', l => l.map(x => x.id === id ? { ...x, ...f } : x));
  return (
    <div className="space-y-2">
      <div className="flex items-center justify-between gap-3">
        <div>
          <h3 className="font-bold">Hardware Sets</h3>
          <p className="text-sm text-[var(--leon-black)]/55">
            A set is a recipe — assigning HW-03 to forty doors fills in every item on all forty.
          </p>
        </div>
        {editable && <Button size="sm" onClick={() => doorLibSet(ctx, 'hardwareSets', l =>
          [...l, makeHardwareSet({ code: `HW-${String(l.length + 1).padStart(2, '0')}` })])}>+ Add set</Button>}
      </div>
      {!sets.length && <EmptyState text="No hardware sets yet." />}
      {sets.map(s => (
        <Collapsible key={s.id} id={`hwset-${s.id}`} title={`${s.code} — ${s.name}`} count={(s.lines || []).length}>
          <div className="space-y-2">
            <div className="grid grid-cols-2 gap-3">
              <Field label="Code"><TextInput value={s.code} disabled={!editable} onChange={e => upd(s.id, { code: e.target.value })} /></Field>
              <Field label="Name"><TextInput value={s.name} disabled={!editable} onChange={e => upd(s.id, { name: e.target.value })} /></Field>
            </div>
            <div className="space-y-1">
              {(s.lines || []).map((l, i) => (
                <div key={i} className="flex items-center gap-2">
                  <input type="number" value={l.qty} disabled={!editable}
                    onChange={e => upd(s.id, { lines: s.lines.map((x, k) => k === i ? { ...x, qty: Number(e.target.value) || 1 } : x) })}
                    className="w-14 px-1 py-1 text-sm border border-[var(--leon-line)] rounded" />
                  <select value={l.itemId || ''} disabled={!editable}
                    onChange={e => upd(s.id, { lines: s.lines.map((x, k) => k === i ? { ...x, itemId: e.target.value } : x) })}
                    className="flex-1 px-2 py-1 text-sm border border-[var(--leon-line)] rounded bg-white">
                    <option value="">— pick an item —</option>
                    {(lib.hardware || []).map(h => <option key={h.id} value={h.id}>{h.name} ({h.category})</option>)}
                  </select>
                  {editable && <button className="text-red-600" onClick={() => upd(s.id, { lines: s.lines.filter((_, k) => k !== i) })}>✕</button>}
                </div>
              ))}
            </div>
            {editable && (
              <div className="flex gap-2">
                <button onClick={() => upd(s.id, { lines: [...(s.lines || []), { itemId: '', qty: 1 }] })}
                  className="text-xs font-semibold text-[var(--leon-brown)]">+ Add item</button>
                <button onClick={() => { if (confirm(`Remove ${s.code}?`)) doorLibSet(ctx, 'hardwareSets', l => l.filter(x => x.id !== s.id)); }}
                  className="text-xs font-semibold text-red-600 ml-auto">Remove set</button>
              </div>
            )}
          </div>
        </Collapsible>
      ))}
    </div>
  );
}

// Door Types — the project's own, plus the shipped global templates.
function DoorTypesPanel({ ctx, project, system, editable }) {
  const lib = doorCtxLib(ctx);
  const own = project.doorTypes || [];
  function importTemplate(t) {
    ctx.updateProject(project.id, draft => {
      if (!Array.isArray(draft.doorTypes)) draft.doorTypes = [];
      const n = draft.doorTypes.length;
      draft.doorTypes.push(makeDoorType({
        ...t, id: uid('dtype'), global: false,
        code: `TYPE ${String.fromCharCode(65 + n)}`,
      }, ctx.currentUserName));
      ctx.logAction(draft, `Imported door type "${t.name}" from the global library.`);
    });
  }
  const upd = (id, f) => ctx.updateProject(project.id, draft => {
    const t = (draft.doorTypes || []).find(x => x.id === id);
    if (t) Object.assign(t, f);
  });
  return (
    <div className="space-y-4">
      <div>
        <h3 className="font-bold">Project Door Types</h3>
        <p className="text-sm text-[var(--leon-black)]/55 max-w-2xl">
          A type is the standard; a mark is the door. Forty doors can point at TYPE A, and changing TYPE A
          reaches all forty — except any door that set its own value, which stays as it is.
        </p>
      </div>
      {!own.length && <EmptyState text="No project types yet. Import one from the global library below." />}
      <div className="grid gap-3 md:grid-cols-2">
        {own.map(t => {
          const used = (project.doors || []).filter(d => d.typeId === t.id);
          return (
            <div key={t.id} className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
              <div className="flex items-center gap-2 mb-2">
                <input value={t.code} disabled={!editable} onChange={e => upd(t.id, { code: e.target.value })}
                  className="w-24 px-1 py-0.5 font-bold bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                <input value={t.name} disabled={!editable} onChange={e => upd(t.id, { name: e.target.value })}
                  className="flex-1 px-1 py-0.5 text-sm bg-transparent border border-transparent hover:border-[var(--leon-line)] rounded" />
                <Badge>{used.length} door{used.length === 1 ? '' : 's'}</Badge>
              </div>
              <div className="grid grid-cols-2 gap-2 text-xs">
                <Field label="Leaf W"><DimField value={t.leafW} system={system} disabled={!editable} onChange={v => upd(t.id, { leafW: v })} /></Field>
                <Field label="Leaf H"><DimField value={t.leafH} system={system} disabled={!editable} onChange={v => upd(t.id, { leafH: v })} /></Field>
                <Field label="Frame">
                  <Select value={t.frameId || ''} disabled={!editable} onChange={e => upd(t.id, { frameId: e.target.value || null })}>
                    <option value="">— none —</option>
                    {(lib.frames || []).map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
                  </Select>
                </Field>
                <Field label="Fire rating">
                  <Select value={t.fireRating} disabled={!editable} onChange={e => upd(t.id, { fireRating: e.target.value })}>
                    {DOOR_FIRE_RATINGS.map(f => <option key={f}>{f}</option>)}
                  </Select>
                </Field>
              </div>
            </div>
          );
        })}
      </div>

      <div className="pt-2 border-t border-[var(--leon-line)]">
        <h4 className="font-bold text-sm mb-2">Import from the Global Library</h4>
        <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
          {(lib.types || []).filter(t => t.global).map(t => (
            <button key={t.id} disabled={!editable} onClick={() => importTemplate(t)}
              className="rounded-lg border border-[var(--leon-line)] p-2.5 text-left hover:border-[var(--leon-brown)]">
              <div className="font-semibold text-sm">{t.name}</div>
              <div className="text-[11px] text-[var(--leon-black)]/50">
                {t.category} · {fmtDim(t.leafW, system, { inchesOnly: true })} × {fmtDim(t.leafH, system, { inchesOnly: true })}
                {t.fireRating !== 'None' ? ` · ${t.fireRating}` : ''}
              </div>
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

function DoorGlobalLibrary({ ctx, system, editable }) {
  const lib = doorCtxLib(ctx);
  const demo = t => ({ leaf: { w: t.leafW, h: t.leafH }, frame: { w: t.leafW + 51, h: t.leafH + 51 },
                       ro: { w: t.leafW + 102, h: t.leafH + 102 }, overridden: [] });
  return (
    <div className="space-y-3">
      <div>
        <h3 className="font-bold">Global LEON Door Library</h3>
        <p className="text-sm text-[var(--leon-black)]/55 max-w-2xl">
          Standard configurations, ready to use. Each is a real configuration — pick one and the size,
          frame and opening rule are already right — not a picture to copy.
        </p>
      </div>
      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
        {(lib.types || []).filter(t => t.global).map(t => {
          const d = (lib.designs || []).find(x => x.id === t.designId);
          const f = (lib.frames || []).find(x => x.id === t.frameId);
          return (
            <div key={t.id} className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
              <div className="bg-[var(--leon-cream)] rounded mb-2 grid place-items-center h-36 overflow-hidden">
                <DoorElevation sizes={demo(t)} design={d} frame={f} handing={t.handing}
                  liteKind={t.liteKind} system={system} height={130} />
              </div>
              <div className="font-semibold text-sm leading-tight">{t.name}</div>
              <div className="text-[11px] text-[var(--leon-black)]/50">
                {fmtDim(t.leafW, system, { inchesOnly: true })} × {fmtDim(t.leafH, system, { inchesOnly: true })}
                {f ? ` · ${f.name}` : ''}
              </div>
              {t.fireRating !== 'None' && <Badge tone="brown">{t.fireRating}</Badge>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ── Create doors from an architect's schedule ─────────────────────────────
// The other half of this module's reason to exist: a hundred doors arrive as a
// spreadsheet, not as a hundred trips through the configurator. Nothing is
// created until the review screen has been looked at — an import that quietly
// makes 100 wrong records is worse than one that makes none.
function DoorScheduleImport({ ctx, project, system, open, onClose, onDone }) {
  const [stage, setStage] = useState('pick');     // pick -> map -> review -> done
  const [headers, setHeaders] = useState([]);
  const [rows, setRows] = useState([]);
  const [map, setMap] = useState({});
  const [fileName, setFileName] = useState('');
  const [err, setErr] = useState('');
  const [skip, setSkip] = useState([]);
  const [busy, setBusy] = useState(false);
  const [created, setCreated] = useState(0);
  const fileRef = useRef(null);
  const types = doorTypesFor(ctx, project);

  useEffect(() => {
    if (!open) return;
    setStage('pick'); setHeaders([]); setRows([]); setMap({});
    setFileName(''); setErr(''); setSkip([]); setCreated(0);
  }, [open]);

  function ingest(hdrs, data, name) {
    const clean = hdrs.filter(h => String(h || '').trim());
    setHeaders(clean);
    setRows(data);
    // A remembered mapping beats guessing — architects reuse their own template.
    const remembered = (project.doorImports || []).slice(-1)[0];
    const guess = guessColumnMap(clean, DOOR_IMPORT_FIELDS);
    const useRemembered = remembered && remembered.headers
      && remembered.headers.join('|') === clean.join('|');
    setMap(useRemembered ? remembered.map : guess);
    setFileName(name);
    setStage('map');
  }

  async function onFile(e) {
    const f = e.target.files[0];
    e.target.value = '';
    if (!f) return;
    setErr(''); setBusy(true);
    try {
      if (/\.csv$/i.test(f.name)) {
        const text = await f.text();
        const { headers: h, rows: r } = parseHeaderCsv(text);
        if (!h.length) throw new Error('That CSV has no header row.');
        ingest(h, r, f.name);
      } else {
        if (!window.XLSX) throw new Error('The Excel library did not load — reload and try again.');
        const buf = await f.arrayBuffer();
        const wb = XLSX.read(buf, { type: 'array' });
        const sheet = wb.Sheets[wb.SheetNames[0]];
        const grid = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' });
        // The header is rarely row 1 on a real schedule — it sits under a title
        // block. Take the first row that looks like a door schedule header.
        let hi = grid.findIndex(r => r.some(c => /door\s*(no|mark|number)|^mark$|^mk$/i.test(String(c || ''))));
        if (hi < 0) hi = grid.findIndex(r => r.filter(c => String(c || '').trim()).length >= 3);
        if (hi < 0) throw new Error('No header row could be found in that sheet.');
        const h = grid[hi].map(c => String(c || '').trim());
        const r = grid.slice(hi + 1)
          .filter(row => row.some(c => String(c || '').trim()))
          .map(row => { const o = {}; h.forEach((k, i) => { if (k) o[k] = String(row[i] === undefined ? '' : row[i]).trim(); }); return o; });
        ingest(h, r, f.name);
      }
    } catch (ex) { setErr(ex.message || 'That file could not be read.'); }
    setBusy(false);
  }

  const parsed = useMemo(
    () => rows.map((r, i) => ({ i, ...readDoorScheduleRow(r, map, system, types) })),
    [rows, map, system, types.length]);
  const valid = parsed.filter(p => p.ok && !skip.includes(p.i));
  const matched = valid.filter(p => p.matchedTypeId).length;
  const needsReview = parsed.filter(p => p.ok && p.notes.some(n => n.level === 'warn')).length;
  const bad = parsed.filter(p => !p.ok).length;

  function create() {
    const marks = new Set((project.doors || []).map(d => String(d.mark || '').toLowerCase()));
    let n = 0;
    ctx.updateProject(project.id, draft => {
      if (!Array.isArray(draft.doors)) draft.doors = [];
      if (!Array.isArray(draft.doorImports)) draft.doorImports = [];
      valid.forEach(p => {
        // A mark already on the job is updated, not duplicated — a re-issued
        // schedule is the normal case, not an exception.
        const existing = draft.doors.find(d => String(d.mark || '').toLowerCase() === String(p.door.mark).toLowerCase());
        if (existing) {
          Object.assign(existing, p.door, p.matchedTypeId ? { typeId: p.matchedTypeId } : {});
        } else {
          draft.doors.push(makeDoor({ ...p.door, typeId: p.matchedTypeId || null }, ctx.currentUserName));
          n++;
        }
      });
      draft.doorImports.push({
        id: uid('dimp'), file: fileName, date: todayISO(), by: ctx.currentUserName,
        headers, map, rows: rows.length, created: n, updated: valid.length - n,
      });
      ctx.logAction(draft, `Imported ${valid.length} rows from "${fileName}" — ${n} doors created, ${valid.length - n} updated.`);
    });
    setCreated(n);
    setStage('done');
  }

  return (
    <Modal open={open} onClose={onClose} wide
      title={stage === 'done' ? 'Doors created' : 'Create doors from a schedule'}
      footer={
        stage === 'pick' ? <>
          <Button variant="ghost" onClick={onClose}>Cancel</Button>
          <Button onClick={() => fileRef.current.click()} disabled={busy}>{busy ? 'Reading…' : 'Choose a file…'}</Button>
        </> : stage === 'map' ? <>
          <Button variant="ghost" onClick={() => setStage('pick')}>Back</Button>
          <Button onClick={() => setStage('review')} disabled={!map.mark}>Review {rows.length} rows</Button>
        </> : stage === 'review' ? <>
          <Button variant="ghost" onClick={() => setStage('map')}>Back to mapping</Button>
          <Button onClick={create} disabled={!valid.length}>Create {valid.length} door{valid.length === 1 ? '' : 's'}</Button>
        </> : <Button onClick={() => { onDone(); onClose(); }}>Done</Button>
      }>
      <input ref={fileRef} type="file" accept=".csv,.xlsx,.xls" className="hidden" onChange={onFile} />

      {stage === 'pick' && (
        <div className="space-y-3">
          <p className="text-sm text-[var(--leon-black)]/65">
            Point this at the architect&rsquo;s door schedule — Excel or CSV. It reads the rows, maps the
            columns to door fields, matches each row to a door type where it can, and shows you
            everything before a single record is created.
          </p>
          <div className="rounded-lg border border-dashed border-[var(--leon-line)] p-5 text-center">
            <div className="text-3xl mb-1">📋</div>
            <div className="text-sm font-semibold">Excel or CSV</div>
            <div className="text-[11px] text-[var(--leon-black)]/50">
              The header row does not have to be the first row — a title block above it is normal and expected.
            </div>
          </div>
          {err && <div className="rounded bg-red-50 border border-red-200 text-red-700 text-sm p-3">{err}</div>}
        </div>
      )}

      {stage === 'map' && (
        <div className="space-y-3">
          <div className="flex items-center gap-2 text-sm">
            <span className="font-semibold">{fileName}</span>
            <span className="text-[var(--leon-black)]/50">{rows.length} rows · {headers.length} columns</span>
          </div>
          <p className="text-xs text-[var(--leon-black)]/55">
            Mapping is guessed from the column names. Only <b>Door mark</b> is required — anything left
            unmapped simply comes from the door&rsquo;s type instead.
          </p>
          <div className="grid gap-2 sm:grid-cols-2 max-h-[380px] overflow-y-auto pr-1">
            {DOOR_IMPORT_FIELDS.map(f => (
              <label key={f.key} className="flex items-center gap-2 text-xs">
                <span className={`w-36 shrink-0 ${f.required ? 'font-bold' : 'text-[var(--leon-black)]/65'}`}>
                  {f.label}{f.required ? ' *' : ''}
                </span>
                <select value={map[f.key] || ''} onChange={e => setMap({ ...map, [f.key]: e.target.value })}
                  className="flex-1 px-1.5 py-1 border border-[var(--leon-line)] rounded bg-white">
                  <option value="">— not mapped —</option>
                  {headers.map(h => <option key={h} value={h}>{h}</option>)}
                </select>
              </label>
            ))}
          </div>
        </div>
      )}

      {stage === 'review' && (
        <div className="space-y-3">
          <div className="grid grid-cols-4 gap-2 text-center">
            {[['Rows found', rows.length, ''], ['Matched to a type', matched, 'text-green-700'],
              ['Need review', needsReview, 'text-amber-700'], ['Cannot import', bad, 'text-red-700']].map(([k, v, c]) => (
              <div key={k} className="rounded-lg border border-[var(--leon-line)] bg-white p-2">
                <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/40">{k}</div>
                <div className={`text-xl font-bold ${c}`}>{v}</div>
              </div>
            ))}
          </div>
          <div className="rounded-lg border border-[var(--leon-line)] overflow-auto max-h-[380px]">
            <table className="w-full text-[11px]">
              <thead className="sticky top-0 bg-[var(--leon-cream)]">
                <tr className="text-left uppercase tracking-wide text-[10px] text-[var(--leon-black)]/45">
                  <th className="px-2 py-1.5 w-8"></th>
                  <th className="px-2 py-1.5">Mark</th><th className="px-2 py-1.5">Type</th>
                  <th className="px-2 py-1.5">Leaf</th><th className="px-2 py-1.5">Handing</th>
                  <th className="px-2 py-1.5">Fire</th><th className="px-2 py-1.5">Location</th>
                  <th className="px-2 py-1.5">Notes</th>
                </tr>
              </thead>
              <tbody>
                {parsed.map(p => {
                  const off = skip.includes(p.i);
                  return (
                    <tr key={p.i} className={`border-t border-[var(--leon-line)]/60 ${!p.ok ? 'bg-red-50/50' : off ? 'opacity-40' : ''}`}>
                      <td className="px-2 py-1">
                        <input type="checkbox" checked={!off && p.ok} disabled={!p.ok}
                          onChange={e => setSkip(e.target.checked ? skip.filter(x => x !== p.i) : [...skip, p.i])} />
                      </td>
                      <td className="px-2 py-1 font-bold">{p.door.mark || <span className="text-red-600">—</span>}</td>
                      <td className="px-2 py-1">
                        {p.matchedTypeCode
                          ? <span className="text-green-700 font-semibold">{p.matchedTypeCode}</span>
                          : p.typeCode ? <span className="text-amber-700" title="No matching type on this project">{p.typeCode} ?</span>
                          : <span className="text-[var(--leon-black)]/30">—</span>}
                      </td>
                      <td className="px-2 py-1 whitespace-nowrap">
                        {p.door.leafW && p.door.leafH
                          ? `${fmtDim(p.door.leafW, system, { inchesOnly: true })} × ${fmtDim(p.door.leafH, system, { inchesOnly: true })}`
                          : <span className="text-[var(--leon-black)]/30">from type</span>}
                      </td>
                      <td className="px-2 py-1">{p.door.handing || <span className="text-[var(--leon-black)]/30">—</span>}</td>
                      <td className="px-2 py-1">{p.door.fireRating || <span className="text-[var(--leon-black)]/30">—</span>}</td>
                      <td className="px-2 py-1 max-w-[140px] truncate">{p.door.location || ''}</td>
                      <td className="px-2 py-1">
                        {p.notes.map((n, k) => (
                          <div key={k} className={n.level === 'error' ? 'text-red-700' : n.level === 'warn' ? 'text-amber-700' : 'text-[var(--leon-black)]/45'}>{n.msg}</div>
                        ))}
                        {p.suggestions.map((sg, k) => (
                          <div key={`s${k}`} className="text-[var(--leon-brown)]">
                            {sg.raw} → suggested <b>{sg.suggested}</b> (not applied)
                          </div>
                        ))}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
          <p className="text-[11px] text-[var(--leon-black)]/55">
            A mark already on this job is <b>updated</b>, not duplicated — a re-issued schedule is the
            normal case. Abbreviations are only ever suggested; nothing is substituted for you.
          </p>
        </div>
      )}

      {stage === 'done' && (
        <div className="space-y-2 text-sm">
          <div className="text-3xl">✅</div>
          <p><b>{created} door{created === 1 ? '' : 's'} created</b>{valid.length - created > 0 ? `, ${valid.length - created} updated` : ''} from {fileName}.</p>
          <p className="text-[var(--leon-black)]/60">
            They are in the Door Schedule now. Any row that matched a type inherited its size, frame and
            rule; the rest are waiting for one.
          </p>
        </div>
      )}
    </Modal>
  );
}
