/* game.jsx — El Reto v4. Drag RACKS into ROWS. CPU demand + AC/DC voltage (miswire allowed) +
   capacity ceiling + cooling + cost budget + PUE. Specs hidden until tapped. Energise always on;
   unstable energise → alarm → full restart. */

function makeAccessCode() {
  const a = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; let s = "";
  for (let i = 0; i < 4; i++) s += a[Math.floor(Math.random() * a.length)];
  return "AI-CPD-" + s;
}
const busAccepts = (rack, bus) => rack.buses.includes(bus);
const busList = (rack, lang) => rack.buses.map(b => BUSES[b].label[lang]).join(" · ");

function validateRoom(room, placement) { return null; } // placeholder (unused)

/* PUE dial */
const PueDial = ({ value, target, ok, active }) => {
  const min = 1.0, max = 1.8;
  const frac = Math.max(0, Math.min(1, (value - min) / (max - min)));
  const tgtFrac = Math.max(0, Math.min(1, (target - min) / (max - min)));
  const ang = (f) => -90 + f * 180;
  const pt = (f, r) => { const a = (ang(f) - 90) * Math.PI / 180; return [50 + r * Math.cos(a), 50 + r * Math.sin(a)]; };
  const arc = (f0, f1, r) => { const [x0, y0] = pt(f0, r), [x1, y1] = pt(f1, r); return `M ${x0} ${y0} A ${r} ${r} 0 0 1 ${x1} ${y1}`; };
  const [nx, ny] = pt(frac, 33);
  return (
    <div className={"pue-dial" + (active ? "" : " is-locked")}>
      <svg viewBox="0 0 100 60" className="pue-dial__svg">
        <path d={arc(0, 1, 40)} className="pue-dial__track" />
        {active && <path d={arc(0, frac, 40)} className={"pue-dial__fill " + (ok ? "is-ok" : "is-over")} />}
        <line x1="50" y1="50" x2={pt(tgtFrac, 44)[0]} y2={pt(tgtFrac, 44)[1]} className="pue-dial__target" />
        {active && <line x1="50" y1="50" x2={nx} y2={ny} className="pue-dial__needle" />}
        <circle cx="50" cy="50" r="3" className="pue-dial__hub" />
      </svg>
      <div className="pue-dial__read">
        <span className={"pue-dial__val " + (active ? (ok ? "is-ok" : "is-over") : "is-locked")}>{active ? value.toFixed(2) : "—"}</span>
        <span className="pue-dial__tgt">≤ {target.toFixed(2)}</span>
      </div>
    </div>
  );
};

/* Rack illustration — face shows only this + the model name, no specs */
const RackGlyph = ({ s = 26 }) => (
  <svg width={s} height={s * 1.42} viewBox="0 0 26 37" fill="none" aria-hidden="true">
    <rect x="2.5" y="1.5" width="21" height="34" rx="1.5" stroke="currentColor" strokeWidth="1.4" />
    <line x1="6" y1="1.5" x2="6" y2="35.5" stroke="currentColor" strokeWidth="0.8" opacity="0.5" />
    <line x1="20" y1="1.5" x2="20" y2="35.5" stroke="currentColor" strokeWidth="0.8" opacity="0.5" />
    {[6, 11, 16, 21, 26, 31].map((y, i) => (<g key={i}><rect x="7.5" y={y} width="11" height="3" rx="0.5" fill="currentColor" opacity="0.16" /><circle cx="9.2" cy={y + 1.5} r="0.7" fill="currentColor" opacity="0.55" /></g>))}
  </svg>
);

/* Rack chip — drawing + name only */
const RackChip = ({ rack, dragging, pulse, onPointerDown }) => (
  <div className={"rk-chip" + (dragging ? " is-dragging" : "") + (pulse ? " is-tour-pulse" : "")} onPointerDown={(e) => onPointerDown(e, rack)} title={rack.model}>
    <span className="rk-chip__glyph"><RackGlyph s={24} /></span>
    <span className="rk-chip__name">{rack.model}</span>
  </div>
);

const CoolMeter = ({ label, cap, heat, ok }) => {
  const pct = heat > 0 ? Math.min(cap / heat, 1) * 100 : 0;
  return (
    <div className="cool-meter">
      <div className="cool-meter__row"><span className="cool-meter__k">{label}</span><span className={"cool-meter__v" + (ok ? " is-ok" : " is-under")}>{cap}<em>/{heat} kW</em></span></div>
      <div className="cool-meter__track"><div className={"cool-meter__fill" + (ok ? " is-ok" : " is-under")} style={{ width: pct + "%" }} /></div>
    </div>
  );
};

/* Vertical control-panel gauge */
const VBar = ({ label, value, target, unit, euro, ok, mode }) => {
  const pct = target > 0 ? Math.min(value / target, 1) * 100 : 0;
  const over = mode === "atmost" && value > target;
  const cls = ok ? "is-ok" : over ? "is-over" : "is-wait";
  return (
    <div className="vbar">
      <div className="vbar__val-wrap">
        <span className={"vbar__val " + cls}>{euro ? "€" : ""}{value}</span>
        <span className="vbar__tgt">{mode === "atmost" ? "≤" : "≥"} {euro ? "€" : ""}{target}{euro ? "k" : ""}</span>
      </div>
      <div className="vbar__track">
        <div className={"vbar__fill " + cls} style={{ height: pct + "%" }} />
        <div className="vbar__targetline" />
      </div>
      <div className="vbar__lbl">{label}<em>{unit}</em></div>
    </div>
  );
};

const Game = ({ onWin, treatment = "flat" }) => {
  const { t, lang } = useReto();
  const G = t.game;
  const rounds = RETO_ROUNDS;
  const [roundIdx, setRoundIdx] = React.useState(0);
  const round = rounds[roundIdx];
  const hasPue = round.features.includes("pue");

  const initPlacement = (r) => { const p = {}; r.racks.forEach(rk => p[rk.id] = "tray"); return p; };
  const initCooling = (r) => new Array(r.coolingSlots).fill(null);
  const [placement, setPlacement] = React.useState(() => initPlacement(rounds[0]));
  const [cooling, setCooling] = React.useState(() => initCooling(rounds[0]));
  const [energisations, setEnergisations] = React.useState(0);
  const [cleared, setCleared] = React.useState(false);
  const [alarm, setAlarm] = React.useState(null);
  const [pickSlot, setPickSlot] = React.useState(null);
  const [sheet, setSheet] = React.useState(null);
  const [showPue, setShowPue] = React.useState(false);
  const [showHarm, setShowHarm] = React.useState(false);
  const [showSw, setShowSw] = React.useState(false);
  const [showPm, setShowPm] = React.useState(false);
  const [showIntro, setShowIntro] = React.useState(true);
  const [boot, setBoot] = React.useState("loading");
  /* Boot bar — user-triggered. Previously the bar auto-advanced after
     a 3s setTimeout; now `bootStarted` gates it so nothing moves
     until the user clicks the Start button in the loading UI. Once
     clicked, the bar's CSS animation runs and the same 3s timeout
     transitions the boot state to "brief1". */
  const [bootStarted, setBootStarted] = React.useState(false);
  React.useEffect(() => { if (boot === "loading" && bootStarted) { const t = setTimeout(() => setBoot("brief1"), 3000); return () => clearTimeout(t); } }, [boot, bootStarted]);
  const startGame = () => { setBoot("done"); setShowIntro(false); setTimeout(() => setTour(0), 250); };
  const [drag, setDrag] = React.useState(null);
  const [hoverZone, setHoverZone] = React.useState(null);
  const zoneRefs = React.useRef(new Map());
  const stageRef = React.useRef(null);

  /* ---- live validation ---- */
  const rd = (id) => round.racks.find(r => r.id === id);
  const rowTotals = {}; round.rows.forEach(f => rowTotals[f.id] = 0);
  let heat = 0, rackCost = 0, cpuUsed = 0;
  const rowMiswire = {}; round.rows.forEach(f => rowMiswire[f.id] = false);
  const rowPlats = {}; round.rows.forEach(f => rowPlats[f.id] = new Set());
  let maxLead = 0, maxComm = 0;
  round.racks.forEach(r => { const z = placement[r.id]; if (z && z !== "tray" && rowTotals[z] != null) { rowTotals[z] += r.e; heat += r.t; rackCost += r.cost; cpuUsed += r.cpu; const row = round.rows.find(f => f.id === z); if (row && !busAccepts(r, row.bus)) rowMiswire[z] = true; if (r.p) rowPlats[z].add(r.p); if (r.lead) maxLead = Math.max(maxLead, r.lead); if (r.comm) maxComm = Math.max(maxComm, r.comm); } });
  const hasSoftware = round.features.includes("software");
  const hasPm = round.features.includes("pm");
  const rowChecks = round.rows.map(f => {
    const total = rowTotals[f.id]; let st = "empty";
    if (rowMiswire[f.id]) st = "miswire";
    else if (hasSoftware && rowPlats[f.id].size > 1) st = "mixplat";
    else if (total > f.cap) st = "trip";
    else if (total > f.hi) st = "over";
    else if (total > 0 && total < f.lo) st = "under";
    else if (total > 0) st = "ok";
    return { id: f.id, total, st, ok: st === "ok", plat: [...rowPlats[f.id]][0] };
  });
  const units = cooling.filter(Boolean).map(k => COOLING_UNITS[k]);
  const coolCap = units.reduce((s, u) => s + u.capKw, 0);
  const coolCost = units.reduce((s, u) => s + u.costK, 0);
  const coolDraw = units.reduce((s, u) => s + u.drawKw, 0);
  const coolingOk = coolCap >= heat && heat > 0;
  const spent = rackCost + coolCost;
  const budgetOk = spent <= round.budget;
  const cpuOk = cpuUsed >= round.cpuDemand;
  const pueVal = cpuUsed > 0 ? (rowTotalsSum() + coolDraw) / rowTotalsSum() : 1;
  function rowTotalsSum() { return round.rows.reduce((s, f) => s + rowTotals[f.id], 0); }
  const pueOk = hasPue ? (rowTotalsSum() > 0 && pueVal <= round.pueTarget + 1e-9) : true;
  const hasHarm = round.features.includes("harmonics");
  const harmCounts = {};
  if (hasHarm) round.racks.forEach(r => { const z = placement[r.id]; if (z && z !== "tray" && r.h) harmCounts[r.h] = (harmCounts[r.h] || 0) + 1; });
  const harmBad = hasHarm ? Object.keys(harmCounts).filter(h => harmCounts[h] > round.harmLimit) : [];
  const harmOk = harmBad.length === 0;
  const softwareOk = !hasSoftware || rowChecks.every(c => c.st !== "mixplat");
  const timing = maxLead + maxComm;
  const pmOk = !hasPm || timing <= round.deadline;
  const rowsClean = rowChecks.every(c => c.st === "ok");
  const allOk = rowsClean && cpuOk && coolingOk && budgetOk && pueOk && harmOk && softwareOk && pmOk;

  const loadRound = (i) => { const r = rounds[i]; setPlacement(initPlacement(r)); setCooling(initCooling(r)); setPickSlot(null); setSheet(null); setShowIntro(true); };
  const resetRound = () => loadRound(roundIdx);

  const placeRack = (rackId, zoneId) => { setPlacement(p => (p[rackId] === zoneId ? p : { ...p, [rackId]: zoneId })); };

  const findZone = (x, y) => { let f = null; zoneRefs.current.forEach((el, id) => { if (!el) return; const r = el.getBoundingClientRect(); if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) f = id; }); return f; };
  const registerZone = (id) => (el) => { if (el) zoneRefs.current.set(id, el); else zoneRefs.current.delete(id); };

  const onChipDown = (e, rack) => {
    if (alarm || cleared) return;
    e.preventDefault();
    const r = e.currentTarget.getBoundingClientRect();
    const start = { x0: e.clientX, y0: e.clientY, ox: e.clientX - r.left, oy: e.clientY - r.top, w: r.width, h: r.height };
    let started = false;
    const move = (ev) => {
      const dist = Math.hypot(ev.clientX - start.x0, ev.clientY - start.y0);
      if (!started && dist > 6) { started = true; setDrag({ rack, x: ev.clientX, y: ev.clientY, ox: start.ox, oy: start.oy, w: start.w, h: start.h }); }
      if (started) { setDrag(d => d ? { ...d, x: ev.clientX, y: ev.clientY } : d); setHoverZone(findZone(ev.clientX, ev.clientY)); }
    };
    const up = (ev) => {
      window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up);
      if (started) { const z = findZone(ev.clientX, ev.clientY); if (z) placeRack(rack.id, z); setDrag(null); setHoverZone(null); }
      else { setSheet(rack.id); }
    };
    window.addEventListener("pointermove", move); window.addEventListener("pointerup", up);
  };

  const setSlot = (i, unitKey) => { setCooling(c => { const n = [...c]; n[i] = unitKey; return n; }); setPickSlot(null); };
  const racksIn = (zoneId) => round.racks.filter(rk => placement[rk.id] === zoneId);
  const trayRacks = racksIn("tray");

  const failReason = () => {
    const trip = rowChecks.find(c => c.st === "trip"); if (trip) return G.causeTrip.replace("{id}", trip.id);
    const mis = rowChecks.find(c => c.st === "miswire"); if (mis) return G.causeMiswire.replace("{id}", mis.id);
    const over = rowChecks.find(c => c.st === "over"); if (over) return G.causeOverband.replace("{id}", over.id);
    const under = rowChecks.find(c => c.st === "under"); if (under) return G.causeUnder.replace("{id}", under.id);
    const empty = rowChecks.find(c => c.st === "empty"); if (empty) return G.causeUnder.replace("{id}", empty.id);
    const mix = rowChecks.find(c => c.st === "mixplat"); if (mix) return G.causeSoftware.replace("{id}", mix.id);
    if (!cpuOk) return G.causeCpu;
    if (!coolingOk) return G.causeCool;
    if (!harmOk) return G.causeHarm.replace("{h}", harmBad.join(", "));
    if (!budgetOk) return G.causeBudget;
    if (!pmOk) return G.causePm;
    if (!pueOk) return G.causePue;
    return "";
  };
  const energise = () => { if (alarm || cleared) return; setEnergisations(n => n + 1); if (allOk) setCleared(true); else setAlarm({ reason: failReason() }); };
  const afterAlarm = () => { setAlarm(null); setRoundIdx(0); loadRound(0); };
  const next = () => {
    if (roundIdx + 1 >= rounds.length) {
      /* Final round cleared → exit fullscreen and surface the WinPanel.
         The parent flow shows the access-code reveal + candidatura form
         outside the immersive stage. */
      if (document.fullscreenElement && document.exitFullscreen) {
        document.exitFullscreen().catch(() => {});
      }
      onWin(makeAccessCode(), energisations);
    } else {
      const ni = roundIdx + 1; setRoundIdx(ni); loadRound(ni); setCleared(false);
    }
  };

  const [isFs, setIsFs] = React.useState(false);
  React.useEffect(() => {
    const onFs = () => {
      const fsNow = !!document.fullscreenElement;
      setIsFs(prev => {
        /* If we just LEFT fullscreen (was true, now false), scroll
           the now-minimised stage back into view so the player
           lands on the game window instead of wherever the page
           was scrolled before entering fullscreen. */
        if (prev && !fsNow) {
          const el = stageRef.current;
          if (el) {
            requestAnimationFrame(() => {
              el.scrollIntoView({ behavior: "smooth", block: "center" });
            });
          }
        }
        return fsNow;
      });
    };
    document.addEventListener("fullscreenchange", onFs);
    return () => document.removeEventListener("fullscreenchange", onFs);
  }, []);
  const toggleFs = () => { const el = stageRef.current; if (!document.fullscreenElement) el && el.requestFullscreen && el.requestFullscreen(); else document.exitFullscreen && document.exitFullscreen(); };
  /* enterFsFromBoot — called by the Start button on the boot screen.
     Requests fullscreen on the game stage so the user enters an
     immersive view as soon as they commit to playing. Wrapped in a
     try/catch since some browsers reject requestFullscreen calls
     outside a direct user gesture; if it fails, the game still plays
     in the embedded position. The HUD's fullscreen toggle (top-right)
     and the standard browser Esc key let the user exit at any time. */
  const enterFsFromBoot = () => {
    setBootStarted(true);
    const el = stageRef.current;
    if (el && el.requestFullscreen && !document.fullscreenElement) {
      try { el.requestFullscreen().catch(() => {}); } catch (e) {}
    }
  };

  /* ---- guided tour ----
     8 steps, three of them GATED on a player action:
       0  rows                — explanatory
       1  racks intro         — explanatory
       2  open a rack sheet   — gated: advances when `sheet` is set
       3  place a rack        — gated: advances when any placement exists
       4  cooling intro       — explanatory
       5  add a cooling unit  — gated: advances when any cooling slot is filled
       6  the limits / HUD    — explanatory
       7  energise            — explanatory (last)
     Gated steps replace the "Next" button with a waiting label and
     auto-advance after the player completes the action (small delay so
     they get to read the resulting state). */
  /* liveSel is what we should highlight ONCE the gate is met — used
     for the open-sheet step so that as soon as the player taps a rack
     and the ficha appears, the spot moves from the tray to the sheet.
     That keeps the sheet INSIDE the cutout (undimmed) instead of
     getting buried under the tour's 9999px box-shadow.
     extraSel adds a SECOND element to the spot via bounding-rect
     union — used on "place a rack" so BOTH the rack tray (source)
     and the rows (target) light up. Player has to drag from one
     to the other, both need to be visible. */
  const TOUR_STEPS_DEF = [
    { sel: ".game-feeds"    },
    { sel: ".game-tray"     },
    { sel: ".game-tray",     gate: "sheet", liveSel: ".rack-sheet" },
    { sel: ".game-tray",     gate: "place", extraSel: ".game-feeds" },
    { sel: ".cool-bay"      },
    { sel: ".cool-bay",      gate: "cool"  },
    { sel: ".game-hud__r"   },
    { sel: ".game-energise" },
  ];
  const TOUR_SEL = TOUR_STEPS_DEF.map(s => s.sel);
  const [tour, setTour] = React.useState(null);
  const [tourRect, setTourRect] = React.useState(null);
  React.useLayoutEffect(() => {
    if (tour === null) { setTourRect(null); return; }
    const step = TOUR_STEPS_DEF[tour];
    /* Pick the live selector if its gate is met. Right now only the
       sheet step uses this — when sheet !== null we re-target to the
       .rack-sheet element so the cutout surrounds it (otherwise the
       ficha would sit in the dim and look unreadable). */
    const gateMetLive = step.liveSel && (
      (step.gate === "sheet" && sheet !== null) ||
      (step.gate === "place" && Object.values(placement).some(v => v != null && v !== "tray")) ||
      (step.gate === "cool"  && cooling.some(Boolean))
    );
    const liveTarget = gateMetLive ? step.liveSel : step.sel;
    const measure = () => {
      const el = document.querySelector(liveTarget);
      if (!el) return;
      const r = el.getBoundingClientRect();
      /* If the step has an extraSel, expand to the bounding rect that
         covers BOTH elements. This is how "place a rack" lights the
         rack tray AND the rows at once. Skip the union when we're in
         live-target mode (gate met) — at that point the focus has
         shifted to the result element. */
      let top = r.top, left = r.left, right = r.right, bottom = r.bottom;
      if (step.extraSel && !gateMetLive) {
        const ex = document.querySelector(step.extraSel);
        if (ex) {
          const er = ex.getBoundingClientRect();
          top    = Math.min(top, er.top);
          left   = Math.min(left, er.left);
          right  = Math.max(right, er.right);
          bottom = Math.max(bottom, er.bottom);
        }
      }
      setTourRect({ top, left, width: right - left, height: bottom - top });
    };
    measure();
    window.addEventListener("resize", measure); window.addEventListener("scroll", measure, true);
    return () => { window.removeEventListener("resize", measure); window.removeEventListener("scroll", measure, true); };
  }, [tour, sheet, placement, cooling]);
  const startTour = () => setTour(0);
  const endTour = () => { setTour(null); try { localStorage.setItem("reto_tour_seen", "1"); } catch (e) {} };
  const tourNext = () => { setTour(s => (s + 1 >= TOUR_SEL.length ? (endTour(), null) : s + 1)); };
  const dismissIntro = () => { setShowIntro(false); let seen = false; try { seen = !!localStorage.getItem("reto_tour_seen"); } catch (e) {} if (roundIdx === 0 && !seen) setTimeout(() => setTour(0), 220); };

  /* Evaluate the current tour step's gate (null for explanatory steps).
     Each gate maps to a piece of game state that proves the player
     completed the requested action. */
  const currentTourStep = tour !== null ? TOUR_STEPS_DEF[tour] : null;
  const tourGateMet = (() => {
    if (!currentTourStep || !currentTourStep.gate) return false;
    if (currentTourStep.gate === "sheet") return sheet !== null;
    if (currentTourStep.gate === "place") return Object.values(placement).some(v => v != null && v !== "tray");
    if (currentTourStep.gate === "cool")  return cooling.some(Boolean);
    return false;
  })();

  /* Auto-advance for the instant gates (place, cool). Sheet has its
     own effect below because it needs different rules. */
  React.useEffect(() => {
    if (!currentTourStep || !currentTourStep.gate || !tourGateMet) return;
    if (currentTourStep.gate === "sheet") return;
    tourNext();
  }, [tour, tourGateMet]);

  /* Sheet step (step 2) auto-advance — two ways to fire:
       1. Player opens the sheet and closes it before the safety
          timer → advance instantly. They've signalled "I'm done
          reading", no point making them hunt for Siguiente.
       2. Player opens the sheet and leaves it open for 15s → safety
          timeout closes the sheet and advances anyway.
     A ref tracks whether the sheet has been opened during this step
     so we can detect the close transition (open → null) and react
     to it without firing on the initial null state. */
  const sheetOpenedRef = React.useRef(false);
  React.useEffect(() => {
    if (tour !== 2) { sheetOpenedRef.current = false; return; }
    if (sheet !== null) {
      sheetOpenedRef.current = true;
      const id = setTimeout(() => {
        sheetOpenedRef.current = false;
        setSheet(null);
        tourNext();
      }, 15000);
      return () => clearTimeout(id);
    }
    /* sheet === null on step 2 — if it was opened then closed during
       this step, advance immediately. */
    if (sheetOpenedRef.current) {
      sheetOpenedRef.current = false;
      tourNext();
    }
  }, [tour, sheet]);

  const sheetRack = sheet ? rd(sheet) : null;

  return (
    <div ref={stageRef} className={"game-stage game-stage--" + treatment + (isFs ? " is-fs" : "") + (alarm ? " is-alarm" : "")} onClick={() => setPickSlot(null)}>
      {/* HUD */}
      <div className="game-hud">
        <div className="game-hud__l">
          <div className="game-hud__room"><span className="game-hud__roomlbl">{round.name[lang]}</span><span className="game-hud__roomidx">{G.roomLabel} {roundIdx + 1} {G.ofRooms} {rounds.length}</span></div>
          <div className="game-hud__dots">{rounds.map((r, i) => <span key={r.id} className={"game-dot" + (i < roundIdx || (i === roundIdx && cleared) ? " is-done" : "") + (i === roundIdx && !cleared ? " is-now" : "")} />)}</div>
        </div>
        <div className="game-hud__r">
          <div className="hud-meter"><span className="hud-meter__k">{G.mCpu}</span><span className={"hud-meter__v" + (cpuOk ? " is-ok" : "")}>{cpuUsed}<em>/{round.cpuDemand} kW</em></span><div className="hud-bar"><div className={"hud-bar__fill" + (cpuOk ? " is-ok" : "")} style={{ width: Math.min(cpuUsed / round.cpuDemand, 1) * 100 + "%" }} /></div></div>
          <div className="hud-meter"><span className="hud-meter__k">{G.mCooling}</span><span className={"hud-meter__v" + (coolingOk ? " is-ok" : "")}>{coolCap}<em>/{heat} kW</em></span><div className="hud-bar"><div className={"hud-bar__fill" + (coolingOk ? " is-ok" : "")} style={{ width: (heat > 0 ? Math.min(coolCap / heat, 1) * 100 : 0) + "%" }} /></div></div>
          <div className="hud-meter"><span className="hud-meter__k">{G.mBudget}</span><span className={"hud-meter__v" + (budgetOk ? "" : " is-over")}>€{spent}<em>/<span className="hud-meter__eur">€</span>{round.budget}k</em></span><div className="hud-bar"><div className={"hud-bar__fill" + (budgetOk ? " is-budget" : " is-overbudget")} style={{ width: Math.min(spent / round.budget, 1) * 100 + "%" }} /></div></div>
          <button className="game-fsbtn" onClick={(e) => { e.stopPropagation(); toggleFs(); }} aria-label={isFs ? G.exitFullscreen : G.fullscreen}>{isFs ? <RIcon.Shrink s={15} c="currentColor" /> : <RIcon.Expand s={15} c="currentColor" />}<span>{isFs ? G.exitFullscreen : G.fullscreen}</span></button>
        </div>
      </div>

      <div className="game-board">
        <div className="game-left">
          <div className={"game-feeds game-feeds--" + round.rows.length}>
            {round.rows.map(f => {
              const c = rowChecks.find(x => x.id === f.id), total = c.total, st = c.st;
              const fillPct = Math.min(total / f.cap, 1) * 100, loPct = f.lo / f.cap * 100, hiPct = f.hi / f.cap * 100;
              const stLabel = { empty: G.statusEmpty, under: G.statusUnder, ok: G.statusOk, over: G.statusOver, trip: G.statusTrip, miswire: G.statusMiswire, mixplat: G.statusMixplat }[st];
              const vc = BUSES[f.bus].color, isHover = hoverZone === f.id;
              return (
                <div key={f.id} ref={registerZone(f.id)} className={"feed feed--" + st + " v-" + vc + (isHover ? " is-hover" : "") + (cleared ? " is-live" : "")} onClick={(e) => e.stopPropagation()}>
                  <div className="feed__head"><div className="feed__id"><RIcon.Bolt s={14} c="currentColor" /><span>{G.row} {f.id}</span>{hasSoftware && c.plat && <span className={"plat-badge" + (st === "mixplat" ? " is-bad" : "")}>{st === "mixplat" ? "⚠ mix" : c.plat}</span>}</div><span className={"bus-badge v-" + vc}>{BUSES[f.bus].label[lang]}</span></div>
                  <div className="feed__gaugewrap">
                    <div className="feed__gauge"><div className="feed__band" style={{ left: loPct + "%", width: (hiPct - loPct) + "%" }} /><div className="feed__fill" style={{ width: fillPct + "%" }} /><div className="feed__tick" style={{ left: loPct + "%" }} /><div className="feed__tick" style={{ left: hiPct + "%" }} /></div>
                    <div className="feed__readout"><span className="feed__total">{total}<em>/{f.cap} kW</em></span><span className={"feed__status feed__status--" + st}>{stLabel}</span></div>
                    <div className="feed__bandlbl">{G.band} {f.lo}–{f.hi} kW</div>
                  </div>
                  <div className="feed__drop">
                    {racksIn(f.id).map(rk => <RackChip key={rk.id} rack={rk} dragging={drag && drag.rack.id === rk.id} onPointerDown={onChipDown} />)}
                    {racksIn(f.id).length === 0 && <span className="feed__placeholder">{G.band} {f.lo}–{f.hi} kW · {BUSES[f.bus].label[lang]}</span>}
                  </div>
                  <div className={"feed__delta feed__delta--" + (st === "over" || st === "trip" ? "less" : st === "under" || st === "empty" ? "more" : st === "miswire" || st === "mixplat" ? "mis" : "ok")}>
                    {st === "ok" ? <span className="feed__ok"><RIcon.Check s={13} c="currentColor" /> {G.statusOk}</span>
                      : st === "miswire" ? <span className="feed__miswire"><RIcon.Alert s={12} c="currentColor" /> {G.statusMiswire}</span>
                      : st === "mixplat" ? <span className="feed__miswire"><RIcon.Alert s={12} c="currentColor" /> {G.statusMixplat}</span>
                      : st === "under" ? <span>+{f.lo - total} {G.deltaNeedMore}</span>
                      : st === "empty" ? <span>+{f.lo} {G.deltaNeedMore}</span>
                      : st === "over" ? <span>−{total - f.hi} {G.deltaOver}</span>
                      : st === "trip" ? <span>−{total - f.cap} {G.deltaOver}</span> : null}
                  </div>
                </div>
              );
            })}
          </div>

          <div ref={registerZone("tray")} className={"game-tray" + (hoverZone === "tray" ? " is-hover" : "")} onClick={(e) => e.stopPropagation()}>
            <div className="game-tray__head"><span className="game-tray__lbl">{trayRacks.length ? G.tray : G.trayEmpty}</span><span className="game-tray__hint">{G.dragHint}</span></div>
            <div className="game-tray__chips">
              {trayRacks.map((rk, i) => <RackChip key={rk.id} rack={rk} dragging={drag && drag.rack.id === rk.id} pulse={i === 3 && tour === 2 && sheet === null} onPointerDown={onChipDown} />)}
              {trayRacks.length === 0 && <span className="game-tray__empty"><RIcon.Check s={15} c="currentColor" /> {G.trayEmpty}</span>}
            </div>
          </div>
        </div>

        {/* Cooling bay */}
        <div className="cool-bay">
          <div className="cool-bay__head"><span className="cool-bay__title"><RIcon.Air s={15} c="currentColor" /> {G.coolingBay}</span><span className="cool-bay__cost">€{coolCost}k</span></div>
          <div className="cool-slots">
            {cooling.map((u, i) => { const unit = u ? COOLING_UNITS[u] : null; return (
              <div key={i} className={"cool-slot" + (unit ? " is-filled" : "") + (pickSlot === i ? " is-active" : "")} onClick={(e) => { e.stopPropagation(); setPickSlot(p => p === i ? null : i); }}>
                {unit ? (<>
                  <div className="cool-slot__top"><span className="cool-slot__name">{u}</span><button className="cool-slot__x" onClick={(e) => { e.stopPropagation(); setSlot(i, null); }} aria-label={G.close}><RIcon.Close s={13} c="currentColor" /></button></div>
                  <div className="cool-slot__stats"><span><b>{unit.capKw}</b> kW</span>{hasPue && <span className="cool-slot__draw"><b>{unit.drawKw}</b> {G.drawLabel}</span>}<span className="cool-slot__money">€{unit.costK}k</span></div>
                </>) : (<div className="cool-slot__empty"><RIcon.Plus s={18} c="currentColor" /><span>{G.addUnit}</span></div>)}
                {pickSlot === i && (
                  <div className="cool-picker" onClick={(e) => e.stopPropagation()}>
                    <div className="cool-picker__hd">{G.chooseUnit}</div>
                    {round.coolingCatalog.map(k => { const cu = COOLING_UNITS[k]; return (
                      <button key={k} className="cool-picker__opt" onClick={() => setSlot(i, k)}><span className="cool-picker__name">{k}</span><span className="cool-picker__meta"><b>{cu.capKw}</b>kW{hasPue && <span> · <span className="cool-picker__draw">{cu.drawKw}kW</span></span>} · <span className="cool-picker__money">€{cu.costK}k</span></span></button>); })}
                    {u && <button className="cool-picker__opt cool-picker__clear" onClick={() => setSlot(i, null)}>{G.close}</button>}
                  </div>
                )}
              </div>); })}
          </div>
          {hasPue ? (
          <div className="cool-pue">
            <div className="cool-pue__hd"><span className="cool-pue__k">{G.pue}<button className="cool-pue__info" onClick={(e) => { e.stopPropagation(); setShowPue(true); }}><RIcon.Info s={13} c="currentColor" /></button></span></div>
            <PueDial value={pueVal} target={round.pueTarget} ok={pueOk} active={true} />
          </div>
          ) : roundIdx === 0 ? (
          <div className="cool-pue cool-pue--locked">
            <div className="cool-pue__hd"><span className="cool-pue__k">{G.pue}</span></div>
            <div className="pue-locked"><RIcon.Lock s={15} c="currentColor" /><span>{G.locked}</span></div>
          </div>
          ) : null}
          {hasHarm && (
            <div className="harm">
              <div className="harm__hd"><span className="cool-pue__k">{G.harmonics}<button className="cool-pue__info" onClick={(e) => { e.stopPropagation(); setShowHarm(true); }}><RIcon.Info s={13} c="currentColor" /></button></span><span className="harm__limit">máx {round.harmLimit}/orden</span></div>
              <div className="harm__chips">
                {[...new Set(round.racks.filter(r => r.h).map(r => r.h))].sort().map(h => { const n = harmCounts[h] || 0; const bad = n > round.harmLimit; return (
                  <span key={h} className={"harm__chip" + (n > 0 ? " is-on" : "") + (bad ? " is-bad" : "")}>{h}<b>{n}</b></span>); })}
              </div>
            </div>
          )}
          {hasPm && (
            <div className="sched">
              <div className="harm__hd"><span className="cool-pue__k">{G.projectTitle}<button className="cool-pue__info" onClick={(e) => { e.stopPropagation(); setShowPm(true); }}><RIcon.Info s={13} c="currentColor" /></button></span><span className={"sched__total" + (pmOk ? "" : " is-bad")}>{timing}/{round.deadline} {G.weeks}</span></div>
              <div className="sched__bar">
                <div className="sched__seg sched__seg--lead" style={{ width: (maxLead / round.deadline) * 100 + "%" }}>{maxLead > 0 ? maxLead : ""}</div>
                <div className={"sched__seg sched__seg--comm" + (pmOk ? "" : " is-bad")} style={{ width: (maxComm / round.deadline) * 100 + "%" }}>{maxComm > 0 ? maxComm : ""}</div>
                <div className="sched__deadline" style={{ left: "100%" }} />
              </div>
              <div className="sched__legend"><span><i className="sched__dot sched__dot--lead" />{G.lead}</span><span><i className="sched__dot sched__dot--comm" />{G.comm}</span></div>
            </div>
          )}
        </div>
      </div>

      {/* Controls — Energise always active */}
      <div className="game-controls">
        <button className="game-reset" onClick={(e) => { e.stopPropagation(); resetRound(); }}>{G.reset}</button>
        <button className="game-reset game-tourbtn" onClick={(e) => { e.stopPropagation(); startTour(); }}><RIcon.Info s={13} c="currentColor" /><span>{G.tutorial}</span></button>
        <div className="game-controls__r">
          <span className="game-tries">{G.tries}: {energisations}</span>
          <button className="btn btn-primary game-energise" onClick={(e) => { e.stopPropagation(); energise(); }}><RIcon.Bolt s={16} c="#fff" /><span>{G.energise}</span></button>
        </div>
      </div>

      {/* Drag ghost */}
      {drag && (<div className="rk-ghost" style={{ left: drag.x - drag.ox, top: drag.y - drag.oy, width: drag.w, height: drag.h }}><span className="rk-chip__glyph"><RackGlyph s={24} /></span><span className="rk-chip__name">{drag.rack.model}</span></div>)}

      {/* Rack spec sheet */}
      {sheetRack && (
        <div className="game-modal" onClick={() => setSheet(null)}>
          <div className="rack-sheet" onClick={(e) => e.stopPropagation()}>
            <button className="game-modal__close" onClick={() => setSheet(null)}><RIcon.Close s={20} c="currentColor" /></button>
            <div className="rack-sheet__title">{G.rackSheet}</div>
            <div className="rack-sheet__name"><span className="rack-sheet__glyph"><RackGlyph s={30} /></span>{sheetRack.model}</div>
            <dl className="rack-specs">
              <div className="rack-specs__row"><dt>{G.vendor}</dt><dd>{sheetRack.vendor}</dd></div>
              <div className="rack-specs__row"><dt>{G.elec}</dt><dd>{sheetRack.e} kW</dd></div>
              <div className="rack-specs__row"><dt>{G.cpu}</dt><dd className="rack-specs__cpu">{sheetRack.cpu} kW</dd></div>
              <div className="rack-specs__row"><dt>{G.therm}</dt><dd>{sheetRack.t} kW</dd></div>
              <div className="rack-specs__row"><dt>{G.voltage}</dt><dd>{busList(sheetRack, lang)}</dd></div>
              {sheetRack.h && <div className="rack-specs__row"><dt>{G.harmOrder}</dt><dd>{sheetRack.h}</dd></div>}
              {sheetRack.p && <div className="rack-specs__row"><dt>{G.platform}</dt><dd>{sheetRack.p}</dd></div>}
              {sheetRack.lead != null && <div className="rack-specs__row"><dt>{G.lead}</dt><dd>{sheetRack.lead} {G.weeks}</dd></div>}
              {sheetRack.comm != null && <div className="rack-specs__row"><dt>{G.comm}</dt><dd>{sheetRack.comm} {G.weeks}</dd></div>}
              <div className="rack-specs__row"><dt>{G.cost}</dt><dd className="rack-specs__cost">€{sheetRack.cost}k</dd></div>
            </dl>
          </div>
        </div>
      )}

      {boot !== "done" && (() => { const B = G.boot; return (
        <div className="boot" onClick={(e) => e.stopPropagation()}>
          {/* Persistent fullscreen exit — visible on every boot/brief
              screen as long as the user is in fullscreen, so they can
              bail out at any point without waiting for the game to
              load. The HUD's .game-fsbtn carries the same behaviour
              once the game itself is on stage. */}
          {isFs && (
            <button className="boot__fsexit game-fsbtn" onClick={(e) => { e.stopPropagation(); toggleFs(); }} aria-label={G.exitFullscreen}>
              <RIcon.Shrink s={15} c="currentColor" />
              <span>{G.exitFullscreen}</span>
            </button>
          )}
          {boot === "loading" ? (
            <div className="boot__load">
              <RetoLogo size={28} />
              <div className="boot__name">{B.name}</div>
              <div className="boot__sys">{B.system}</div>
              {/* Bar is rendered always — `is-running` class triggers
                  the fill animation only after the user clicks Start.
                  See the matching CSS rule in reto.css.  */}
              <div className={"boot__track" + (bootStarted ? " is-running" : "")}><div className="boot__fill" /></div>
              <div className="boot__loading">{bootStarted ? B.loadingLabel + "…" : ""}</div>
              {/* Click-to-start button — replaces the previous auto-
                  advance setTimeout. Hidden after click while the bar
                  animates. */}
              {!bootStarted && (
                <button className="btn btn-primary boot__start" onClick={enterFsFromBoot}>
                  <RIcon.Bolt s={16} c="#fff" /><span>{B.start}</span><span className="arr">→</span>
                </button>
              )}
            </div>
          ) : boot === "brief1" ? (
            <div className="boot__brief">
              <span className="eyebrow">{B.tag} · {B.system}</span>
              <h2 className="boot__title">{B.name}</h2>
              <p className="boot__intro">{B.intro}</p>
              <div className="boot__sec">
                <div className="boot__sec-h">{B.howTitle}</div>
                <ol className="boot__rules">{B.rules.map((r, i) => <li key={i}>{r}</li>)}</ol>
              </div>
              <div className="boot__nav">
                <span className="boot__step">1 / 2</span>
                <button className="btn btn-primary boot__start" onClick={() => setBoot("brief2")}><span>{B.next}</span><span className="arr">→</span></button>
              </div>
            </div>
          ) : (
            <div className="boot__brief">
              <span className="eyebrow">{B.tag} · {B.system}</span>
              <h2 className="boot__title boot__title--sm">{B.termsTitle}</h2>
              <ul className="boot__terms-list">{B.terms.map((tm, i) => <li key={i}><span className="boot__term-k">{tm.k}</span><span className="boot__term-v">{tm.v}</span></li>)}</ul>
              <div className="boot__nav">
                <button className="boot__backbtn" onClick={() => setBoot("brief1")}>← {B.back}</button>
                <span className="boot__step">2 / 2</span>
                <button className="btn btn-primary boot__start" onClick={startGame}><RIcon.Bolt s={16} c="#fff" /><span>{B.start}</span><span className="arr">→</span></button>
              </div>
            </div>
          )}
        </div>
      ); })()}

      {showIntro && boot === "done" && !cleared && !alarm && (
        <div className="game-intro" onClick={(e) => e.stopPropagation()}>
          <div className="game-intro__card"><span className="eyebrow">{round.name[lang]}</span><p className="game-intro__txt">{round.intro[lang]}</p><button className="btn btn-primary game-intro__btn" onClick={dismissIntro}><span>{lang === "es" ? "Entendido" : "Got it"}</span><span className="arr">→</span></button></div>
        </div>
      )}

      {/* Guided tour */}
      {tour !== null && tourRect && (() => {
        const vw = window.innerWidth, vh = window.innerHeight;
        /* Per-step placement preference, indexed to TOUR_STEPS_DEF.
           default is "side" (sit beside the highlight spot — what the
           cooling + HUD steps need). The 3 rack-tray steps go ABOVE
           because the tray runs across the centre row; the rows step
           goes BELOW because the feeds span the top of the board.
           Energise (last, 7) is side-nudged-up so the card clears
           the viewport bottom. */
        const PREF = [
          "below", // 0 rows
          "above", // 1 racks intro
          "above", // 2 open a sheet (gated)
          "above", // 3 place a rack (gated)
          "side",  // 4 cooling intro
          "side",  // 5 add a cooling unit (gated)
          "side",  // 6 limits / HUD
          "side",  // 7 energise (last)
        ];
        const pref = PREF[tour] || "side";
        const CARD_W = 360;
        const CARD_H_EST = 220;
        const GAP = 18;
        const style = {};

        /* Right-pin override for the two rack-action steps:
             · step 2 ("Abra una ficha") ONCE the sheet is open — the
               sheet is a centred modal and the card on top of it
               interrupts the read.
             · step 3 ("Coloque un rack") ALWAYS — the player needs
               to see BOTH the rack tray (bottom) and the rows
               (top) at once to drag from one to the other; a
               centred card blocks the row area.
           Pinned to the FAR RIGHT of the viewport with vertical
           alignment to the spot, well clear of the play area. The
           rack tray's blue highlight ring stays as the focus signal. */
        const sheetOpenOnStep2 = (tour === 2 && sheet !== null);
        const rightPin = sheetOpenOnStep2 || tour === 3;
        if (rightPin) {
          style.left = vw - CARD_W - 24;
          const desiredTop = tourRect.top + tourRect.height / 2 - CARD_H_EST / 2;
          style.top  = Math.max(14, Math.min(desiredTop, vh - CARD_H_EST - 14));
        } else if (pref === "below") {
          /* Card sits below the highlight, centred horizontally on the
             spot then clamped to the viewport. */
          style.left = Math.max(14, Math.min(
            tourRect.left + tourRect.width / 2 - CARD_W / 2,
            vw - CARD_W - 14));
          style.top  = Math.min(vh - CARD_H_EST - 14,
            tourRect.top + tourRect.height + GAP);
        } else if (pref === "above") {
          /* Card sits above the highlight, centred horizontally on
             the spot then clamped to the viewport. The 3 rack-tray
             steps (1 racks intro, 2 open-sheet, 3 place-rack) get a
             much larger gap because the rendered card runs taller
             than CARD_H_EST — we want clear breathing room between
             the card and the rack tray. */
          const aboveGap = (tour === 1 || tour === 2 || tour === 3) ? 200 : GAP;
          style.left = Math.max(14, Math.min(
            tourRect.left + tourRect.width / 2 - CARD_W / 2,
            vw - CARD_W - 14));
          style.top  = Math.max(14, tourRect.top - CARD_H_EST - aboveGap);
        } else {
          /* "side" — prefer the side with more room, fall back to
             above/below if neither side fits. This is the existing
             cooling/HUD behaviour. */
          const onRight = tourRect.left + tourRect.width / 2 > vw / 2;
          const leftSpace  = tourRect.left - 14;
          const rightSpace = vw - (tourRect.left + tourRect.width) - 14;
          if (onRight && leftSpace >= CARD_W + GAP) {
            style.left = tourRect.left - CARD_W - GAP;
          } else if (!onRight && rightSpace >= CARD_W + GAP) {
            style.left = tourRect.left + tourRect.width + GAP;
          } else {
            style.left = Math.min(Math.max(tourRect.left, 14), vw - CARD_W - 14);
          }
          /* Vertical: centre on the spot, clamped — and for the
             energise step (7, last) lift it a touch further to keep
             the card clear of the viewport bottom edge. */
          const lift = (tour === 7) ? 48 : 0;
          const desiredTop = tourRect.top + tourRect.height / 2 - CARD_H_EST / 2 - lift;
          style.top = Math.max(14, Math.min(desiredTop, vh - CARD_H_EST - 14));
        }
        /* Gated steps don't show a Next button until the gate is
           met — then auto-advance kicks in (see useEffect above).
           While waiting, show a pulsing "Esperando…" label so the
           player knows the tutorial is paused on their action. */
        const isGated = !!(currentTourStep && currentTourStep.gate);
        const showWaiting = isGated && !tourGateMet;
        return (
          <div className="tour">
            <div className="tour__block" />
            <div className="tour__spot" style={{ top: tourRect.top - 6, left: tourRect.left - 6, width: tourRect.width + 12, height: tourRect.height + 12 }} />
            <div className="tour__card" style={style} onClick={(e) => e.stopPropagation()}>
              <span className="tour__step">{tour + 1} / {TOUR_SEL.length}</span>
              <div className="tour__title">{G.tour[tour].title}</div>
              <p className="tour__body">{G.tour[tour].body}</p>
              <div className="tour__actions">
                <button className="tour__skip" onClick={endTour}>{G.tourSkip}</button>
                {showWaiting ? (
                  <span className="tour__waiting"><span className="tour__waiting-dot" />{G.tourWaiting}</span>
                ) : (
                  <button className="btn btn-primary tour__next" onClick={tourNext}><span>{tour + 1 >= TOUR_SEL.length ? G.tourDone : G.tourNext}</span><span className="arr">→</span></button>
                )}
              </div>
            </div>
          </div>
        );
      })()}
      {showPue && (
        <div className="game-modal" onClick={() => setShowPue(false)}><div className="game-modal__card" onClick={(e) => e.stopPropagation()}><span className="eyebrow">{G.pueExplain}</span><p className="game-modal__txt">{G.pueExplainBody}</p><button className="game-modal__close" onClick={() => setShowPue(false)}><RIcon.Close s={20} c="currentColor" /></button></div></div>
      )}
      {showHarm && (
        <div className="game-modal" onClick={() => setShowHarm(false)}><div className="game-modal__card" onClick={(e) => e.stopPropagation()}><span className="eyebrow">{G.harmExplain}</span><p className="game-modal__txt">{G.harmExplainBody}</p><button className="game-modal__close" onClick={() => setShowHarm(false)}><RIcon.Close s={20} c="currentColor" /></button></div></div>
      )}
      {showSw && (
        <div className="game-modal" onClick={() => setShowSw(false)}><div className="game-modal__card" onClick={(e) => e.stopPropagation()}><span className="eyebrow">{G.softwareExplain}</span><p className="game-modal__txt">{G.softwareExplainBody}</p><button className="game-modal__close" onClick={() => setShowSw(false)}><RIcon.Close s={20} c="currentColor" /></button></div></div>
      )}
      {showPm && (
        <div className="game-modal" onClick={() => setShowPm(false)}><div className="game-modal__card" onClick={(e) => e.stopPropagation()}><span className="eyebrow">{G.pmExplain}</span><p className="game-modal__txt">{G.pmExplainBody}</p><button className="game-modal__close" onClick={() => setShowPm(false)}><RIcon.Close s={20} c="currentColor" /></button></div></div>
      )}
      {cleared && (
        <div className="game-clear" onClick={(e) => e.stopPropagation()}><div className="game-clear__card"><span className="game-clear__ic"><RIcon.Check s={26} c="#fff" /></span><div className="game-clear__t">{G.roundClear}</div><div className="game-clear__sub">{round.name[lang]}</div><button className="btn btn-primary game-clear__btn" onClick={next}><span>{roundIdx + 1 >= rounds.length ? G.energiseAll : G.next}</span><span className="arr">→</span></button></div></div>
      )}
      {alarm && (
        <div className="game-alarm" onClick={(e) => e.stopPropagation()}>
          <div className="game-alarm__rule" />
          <div className="game-alarm__card"><span className="game-alarm__ic"><RIcon.Alert s={30} c="currentColor" /></span><div className="game-alarm__t">{G.alarmTitle}</div><div className="game-alarm__sub">{G.alarmSub}</div><div className="game-alarm__reason"><span>{G.alarmReason}</span><b>{alarm.reason}</b></div><div className="game-alarm__restart">{G.alarmRestart}</div><button className="btn btn-outline-white game-alarm__btn" onClick={afterAlarm}><span>{G.alarmContinue}</span><span className="arr">→</span></button></div>
          <div className="game-alarm__rule" />
        </div>
      )}
    </div>
  );
};

Object.assign(window, { Game });
