<!-- SOLID CITY · part 54 -->
w T.Vector3(1, 1, 1))); if (!g.attributes.uv) g.setAttribute('uv', new T.BufferAttribute(new Float32Array(g.attributes.position.count * 2), 2)); this.g.push(g); return this; };
Merge.prototype.mesh = function (mat) {
  var n = 0; this.g.forEach(function (p) { n += p.attributes.position.count; }); var P = new Float32Array(n * 3), N = new Float32Array(n * 3), U = new Float32Array(n * 2), o = 0;
  this.g.forEach(function (p) { var c = p.attributes.position.count; P.set(p.attributes.position.array, o * 3); N.set(p.attributes.normal.array, o * 3); U.set(p.attributes.uv.array, o * 2); o += c; p.dispose(); });
  var g = new T.BufferGeometry(); g.setAttribute('position', new T.BufferAttribute(P, 3)); g.setAttribute('normal', new T.BufferAttribute(N, 3)); g.setAttribute('uv', new T.BufferAttribute(U, 2)); g.computeBoundingSphere(); return new T.Mesh(g, mat);
};
function gradTex(w, h, fn) { var c = document.createElement('canvas'); c.width = w; c.height = h; fn(c.getContext('2d'), w, h); var t = new T.CanvasTexture(c); return t; }
function pageGeo(w, d, left) {   // one curved page: rises from the spine and rolls down to the fore-edge; uv covers its half of the spread
  var g = new T.PlaneGeometry(w, d, 24, 1), p = g.attributes.position, uv = g.attributes.uv;
  for (var i = 0; i < p.count; i++) {
    var x = p.getX(i), u = (x + w / 2) / w, fromSpine = Math.min(1, Math.max(0, left ? 1 - u : u)), y;
    y = 0.05 + 0.16 * Math.sin(Math.PI * Math.pow(fromSpine, 0.62)) * (1 - 0.25 * fromSpine);
    p.setXYZ(i, x + (left ? -w / 2 : w / 2), p.getY(i), y);
    uv.setX(i, left ? u * 0.5 : 0.5 + u * 0.5);
  }
  g.rotateX(-Math.PI / 2); g.computeVertexNormals(); return g;
}
function build(book) {
  var stone = curve(new T.MeshLambertMaterial({ color: 0x0c090b })), gold = curve(new T.MeshLambertMaterial({ color: 0xd9ab46, emissive: 0x4a3208 })), red = curve(new T.MeshBasicMaterial({ color: 0xc8102e }));
  var st = new Merge(), gd = new Merge(), rd = new Merge();
  st.add(new T.CylinderGeometry(3.7, 3.9, 0.2, 48), 0, 0.1, 0);                          // floor disc
  gd.add(new T.TorusGeometry(3.3, 0.05, 4, 64), 0, 0.21, 0, Math.PI / 2);
  gd.add(new T.TorusGeometry(3.75, 0.06, 4, 64), 0, 0.2, 0, Math.PI / 2);
  st.add(new T.BoxGeometry(2.6, 0.5, 2.0), 0, 0.45, 0); gd.add(new T.BoxGeometry(2.72, 0.08, 2.12), 0, 0.72, 0);   // plinth + gold band
  st.add(new T.CylinderGeometry(0.72, 0.9, 1.45, 8), 0, 1.48, 0);                       // octagonal column
  [0.82, 1.2, 1.76, 2.16].forEach(function (y, i) { gd.add(new T.CylinderGeometry(i % 3 ? 0.8 : 0.94, i % 3 ? 0.8 : 0.94, 0.07, 8), 0, y, 0); });
  st.add(new T.BoxGeometry(2.3, 0.22, 1.7), 0, 2.32, 0); gd.add(new T.BoxGeometry(2.42, 0.06, 1.82), 0, 2.45, 0);   // capital
  // spotlight post behind the book, arm over it, lamp head
  st.add(new T.CylinderGeometry(0.11, 0.15, 7.6, 10), 0, 3.8, -3.1); gd.add(new T.SphereGeometry(0.2, 12, 8), 0, 7.65, -3.1);
  st.add(new T.BoxGeometry(0.14, 0.14, 3.2), 0, 7.5, -1.55); gd.add(new T.CylinderGeometry(0.22, 0.42, 0.5, 16, 1, true), 0, 7.25, 0.1);
  // the open book on a lectern tilted toward the reader
  var lec = new T.Group(); lec.position.set(0, 2.62, 0.05); lec.rotation.x = 0.34;
  var lm = new Merge(), lg = new Merge();
  lm.add(new T.BoxGeometry(3.7, 0.07, 2.5), 0, 0.0, 0);                                 // black cover boards
  lg.add(new T.BoxGeometry(3.78, 0.03, 2.58), 0, -0.04, 0);                             // gilt edge of the boards
  lg.add(new T.BoxGeometry(1.7, 0.1, 2.3), -0.88, 0.07, 0); lg.add(new T.BoxGeometry(1.7, 0.1, 2.3), 0.88, 0.07, 0);   // gilded page blocks
  lg.add(new T.CylinderGeometry(0.07, 0.07, 2.4, 8), 0, 0.06, 0, Math.PI / 2);          // spine
  rd.add(new T.BoxGeometry(0.08, 0.01, 1.5), 0.55, 0.31, 0.45);                      // red ribbon bookmark, lying in the right page (correction 2026-10-14: it hung 1.3 m in the air down the pedestal's face; now it rests in the book as a bookmark rests)
  lec.add(lm.mesh(stone)); lec.add(lg.mesh(gold)); lec.add(rd.mesh(red));
  var W = A.MOBILE ? 1024 : 2048, cv = BookLawArt.engrave(W, W / 2, book);
  var tex = new T.CanvasTexture(cv); tex.encoding = T.sRGBEncoding; tex.anisotropy = 8; tex.generateMipmaps = true; tex.minFilter = T.LinearMipmapLinearFilter;
  var pm = curve(new T.MeshBasicMaterial({ map: tex }));
  var pg = new T.Mesh((function () { var a = pageGeo(1.72, 2.32, true), b = pageGeo(1.72, 2.32, false), m = new Merge(); m.g.push(a.toNonIndexed(), b.toNonIndexed()); a.dispose(); b.dispose(); return m.mesh(pm).geometry; })(), pm);
  pg.position.y = 0.1; lec.add(pg);
  // soft spotlight: additive cone + floor glow (no real light: adding one would recompile every material in the city)
  var cone = new T.Mesh(new T.CylinderGeometry(0.4, 2.6, 4.9, 32, 1, true), new T.MeshBasicMaterial({ map: gradTex(4, 128, function (g, w, h) { var gr = g.createLinearGradient(0, 0, 0, h); gr.addColorStop(0, 'rgba(255,214,130,0.42)'); gr.addColorSt