<!-- SOLID CITY · part 116 -->
push(part(G.torus, k2.gold, 0, top + 0.18, 0, Math.PI / 2, 0, 1, 1, 1));
  else if (hat === 'antenna') { b.push(part(G.cyl6, k2.gun, 0.1, top + 0.18, 0, 0, 0, 0.03, 0.36, 0.03)); w.push(part(G.ball, k2.gold, 0.1, top + 0.38, 0, 0, 0, 1.2, 1.2, 1.2)); }
  else if (hat === 'cap') { b.push(part(B, k2.black, 0, top + 0.04, 0, 0, 0, hs + 0.04, 0.08, hs + 0.04)); w.push(part(B, k2.gold, 0, top + 0.01, hs / 2 + 0.08, 0, 0, hs, 0.03, 0.16)); }
  else if (hat === 'tophat') { b.push(part(G.cyl6, k2.black, 0, top + 0.02, 0, 0, 0, 0.5, 0.03, 0.5)); b.push(part(G.cyl6, k2.black, 0, top + 0.18, 0, 0, 0, 0.3, 0.3, 0.3)); w.push(part(G.cyl6, k2.gold, 0, top + 0.07, 0, 0, 0, 0.31, 0.05, 0.31)); }
  else if (hat === 'hood') b.push(part(G.cone6, k2.black, 0, top + 0.1, -0.02, 0, 0, 0.5, 0.5, 0.5));
  else if (hat === 'fin') w.push(part(B, k2.gold, 0, top + 0.08, -0.02, 0, 0, 0.05, 0.16, hs + 0.04));
  else if (hat === 'phones') { b.push(part(B, k2.gun, 0, top + 0.03, 0, 0, 0, hs + 0.12, 0.05, 0.08)); [-1, 1].forEach(function (s) { w.push(part(B, k2.gold, (hs / 2 + 0.05) * s, 1.8, 0, 0, 0, 0.08, 0.18, 0.18)); }); }
  return (shapeCache[k] = { body: merge(b), glow: merge(w) });
}
var limbGeo = {
  arm: merge([part(G.box, COL.black, 0, -0.33, 0, 0, 0, 0.14, 0.68, 0.14)]),
  leg: merge([part(G.box, COL.black, 0, -0.37, 0, 0, 0, 0.19, 0.74, 0.19)])
};
var shipGeo = {
  hull: merge([part(G.cone6, COL.black, 0, 0, 0.6, Math.PI / 2, 0, 1.3, 4.6, 1.3), part(G.box, COL.gun, 0, -0.1, -1.2, 0, 0, 5.2, 0.14, 1.5), part(G.box, COL.gun, 0, 0.6, -1.7, 0, 0, 0.12, 1.1, 1.2), part(G.cyl6, COL.gun, 0, 0, -1.9, Math.PI / 2, 0, 1.0, 0.8, 1.0)]),
  trim: merge([part(G.box, COL.gold, 0, 0.62, 0.2, 0, 0, 0.18, 0.08, 2.8), part(G.box, COL.gold, 2.6, 0.2, -1.4, 0, 0, 0.12, 0.9, 1.0), part(G.box, COL.gold, -2.6, 0.2, -1.4, 0, 0, 0.12, 0.9, 1.0)]),
  flame: new T.ConeGeometry(0.34, 1.6, 6).rotateX(-Math.PI / 2).translate(0, 0, -2.9)
};
var hitGeo = new T.BoxGeometry(1.0, 2.2, 1.0).translate(0, 1.1, 0), hitMat = new T.MeshBasicMaterial({ visible: false });
var bodyMat = A.curve(new T.MeshLambertMaterial({ vertexColors: true }));
var flameMat = new T.MeshBasicMaterial({ color: 0xff5a1a, transparent: true, opacity: 0.8, blending: T.AdditiveBlending, depthWrite: false });

/* ---------------- textures: emblem + name tag (portrait from the site when we have one) ---------------- */
function hex(c) { return '#' + ('000000' + c.toString(16)).slice(-6); }
function canvas(w, h) { var c = document.createElement('canvas'); c.width = w; c.height = h; return c; }
var imgs = {};
function loadImg(name, cb) {
  if (imgs[name]) { if (imgs[name].done) cb(imgs[name].el); else imgs[name].q.push(cb); return; }
  var r = imgs[name] = { el: new Image(), q: [cb], done: false };
  r.el.onload = function () { r.done = true; r.q.forEach(function (f) { f(r.el); }); r.q = []; };
  r.el.src = '/city/cz/' + name + '.webp?v=1';
}
function drawDisc(g, c, cx, cy, R, img) { // portrait or generated emblem in an accent ring
  g.save(); g.beginPath(); g.arc(cx, cy, R, 0, Math.PI * 2); g.closePath();
  var gr = g.createRadialGradient(cx, cy - R * 0.3, R * 0.1, cx, cy, R); gr.addColorStop(0, '#2a0a12'); gr.addColorStop(1, '#070407'); g.fillStyle = gr; g.fill(); g.clip();
  if (img) g.drawImage(img, cx - R, cy - R, R * 2, R * 2);
  else { g.fillStyle = hex(c.accent); g.textAlign = 'center'; g.textBaseline = 'middle'; g.font = '700 ' + Math.round(R * (c.emb.length > 1 ? 0.8 : 1.15)) + 'px Georgia, "Segoe UI Symbol", serif'; g.fillText(c.emb, cx, cy + R * 0.06); }
  g.restore(); g.lineWidth = Math.max(3, R * 0.12); g.strokeStyle = hex(c.accent); g.beginPath(); g.arc(cx, cy, R - g.lineWidth / 2, 0, Math.PI * 2); g.stroke();
  g.lineWidth = 2; g.strokeStyle = '#e8c36a'; g.beginPath(); g.arc(cx, cy, R, 0, Math.PI * 2); g.stroke();
}
function tagCanvas(c, img) {
  var cv = c._tagCv || (c._tagCv = canvas(320, 96)), g = cv.getContext('2d'), W = 320, H = 96;
  g.clearRect(0, 0, W, H); g.fillStyle = 'rgba(8,4,8,.86)'; g.beginPath(); g.moveTo(40, 6); g.lineTo(W - 10, 6); g.quadraticCurveTo(W - 4, 6, W - 4, 14); g.lineTo(W - 4, H - 14); g.quadraticCurveTo(W - 4, H - 6, W - 10, H - 6); g.lineTo(40, H - 6); g.closePath(); g.fill();
  g.strokeStyle = hex(c.accent); g.lineWidth = 2; g.stroke();
  drawDisc(g, c, 46, 48, 40, img);
  g.textAlign = 'left'; g.fillStyle = '#f2d27a'; g.font = '700 30px Georgia, serif'; g.fillText(c.name, 96, 46, W - 106);
  g.fillStyle = hex(c.accent); g.font = '700 17px system-ui, sans-serif'; g.fillText(c.role.toUpperCase(), 97, 72, W - 106);
  return cv;
}
function emblemCanvas(c, img) { var cv = c._embCv || (c._embCv = canvas(128, 128)), g = cv.getContext('2d'); g.clearRect(0, 0, 128, 128); drawDisc(g, c, 64, 64, 60, img); return cv; }
function portraitURL(c) { // small avatar for the talk panel / list
  if (c.img) return '/city/cz/' + c.img + '.webp?v=1';
  return emblemCanvas(c, null).toDataURL('image/png');
}

/* ---------------- build one ci