<!-- SOLID CITY · part 2041 -->
----------- ghost preview ---------------- */
function normalize(datas) { // bottom-centre of the union at the origin
  var u = C.unionAABB(boxesOf(datas)), cx = (u.x0 + u.x1) / 2, cz = (u.z0 + u.z1) / 2;
  datas.forEach(function (d) { d.p = [d.p[0] - cx, d.p[1] - u.y0, d.p[2] - cz]; });
  return datas;
}
function setGhost(datas, name) {
  F.ghostRel = normalize(datas); F.ghostName = name; F.ghostVer++; F.lift = 0; F.gOff = { x: 0, z: 0 };
  if (ghostGroup) root.remove(ghostGroup);
  ghostGroup = new T.Group(); F.ghostRel.forEach(function (d) { ghostGroup.add(makeMesh(d, true)); });
  ghostGroup.visible = false; root.add(ghostGroup); F.lastKey = '';
}
function setGhostKeep() { var lift = F.lift, off = F.gOff; setGhost(F.ghostRel, F.ghostName); F.lift = lift; F.gOff = off; }
function setShape(t) { F.shape = t; select(null); setGhost([newPiece(t, F.color, F.finish)], C.shapeName(t)); render(); }
function shifted(rel, x, y, z) { return rel.map(function (d) { var n = clone(d); n.p = [d.p[0] + x, d.p[1] + y, d.p[2] + z]; return n; }); }
function ghostFrame() {
  var show = F.on && !F.sel && F.ghostRel && S.mode === 'walk';
  if (ghostGroup) ghostGroup.visible = !!show; ghostHelp.visible = !!show;
  if (!show) return;
  var P = S.p, fx = -Math.sin(S.camYaw), fz = -Math.cos(S.camYaw), u = C.unionAABB(boxesOf(F.ghostRel));
  var dist = 3 + Math.max(u.x1 - u.x0, u.z1 - u.z0) / 2, g = F.grid;
  var ax = Math.round((P.x + fx * dist + F.gOff.x) / g) * g, az = Math.round((P.z + fz * dist + F.gOff.z) / g) * g;
  var key = ax + ',' + az + ',' + F.lift + ',' + F.ghostVer + ',' + (F.cur ? F.cur.items.length : 0);
  if (key === F.lastKey) return; F.lastKey = key;
  // stack: start on the ground, rise onto any creation the ghost would overlap
  var base = Math.max(gnd(ax + u.x0, az + u.z0), gnd(ax + u.x1, az + u.z1), gnd(ax + u.x0, az + u.z1), gnd(ax + u.x1, az + u.z0), gnd(ax, az));
  var ground = base;
  for (var it = 0; it < 40; it++) {
    var bx = { x0: ax + u.x0, x1: ax + u.x1, z0: az + u.z0, z1: az + u.z1, y0: base, y1: base + (u.y1 - u.y0) }, top = -1e9;
    for (var k = 0; k < A.solids.length; k++) { var s = A.solids[k]; if (s.forge && C.overlap(bx, s) && s.y1 > top) top = s.y1; }
    if (top < -1e8) break; base = top;
  }
  base = Math.max(ground, base + F.lift);
  F.ghostAbs = shifted(F.ghostRel, ax, base, az);
  var r = check(F.ghostAbs, F.cur && F.cur.local ? F.cur : null, null);
  F.ghostOk = r.ok; F.ghostWhy = r.why || '';
  ghostGroup.position.set(ax, base, az);
  ghostBox.min.set(ax + u.x0, base, az + u.z0); ghostBox.max.set(ax + u.x1, base + (u.y1 - u.y0), az + u.z1);
  ghostHelp.material.color.setHex(r.ok ? 0xffd27a : 0xff2a40);
  var pb = $('fg-place'); if (pb) pb.classList.toggle('bad', !r.ok);
  if (!r.ok) say(r.why, true, true); else if (F.msgBad) say(F.ghostName + ' ready — tap PLACE.', false, true);
}
function place() {
  if (F.sel) { select(null); say('Done. The ghost is back — pick a shape or PLACE again.'); return; }
  if (!F.ghostRel) { say('Pick a shape first.', true); return; }
  F.lastKey = ''; ghostFrame();
  if (!F.ghostOk) { say(F.ghostWhy, true); buzz(); return; }
  var cr = F.cur && F.cur.local ? F.cur : newCreation();
  var r = check(F.ghostAbs, cr, null); if (!r.ok) { say(r.why, true); buzz(); return; }
  pushUndo();
  F.ghostAbs.forEach(function (d) { addItem(cr, clone(d)); });
  changed(); say('Placed ' + F.ghostName + ' in \u201c' + cr.name + '\u201d (' + cr.items.length + ' piece' + (cr.items.length === 1 ? '' : 's') + ').');
}
function buzz() { try { navigator.vibrate && navigator.vibrate(30); } catch (e) {} }

/* ---------------- selection + edits ---------------- */
var ray = new T.Raycaster(), ndc = new T.Vector2();
function tap(x, y) {
  if (!F.on) return false;
  ndc.set(x / innerWidth * 2 - 1, -(y / innerHeight) * 2 + 1); ray.setFromCamera(ndc, A.camera);
  var meshes = []; F.bench.concat(F.world).forEach(function (cr) { cr.items.forEach(function (it) { meshes.push(it.mesh); }); });
  var hit = ray.intersectObjects(meshes, true)[0];
  if (hit) { var o = hit.object; while (o && !o.userData.forgeItem) o = o.parent; if (o) { select(o.userData.forgeItem); return true; } }
  if (F.sel) { select(null); return true; }
  return false;
}
function select(it) {
  F.sel = it || null;
  if (it && it.cr.local) F.cur = it.cr;
  if (it) { var cr = it.cr; say(cr.local ? 'Selected ' + C.shapeName(it.d.t) + ' in \u201c' + cr.name + '\u201d. Edit it with the right column; PLACE = done.' : '\uD83D\uDD12 \u201c' + cr.name + '\u201d by ' + (cr.owner || '?') + (cr.status === 'pending' ? ' (awaiting approval)' : '') + ' — published builds are locked' + (cr.mine ? '; ☰ → PICK UP to edit it.' : '.')); }
  updateSel(); render();
}
function targets() { if (!F.sel) return null; return F.scope === 'whole' ? F.sel.cr.items.slice() : [F.sel]; }
function updateSel() {
  var ts = targets(); selHelp.visible = !!(F.on && ts && ts.length);
  if (!selHelp.visible) return;
  var u = 