<!-- SOLID CITY · part 38 -->
.id; grp.add(hit);
    scene.add(grp);
    consoles[d.id] = { group: grp, ring: ring, screenCtx: c.getContext('2d'), screenTex: tex, hit: hit };
    colliders.push({ x: d.x, z: d.z, hw: 1.5, hd: 1.0 });
    drawScreen(d.id);
  });
}

function refreshConsole(taskId) {
  drawScreen(taskId);
  consoles[taskId].ring.material.color.setHex(DMG_COLORS[SYS[taskId].dmg]);
}

/* ---------- Input: keys + touch (joystick move, drag look, tap claim) ---------- */
var keys = {};
var yaw = 0, pitch = 0;   // face -z initially (yaw PI to match camera.rotation.y=PI)
var joy = { active: false, id: -1, ox: 0, oy: 0, dx: 0, dy: 0 };
var look = { id: -1, lx: 0, ly: 0 };
var tapInfo = null;
var raycaster = null;

addEventListener('keydown', function (e) {
  keys[e.code] = true;
  if (e.code === 'KeyP' && G.mode === 'play') pauseGame();
  if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].indexOf(e.code) >= 0) e.preventDefault();
});
addEventListener('keyup', function (e) { keys[e.code] = false; });

function setupTouch() {
  var el = renderer.domElement;
  raycaster = new THREE.Raycaster();
  el.style.touchAction = 'none';
  el.addEventListener('touchstart', function (e) {
    for (var i = 0; i < e.changedTouches.length; i++) {
      var t = e.changedTouches[i];
      if (t.clientX < innerWidth * 0.45 && joy.id === -1) {
        joy.id = t.identifier; joy.active = true; joy.ox = t.clientX; joy.oy = t.clientY; joy.dx = 0; joy.dy = 0;
        showJoy(t.clientX, t.clientY);
      } else if (look.id === -1) {
        look.id = t.identifier; look.lx = t.clientX; look.ly = t.clientY;
        tapInfo = { x: t.clientX, y: t.clientY, t: performance.now(), moved: false };
      }
    }
    e.preventDefault();
  }, { passive: false });
  el.addEventListener('touchmove', function (e) {
    for (var i = 0; i < e.changedTouches.length; i++) {
      var t = e.changedTouches[i];
      if (t.identifier === joy.id) {
        joy.dx = Math.max(-1, Math.min(1, (t.clientX - joy.ox) / 60));
        joy.dy = Math.max(-1, Math.min(1, (t.clientY - joy.oy) / 60));
        moveJoyKnob(joy.dx, joy.dy);
      } else if (t.identifier === look.id) {
        var dx = t.clientX - look.lx, dy = t.clientY - look.ly;
        look.lx = t.clientX; look.ly = t.clientY;
        if (tapInfo && Math.hypot(dx, dy) > 8) tapInfo.moved = true;
        yaw -= dx * 0.0045; pitch = Math.max(-1.2, Math.min(1.2, pitch - dy * 0.0045));
      }
    }
    e.preventDefault();
  }, { passive: false });
  function endTouch(e) {
    for (var i = 0; i < e.changedTouches.length; i++) {
      var t = e.changedTouches[i];
      if (t.identifier === joy.id) { joy.id = -1; joy.active = false; joy.dx = joy.dy = 0; hideJoy(); }
      if (t.identifier === look.id) {
        if (tapInfo && !tapInfo.moved && performance.now() - tapInfo.t < 300) tapAt(t.clientX, t.clientY);
        look.id = -1; tapInfo = null;
      }
    }
  }
  el.addEventListener('touchend', endTouch); el.addEventListener('touchcancel', endTouch);
  // mouse: drag to look, click to claim
  var mdown = false, mlx = 0, mly = 0, mMoved = false;
  el.addEventListener('mousedown', function (e) { mdown = true; mMoved = false; mlx = e.clientX; mly = e.clientY; });
  el.addEventListener('mousemove', function (e) {
    if (!mdown) return;
    var dx = e.clientX - mlx, dy = e.clientY - mly; mlx = e.clientX; mly = e.clientY;
    if (Math.abs(dx) + Math.abs(dy) > 3) mMoved = true;
    yaw -= dx * 0.004; pitch = Math.max(-1.2, Math.min(1.2, pitch - dy * 0.004));
  });
  el.addEventListener('mouseup', function (e) { if (mdown && !mMoved) tapAt(e.clientX, e.clientY); mdown = false; });
}

function showJoy(x, y) {
  var j = $('joy'); j.style.display = 'block'; j.style.left = (x - 60) + 'px'; j.style.top = (y - 60) + 'px';
}
function moveJoyKnob(dx, dy) {
  var k = $('joyknob'); k.style.transform = 'translate(' + (dx * 34) + 'px,' + (dy * 34) + 'px)';
}
function hideJoy() { $('joy').style.display = 'none'; }

function tapAt(px, py) {
  if (G.mode !== 'play') return;
  var ndc = new THREE.Vector2((px / innerWidth) * 2 - 1, -(py / innerHeight) * 2 + 1);
  raycaster.setFromCamera(ndc, camera);
  var hits = [];
  Object.keys(consoles).forEach(function (id) { hits.push(consoles[id].hit); });
  var found = raycaster.intersectObjects(hits, false);
  if (found.length) tryClaim(found[0].object.userData.taskId);
}

/* ---------- Claiming & repair ---------- */
function tryClaim(taskId) {
  var owner = taskOwner(taskId);
  if (owner === 'YOU') { promptRepair(taskId); return; }
  var r = coord.claim(taskId, 'YOU');
  if (r.ok) {
    G.holds = taskId; G.repairProg = 0;
    if (G.solidRepair && G.solidRepair.task === taskId) {
      G.solidRepair = null;
      solidSays('Yours. I\'ll take the next.');
    }
    feed('YOU', 'Took ' + SYS[taskId].label + (r.unchanged ? '' : ' — Solid yields.'));
    promptRepair(taskId);
  } else {
    feed('SYS', 'Claim refused: ' + r.reason);
  }
}
function releaseHold() {
  if (!G