<script lang="ts">
  import { onMount } from "svelte";
  import * as THREE from "three";
  import { DEFAULT_PRESSURE_RANGE_MAX, DEFAULT_PRESSURE_RANGE_MIN } from "$lib/config/pressure-range";
  import { pressureColorPalettes } from "$lib/config/color-map";
  import type { LocaleCode, PressureColorMapPreset } from "$lib/types/hud";

  type CornerId = "tl" | "tr" | "bl" | "br";

  interface CornerMap {
    tl: number;
    tr: number;
    bl: number;
    br: number;
  }

  interface Brick {
    mesh: THREE.Mesh<THREE.BoxGeometry, THREE.MeshStandardMaterial>;
    alive: boolean;
    left: number;
    right: number;
    top: number;
    bottom: number;
  }

  interface Copy {
    title: string;
    start: string;
    restart: string;
    pause: string;
    resume: string;
    running: string;
    paused: string;
    over: string;
    score: string;
    combo: string;
    lives: string;
    level: string;
    bricks: string;
    chase: string;
    pausedOverlay: string;
    online: string;
    local: string;
    createRoom: string;
    discoverRooms: string;
    joinRoom: string;
    pairingCode: string;
    joinCode: string;
    playerName: string;
    ready: string;
    cancelReady: string;
    disconnect: string;
    opponent: string;
    waitingOpponent: string;
    roomStatus: string;
    latency: string;
    noRooms: string;
    apiOffline: string;
    connected: string;
    connecting: string;
    hosting: string;
    discovering: string;
  }

  type LanStatus = "offline" | "discovering" | "hosting" | "connecting" | "lobby" | "running" | "error";
  type PlayerRole = "host" | "guest";
  type GamePhase = "lobby" | "countdown" | "running" | "paused" | "finished";

  interface LanRoom {
    roomId: string;
    pairingCode: string;
    hostName: string;
    address: string;
    port: number;
    players: number;
    maxPlayers: number;
    protocolVersion: number;
    appVersion: string;
  }

  interface LanPlayer {
    id: string;
    name: string;
    ready: boolean;
    role: PlayerRole;
    score?: number;
    lives?: number;
    latencyMs?: number;
  }

  interface JoinRoomResponse {
    roomId: string;
    playerId: string;
    ticket: string;
    wsUrl: string;
    pairingCode?: string;
  }

  interface CreateRoomResponse extends JoinRoomResponse {
    pairingCode: string;
    expiresAt?: number;
  }

  type ClientMessage =
    | {
        type: "hello";
        protocolVersion: 1;
        roomId: string;
        playerId: string;
        ticket: string;
        playerName: string;
      }
    | { type: "ready"; ready: boolean }
    | { type: "restart" }
    | {
        type: "input";
        seq: number;
        clientTime: number;
        axis: number;
        launch: boolean;
        pause: boolean;
      }
    | { type: "ping"; clientTime: number };

  type ServerMessage =
    | {
        type: "welcome";
        roomId: string;
        playerId: string;
        role: PlayerRole;
        tickRate: number;
        seed: number;
      }
    | { type: "lobby"; players: LanPlayer[] }
    | { type: "start"; startAt: number; seed: number }
    | {
        type: "snapshot";
        tick: number;
        serverTime: number;
        phase: GamePhase;
        players: Array<{
          id: string;
          name?: string;
          role?: PlayerRole;
          ready?: boolean;
          paddleX: number;
          score: number;
          lives: number;
          latencyMs?: number;
        }>;
        ball: { x: number; y: number; vx: number; vy: number };
        bricks: string;
      }
    | { type: "pong"; clientTime: number; serverTime: number }
    | { type: "error"; code: string; message: string };

  export let locale: LocaleCode = "zh-CN";
  export let pressureMatrix: number[] | null = null;
  export let matrixRows = 12;
  export let matrixCols = 7;
  export let rangeMin = DEFAULT_PRESSURE_RANGE_MIN;
  export let rangeMax = DEFAULT_PRESSURE_RANGE_MAX;
  export let colorMapPreset: PressureColorMapPreset = "emerald";

  const FIELD_HALF_W = 46;
  const FIELD_HALF_H = 62;
  const PADDLE_Y = -53;
  const PADDLE_W = 16;
  const PADDLE_H = 2.6;
  const PADDLE_SPEED = 74;
  const BALL_RADIUS = 1.45;
  const BASE_BALL_SPEED = 58;
  const MAX_BALL_SPEED = 106;
  const BRICK_COLS = 12;
  const BRICK_ROWS = 7;
  const BRICK_W = 6.2;
  const BRICK_H = 2.9;
  const BRICK_GAP_X = 0.8;
  const BRICK_GAP_Y = 0.9;
  const BRICK_TOP = 44;
  const START_DELAY_MS = 520;
  const PAUSE_COOLDOWN_MS = 640;
  const UPPER_STALL_THRESHOLD_MS = 1700;
  const UPPER_STALL_Y = 14;
  const ANTI_STALL_COOLDOWN_MS = 420;
  const MAX_BRICK_HITS_WITHOUT_PADDLE = 5;
  const LAN_PROTOCOL_VERSION = 1;
  const LAN_API_BASE_STORAGE_KEY = "je-skin-lan-api-base";
  const LAN_DEFAULT_API_BASE = "http://127.0.0.1:47888";
  const LAN_INPUT_INTERVAL_MS = 33;

  const copyByLocale: Record<LocaleCode, Copy> = {
    "zh-CN": {
      title: "NEON BREAKOUT",
      start: "开始",
      restart: "重开",
      pause: "暂停",
      resume: "继续",
      running: "运行中",
      paused: "已暂停",
      over: "结束",
      score: "得分",
      combo: "连击",
      lives: "生命",
      level: "关卡",
      bricks: "剩余",
      chase: "追击",
      pausedOverlay: "已暂停 / 顶部双角施压继续",
      online: "联机",
      local: "本地",
      createRoom: "创建房间",
      discoverRooms: "搜索",
      joinRoom: "加入",
      pairingCode: "配对码",
      joinCode: "输入配对码",
      playerName: "玩家名",
      ready: "准备",
      cancelReady: "取消准备",
      disconnect: "断开",
      opponent: "对手",
      waitingOpponent: "等待对手加入",
      roomStatus: "房间",
      latency: "延迟",
      noRooms: "暂无局域网房间",
      apiOffline: "联机后端未启动或不可访问",
      connected: "已连接",
      connecting: "连接中",
      hosting: "房主等待中",
      discovering: "搜索中"
    },
    "en-US": {
      title: "NEON BREAKOUT",
      start: "Start",
      restart: "Restart",
      pause: "Pause",
      resume: "Resume",
      running: "Running",
      paused: "Paused",
      over: "Game Over",
      score: "Score",
      combo: "Combo",
      lives: "Lives",
      level: "Level",
      bricks: "Bricks",
      chase: "Chase",
      pausedOverlay: "Paused / Top corners to resume",
      online: "Online",
      local: "Local",
      createRoom: "Create",
      discoverRooms: "Scan",
      joinRoom: "Join",
      pairingCode: "Code",
      joinCode: "Pairing code",
      playerName: "Name",
      ready: "Ready",
      cancelReady: "Cancel",
      disconnect: "Disconnect",
      opponent: "Opponent",
      waitingOpponent: "Waiting for opponent",
      roomStatus: "Room",
      latency: "Ping",
      noRooms: "No LAN rooms found",
      apiOffline: "LAN backend is offline or unreachable",
      connected: "Connected",
      connecting: "Connecting",
      hosting: "Hosting",
      discovering: "Scanning"
    }
  };

  let hostEl: HTMLElement | undefined;
  let canvasEl: HTMLCanvasElement | undefined;
  let rafId: number | null = null;
  let renderer: THREE.WebGLRenderer | null = null;
  let scene: THREE.Scene | null = null;
  let camera: THREE.OrthographicCamera | null = null;
  let paddle: THREE.Mesh<THREE.BoxGeometry, THREE.MeshStandardMaterial> | null = null;
  let ball: THREE.Mesh<THREE.SphereGeometry, THREE.MeshStandardMaterial> | null = null;
  let brickGroup: THREE.Group | null = null;
  let borderLine: THREE.LineLoop | null = null;
  let bgPlane: THREE.Mesh<THREE.PlaneGeometry, THREE.MeshBasicMaterial> | null = null;
  let ambientLight: THREE.AmbientLight | null = null;
  let keyLight: THREE.PointLight | null = null;
  let rimLight: THREE.PointLight | null = null;
  let flashSprite: THREE.Sprite | null = null;
  let flashLife = 0;
  let flashMaxLife = 0.2;
  let lanSocket: WebSocket | null = null;
  let lanMode: "local" | "online" = "local";
  let lanStatus: LanStatus = "offline";
  let lanRooms: LanRoom[] = [];
  let lanNotice = "";
  let lanApiBase = LAN_DEFAULT_API_BASE;
  let playerName = "Player";
  let joinCode = "";
  let pairingCode = "";
  let roomId = "";
  let localPlayerId = "";
  let localRole: PlayerRole | null = null;
  let localReady = false;
  let lanPlayers: LanPlayer[] = [];
  let pingMs: number | null = null;
  let lastLanInputAt = 0;
  let lanInputSeq = 0;
  let intentionalLanDisconnect = false;
  let lanLaunchAt = 0;

  let keyLeft = false;
  let keyRight = false;
  let ballLaunched = false;
  let nextLaunchAt = 0;
  let paddleX = 0;
  const ballPos = new THREE.Vector2(0, 0);
  const ballVel = new THREE.Vector2(0, 0);
  let bricks: Brick[] = [];

  let gameState: "idle" | "running" | "paused" | "over" = "idle";
  let score = 0;
  let combo = 0;
  let lives = 3;
  let level = 1;
  let bricksLeft = 0;
  let lastFrameTs = 0;
  let prevPauseGesture = false;
  let pauseGestureLockUntil = 0;
  let themedBgTexture: THREE.CanvasTexture | null = null;
  let lastPaddleContactAt = 0;
  let bricksSincePaddle = 0;
  let lastAntiStallAt = 0;

  let cornerForce: CornerMap = { tl: 0, tr: 0, bl: 0, br: 0 };
  $: cornerForce = readCorners(pressureMatrix, matrixRows, matrixCols);
  $: themePalette = pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
  $: ui = copyByLocale[locale];
  $: leftForce = cornerForce.tl + cornerForce.bl;
  $: rightForce = cornerForce.tr + cornerForce.br;
  $: topForce = cornerForce.tl + cornerForce.tr;
  $: pauseGestureThreshold = Math.max(420, Math.round(Math.max(1000, rangeMax - rangeMin) * 0.07));
  $: statusText =
    gameState === "running" ? ui.running : gameState === "paused" ? ui.paused : gameState === "over" ? ui.over : ui.start;
  $: isLanBusy = lanStatus === "discovering" || lanStatus === "hosting" || lanStatus === "connecting";
  $: isLanConnected = lanStatus === "lobby" || lanStatus === "running";
  $: selfPlayer = lanPlayers.find((player) => player.id === localPlayerId) ?? null;
  $: opponentPlayers = lanPlayers.filter((player) => player.id !== localPlayerId);
  $: primaryOpponent = opponentPlayers[0] ?? null;
  $: roomStatusText =
    lanStatus === "discovering"
      ? ui.discovering
      : lanStatus === "hosting"
        ? ui.hosting
        : lanStatus === "connecting"
          ? ui.connecting
          : isLanConnected
            ? ui.connected
            : lanStatus === "error"
              ? ui.apiOffline
              : ui.local;

  $: if (scene && renderer) {
    applyTheme();
  }

  function rgbTripletToColor(rgbTriplet: string): THREE.Color {
    return new THREE.Color(`rgb(${rgbTriplet.replace(/\s+/g, ", ")})`);
  }

  function rgbaFromTriplet(rgbTriplet: string, alpha: number): string {
    return `rgba(${rgbTriplet.replace(/\s+/g, ",")},${alpha})`;
  }

  function themedBrickPalette(): string[] {
    const { rangeStops } = themePalette;
    return [rangeStops[1], rangeStops[2], rangeStops[3], rangeStops[4], rangeStops[5], rangeStops[2], rangeStops[3]];
  }

  function applyTheme(): void {
    if (!scene || !renderer) return;
    const uiTheme = themePalette.uiTheme;
    scene.background = new THREE.Color(uiTheme.bg30);

    ambientLight?.color.copy(rgbTripletToColor(uiTheme.textMainRgb));
    keyLight?.color.copy(rgbTripletToColor(uiTheme.glowAltRgb));
    rimLight?.color.copy(rgbTripletToColor(uiTheme.glowRgb));

    const borderMaterial = borderLine?.material as THREE.LineBasicMaterial | undefined;
    if (borderMaterial) {
      borderMaterial.color.copy(rgbTripletToColor(uiTheme.glowRgb));
      borderMaterial.opacity = 0.48;
      borderMaterial.needsUpdate = true;
    }

    if (paddle) {
      paddle.material.color.copy(new THREE.Color(themePalette.rangeStops[2]));
      paddle.material.emissive.copy(new THREE.Color(themePalette.rangeStops[2]));
      paddle.material.emissiveIntensity = 0.72;
      paddle.material.needsUpdate = true;
    }

    if (ball) {
      ball.material.color.copy(new THREE.Color(themePalette.rangeStops[0]));
      ball.material.emissive.copy(new THREE.Color(themePalette.rangeStops[3]));
      ball.material.emissiveIntensity = 0.92;
      ball.material.needsUpdate = true;
    }

    if (bgPlane) {
      const material = bgPlane.material;
      themedBgTexture?.dispose();
      themedBgTexture = createGridTexture(themePalette);
      material.map = themedBgTexture;
      material.opacity = 0.78;
      material.needsUpdate = true;
    }

    const colors = themedBrickPalette();
    for (let index = 0; index < bricks.length; index += 1) {
      const brick = bricks[index];
      const color = colors[index % colors.length] ?? "#7ad0ff";
      brick.mesh.material.color.set(color);
      brick.mesh.material.emissive.set(color);
      brick.mesh.material.emissiveIntensity = 0.7;
      brick.mesh.material.needsUpdate = true;
    }
  }

  function getLanApiBase(): string {
    if (typeof window === "undefined") {
      return LAN_DEFAULT_API_BASE;
    }

    return window.localStorage.getItem(LAN_API_BASE_STORAGE_KEY)?.trim() || LAN_DEFAULT_API_BASE;
  }

  function normalizePairingCode(value: string): string {
    return value.replace(/\D/g, "").slice(0, 8);
  }

  function makeDefaultPlayerName(): string {
    if (typeof window === "undefined") {
      return "Player";
    }

    const saved = window.localStorage.getItem("je-skin-lan-player-name")?.trim();
    if (saved) {
      return saved.slice(0, 18);
    }

    return `JE-${Math.floor(1000 + Math.random() * 9000)}`;
  }

  function persistPlayerName(): void {
    if (typeof window === "undefined") {
      return;
    }

    const safeName = playerName.trim().slice(0, 18) || makeDefaultPlayerName();
    playerName = safeName;
    window.localStorage.setItem("je-skin-lan-player-name", safeName);
  }

  async function lanFetch<T>(path: string, init?: RequestInit): Promise<T> {
    const response = await fetch(`${lanApiBase}${path}`, {
      ...init,
      headers: {
        "content-type": "application/json",
        ...(init?.headers ?? {})
      }
    });

    if (!response.ok) {
      let message = "";
      try {
        const body = (await response.json()) as { message?: string };
        message = body.message ?? "";
      } catch {
        message = "";
      }

      if (response.status === 404) {
        throw new Error(locale === "zh-CN" ? "未找到这个配对码，请确认房主已创建房间。" : "Pairing code not found. Make sure the host created a room.");
      }

      throw new Error(message || `LAN API ${response.status}`);
    }

    return (await response.json()) as T;
  }

  function setLanError(error: unknown): void {
    lanStatus = "error";
    lanNotice = error instanceof Error ? error.message : ui.apiOffline;
    console.error("[lan] operation failed:", error);
  }

  async function discoverLanRooms(): Promise<void> {
    lanMode = "online";
    lanStatus = "discovering";
    lanNotice = "";
    lanApiBase = getLanApiBase();

    try {
      const result = await lanFetch<{ rooms: LanRoom[] }>("/lan/rooms");
      lanRooms = result.rooms ?? [];
      lanStatus = "offline";
      lanNotice = lanRooms.length ? "" : ui.noRooms;
    } catch (error) {
      lanRooms = [];
      setLanError(error);
    }
  }

  async function createLanRoom(): Promise<void> {
    lanMode = "online";
    lanStatus = "hosting";
    lanNotice = "";
    persistPlayerName();
    lanApiBase = getLanApiBase();

    try {
      const result = await lanFetch<CreateRoomResponse>("/lan/rooms", {
        method: "POST",
        body: JSON.stringify({
          playerName,
          protocolVersion: LAN_PROTOCOL_VERSION
        })
      });

      await connectLanSocket(result);
      pairingCode = result.pairingCode;
    } catch (error) {
      setLanError(error);
    }
  }

  async function joinLanRoom(code = joinCode): Promise<void> {
    const normalizedCode = normalizePairingCode(code);
    if (!normalizedCode) {
      return;
    }

    joinCode = normalizedCode;
    lanMode = "online";
    lanStatus = "connecting";
    lanNotice = "";
    persistPlayerName();
    lanApiBase = getLanApiBase();

    try {
      const result = await lanFetch<JoinRoomResponse>("/lan/join", {
        method: "POST",
        body: JSON.stringify({
          pairingCode: normalizedCode,
          playerName,
          protocolVersion: LAN_PROTOCOL_VERSION
        })
      });

      await connectLanSocket(result);
      pairingCode = result.pairingCode ?? normalizedCode;
    } catch (error) {
      setLanError(error);
    }
  }

  async function connectLanSocket(result: JoinRoomResponse): Promise<void> {
    disconnectLanSocket(false);
    intentionalLanDisconnect = false;
    roomId = result.roomId;
    localPlayerId = result.playerId;
    lanStatus = "connecting";

    await new Promise<void>((resolve, reject) => {
      const socket = new WebSocket(result.wsUrl);
      lanSocket = socket;

      socket.onopen = () => {
        sendLanMessage({
          type: "hello",
          protocolVersion: LAN_PROTOCOL_VERSION,
          roomId: result.roomId,
          playerId: result.playerId,
          ticket: result.ticket,
          playerName
        });
        resolve();
      };

      socket.onerror = () => {
        reject(new Error("WebSocket connect failed"));
      };

      socket.onmessage = (event) => {
        handleLanMessage(JSON.parse(event.data) as ServerMessage);
      };

      socket.onclose = () => {
        if (!intentionalLanDisconnect) {
          lanStatus = "offline";
          lanNotice = locale === "zh-CN" ? "联机连接已断开。" : "LAN connection closed.";
        }
        lanSocket = null;
      };
    });
  }

  function disconnectLanSocket(resetMode = true): void {
    intentionalLanDisconnect = true;
    lanSocket?.close(1000, "client disconnect");
    lanSocket = null;
    lanStatus = "offline";
    localReady = false;
    localRole = null;
    localPlayerId = "";
    roomId = "";
    pairingCode = "";
    pingMs = null;
    lanPlayers = [];
    lanLaunchAt = 0;
    if (resetMode) {
      lanMode = "local";
      lanNotice = "";
    }
  }

  function sendLanMessage(message: ClientMessage): void {
    if (lanSocket?.readyState !== WebSocket.OPEN) {
      return;
    }

    lanSocket.send(JSON.stringify(message));
  }

  function handleLanMessage(message: ServerMessage): void {
    if (message.type === "welcome") {
      roomId = message.roomId;
      localPlayerId = message.playerId;
      localRole = message.role;
      lanStatus = "lobby";
      lanNotice = "";
      return;
    }

    if (message.type === "lobby") {
      lanPlayers = message.players;
      localReady = lanPlayers.find((player) => player.id === localPlayerId)?.ready ?? localReady;
      lanStatus = "lobby";
      return;
    }

    if (message.type === "start") {
      lanStatus = "running";
      if (gameState !== "running") {
        startGame();
      }
      return;
    }

    if (message.type === "snapshot") {
      lanStatus = message.phase === "running" ? "running" : "lobby";
      applyLanSnapshot(message);
      return;
    }

    if (message.type === "pong") {
      pingMs = Math.max(0, Math.round(performance.now() - message.clientTime));
      return;
    }

    if (message.type === "error") {
      lanNotice = message.message;
      lanStatus = "error";
    }
  }

  function applyLanSnapshot(snapshot: Extract<ServerMessage, { type: "snapshot" }>): void {
    const phaseToState: Record<GamePhase, typeof gameState> = {
      lobby: "idle",
      countdown: "idle",
      running: "running",
      paused: "paused",
      finished: "over"
    };

    const prevGameState = gameState;
    gameState = phaseToState[snapshot.phase];

    // Detect transition to running → schedule launch after delay
    if (gameState === "running" && prevGameState !== "running" && lanLaunchAt === 0) {
      lanLaunchAt = performance.now() + START_DELAY_MS;
    }

    // Reset launch timer when game ends or returns to lobby
    if (gameState === "over" || gameState === "idle") {
      lanLaunchAt = 0;
    }
    ballPos.set(snapshot.ball.x, snapshot.ball.y);
    ballVel.set(snapshot.ball.vx, snapshot.ball.vy);
    ball?.position.set(ballPos.x, ballPos.y, 1.2);

    const playersById = new Map(lanPlayers.map((player) => [player.id, player]));
    lanPlayers = snapshot.players.map((player, index) => {
      const previous = playersById.get(player.id);
      return {
        id: player.id,
        name: player.name ?? previous?.name ?? (index === 0 ? "Host" : "Guest"),
        role: player.role ?? previous?.role ?? (index === 0 ? "host" : "guest"),
        ready: player.ready ?? previous?.ready ?? true,
        score: player.score,
        lives: player.lives,
        latencyMs: player.latencyMs ?? previous?.latencyMs
      };
    });

    const selfSnapshot = snapshot.players.find((player) => player.id === localPlayerId) ?? snapshot.players[0];
    if (selfSnapshot) {
      paddleX = Math.min(FIELD_HALF_W - PADDLE_W * 0.5, Math.max(-FIELD_HALF_W + PADDLE_W * 0.5, selfSnapshot.paddleX));
      paddle?.position.setX(paddleX);
      score = selfSnapshot.score;
      lives = selfSnapshot.lives;
    }

    applyBrickMask(snapshot.bricks);
  }

  function applyBrickMask(mask: string): void {
    if (!mask || !bricks.length) {
      return;
    }

    let aliveCount = 0;
    for (let index = 0; index < bricks.length; index += 1) {
      const alive = mask[index] !== "0";
      bricks[index].alive = alive;
      bricks[index].mesh.visible = alive;
      if (alive) {
        aliveCount += 1;
      }
    }
    bricksLeft = aliveCount;
  }

  function requestLanRestart(): void {
    if (isLanConnected) {
      sendLanMessage({ type: "restart" });
    } else {
      startGame();
    }
  }

  function toggleLanReady(): void {
    if (!isLanConnected) {
      return;
    }

    localReady = !localReady;
    sendLanMessage({ type: "ready", ready: localReady });
  }

  function sendLanInput(nowMs: number, axis: number, launch: boolean, pause: boolean): void {
    if (!isLanConnected || nowMs - lastLanInputAt < LAN_INPUT_INTERVAL_MS) {
      return;
    }

    lastLanInputAt = nowMs;
    sendLanMessage({
      type: "input",
      seq: ++lanInputSeq,
      clientTime: nowMs,
      axis,
      launch,
      pause
    });

    if (lanInputSeq % 60 === 0) {
      sendLanMessage({ type: "ping", clientTime: nowMs });
    }
  }

  onMount(() => {
    if (!hostEl || !canvasEl) return;
    lanApiBase = getLanApiBase();
    playerName = makeDefaultPlayerName();
    const uiTheme = themePalette.uiTheme;
    const paddleColor = themePalette.rangeStops[2];
    const ballColor = themePalette.rangeStops[0];
    const ballGlow = themePalette.rangeStops[3];

    const nextScene = new THREE.Scene();
    nextScene.background = new THREE.Color(uiTheme.bg30);
    scene = nextScene;

    const nextCamera = new THREE.OrthographicCamera(-60, 60, 70, -70, 0.1, 200);
    nextCamera.position.set(0, 0, 54);
    nextCamera.lookAt(0, 0, 0);
    camera = nextCamera;

    const nextRenderer = new THREE.WebGLRenderer({
      canvas: canvasEl,
      antialias: true,
      alpha: true,
      powerPreference: "high-performance"
    });
    renderer = nextRenderer;
    nextRenderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));

    const ambient = new THREE.AmbientLight(rgbTripletToColor(uiTheme.textMainRgb), 0.44);
    const key = new THREE.PointLight(rgbTripletToColor(uiTheme.glowAltRgb), 1.2, 180, 1.8);
    key.position.set(0, 20, 38);
    const rim = new THREE.PointLight(rgbTripletToColor(uiTheme.glowRgb), 0.65, 160, 1.7);
    rim.position.set(-22, -14, 26);
    nextScene.add(ambient, key, rim);
    ambientLight = ambient;
    keyLight = key;
    rimLight = rim;

    const bgTexture = createGridTexture(themePalette);
    themedBgTexture = bgTexture;
    const bg = new THREE.Mesh(
      new THREE.PlaneGeometry(160, 170),
      new THREE.MeshBasicMaterial({
        map: bgTexture,
        transparent: true,
        opacity: 0.78,
        depthWrite: false
      })
    );
    bg.position.set(0, 0, -12);
    nextScene.add(bg);
    bgPlane = bg;

    const border = new THREE.LineLoop(
      new THREE.BufferGeometry().setFromPoints([
        new THREE.Vector3(-FIELD_HALF_W, -FIELD_HALF_H, 0),
        new THREE.Vector3(FIELD_HALF_W, -FIELD_HALF_H, 0),
        new THREE.Vector3(FIELD_HALF_W, FIELD_HALF_H, 0),
        new THREE.Vector3(-FIELD_HALF_W, FIELD_HALF_H, 0)
      ]),
      new THREE.LineBasicMaterial({ color: rgbTripletToColor(uiTheme.glowRgb), transparent: true, opacity: 0.46 })
    );
    nextScene.add(border);
    borderLine = border;

    const nextBrickGroup = new THREE.Group();
    brickGroup = nextBrickGroup;
    nextScene.add(nextBrickGroup);

    const paddleMesh = new THREE.Mesh(
      new THREE.BoxGeometry(PADDLE_W, PADDLE_H, 2.1),
      new THREE.MeshStandardMaterial({
        color: paddleColor,
        emissive: paddleColor,
        emissiveIntensity: 0.72,
        roughness: 0.34,
        metalness: 0.18
      })
    );
    paddleMesh.position.set(0, PADDLE_Y, 0.4);
    nextScene.add(paddleMesh);
    paddle = paddleMesh;

    const ballMesh = new THREE.Mesh(
      new THREE.SphereGeometry(BALL_RADIUS, 22, 16),
      new THREE.MeshStandardMaterial({
        color: ballColor,
        emissive: ballGlow,
        emissiveIntensity: 0.88,
        roughness: 0.2,
        metalness: 0.04
      })
    );
    nextScene.add(ballMesh);
    ball = ballMesh;

    const flashTexture = createFlashTexture(themePalette);
    const flash = new THREE.Sprite(
      new THREE.SpriteMaterial({
        map: flashTexture,
        color: "#74ffff",
        transparent: true,
        blending: THREE.AdditiveBlending,
        opacity: 0,
        depthWrite: false
      })
    );
    flash.scale.set(1, 1, 1);
    flash.visible = false;
    nextScene.add(flash);
    flashSprite = flash;

    const onKeyDown = (event: KeyboardEvent) => {
      if (event.code === "ArrowLeft" || event.code === "KeyA") keyLeft = true;
      if (event.code === "ArrowRight" || event.code === "KeyD") keyRight = true;
      if (event.code === "Space" && gameState !== "running") startGame();
    };
    const onKeyUp = (event: KeyboardEvent) => {
      if (event.code === "ArrowLeft" || event.code === "KeyA") keyLeft = false;
      if (event.code === "ArrowRight" || event.code === "KeyD") keyRight = false;
    };

    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);

    const resize = () => {
      if (!hostEl || !renderer || !camera) return;
      const width = hostEl.clientWidth;
      const height = hostEl.clientHeight;
      if (width <= 0 || height <= 0) return;
      renderer.setSize(width, height, false);
      const aspect = width / Math.max(1, height);
      const halfH = 70;
      const halfW = halfH * aspect;
      camera.left = -halfW;
      camera.right = halfW;
      camera.top = halfH;
      camera.bottom = -halfH;
      camera.updateProjectionMatrix();
    };

    const observer = new ResizeObserver(() => resize());
    observer.observe(hostEl);
    resize();

    resetGameToIdle();
    lastFrameTs = performance.now();
    const loop = (ts: number) => {
      const dt = Math.min((ts - lastFrameTs) / 1000, 0.034);
      lastFrameTs = ts;
      update(dt, ts);
      renderer?.render(nextScene, nextCamera);
      rafId = window.requestAnimationFrame(loop);
    };
    rafId = window.requestAnimationFrame(loop);

    return () => {
      if (rafId != null) window.cancelAnimationFrame(rafId);
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("keyup", onKeyUp);
      observer.disconnect();
      disposeBricks();
      border.geometry.dispose();
      (border.material as THREE.LineBasicMaterial).dispose();
      bg.geometry.dispose();
      (bg.material as THREE.MeshBasicMaterial).map?.dispose();
      (bg.material as THREE.MeshBasicMaterial).dispose();
      paddleMesh.geometry.dispose();
      paddleMesh.material.dispose();
      ballMesh.geometry.dispose();
      ballMesh.material.dispose();
      (flash.material as THREE.SpriteMaterial).map?.dispose();
      (flash.material as THREE.SpriteMaterial).dispose();
      disconnectLanSocket(false);
      renderer?.dispose();
    };
  });

  function createGridTexture(palette = themePalette): THREE.CanvasTexture {
    const canvas = document.createElement("canvas");
    canvas.width = 512;
    canvas.height = 512;
    const ctx = canvas.getContext("2d");
    if (!ctx) return new THREE.CanvasTexture(canvas);

    const { uiTheme } = palette;
    const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
    gradient.addColorStop(0, uiTheme.bg20);
    gradient.addColorStop(0.5, uiTheme.bg10);
    gradient.addColorStop(1, uiTheme.bg30);
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.strokeStyle = rgbaFromTriplet(uiTheme.glowRgb, 0.18);
    ctx.lineWidth = 1;
    for (let y = 0; y < canvas.height; y += 24) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(canvas.width, y);
      ctx.stroke();
    }
    ctx.strokeStyle = rgbaFromTriplet(uiTheme.glowAltRgb, 0.08);
    for (let x = 0; x < canvas.width; x += 24) {
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, canvas.height);
      ctx.stroke();
    }

    const texture = new THREE.CanvasTexture(canvas);
    texture.colorSpace = THREE.SRGBColorSpace;
    texture.needsUpdate = true;
    return texture;
  }

  function createFlashTexture(palette = themePalette): THREE.CanvasTexture {
    const canvas = document.createElement("canvas");
    canvas.width = 128;
    canvas.height = 128;
    const ctx = canvas.getContext("2d");
    if (!ctx) return new THREE.CanvasTexture(canvas);
    const { uiTheme } = palette;
    const gradient = ctx.createRadialGradient(64, 64, 8, 64, 64, 64);
    gradient.addColorStop(0, "rgba(255,255,255,1)");
    gradient.addColorStop(0.25, rgbaFromTriplet(uiTheme.glowAltRgb, 0.86));
      gradient.addColorStop(1, rgbaFromTriplet(uiTheme.glowRgb, 0));
      ctx.fillStyle = gradient;
      ctx.fillRect(0, 0, 128, 128);
    const texture = new THREE.CanvasTexture(canvas);
    texture.colorSpace = THREE.SRGBColorSpace;
    texture.needsUpdate = true;
    return texture;
  }

  function rand(min: number, max: number): number {
    return Math.random() * (max - min) + min;
  }

  function disposeBricks(): void {
    if (!brickGroup) return;
    for (const brick of bricks) {
      brick.mesh.geometry.dispose();
      brick.mesh.material.dispose();
      brickGroup.remove(brick.mesh);
    }
    bricks = [];
    bricksLeft = 0;
  }

  function buildBricks(): void {
    if (!brickGroup) return;
    disposeBricks();
    const palette = themedBrickPalette();
    const totalWidth = BRICK_COLS * BRICK_W + (BRICK_COLS - 1) * BRICK_GAP_X;
    const startX = -totalWidth / 2 + BRICK_W / 2;
    for (let row = 0; row < BRICK_ROWS; row += 1) {
      for (let col = 0; col < BRICK_COLS; col += 1) {
        const color = palette[(row + col + level) % palette.length] ?? "#7ad0ff";
        const material = new THREE.MeshStandardMaterial({
          color,
          emissive: color,
          emissiveIntensity: 0.7,
          roughness: 0.35,
          metalness: 0.15
        });
        const mesh = new THREE.Mesh(new THREE.BoxGeometry(BRICK_W, BRICK_H, 2.4), material);
        const x = startX + col * (BRICK_W + BRICK_GAP_X);
        const y = BRICK_TOP - row * (BRICK_H + BRICK_GAP_Y);
        mesh.position.set(x, y, 0.2);
        brickGroup.add(mesh);
        bricks.push({
          mesh,
          alive: true,
          left: x - BRICK_W / 2,
          right: x + BRICK_W / 2,
          top: y + BRICK_H / 2,
          bottom: y - BRICK_H / 2
        });
      }
    }
    bricksLeft = bricks.length;
  }

  function launchBall(nowMs: number): void {
    const speed = Math.min(MAX_BALL_SPEED, BASE_BALL_SPEED + (level - 1) * 4.2);
    const directionX = rand(-0.72, 0.72);
    ballVel.set(directionX, 1).normalize().multiplyScalar(speed);
    ballLaunched = true;
    nextLaunchAt = nowMs;
    lastPaddleContactAt = nowMs;
    lastAntiStallAt = nowMs;
    bricksSincePaddle = 0;
  }

  function prepareBall(nowMs: number, delayMs: number): void {
    ballLaunched = false;
    ballVel.set(0, 0);
    ballPos.set(paddleX, PADDLE_Y + PADDLE_H * 0.5 + BALL_RADIUS + 0.4);
    nextLaunchAt = nowMs + delayMs;
    bricksSincePaddle = 0;
  }

  function resetGameToIdle(): void {
    if (!paddle || !ball) return;
    score = 0;
    combo = 0;
    lives = 3;
    level = 1;
    gameState = "idle";
    prevPauseGesture = false;
    pauseGestureLockUntil = 0;
    lastPaddleContactAt = performance.now();
    lastAntiStallAt = 0;
    bricksSincePaddle = 0;
    paddleX = 0;
    paddle.position.x = 0;
    buildBricks();
    prepareBall(performance.now(), Number.POSITIVE_INFINITY);
    ball.position.set(ballPos.x, ballPos.y, 1.2);
  }

  function startGame(): void {
    if (!paddle || !ball) return;
    resetGameToIdle();
    gameState = "running";
    prepareBall(performance.now(), START_DELAY_MS);
  }

  function restartGame(): void {
    startGame();
  }

  function togglePause(): void {
    if (gameState === "running") {
      gameState = "paused";
      return;
    }
    if (gameState === "paused") {
      gameState = "running";
      lastFrameTs = performance.now();
    }
  }

  function readCorners(matrix: number[] | null, rows: number, cols: number): CornerMap {
    if (!matrix || matrix.length === 0) return { tl: 0, tr: 0, bl: 0, br: 0 };
    const r = Math.max(Math.floor(rows), 1);
    const c = Math.max(Math.floor(cols), 1);
    const rs = Math.min(2, r);
    const cs = Math.min(2, c);
    const avg = (r0: number, r1: number, c0: number, c1: number): number => {
      let sum = 0;
      let count = 0;
      for (let i = r0; i < r1; i += 1) {
        for (let j = c0; j < c1; j += 1) {
          const v = Number(matrix[i * c + j] ?? 0);
          if (!Number.isFinite(v)) continue;
          sum += Math.max(0, v);
          count += 1;
        }
      }
      return count ? sum / count : 0;
    };
    return {
      tl: avg(0, rs, 0, cs),
      tr: avg(0, rs, c - cs, c),
      bl: avg(r - rs, r, 0, cs),
      br: avg(r - rs, r, c - cs, c)
    };
  }

  function sensorControlAxis(): number {
    const span = Math.max(1200, (rangeMax - rangeMin) * 0.22);
    const raw = (rightForce - leftForce) / span;
    if (Math.abs(raw) < 0.045) return 0;
    return Math.min(1, Math.max(-1, raw));
  }

  function handleSensorPauseGesture(nowMs: number): void {
    if (gameState !== "idle" && gameState !== "running" && gameState !== "paused") {
      prevPauseGesture = false;
      return;
    }

    const active = topForce >= pauseGestureThreshold;
    if (active && !prevPauseGesture && nowMs >= pauseGestureLockUntil) {
      if (gameState === "idle") {
        startGame();
      } else {
        togglePause();
      }
      pauseGestureLockUntil = nowMs + PAUSE_COOLDOWN_MS;
    }
    prevPauseGesture = active;
  }

  function update(dt: number, nowMs: number): void {
    if (!paddle || !ball) return;
    handleSensorPauseGesture(nowMs);

    const keyAxis = (keyRight ? 1 : 0) - (keyLeft ? 1 : 0);
    const axis = keyAxis !== 0 ? keyAxis : sensorControlAxis();
    const shouldLaunch = lanLaunchAt > 0 && nowMs >= lanLaunchAt;
    const launchIntent = shouldLaunch || (gameState !== "running" && topForce >= pauseGestureThreshold);
    const pauseIntent = topForce >= pauseGestureThreshold;
    sendLanInput(nowMs, axis, launchIntent, pauseIntent);

    // Reset launch timer after sending
    if (shouldLaunch) {
      lanLaunchAt = 0;
    }

    if (gameState !== "running") {
      ball.position.set(ballPos.x, ballPos.y, 1.2);
      updateFlash(dt);
      return;
    }

    if (!isLanConnected) {
      paddleX += axis * PADDLE_SPEED * dt;
      paddleX = Math.min(FIELD_HALF_W - PADDLE_W * 0.5, Math.max(-FIELD_HALF_W + PADDLE_W * 0.5, paddleX));
      paddle.position.x = paddleX;
    }

    if (!ballLaunched) {
      ballPos.set(paddleX, PADDLE_Y + PADDLE_H * 0.5 + BALL_RADIUS + 0.4);
      if (nowMs >= nextLaunchAt) launchBall(nowMs);
    } else if (!isLanConnected) {
      ballPos.addScaledVector(ballVel, dt);
      resolveWallCollision(nowMs);
      resolvePaddleCollision();
      resolveBrickCollision(nowMs);
      applyAntiStall(nowMs);
    }

    ball.position.set(ballPos.x, ballPos.y, 1.2);
    updateFlash(dt);
  }

  function resolveWallCollision(nowMs: number): void {
    if (ballPos.x - BALL_RADIUS <= -FIELD_HALF_W) {
      ballPos.x = -FIELD_HALF_W + BALL_RADIUS;
      ballVel.x = Math.abs(ballVel.x);
    } else if (ballPos.x + BALL_RADIUS >= FIELD_HALF_W) {
      ballPos.x = FIELD_HALF_W - BALL_RADIUS;
      ballVel.x = -Math.abs(ballVel.x);
    }

    if (ballPos.y + BALL_RADIUS >= FIELD_HALF_H) {
      ballPos.y = FIELD_HALF_H - BALL_RADIUS;
      ballVel.y = -Math.abs(ballVel.y);
      if (nowMs - lastPaddleContactAt > UPPER_STALL_THRESHOLD_MS * 0.6) {
        forceBallDownward(0.62);
      }
    }

    if (ballPos.y - BALL_RADIUS < -FIELD_HALF_H) {
      lives -= 1;
      combo = 0;
      if (lives <= 0) {
        gameState = "over";
        ballLaunched = false;
      } else {
        prepareBall(nowMs, 440);
      }
    }
  }

  function resolvePaddleCollision(): void {
    if (!ballLaunched || ballVel.y >= 0) return;
    const paddleTop = PADDLE_Y + PADDLE_H * 0.5;
    const paddleBottom = PADDLE_Y - PADDLE_H * 0.5;
    const left = paddleX - PADDLE_W * 0.5;
    const right = paddleX + PADDLE_W * 0.5;
    const hit =
      ballPos.y - BALL_RADIUS <= paddleTop &&
      ballPos.y + BALL_RADIUS >= paddleBottom &&
      ballPos.x >= left - BALL_RADIUS &&
      ballPos.x <= right + BALL_RADIUS;

    if (!hit) return;
    ballPos.y = paddleTop + BALL_RADIUS + 0.04;
    const offset = (ballPos.x - paddleX) / (PADDLE_W * 0.5);
    const speed = Math.min(MAX_BALL_SPEED, Math.max(BASE_BALL_SPEED, ballVel.length()));
    ballVel.set(offset * speed * 0.9, Math.abs(ballVel.y) + 4).normalize().multiplyScalar(speed);
    combo = 0;
    lastPaddleContactAt = performance.now();
    bricksSincePaddle = 0;
  }

  function resolveBrickCollision(nowMs: number): void {
    if (!ballLaunched) return;
    for (const brick of bricks) {
      if (!brick.alive) continue;
      const closestX = Math.max(brick.left, Math.min(ballPos.x, brick.right));
      const closestY = Math.max(brick.bottom, Math.min(ballPos.y, brick.top));
      const dx = ballPos.x - closestX;
      const dy = ballPos.y - closestY;
      if (dx * dx + dy * dy > BALL_RADIUS * BALL_RADIUS) continue;

      const overlapX = Math.min(ballPos.x + BALL_RADIUS - brick.left, brick.right - (ballPos.x - BALL_RADIUS));
      const overlapY = Math.min(ballPos.y + BALL_RADIUS - brick.bottom, brick.top - (ballPos.y - BALL_RADIUS));
      if (overlapX < overlapY) {
        ballVel.x *= -1;
      } else {
        ballVel.y *= -1;
      }

      brick.alive = false;
      brick.mesh.visible = false;
      bricksLeft -= 1;
      combo += 1;
      score += 40 + combo * 5;
      bricksSincePaddle += 1;

      const nextSpeed = Math.min(MAX_BALL_SPEED, ballVel.length() * 1.014);
      ballVel.normalize().multiplyScalar(nextSpeed);
      if (bricksSincePaddle >= MAX_BRICK_HITS_WITHOUT_PADDLE && ballPos.y > UPPER_STALL_Y) {
        forceBallDownward(1);
        lastAntiStallAt = nowMs;
      }
      triggerFlash(brick.mesh.position.x, brick.mesh.position.y, 10 + combo * 0.4);

      if (bricksLeft <= 0) {
        level += 1;
        buildBricks();
        prepareBall(nowMs, 560);
      }
      break;
    }
  }

  function triggerFlash(x: number, y: number, scale: number): void {
    if (!flashSprite) return;
    flashLife = flashMaxLife;
    flashSprite.visible = true;
    flashSprite.position.set(x, y, 2.5);
    flashSprite.scale.set(scale, scale, 1);
    const material = flashSprite.material as THREE.SpriteMaterial;
    material.opacity = 0.76;
  }

  function updateFlash(dt: number): void {
    if (!flashSprite || flashLife <= 0) return;
    flashLife -= dt;
    const t = Math.max(0, flashLife / flashMaxLife);
    const material = flashSprite.material as THREE.SpriteMaterial;
    material.opacity = t * 0.78;
    flashSprite.scale.multiplyScalar(1 + dt * 2.6);
    if (flashLife <= 0) flashSprite.visible = false;
  }

  function forceBallDownward(forceScale: number): void {
    const speed = Math.min(MAX_BALL_SPEED, Math.max(BASE_BALL_SPEED, ballVel.length()));
    let dirX = ballVel.x + rand(-8, 8);
    if (Math.abs(dirX) < 6) dirX = (Math.random() < 0.5 ? -1 : 1) * 6;
    const dirY = -Math.abs(ballVel.y) - 6 * forceScale;
    ballVel.set(dirX, dirY).normalize().multiplyScalar(speed);
  }

  function applyAntiStall(nowMs: number): void {
    if (!ballLaunched) return;
    if (ballPos.y < UPPER_STALL_Y) return;
    if (nowMs - lastPaddleContactAt < UPPER_STALL_THRESHOLD_MS) return;
    if (nowMs - lastAntiStallAt < ANTI_STALL_COOLDOWN_MS) return;
    forceBallDownward(0.85);
    lastAntiStallAt = nowMs;
  }
</script>

<section class="breakout-root" bind:this={hostEl}>
  <canvas class="breakout-canvas" bind:this={canvasEl} aria-label="Neon Breakout"></canvas>

  <div class="breakout-overlay">
    <header class="overlay-head">
      <div class="title-block">
        <p class="title">{ui.title}</p>
      </div>
      <div class="action-group">
        <button type="button" on:click={startGame}>{gameState === "idle" ? ui.start : ui.restart}</button>
        <button type="button" on:click={togglePause} disabled={gameState === "idle" || gameState === "over"}>
          {gameState === "paused" ? ui.resume : ui.pause}
        </button>
      </div>
    </header>

    <div class="hud-grid">
      <article><span>{ui.score}</span><strong>{Math.round(score)}</strong></article>
      <article><span>{ui.combo}</span><strong>{combo}</strong></article>
      <article><span>{ui.lives}</span><strong>{lives}</strong></article>
      <article><span>{ui.level}</span><strong>{level}</strong></article>
      <article><span>{ui.bricks}</span><strong>{Math.max(0, bricksLeft)}</strong></article>
    </div>

    <section class="lan-panel" class:is-online={lanMode === "online"} aria-label="LAN Battle">
      <header class="lan-panel-head">
        <div>
          <p class="lan-kicker">{ui.online}</p>
          <strong>{roomStatusText}</strong>
        </div>
        <div class="lan-head-actions">
          {#if pingMs !== null}
            <span class="lan-ping">{ui.latency} {pingMs}ms</span>
          {/if}
          <button type="button" class="lan-mode-btn" on:click={() => (lanMode = lanMode === "online" ? "local" : "online")}>
            {lanMode === "online" ? ui.local : ui.online}
          </button>
        </div>
      </header>

      {#if lanMode === "online"}
        <div class="lan-controls">
          <label class="lan-input-label">
            <span>{ui.playerName}</span>
            <input bind:value={playerName} maxlength="18" on:blur={persistPlayerName} />
          </label>

          <div class="lan-action-row">
            <button type="button" disabled={isLanBusy || isLanConnected} on:click={createLanRoom}>{ui.createRoom}</button>
            <button type="button" disabled={isLanBusy} on:click={discoverLanRooms}>{ui.discoverRooms}</button>
            {#if isLanConnected}
              <button type="button" class="is-danger" on:click={() => disconnectLanSocket()}>{ui.disconnect}</button>
            {/if}
          </div>

          <div class="lan-join-row">
            <label class="lan-input-label is-code">
              <span>{ui.joinCode}</span>
              <input
                value={joinCode}
                inputmode="numeric"
                maxlength="8"
                on:input={(event) => (joinCode = normalizePairingCode((event.currentTarget as HTMLInputElement).value))}
              />
            </label>
            <button type="button" disabled={isLanBusy || isLanConnected || !joinCode} on:click={() => joinLanRoom()}>{ui.joinRoom}</button>
          </div>

          {#if pairingCode}
            <div class="pair-code-card">
              <span>{ui.pairingCode}</span>
              <strong>{pairingCode}</strong>
            </div>
          {/if}

          {#if lanRooms.length > 0 && !isLanConnected}
            <div class="room-list">
              {#each lanRooms as room (room.roomId)}
                <button type="button" class="room-card" on:click={() => joinLanRoom(room.pairingCode)}>
                  <span>{room.hostName || room.address}</span>
                  <strong>{room.pairingCode}</strong>
                  <em>{room.players}/{room.maxPlayers}</em>
                </button>
              {/each}
            </div>
          {/if}

          {#if lanNotice}
            <p class="lan-notice">{lanNotice}</p>
          {/if}
        </div>

        {#if isLanConnected}
          <div class="versus-strip">
            <article>
              <span>{playerName || "You"} {localRole ? `/${localRole}` : ""}</span>
              <strong>{selfPlayer?.ready || localReady ? ui.ready : statusText}</strong>
              <em>{Math.round(score)} / {lives}</em>
            </article>
            <article class:waiting={!primaryOpponent}>
              <span>{ui.opponent}</span>
              <strong>{primaryOpponent?.name ?? ui.waitingOpponent}</strong>
              <em>
                {#if primaryOpponent}
                  {(primaryOpponent.score ?? 0)} / {(primaryOpponent.lives ?? 3)}
                {:else}
                  --
                {/if}
              </em>
            </article>
            <button type="button" on:click={toggleLanReady}>{localReady ? ui.cancelReady : ui.ready}</button>
          </div>
        {/if}
      {/if}
    </section>

    <footer class="overlay-foot">
      <p class="status">{ui.chase} / {statusText}</p>
    </footer>
  </div>

  {#if gameState === "paused"}
    <div class="pause-mask" aria-live="polite">
      <div class="pause-panel">
        <p>{ui.pausedOverlay}</p>
      </div>
    </div>
  {/if}

  {#if gameState === "over"}
    <div class="pause-mask is-over" aria-live="assertive">
      <div class="pause-panel is-over">
        <p>{ui.over}</p>
        <button type="button" class="overlay-restart-btn" on:click={requestLanRestart}>{ui.restart}</button>
      </div>
    </div>
  {/if}
</section>

<style>
  .breakout-root {
    position: absolute;
    inset: 0;
    overflow: hidden;
    border-radius: 0.58rem;
    border: 1px solid rgb(var(--hud-border-rgb) / 0.28);
    background: linear-gradient(180deg, rgb(var(--hud-surface-deep-rgb) / 0.92), rgb(var(--hud-surface-rgb) / 0.86));
    box-shadow:
      inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.08),
      0 0 24px rgb(var(--hud-glow-rgb) / 0.12);
  }

  .breakout-canvas {
    position: absolute;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    display: block;
  }

  .breakout-overlay {
    position: absolute;
    inset: 0;
    pointer-events: none;
    display: grid;
    grid-template-rows: auto auto 1fr auto;
    gap: 0.42rem;
    padding: 0.5rem 0.58rem 0.42rem;
  }

  .overlay-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 0.5rem;
  }

  .title {
    margin: 0;
    color: rgb(var(--hud-text-main-rgb) / 0.96);
    font-size: 0.68rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
  }

  .action-group {
    display: inline-flex;
    gap: 0.28rem;
    pointer-events: auto;
  }

  .action-group button {
    min-block-size: 1.65rem;
    border: 1px solid rgb(var(--hud-cyan-rgb) / 0.56);
    border-radius: 999px;
    padding: 0.14rem 0.52rem;
    background: linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.92), rgb(var(--hud-surface-rgb) / 0.86));
    color: rgb(var(--hud-text-main-rgb) / 0.96);
    font: inherit;
    font-size: 0.6rem;
    letter-spacing: 0.06em;
    cursor: pointer;
  }

  .action-group button:disabled {
    opacity: 0.42;
    cursor: not-allowed;
  }

  .hud-grid {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 0.24rem;
  }

  .hud-grid article {
    min-block-size: 1.88rem;
    border: 1px solid rgb(var(--hud-border-rgb) / 0.24);
    border-radius: 0.42rem;
    padding: 0.16rem 0.34rem 0.24rem;
    background: rgb(var(--hud-surface-rgb) / 0.68);
  }

  .hud-grid span {
    display: block;
    color: rgb(var(--hud-text-dim-rgb) / 0.82);
    font-size: 0.5rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }

  .hud-grid strong {
    color: rgb(var(--hud-text-main-rgb) / 0.98);
    font-size: 0.84rem;
    line-height: 1;
  }

  .lan-panel {
    align-self: start;
    justify-self: end;
    inline-size: min(20rem, 58%);
    display: grid;
    gap: 0.42rem;
    border: 1px solid rgb(var(--hud-border-rgb) / 0.24);
    border-radius: 0.56rem;
    padding: 0.48rem;
    background:
      linear-gradient(180deg, rgb(var(--hud-surface-rgb) / 0.72), rgb(var(--hud-surface-deep-rgb) / 0.78)),
      radial-gradient(circle at 50% 0, rgb(var(--hud-glow-rgb) / 0.05), transparent 58%);
    box-shadow: inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.07);
    pointer-events: auto;
  }

  .lan-panel:not(.is-online) {
    inline-size: auto;
    padding: 0.32rem;
    background: rgb(var(--hud-surface-deep-rgb) / 0.54);
  }

  .lan-panel-head,
  .lan-head-actions,
  .lan-action-row,
  .lan-join-row,
  .versus-strip {
    display: flex;
    align-items: center;
    gap: 0.36rem;
  }

  .lan-panel-head {
    justify-content: space-between;
  }

  .lan-panel-head div:first-child {
    min-inline-size: 0;
    display: grid;
    gap: 0.08rem;
  }

  .lan-kicker {
    margin: 0;
    color: rgb(var(--hud-text-dim-rgb) / 0.84);
    font-size: 0.48rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
  }

  .lan-panel-head strong {
    color: rgb(var(--hud-text-main-rgb) / 0.96);
    font-size: 0.66rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }

  .lan-ping,
  .lan-notice {
    color: rgb(var(--hud-text-dim-rgb) / 0.84);
    font-size: 0.54rem;
    letter-spacing: 0.05em;
  }

  .lan-controls {
    display: grid;
    gap: 0.36rem;
  }

  .lan-input-label {
    min-inline-size: 0;
    display: grid;
    gap: 0.18rem;
  }

  .lan-input-label span {
    color: rgb(var(--hud-text-dim-rgb) / 0.82);
    font-size: 0.5rem;
    letter-spacing: 0.09em;
    text-transform: uppercase;
  }

  .lan-input-label input {
    min-inline-size: 0;
    inline-size: 100%;
    border: 1px solid rgb(var(--hud-border-rgb) / 0.34);
    border-radius: 999px;
    padding: 0.28rem 0.52rem;
    background: rgb(var(--hud-surface-deep-rgb) / 0.78);
    color: rgb(var(--hud-text-main-rgb) / 0.96);
    font: inherit;
    font-size: 0.64rem;
    letter-spacing: 0.05em;
    outline: none;
  }

  .lan-input-label input:focus {
    border-color: rgb(var(--hud-cyan-rgb) / 0.52);
    box-shadow: 0 0 0 2px rgb(var(--hud-cyan-rgb) / 0.12);
  }

  .lan-input-label.is-code {
    flex: 1;
  }

  .lan-action-row,
  .lan-join-row {
    flex-wrap: wrap;
  }

  .lan-mode-btn,
  .lan-action-row button,
  .lan-join-row button,
  .versus-strip button,
  .room-card {
    min-block-size: 1.42rem;
    border: 1px solid rgb(var(--hud-cyan-rgb) / 0.38);
    border-radius: 999px;
    padding: 0.18rem 0.52rem;
    background: rgb(var(--hud-surface-alt-rgb) / 0.74);
    color: rgb(var(--hud-text-main-rgb) / 0.96);
    font: inherit;
    font-size: 0.56rem;
    letter-spacing: 0.05em;
    cursor: pointer;
  }

  .lan-mode-btn:hover,
  .lan-action-row button:hover:not(:disabled),
  .lan-join-row button:hover:not(:disabled),
  .versus-strip button:hover:not(:disabled),
  .room-card:hover {
    border-color: rgb(var(--hud-lime-rgb) / 0.54);
    box-shadow: 0 0 10px rgb(var(--hud-lime-rgb) / 0.1);
  }

  .lan-action-row button:disabled,
  .lan-join-row button:disabled,
  .versus-strip button:disabled {
    opacity: 0.42;
    cursor: not-allowed;
  }

  .lan-action-row button.is-danger {
    border-color: rgb(var(--hud-orange-rgb) / 0.46);
    color: rgb(var(--hud-orange-rgb) / 0.96);
    background: rgb(var(--hud-surface-deep-rgb) / 0.82);
  }

  .pair-code-card {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    border: 1px solid rgb(var(--hud-lime-rgb) / 0.36);
    border-radius: 0.5rem;
    padding: 0.36rem 0.48rem;
    background: rgb(var(--hud-surface-alt-rgb) / 0.58);
  }

  .pair-code-card span {
    color: rgb(var(--hud-text-dim-rgb) / 0.84);
    font-size: 0.5rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }

  .pair-code-card strong {
    color: rgb(var(--hud-lime-rgb) / 0.96);
    font-size: 1rem;
    letter-spacing: 0.18em;
  }

  .room-list {
    display: grid;
    gap: 0.24rem;
  }

  .room-card {
    inline-size: 100%;
    display: grid;
    grid-template-columns: 1fr auto auto;
    align-items: center;
    gap: 0.4rem;
    border-radius: 0.44rem;
    text-align: left;
  }

  .room-card span {
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .room-card strong {
    color: rgb(var(--hud-lime-rgb) / 0.96);
    letter-spacing: 0.12em;
  }

  .room-card em {
    color: rgb(var(--hud-text-dim-rgb) / 0.82);
    font-style: normal;
  }

  .lan-notice {
    margin: 0;
    color: rgb(var(--hud-orange-rgb) / 0.94);
  }

  .versus-strip {
    justify-content: stretch;
    align-items: stretch;
  }

  .versus-strip article {
    flex: 1;
    min-inline-size: 0;
    border: 1px solid rgb(var(--hud-border-rgb) / 0.26);
    border-radius: 0.46rem;
    padding: 0.28rem 0.38rem;
    background: rgb(var(--hud-surface-rgb) / 0.58);
  }

  .versus-strip article.waiting {
    border-style: dashed;
    opacity: 0.78;
  }

  .versus-strip span,
  .versus-strip em {
    display: block;
    color: rgb(var(--hud-text-dim-rgb) / 0.82);
    font-size: 0.48rem;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    font-style: normal;
  }

  .versus-strip strong {
    display: block;
    margin-block: 0.1rem;
    overflow: hidden;
    color: rgb(var(--hud-text-main-rgb) / 0.96);
    font-size: 0.62rem;
    letter-spacing: 0.04em;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .overlay-foot {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    gap: 0.52rem;
  }

  .status {
    margin: 0;
    border: 1px solid rgb(var(--hud-border-rgb) / 0.3);
    border-radius: 999px;
    padding: 0.18rem 0.56rem;
    background: rgb(var(--hud-surface-deep-rgb) / 0.66);
    color: rgb(var(--hud-text-main-rgb) / 0.94);
    font-size: 0.56rem;
    letter-spacing: 0.07em;
    text-transform: uppercase;
  }

  .pause-mask {
    position: absolute;
    inset: 0;
    z-index: 12;
    pointer-events: none;
    display: grid;
    place-items: center;
    background:
      radial-gradient(circle at center, rgb(var(--hud-surface-rgb) / 0.24), rgb(var(--hud-bg-30) / 0.72)),
      repeating-linear-gradient(
        180deg,
        rgb(var(--hud-glow-alt-rgb) / 0.08) 0,
        rgb(var(--hud-glow-alt-rgb) / 0.08) 1px,
        transparent 1px,
        transparent 4px
      );
    backdrop-filter: blur(3px);
  }

  .pause-panel {
    border: 1px solid rgb(var(--hud-lime-rgb) / 0.56);
    border-radius: 0.66rem;
    padding: 0.62rem 0.92rem;
    background: rgb(var(--hud-surface-deep-rgb) / 0.8);
    box-shadow:
      inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.1),
      0 0 26px rgb(var(--hud-lime-rgb) / 0.22);
    pointer-events: auto;
  }

  .pause-panel p {
    margin: 0;
    color: rgb(var(--hud-text-main-rgb) / 0.98);
    font-size: clamp(0.76rem, 1.5vw, 1.08rem);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-shadow: 0 0 10px rgb(var(--hud-lime-rgb) / 0.28);
  }

  .pause-mask.is-over {
    background:
      radial-gradient(circle at center, rgb(var(--hud-surface-rgb) / 0.22), rgb(var(--hud-bg-30) / 0.8)),
      repeating-linear-gradient(
        180deg,
        rgb(var(--hud-orange-rgb) / 0.1) 0,
        rgb(var(--hud-orange-rgb) / 0.1) 1px,
        transparent 1px,
        transparent 4px
      );
  }

  .pause-panel.is-over {
    border-color: rgb(var(--hud-orange-rgb) / 0.62);
    box-shadow:
      inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.1),
      0 0 32px rgb(var(--hud-orange-rgb) / 0.24);
    min-inline-size: min(24rem, 72%);
    display: grid;
    gap: 0.72rem;
    justify-items: center;
  }

  .pause-panel.is-over p {
    color: rgb(var(--hud-orange-rgb) / 0.98);
    text-shadow: 0 0 12px rgb(var(--hud-orange-rgb) / 0.36);
  }

  .overlay-restart-btn {
    min-block-size: clamp(2.2rem, 4.2vh, 2.8rem);
    min-inline-size: clamp(8.8rem, 18vw, 13rem);
    border: 1px solid rgb(var(--hud-cyan-rgb) / 0.62);
    border-radius: 999px;
    padding: 0.24rem 1rem;
    background: linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.96), rgb(var(--hud-surface-rgb) / 0.9));
    color: rgb(var(--hud-text-main-rgb) / 0.98);
    font: inherit;
    font-size: clamp(0.74rem, 1.3vw, 0.96rem);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    box-shadow: 0 0 16px rgb(var(--hud-cyan-rgb) / 0.2);
    transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
  }

  .overlay-restart-btn:hover {
    transform: translateY(-1px);
    border-color: rgb(var(--hud-lime-rgb) / 0.66);
    box-shadow: 0 0 20px rgb(var(--hud-lime-rgb) / 0.28);
  }

  @media (max-width: 1120px) {
    .hud-grid {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
  }
</style>
