// ============================================================
// Rack3D — single-rack detailed Three.js view
// Adapted from low-poly-rack-threejs-asset-factory approach.
// Uses global THREE (loaded in index.html).
// ============================================================
const { useRef: useRef3R, useEffect: useEffect3R, useState: useState3R, useMemo: useMemo3R } = React;

// ── Color palette ───────────────────────────────────────────────────────
const R3_COL = {
  bg:        0x010912,  // deep void navy
  rackMetal: 0x091826,
  rackEdge:  0x12273e,
  panel:     0x040d1c,
  panel2:    0x050f1e,
  bevel:     0x0d1e32,
  cyan:      0x00eeff,  // electric cyan
  cyanDim:   0x024055,
  ok:        0x00e599,  // emerald
  green:     0x00eca8,
  amber:     0xf5b942,  // gold amber
  off:       0x081122,
  dark:      0x0c1422,
  portDark:  0x010810,
  text:      0x6ab8d8,
  white:     0xe8f6ff,
  line:      0x0f2038,
};

// ── One-time animated style injection ────────────────────────────────────
(function r3InjectStyles() {
  if (document.getElementById('r3d-anim')) return;
  const s = document.createElement('style');
  s.id = 'r3d-anim';
  s.textContent = [
    '@keyframes r3scanBar { 0%{transform:translateX(-120%)} 100%{transform:translateX(120%)} }',
    '@keyframes r3pulse   { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.3;transform:scale(.65)} }',
    '@keyframes r3blink   { 0%,100%{opacity:1} 50%{opacity:.1} }',
    '@keyframes r3shimmer { 0%{opacity:.25} 50%{opacity:.7} 100%{opacity:.25} }',
    '.r3-rack-row:hover   { background:rgba(0,238,255,0.05) !important; border-color:rgba(0,238,255,0.22) !important; }',
  ].join('\n');
  document.head.appendChild(s);
})();

const R3_RACK = {
  width:               7.8,
  height:              8.8,
  depth:               1.15,
  unitGap:             0.08,
  sideRailWidth:       0.18,
  topBottomRailHeight: 0.18,
};

const R3_DEVICE = {
  width:  7.05,
  height: 1.5,     // taller — gives clear non-overlapping zones for bezel, ports, labels, LEDs
  depth:  0.48,
  faceZ:  0.38,
};

// Device-local vertical zones (bottom→top, device.height = 1.5):
//   y =  0.525..  0.745  — TOP BEZEL          (height 0.22, holds device name)
//   y =  0.225..  0.525  — ODD-PORT LABEL     (height 0.30, centre 0.380)
//   y =  0.120..  0.230  — ODD-PORT ROW       (port_y = 0.175, port height 0.11)
//   y = -0.130..  0.120  — INTER-ROW GAP      (height 0.25)
//   y = -0.130.. -0.020  — EVEN-PORT ROW      (port_y = -0.075)
//   y = -0.470.. -0.130  — EVEN-PORT LABEL    (height 0.34, centre -0.325)
//   y = -0.530.. -0.470  — LED ROW            (ledY = -0.50, radius 0.03)
//   y = -0.750.. -0.530  — LED LABEL ZONE     (height 0.22, centre -0.670)
const R3_SWITCH = {
  portSizeX:    0.180,
  portSizeY:    0.110,
  portDepth:    0.012,
  portSpacingX: 0.260,
  portRowGap:   0.250,
  portTopY:     0.050,
  ledRadius:    0.030,
  ledY:        -0.500,    // raised slightly so a label zone fits below the LED row
  ledSpacingX:  0.130,
  portOffsetX:  0,
  // Label offsets are pre-computed centres of label zones; they never overlap
  // with the bezel, the ports, the opposite-row labels, or the LEDs.
  oddLabelOffsetY:  0.205,   // odd port_y (0.175) + 0.205 = 0.380
  evenLabelOffsetY: -0.250,  // even port_y (-0.075) - 0.250 = -0.325
  ledLabelOffsetY: -0.170,   // LED y (-0.500) - 0.170 = -0.670
  labelZoneLocal:   0.300,
  ledLabelZoneLocal:0.180,
  get faceZ() { return R3_DEVICE.faceZ + 0.245; },
};

// ── Helpers ─────────────────────────────────────────────────────────────
function r3Mat(opts) {
  const { color, emissive = 0x000000, emissiveIntensity = 0,
          metalness = 0.2, roughness = 0.65, transparent = false, opacity = 1 } = opts;
  return new THREE.MeshStandardMaterial({ color, emissive, emissiveIntensity,
    metalness, roughness, transparent, opacity });
}

// Indicator elements (LEDs, power dots, port plugs) must match CSS colors exactly.
// MeshBasicMaterial skips lighting; toneMapped=false skips ACES tone mapping.
// transparent=true enables opacity-based blink animation.
function r3IndicatorMat(color) {
  const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 1.0 });
  mat.toneMapped = false;
  return mat;
}

function r3RoundedBox(w, h, d, mat) {
  const grp = new THREE.Group();
  const body = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
  grp.add(body);
  return grp;
}

function r3TextSprite(text, { font = '500 28px Consolas,monospace', fill = '#8fa3bd',
                               bg = 'rgba(0,0,0,0)', padding = 12, scale = 0.22, align = 'center' } = {}) {
  const fontSize = parseInt(font.match(/(\d+)px/)?.[1] || '28');
  const canvasH  = Math.ceil(fontSize * 2.4);          // canvas height proportional to font — no clipping
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  ctx.font = font;
  const metrics = ctx.measureText(text);
  canvas.width  = Math.ceil(metrics.width + padding * 2);
  canvas.height = canvasH;
  ctx.font = font;
  ctx.fillStyle = bg;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = fill;
  ctx.textBaseline = 'middle';
  ctx.textAlign = align;
  const x = align === 'left' ? padding : canvas.width / 2;
  ctx.fillText(text, x, canvas.height / 2);
  const texture  = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace || texture.colorSpace;
  const aspect = canvas.width / canvas.height;
  const mat = new THREE.MeshBasicMaterial({
    map: texture, transparent: true,
    depthWrite: false, depthTest: false,   // always render on top — never occluded by geometry
    side: THREE.DoubleSide,
  });
  mat.toneMapped = false;
  const plane = new THREE.Mesh(new THREE.PlaneGeometry(aspect * scale, scale), mat);
  plane.renderOrder = 10;                  // render after all opaque meshes
  plane.userData._r3dispose = () => { texture.dispose(); mat.dispose(); };
  return plane;
}

function r3UnitY(uPos, maxU) {
  const inner    = R3_RACK.height - R3_RACK.topBottomRailHeight * 2;
  const bottom   = -R3_RACK.height / 2 + R3_RACK.topBottomRailHeight;
  const unitH    = inner / maxU;
  return bottom + unitH * (uPos - 0.5);
}

function r3UnitHeight(maxU, unitHeight = 1) {
  const inner = R3_RACK.height - R3_RACK.topBottomRailHeight * 2;
  return (inner / maxU) * unitHeight - R3_RACK.unitGap;
}

function r3PortPos(portNumber) {
  const col   = Math.floor((portNumber - 1) / 2);
  const isOdd = portNumber % 2 === 1;
  return {
    x: (col - 11.5) * R3_SWITCH.portSpacingX + R3_SWITCH.portOffsetX,
    y: R3_SWITCH.portTopY + (isOdd ? R3_SWITCH.portRowGap / 2 : -R3_SWITCH.portRowGap / 2),
    z: R3_SWITCH.faceZ,
  };
}

function r3LedPos(portNumber) {
  return {
    x: (portNumber - 24.5) * R3_SWITCH.ledSpacingX + R3_SWITCH.portOffsetX,
    y: R3_SWITCH.ledY,
    z: R3_SWITCH.faceZ + 0.012,
  };
}

function r3LedMat(state) {
  if (state === 'green_on' || state === 'green_blink')
    return r3IndicatorMat(R3_COL.green);
  if (state === 'amber_on' || state === 'amber_blink')
    return r3IndicatorMat(R3_COL.amber);
  return r3IndicatorMat(R3_COL.off);
}

// ── Port builder ────────────────────────────────────────────────────────
function r3MakePort(portStatus, switchId, cableLen = 0.0012, registry = null) {
  const grp = new THREE.Group();
  const pos = r3PortPos(portStatus.portNumber);
  grp.position.set(pos.x, pos.y, pos.z);

  const frameMat  = r3Mat({ color: 0x123044, metalness: 0.4, roughness: 0.45 });
  const recessMat = r3Mat({ color: R3_COL.portDark, metalness: 0.1, roughness: 0.9 });

  const frame = new THREE.Mesh(
    new THREE.BoxGeometry(R3_SWITCH.portSizeX, R3_SWITCH.portSizeY, R3_SWITCH.portDepth), frameMat);
  frame.name = `port-frame-${portStatus.portNumber}`;
  grp.add(frame);

  const recess = new THREE.Mesh(
    new THREE.BoxGeometry(R3_SWITCH.portSizeX * 0.78, R3_SWITCH.portSizeY * 0.72, R3_SWITCH.portDepth * 0.7),
    recessMat);
  recess.position.z = R3_SWITCH.portDepth * 0.58;
  grp.add(recess);

  if (portStatus.connection === 'connected') {
    // RJ45 plug body
    const pW = R3_SWITCH.portSizeX * 0.62, pH = R3_SWITCH.portSizeY * 0.52, pD = R3_SWITCH.portDepth * 1.4;
    const plug = new THREE.Mesh(new THREE.BoxGeometry(pW, pH, pD), r3IndicatorMat(R3_COL.green));
    plug.position.z = R3_SWITCH.portDepth * 1.1;
    grp.add(plug);

    // Energized edge — neon outline that pulses like current flowing
    const edgeMat = new THREE.LineBasicMaterial({ color: 0xc0fff0, toneMapped: false, transparent: true, opacity: 1.0 });
    const edgeMesh = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(pW, pH, pD)), edgeMat);
    edgeMesh.position.z = R3_SWITCH.portDepth * 1.1;
    grp.add(edgeMesh);
    if (registry) registry.energizedEdges.push({ mat: edgeMat, pn: portStatus.portNumber });

    // Latch clip — small tab on underside of plug
    const latch = new THREE.Mesh(
      new THREE.BoxGeometry(R3_SWITCH.portSizeX * 0.22, R3_SWITCH.portSizeY * 0.15, R3_SWITCH.portDepth * 0.55),
      r3IndicatorMat(R3_COL.green));
    latch.position.set(0, -R3_SWITCH.portSizeY * 0.30, R3_SWITCH.portDepth * 1.42);
    grp.add(latch);

    // Strain-relief boot
    const boot = new THREE.Mesh(
      new THREE.CylinderGeometry(0.025, 0.019, 0.030, 8),
      r3Mat({ color: 0x1a3c4b, roughness: 0.78 }));
    boot.rotation.x = Math.PI / 2;
    boot.position.z = R3_SWITCH.portDepth * 2.2;
    grp.add(boot);

    // Cable stub — length controlled by cableLen setting
    const cable = new THREE.Mesh(
      new THREE.CylinderGeometry(0.019, 0.019, cableLen, 8),
      r3Mat({ color: 0x1a3c4b, roughness: 0.90, metalness: 0.0 }));
    cable.rotation.x = Math.PI / 2;
    cable.position.z = R3_SWITCH.portDepth * 2.4;
    grp.add(cable);
  }

  if (portStatus.connection === 'module_only') {
    const mod = new THREE.Mesh(
      new THREE.BoxGeometry(R3_SWITCH.portSizeX * 0.74, R3_SWITCH.portSizeY * 0.55, R3_SWITCH.portDepth * 1.15),
      r3IndicatorMat(R3_COL.amber));
    mod.position.z = R3_SWITCH.portDepth * 1.0;
    grp.add(mod);
  }

  grp.userData = { kind: 'port', switchId, portNumber: portStatus.portNumber, portStatus };
  return grp;
}

// Port labels live in the rack group (no device scale.y) at fixed offsets from each
// port. The offsets are pre-computed centres of label zones that never overlap with
// the bezel, the opposite port row, or the LEDs — see R3_SWITCH layout comment above.
function r3AddPortLabels(parent, deviceY, scaleY, labelScale = 0.20) {
  for (let p = 1; p <= 48; p++) {
    const pos = r3PortPos(p);
    const lbl = r3TextSprite(String(p),
      { font: '700 56px Consolas,monospace', fill: '#9bc6dc', scale: labelScale });
    const offset = (p % 2 === 1) ? R3_SWITCH.oddLabelOffsetY : R3_SWITCH.evenLabelOffsetY;
    lbl.position.set(pos.x, deviceY + (pos.y + offset) * scaleY, R3_SWITCH.faceZ + 0.025);
    parent.add(lbl);
  }
}

// LED labels sit in the dedicated zone below each LED — never overlaps the LED or
// the device bottom because the zone height is bounded by ledLabelZoneLocal.
function r3AddLedLabels(parent, deviceY, scaleY, labelScale = 0.14) {
  for (let p = 1; p <= 48; p++) {
    const lp = r3LedPos(p);
    const lbl = r3TextSprite(String(p),
      { font: '700 48px Consolas,monospace', fill: '#7aa8c0', scale: labelScale });
    lbl.position.set(lp.x, deviceY + (lp.y + R3_SWITCH.ledLabelOffsetY) * scaleY, lp.z + 0.01);
    parent.add(lbl);
  }
}

// ── Power button ────────────────────────────────────────────────────────
// Canvas-drawn lightning bolt sprite — white fill with accent colour glow
function r3BoltSprite(hexColor) {
  const glow = '#' + hexColor.toString(16).padStart(6, '0');
  const W = 40, H = 56;
  const canvas = document.createElement('canvas');
  canvas.width = W; canvas.height = H;
  const ctx = canvas.getContext('2d');
  ctx.beginPath();
  ctx.moveTo(24, 0);
  ctx.lineTo(0,  32);
  ctx.lineTo(16, 32);
  ctx.lineTo(12, 56);
  ctx.lineTo(40, 20);
  ctx.lineTo(24, 20);
  ctx.closePath();
  // Glow pass — coloured halo behind the white bolt
  ctx.shadowColor = glow;
  ctx.shadowBlur  = 14;
  ctx.fillStyle   = glow;
  ctx.fill();
  // White bolt on top for contrast
  ctx.shadowBlur = 0;
  ctx.fillStyle  = '#ffffff';
  ctx.fill();
  const tex = new THREE.CanvasTexture(canvas);
  tex.colorSpace = THREE.SRGBColorSpace || tex.colorSpace;
  const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthWrite: false });
  mat.toneMapped = false;
  const sprite = new THREE.Sprite(mat);
  sprite.scale.set(0.13, 0.18, 1);
  sprite.userData._r3dispose = () => { tex.dispose(); mat.dispose(); };
  return sprite;
}

function r3MakePower(powerState) {
  const grp = new THREE.Group();
  const isOn   = powerState === 'on';
  const isWarn = powerState === 'warn';
  const active = isOn || isWarn;

  // Match LED palette: green=#00eca8, amber=#f5b454 (same as port LEDs)
  const accentCol = isOn ? R3_COL.green : isWarn ? R3_COL.amber : 0x000000;
  const panelBg   = isOn ? 0x041c10 : isWarn ? 0x1c1000 : 0x0b1520;

  // Panel body — dark background + colour glow when active
  const panel = new THREE.Mesh(
    new THREE.BoxGeometry(0.32, 0.44, 0.008),
    r3Mat({ color: panelBg,
            emissive: active ? accentCol : 0x000000,
            emissiveIntensity: active ? 0.18 : 0,
            metalness: 0.1, roughness: 0.65 }));
  // Flag this mesh so the rack-selection highlight does NOT boost its emissive
  // (otherwise the green/amber would saturate to white when the rack is selected).
  panel.userData._r3noHighlight = true;
  grp.add(panel);

  // Border outline — coloured when active, dim when off
  const edgeGeo = new THREE.EdgesGeometry(new THREE.BoxGeometry(0.32, 0.44, 0.008));
  const edgeMat = new THREE.LineBasicMaterial({
    color: active ? accentCol : 0x1e3248,
    toneMapped: false,
  });
  grp.add(new THREE.LineSegments(edgeGeo, edgeMat));

  if (active) {
    // Bolt icon in the same colour as port LEDs
    const bolt = r3BoltSprite(accentCol);
    bolt.position.set(0, 0.07, 0.012);
    grp.add(bolt);
  } else {
    const dot = new THREE.Mesh(
      new THREE.SphereGeometry(0.028, 8, 5),
      r3IndicatorMat(0x263850));
    dot.position.set(0, 0.07, 0.008);
    dot.scale.z = 1 / 3;
    grp.add(dot);
  }

  return grp;
}

// ── Device builders ─────────────────────────────────────────────────────
function r3MakeSwitch(asset, registry, cableLen = 0.0012) {
  const grp = new THREE.Group();
  grp.name = asset.id;
  grp.userData = { kind: 'switch', asset };

  const body = r3RoundedBox(R3_DEVICE.width, R3_DEVICE.height, R3_DEVICE.depth,
    r3Mat({ color: R3_COL.panel, metalness: 0.45, roughness: 0.55 }));
  grp.add(body);

  const frontPlate = new THREE.Mesh(
    new THREE.BoxGeometry(R3_DEVICE.width * 0.985, R3_DEVICE.height * 0.9, 0.035),
    r3Mat({ color: R3_COL.panel2, metalness: 0.35, roughness: 0.62 }));
  frontPlate.position.z = R3_SWITCH.faceZ - 0.035;
  grp.add(frontPlate);

  // ── Top bezel strip — rack-coloured nameplate flush against the device top ──
  const tbW = R3_DEVICE.width * 0.94;
  const tbH = 0.22;
  const tbY = R3_DEVICE.height / 2 - tbH / 2 - 0.005;   // moved up, near the top edge
  const topBezel = new THREE.Mesh(
    new THREE.BoxGeometry(tbW, tbH, 0.025),
    r3Mat({ color: R3_COL.rackEdge, metalness: 0.5, roughness: 0.48 }));
  topBezel.position.set(0, tbY, R3_SWITCH.faceZ - 0.005);
  grp.add(topBezel);
  // Cyan accent at the bottom edge of the bezel
  const tbAccent = new THREE.Mesh(
    new THREE.BoxGeometry(tbW * 0.96, 0.010, 0.018),
    r3Mat({ color: R3_COL.cyan, emissive: R3_COL.cyan, emissiveIntensity: 2.4, roughness: 0.0 }));
  tbAccent.position.set(0, tbY - tbH / 2 + 0.006, R3_SWITCH.faceZ + 0.010);
  grp.add(tbAccent);

  const ports = asset.ports || [];
  ports.forEach(port => {
    const pm = r3MakePort(port, asset.id, cableLen, registry);
    grp.add(pm);
    registry.portMeshes.set(`${asset.id}:${port.portNumber}`, pm);
    const led = new THREE.Mesh(
      new THREE.SphereGeometry(R3_SWITCH.ledRadius, 10, 6), r3LedMat(port.led));
    const lp  = r3LedPos(port.portNumber);
    led.position.set(lp.x, lp.y, lp.z);
    led.scale.z = 1 / 3;
    led.userData = { kind: 'led', switchId: asset.id, portNumber: port.portNumber, ledState: port.led };
    grp.add(led);
    registry.ledMeshes.set(`${asset.id}:${port.portNumber}`, led);
    registry.blinkingLeds.push(led);
  });

  return grp;
}

function r3MakeServer(asset) {
  const grp = new THREE.Group();
  grp.userData = { kind: 'server', asset };
  const body = r3RoundedBox(R3_DEVICE.width, R3_DEVICE.height, R3_DEVICE.depth,
    r3Mat({ color: R3_COL.panel2, metalness: 0.42, roughness: 0.55 }));
  grp.add(body);

  const isOn = asset.powerState === 'on';

  // ── Top bezel strip — rack-coloured nameplate flush against the device top ──
  const tbW = R3_DEVICE.width * 0.94;
  const tbH = 0.22;
  const tbY = R3_DEVICE.height / 2 - tbH / 2 - 0.005;
  const topBezel = new THREE.Mesh(
    new THREE.BoxGeometry(tbW, tbH, 0.025),
    r3Mat({ color: R3_COL.rackEdge, metalness: 0.5, roughness: 0.48 }));
  topBezel.position.set(0, tbY, R3_DEVICE.faceZ - 0.005);
  grp.add(topBezel);
  const tbAccent = new THREE.Mesh(
    new THREE.BoxGeometry(tbW * 0.96, 0.010, 0.018),
    r3Mat({ color: R3_COL.cyan, emissive: R3_COL.cyan, emissiveIntensity: 2.4, roughness: 0.0 }));
  tbAccent.position.set(0, tbY - tbH / 2 + 0.006, R3_DEVICE.faceZ + 0.010);
  grp.add(tbAccent);

  // Activity bars — restored to original left position
  for (let i = 0; i < 5; i++) {
    const active = isOn && Math.random() > 0.35;
    const bar = new THREE.Mesh(
      new THREE.BoxGeometry(0.12, 0.18 + Math.random() * 0.14, 0.025),
      r3Mat({ color: active ? 0x00c8d8 : 0x182233,
              emissive: active ? R3_COL.cyan : 0x000000,
              emissiveIntensity: active ? 0.55 : 0 }));
    bar.position.set(-R3_DEVICE.width / 2 + 0.22 + i * 0.18, -0.18, R3_DEVICE.faceZ + 0.02);
    grp.add(bar);
  }

  const power = r3MakePower(asset.powerState || 'off');
  power.position.set(R3_DEVICE.width / 2 - 0.34, -0.10, R3_DEVICE.faceZ + 0.025);
  grp.add(power);
  return grp;
}

function r3MakeEmpty() {
  const grp = new THREE.Group();
  grp.userData = { kind: 'empty' };
  const body = new THREE.Mesh(
    new THREE.BoxGeometry(R3_DEVICE.width, R3_DEVICE.height * 0.82, 0.08),
    r3Mat({ color: 0x050b14, metalness: 0.2, roughness: 0.8, transparent: true, opacity: 0.75 }));
  body.position.z = R3_DEVICE.faceZ - 0.08;
  grp.add(body);
  return grp;
}

// ── Rack frame ──────────────────────────────────────────────────────────
function r3MakeRack(rackData, registry, cableLen = 0.0012) {
  const grp = new THREE.Group();
  grp.name = rackData.rackId;

  const railMat = r3Mat({ color: R3_COL.rackMetal, metalness: 0.55, roughness: 0.48 });
  const edgeMat = r3Mat({ color: R3_COL.rackEdge,  metalness: 0.5,  roughness: 0.48 });

  const leftRail = new THREE.Mesh(
    new THREE.BoxGeometry(R3_RACK.sideRailWidth, R3_RACK.height, R3_RACK.depth), railMat);
  leftRail.position.x = -R3_RACK.width / 2 + R3_RACK.sideRailWidth / 2;
  grp.add(leftRail);
  const rightRail = leftRail.clone();
  rightRail.position.x = R3_RACK.width / 2 - R3_RACK.sideRailWidth / 2;
  grp.add(rightRail);

  const topRail = new THREE.Mesh(
    new THREE.BoxGeometry(R3_RACK.width, R3_RACK.topBottomRailHeight, R3_RACK.depth), edgeMat);
  topRail.position.y = R3_RACK.height / 2 - R3_RACK.topBottomRailHeight / 2;
  grp.add(topRail);
  const botRail = topRail.clone();
  botRail.position.y = -R3_RACK.height / 2 + R3_RACK.topBottomRailHeight / 2;
  grp.add(botRail);

  // Cyan glow strip on left edge
  const glowRail = new THREE.Mesh(
    new THREE.BoxGeometry(0.016, R3_RACK.height * 0.93, 0.028),
    r3Mat({ color: R3_COL.cyan, emissive: R3_COL.cyan, emissiveIntensity: 3.2, roughness: 0.0 }));
  glowRail.position.set(-R3_RACK.width / 2 + 0.10, 0, R3_RACK.depth / 2 + 0.018);
  grp.add(glowRail);

  // Secondary thin strip — right edge for symmetry
  const glowRailR = new THREE.Mesh(
    new THREE.BoxGeometry(0.008, R3_RACK.height * 0.93, 0.018),
    r3Mat({ color: R3_COL.cyan, emissive: R3_COL.cyan, emissiveIntensity: 1.4, roughness: 0.0, transparent: true, opacity: 0.5 }));
  glowRailR.position.set(R3_RACK.width / 2 - 0.10, 0, R3_RACK.depth / 2 + 0.015);
  grp.add(glowRailR);

  // Corner accent brackets — precision L-brackets at all 4 front corners
  const bMat = r3Mat({ color: R3_COL.cyan, emissive: R3_COL.cyan, emissiveIntensity: 2.8, metalness: 0.0, roughness: 0.0 });
  const bLen = 0.72, bThk = 0.042, bZ = R3_RACK.depth / 2 + 0.015;
  const hw = R3_RACK.width / 2, hh = R3_RACK.height / 2;
  [[-1,1],[1,1],[-1,-1],[1,-1]].forEach(([sx, sy]) => {
    const bh = new THREE.Mesh(new THREE.BoxGeometry(bLen, bThk, 0.016), bMat);
    bh.position.set(sx * (hw - bLen / 2), sy * (hh - bThk / 2), bZ);
    grp.add(bh);
    const bv = new THREE.Mesh(new THREE.BoxGeometry(bThk, bLen, 0.016), bMat);
    bv.position.set(sx * (hw - bThk / 2), sy * (hh - bLen / 2), bZ);
    grp.add(bv);
  });

  // ── Nameplate panel — physical container for the rack title at the top of the rack ──
  const npH = 0.85, npW = R3_RACK.width, npD = R3_RACK.depth;
  const npY = R3_RACK.height / 2 + npH / 2;  // sits directly on top of the rack frame
  const npFrontZ = npD / 2 + 0.001;

  // Plate body
  const namePlate = new THREE.Mesh(
    new THREE.BoxGeometry(npW, npH, npD * 0.96),
    r3Mat({ color: 0x081728, metalness: 0.58, roughness: 0.42 }));
  namePlate.position.set(0, npY, 0);
  grp.add(namePlate);

  // Inset darker recess on the front for the title text
  const npRecess = new THREE.Mesh(
    new THREE.BoxGeometry(npW * 0.95, npH * 0.78, 0.02),
    r3Mat({ color: 0x040d1c, metalness: 0.4, roughness: 0.6 }));
  npRecess.position.set(0, npY, npFrontZ + 0.01);
  grp.add(npRecess);

  // Cyan accent line at bottom of nameplate (joins with top rail)
  const npAccent = new THREE.Mesh(
    new THREE.BoxGeometry(npW * 0.96, 0.03, 0.02),
    r3Mat({ color: R3_COL.cyan, emissive: R3_COL.cyan, emissiveIntensity: 2.4, roughness: 0.0 }));
  npAccent.position.set(0, npY - npH / 2 + 0.02, npFrontZ + 0.012);
  grp.add(npAccent);

  // Title — sits inside the nameplate recess
  const title = r3TextSprite(`⚡ ${rackData.rackId}`,
    { font: '800 64px Consolas,monospace', fill: '#7ed8ff', scale: 0.42, align: 'left' });
  title.position.set(-R3_RACK.width / 2 + 0.28, npY + 0.10, npFrontZ + 0.025);
  grp.add(title);

  if (rackData.location) {
    const loc = r3TextSprite(rackData.location,
      { font: '500 44px Consolas,monospace', fill: '#5a7a94', scale: 0.22, align: 'left' });
    loc.position.set(-R3_RACK.width / 2 + 0.28, npY - 0.22, npFrontZ + 0.025);
    grp.add(loc);
  }

  // U-slot divider lines
  const maxU = rackData.maxU || 12;
  for (let u = 1; u <= maxU; u++) {
    const y = r3UnitY(u, maxU);
    const line = new THREE.Mesh(
      new THREE.BoxGeometry(R3_RACK.width - 0.5, 0.008, 0.01),
      r3Mat({ color: 0x1a2e44, transparent: true, opacity: 0.55 }));
    line.position.set(0, y + r3UnitHeight(maxU) / 2, R3_RACK.depth / 2 + 0.015);
    grp.add(line);
  }

  // Devices — text labels are added to grp (rack group, scale=1) not to device group,
  // so device.scale.y never distorts label geometry or position.
  rackData.units.forEach(asset => {
    let device;
    if (asset.type === 'switch')      device = r3MakeSwitch(asset, registry, cableLen);
    else if (asset.type === 'server') device = r3MakeServer(asset);
    else                              device = r3MakeEmpty();

    const unitH  = r3UnitHeight(maxU, asset.unitHeight);
    const deviceY = r3UnitY(asset.uPosition, maxU);
    const scaleY  = unitH / R3_DEVICE.height;
    device.position.set(0, deviceY, 0);
    device.scale.y = scaleY;
    grp.add(device);
    registry.deviceMeshes.set(asset.id, device);

    // ── Label layout (all sizes derived from world-space zone heights — no overlap by construction) ──
    // The top bezel mesh lives in device-local space at y = R3_DEVICE.height/2 - 0.11 - 0.005 = 0.635,
    // height 0.22. World height = 0.22 * scaleY.
    const tbLocalY     = R3_DEVICE.height / 2 - 0.22 / 2 - 0.005;
    const bezelWorldY  = deviceY + tbLocalY * scaleY;
    const bezelWorldH  = 0.22 * scaleY;
    // Name height should never exceed the bezel's world height (else it spills out).
    const nameScale = Math.min(0.32, bezelWorldH * 0.90);
    const tagScale  = Math.min(0.18, bezelWorldH * 0.40);
    // Label sizes derived from world-space zone heights — guaranteed no overlap.
    const portLabelScale = Math.max(0.07,
      Math.min(0.24, R3_SWITCH.labelZoneLocal * scaleY * 0.70));
    const ledLabelScale  = Math.max(0.05,
      Math.min(0.18, R3_SWITCH.ledLabelZoneLocal * scaleY * 0.70));
    const tagX  = -R3_DEVICE.width / 2 + 0.30;
    const nameX = -R3_DEVICE.width / 2 + 1.40;

    if (asset.type === 'switch') {
      const tagLbl = r3TextSprite(`SW · U${String(asset.uPosition).padStart(2, '0')}`,
        { font: '800 52px Consolas,monospace', fill: '#7ed8ff', scale: tagScale, align: 'left' });
      tagLbl.position.set(tagX, bezelWorldY, R3_SWITCH.faceZ + 0.025);
      grp.add(tagLbl);

      const nameLbl = r3TextSprite(asset.name || asset.id,
        { font: '800 72px Consolas,monospace', fill: '#ecf6ff', scale: nameScale, align: 'left' });
      nameLbl.position.set(nameX, bezelWorldY, R3_SWITCH.faceZ + 0.025);
      grp.add(nameLbl);

      r3AddPortLabels(grp, deviceY, scaleY, portLabelScale);
      r3AddLedLabels(grp, deviceY, scaleY, ledLabelScale);

    } else if (asset.type === 'server') {
      const tagLbl = r3TextSprite(`SRV · U${String(asset.uPosition).padStart(2, '0')}`,
        { font: '800 52px Consolas,monospace', fill: '#7ed8ff', scale: tagScale, align: 'left' });
      tagLbl.position.set(tagX, bezelWorldY, R3_DEVICE.faceZ + 0.025);
      grp.add(tagLbl);

      const nameLbl = r3TextSprite(asset.name || asset.id,
        { font: '800 72px Consolas,monospace', fill: '#ecf6ff', scale: nameScale, align: 'left' });
      nameLbl.position.set(nameX, bezelWorldY, R3_DEVICE.faceZ + 0.025);
      grp.add(nameLbl);

      // Power state badge — anchored to the power button at world y = deviceY + power_local_y * scaleY
      const pwrText = asset.powerState === 'on' ? 'ON' : asset.powerState === 'warn' ? 'WARN' : 'OFF';
      const pwrFill = asset.powerState === 'on' ? '#00eca8' : asset.powerState === 'warn' ? '#f5b942' : '#4a6070';
      const pwrScale = Math.min(0.22, unitH * 0.18);
      const pwrLbl = r3TextSprite(pwrText,
        { font: '800 56px Consolas,monospace', fill: pwrFill, scale: pwrScale });
      pwrLbl.position.set(R3_DEVICE.width / 2 - 0.34,
        deviceY + (-0.10 - 0.36) * scaleY, R3_DEVICE.faceZ + 0.037);
      grp.add(pwrLbl);

    } else {
      const emptyScale = Math.min(0.22, unitH * 0.20);
      const emptyLbl = r3TextSprite('— EMPTY —',
        { font: '600 50px Consolas,monospace', fill: '#3a526a', scale: emptyScale, align: 'left' });
      emptyLbl.position.set(-R3_DEVICE.width / 2 + 0.30, deviceY, R3_DEVICE.faceZ + 0.025);
      grp.add(emptyLbl);
    }
  });

  return grp;
}

// ── Store format → factory format converter ─────────────────────────────
// Maps store's LED string values (green-blink, orange-blink, …) to 3D state tokens.
function r3LedStateFromStore(ledValue) {
  if (ledValue === 'green')        return 'green_on';
  if (ledValue === 'orange')       return 'amber_on';
  if (ledValue === 'green-blink')  return 'green_blink';
  if (ledValue === 'orange-blink') return 'amber_blink';
  return 'off';
}

function r3LedColorFromState(state) {
  if (state === 'green_on' || state === 'green_blink') return R3_COL.green;
  if (state === 'amber_on' || state === 'amber_blink') return R3_COL.amber;
  return R3_COL.off;
}

// Live-sync LED mesh states from cfg slot data without rebuilding the scene.
function r3SyncLeds(ledMeshes, slots, rackId) {
  const K = slots.length;
  slots.forEach((slot, slotIdx) => {
    if (slot.kind !== 'switch') return;
    const uPos     = K - slotIdx;
    const switchId = slot.id || `${rackId}-u${uPos}`;
    (slot.ports || []).forEach((p, idx) => {
      const portNumber = idx + 1;
      const mesh = ledMeshes.get(`${switchId}:${portNumber}`);
      if (!mesh) return;
      const newState = r3LedStateFromStore(p?.led || 'off');
      if (mesh.userData.ledState === newState) return;
      mesh.userData.ledState = newState;
      mesh.material.color.setHex(r3LedColorFromState(newState));
      mesh.material.opacity = 1.0;
    });
  });
}

function storeRackToR3(rack, group, M) {
  const units = [];
  const K = rack.slots.length;
  rack.slots.forEach((slot, i) => {
    const uPos = K - i; // U1 at bottom
    const ports = (slot.ports || []).slice(0, 48).map((p, idx) => {
      const plug = p?.plug || (typeof p === 'string' ? p : 'empty');
      const led  = p?.led  || 'off';
      let connection = 'empty';
      if (plug === 'rj45' || p === 'connected') connection = 'connected';
      else if (plug === 'sfp')                   connection = 'module_only';
      let ledState = 'off';
      if (led === 'green')         ledState = 'green_on';
      else if (led === 'orange')   ledState = 'amber_on';
      else if (led === 'green-blink')  ledState = 'green_blink';
      else if (led === 'orange-blink') ledState = 'amber_blink';
      return { portNumber: idx + 1, connection, led: ledState, speed: '1G', vlan: '10' };
    });
    // Pad to 48 if switch
    if (slot.kind === 'switch') {
      while (ports.length < 48)
        ports.push({ portNumber: ports.length + 1, connection: 'empty', led: 'off' });
    }
    units.push({
      id:         slot.id || `${rack.id}-u${uPos}`,
      uPosition:  uPos,
      unitHeight: 1,
      type:       slot.kind === 'empty' ? 'empty' : slot.kind,
      name:       aliasOr(slot, slot.name) || slot.id || '',
      powerState: ['on', 'warn'].includes(slot.power) ? slot.power : 'off',
      ports:      slot.kind === 'switch' ? ports : [],
    });
  });
  return {
    rackId:   aliasOr(rack, rack.label) || rack.id,
    location: aliasOr(group, group.tag) || group.id,
    maxU:     K,
    units,
  };
}


// ── Tooltip DOM helper ───────────────────────────────────────────────────
function r3FormatTooltip(userData) {
  if (userData.kind !== 'port') return '';
  const p = userData.portStatus;
  return [
    `<b>${userData.switchId} · Port ${p.portNumber}</b>`,
    `status: ${p.connection}`,
    `led: ${p.led}`,
    p.speed      ? `speed: ${p.speed}` : null,
    p.vlan       ? `vlan: ${p.vlan}`   : null,
    p.peerDevice ? `peer: ${p.peerDevice}` : null,
  ].filter(Boolean).join('<br/>');
}

function r3SetHighlight(obj, on) {
  obj.traverse(child => {
    if (!child.isMesh || !child.material?.emissive) return;
    if (on) {
      child.userData._r3prevEI = child.material.emissiveIntensity;
      child.material.emissiveIntensity = Math.max(child.material.emissiveIntensity, 0.75);
    } else if (child.userData._r3prevEI !== undefined) {
      child.material.emissiveIntensity = child.userData._r3prevEI;
      delete child.userData._r3prevEI;
    }
  });
}

function r3DisposeObj(obj) {
  obj.traverse(child => {
    child.geometry?.dispose();
    if (child.material) {
      (Array.isArray(child.material) ? child.material : [child.material]).forEach(m => m.dispose());
    }
    child.userData._r3dispose?.();
  });
}

// ── Main 3D canvas component ─────────────────────────────────────────────
function Rack3DCanvas({ rackData, cableLength = 0.0012, syncCfg, syncRackId }) {
  const mountRef = useRef3R(null);
  const r        = useRef3R({});

  useEffect3R(() => {
    const mount = mountRef.current;
    if (!mount || !rackData) return;
    const prev = r.current;
    if (prev.renderer) {
      prev.renderer.domElement.removeEventListener('pointermove', prev._onMove);
      prev.renderer.domElement.removeEventListener('click',       prev._onClick);
      window.removeEventListener('resize', prev._onResize);
      if (prev.rack) r3DisposeObj(prev.rack);
      prev.scene?.clear();
      prev.renderer.dispose();
      if (prev.renderer.domElement.parentNode === mount)
        mount.removeChild(prev.renderer.domElement);
      if (prev.tooltip?.parentNode) prev.tooltip.parentNode.removeChild(prev.tooltip);
      if (prev.raf) cancelAnimationFrame(prev.raf);
    }

    const W = mount.clientWidth  || 800;
    const H = mount.clientHeight || 600;

    const scene    = new THREE.Scene();
    scene.background = new THREE.Color(R3_COL.bg);
    scene.fog        = new THREE.FogExp2(R3_COL.bg, 0.018);

    const camera = new THREE.PerspectiveCamera(45, W / H, 0.05, 100);
    camera.position.set(1.2, 1.2, 12.0);
    camera.lookAt(0, 0.4, 0);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(W, H);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.outputColorSpace = THREE.SRGBColorSpace || renderer.outputColorSpace;
    renderer.toneMapping         = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.05;
    renderer.domElement.style.display = 'block';
    mount.appendChild(renderer.domElement);

    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping  = true;
    controls.target.set(0, 0.4, 0);
    controls.minDistance    = 3;
    controls.maxDistance    = 16;

    // ── Cinematic lighting for single-rack detail ───────────────────────
    scene.add(new THREE.AmbientLight(0x0c1e30, 0.55));
    const key = new THREE.DirectionalLight(0x90c0e8, 1.6);
    key.position.set(4, 7, 8);
    scene.add(key);
    const fill = new THREE.DirectionalLight(0x182838, 0.4);
    fill.position.set(-5, 2, -3);
    scene.add(fill);
    const cyan = new THREE.PointLight(R3_COL.cyan, 3.5, 8);
    cyan.position.set(-4.0, 0.8, 2.2);
    scene.add(cyan);
    const rimLight = new THREE.PointLight(0x3030cc, 1.2, 10);
    rimLight.position.set(5, 3, -3);
    scene.add(rimLight);

    // Floor grid
    const floor = new THREE.GridHelper(11, 18, 0x123347, 0x0a1b2b);
    floor.position.y = -R3_RACK.height / 2 - 0.25;
    scene.add(floor);

    const registry = { portMeshes: new Map(), ledMeshes: new Map(), deviceMeshes: new Map(), blinkingLeds: [], energizedEdges: [] };
    const rack = r3MakeRack(rackData, registry, cableLength);
    scene.add(rack);

    // Tooltip DOM
    const tooltip = document.createElement('div');
    Object.assign(tooltip.style, {
      position: 'absolute', pointerEvents: 'none',
      padding: '8px 10px', border: '1px solid rgba(0,229,195,0.45)',
      borderRadius: '8px', background: 'rgba(5,12,22,0.92)',
      color: '#dbeafe', font: '12px Consolas,monospace',
      boxShadow: '0 0 20px rgba(0,229,195,0.22)',
      display: 'none', zIndex: '10',
    });
    mount.style.position = mount.style.position || 'relative';
    mount.appendChild(tooltip);

    const raycaster = new THREE.Raycaster();
    const pointer   = new THREE.Vector2();
    let hovered     = null;

    function findPort(obj) {
      let cur = obj;
      while (cur) { if (cur.userData?.kind === 'port') return cur; cur = cur.parent; }
      return null;
    }

    const _onMove = (e) => {
      const rect = renderer.domElement.getBoundingClientRect();
      pointer.x =  ((e.clientX - rect.left) / rect.width)  * 2 - 1;
      pointer.y = -((e.clientY - rect.top)  / rect.height) * 2 + 1;
      raycaster.setFromCamera(pointer, camera);
      const hits = raycaster.intersectObjects(Array.from(registry.portMeshes.values()), true);
      const root = hits.length ? findPort(hits[0].object) : null;
      if (hovered !== root) {
        if (hovered) r3SetHighlight(hovered, false);
        hovered = root;
        if (hovered) r3SetHighlight(hovered, true);
      }
      if (hovered) {
        tooltip.innerHTML = r3FormatTooltip(hovered.userData);
        tooltip.style.display = 'block';
        tooltip.style.left = `${e.clientX - rect.left + 14}px`;
        tooltip.style.top  = `${e.clientY - rect.top  + 14}px`;
        renderer.domElement.style.cursor = 'pointer';
      } else {
        tooltip.style.display = 'none';
        renderer.domElement.style.cursor = 'default';
      }
    };

    const _onResize = () => {
      const w = mount.clientWidth, h = mount.clientHeight;
      if (!w || !h) return;
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      renderer.setSize(w, h);
    };

    renderer.domElement.addEventListener('pointermove', _onMove);
    window.addEventListener('resize', _onResize);

    let rafId;
    const animate = (time) => {
      rafId = requestAnimationFrame(animate);
      const t = time / 1000;
      registry.blinkingLeds.forEach(led => {
        const state = led.userData.ledState;
        if (state === 'green_blink' || state === 'amber_blink') {
          // Match CSS: port-blink 1.2s ease-in-out (opacity 1→0.25→1)
          const phase = (t / 1.2 + (led.userData.portNumber || 0) * 0.11) % 1.0;
          led.material.opacity = 0.625 + 0.375 * Math.cos(2 * Math.PI * phase);
        } else {
          led.material.opacity = 1.0;
        }
      });
      // Energized plug edge pulse — electricity-discharge pattern
      registry.energizedEdges.forEach(({ mat, pn }) => {
        const base = Math.abs(Math.sin(t * 3.2 + pn * 0.7));
        const spark = Math.pow(Math.abs(Math.sin(t * 11.0 + pn * 2.3)), 4);
        mat.opacity = 0.3 + 0.45 * base + 0.25 * spark;
      });
      controls.update();
      renderer.render(scene, camera);
    };
    rafId = requestAnimationFrame(animate);

    Object.assign(r.current, { renderer, scene, camera, controls, rack, registry, tooltip, raf: rafId, _onMove, _onResize });

    return () => {
      renderer.domElement.removeEventListener('pointermove', _onMove);
      window.removeEventListener('resize', _onResize);
      cancelAnimationFrame(rafId);
      r3DisposeObj(rack);
      scene.clear();
      renderer.dispose();
      if (renderer.domElement.parentNode === mount) mount.removeChild(renderer.domElement);
      if (tooltip.parentNode) tooltip.parentNode.removeChild(tooltip);
    };
  }, [rackData, cableLength]);

  // Live LED sync for the single-rack detail view.
  useEffect3R(() => {
    if (!syncCfg || !syncRackId) return;
    const { registry } = r.current;
    if (!registry?.ledMeshes) return;
    for (const g of syncCfg.groups) {
      const rack = g.racks.find(rk => rk.id === syncRackId);
      if (!rack) continue;
      r3SyncLeds(registry.ledMeshes, rack.slots, rack.id);
      break;
    }
  }, [syncCfg, syncRackId]);

  return <div ref={mountRef} style={{ position: 'absolute', inset: 0 }} />;
}

// ── Sidebar ──────────────────────────────────────────────────────────────
function Rack3DSidebar({ cfg, selectedRackId, onSelect }) {
  const [query, setQuery] = useState3R('');
  const q = query.trim().toLowerCase();

  const allRacks = useMemo3R(() => {
    const out = [];
    for (const g of cfg.groups) {
      for (const rack of g.racks) {
        const rh = (window.rackHealth || (() => ({ level: 'empty', total: 0 })))(rack);
        out.push({ id: rack.id, label: aliasOr(rack, rack.label), groupTag: aliasOr(g, g.tag),
                   groupId: g.id, level: rh.level || 'empty', devices: rh.total || 0,
                   rack, group: g });
      }
    }
    return out;
  }, [cfg]);

  const filtered = q
    ? allRacks.filter(r => r.label.toLowerCase().includes(q) || r.groupTag?.toLowerCase?.().includes(q))
    : allRacks;

  const statusColor = { ok: '#00d9a3', warn: '#f5b454', bad: '#ff5470', off: '#3a4658', empty: '#1e2e40' };

  return (
    <aside style={{
      position: 'absolute', top: 14, left: 14, bottom: 14, width: 240,
      background: 'rgba(5,10,18,0.92)',
      backdropFilter: 'blur(18px)', WebkitBackdropFilter: 'blur(18px)',
      border: '1px solid rgba(0,240,255,0.22)',
      borderRadius: 10, display: 'flex', flexDirection: 'column',
      overflow: 'hidden', zIndex: 20,
    }}>
      <div style={{ padding: '12px 14px 10px', borderBottom: '1px solid rgba(0,240,255,0.10)' }}>
        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600,
                      color: '#00f0ff', letterSpacing: '0.06em' }}>RACK SELECT</div>
        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: '#4a6070',
                      marginTop: 3, letterSpacing: '0.04em' }}>
          {allRacks.length} racks · {cfg.groups.length} groups
        </div>
      </div>
      <div style={{ padding: '8px 10px 6px' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, height: 28,
                      padding: '0 8px', borderRadius: 5,
                      background: 'rgba(12,18,30,0.7)',
                      border: '1px solid rgba(79,209,255,0.12)' }}>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#3a5060" strokeWidth="2">
            <circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/>
          </svg>
          <input value={query} onChange={e => setQuery(e.target.value)}
            placeholder="Search..."
            style={{ flex: 1, background: 'transparent', border: 0, outline: 0,
                     fontSize: 11, color: '#c6d0e2', fontFamily: 'var(--font-mono)' }} />
        </div>
      </div>
      <div style={{ flex: 1, overflowY: 'auto', padding: '4px 8px 12px' }}>
        {filtered.map(r => (
          <div key={r.id}
            onClick={() => onSelect(r.id, r.rack, r.group)}
            style={{
              display: 'flex', alignItems: 'center', gap: 8, height: 30,
              padding: '0 8px', marginBottom: 2, borderRadius: 5, cursor: 'pointer',
              border: `1px solid ${selectedRackId === r.id ? 'rgba(79,209,255,0.45)' : 'transparent'}`,
              background: selectedRackId === r.id ? 'rgba(79,209,255,0.06)' : 'transparent',
              transition: 'background .12s, border-color .12s',
            }}>
            <span style={{ width: 7, height: 7, borderRadius: '50%', flexShrink: 0,
                           background: statusColor[r.level] || statusColor.empty,
                           boxShadow: r.level === 'ok'   ? '0 0 5px rgba(0,217,163,0.5)'
                                    : r.level === 'warn' ? '0 0 5px rgba(245,180,84,0.5)'
                                    : r.level === 'bad'  ? '0 0 5px rgba(255,84,112,0.5)' : 'none' }} />
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12,
                           color: selectedRackId === r.id ? '#00f0ff' : '#c6d0e2',
                           flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
              {r.label}
            </span>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: '#3a5060', flexShrink: 0 }}>
              {r.devices}
            </span>
          </div>
        ))}
      </div>
    </aside>
  );
}

// ── Full floor plan canvas — detailed r3MakeRack, same layout as 3D tab ──
// Uses the identical coordinate system and rack-size formulas as
// Front-facing "rack wall" view — one row per group, racks shown from the front
// exactly like RACK DETAIL (Rack3DCanvas) but with multiple racks arranged in a grid.
// Uniform scale so proportions match RACK DETAIL; only overall size differs.
function Rack3DFloorCanvas({ cfg, selectedRackId, onSelect, cableLength = 0.0012 }) {
  const mountRef = useRef3R(null);
  const stateRef = useRef3R({});
  const onSelRef = useRef3R(onSelect);
  useEffect3R(() => { onSelRef.current = onSelect; }, [onSelect]);
  const selIdRef = useRef3R(selectedRackId);
  useEffect3R(() => { selIdRef.current = selectedRackId; }, [selectedRackId]);
  const [inFrontView, setInFrontView] = useState3R(false);

  // Rebuild only when rack/group count or cable length changes.
  // Power-state ticks from simulateEventTick do NOT change this key.
  const layoutKey = useMemo3R(() =>
    cfg.groups.map(g => `${g.id}:${g.racks.length}`).join('|')
    + `:${cfg.params?.M}:cl${cableLength}`,
  [cfg, cableLength]);

  useEffect3R(() => {
    const mount = mountRef.current;
    if (!mount) return;

    const prev = stateRef.current;
    if (prev.renderer) {
      window.removeEventListener('resize', prev._onResize);
      prev.renderer.domElement.removeEventListener('click', prev._onClick);
      cancelAnimationFrame(prev.raf);
      prev.controls?.dispose();
      prev.scene?.clear();
      prev.renderer.dispose();
      if (prev.renderer.domElement.parentNode === mount)
        mount.removeChild(prev.renderer.domElement);
    }

    const W = mount.clientWidth  || 900;
    const H = mount.clientHeight || 400;
    const M = cfg.params?.M || 24;

    const SCALE   = 0.20;   // XY scale — racks ~54% bigger than detail view for floor readability
    const SCALE_Z = 0.870;  // Z-only — rack depth ≈ 1.00 wu (1.0 / R3_RACK.depth 1.15)
    const rackW = R3_RACK.width  * SCALE;   // ≈ 1.56
    const rackH = R3_RACK.height * SCALE;   // ≈ 1.76
    const gapX  = 0.12;

    // Floor world dimensions — enlarged to fit bigger racks
    const GRID_W  = 24;
    const GRID_H  = 14;
    const centerX = GRID_W / 2;
    const centerZ = GRID_H / 2;

    const groups = cfg.groups;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(R3_COL.bg);
    scene.fog = new THREE.FogExp2(R3_COL.bg, 0.012);

    // Elevated camera looking down at the floor — matches 3D tab perspective
    const camera = new THREE.PerspectiveCamera(55, W / H, 0.02, 120);
    camera.position.set(GRID_W * 0.55, Math.max(GRID_W, GRID_H) * 0.65, GRID_H * 1.4);
    camera.lookAt(centerX, 0, centerZ);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(W, H);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.outputColorSpace = THREE.SRGBColorSpace || renderer.outputColorSpace;
    renderer.toneMapping         = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.05;
    renderer.domElement.style.display = 'block';
    mount.appendChild(renderer.domElement);

    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.08;
    controls.target.set(centerX, 0, centerZ);
    controls.minDistance = 0.3;
    controls.maxDistance = 60;

    // ── Lighting — cinematic multi-source ──────────────────────────────
    scene.add(new THREE.AmbientLight(0x10202e, 0.6));
    const key = new THREE.DirectionalLight(0x8ab8e8, 1.5);
    key.position.set(centerX + 6, 18, -3);
    scene.add(key);
    const fill = new THREE.DirectionalLight(0x182838, 0.4);
    fill.position.set(centerX - 8, 6, GRID_H + 5);
    scene.add(fill);
    const cyanLight = new THREE.PointLight(R3_COL.cyan, 4.0, 20);
    cyanLight.position.set(1.5, 7, 2);
    scene.add(cyanLight);
    const rimLight = new THREE.PointLight(0x3030cc, 1.4, 22);
    rimLight.position.set(centerX * 2, 9, GRID_H + 5);
    scene.add(rimLight);
    const warmKey = new THREE.PointLight(0xff9040, 0.7, 15);
    warmKey.position.set(centerX + GRID_W * 0.4, 11, GRID_H - 1);
    scene.add(warmKey);

    // ── Floor — dark metallic + emissive grid lines ─────────────────────
    const floorGeo = new THREE.PlaneGeometry(GRID_W + 10, GRID_H + 10);
    floorGeo.rotateX(-Math.PI / 2);
    const floorMesh = new THREE.Mesh(floorGeo, new THREE.MeshStandardMaterial({
      color: 0x010810, roughness: 0.22, metalness: 0.80,
    }));
    floorMesh.position.set(centerX, -0.002, centerZ);
    scene.add(floorMesh);

    // Custom grid lines — emissive teal with accent lines every 4th
    const gSize = Math.max(GRID_W, GRID_H) + 10, gDiv = 26, gStep = gSize / gDiv;
    const gVerts = [], gAccent = [];
    for (let i = 0; i <= gDiv; i++) {
      const p = -gSize / 2 + i * gStep;
      gVerts.push(p, 0, -gSize / 2, p, 0, gSize / 2, -gSize / 2, 0, p, gSize / 2, 0, p);
      if (i % 4 === 0) gAccent.push(p, 0, -gSize / 2, p, 0, gSize / 2, -gSize / 2, 0, p, gSize / 2, 0, p);
    }
    const gGeo = new THREE.BufferGeometry();
    gGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(gVerts), 3));
    const gMat = new THREE.LineBasicMaterial({ color: 0x081e38, fog: false, transparent: true, opacity: 0.5 });
    const gridLines = new THREE.LineSegments(gGeo, gMat);
    gridLines.position.set(centerX, 0.003, centerZ);
    scene.add(gridLines);

    const gAGeo = new THREE.BufferGeometry();
    gAGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(gAccent), 3));
    const gAMat = new THREE.LineBasicMaterial({ color: 0x0c3060, fog: false, transparent: true, opacity: 0.75 });
    const accentLines = new THREE.LineSegments(gAGeo, gAMat);
    accentLines.position.set(centerX, 0.004, centerZ);
    scene.add(accentLines);

    // ── Atmospheric particles ──────────────────────────────────────────
    const PART_COUNT = 300;
    const partPositions  = new Float32Array(PART_COUNT * 3);
    const partVelocities = new Float32Array(PART_COUNT);
    for (let i = 0; i < PART_COUNT; i++) {
      partPositions[i*3]     = (Math.random() - 0.5) * (GRID_W + 4) + centerX;
      partPositions[i*3 + 1] = Math.random() * 9;
      partPositions[i*3 + 2] = (Math.random() - 0.5) * (GRID_H + 4) + centerZ;
      partVelocities[i]      = 0.012 + Math.random() * 0.022;
    }
    const partGeo = new THREE.BufferGeometry();
    partGeo.setAttribute('position', new THREE.BufferAttribute(partPositions, 3));
    const partMat = new THREE.PointsMaterial({ color: 0x246090, size: 0.045, transparent: true, opacity: 0.48, sizeAttenuation: true, fog: true });
    const particles = new THREE.Points(partGeo, partMat);
    scene.add(particles);

    // ── Scan beam — gradient plane sweeping the floor ─────────────────
    const sbCvs = document.createElement('canvas');
    sbCvs.width = 4; sbCvs.height = 64;
    const sbCtx = sbCvs.getContext('2d');
    const sbGrad = sbCtx.createLinearGradient(0, 0, 0, 64);
    sbGrad.addColorStop(0,    'rgba(0,238,255,0)');
    sbGrad.addColorStop(0.35, 'rgba(0,238,255,0.055)');
    sbGrad.addColorStop(0.5,  'rgba(0,238,255,0.18)');
    sbGrad.addColorStop(0.65, 'rgba(0,238,255,0.055)');
    sbGrad.addColorStop(1,    'rgba(0,238,255,0)');
    sbCtx.fillStyle = sbGrad;
    sbCtx.fillRect(0, 0, 4, 64);
    const sbTex = new THREE.CanvasTexture(sbCvs);
    sbTex.colorSpace = THREE.SRGBColorSpace || sbTex.colorSpace;
    const sbMat = new THREE.MeshBasicMaterial({ map: sbTex, transparent: true, depthWrite: false, side: THREE.DoubleSide, fog: false });
    sbMat.toneMapped = false;
    const scanBeam = new THREE.Mesh(new THREE.PlaneGeometry(GRID_W + 8, 4.0), sbMat);
    scanBeam.rotation.x = -Math.PI / 2;
    scanBeam.position.y = 0.012;
    scene.add(scanBeam);

    const rackMeshMap       = new Map();
    const allBlinkLeds      = [];
    const allLedMeshes      = new Map(); // key: "switchId:portNumber" — for live LED sync
    const allEnergizedEdges = [];        // pulsing plug edge materials

    for (let gi = 0; gi < groups.length; gi++) {
      const g = groups[gi];
      const N = g.racks.length;
      // g.x → world X offset, g.y → world Z offset (matching 3D tab grid convention)
      const groupX = (g.x !== undefined ? g.x : gi * 5) + rackW / 2;
      const groupZ =  g.y !== undefined ? g.y : gi * 4;

      for (let i = 0; i < N; i++) {
        const rack = g.racks[i];
        if (rack.absent) continue;

        const rackData = storeRackToR3(rack, g, M);
        const registry = {
          deviceMeshes: new Map(), portMeshes: new Map(),
          ledMeshes: new Map(), blinkingLeds: [], energizedEdges: [],
        };
        const rackGrp = r3MakeRack(rackData, registry, cableLength);
        rackGrp.scale.set(SCALE, SCALE, SCALE_Z);
        // Rack origin is at its vertical center — lift by rackH/2 so bottom sits on floor (y=0)
        rackGrp.position.set(groupX + i * (rackW + gapX), rackH / 2, groupZ);

        rackGrp.traverse(child => { child.userData.rackId = rack.id; });
        rackGrp.traverse(child => {
          if (child.isMesh && child.material?.emissive)
            child.userData._r3baseEI = child.material.emissiveIntensity;
        });

        scene.add(rackGrp);
        rackMeshMap.set(rack.id, { grp: rackGrp, rack, group: g });
        allBlinkLeds.push(...registry.blinkingLeds);
        allEnergizedEdges.push(...registry.energizedEdges);
        registry.ledMeshes.forEach((m, k) => allLedMeshes.set(k, m));
      }
    }

    // ── Robots — floor-positioned, animated via robotMotion() ────────────
    const robotVisuals = [];
    if (typeof robotMotion === 'function') {
      for (const robot of (cfg.robots || [])) {
        if (!robot.path || robot.path.length === 0) continue;

        const rgrp = new THREE.Group();

        // Chassis base
        const chassis = new THREE.Mesh(
          new THREE.BoxGeometry(0.48, 0.06, 0.34),
          new THREE.MeshStandardMaterial({ color: 0x08101e, roughness: 0.7, metalness: 0.5 })
        );
        chassis.position.set(0, 0.03, 0);
        rgrp.add(chassis);

        // Body with cyan emissive
        const bodyMat = new THREE.MeshStandardMaterial({
          color: 0x0d1826, roughness: 0.55, metalness: 0.45,
          emissive: new THREE.Color(R3_COL.cyan), emissiveIntensity: 0.12,
        });
        const bodyMesh = new THREE.Mesh(new THREE.BoxGeometry(0.40, 0.16, 0.30), bodyMat);
        bodyMesh.position.set(0, 0.12, 0);
        rgrp.add(bodyMesh);

        // Accent stripe
        const stripe = new THREE.Mesh(
          new THREE.BoxGeometry(0.41, 0.014, 0.31),
          new THREE.MeshStandardMaterial({ color: R3_COL.cyan, emissive: new THREE.Color(R3_COL.cyan), emissiveIntensity: 0.8 })
        );
        stripe.position.set(0, 0.206, 0);
        rgrp.add(stripe);

        // Sensor dome on top
        const lensMat = new THREE.MeshStandardMaterial({
          color: 0x040b16, roughness: 0.3, metalness: 0.8,
          emissive: new THREE.Color(R3_COL.cyan), emissiveIntensity: 0.85,
        });
        const lensMesh = new THREE.Mesh(new THREE.CylinderGeometry(0.055, 0.075, 0.08, 12), lensMat);
        lensMesh.position.set(0, 0.25, 0);
        rgrp.add(lensMesh);

        // Forward pointer — robot faces local +X, so pointer is at +X side
        const ptr = new THREE.Mesh(
          new THREE.BoxGeometry(0.12, 0.06, 0.06),
          new THREE.MeshStandardMaterial({ color: 0x000000, emissive: new THREE.Color(R3_COL.cyan), emissiveIntensity: 0.9 })
        );
        ptr.position.set(0.26, 0.12, 0);
        rgrp.add(ptr);

        // Scan cone: ConeGeometry tip→+X after rotation.z=-π/2; base at x≈0, tip extends to x=2.0
        const coneMesh = new THREE.Mesh(
          new THREE.ConeGeometry(0.9, 2.0, 16),
          new THREE.MeshBasicMaterial({ color: 0xf5b454, transparent: true, opacity: 0.20, side: THREE.DoubleSide, fog: false })
        );
        coneMesh.rotation.z = -Math.PI / 2;
        coneMesh.position.set(1.0, 0.12, 0);
        coneMesh.visible = false;
        rgrp.add(coneMesh);

        // Initial position
        const m0 = robotMotion(robot.path, robot.speed, 0);
        rgrp.position.set(m0.x, 0, m0.y);
        rgrp.rotation.y = -m0.angle;
        scene.add(rgrp);

        // Trail line — grows during movement up to MAX_TRAIL samples
        const MAX_TRAIL = 40;
        const trailBuf = new Float32Array(MAX_TRAIL * 3);
        const trailGeo = new THREE.BufferGeometry();
        trailGeo.setAttribute('position', new THREE.BufferAttribute(trailBuf, 3));
        trailGeo.setDrawRange(0, 0);
        const trailLine = new THREE.Line(trailGeo,
          new THREE.LineBasicMaterial({ color: R3_COL.cyan, transparent: true, opacity: 0.35, fog: false })
        );
        trailLine.frustumCulled = false;
        scene.add(trailLine);

        robotVisuals.push({ grp: rgrp, robot, bodyMat, lensMat, coneMesh, trailLine, trailGeo, trailBuf, trailHistory: [], trailTick: 0 });
      }
    }

    // Apply selection highlight for whichever rack is currently selected
    rackMeshMap.forEach((v, id) => {
      if (id !== selectedRackId) return;
      v.grp.traverse(child => {
        if (!child.isMesh || !child.material?.emissive) return;
        if (child.userData._r3noHighlight) return;
        const base = child.userData._r3baseEI ?? 0;
        child.material.emissiveIntensity = Math.max(base + 1.2, 1.5);
      });
    });

    // ── Hover wireframe box ─────────────────────────────────────────────
    const hoverBox = new THREE.LineSegments(
      new THREE.EdgesGeometry(new THREE.BoxGeometry(rackW + 0.06, rackH + 0.06, R3_RACK.depth * SCALE_Z + 0.06)),
      new THREE.LineBasicMaterial({ color: R3_COL.cyan, fog: false, transparent: true, opacity: 0.85 })
    );
    hoverBox.visible = false;
    scene.add(hoverBox);

    // ── Camera fly-to animation state ────────────────────────────────────
    const animRef = {
      active: false, t: 0,
      camFrom: new THREE.Vector3(), camTo: new THREE.Vector3(),
      lookFrom: new THREE.Vector3(), lookTo: new THREE.Vector3(),
    };

    // ── Raycaster shared by hover + click ────────────────────────────────
    const raycaster = new THREE.Raycaster();
    const ptr = new THREE.Vector2();

    function getRackMeshes() {
      const out = [];
      rackMeshMap.forEach(v => v.grp.traverse(c => { if (c.isMesh) out.push(c); }));
      return out;
    }

    // Hover — highlight rack under cursor with wireframe box
    let hoveredId = null;
    const _onMouseMove = (e) => {
      const rect = renderer.domElement.getBoundingClientRect();
      ptr.x =  ((e.clientX - rect.left) / rect.width)  * 2 - 1;
      ptr.y = -((e.clientY - rect.top)  / rect.height) * 2 + 1;
      raycaster.setFromCamera(ptr, camera);
      const hits = raycaster.intersectObjects(getRackMeshes(), false);
      const newId = hits.length ? hits[0].object.userData.rackId : null;
      if (newId === hoveredId) return;
      hoveredId = newId;
      renderer.domElement.style.cursor = newId ? 'pointer' : 'default';
      if (newId) {
        const v = rackMeshMap.get(newId);
        if (v) hoverBox.position.copy(v.grp.position);
      }
      hoverBox.visible = !!newId;
    };
    renderer.domElement.addEventListener('mousemove', _onMouseMove);

    // Click — select rack + fly camera to its front face
    const _onClick = (e) => {
      const rect = renderer.domElement.getBoundingClientRect();
      ptr.x =  ((e.clientX - rect.left) / rect.width)  * 2 - 1;
      ptr.y = -((e.clientY - rect.top)  / rect.height) * 2 + 1;
      raycaster.setFromCamera(ptr, camera);
      const hits = raycaster.intersectObjects(getRackMeshes(), false);
      if (!hits.length) return;
      const id = hits[0].object.userData.rackId;
      if (!id) return;
      const v = rackMeshMap.get(id);
      if (!v) return;
      onSelRef.current(id, v.rack, v.group);
      setInFrontView(true);

      // Fly to front face: rack faces +Z, so front is at rackGrp.z + depth/2
      const rp = v.grp.position;
      const frontZ = rp.z + R3_RACK.depth / 2 * SCALE_Z;
      const viewDist = Math.max(rackW, rackH) * 1.8;
      animRef.camFrom.copy(camera.position);
      animRef.camTo.set(rp.x, rp.y, frontZ + viewDist);
      animRef.lookFrom.copy(controls.target);
      animRef.lookTo.copy(rp);
      animRef.t = 0;
      animRef.active = true;
    };
    renderer.domElement.addEventListener('click', _onClick);

    const _onResize = () => {
      const w = mount.clientWidth, h = mount.clientHeight;
      if (!w || !h) return;
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      renderer.setSize(w, h);
    };
    window.addEventListener('resize', _onResize);

    let rafId;
    const animate = (time) => {
      rafId = requestAnimationFrame(animate);
      const t = time / 1000;
      allBlinkLeds.forEach(led => {
        const s = led.userData.ledState;
        if (s === 'green_blink' || s === 'amber_blink') {
          // Match CSS: port-blink 1.2s ease-in-out (opacity 1→0.25→1)
          const phase = (t / 1.2 + (led.userData.portNumber || 0) * 0.11) % 1.0;
          led.material.opacity = 0.625 + 0.375 * Math.cos(2 * Math.PI * phase);
        } else {
          led.material.opacity = 1.0;
        }
      });
      // Energized plug edge pulse — electricity-discharge pattern
      allEnergizedEdges.forEach(({ mat, pn }) => {
        const base  = Math.abs(Math.sin(t * 3.2 + pn * 0.7));
        const spark = Math.pow(Math.abs(Math.sin(t * 11.0 + pn * 2.3)), 4);
        mat.opacity = 0.3 + 0.45 * base + 0.25 * spark;
      });
      // Robot motion + trail update
      robotVisuals.forEach(rv => {
        const m = robotMotion(rv.robot.path, rv.robot.speed, t);
        rv.grp.position.set(m.x, 0, m.y);
        rv.grp.rotation.y = -m.angle;
        const scanning = m.state === 'inspecting';
        rv.coneMesh.visible = scanning;
        rv.lensMat.emissiveIntensity = scanning ? 1.4 : 0.8 + Math.sin(t * 3.5) * 0.1;
        rv.bodyMat.emissiveIntensity = scanning ? 0.55 : 0.12;
        // Sample trail every 3 frames while moving
        rv.trailTick++;
        if (!scanning && rv.trailTick % 3 === 0) {
          rv.trailHistory.push([m.x, m.y]);
          if (rv.trailHistory.length > 40) rv.trailHistory.shift();
          const n = rv.trailHistory.length;
          for (let i = 0; i < n; i++) {
            rv.trailBuf[i * 3]     = rv.trailHistory[i][0];
            rv.trailBuf[i * 3 + 1] = 0.02;
            rv.trailBuf[i * 3 + 2] = rv.trailHistory[i][1];
          }
          rv.trailGeo.setDrawRange(0, n);
          rv.trailGeo.attributes.position.needsUpdate = true;
        }
      });
      // ── Scan beam sweep — 9-second cycle ─────────────────────────────
      const beamT = (t % 9.0) / 9.0;
      scanBeam.position.set(centerX, 0.012, beamT * (GRID_H + 10) + (centerZ - GRID_H / 2 - 5));

      // ── Atmospheric particle drift ──────────────────────────────────
      const pPos = partGeo.attributes.position.array;
      for (let i = 0; i < PART_COUNT; i++) {
        pPos[i*3 + 1] += partVelocities[i] * 0.009;
        if (pPos[i*3 + 1] > 9.8) {
          pPos[i*3 + 1] = 0.05;
          pPos[i*3]     = (Math.random() - 0.5) * (GRID_W + 4) + centerX;
          pPos[i*3 + 2] = (Math.random() - 0.5) * (GRID_H + 4) + centerZ;
        }
      }
      partGeo.attributes.position.needsUpdate = true;

      // Camera fly-to animation
      if (animRef.active) {
        controls.enabled = false;
        animRef.t = Math.min(1, animRef.t + 0.028);          // ~36 frames ≈ 0.6 s
        const ease = 1 - Math.pow(1 - animRef.t, 3);         // cubic ease-out
        camera.position.lerpVectors(animRef.camFrom, animRef.camTo, ease);
        controls.target.lerpVectors(animRef.lookFrom, animRef.lookTo, ease);
        if (animRef.t >= 1) { animRef.active = false; controls.enabled = true; }
      }
      controls.update();
      renderer.render(scene, camera);
    };
    rafId = requestAnimationFrame(animate);

    const initCamPos = camera.position.clone();
    const initTarget = controls.target.clone();
    stateRef.current = { renderer, scene, camera, controls, rackMeshMap, allLedMeshes, raf: rafId, _onClick, _onResize, animRef, initCamPos, initTarget };

    return () => {
      window.removeEventListener('resize', _onResize);
      renderer.domElement.removeEventListener('click', _onClick);
      renderer.domElement.removeEventListener('mousemove', _onMouseMove);
      cancelAnimationFrame(rafId);
      controls.dispose();
      scene.clear();
      renderer.dispose();
      if (renderer.domElement.parentNode === mount) mount.removeChild(renderer.domElement);
    };
  }, [layoutKey]);

  // Selection highlight without scene rebuild
  useEffect3R(() => {
    const { rackMeshMap } = stateRef.current;
    if (!rackMeshMap) return;
    rackMeshMap.forEach((v, id) => {
      const sel = id === selectedRackId;
      v.grp.traverse(child => {
        if (!child.isMesh || !child.material?.emissive) return;
        if (child.userData._r3noHighlight) return;
        const base = child.userData._r3baseEI ?? 0;
        child.material.emissiveIntensity = sel ? Math.max(base + 1.2, 1.5) : base;
      });
    });
  }, [selectedRackId]);

  // Live LED sync: update LED states when cfg changes without rebuilding the scene.
  useEffect3R(() => {
    const { allLedMeshes } = stateRef.current;
    if (!allLedMeshes || allLedMeshes.size === 0) return;
    for (const g of cfg.groups) {
      for (const rack of g.racks) {
        if (rack.absent) continue;
        r3SyncLeds(allLedMeshes, rack.slots, rack.id);
      }
    }
  }, [cfg]);

  const handleReset = React.useCallback(() => {
    const { animRef: ar, camera, controls, initCamPos, initTarget } = stateRef.current;
    if (!ar || !camera || !initCamPos) return;
    ar.camFrom.copy(camera.position);
    ar.camTo.copy(initCamPos);
    ar.lookFrom.copy(controls.target);
    ar.lookTo.copy(initTarget);
    ar.t = 0;
    ar.active = true;
    setInFrontView(false);
  }, []);

  return (
    <div style={{ width: '100%', height: '100%', position: 'relative' }}>
      <div ref={mountRef} style={{ width: '100%', height: '100%' }} />
      {inFrontView && (
        <button
          onClick={handleReset}
          style={{
            position: 'absolute', top: 12, left: 12,
            background: 'rgba(4,11,22,0.88)',
            border: '1px solid rgba(0,240,255,0.35)',
            color: '#00f0ff',
            fontFamily: 'var(--font-mono, monospace)',
            fontSize: 11, letterSpacing: '0.12em',
            padding: '5px 12px', cursor: 'pointer',
            backdropFilter: 'blur(4px)',
          }}
        >
          ↩ OVERVIEW
        </button>
      )}
    </div>
  );
}

// ── Premium rack-list sidebar ────────────────────────────────────────────
function Rack3DRackList({ cfg, selectedRackId, onSelect }) {
  const [query, setQuery] = useState3R('');
  const q = query.trim().toLowerCase();

  const { groups, totalRacks, totalOnline, totalWarn } = useMemo3R(() => {
    let totalRacks = 0, totalOnline = 0, totalWarn = 0;
    const groups = cfg.groups.map(g => {
      const racks = g.racks.map(rack => {
        const rh = (window.rackHealth || (() => ({ level: 'empty', total: 0 })))(rack);
        const level = rh.level || 'empty';
        totalRacks++;
        if (level === 'ok') totalOnline++;
        else if (level === 'warn') totalWarn++;
        return { id: rack.id, label: aliasOr(rack, rack.label),
                 groupTag: aliasOr(g, g.tag), groupId: g.id,
                 level, devices: rh.total || 0, rack, group: g };
      });
      return { id: g.id, tag: aliasOr(g, g.tag) || g.id, racks };
    });
    return { groups, totalRacks, totalOnline, totalWarn };
  }, [cfg]);

  const filteredGroups = useMemo3R(() => {
    if (!q) return groups;
    return groups.map(g => ({
      ...g,
      racks: g.racks.filter(r =>
        r.label.toLowerCase().includes(q) || r.groupTag?.toLowerCase().includes(q))
    })).filter(g => g.racks.length > 0);
  }, [groups, q]);

  const sCol = { ok: '#00e599', warn: '#f5b942', bad: '#ff4f6e', off: '#2a3d52', empty: '#162030' };
  const sGlow = { ok: '0 0 7px rgba(0,229,153,0.55)', warn: '0 0 7px rgba(245,185,66,0.55)', bad: '0 0 7px rgba(255,79,110,0.55)' };

  return (
    <aside style={{
      width: 252, flexShrink: 0, display: 'flex', flexDirection: 'column',
      background: 'rgba(1,6,15,0.98)',
      borderRight: '1px solid rgba(0,238,255,0.10)',
      overflow: 'hidden', position: 'relative',
    }}>
      {/* Animated scan line at top */}
      <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 1.5, overflow: 'hidden', zIndex: 10 }}>
        <div style={{
          position: 'absolute', top: 0, left: 0, right: 0, height: '100%',
          background: 'linear-gradient(90deg, transparent 0%, #00eeff 50%, transparent 100%)',
          animation: 'r3scanBar 3.5s linear infinite',
          opacity: 0.8,
        }} />
      </div>

      {/* Header */}
      <div style={{ padding: '16px 14px 12px', borderBottom: '1px solid rgba(0,238,255,0.07)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
          <div style={{ width: 2, height: 18, background: '#00eeff', boxShadow: '0 0 10px rgba(0,238,255,0.9)', flexShrink: 0 }} />
          <span style={{
            fontFamily: 'JetBrains Mono, var(--font-mono), monospace',
            fontSize: 11, fontWeight: 700, color: '#00eeff', letterSpacing: '0.14em',
          }}>RACK NAVIGATOR</span>
          <div style={{ flex: 1 }} />
          <div style={{
            width: 7, height: 7, borderRadius: '50%',
            background: '#00e599', boxShadow: '0 0 10px rgba(0,229,153,0.9)',
            animation: 'r3pulse 2.2s ease-in-out infinite',
          }} />
        </div>

        {/* Stats */}
        <div style={{ display: 'flex', gap: 5 }}>
          {[
            { v: totalRacks,  k: 'TOTAL',  c: '#3a6888' },
            { v: totalOnline, k: 'ONLINE', c: '#00e599' },
            { v: totalWarn,   k: 'WARN',   c: '#f5b942' },
          ].map(s => (
            <div key={s.k} style={{
              flex: 1, padding: '6px 4px', borderRadius: 5, textAlign: 'center',
              background: 'rgba(255,255,255,0.025)',
              border: '1px solid rgba(255,255,255,0.05)',
            }}>
              <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 16, fontWeight: 800, color: s.c, lineHeight: 1 }}>{s.v}</div>
              <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 8, color: '#1e3a52', letterSpacing: '0.1em', marginTop: 3 }}>{s.k}</div>
            </div>
          ))}
        </div>
      </div>

      {/* Search */}
      <div style={{ padding: '8px 10px 5px' }}>
        <div style={{
          display: 'flex', alignItems: 'center', gap: 6, height: 30,
          padding: '0 9px', borderRadius: 6,
          background: 'rgba(4,12,26,0.95)',
          border: '1px solid rgba(0,238,255,0.14)',
        }}>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#1a608a" strokeWidth="2.5">
            <circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/>
          </svg>
          <input value={query} onChange={e => setQuery(e.target.value)}
            placeholder="Filter racks..."
            style={{ flex: 1, background: 'transparent', border: 0, outline: 0,
                     fontSize: 11, color: '#9cc0da', fontFamily: 'JetBrains Mono, var(--font-mono), monospace' }} />
        </div>
      </div>

      {/* Rack list grouped */}
      <div style={{ flex: 1, overflowY: 'auto', padding: '2px 7px 12px' }}>
        {filteredGroups.map(g => (
          <div key={g.id}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 7, padding: '7px 5px 4px', marginTop: 3 }}>
              <div style={{ flex: 1, height: 1, background: 'linear-gradient(90deg, rgba(0,238,255,0.15), transparent)' }} />
              <span style={{
                fontFamily: 'JetBrains Mono, monospace', fontSize: 9,
                color: '#1e5070', letterSpacing: '0.18em',
              }}>{g.tag}</span>
              <div style={{ flex: 1, height: 1, background: 'linear-gradient(270deg, rgba(0,238,255,0.15), transparent)' }} />
            </div>

            {g.racks.map(r => {
              const sel = selectedRackId === r.id;
              const pct = r.devices > 0 ? Math.min(100, (r.devices / 12) * 100) : 0;
              return (
                <div key={r.id} className="r3-rack-row"
                  onClick={() => onSelect(r.id, r.rack, r.group)}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 8,
                    height: 36, padding: '0 9px', marginBottom: 2,
                    borderRadius: 6, cursor: 'pointer', position: 'relative', overflow: 'hidden',
                    border: `1px solid ${sel ? 'rgba(0,238,255,0.48)' : 'rgba(0,238,255,0.0)'}`,
                    background: sel ? 'rgba(0,238,255,0.07)' : 'rgba(255,255,255,0.018)',
                    transition: 'background .12s, border-color .12s',
                  }}>
                  {sel && <div style={{
                    position: 'absolute', left: 0, top: 0, bottom: 0, width: 2,
                    background: '#00eeff', boxShadow: '0 0 8px rgba(0,238,255,0.9)',
                  }} />}
                  <span style={{
                    width: 7, height: 7, borderRadius: '50%', flexShrink: 0,
                    background: sCol[r.level] || sCol.empty,
                    boxShadow: sGlow[r.level] || 'none',
                  }} />
                  <div style={{ flex: 1, overflow: 'hidden', minWidth: 0 }}>
                    <div style={{
                      fontFamily: 'JetBrains Mono, var(--font-mono), monospace', fontSize: 11,
                      color: sel ? '#00eeff' : '#9abcd8',
                      overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
                    }}>{r.label}</div>
                    <div style={{ height: 2, background: 'rgba(255,255,255,0.05)', borderRadius: 1, marginTop: 3 }}>
                      <div style={{
                        height: '100%', width: `${pct}%`, borderRadius: 1,
                        background: sCol[r.level] || '#0e2438',
                        boxShadow: r.level === 'ok' ? '0 0 4px rgba(0,229,153,0.45)' : 'none',
                        transition: 'width .4s ease',
                      }} />
                    </div>
                  </div>
                  <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: '#1e3a52', flexShrink: 0 }}>
                    {r.devices}
                  </span>
                </div>
              );
            })}
          </div>
        ))}
      </div>

      {/* Footer */}
      <div style={{
        padding: '8px 14px', borderTop: '1px solid rgba(0,238,255,0.07)',
        display: 'flex', alignItems: 'center', gap: 8,
      }}>
        <div style={{ width: 4, height: 4, borderRadius: '50%', background: '#00eeff', opacity: 0.4, animation: 'r3blink 3s ease-in-out infinite' }} />
        <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 9, color: '#0e2840', letterSpacing: '0.12em' }}>
          NOC·KR·SV
        </span>
        <div style={{ flex: 1, height: 1, background: 'linear-gradient(90deg, rgba(0,238,255,0.12), transparent)' }} />
      </div>
    </aside>
  );
}

// ── HUD corner decorator ─────────────────────────────────────────────────
function R3HudCorners() {
  const C = 14, T = 1.5, L = 20;
  const corner = (style) => (
    <div style={{ position: 'absolute', width: C, height: C, ...style }}>
      <div style={{ position: 'absolute', top: 0, left: 0, width: L, height: T, background: '#00eeff', opacity: 0.65 }} />
      <div style={{ position: 'absolute', top: 0, left: 0, width: T, height: L, background: '#00eeff', opacity: 0.65 }} />
    </div>
  );
  const cornerBR = (style) => (
    <div style={{ position: 'absolute', width: C, height: C, ...style }}>
      <div style={{ position: 'absolute', bottom: 0, right: 0, width: L, height: T, background: '#00eeff', opacity: 0.65 }} />
      <div style={{ position: 'absolute', bottom: 0, right: 0, width: T, height: L, background: '#00eeff', opacity: 0.65 }} />
    </div>
  );
  const cornerTR = (style) => (
    <div style={{ position: 'absolute', width: C, height: C, ...style }}>
      <div style={{ position: 'absolute', top: 0, right: 0, width: L, height: T, background: '#00eeff', opacity: 0.65 }} />
      <div style={{ position: 'absolute', top: 0, right: 0, width: T, height: L, background: '#00eeff', opacity: 0.65 }} />
    </div>
  );
  const cornerBL = (style) => (
    <div style={{ position: 'absolute', width: C, height: C, ...style }}>
      <div style={{ position: 'absolute', bottom: 0, left: 0, width: L, height: T, background: '#00eeff', opacity: 0.65 }} />
      <div style={{ position: 'absolute', bottom: 0, left: 0, width: T, height: L, background: '#00eeff', opacity: 0.65 }} />
    </div>
  );
  return (
    <>
      {corner({ top: 5, left: 5 })}
      {cornerTR({ top: 5, right: 5 })}
      {cornerBL({ bottom: 5, left: 5 })}
      {cornerBR({ bottom: 5, right: 5 })}
    </>
  );
}

// ── Top-level view ───────────────────────────────────────────────────────
function Rack3DView({ cfg, cableLength = 0.0012 }) {
  const M = cfg.params?.M || 12;

  const firstRack  = cfg.groups[0]?.racks[0];
  const firstGroup = cfg.groups[0];
  const [selRackId, setSelRackId] = useState3R(firstRack?.id || null);
  const [selRack,   setSelRack]   = useState3R(firstRack || null);
  const [selGroup,  setSelGroup]  = useState3R(firstGroup || null);

  const cfgRef = useRef3R(cfg);
  useEffect3R(() => { cfgRef.current = cfg; }, [cfg]);

  const rackData = useMemo3R(() => {
    if (!selRack || !selGroup) return null;
    return storeRackToR3(selRack, selGroup, M);
  }, [selRack, selGroup, M]);

  const onSelect = (id, rack, group) => {
    setSelRackId(id);
    setSelRack(rack);
    setSelGroup(group);
  };

  useEffect3R(() => {
    if (!selRackId) return;
    for (const g of cfgRef.current.groups) {
      const r = g.racks.find(r => r.id === selRackId);
      if (r) { setSelRack(r); setSelGroup(g); return; }
    }
  }, [selRackId]);

  const selLabel    = selRack  ? aliasOr(selRack,  selRack.label)  : null;
  const selGroupTag = selGroup ? aliasOr(selGroup, selGroup.tag)   : null;

  // Quick rack health for info panel
  const selHealth = useMemo3R(() => {
    if (!selRack) return null;
    return (window.rackHealth || (() => null))(selRack);
  }, [selRack]);

  return (
    <div className="rack3d-view" style={{
      position: 'absolute', inset: 0, display: 'flex',
      background: `#${R3_COL.bg.toString(16).padStart(6, '0')}`,
      overflow: 'hidden',
    }}>
      {/* Left: rack navigator */}
      <Rack3DRackList cfg={cfg} selectedRackId={selRackId} onSelect={onSelect} />

      {/* Right: floor plan + rack detail */}
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', position: 'relative' }}>

        {/* Floor plan view */}
        <div style={{ flex: '0 0 62%', position: 'relative', overflow: 'hidden' }}>
          <Rack3DFloorCanvas cfg={cfg} selectedRackId={selRackId} onSelect={onSelect} cableLength={cableLength} />

          {/* HUD overlay — top-left data strip */}
          <div style={{
            position: 'absolute', top: 10, left: 10, right: 10,
            display: 'flex', alignItems: 'center', gap: 0,
            pointerEvents: 'none', zIndex: 5,
          }}>
            <div style={{
              display: 'flex', alignItems: 'center', gap: 10,
              padding: '5px 14px', borderRadius: 6,
              background: 'rgba(1,6,15,0.82)', backdropFilter: 'blur(12px)',
              border: '1px solid rgba(0,238,255,0.15)',
              boxShadow: '0 0 24px rgba(0,238,255,0.06)',
            }}>
              <div style={{ width: 2, height: 16, background: '#00eeff', boxShadow: '0 0 6px rgba(0,238,255,0.8)' }} />
              <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, fontWeight: 700, color: '#00eeff', letterSpacing: '0.12em' }}>
                FLOOR OVERVIEW
              </span>
              <div style={{ width: 1, height: 12, background: 'rgba(0,238,255,0.18)' }} />
              {[
                { k: 'GROUPS', v: cfg.groups.length },
                { k: 'RACKS',  v: cfg.groups.reduce((s, g) => s + g.racks.filter(r => !r.absent).length, 0) },
                { k: 'ROBOTS', v: (cfg.robots || []).length },
              ].map(s => (
                <div key={s.k} style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
                  <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 14, fontWeight: 800, color: '#c8e4f4' }}>{s.v}</span>
                  <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 8, color: '#2a5070', letterSpacing: '0.1em' }}>{s.k}</span>
                </div>
              ))}
              <div style={{ flex: 1 }} />
              <div style={{ width: 6, height: 6, borderRadius: '50%', background: '#00e599', boxShadow: '0 0 8px rgba(0,229,153,0.8)', animation: 'r3pulse 2s infinite' }} />
              <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 8, color: '#1e6040', letterSpacing: '0.12em' }}>LIVE</span>
            </div>
          </div>

          {/* HUD corners on floor canvas */}
          <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 4 }}>
            <R3HudCorners />
          </div>
        </div>

        {/* Rack detail section */}
        <div style={{
          flex: '1 1 38%', position: 'relative', overflow: 'hidden',
          borderTop: '1px solid rgba(0,238,255,0.12)',
          display: 'flex', flexDirection: 'column',
          background: 'rgba(1,5,12,0.95)',
        }}>
          {/* Detail header */}
          <div style={{
            height: 32, flexShrink: 0, display: 'flex', alignItems: 'center',
            padding: '0 14px', gap: 10,
            background: 'rgba(1,6,15,0.98)',
            borderBottom: '1px solid rgba(0,238,255,0.08)',
          }}>
            <div style={{ width: 2, height: 14, background: '#00eeff', opacity: 0.6 }} />
            <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 9, color: '#1a5a78', letterSpacing: '0.12em' }}>RACK DETAIL</span>
            {selLabel && (
              <>
                <div style={{ width: 1, height: 10, background: 'rgba(0,238,255,0.2)' }} />
                <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 12, color: '#00eeff', fontWeight: 700, letterSpacing: '0.06em' }}>
                  {selLabel}
                </span>
                {selGroupTag && (
                  <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, color: '#2a5070', letterSpacing: '0.06em' }}>
                    · {selGroupTag}
                  </span>
                )}
              </>
            )}
            <div style={{ flex: 1 }} />
            {selHealth && (
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                {[
                  { k: 'DEV', v: selHealth.total || 0, c: '#4a88b0' },
                  { k: 'SUSP', v: selHealth.susp || 0, c: selHealth.susp > 0 ? '#f5b942' : '#1e3a52' },
                ].map(s => (
                  <div key={s.k} style={{ display: 'flex', alignItems: 'baseline', gap: 3 }}>
                    <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 13, fontWeight: 700, color: s.c }}>{s.v}</span>
                    <span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 8, color: '#1e3a52', letterSpacing: '0.1em' }}>{s.k}</span>
                  </div>
                ))}
              </div>
            )}
          </div>

          <div style={{ flex: 1, position: 'relative' }}>
            {rackData
              ? <Rack3DCanvas rackData={rackData} key={selRackId} cableLength={cableLength} syncCfg={cfg} syncRackId={selRackId} />
              : <div style={{ display: 'grid', placeItems: 'center', height: '100%', flexDirection: 'column', gap: 8 }}>
                  <div style={{ fontFamily: 'JetBrains Mono, monospace', color: '#0e2840', fontSize: 11, letterSpacing: '0.12em', textAlign: 'center' }}>
                    <div style={{ marginBottom: 6, opacity: 0.5 }}>▲</div>
                    FLOOR에서 랙을 클릭하세요
                  </div>
                </div>
            }
          </div>
        </div>

      </div>
    </div>
  );
}
