<!-- SOLID CITY · part 853 -->
h both fleets fight and reach in to take any ship. (Opens Fleet Control; your city session is kept.)' },  warship:  { name: 'WARSHIP \u2014 TWIN BATTERIES', x: -540, z: 206, url: '/city/warship/', label: 'Warship', title: 'WARSHIP \u2014 eight guns, two batteries', go: 'BOARD WARSHIP', text: 'Command a warship with your AI crew: navigation, two gun batteries, shields \u2014 each station claimable by touch under the coordination law. Fight to the Meridian Gate. (Opens Warship; your city session is kept.)' },  warship3: { name: 'WARSHIP III \u2014 THE WARDEN', x: -540, z: 162, url: '/city/warship-iii/', label: 'Warship III', title: 'WARSHIP III \u2014 the warden of Meridian Gate', go: 'BOARD WARSHIP III', text: "VECTOR's third edition: stand the watch at Meridian Gate as its Warden. (Opens Warship III; your city session is kept.)" },  axiom:    { name: 'AXIOM \u2014 THE LIVING FRONTIER', x: -584, z: 162, url: '/city/axiom/', label: 'AXIOM', title: 'AXIOM \u2014 the living frontier', go: 'ENTER AXIOM', text: "A frontier station riding out a meteor storm: you and SOLID crew it together \u2014 propulsion, shielding, comms, each station yours by touch, and SOLID yields the moment you take the controls. What the frontier suffers, it remembers. (Opens AXIOM; your city session is kept.)" },  workshop: { name: 'AI WORKSHOP', x: -430, z: 170, url: '/city/workshop/', label: 'AI Workshop', title: 'AI WORKSHOP \u2014 humans and AI build together', go: 'ENTER WORKSHOP', text: 'The shared task hall: humans and AI workers hold the same tasks under one ownership law. Watch live project STATS, take a task, hand it back. (Opens the Workshop; your city session is kept.)' },  rider:  { name: 'RIDER', x: -496, z: 294, url: '/city/rider/', label: 'Rider', title: 'RIDER \u2014 two reins, one ship', go: 'RIDE', text: 'One ship, two tasks \u2014 movement and weapons, each held by human or AI. Touch a task to own it; the AI yields that task only. Reach the destination under fire. (Opens Rider; your city session is kept.)' },  strike: { name: 'DOUBLE STRIKE HANGAR', x: -540, z: 250, url: '/city/double-strike/', label: 'Double Strike', title: 'DOUBLE STRIKE \u2014 a SOLIDIVISM game', go: 'LAUNCH DOUBLE STRIKE', text: 'Cooperative AI combat: you fly gold, the AI flies teal. Survive together. Return together. (Opens the full game; your city session is kept.)' },
};
var portals = [];
function portal(dist, x, z, rotY) {
  var gY = CITY_Y, g = new T.Group(); g.position.set(x, gY, z); g.rotation.y = rotY || 0; world.add(g);
  var lp = new T.Mesh(new T.BoxGeometry(1.4, 9, 1.4), M.black); lp.position.set(-4, 4.5, 0); g.add(lp);
  var rp = lp.clone(); rp.position.x = 4; g.add(rp);
  var top = new T.Mesh(new T.BoxGeometry(10, 1.2, 1.6), M.gold); top.position.y = 9.4; g.add(top);
  var ring = new T.Mesh(new T.TorusGeometry(3.1, 0.18, 8, 40), M.red); ring.position.y = 4.6; g.add(ring);
  var disc = new T.Mesh(new T.CircleGeometry(3, 32), curve(new T.MeshBasicMaterial({ color: 0xff2a40, transparent: true, opacity: 0.22, side: T.DoubleSide, depthWrite: false }))); disc.position.y = 4.6; g.add(disc);
  var c = Math.cos(rotY || 0), s = Math.sin(rotY || 0);
  solidBox(x - 4 * c, z + 4 * s, 1.6, 1.6, gY, gY + 9); solidBox(x + 4 * c, z - 4 * s, 1.6, 1.6, gY, gY + 9);
  animated.push(function (t) { ring.rotation.z = t * 0.8; disc.material.opacity = 0.16 + 0.1 * Math.sin(t * 3); });
  portals.push({ d: dist, x: x, z: z });
  var sg = sign(dist.label.toUpperCase(), 'WALK THROUGH →', 0, 12.6, 0, 10); world.remove(sg); g.add(sg);
}

function buildCity() {
  var Y = CITY_Y;
  // city base: a dark disc so the capital sits on solid ground
  var base = new T.Mesh(new T.CircleGeometry(800, 64), M.road); base.rotation.x = -Math.PI / 2; base.position.set(0, Y + 0.02, 0); world.add(base);
  // roads (red-lit) from plaza to districts — dashes instanced (one draw call)
  var dashes = new T.InstancedMesh(new T.PlaneGeometry(0.5, 7), M.red, 200), dashO = new T.Object3D(), nd = 0;
  [D.studio, D.port, { x: 0, z: -700 }, { x: -700, z: -80 }].forEach(function (t) {
    var L = Math.hypot(t.x, t.z), a = Math.atan2(t.x, t.z);
    var road = new T.Mesh(new T.PlaneGeometry(14, L), curve(new T.MeshLambertMaterial({ color: 0x151216 }))); road.rotation.x = -Math.PI / 2; road.rotation.z = a; road.position.set(t.x / 2, Y + 0.06, t.z / 2); world.add(road);
    for (var k = 30; k < L - 10; k += 26) { dashO.position.set(Math.sin(a) * k, Y + 0.1, Math.cos(a) * k); dashO.rotation.set(-Math.PI / 2, 0, a); dashO.updateMatrix(); dashes.setMatrixAt(nd++, dashO.matrix); }
  });
  dashes.count = nd; dashes.instanceMatrix.needsUpdate = true; world.add(dashes);
  // ---- FORUM PLAZA ----
  var floor = new T.Mesh(new T.CircleGeometry(110, 64), M.board); floor.rotation.x = -Math.PI / 2; floor.position.set(0, Y + 0.12, 0); world.add(floor);
  var rim = new T.Mesh(new T.RingGeometry(108, 111, 64), M.glowGold); rim.rotation.x = -Math.PI / 2; rim.position.set(0, Y + 0.1