<!-- SOLID CITY · part 58 -->
dense intaglio linework").
   WHERE: the Forum Plaza, 18 m in front of the Crest Obelisk on the arrival axis, the first thing every visitor sees from the
   spawn point (the University arch is 1.2 km out); placed clear of every citizen walking line (>= 5 m) and the pavilions.
   WHAT: an open book on a black-and-gold pedestal under a soft (faked, additive) spotlight; its spread is ONE procedural canvas
   (book-law-art.js engrave(): guilloche, hatched engraved serif, crown, ornamental border; 2048x1024 desktop / 1024x512 iPhone,
   mipmapped, built once). Collision, no-build zone (forge-core.js), radar "LAW", Hyper stop "the King's Book of Law", Watch
   CAMPUS TOUR caption, "READ THE BOOK" -> an engraved gold-on-black reader (13 pages + title page, page turning, swipe, keys).
   The King's words come verbatim from /city/book-of-law.md and are only ever put on screen with textContent. */
(function () {
'use strict';
if (!window.SC || !SC.api || !window.BookLawArt) return;
var A = SC.api, T = A.T, S = A.S, Y = A.CITY_Y, curve = A.curve || function (m) { return m; };
var POS = { x: -4, z: 46 }, FACE = Math.atan2(6 - POS.x, 62 - POS.z);   // the book faces the arrival point (6, 62)
var READ = { x: POS.x + Math.sin(FACE) * 4.6, z: POS.z + Math.cos(FACE) * 4.6 };
var MD_URL = '/city/book-of-law.md?v=20261010a';
var B = { state: 'idle', book: null, page: 0, built: false, wait: 1.0, auto: false };
function $(id) { return document.getElementById(id); }
function text() { if (B.book) return Promise.resolve(B.book); return fetch(MD_URL, { cache: 'force-cache' }).then(function (r) { if (!r.ok) throw new Error('book ' + r.status); return r.text(); }).then(function (md) { B.book = BookLawArt.parse(md); return B.book; }); }

/* ---------------- the sculpture ---------------- */
function Merge() { this.g = []; }
Merge.prototype.add = function (geo, x, y, z, rx, ry, rz) { var g = geo.index ? geo.toNonIndexed() : geo.clone(); g.applyMatrix4(new T.Matrix4().compose(new T.Vector3(x, y, z), new T.Quaternion().setFromEuler(new T.Euler(rx || 0, ry || 0, rz || 0)), new 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.S