<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8" />

  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />

  <title>SKY RACE — 3D Low-Poly Edition (WebRTC P2P Multiplayer)</title>

  <link rel="preconnect" href="https://fonts.googleapis.com">

  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

  <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Chakra+Petch:ital,wght@0,600;0,700;1,700&display=swap" rel="stylesheet">

  <!-- PeerJS 1.5.5 for Zero-Server Internet WebRTC Multiplayer -->

  <script src="https://cdnjs.cloudflare.com/ajax/libs/peerjs/1.5.5/peerjs.min.js"></script>

  <style>

    :root {

      --navy-dark: #0c1738;

      --navy-panel: rgba(13, 25, 62, 0.88);

      --border-light: #7ccbff;

      --gold: #ffcc29;

      --red-btn: #e82634;

      --blue-btn: #1858d6;

      --teal-btn: #147a68;

      --purple-btn: #5928b5;

      --text-white: #f8fbff;

      --font-pixel: 'Press Start 2P', monospace;

      --font-hud: 'Chakra Petch', sans-serif;

    }


    * {

      box-sizing: border-box;

      margin: 0;

      padding: 0;

      user-select: none;

      -webkit-user-select: none;

      -webkit-touch-callout: none;

    }


    html, body {

      width: 100vw;

      height: 100vh;

      overflow: hidden;

      background: #060b18;

      font-family: var(--font-pixel);

      color: var(--text-white);

      touch-action: none;

    }


    #game-wrapper {

      position: relative;

      width: 100vw;

      height: 100vh;

      overflow: hidden;

    }


    #three-container {

      position: absolute;

      inset: 0;

      width: 100%;

      height: 100%;

      z-index: 1;

    }


    #ui-layer {

      position: absolute;

      inset: 0;

      z-index: 10;

      pointer-events: none;

      display: flex;

      flex-direction: column;

      justify-content: space-between;

    }


    .interactive { pointer-events: auto; }


    .screen-overlay {

      position: absolute;

      inset: 0;

      display: none;

      flex-direction: column;

      align-items: center;

      justify-content: center;

      padding: 16px;

      pointer-events: none;

    }

    .screen-overlay.active { display: flex; }


    .retro-panel {

      background: var(--navy-panel);

      backdrop-filter: blur(8px);

      -webkit-backdrop-filter: blur(8px);

      border: 2px solid var(--border-light);

      box-shadow: 0 8px 28px rgba(0, 0, 0, 0.65), inset 0 0 18px rgba(68, 164, 255, 0.22);

      border-radius: 8px;

      padding: 16px;

      pointer-events: auto;

    }


    .arcade-btn {

      position: relative;

      display: flex;

      align-items: center;

      justify-content: flex-start;

      width: 310px;

      height: 50px;

      padding: 0 18px;

      border: 2px solid #ffffff;

      border-radius: 8px;

      font-family: var(--font-pixel);

      font-size: 13px;

      color: #ffffff;

      text-shadow: 2px 2px 0 #09122c;

      cursor: pointer;

      pointer-events: auto;

      transition: transform 0.08s ease, filter 0.12s ease, box-shadow 0.12s ease;

      box-shadow: 0 5px 0 #09122c, 0 8px 18px rgba(0,0,0,0.55);

      margin-bottom: 10px;

      gap: 14px;

    }

    .arcade-btn:hover, .arcade-btn.focused {

      filter: brightness(1.16);

      transform: translateY(-2px) scale(1.01);

      border-color: var(--gold);

      box-shadow: 0 7px 0 #09122c, 0 0 16px rgba(255, 204, 41, 0.65);

    }

    .arcade-btn:active {

      transform: translateY(3px);

      box-shadow: 0 2px 0 #09122c;

    }

    .arcade-btn.red { background: linear-gradient(180deg, #ff4b59 0%, #d41826 55%, #9c0c18 100%); }

    .arcade-btn.blue { background: linear-gradient(180deg, #3d8bff 0%, #1552c9 55%, #0c358a 100%); }

    .arcade-btn.teal { background: linear-gradient(180deg, #27b89d 0%, #107864 55%, #094f41 100%); }

    .arcade-btn.purple { background: linear-gradient(180deg, #8b52ff 0%, #5622b5 55%, #381182 100%); }

    .arcade-btn.gold {

      background: linear-gradient(180deg, #ffe055 0%, #e69d00 60%, #a86800 100%);

      color: #09122c;

      text-shadow: 1px 1px 0 #fff5b8;

    }

    .arcade-btn.sm {

      width: auto;

      height: 38px;

      padding: 0 14px;

      font-size: 10px;

      margin-bottom: 0;

    }


    .logo-header {

      text-align: center;

      margin-bottom: 18px;

      pointer-events: none;

      filter: drop-shadow(0 6px 12px rgba(0,0,0,0.75));

    }

    .logo-wings {

      display: inline-flex;

      align-items: center;

      gap: 12px;

      background: linear-gradient(180deg, rgba(14,27,68,0.92), rgba(8,15,38,0.95));

      border: 3px solid #8ad4ff;

      border-radius: 12px;

      padding: 10px 26px;

      box-shadow: 0 6px 0 #071029;

    }

    .logo-sky {

      font-size: clamp(22px, 4vw, 36px);

      background: linear-gradient(180deg, #ffffff 0%, #89d4ff 55%, #2e78db 100%);

      -webkit-background-clip: text;

      -webkit-text-fill-color: transparent;

      filter: drop-shadow(3px 3px 0 #091436);

    }

    .logo-race {

      font-size: clamp(22px, 4vw, 36px);

      background: linear-gradient(180deg, #fff066 0%, #ff9914 55%, #db2929 100%);

      -webkit-background-clip: text;

      -webkit-text-fill-color: transparent;

      filter: drop-shadow(3px 3px 0 #091436);

    }

    .logo-sub {

      margin-top: 6px;

      font-size: 9px;

      color: #a8e0ff;

      letter-spacing: 2px;

      text-shadow: 1px 1px 0 #000;

    }


    .top-nav-bar {

      position: absolute;

      top: 14px;

      left: 16px;

      right: 16px;

      display: flex;

      justify-content: space-between;

      align-items: center;

      pointer-events: none;

    }

    .screen-title-banner {

      background: linear-gradient(180deg, #1b3882 0%, #0e1e4d 100%);

      border: 2px solid #84d2ff;

      border-radius: 8px;

      padding: 8px 22px;

      font-size: clamp(12px, 2vw, 18px);

      color: #ffffff;

      text-shadow: 2px 2px 0 #071029;

      box-shadow: 0 4px 0 #071029;

    }


    .hangar-stats-panel {

      position: absolute;

      top: 68px;

      right: 20px;

      width: min(320px, 42vw);

      z-index: 15;

    }

    .plane-title {

      font-size: 14px;

      color: #ffffff;

      margin-bottom: 4px;

    }

    .plane-class {

      font-size: 9px;

      color: #63c5ff;

      margin-bottom: 10px;

      padding-bottom: 6px;

      border-bottom: 1px solid rgba(124, 203, 255, 0.35);

    }

    .stat-row {

      display: flex;

      align-items: center;

      justify-content: space-between;

      margin-bottom: 7px;

      font-size: 8px;

    }

    .stat-segments {

      display: flex;

      gap: 3px;

      width: 135px;

      background: #07112b;

      padding: 3px;

      border: 1px solid #3569b8;

      border-radius: 4px;

    }

    .stat-seg {

      flex: 1;

      height: 8px;

      background: #162952;

      transform: skewX(-18deg);

    }

    .stat-seg.filled {

      background: linear-gradient(180deg, #ffe247 0%, #ff9900 100%);

      box-shadow: 0 0 6px rgba(255, 204, 41, 0.6);

    }

    .plane-desc {

      margin-top: 8px;

      font-family: var(--font-hud);

      font-size: 13px;

      line-height: 1.35;

      color: #dbeeff;

    }


    .plane-grid-dock {

      position: absolute;

      bottom: 12px;

      left: 50%;

      transform: translateX(-50%);

      width: min(960px, 96vw);

      display: grid;

      grid-template-columns: repeat(5, 1fr);

      gap: 8px;

      pointer-events: auto;

    }

    .plane-card {

      background: linear-gradient(180deg, rgba(22, 43, 98, 0.92) 0%, rgba(10, 21, 52, 0.96) 100%);

      border: 2px solid #4888d6;

      border-radius: 6px;

      padding: 7px 8px;

      cursor: pointer;

      position: relative;

      transition: transform 0.1s ease, border-color 0.1s ease;

      display: flex;

      flex-direction: column;

      justify-content: space-between;

      min-height: 62px;

    }

    .plane-card:hover {

      transform: translateY(-2px);

      border-color: #9be0ff;

    }

    .plane-card.selected {

      border-color: var(--gold);

      box-shadow: 0 0 14px rgba(255, 204, 41, 0.65), inset 0 0 10px rgba(255, 204, 41, 0.25);

    }

    .plane-card-header {

      display: flex;

      justify-content: space-between;

      align-items: center;

      font-size: 8px;

      color: #9ecfff;

    }

    .plane-swatch {

      width: 28px;

      height: 12px;

      border-radius: 3px;

      border: 1px solid #fff;

    }

    .plane-card-name {

      font-size: 8px;

      color: #ffffff;

      margin-top: 6px;

      white-space: nowrap;

      overflow: hidden;

      text-overflow: ellipsis;

    }

    .plane-check {

      position: absolute;

      top: -6px;

      right: -6px;

      width: 18px;

      height: 18px;

      border-radius: 4px;

      background: var(--gold);

      color: #09122c;

      font-size: 10px;

      display: none;

      align-items: center;

      justify-content: center;

      font-weight: bold;

      border: 1px solid #fff;

    }

    .plane-card.selected .plane-check { display: flex; }


    .routes-grid {

      display: grid;

      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));

      gap: 10px;

      width: min(980px, 95vw);

      max-height: 64vh;

      overflow-y: auto;

      padding: 6px;

      pointer-events: auto;

    }

    .route-card {

      background: linear-gradient(180deg, rgba(20, 39, 90, 0.92), rgba(10, 20, 48, 0.96));

      border: 2px solid #4d8fe0;

      border-radius: 8px;

      padding: 12px;

      cursor: pointer;

      transition: transform 0.1s ease, border-color 0.1s ease;

    }

    .route-card:hover { transform: translateY(-2px); border-color: #9be0ff; }

    .route-card.selected {

      border-color: var(--gold);

      box-shadow: 0 0 16px rgba(255, 204, 41, 0.55);

    }

    .route-title { font-size: 10px; color: var(--gold); margin-bottom: 5px; }

    .route-sub { font-family: var(--font-hud); font-size: 12px; color: #cce6ff; margin-bottom: 8px; }

    .route-badges { display: flex; gap: 6px; font-size: 7px; color: #8ad4ff; flex-wrap: wrap; }

    .route-badge { background: rgba(0,0,0,0.4); padding: 3px 6px; border-radius: 4px; border: 1px solid #3569b8; }


    #hud-screen {

      justify-content: space-between;

      padding: 12px 16px;

    }

    .hud-top-bar {

      width: 100%;

      display: flex;

      justify-content: space-between;

      align-items: flex-start;

      pointer-events: none;

    }

    .hud-box {

      background: linear-gradient(180deg, rgba(16, 33, 78, 0.9) 0%, rgba(9, 19, 46, 0.94) 100%);

      border: 2px solid #84d2ff;

      border-radius: 8px;

      padding: 8px 14px;

      box-shadow: 0 4px 0 #060d24, 0 6px 16px rgba(0,0,0,0.5);

      pointer-events: auto;

    }

    .hud-lap { font-size: clamp(11px, 1.8vw, 15px); color: #ffffff; }

    .hud-lap span { color: var(--gold); }

    .hud-timer { font-size: clamp(12px, 2vw, 16px); color: #ffffff; letter-spacing: 1px; }

    .hud-pos { font-size: clamp(16px, 2.6vw, 24px); color: var(--gold); }

    .hud-pos small { font-size: 12px; color: #ffffff; }


    .hud-bottom-bar {

      width: 100%;

      display: flex;

      justify-content: space-between;

      align-items: flex-end;

      pointer-events: none;

    }

    .minimap-box {

      width: 132px;

      height: 150px;

      padding: 6px;

      display: flex;

      flex-direction: column;

      align-items: center;

      justify-content: center;

    }

    #minimap-canvas {

      width: 116px;

      height: 132px;

      background: rgba(7, 16, 38, 0.85);

      border-radius: 4px;

    }

    .speedo-box {

      width: 215px;

      padding: 10px 14px;

      text-align: right;

    }

    .speed-bars {

      display: flex;

      gap: 3px;

      height: 14px;

      margin-bottom: 6px;

      align-items: flex-end;

    }

    .speed-bar-seg {

      flex: 1;

      height: 100%;

      background: #1b2d59;

      transform: skewX(-16deg);

      border-radius: 1px;

    }

    .speed-readout {

      font-size: 20px;

      color: #ffffff;

      text-shadow: 2px 2px 0 #060d24;

    }

    .speed-unit { font-size: 9px; color: #8ad4ff; }

    .boost-meter-wrap {

      margin-top: 6px;

      background: #07112b;

      border: 1px solid #4888d6;

      border-radius: 4px;

      height: 10px;

      overflow: hidden;

      position: relative;

    }

    .boost-meter-fill {

      height: 100%;

      width: 100%;

      background: linear-gradient(90deg, #00e1ff 0%, #ffcc29 65%, #ff3b2f 100%);

      transition: width 0.06s linear;

    }


    #countdown-banner {

      position: absolute;

      top: 36%;

      left: 50%;

      transform: translate(-50%, -50%);

      font-size: clamp(32px, 6vw, 64px);

      color: var(--gold);

      text-shadow: 4px 4px 0 #09122c, 0 0 28px rgba(255, 204, 41, 0.85);

      pointer-events: none;

      text-align: center;

    }

    #checkpoint-toast {

      position: absolute;

      top: 22%;

      left: 50%;

      transform: translateX(-50%);

      font-size: 13px;

      color: #5eff9c;

      text-shadow: 2px 2px 0 #060d24;

      pointer-events: none;

      opacity: 0;

      transition: opacity 0.2s ease;

    }


    .touch-controls {

      position: absolute;

      bottom: 18px;

      left: 50%;

      transform: translateX(-50%);

      display: none;

      gap: 16px;

      align-items: center;

      pointer-events: auto;

      z-index: 25;

    }

    @media (pointer: coarse), (max-width: 900px) {

      .touch-controls { display: flex; }

      .hangar-stats-panel { top: 58px; right: 10px; width: 250px; padding: 10px; }

      .plane-grid-dock { grid-template-columns: repeat(5, 1fr); bottom: 6px; }

    }

    .touch-pad-btn {

      width: 58px;

      height: 58px;

      border-radius: 12px;

      background: rgba(16, 35, 84, 0.82);

      border: 2px solid #84d2ff;

      color: #fff;

      font-family: var(--font-pixel);

      font-size: 16px;

      display: flex;

      align-items: center;

      justify-content: center;

      box-shadow: 0 4px 0 #060d24;

    }

    .touch-pad-btn:active {

      background: var(--gold);

      color: #09122c;

      transform: translateY(2px);

    }

    .touch-boost-btn {

      width: 82px;

      height: 58px;

      background: linear-gradient(180deg, #ff512f, #dd2476);

      border: 2px solid #fff;

      border-radius: 12px;

      font-family: var(--font-pixel);

      font-size: 10px;

      color: #fff;

    }


    .lobby-layout {

      display: grid;

      grid-template-columns: 1.15fr 1fr;

      gap: 14px;

      width: min(920px, 95vw);

      margin-top: 44px;

    }

    @media (max-width: 740px) {

      .lobby-layout { grid-template-columns: 1fr; }

    }

    .lobby-input {

      background: #07112b;

      border: 2px solid #4d8fe0;

      border-radius: 6px;

      color: #fff;

      padding: 8px 10px;

      font-family: var(--font-pixel);

      font-size: 10px;

      width: 100%;

    }

    .player-slot {

      display: flex;

      justify-content: space-between;

      align-items: center;

      background: rgba(7, 17, 43, 0.78);

      border: 1px solid #3569b8;

      border-radius: 6px;

      padding: 8px 10px;

      margin-bottom: 6px;

      font-size: 9px;

    }

    .status-pill {

      padding: 3px 7px;

      border-radius: 4px;

      font-size: 8px;

    }

    .status-pill.ready { background: #168a4c; color: #fff; }

    .status-pill.wait { background: #7a5af8; color: #fff; }


    .turntable-hint {

      position: absolute;

      bottom: 155px;

      left: 50%;

      transform: translateX(-50%);

      font-size: 9px;

      color: #a8e0ff;

      background: rgba(8, 17, 42, 0.78);

      padding: 5px 12px;

      border-radius: 20px;

      border: 1px solid #4888d6;

      pointer-events: none;

    }


    .top-utility-pill {

      position: fixed;

      top: 10px;

      left: 12px;

      z-index: 40;

      display: flex;

      gap: 6px;

      pointer-events: auto;

    }

    .util-chip {

      background: rgba(10, 20, 48, 0.82);

      border: 1px solid #5caeff;

      color: #bfe4ff;

      font-family: var(--font-pixel);

      font-size: 8px;

      padding: 5px 8px;

      border-radius: 5px;

      cursor: pointer;

    }

    .util-chip:hover { border-color: var(--gold); color: #fff; }

  </style>

  <script type="importmap">

    {

      "imports": {

        "three": "https://unpkg.com/three@0.160.0/build/three.module.js"

      }

    }

  </script>

</head>

<body>

  <div id="game-wrapper">

    <!-- 3D LOW-POLY THREE.JS VIEWPORT -->

    <div id="three-container"></div>


    <!-- QUICK UTILITY CHIPS -->

    <div class="top-utility-pill" id="top-util-bar">

      <button class="util-chip" id="btn-mute-quick" title="Toggle Audio">🔊 AUDIO: ON</button>

      <button class="util-chip" id="btn-cam-mode" title="Cycle 3D Camera View">🎥 CAM: CHASE</button>

      <button class="util-chip" id="btn-download-html" title="Download Standalone HTML Game File">⬇ SAVE HTML</button>

      <button class="util-chip" id="btn-self-test" title="Run Automated Verification Suite">✔ VERIFY</button>

    </div>


    <!-- UI OVERLAY LAYER -->

    <div id="ui-layer">


      <!-- 1. MAIN MENU SCREEN -->

      <section id="screen-menu" class="screen-overlay active">

        <div class="logo-header">

          <div class="logo-wings">

            <span class="logo-sky">SKY</span>

            <span style="font-size:22px;">✈️</span>

            <span class="logo-race">RACE</span>

          </div>

          <div class="logo-sub">3D LOW-POLY WORLD GRAND PRIX • WEBRTC P2P EDITION</div>

        </div>


        <nav aria-label="Main Menu">

          <button class="arcade-btn red" data-action="nav-singleplayer">

            <span>✈️</span> <span>SINGLEPLAYER</span>

          </button>

          <button class="arcade-btn blue" data-action="nav-multiplayer">

            <span>🌐</span> <span>MULTIPLAYER</span>

          </button>

          <button class="arcade-btn teal" data-action="nav-planes">

            <span>🛠️</span> <span>SELECT PLANES</span>

          </button>

          <button class="arcade-btn purple" data-action="nav-options">

            <span>⚙️</span> <span>OPTIONS</span>

          </button>

        </nav>

      </section>


      <!-- 2. SELECT PLANES (3D HANGAR TURNTABLE) -->

      <section id="screen-planes" class="screen-overlay">

        <div class="top-nav-bar">

          <button class="arcade-btn blue sm interactive" data-action="planes-back">⬅ BACK</button>

          <div class="screen-title-banner">SELECT PLANES</div>

          <button class="arcade-btn gold sm interactive" data-action="planes-confirm" id="btn-plane-confirm">CONFIRM ➔</button>

        </div>


        <aside class="retro-panel hangar-stats-panel">

          <div class="plane-title" id="hangar-plane-name">RED ARROW</div>

          <div class="plane-class" id="hangar-plane-class">Racer Class • #01</div>


          <div class="stat-row">

            <span>SPEED</span>

            <div class="stat-segments" id="stat-speed"></div>

          </div>

          <div class="stat-row">

            <span>ACCELERATION</span>

            <div class="stat-segments" id="stat-accel"></div>

          </div>

          <div class="stat-row">

            <span>HANDLING</span>

            <div class="stat-segments" id="stat-handling"></div>

          </div>

          <div class="stat-row">

            <span>BOOST</span>

            <div class="stat-segments" id="stat-boost"></div>

          </div>


          <p class="plane-desc" id="hangar-plane-desc">

            Balanced handling and dependable performance across all circuits.

          </p>

        </aside>


        <div class="turntable-hint">🖱️ DRAG OR SWIPE TO ROTATE 3D AIRCRAFT</div>


        <div class="plane-grid-dock" id="plane-cards-container"></div>

      </section>


      <!-- 3. ROUTE SELECTION SCREEN (7 WORLD ROUTES) -->

      <section id="screen-routes" class="screen-overlay">

        <div class="top-nav-bar">

          <button class="arcade-btn blue sm interactive" data-action="routes-back">⬅ BACK</button>

          <div class="screen-title-banner">SELECT WORLD ROUTE (7 CIRCUITS)</div>

          <div style="display:flex; gap:8px;" class="interactive">

            <button class="arcade-btn teal sm" data-action="routes-change-plane">✈️ PLANE</button>

            <button class="arcade-btn gold sm" data-action="routes-launch">LAUNCH RACE ➔</button>

          </div>

        </div>


        <div class="routes-grid" id="routes-grid-container"></div>

      </section>


      <!-- 4. ONLINE MULTIPLAYER LOBBY -->

      <section id="screen-multiplayer" class="screen-overlay">

        <div class="top-nav-bar">

          <button class="arcade-btn blue sm interactive" data-action="mp-back">⬅ MENU</button>

          <div class="screen-title-banner">ONLINE MULTIPLAYER LOBBY</div>

          <button class="arcade-btn teal sm interactive" data-action="mp-change-plane">✈️ CHANGE PLANE</button>

        </div>


        <div class="lobby-layout">

          <div class="retro-panel">

            <div style="font-size:11px; color:var(--gold); margin-bottom:10px;">CONNECTION & MATCHMAKING</div>


            <div style="margin-bottom:8px;">

              <label style="font-size:8px; color:#9ecfff; display:block; margin-bottom:4px;">PILOT CALLSIGN & NETWORK MODE</label>

              <div style="display:flex; gap:6px;">

                <input id="mp-pilot-name" class="lobby-input" value="ACE-01" maxlength="12" style="width:42%;" />

                <select id="mp-mode-select" class="lobby-input" style="width:58%;">

                  <option value="webrtc">Internet P2P (WebRTC / Zero-Server)</option>

                  <option value="cloud">Same-PC / Multi-Tab Sync</option>

                  <option value="colyseus">Colyseus WebSocket Server</option>

                </select>

              </div>

            </div>


            <div id="colyseus-url-row" style="margin-bottom:8px; display:none;">

              <label style="font-size:8px; color:#9ecfff; display:block; margin-bottom:4px;">COLYSEUS SERVER ENDPOINT</label>

              <input id="mp-server-url" class="lobby-input" value="ws://localhost:2567" />

            </div>


            <div style="display:flex; gap:6px; margin-bottom:8px; flex-wrap:wrap;">

              <button class="arcade-btn red sm" data-action="mp-quick">⚡ QUICK MATCH</button>

              <button class="arcade-btn blue sm" data-action="mp-create">➕ HOST ROOM</button>

              <button class="arcade-btn gold sm" data-action="mp-copy-invite" title="Copy direct URL link for friends">📋 COPY INVITE LINK</button>

            </div>


            <div style="display:flex; gap:6px; margin-bottom:8px;">

              <input id="mp-room-code-input" class="lobby-input" placeholder="ROOM CODE (E.G. SKY3D)" maxlength="6" style="text-transform:uppercase;" />

              <button class="arcade-btn teal sm" data-action="mp-join">JOIN BY CODE</button>

            </div>


            <div style="margin-bottom:8px;">

              <label style="font-size:8px; color:#9ecfff; display:block; margin-bottom:4px;">HOST CIRCUIT SELECTION</label>

              <select id="mp-route-select" class="lobby-input"></select>

            </div>


            <div id="mp-status-log" style="font-family:var(--font-hud); font-size:12px; color:#74f7a5; min-height:20px;">

              Ready! Click HOST ROOM and share your Room Code or Invite Link with friends anywhere on the internet.

            </div>

          </div>


          <div class="retro-panel" style="display:flex; flex-direction:column; justify-content:space-between;">

            <div>

              <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">

                <span style="font-size:11px; color:var(--gold);">ROOM ROSTER (MAX 6)</span>

                <span id="mp-room-badge" style="font-size:10px; color:#8ad4ff;">CODE: ----</span>

              </div>

              <div id="mp-roster-list"></div>

            </div>


            <div style="display:flex; gap:8px; justify-content:space-between; margin-top:10px; flex-wrap:wrap;">

              <button class="arcade-btn teal sm" data-action="mp-toggle-ready" id="btn-mp-ready">✓ READY UP</button>

              <button class="arcade-btn gold sm" data-action="mp-start-race" id="btn-mp-start">🏁 START 3D RACE</button>

              <button class="arcade-btn purple sm" data-action="mp-export-server" title="Download Colyseus Server Room Source">⬇ SERVER .TS</button>

            </div>

          </div>

        </div>

      </section>


      <!-- 5. ACTIVE 3D RACE HUD -->

      <section id="hud-screen" class="screen-overlay">

        <div class="hud-top-bar">

          <div class="hud-box hud-lap">

            LAP <span id="hud-lap-val">1/3</span>

          </div>


          <div class="hud-box hud-timer">

            ⏱ <span id="hud-time-val">00:00.00</span>

          </div>


          <div style="display:flex; gap:8px; align-items:center;">

            <div class="hud-box hud-pos" id="hud-pos-val">

              1st <small>/6</small>

            </div>

            <button class="arcade-btn blue sm interactive" data-action="race-pause" title="Pause (ESC or P)">❚❚</button>

          </div>

        </div>


        <div id="checkpoint-toast">CHECKPOINT PASSED! +BOOST</div>

        <div id="countdown-banner"></div>


        <!-- Mobile / Tablet On-Screen Controls -->

        <div class="touch-controls">

          <button class="touch-pad-btn" id="touch-left">◀</button>

          <button class="touch-pad-btn" id="touch-up">▲</button>

          <button class="touch-pad-btn" id="touch-down">▼</button>

          <button class="touch-pad-btn" id="touch-right">▶</button>

          <button class="touch-boost-btn" id="touch-boost">🔥 BOOST</button>

        </div>


        <div class="hud-bottom-bar">

          <div class="hud-box minimap-box">

            <canvas id="minimap-canvas" width="116" height="132"></canvas>

          </div>


          <div class="hud-box speedo-box">

            <div class="speed-bars" id="hud-speed-bars"></div>

            <div>

              <span class="speed-readout" id="hud-speed-num">420</span>

              <span class="speed-unit">KM/H</span>

            </div>

            <div class="boost-meter-wrap">

              <div class="boost-meter-fill" id="hud-boost-fill"></div>

            </div>

          </div>

        </div>

      </section>


      <!-- 6. PAUSE OVERLAY -->

      <section id="screen-pause" class="screen-overlay" style="background:rgba(5,10,25,0.65);">

        <div class="retro-panel" style="text-align:center; width:340px;">

          <div style="font-size:16px; color:var(--gold); margin-bottom:18px;">RACE PAUSED</div>

          <button class="arcade-btn gold" style="width:100%;" data-action="pause-resume">▶ RESUME RACE</button>

          <button class="arcade-btn teal" style="width:100%;" data-action="pause-restart">↻ RESTART CIRCUIT</button>

          <button class="arcade-btn red" style="width:100%;" data-action="pause-quit">⬅ EXIT TO MENU</button>

        </div>

      </section>


      <!-- 7. RACE RESULTS OVERLAY -->

      <section id="screen-results" class="screen-overlay" style="background:rgba(5,10,25,0.72);">

        <div class="retro-panel" style="width:min(620px, 94vw); text-align:center;">

          <div id="results-headline" style="font-size:18px; color:var(--gold); margin-bottom:6px;">🏁 GRAND PRIX FINISHED!</div>

          <div id="results-sub" style="font-family:var(--font-hud); font-size:14px; color:#a8e0ff; margin-bottom:14px;">Circuit Standings</div>

          <div id="results-standings" style="margin-bottom:16px; text-align:left;"></div>

          <div style="display:flex; gap:10px; justify-content:center; flex-wrap:wrap;">

            <button class="arcade-btn gold sm" data-action="results-rematch">↻ RACE AGAIN</button>

            <button class="arcade-btn teal sm" data-action="results-routes">🌍 CHANGE ROUTE</button>

            <button class="arcade-btn blue sm" data-action="results-menu">🏠 MAIN MENU</button>

          </div>

        </div>

      </section>


      <!-- 8. OPTIONS & PERSISTENT SETTINGS OVERLAY -->

      <section id="screen-options" class="screen-overlay">

        <div class="retro-panel" style="width:min(540px, 94vw);">

          <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:14px;">

            <span style="font-size:14px; color:var(--gold);">⚙️ OPTIONS & CONTROLS</span>

            <button class="arcade-btn blue sm" data-action="options-close">✓ DONE</button>

          </div>


          <div style="margin-bottom:12px;">

            <label style="font-size:9px; display:flex; justify-content:space-between; margin-bottom:4px;">

              <span>16-BIT MUSIC VOLUME</span> <span id="lbl-opt-music">75%</span>

            </label>

            <input type="range" id="opt-music" min="0" max="100" value="75" style="width:100%;" />

          </div>


          <div style="margin-bottom:12px;">

            <label style="font-size:9px; display:flex; justify-content:space-between; margin-bottom:4px;">

              <span>ARCADE SFX & ENGINE VOLUME</span> <span id="lbl-opt-sfx">80%</span>

            </label>

            <input type="range" id="opt-sfx" min="0" max="100" value="80" style="width:100%;" />

          </div>


          <div style="margin-bottom:12px;">

            <label style="font-size:9px; display:flex; justify-content:space-between; margin-bottom:4px;">

              <span>3D STEERING SENSITIVITY</span> <span id="lbl-opt-sens">1.0x</span>

            </label>

            <input type="range" id="opt-sens" min="50" max="160" value="100" style="width:100%;" />

          </div>


          <div style="margin-bottom:14px; display:flex; justify-content:space-between; align-items:center;">

            <span style="font-size:9px;">AI DIFFICULTY</span>

            <select id="opt-diff" class="lobby-input" style="width:180px;">

              <option value="0.9">ROOKIE (CASUAL)</option>

              <option value="1.0" selected>ARCADE STANDARD</option>

              <option value="1.12">ACE PILOT (PRO)</option>

            </select>

          </div>


          <div style="background:rgba(0,0,0,0.35); border:1px solid #3569b8; border-radius:6px; padding:10px; font-family:var(--font-hud); font-size:13px; line-height:1.45; color:#cce6ff; margin-bottom:12px;">

            <strong>FLIGHT CONTROLS:</strong><br/>

            • <strong>Steer & Bank (3D Roll/Yaw):</strong> A / D or Left / Right Arrow<br/>

            • <strong>Pitch Altitude (Climb / Dive):</strong> W / S or Up / Down Arrow<br/>

            • <strong>Afterburner Boost:</strong> SPACEBAR or SHIFT<br/>

            • <strong>Pause / Camera View:</strong> ESC or P / C Key

          </div>


          <div style="display:flex; justify-content:space-between;">

            <button class="arcade-btn red sm" data-action="options-reset">RESET DEFAULTS</button>

            <button class="arcade-btn teal sm" data-action="options-mute" id="btn-opt-mute">MUTE ALL: OFF</button>

          </div>

        </div>

      </section>


    </div>

  </div>


  <script type="module">

    import * as THREE from 'three';


    // ============================================================================

    // 1. STRUCTURED DATA: ALL 10 AIRCRAFT & ALL 7 WORLD ROUTES

    // ============================================================================

    export const PLANES_DATA = [

      {

        id: 'red-arrow', num: '01', name: 'RED ARROW',

        desc: 'Balanced handling and dependable performance.',

        primary: 0xe6222e, secondary: 0xffffff, accent: 0xffcc29,

        wingStyle: 'swept', tailStyle: 'single',

        stats: { speed: 7, accel: 7, handling: 8, boost: 7 }

      },

      {

        id: 'sky-wolf', num: '02', name: 'SKY WOLF',

        desc: 'High top speed and aggressive acceleration.',

        primary: 0x1660e8, secondary: 0xffffff, accent: 0x66d9ff,

        wingStyle: 'forward', tailStyle: 'twin',

        stats: { speed: 9, accel: 8, handling: 6, boost: 6 }

      },

      {

        id: 'forest-fang', num: '03', name: 'FOREST FANG',

        desc: 'Stable flight and strong handling.',

        primary: 0x199c49, secondary: 0xe8f5e9, accent: 0xaeea00,

        wingStyle: 'wide', tailStyle: 'single',

        stats: { speed: 6, accel: 7, handling: 9, boost: 7 }

      },

      {

        id: 'sun-flare', num: '04', name: 'SUN FLARE',

        desc: 'Excellent acceleration.',

        primary: 0xffb300, secondary: 0xfff8e1, accent: 0xff3d00,

        wingStyle: 'tapered', tailStyle: 'vtail',

        stats: { speed: 7, accel: 10, handling: 7, boost: 6 }

      },

      {

        id: 'violet-storm', num: '05', name: 'VIOLET STORM',

        desc: 'High boost performance.',

        primary: 0x7b1fa2, secondary: 0xf3e5f5, accent: 0x00e5ff,

        wingStyle: 'delta', tailStyle: 'twin',

        stats: { speed: 7, accel: 6, handling: 6, boost: 10 }

      },

      {

        id: 'shadow-rider', num: '06', name: 'SHADOW RIDER',

        desc: 'Agile handling and quick directional changes.',

        primary: 0x262936, secondary: 0xe53935, accent: 0xb0bec5,

        wingStyle: 'forward', tailStyle: 'vtail',

        stats: { speed: 7, accel: 7, handling: 10, boost: 6 }

      },

      {

        id: 'ocean-blade', num: '07', name: 'OCEAN BLADE',

        desc: 'Balanced speed and long-distance stability.',

        primary: 0xf5f9ff, secondary: 0x0288d1, accent: 0xff6d00,

        wingStyle: 'wide', tailStyle: 'single',

        stats: { speed: 8, accel: 7, handling: 8, boost: 7 }

      },

      {

        id: 'fire-hawk', num: '08', name: 'FIRE HAWK',

        desc: 'High speed and powerful boost.',

        primary: 0xff6d00, secondary: 0xfff3e0, accent: 0xd50000,

        wingStyle: 'swept', tailStyle: 'twin',

        stats: { speed: 9, accel: 6, handling: 5, boost: 9 }

      },

      {

        id: 'tide-jet', num: '09', name: 'TIDE JET',

        desc: 'Smooth steering and consistent acceleration.',

        primary: 0x00bfa5, secondary: 0xe0f2f1, accent: 0xffea00,

        wingStyle: 'pods', tailStyle: 'single',

        stats: { speed: 6, accel: 8, handling: 9, boost: 7 }

      },

      {

        id: 'pink-flash', num: '10', name: 'PINK FLASH',

        desc: 'Lightweight handling and rapid acceleration.',

        primary: 0xf50057, secondary: 0xfce4ec, accent: 0x00e5ff,

        wingStyle: 'tapered', tailStyle: 'vtail',

        stats: { speed: 7, accel: 9, handling: 9, boost: 6 }

      }

    ];


    export const ROUTES_DATA = [

      {

        id: 'miami',

        name: 'ROUTE 1 — MIAMI, USA',

        subtitle: 'Art Deco South Beach, Turquoise Bay & Palm Islands',

        difficulty: 'EASY',

        skyTop: 0x1b75ff, skyHorizon: 0x99ebff, waterColor: 0x00c2d1, groundColor: 0xf5deb3,

        musicRoot: 261.63,

        windForce: 0.15,

        waypoints: [

          [0, 26, 0], [180, 28, -280], [60, 34, -620], [-220, 26, -740],

          [-420, 24, -460], [-360, 30, -140]

        ]

      },

      {

        id: 'tokyo',

        name: 'ROUTE 2 — TOKYO, JAPAN',

        subtitle: 'Tokyo Tower, Neon Metropolis, Elevated Expressways & Mt. Fuji',

        difficulty: 'MEDIUM',

        skyTop: 0x0f1c4d, skyHorizon: 0x5c6bc0, waterColor: 0x15315c, groundColor: 0x263238,

        musicRoot: 293.66,

        windForce: 0.25,

        waypoints: [

          [0, 30, 0], [240, 38, -260], [160, 24, -580], [-140, 42, -780],

          [-440, 32, -520], [-320, 26, -180]

        ]

      },

      {

        id: 'rome',

        name: 'ROUTE 3 — ROME, ITALY',

        subtitle: 'The Colosseum, Ancient Aqueducts, Domes & Tiber River',

        difficulty: 'MEDIUM',

        skyTop: 0x2979ff, skyHorizon: 0xffe0b2, waterColor: 0x1e88e5, groundColor: 0xd7ccc8,

        musicRoot: 246.94,

        windForce: 0.2,

        waypoints: [

          [0, 28, 0], [210, 32, -310], [-20, 36, -640], [-290, 28, -680],

          [-480, 34, -360], [-260, 26, -120]

        ]

      },

      {

        id: 'shanghai',

        name: 'ROUTE 4 — SHANGHAI, CHINA',

        subtitle: 'Oriental Pearl Tower, Pudong Skyline & Huangpu Bridges',

        difficulty: 'HARD',

        skyTop: 0x102a5c, skyHorizon: 0x4dd0e1, waterColor: 0x0d47a1, groundColor: 0x37474f,

        musicRoot: 329.63,

        windForce: 0.35,

        waypoints: [

          [0, 32, 0], [280, 44, -240], [220, 26, -600], [-160, 46, -820],

          [-500, 30, -540], [-380, 28, -200]

        ]

      },

      {

        id: 'egypt',

        name: 'ROUTE 5 — EGYPT (GIZA & NILE)',

        subtitle: 'Great Pyramids of Giza, Great Sphinx & Golden Nile Delta',

        difficulty: 'MEDIUM',

        skyTop: 0x1565c0, skyHorizon: 0xffd54f, waterColor: 0x0277bd, groundColor: 0xfbc02d,

        musicRoot: 220.0,

        windForce: 0.4,

        waypoints: [

          [0, 28, 0], [230, 26, -300], [110, 38, -660], [-240, 32, -740],

          [-460, 26, -420], [-290, 28, -140]

        ]

      },

      {

        id: 'paris',

        name: 'ROUTE 6 — PARIS, FRANCE',

        subtitle: 'Eiffel Tower, Seine River Bridges & Haussmann Rooftops',

        difficulty: 'EASY',

        skyTop: 0x42a5f5, skyHorizon: 0xe3f2fd, waterColor: 0x29b6f6, groundColor: 0xcfd8dc,

        musicRoot: 277.18,

        windForce: 0.18,

        waypoints: [

          [0, 28, 0], [190, 34, -290], [40, 30, -610], [-250, 34, -710],

          [-430, 28, -430], [-310, 26, -150]

        ]

      },

      {

        id: 'brazil',

        name: 'ROUTE 7 — RIO DE JANEIRO, BRAZIL',

        subtitle: 'Christ the Redeemer, Sugarloaf Peaks & Guanabara Bay',

        difficulty: 'ACE',

        skyTop: 0x00838f, skyHorizon: 0xb2ff59, waterColor: 0x00acc1, groundColor: 0x2e7d32,

        musicRoot: 349.23,

        windForce: 0.32,

        waypoints: [

          [0, 32, 0], [260, 48, -270], [140, 26, -630], [-210, 50, -790],

          [-490, 28, -480], [-340, 32, -160]

        ]

      }

    ];


    // ============================================================================

    // 2. PERSISTENT SETTINGS MANAGER

    // ============================================================================

    const STORAGE_KEY = 'skyrace_3d_settings_v2';

    const DEFAULT_SETTINGS = {

      musicVolume: 75,

      sfxVolume: 80,

      muted: false,

      sensitivity: 1.0,

      aiDifficulty: 1.0,

      selectedPlaneIndex: 0,

      selectedRouteIndex: 0,

      pilotName: 'ACE-' + Math.floor(10 + Math.random() * 89)

    };


    function loadSettings() {

      try {

        const raw = localStorage.getItem(STORAGE_KEY);

        if (!raw) return { ...DEFAULT_SETTINGS };

        return { ...DEFAULT_SETTINGS, ...JSON.parse(raw) };

      } catch {

        return { ...DEFAULT_SETTINGS };

      }

    }


    function saveSettings(settings) {

      try {

        localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));

      } catch {

        // Ignore storage blocked errors

      }

    }


    const appSettings = loadSettings();


    // ============================================================================

    // 3. ORIGINAL 16-BIT CHIPTUNE WEB AUDIO SYNTHESIZER (PRESERVED RETRO IDENTITY)

    // ============================================================================

    class ChiptuneAudioEngine {

      constructor() {

        this.ctx = null;

        this.musicTimer = null;

        this.currentMode = 'none';

        this.step = 0;

        this.routeFreq = 261.63;

        this.engineOsc = null;

        this.engineGain = null;

      }


      ensureContext() {

        if (!this.ctx) {

          const AudioCtx = window.AudioContext || window.webkitAudioContext;

          if (AudioCtx) this.ctx = new AudioCtx();

        }

        if (this.ctx && this.ctx.state === 'suspended') {

          this.ctx.resume().catch(() => {});

        }

      }


      getMusicGain() {

        if (appSettings.muted) return 0;

        return (appSettings.musicVolume / 100) * 0.14;

      }


      getSfxGain() {

        if (appSettings.muted) return 0;

        return (appSettings.sfxVolume / 100) * 0.22;

      }


      playTone(freq, duration = 0.12, type = 'square', gainScale = 1.0, slideTo = null) {

        this.ensureContext();

        if (!this.ctx || appSettings.muted) return;

        const now = this.ctx.currentTime;

        const osc = this.ctx.createOscillator();

        const gain = this.ctx.createGain();

        osc.type = type;

        osc.frequency.setValueAtTime(freq, now);

        if (slideTo) {

          osc.frequency.exponentialRampToValueAtTime(Math.max(40, slideTo), now + duration);

        }

        const vol = Math.max(0.0001, this.getSfxGain() * gainScale);

        gain.gain.setValueAtTime(vol, now);

        gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);

        osc.connect(gain);

        gain.connect(this.ctx.destination);

        osc.start(now);

        osc.stop(now + duration + 0.02);

      }


      playUiClick() { this.playTone(660, 0.06, 'square', 0.6, 880); }

      playUiConfirm() {

        this.playTone(523.25, 0.08, 'square', 0.7);

        setTimeout(() => this.playTone(783.99, 0.14, 'square', 0.8), 70);

      }

      playCountdownBeep(finalGo = false) {

        this.playTone(finalGo ? 880 : 440, finalGo ? 0.32 : 0.15, 'square', 0.9);

      }

      playCheckpoint() {

        this.playTone(587.33, 0.09, 'triangle', 0.85);

        setTimeout(() => this.playTone(880, 0.16, 'square', 0.85), 80);

      }

      playBoostSound() {

        this.playTone(180, 0.25, 'sawtooth', 0.8, 620);

      }

      playVictoryFanfare() {

        const notes = [523.25, 659.25, 783.99, 1046.5];

        notes.forEach((n, idx) => {

          setTimeout(() => this.playTone(n, 0.18, 'square', 0.85), idx * 130);

        });

      }


      startLoop(mode = 'menu', routeRoot = 261.63) {

        this.ensureContext();

        this.routeFreq = routeRoot;

        if (this.currentMode === mode && this.musicTimer) return;

        this.stopLoop();

        this.currentMode = mode;

        this.step = 0;


        const tempoMs = mode === 'race' ? 135 : 175;

        const ratios = mode === 'race'

          ? [1, 1.25, 1.5, 1.25, 1.333, 1.5, 1.875, 2.0]

          : [1, 1.125, 1.25, 1.5, 1.25, 1.5, 2.0, 1.5];


        this.musicTimer = setInterval(() => {

          if (!this.ctx || appSettings.muted || appSettings.musicVolume <= 0) return;

          const now = this.ctx.currentTime;

          const root = mode === 'race' ? this.routeFreq : 220;

          const note = root * ratios[this.step % ratios.length];

          const bass = (root * 0.5) * (this.step % 2 === 0 ? 1 : 1.5);


          const leadOsc = this.ctx.createOscillator();

          const leadGain = this.ctx.createGain();

          leadOsc.type = 'square';

          leadOsc.frequency.setValueAtTime(note, now);

          const mVol = this.getMusicGain();

          leadGain.gain.setValueAtTime(mVol, now);

          leadGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.11);

          leadOsc.connect(leadGain);

          leadGain.connect(this.ctx.destination);

          leadOsc.start(now);

          leadOsc.stop(now + 0.12);


          if (this.step % 2 === 0) {

            const bassOsc = this.ctx.createOscillator();

            const bassGain = this.ctx.createGain();

            bassOsc.type = 'triangle';

            bassOsc.frequency.setValueAtTime(bass, now);

            bassGain.gain.setValueAtTime(mVol * 0.9, now);

            bassGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.18);

            bassOsc.connect(bassGain);

            bassGain.connect(this.ctx.destination);

            bassOsc.start(now);

            bassOsc.stop(now + 0.2);

          }


          this.step++;

        }, tempoMs);

      }


      updateEnginePitch(speedKmh, active) {

        if (!this.ctx || appSettings.muted || !active) {

          if (this.engineGain && this.ctx) {

            this.engineGain.gain.setValueAtTime(0.0001, this.ctx.currentTime);

          }

          return;

        }

        if (!this.engineOsc) {

          this.engineOsc = this.ctx.createOscillator();

          this.engineGain = this.ctx.createGain();

          this.engineOsc.type = 'sawtooth';

          this.engineGain.gain.setValueAtTime(0.0001, this.ctx.currentTime);

          this.engineOsc.connect(this.engineGain);

          this.engineGain.connect(this.ctx.destination);

          this.engineOsc.start();

        }

        const now = this.ctx.currentTime;

        const targetFreq = 58 + (speedKmh / 550) * 115;

        this.engineOsc.frequency.setTargetAtTime(targetFreq, now, 0.05);

        this.engineGain.gain.setTargetAtTime(this.getSfxGain() * 0.16, now, 0.05);

      }


      stopLoop() {

        if (this.musicTimer) {

          clearInterval(this.musicTimer);

          this.musicTimer = null;

        }

        this.currentMode = 'none';

      }

    }


    const audio = new ChiptuneAudioEngine();


    // ============================================================================

    // 4. 3D LOW-POLY AIRCRAFT FACTORY & WORLD BUILDERS (THREE.JS)

    // ============================================================================

    function makeLowPolyMaterial(colorHex, roughness = 0.48, metalness = 0.12, emissiveHex = 0x000000) {

      return new THREE.MeshStandardMaterial({

        color: colorHex,

        roughness,

        metalness,

        emissive: emissiveHex,

        flatShading: true

      });

    }


    export function createLowPolyAirplaneMesh(planeConfig) {

      const group = new THREE.Group();

      group.name = `Plane_${planeConfig.id}`;


      const matPrimary = makeLowPolyMaterial(planeConfig.primary, 0.4, 0.18);

      const matSecondary = makeLowPolyMaterial(planeConfig.secondary, 0.45, 0.12);

      const matAccent = makeLowPolyMaterial(planeConfig.accent, 0.35, 0.2);

      const matDark = makeLowPolyMaterial(0x1b2234, 0.6, 0.3);

      const matCanopy = new THREE.MeshStandardMaterial({

        color: 0x7ce8ff,

        roughness: 0.15,

        metalness: 0.75,

        transparent: true,

        opacity: 0.84,

        flatShading: true

      });


      // 1. Nose Cowl + Main Fuselage + Rear Tail Cone

      const cowlGeo = new THREE.CylinderGeometry(0.42, 0.56, 1.15, 8);

      cowlGeo.rotateX(Math.PI / 2);

      const cowl = new THREE.Mesh(cowlGeo, matSecondary);

      cowl.position.z = -1.65;

      cowl.castShadow = true;

      group.add(cowl);


      const bodyGeo = new THREE.CylinderGeometry(0.56, 0.48, 2.2, 8);

      bodyGeo.rotateX(Math.PI / 2);

      const body = new THREE.Mesh(bodyGeo, matPrimary);

      body.position.z = 0;

      body.castShadow = true;

      group.add(body);


      const tailConeGeo = new THREE.CylinderGeometry(0.48, 0.18, 1.85, 8);

      tailConeGeo.rotateX(Math.PI / 2);

      const tailCone = new THREE.Mesh(tailConeGeo, matPrimary);

      tailCone.position.z = 2.0;

      tailCone.castShadow = true;

      group.add(tailCone);


      // Fuselage Racing Stripe Band

      const bandGeo = new THREE.CylinderGeometry(0.575, 0.53, 0.55, 8);

      bandGeo.rotateX(Math.PI / 2);

      const band = new THREE.Mesh(bandGeo, matAccent);

      band.position.z = -0.35;

      group.add(band);


      // 2. Bubble Canopy & Pilot Helmet

      const canopyGeo = new THREE.dodecahedronGeometry ? new THREE.DodecahedronGeometry(0.44, 0) : new THREE.SphereGeometry(0.44, 7, 5);

      canopyGeo.scale(0.88, 0.72, 2.1);

      const canopy = new THREE.Mesh(canopyGeo, matCanopy);

      canopy.position.set(0, 0.42, -0.1);

      group.add(canopy);


      const helmetGeo = new THREE.OctahedronGeometry(0.19, 1);

      const helmet = new THREE.Mesh(helmetGeo, matSecondary);

      helmet.position.set(0, 0.48, -0.05);

      group.add(helmet);


      // 3. Main Wings (Varied by planeConfig.wingStyle)

      const wingSpan = planeConfig.wingStyle === 'wide' ? 6.2 : 5.4;

      const wingChord = planeConfig.wingStyle === 'delta' ? 1.65 : 1.15;

      const wingGeo = new THREE.BoxGeometry(wingSpan, 0.14, wingChord);

      const mainWing = new THREE.Mesh(wingGeo, matPrimary);

      mainWing.position.set(0, -0.05, -0.2);

      mainWing.castShadow = true;

      group.add(mainWing);


      // Checkered/Striped Wingtip Panels

      [-1, 1].forEach((side) => {

        const stripe = new THREE.Mesh(

          new THREE.BoxGeometry(0.85, 0.155, wingChord * 0.96),

          matSecondary

        );

        stripe.position.set(side * (wingSpan * 0.34), -0.05, -0.2);

        group.add(stripe);


        const tip = new THREE.Mesh(

          new THREE.BoxGeometry(0.42, 0.16, wingChord * 0.92),

          matAccent

        );

        tip.position.set(side * (wingSpan * 0.47), -0.04, -0.2);

        group.add(tip);


        if (planeConfig.wingStyle === 'pods') {

          const podGeo = new THREE.ConeGeometry(0.18, 0.9, 6);

          podGeo.rotateX(Math.PI / 2);

          const pod = new THREE.Mesh(podGeo, matAccent);

          pod.position.set(side * (wingSpan * 0.49), -0.05, -0.2);

          group.add(pod);

        }

      });


      // 4. Tail Fin & Horizontal Stabilizers

      const hStab = new THREE.Mesh(new THREE.BoxGeometry(2.15, 0.11, 0.68), matSecondary);

      hStab.position.set(0, 0.08, 2.55);

      group.add(hStab);


      if (planeConfig.tailStyle === 'twin') {

        [-0.55, 0.55].forEach((xOff) => {

          const vFin = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.95, 0.78), matAccent);

          vFin.position.set(xOff, 0.5, 2.48);

          vFin.rotation.z = xOff > 0 ? -0.14 : 0.14;

          group.add(vFin);

        });

      } else if (planeConfig.tailStyle === 'vtail') {

        [-0.35, 0.35].forEach((xOff) => {

          const vFin = new THREE.Mesh(new THREE.BoxGeometry(0.12, 1.0, 0.75), matAccent);

          vFin.position.set(xOff, 0.48, 2.5);

          vFin.rotation.z = xOff > 0 ? -0.48 : 0.48;

          group.add(vFin);

        });

      } else {

        const vFin = new THREE.Mesh(new THREE.BoxGeometry(0.14, 1.08, 0.85), matPrimary);

        vFin.position.set(0, 0.56, 2.48);

        const vFinTip = new THREE.Mesh(new THREE.BoxGeometry(0.15, 0.32, 0.82), matSecondary);

        vFinTip.position.set(0, 0.95, 2.48);

        group.add(vFin, vFinTip);

      }


      // 5. Fixed Landing Gear Spats

      [-0.78, 0.78].forEach((xOff) => {

        const strut = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.52, 0.14), matDark);

        strut.position.set(xOff, -0.38, -0.55);

        strut.rotation.z = xOff > 0 ? -0.22 : 0.22;

        const spatGeo = new THREE.OctahedronGeometry(0.22, 1);

        spatGeo.scale(0.65, 0.7, 1.8);

        const spat = new THREE.Mesh(spatGeo, matPrimary);

        spat.position.set(xOff * 1.1, -0.64, -0.55);

        group.add(strut, spat);

      });


      // 6. Spinning 3-Blade Propeller + Spinner Cone

      const propGroup = new THREE.Group();

      propGroup.position.set(0, 0, -2.28);

      const spinnerGeo = new THREE.ConeGeometry(0.32, 0.55, 8);

      spinnerGeo.rotateX(-Math.PI / 2);

      const spinner = new THREE.Mesh(spinnerGeo, matAccent);

      propGroup.add(spinner);


      for (let b = 0; b < 3; b++) {

        const bladeWrap = new THREE.Group();

        bladeWrap.rotation.z = (b * Math.PI * 2) / 3;

        const blade = new THREE.Mesh(new THREE.BoxGeometry(0.14, 1.35, 0.04), matDark);

        blade.position.y = 0.65;

        const tip = new THREE.Mesh(new THREE.BoxGeometry(0.145, 0.22, 0.045), matAccent);

        tip.position.y = 1.22;

        bladeWrap.add(blade, tip);

        propGroup.add(bladeWrap);

      }

      group.add(propGroup);


      // 7. Boost Afterburner Flame Cone (Toggled when boosting)

      const flameMat = new THREE.MeshBasicMaterial({

        color: 0x00ffff,

        transparent: true,

        opacity: 0.85

      });

      const flameGeo = new THREE.ConeGeometry(0.28, 1.4, 7);

      flameGeo.rotateX(Math.PI / 2);

      const flameMesh = new THREE.Mesh(flameGeo, flameMat);

      flameMesh.position.set(0, 0, 3.35);

      flameMesh.visible = false;

      group.add(flameMesh);


      group.userData = {

        propeller: propGroup,

        flame: flameMesh,

        wingSpan

      };


      return group;

    }


    // Create a 3D Checkered Race Gate Arch matching Reference 1 & 3

    function createRaceGateArch3D(gateIndex, isStartLine = false) {

      const gateGroup = new THREE.Group();

      const pillarHeight = 16;

      const archWidth = 24;


      const redMat = makeLowPolyMaterial(0xe6222e, 0.45);

      const whiteMat = makeLowPolyMaterial(0xf8fbff, 0.45);

      const bannerMat = makeLowPolyMaterial(isStartLine ? 0xffcc29 : 0xe6222e, 0.35, 0.1, isStartLine ? 0x332200 : 0x220000);


      // Left & Right Checkered Pylons

      [-archWidth / 2, archWidth / 2].forEach((xPos) => {

        const pylonGroup = new THREE.Group();

        pylonGroup.position.set(xPos, 0, 0);

        const segCount = 6;

        const segH = pillarHeight / segCount;

        for (let i = 0; i < segCount; i++) {

          const seg = new THREE.Mesh(

            new THREE.CylinderGeometry(0.85, 0.95, segH, 7),

            i % 2 === 0 ? redMat : whiteMat

          );

          seg.position.y = (i - segCount / 2 + 0.5) * segH;

          seg.castShadow = true;

          pylonGroup.add(seg);

        }

        gateGroup.add(pylonGroup);

      });


      // Top Overhead Truss Banner with ">>>" Chevrons

      const topBeam = new THREE.Mesh(

        new THREE.BoxGeometry(archWidth + 2.2, 2.4, 1.4),

        bannerMat

      );

      topBeam.position.y = pillarHeight / 2 + 0.8;

      gateGroup.add(topBeam);


      for (let c = -1; c <= 1; c++) {

        const chev = new THREE.Mesh(new THREE.OctahedronGeometry(0.7, 0), whiteMat);

        chev.position.set(c * 2.5, pillarHeight / 2 + 0.8, 0.8);

        gateGroup.add(chev);

      }


      const ringMat = new THREE.MeshBasicMaterial({

        color: isStartLine ? 0xffea00 : 0x52e5ff,

        wireframe: true,

        transparent: true,

        opacity: 0.35

      });

      const ring = new THREE.Mesh(new THREE.RingGeometry(8.5, 9.2, 12), ringMat);

      gateGroup.add(ring);


      gateGroup.userData = { ring, gateIndex };

      return gateGroup;

    }


    // Create a Low-Poly Cloud Cluster

    function createLowPolyCloudCluster(scale = 1) {

      const cloud = new THREE.Group();

      const cloudMat = new THREE.MeshStandardMaterial({

        color: 0xffffff,

        roughness: 0.85,

        metalness: 0.0,

        flatShading: true

      });

      const puffs = [

        [0, 0, 0, 4.2],

        [-3.2, -0.6, 0.8, 3.1],

        [3.4, -0.4, -0.5, 3.3],

        [1.2, 1.6, 0.4, 2.8],

        [-1.5, 1.3, -0.6, 2.6]

      ];

      puffs.forEach(([x, y, z, r]) => {

        const geo = new THREE.DodecahedronGeometry(r, 1);

        const mesh = new THREE.Mesh(geo, cloudMat);

        mesh.position.set(x, y, z);

        cloud.add(mesh);

      });

      cloud.scale.setScalar(scale);

      return cloud;

    }


    // Build Route-Specific 3D Low-Poly World Landmarks

    function buildRouteLandmark3D(routeId) {

      const landmark = new THREE.Group();

      const stoneMat = makeLowPolyMaterial(0xe0cda9, 0.7);

      const redMat = makeLowPolyMaterial(0xe53935, 0.45);

      const whiteMat = makeLowPolyMaterial(0xf5f7fa, 0.45);

      const goldMat = makeLowPolyMaterial(0xffca28, 0.3, 0.4);

      const glassMat = makeLowPolyMaterial(0x29b6f6, 0.25, 0.6);

      const greenMat = makeLowPolyMaterial(0x2e7d32, 0.75);


      if (routeId === 'miami') {

        for (let i = 0; i < 9; i++) {

          const h = 18 + (i % 4) * 10;

          const bldg = new THREE.Mesh(

            new THREE.BoxGeometry(10, h, 10),

            makeLowPolyMaterial(i % 2 === 0 ? 0xff80ab : 0x80deea, 0.4)

          );

          bldg.position.set((i - 4) * 16, h / 2, -25);

          landmark.add(bldg);

        }

      } else if (routeId === 'tokyo') {

        const base = new THREE.Mesh(new THREE.CylinderGeometry(3, 14, 42, 4), redMat);

        base.position.y = 21;

        const mid = new THREE.Mesh(new THREE.CylinderGeometry(1.5, 3, 26, 4), whiteMat);

        mid.position.y = 55;

        const fuji = new THREE.Mesh(new THREE.ConeGeometry(65, 58, 9), makeLowPolyMaterial(0x3949ab, 0.7));

        fuji.position.set(-110, 29, -120);

        const snowCap = new THREE.Mesh(new THREE.ConeGeometry(24, 22, 9), whiteMat);

        snowCap.position.set(-110, 48, -120);

        landmark.add(base, mid, fuji, snowCap);

      } else if (routeId === 'rome') {

        const colOuter = new THREE.Mesh(new THREE.CylinderGeometry(28, 28, 22, 14, 1, true), stoneMat);

        colOuter.material.side = THREE.DoubleSide;

        colOuter.position.y = 11;

        const dome = new THREE.Mesh(new THREE.SphereGeometry(16, 10, 8), stoneMat);

        dome.position.set(55, 18, -20);

        landmark.add(colOuter, dome);

      } else if (routeId === 'shanghai') {

        const column = new THREE.Mesh(new THREE.CylinderGeometry(2.6, 4.2, 68, 8), whiteMat);

        column.position.y = 34;

        const lowerSphere = new THREE.Mesh(new THREE.DodecahedronGeometry(10, 1), makeLowPolyMaterial(0xf50057, 0.3));

        lowerSphere.position.y = 24;

        const upperSphere = new THREE.Mesh(new THREE.DodecahedronGeometry(6.5, 1), makeLowPolyMaterial(0xf50057, 0.3));

        upperSphere.position.y = 52;

        landmark.add(column, lowerSphere, upperSphere);

      } else if (routeId === 'egypt') {

        const p1 = new THREE.Mesh(new THREE.ConeGeometry(38, 46, 4), goldMat);

        p1.position.set(0, 23, 0);

        const p2 = new THREE.Mesh(new THREE.ConeGeometry(28, 34, 4), stoneMat);

        p2.position.set(58, 17, -38);

        landmark.add(p1, p2);

      } else if (routeId === 'paris') {

        const eiffelMat = makeLowPolyMaterial(0x8d6e63, 0.5, 0.3);

        const tier1 = new THREE.Mesh(new THREE.CylinderGeometry(7, 16, 22, 4), eiffelMat);

        tier1.position.y = 11;

        const tier2 = new THREE.Mesh(new THREE.CylinderGeometry(3.2, 7, 24, 4), eiffelMat);

        tier2.position.y = 34;

        const spire = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 3.2, 28, 4), eiffelMat);

        spire.position.y = 60;

        landmark.add(tier1, tier2, spire);

      } else {

        const peak = new THREE.Mesh(new THREE.ConeGeometry(34, 52, 8), greenMat);

        peak.position.y = 26;

        const torso = new THREE.Mesh(new THREE.BoxGeometry(4.5, 16, 3.5), whiteMat);

        torso.position.y = 58;

        const arms = new THREE.Mesh(new THREE.BoxGeometry(16, 2.8, 2.8), whiteMat);

        arms.position.y = 62;

        landmark.add(peak, torso, arms);

      }


      return landmark;

    }


    // ============================================================================

    // 5. CORE 3D ENGINE & SCENE CONTROLLER

    // ============================================================================

    const threeContainer = document.getElementById('three-container');

    const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });

    renderer.setSize(window.innerWidth, window.innerHeight);

    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));

    renderer.shadowMap.enabled = true;

    renderer.shadowMap.type = THREE.PCFSoftShadowMap;

    renderer.toneMapping = THREE.ACESFilmicToneMapping;

    renderer.toneMappingExposure = 1.1;

    threeContainer.appendChild(renderer.domElement);


    const scene = new THREE.Scene();

    scene.background = new THREE.Color(0x1b75ff);

    scene.fog = new THREE.FogExp2(0x8cd3ff, 0.0018);


    const camera = new THREE.PerspectiveCamera(58, window.innerWidth / window.innerHeight, 0.5, 1600);


    const hemiLight = new THREE.HemisphereLight(0xffffff, 0x29437a, 1.15);

    scene.add(hemiLight);


    const sunLight = new THREE.DirectionalLight(0xfff5d6, 2.1);

    sunLight.position.set(120, 220, 140);

    sunLight.castShadow = true;

    sunLight.shadow.mapSize.set(1024, 1024);

    sunLight.shadow.camera.near = 10;

    sunLight.shadow.camera.far = 650;

    const d = 140;

    sunLight.shadow.camera.left = -d;

    sunLight.shadow.camera.right = d;

    sunLight.shadow.camera.top = d;

    sunLight.shadow.camera.bottom = -d;

    scene.add(sunLight);


    const worldRoot = new THREE.Group();

    scene.add(worldRoot);


    function clearWorldRoot() {

      while (worldRoot.children.length > 0) {

        const child = worldRoot.children[0];

        worldRoot.remove(child);

        child.traverse((obj) => {

          if (obj.geometry) obj.geometry.dispose();

        });

      }

    }


    // ============================================================================

    // 6. SCENE BUILDERS: MENU DIORAMA, 3D HANGAR TURNTABLE, & 3D RACE WORLD

    // ============================================================================

    let currentAppState = 'menu';

    let planesReturnTarget = 'menu';

    let cameraModeIndex = 0; // 0: Chase, 1: Far Chase, 2: Nose/Cockpit


    let menuAnimData = null;

    let hangarAnimData = null;

    let raceState = null;


    // --- 6A. BUILD 3D MAIN MENU DIORAMA ---

    function buildMenu3DDiorama() {

      clearWorldRoot();

      scene.background.setHex(0x1e78ff);

      scene.fog.color.setHex(0x9be2ff);

      scene.fog.density = 0.0022;


      const water = new THREE.Mesh(

        new THREE.PlaneGeometry(1400, 1400),

        makeLowPolyMaterial(0x0088cc, 0.35, 0.2)

      );

      water.rotation.x = -Math.PI / 2;

      water.position.y = -18;

      worldRoot.add(water);


      const mtnGroup = new THREE.Group();

      const cliff = new THREE.Mesh(new THREE.ConeGeometry(42, 68, 7), makeLowPolyMaterial(0x2e7d32, 0.8));

      cliff.position.set(-48, 12, -55);

      const snowPeak = new THREE.Mesh(new THREE.ConeGeometry(36, 75, 7), makeLowPolyMaterial(0xecfffe, 0.5));

      snowPeak.position.set(-72, 26, -95);

      const castle = new THREE.Mesh(new THREE.BoxGeometry(14, 10, 12), makeLowPolyMaterial(0xffe0b2, 0.6));

      castle.position.set(-46, 38, -48);

      const spire = new THREE.Mesh(new THREE.ConeGeometry(4, 10, 6), makeLowPolyMaterial(0x1565c0, 0.5));

      spire.position.set(-46, 48, -48);

      const waterfall = new THREE.Mesh(new THREE.BoxGeometry(5, 42, 2), makeLowPolyMaterial(0x80d8ff, 0.2));

      waterfall.position.set(-38, 12, -38);

      mtnGroup.add(cliff, snowPeak, castle, spire, waterfall);

      worldRoot.add(mtnGroup);


      const island = new THREE.Mesh(new THREE.CylinderGeometry(22, 32, 12, 8), makeLowPolyMaterial(0x388e3c, 0.8));

      island.position.set(36, -13, -72);

      worldRoot.add(island);


      const gate = createRaceGateArch3D(0, true);

      gate.position.set(16, -2, -62);

      gate.scale.setScalar(0.85);

      worldRoot.add(gate);


      const clouds = [];

      for (let i = 0; i < 14; i++) {

        const c = createLowPolyCloudCluster(1.2 + (i % 3) * 0.5);

        c.position.set((Math.random() - 0.5) * 180, -4 + Math.random() * 35, -30 - Math.random() * 140);

        worldRoot.add(c);

        clouds.push(c);

      }


      const heroPlane = createLowPolyAirplaneMesh(PLANES_DATA[appSettings.selectedPlaneIndex]);

      heroPlane.position.set(-7.5, -1.8, 11.5);

      heroPlane.rotation.set(0.18, -0.55, -0.42);

      worldRoot.add(heroPlane);


      const rivalBlue = createLowPolyAirplaneMesh(PLANES_DATA[1]);

      rivalBlue.position.set(11, 5.2, -8);

      rivalBlue.rotation.set(-0.08, 0.65, 0.32);

      worldRoot.add(rivalBlue);


      const rivalYellow = createLowPolyAirplaneMesh(PLANES_DATA[3]);

      rivalYellow.position.set(14, 1.2, -18);

      rivalYellow.rotation.set(0.05, 0.72, 0.25);

      worldRoot.add(rivalYellow);


      camera.position.set(0, 2.5, 22);

      camera.lookAt(0, 1.5, -20);


      menuAnimData = { heroPlane, rivalBlue, rivalYellow, clouds, t: 0 };

    }


    // --- 6B. BUILD 3D HANGAR TURNTABLE SCENE ---

    function buildHangar3DScene() {

      clearWorldRoot();

      scene.background.setHex(0x1652b8);

      scene.fog.color.setHex(0x64b5f6);

      scene.fog.density = 0.004;


      const tarmac = new THREE.Mesh(

        new THREE.CylinderGeometry(85, 85, 1, 24),

        makeLowPolyMaterial(0x283044, 0.8, 0.1)

      );

      tarmac.position.y = -2.1;

      tarmac.receiveShadow = true;

      worldRoot.add(tarmac);


      const padBase = new THREE.Mesh(

        new THREE.CylinderGeometry(5.2, 5.8, 0.45, 24),

        makeLowPolyMaterial(0x1b2848, 0.4, 0.5)

      );

      padBase.position.set(-1.6, -1.65, 0);

      padBase.receiveShadow = true;

      worldRoot.add(padBase);


      const neonRing = new THREE.Mesh(

        new THREE.RingGeometry(4.5, 4.85, 32),

        new THREE.MeshBasicMaterial({ color: 0x00e5ff, side: THREE.DoubleSide })

      );

      neonRing.rotation.x = -Math.PI / 2;

      neonRing.position.set(-1.6, -1.41, 0);

      worldRoot.add(neonRing);


      const hangarWall = new THREE.Mesh(

        new THREE.BoxGeometry(22, 12, 14),

        makeLowPolyMaterial(0x37474f, 0.7)

      );

      hangarWall.position.set(-18, 4, -12);

      const hangarRoof = new THREE.Mesh(

        new THREE.BoxGeometry(24, 1.2, 16),

        makeLowPolyMaterial(0xd84315, 0.6)

      );

      hangarRoof.position.set(-18, 10.2, -12);

      worldRoot.add(hangarWall, hangarRoof);


      const distMtn = new THREE.Mesh(

        new THREE.ConeGeometry(28, 38, 8),

        makeLowPolyMaterial(0x3949ab, 0.75)

      );

      distMtn.position.set(-8, 15, -55);

      worldRoot.add(distMtn);


      const turntablePivot = new THREE.Group();

      turntablePivot.position.set(-1.6, -0.55, 0);

      worldRoot.add(turntablePivot);


      camera.position.set(-1.2, 1.55, 9.8);

      camera.lookAt(-1.2, -0.2, 0);


      hangarAnimData = {

        pivot: turntablePivot,

        planeMesh: null,

        rotY: 0.65,

        isDragging: false,

        lastX: 0

      };


      updateHangarSelectedPlane(appSettings.selectedPlaneIndex);

    }


    function updateHangarSelectedPlane(index) {

      appSettings.selectedPlaneIndex = index;

      saveSettings(appSettings);

      const planeData = PLANES_DATA[index];


      document.getElementById('hangar-plane-name').textContent = planeData.name;

      document.getElementById('hangar-plane-class').textContent = `Racer Class • #${planeData.num}`;

      document.getElementById('hangar-plane-desc').textContent = planeData.desc;


      const renderBar = (elId, val) => {

        const container = document.getElementById(elId);

        container.innerHTML = '';

        for (let i = 1; i <= 10; i++) {

          const seg = document.createElement('div');

          seg.className = 'stat-seg' + (i <= val ? ' filled' : '');

          container.appendChild(seg);

        }

      };

      renderBar('stat-speed', planeData.stats.speed);

      renderBar('stat-accel', planeData.stats.accel);

      renderBar('stat-handling', planeData.stats.handling);

      renderBar('stat-boost', planeData.stats.boost);


      document.querySelectorAll('.plane-card').forEach((card, idx) => {

        card.classList.toggle('selected', idx === index);

      });


      if (hangarAnimData && hangarAnimData.pivot) {

        while (hangarAnimData.pivot.children.length > 0) {

          hangarAnimData.pivot.remove(hangarAnimData.pivot.children[0]);

        }

        const mesh = createLowPolyAirplaneMesh(planeData);

        mesh.scale.setScalar(1.08);

        hangarAnimData.pivot.add(mesh);

        hangarAnimData.planeMesh = mesh;

      }

    }


    // --- 6C. BUILD 3D SPLINE RACE WORLD FOR ANY OF THE 7 ROUTES ---

    function build3DRaceWorld(routeConfig, isMultiplayer = false, customRoster = null) {

      clearWorldRoot();

      const route = routeConfig || ROUTES_DATA[appSettings.selectedRouteIndex];


      scene.background.setHex(route.skyTop);

      scene.fog.color.setHex(route.skyHorizon);

      scene.fog.density = 0.0014;


      const pts = route.waypoints.map(([x, y, z]) => new THREE.Vector3(x, y, z));

      const curve = new THREE.CatmullRomCurve3(pts, true, 'catmullrom', 0.5);

      const trackLength = curve.getLength();


      const waterPlane = new THREE.Mesh(

        new THREE.PlaneGeometry(3200, 3200),

        makeLowPolyMaterial(route.waterColor, 0.35, 0.15)

      );

      waterPlane.rotation.x = -Math.PI / 2;

      waterPlane.position.y = -4;

      worldRoot.add(waterPlane);


      const gateCount = 18;

      const gates = [];

      for (let i = 0; i < gateCount; i++) {

        const u = i / gateCount;

        const pos = curve.getPointAt(u);

        const tangent = curve.getTangentAt(u).normalize();

        const gateMesh = createRaceGateArch3D(i, i === 0);

        gateMesh.position.copy(pos);

        const lookTarget = pos.clone().add(tangent);

        gateMesh.lookAt(lookTarget);

        worldRoot.add(gateMesh);

        gates.push({ u, pos, tangent, mesh: gateMesh, passedByPlayer: false });

      }


      for (let k = 0; k < 28; k++) {

        const u = k / 28;

        const pos = curve.getPointAt(u);

        const tangent = curve.getTangentAt(u).normalize();

        const right = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();

        const side = k % 2 === 0 ? 1 : -1;

        const offsetDist = 48 + (k % 3) * 26;


        const islandPos = pos.clone().addScaledVector(right, side * offsetDist);

        islandPos.y = -4;


        const hillH = 22 + (k % 5) * 14;

        const hill = new THREE.Mesh(

          new THREE.ConeGeometry(26 + (k % 4) * 8, hillH, 7),

          makeLowPolyMaterial(route.groundColor, 0.8)

        );

        hill.position.copy(islandPos);

        hill.position.y = hillH / 2 - 5;

        worldRoot.add(hill);


        if (k % 4 === 0) {

          const lm = buildRouteLandmark3D(route.id);

          lm.position.copy(islandPos);

          lm.position.y = 0;

          lm.scale.setScalar(0.65);

          worldRoot.add(lm);

        }

      }


      const clouds = [];

      for (let c = 0; c < 36; c++) {

        const u = (c / 36) % 1;

        const p = curve.getPointAt(u);

        const cloud = createLowPolyCloudCluster(1.5 + (c % 3) * 0.7);

        cloud.position.set(

          p.x + (Math.random() - 0.5) * 240,

          p.y + 18 + (Math.random() - 0.5) * 32,

          p.z + (Math.random() - 0.5) * 240

        );

        worldRoot.add(cloud);

        clouds.push(cloud);

      }


      const racers = [];

      const playerPlaneData = PLANES_DATA[appSettings.selectedPlaneIndex];


      const makeRacer = (id, name, planeData, isLocal, isAI, startSlot) => {

        const mesh = createLowPolyAirplaneMesh(planeData);

        worldRoot.add(mesh);


        const trailMax = 34;

        const leftTrailPositions = new Float32Array(trailMax * 3);

        const rightTrailPositions = new Float32Array(trailMax * 3);

        const trailMat = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.65 });

        const leftLine = new THREE.Line(new THREE.BufferGeometry(), trailMat);

        const rightLine = new THREE.Line(new THREE.BufferGeometry(), trailMat);

        leftLine.frustumCulled = false;

        rightLine.frustumCulled = false;

        worldRoot.add(leftLine, rightLine);


        const lateralSlots = [0, -7, 7, -13, 13, -4];

        const startS = (trackLength - startSlot * 14) % trackLength;


        return {

          id,

          name,

          planeData,

          mesh,

          isLocal,

          isAI,

          s: startS,

          lap: 1,

          totalDistance: -startSlot * 14,

          xOffset: lateralSlots[startSlot % lateralSlots.length],

          yOffset: (startSlot % 2) * 1.5,

          vx: 0,

          vy: 0,

          speedKmh: 280,

          boostEnergy: 100,

          boosting: false,

          bankAngle: 0,

          pitchAngle: 0,

          nextGateIndex: 1,

          finished: false,

          finishTime: 0,

          aiSkill: 0.94 + (startSlot * 0.025) * appSettings.aiDifficulty,

          aiTargetX: (startSlot - 2.5) * 3.8,

          leftTrail: [],

          rightTrail: [],

          leftLine,

          rightLine,

          leftTrailPositions,

          rightTrailPositions

        };

      };


      racers.push(makeRacer('local', appSettings.pilotName, playerPlaneData, true, false, 0));


      if (isMultiplayer && Array.isArray(customRoster) && customRoster.length > 0) {

        let slotIdx = 1;

        customRoster.forEach((peer) => {

          if (peer.id === mpState.clientId) return;

          const pData = PLANES_DATA.find((p) => p.id === peer.planeId) || PLANES_DATA[slotIdx % PLANES_DATA.length];

          racers.push(makeRacer(peer.id, peer.name, pData, false, !!peer.isAI, slotIdx++));

        });

      } else {

        for (let r = 1; r <= 5; r++) {

          const aiPlaneIdx = (appSettings.selectedPlaneIndex + r * 2) % PLANES_DATA.length;

          const aiPlane = PLANES_DATA[aiPlaneIdx];

          racers.push(makeRacer(`ai-${r}`, aiPlane.name, aiPlane, false, true, r));

        }

      }


      raceState = {

        route,

        curve,

        trackLength,

        gates,

        clouds,

        racers,

        localRacer: racers[0],

        totalLaps: 3,

        status: 'countdown',

        countdownTimer: 3.4,

        lastBeepSecond: 4,

        elapsedTime: 0,

        isMultiplayer

      };


      positionRacersIn3DSpace(0.016);

      updateChaseCamera(1.0);

    }


    // Position all racers along the 3D closed spline

    function positionRacersIn3DSpace(dt) {

      if (!raceState) return;

      const { curve, trackLength, racers } = raceState;

      const upVec = new THREE.Vector3(0, 1, 0);


      racers.forEach((r) => {

        const u = ((r.s % trackLength) + trackLength) % trackLength / trackLength;

        const centerPt = curve.getPointAt(u);

        const tangent = curve.getTangentAt(u).normalize();

        const right = new THREE.Vector3().crossVectors(tangent, upVec).normalize();

        const normalUp = new THREE.Vector3().crossVectors(right, tangent).normalize();


        const worldPos = centerPt.clone()

          .addScaledVector(right, r.xOffset)

          .addScaledVector(normalUp, r.yOffset);


        r.mesh.position.copy(worldPos);


        const lookTarget = worldPos.clone()

          .addScaledVector(tangent, 12)

          .addScaledVector(right, r.vx * 0.28)

          .addScaledVector(normalUp, r.vy * 0.35);


        r.mesh.lookAt(lookTarget);

        r.mesh.rotateZ(r.bankAngle);


        if (r.mesh.userData.propeller) {

          r.mesh.userData.propeller.rotation.z += dt * (28 + r.speedKmh * 0.08);

        }

        if (r.mesh.userData.flame) {

          r.mesh.userData.flame.visible = r.boosting;

          if (r.boosting) {

            r.mesh.userData.flame.scale.setScalar(0.85 + Math.random() * 0.35);

          }

        }


        const halfSpan = (r.mesh.userData.wingSpan || 5.2) * 0.48;

        const leftTip = new THREE.Vector3(-halfSpan, -0.05, 0.3).applyMatrix4(r.mesh.matrixWorld);

        const rightTip = new THREE.Vector3(halfSpan, -0.05, 0.3).applyMatrix4(r.mesh.matrixWorld);


        r.leftTrail.unshift(leftTip);

        r.rightTrail.unshift(rightTip);

        if (r.leftTrail.length > 26) r.leftTrail.pop();

        if (r.rightTrail.length > 26) r.rightTrail.pop();


        const updateRibbon = (ptsArr, lineObj) => {

          const flat = new Float32Array(ptsArr.length * 3);

          for (let i = 0; i < ptsArr.length; i++) {

            flat[i * 3] = ptsArr[i].x;

            flat[i * 3 + 1] = ptsArr[i].y;

            flat[i * 3 + 2] = ptsArr[i].z;

          }

          lineObj.geometry.setAttribute('position', new THREE.BufferAttribute(flat, 3));

        };

        if (r.leftTrail.length > 1) {

          updateRibbon(r.leftTrail, r.leftLine);

          updateRibbon(r.rightTrail, r.rightLine);

        }

      });

    }


    function updateChaseCamera(lerpFactor = 0.14) {

      if (!raceState || !raceState.localRacer) return;

      const player = raceState.localRacer;

      const { curve, trackLength } = raceState;


      const u = ((player.s % trackLength) + trackLength) % trackLength / trackLength;

      const tangent = curve.getTangentAt(u).normalize();

      const upVec = new THREE.Vector3(0, 1, 0);

      const right = new THREE.Vector3().crossVectors(tangent, upVec).normalize();


      const planePos = player.mesh.position;

      let camDist = 9.5;

      let camHeight = 2.85;

      let lookAhead = 18;


      if (cameraModeIndex === 1) {

        camDist = 14.2;

        camHeight = 4.4;

      } else if (cameraModeIndex === 2) {

        camDist = -1.6;

        camHeight = 0.65;

        lookAhead = 35;

      }


      const desiredCamPos = planePos.clone()

        .addScaledVector(tangent, -camDist)

        .addScaledVector(upVec, camHeight)

        .addScaledVector(right, player.xOffset * -0.05);


      camera.position.lerp(desiredCamPos, lerpFactor);


      const lookTarget = planePos.clone()

        .addScaledVector(tangent, lookAhead)

        .addScaledVector(upVec, 0.7);

      camera.lookAt(lookTarget);

    }


    // ============================================================================

    // 7. INPUT HANDLING (KEYBOARD, MOUSE TURNTABLE & MULTI-TOUCH)

    // ============================================================================

    const inputState = {

      left: false,

      right: false,

      up: false,

      down: false,

      boost: false

    };


    window.addEventListener('keydown', (e) => {

      if (['ArrowLeft', 'KeyA'].includes(e.code)) inputState.left = true;

      if (['ArrowRight', 'KeyD'].includes(e.code)) inputState.right = true;

      if (['ArrowUp', 'KeyW'].includes(e.code)) inputState.up = true;

      if (['ArrowDown', 'KeyS'].includes(e.code)) inputState.down = true;

      if (['Space', 'ShiftLeft', 'ShiftRight'].includes(e.code)) {

        inputState.boost = true;

        if (currentAppState === 'racing') e.preventDefault();

      }

      if (['Escape', 'KeyP'].includes(e.code)) {

        if (currentAppState === 'racing') showScreen('pause');

        else if (currentAppState === 'pause') showScreen('racing');

      }

      if (e.code === 'KeyC' && currentAppState === 'racing') {

        cycleCameraMode();

      }

    });


    window.addEventListener('keyup', (e) => {

      if (['ArrowLeft', 'KeyA'].includes(e.code)) inputState.left = false;

      if (['ArrowRight', 'KeyD'].includes(e.code)) inputState.right = false;

      if (['ArrowUp', 'KeyW'].includes(e.code)) inputState.up = false;

      if (['ArrowDown', 'KeyS'].includes(e.code)) inputState.down = false;

      if (['Space', 'ShiftLeft', 'ShiftRight'].includes(e.code)) inputState.boost = false;

    });


    function bindTouchButton(elId, keyProp) {

      const btn = document.getElementById(elId);

      if (!btn) return;

      const setFlag = (val, ev) => {

        if (ev) ev.preventDefault();

        inputState[keyProp] = val;

      };

      btn.addEventListener('pointerdown', (e) => setFlag(true, e));

      btn.addEventListener('pointerup', (e) => setFlag(false, e));

      btn.addEventListener('pointerleave', (e) => setFlag(false, e));

      btn.addEventListener('pointercancel', (e) => setFlag(false, e));

    }

    bindTouchButton('touch-left', 'left');

    bindTouchButton('touch-right', 'right');

    bindTouchButton('touch-up', 'up');

    bindTouchButton('touch-down', 'down');

    bindTouchButton('touch-boost', 'boost');


    window.addEventListener('pointerdown', (e) => {

      if (currentAppState === 'planes' && hangarAnimData) {

        hangarAnimData.isDragging = true;

        hangarAnimData.lastX = e.clientX;

      }

    });

    window.addEventListener('pointermove', (e) => {

      if (currentAppState === 'planes' && hangarAnimData && hangarAnimData.isDragging) {

        const dx = e.clientX - hangarAnimData.lastX;

        hangarAnimData.lastX = e.clientX;

        hangarAnimData.rotY += dx * 0.012;

      }

    });

    window.addEventListener('pointerup', () => {

      if (hangarAnimData) hangarAnimData.isDragging = false;

    });


    // ============================================================================

    // 8. RACE SIMULATION LOOP, CHECKPOINT DETECTION & HUD RENDERING

    // ============================================================================

    const minimapCanvas = document.getElementById('minimap-canvas');

    const minimapCtx = minimapCanvas.getContext('2d');


    const speedBarsEl = document.getElementById('hud-speed-bars');

    for (let i = 0; i < 14; i++) {

      const seg = document.createElement('div');

      seg.className = 'speed-bar-seg';

      speedBarsEl.appendChild(seg);

    }


    function showCheckpointToast(msg) {

      const el = document.getElementById('checkpoint-toast');

      el.textContent = msg;

      el.style.opacity = '1';

      clearTimeout(el._timer);

      el._timer = setTimeout(() => { el.style.opacity = '0'; }, 1100);

    }


    function formatTime(sec) {

      const m = Math.floor(sec / 60);

      const s = Math.floor(sec % 60);

      const ms = Math.floor((sec % 1) * 100);

      return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(ms).padStart(2, '0')}`;

    }


    function ordinalSuffix(rank) {

      if (rank === 1) return '1st';

      if (rank === 2) return '2nd';

      if (rank === 3) return '3rd';

      return `${rank}th`;

    }


    function updateRaceSimulation(dt) {

      if (!raceState) return;

      const { localRacer, racers, gates, trackLength, totalLaps, route } = raceState;


      if (raceState.status === 'countdown') {

        raceState.countdownTimer -= dt;

        const secLeft = Math.ceil(raceState.countdownTimer);

        const banner = document.getElementById('countdown-banner');

        if (secLeft > 0) {

          banner.textContent = secLeft === 4 ? 'READY' : String(secLeft);

          if (secLeft < raceState.lastBeepSecond && secLeft <= 3) {

            raceState.lastBeepSecond = secLeft;

            audio.playCountdownBeep(false);

          }

        } else {

          banner.textContent = 'GO!!';

          audio.playCountdownBeep(true);

          raceState.status = 'active';

          setTimeout(() => {

            if (document.getElementById('countdown-banner').textContent === 'GO!!') {

              document.getElementById('countdown-banner').textContent = '';

            }

          }, 700);

        }

        positionRacersIn3DSpace(dt);

        updateChaseCamera(0.15);

        return;

      }


      if (raceState.status !== 'active') return;


      raceState.elapsedTime += dt;


      // --- 8A. UPDATE LOCAL PLAYER 3D FLIGHT PHYSICS ---

      const stats = localRacer.planeData.stats;

      const topSpeed = 415 + stats.speed * 13;

      const accelRate = 115 + stats.accel * 15;

      const handleRate = (18 + stats.handling * 2.1) * appSettings.sensitivity;

      const boostBonus = 95 + stats.boost * 9;


      const steerInput = (inputState.right ? 1 : 0) - (inputState.left ? 1 : 0);

      const pitchInput = (inputState.up ? 1 : 0) - (inputState.down ? 1 : 0);


      if (inputState.boost && localRacer.boostEnergy > 4) {

        if (!localRacer.boosting) audio.playBoostSound();

        localRacer.boosting = true;

        localRacer.boostEnergy = Math.max(0, localRacer.boostEnergy - dt * 28);

      } else {

        localRacer.boosting = false;

        localRacer.boostEnergy = Math.min(100, localRacer.boostEnergy + dt * (11 + stats.boost * 1.1));

      }


      const windDrift = Math.sin(raceState.elapsedTime * 0.9) * route.windForce;

      localRacer.vx = THREE.MathUtils.lerp(localRacer.vx, steerInput * handleRate + windDrift, dt * 6.5);

      localRacer.vy = THREE.MathUtils.lerp(localRacer.vy, pitchInput * (handleRate * 0.55), dt * 6.0);


      localRacer.xOffset += localRacer.vx * dt;

      localRacer.yOffset = THREE.MathUtils.clamp(localRacer.yOffset + localRacer.vy * dt, -5.5, 8.5);


      const offCoursePenalty = Math.abs(localRacer.xOffset) > 13.5 ? 0.82 : 1.0;

      if (Math.abs(localRacer.xOffset) > 18) {

        localRacer.xOffset = THREE.MathUtils.lerp(localRacer.xOffset, Math.sign(localRacer.xOffset) * 11, dt * 4);

      }


      const targetSpeed = (topSpeed + (localRacer.boosting ? boostBonus : 0)) * offCoursePenalty;

      localRacer.speedKmh = THREE.MathUtils.lerp(localRacer.speedKmh, targetSpeed, dt * (accelRate / 95));


      localRacer.bankAngle = THREE.MathUtils.lerp(localRacer.bankAngle, -steerInput * 0.68, dt * 8.5);


      const forwardStep = (localRacer.speedKmh * 0.26) * dt;

      const prevS = localRacer.s;

      localRacer.s = (localRacer.s + forwardStep) % trackLength;

      localRacer.totalDistance += forwardStep;


      // Check Gate Progression

      const activeGate = gates[localRacer.nextGateIndex];

      if (activeGate) {

        const gateS = activeGate.u * trackLength;

        const distToGate = Math.abs(localRacer.s - gateS);

        if (distToGate < 14) {

          if (Math.abs(localRacer.xOffset) < 13.5) {

            localRacer.boostEnergy = Math.min(100, localRacer.boostEnergy + 12);

            audio.playCheckpoint();

            showCheckpointToast(`GATE ${localRacer.nextGateIndex + 1}/${gates.length} CLEAR! +BOOST`);

          }

          localRacer.nextGateIndex = (localRacer.nextGateIndex + 1) % gates.length;

        }

      }


      // Check Lap Completion

      if (prevS > trackLength * 0.82 && localRacer.s < trackLength * 0.18) {

        if (localRacer.lap < totalLaps) {

          localRacer.lap++;

          audio.playCheckpoint();

          showCheckpointToast(`🏁 LAP ${localRacer.lap}/${totalLaps}!`);

        } else if (!localRacer.finished) {

          localRacer.finished = true;

          localRacer.finishTime = raceState.elapsedTime;

          audio.playVictoryFanfare();

          completeRaceAndShowStandings();

          return;

        }

      }


      // --- 8B. UPDATE AI & REMOTE MULTIPLAYER OPPONENTS ---

      racers.forEach((r) => {

        if (r.isLocal || r.finished) return;

        if (r.isAI) {

          const aiStats = r.planeData.stats;

          const aiBaseSpeed = (402 + aiStats.speed * 11.5) * r.aiSkill;

          const wave = Math.sin(raceState.elapsedTime * 1.4 + r.s * 0.02) * 14;

          r.speedKmh = THREE.MathUtils.lerp(r.speedKmh, aiBaseSpeed + wave, dt * 2.5);

          r.boosting = Math.sin(raceState.elapsedTime * 0.8 + r.aiSkill * 10) > 0.78;


          const desiredX = Math.sin(r.s * 0.015 + r.aiSkill * 6) * 7.5;

          const dx = desiredX - r.xOffset;

          r.vx = THREE.MathUtils.clamp(dx * 2.2, -14, 14);

          r.xOffset += r.vx * dt;

          r.yOffset = Math.cos(r.s * 0.02 + r.aiSkill * 4) * 2.4;

          r.bankAngle = THREE.MathUtils.lerp(r.bankAngle, -THREE.MathUtils.clamp(dx * 0.08, -0.55, 0.55), dt * 6);


          const step = (r.speedKmh * 0.26) * dt;

          const oldS = r.s;

          r.s = (r.s + step) % trackLength;

          r.totalDistance += step;


          if (oldS > trackLength * 0.82 && r.s < trackLength * 0.18) {

            if (r.lap < totalLaps) r.lap++;

            else {

              r.finished = true;

              r.finishTime = raceState.elapsedTime;

            }

          }

        }

      });


      // Soft Plane-to-Plane Wing Bump Separation

      for (let i = 0; i < racers.length; i++) {

        for (let j = i + 1; j < racers.length; j++) {

          const a = racers[i];

          const b = racers[j];

          if (Math.abs(a.s - b.s) < 6.5 && Math.abs(a.xOffset - b.xOffset) < 3.4) {

            const push = a.xOffset >= b.xOffset ? 1 : -1;

            a.xOffset += push * 8 * dt;

            b.xOffset -= push * 8 * dt;

          }

        }

      }


      if (raceState.isMultiplayer) {

        broadcastMultiplayerTelemetry(localRacer);

      }


      positionRacersIn3DSpace(dt);

      updateChaseCamera(0.16);

      audio.updateEnginePitch(localRacer.speedKmh, true);


      updateRaceHUD();

    }


    function updateRaceHUD() {

      if (!raceState) return;

      const { localRacer, racers, totalLaps } = raceState;


      const sorted = [...racers].sort((a, b) => b.totalDistance - a.totalDistance);

      const playerRank = sorted.findIndex((r) => r.id === localRacer.id) + 1;


      document.getElementById('hud-lap-val').textContent = `${Math.min(localRacer.lap, totalLaps)}/${totalLaps}`;

      document.getElementById('hud-time-val').textContent = formatTime(raceState.elapsedTime);

      document.getElementById('hud-pos-val').innerHTML = `${ordinalSuffix(playerRank)} <small>/${racers.length}</small>`;

      document.getElementById('hud-speed-num').textContent = String(Math.round(localRacer.speedKmh));

      document.getElementById('hud-boost-fill').style.width = `${Math.round(localRacer.boostEnergy)}%`;


      const activeSegs = Math.round((localRacer.speedKmh / 620) * 14);

      const segNodes = speedBarsEl.children;

      for (let i = 0; i < segNodes.length; i++) {

        if (i < activeSegs) {

          segNodes[i].style.background = i > 10 ? '#ff3b2f' : (i > 6 ? '#ffcc29' : '#35e86b');

        } else {

          segNodes[i].style.background = '#1b2d59';

        }

      }


      drawCourseMinimap();

    }


    function drawCourseMinimap() {

      if (!raceState) return;

      const { curve, trackLength, racers, localRacer } = raceState;

      const w = minimapCanvas.width;

      const h = minimapCanvas.height;

      minimapCtx.clearRect(0, 0, w, h);


      const sampleCount = 60;

      let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;

      const pts = [];

      for (let i = 0; i <= sampleCount; i++) {

        const p = curve.getPointAt(i / sampleCount);

        pts.push(p);

        if (p.x < minX) minX = p.x;

        if (p.x > maxX) maxX = p.x;

        if (p.z < minZ) minZ = p.z;

        if (p.z > maxZ) maxZ = p.z;

      }


      const pad = 16;

      const mapX = (x) => pad + ((x - minX) / Math.max(1, maxX - minX)) * (w - pad * 2);

      const mapY = (z) => pad + ((z - minZ) / Math.max(1, maxZ - minZ)) * (h - pad * 2);


      minimapCtx.strokeStyle = '#6ec6ff';

      minimapCtx.lineWidth = 4;

      minimapCtx.beginPath();

      pts.forEach((p, idx) => {

        const mx = mapX(p.x);

        const my = mapY(p.z);

        if (idx === 0) minimapCtx.moveTo(mx, my);

        else minimapCtx.lineTo(mx, my);

      });

      minimapCtx.closePath();

      minimapCtx.stroke();


      const startPt = pts[0];

      minimapCtx.fillStyle = '#ffffff';

      minimapCtx.fillRect(mapX(startPt.x) - 5, mapY(startPt.z) - 5, 10, 10);


      racers.forEach((r) => {

        const u = ((r.s % trackLength) + trackLength) % trackLength / trackLength;

        const p = curve.getPointAt(u);

        const mx = mapX(p.x);

        const my = mapY(p.z);

        minimapCtx.beginPath();

        minimapCtx.arc(mx, my, r.isLocal ? 5 : 3.5, 0, Math.PI * 2);

        minimapCtx.fillStyle = r.isLocal ? '#ff2938' : '#ffcc29';

        minimapCtx.fill();

        minimapCtx.strokeStyle = '#ffffff';

        minimapCtx.lineWidth = 1.5;

        minimapCtx.stroke();

      });

    }


    function completeRaceAndShowStandings() {

      if (!raceState) return;

      raceState.status = 'finished';

      audio.updateEnginePitch(0, false);


      const sorted = [...raceState.racers].sort((a, b) => b.totalDistance - a.totalDistance);

      const playerRank = sorted.findIndex((r) => r.id === 'local') + 1;


      document.getElementById('results-headline').textContent =

        playerRank === 1 ? '🏆 1ST PLACE VICTORY!' : `🏁 FINISHED ${ordinalSuffix(playerRank)} PLACE!`;

      document.getElementById('results-sub').textContent =

        `${raceState.route.name} • Total Time: ${formatTime(raceState.elapsedTime)}`;


      const listEl = document.getElementById('results-standings');

      listEl.innerHTML = '';

      sorted.forEach((r, idx) => {

        const row = document.createElement('div');

        row.className = 'player-slot';

        if (r.isLocal) row.style.borderColor = '#ffcc29';

        const timeStr = r.finishTime > 0

          ? formatTime(r.finishTime)

          : formatTime(raceState.elapsedTime + (idx * 1.42));

        row.innerHTML = `

          <span><strong>#${idx + 1}</strong> ${r.name} <small style="color:#8ad4ff;">(${r.planeData.name})</small></span>

          <span style="color:#ffcc29;">${timeStr}</span>

        `;

        listEl.appendChild(row);

      });


      showScreen('results');

    }


    // ============================================================================

    // 9. ONLINE MULTIPLAYER (SERVERLESS P2P WEBRTC + LOCAL TAB SYNC + COLYSEUS)

    // ============================================================================

    const mpState = {

      clientId: 'p-' + Math.random().toString(36).substring(2, 8),

      roomCode: '',

      isHost: false,

      ready: true,

      mode: 'webrtc', // 'webrtc', 'cloud', or 'colyseus'

      channel: typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel('skyrace_3d_net_v2') : null,

      ws: null,

      peer: null,

      hostConn: null,

      clientConns: new Map(), // peerId -> DataConnection (used by Host)

      players: new Map()

    };


    function setMpLog(msg, color = '#74f7a5') {

      const el = document.getElementById('mp-status-log');

      if (el) {

        el.textContent = msg;

        el.style.color = color;

      }

    }


    function teardownPeerConnections() {

      if (mpState.hostConn) {

        try { mpState.hostConn.close(); } catch {}

        mpState.hostConn = null;

      }

      mpState.clientConns.forEach((conn) => {

        try { conn.close(); } catch {}

      });

      mpState.clientConns.clear();

      if (mpState.peer) {

        try { mpState.peer.destroy(); } catch {}

        mpState.peer = null;

      }

    }


    function refreshLobbyRosterUI() {

      document.getElementById('mp-room-badge').textContent =

        mpState.roomCode ? `CODE: ${mpState.roomCode}` : 'CODE: ----';


      const listEl = document.getElementById('mp-roster-list');

      listEl.innerHTML = '';


      const entries = Array.from(mpState.players.values());

      if (entries.length === 0) {

        const empty = document.createElement('div');

        empty.className = 'player-slot';

        empty.innerHTML = `<span style="color:#9ecfff;">No active room. Click HOST ROOM or QUICK MATCH.</span>`;

        listEl.appendChild(empty);

        return;

      }


      entries.forEach((p) => {

        const row = document.createElement('div');

        row.className = 'player-slot';

        const planeObj = PLANES_DATA.find((pl) => pl.id === p.planeId) || PLANES_DATA[0];

        row.innerHTML = `

          <span>${p.isHost ? '👑 ' : '✈️ '}<strong>${p.name}</strong> <small style="color:#8ad4ff;">(${planeObj.name})</small></span>

          <span class="status-pill ${p.ready ? 'ready' : 'wait'}">${p.ready ? 'READY' : 'WAITING'}</span>

        `;

        listEl.appendChild(row);

      });

    }


    function sendNetPacket(type, payload = {}) {

      const packet = {

        type,

        roomCode: mpState.roomCode,

        senderId: mpState.clientId,

        payload

      };


      // Always mirror to BroadcastChannel for instant local tab testing

      if (mpState.channel) {

        mpState.channel.postMessage(packet);

      }


      if (mpState.mode === 'webrtc') {

        if (mpState.isHost) {

          // Host broadcasts to all connected WebRTC peers

          mpState.clientConns.forEach((conn) => {

            if (conn && conn.open) {

              try { conn.send(packet); } catch {}

            }

          });

        } else if (mpState.hostConn && mpState.hostConn.open) {

          // Client sends to Host over WebRTC DataChannel

          try { mpState.hostConn.send(packet); } catch {}

        }

      } else if (mpState.mode === 'colyseus' && mpState.ws && mpState.ws.readyState === WebSocket.OPEN) {

        mpState.ws.send(JSON.stringify(packet));

      }

    }


    function handleIncomingNetPacket(msg, fromConn = null) {

      if (!msg || !msg.roomCode || msg.roomCode !== mpState.roomCode) return;


      if (msg.type === 'JOIN_REQ') {

        mpState.players.set(msg.senderId, msg.payload);

        refreshLobbyRosterUI();

        if (mpState.isHost) {

          sendNetPacket('ROOM_STATE', {

            routeId: document.getElementById('mp-route-select').value,

            players: Array.from(mpState.players.values())

          });

        }

      } else if (msg.type === 'ROOM_STATE') {

        mpState.players.clear();

        (msg.payload.players || []).forEach((p) => mpState.players.set(p.id, p));

        if (msg.payload.routeId) {

          document.getElementById('mp-route-select').value = msg.payload.routeId;

        }

        refreshLobbyRosterUI();

      } else if (msg.type === 'START_RACE') {

        const routeIdx = ROUTES_DATA.findIndex((r) => r.id === msg.payload.routeId);

        if (routeIdx >= 0) appSettings.selectedRouteIndex = routeIdx;

        start3DRaceSession(true, msg.payload.roster);

      } else if (msg.type === 'TELEMETRY' && raceState && raceState.isMultiplayer) {

        const t = msg.payload;

        let remoteRacer = raceState.racers.find((r) => r.id === msg.senderId);

        if (!remoteRacer && raceState.racers.length < 6) {

          const pData = PLANES_DATA.find((p) => p.id === t.planeId) || PLANES_DATA[1];

          const mesh = createLowPolyAirplaneMesh(pData);

          worldRoot.add(mesh);

          remoteRacer = {

            id: msg.senderId,

            name: t.name || 'PEER',

            planeData: pData,

            mesh,

            isLocal: false,

            isAI: false,

            s: t.s || 0,

            lap: t.lap || 1,

            totalDistance: t.totalDistance || 0,

            xOffset: t.xOffset || 0,

            yOffset: t.yOffset || 0,

            vx: 0, vy: 0,

            speedKmh: t.speedKmh || 400,

            boostEnergy: 100,

            boosting: !!t.boosting,

            bankAngle: t.bankAngle || 0,

            nextGateIndex: 1,

            finished: false,

            finishTime: 0,

            leftTrail: [], rightTrail: [],

            leftLine: new THREE.Line(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: 0xffffff })),

            rightLine: new THREE.Line(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: 0xffffff }))

          };

          worldRoot.add(remoteRacer.leftLine, remoteRacer.rightLine);

          raceState.racers.push(remoteRacer);

        }

        if (remoteRacer && !remoteRacer.isLocal) {

          remoteRacer.s = THREE.MathUtils.lerp(remoteRacer.s, t.s, 0.5);

          remoteRacer.xOffset = THREE.MathUtils.lerp(remoteRacer.xOffset, t.xOffset, 0.5);

          remoteRacer.yOffset = THREE.MathUtils.lerp(remoteRacer.yOffset, t.yOffset, 0.5);

          remoteRacer.bankAngle = t.bankAngle;

          remoteRacer.speedKmh = t.speedKmh;

          remoteRacer.boosting = t.boosting;

          remoteRacer.totalDistance = t.totalDistance;

          remoteRacer.lap = t.lap;

        }

        // If we are WebRTC Host, relay this client's telemetry to all other clients

        if (mpState.mode === 'webrtc' && mpState.isHost && fromConn) {

          mpState.clientConns.forEach((conn) => {

            if (conn !== fromConn && conn.open) {

              try { conn.send(msg); } catch {}

            }

          });

        }

      }

    }


    if (mpState.channel) {

      mpState.channel.onmessage = (ev) => handleIncomingNetPacket(ev.data);

    }


    function broadcastMultiplayerTelemetry(localRacer) {

      sendNetPacket('TELEMETRY', {

        name: localRacer.name,

        planeId: localRacer.planeData.id,

        s: localRacer.s,

        xOffset: localRacer.xOffset,

        yOffset: localRacer.yOffset,

        bankAngle: localRacer.bankAngle,

        speedKmh: localRacer.speedKmh,

        boosting: localRacer.boosting,

        totalDistance: localRacer.totalDistance,

        lap: localRacer.lap

      });

    }


    function getLocalPlayerSummary() {

      const nameInput = document.getElementById('mp-pilot-name').value.trim() || appSettings.pilotName;

      appSettings.pilotName = nameInput;

      saveSettings(appSettings);

      return {

        id: mpState.clientId,

        name: nameInput,

        planeId: PLANES_DATA[appSettings.selectedPlaneIndex].id,

        ready: mpState.ready,

        isHost: mpState.isHost,

        isAI: false

      };

    }


    // Host or Join a Room (supports Serverless Internet WebRTC via PeerJS, Local Tab Sync, or Colyseus)

    function joinOrCreateMultiplayerRoom(code, isHostFlag) {

      mpState.mode = document.getElementById('mp-mode-select').value;

      mpState.roomCode = code.toUpperCase();

      mpState.isHost = isHostFlag;

      mpState.players.clear();


      const localPlayer = getLocalPlayerSummary();

      mpState.players.set(mpState.clientId, localPlayer);

      refreshLobbyRosterUI();


      // 1. COLYSEUS WEBSOCKET MODE

      if (mpState.mode === 'colyseus') {

        teardownPeerConnections();

        const url = document.getElementById('mp-server-url').value.trim();

        setMpLog(`Connecting to Colyseus Server at ${url}...`, '#ffd54f');

        try {

          if (mpState.ws) mpState.ws.close();

          mpState.ws = new WebSocket(url);

          mpState.ws.onopen = () => {

            setMpLog(`Connected to Colyseus Room [${mpState.roomCode}]!`, '#74f7a5');

            sendNetPacket('JOIN_REQ', localPlayer);

          };

          mpState.ws.onmessage = (ev) => {

            try { handleIncomingNetPacket(JSON.parse(ev.data)); } catch {}

          };

          mpState.ws.onerror = () => {

            setMpLog(`Colyseus server unreachable at ${url}. Switch to Internet P2P (WebRTC) for zero-server play.`, '#ff8a80');

          };

        } catch {

          setMpLog(`Invalid WebSocket URL (${url}).`, '#ff8a80');

        }

        return;

      }


      // 2. SERVERLESS INTERNET P2P WEBRTC MODE (PeerJS)

      if (mpState.mode === 'webrtc' && typeof window.Peer !== 'undefined') {

        teardownPeerConnections();

        const hostPeerId = `skyrace3d-room-${mpState.roomCode}`;


        if (isHostFlag) {

          setMpLog(`Registering Internet P2P Room [${mpState.roomCode}] via WebRTC...`, '#ffd54f');

          const peer = new window.Peer(hostPeerId);

          mpState.peer = peer;


          peer.on('open', () => {

            setMpLog(`Room [${mpState.roomCode}] LIVE on Internet P2P! Share Code or click COPY INVITE LINK.`, '#74f7a5');

            sendNetPacket('JOIN_REQ', localPlayer);

          });


          peer.on('connection', (conn) => {

            mpState.clientConns.set(conn.peer, conn);

            conn.on('data', (data) => handleIncomingNetPacket(data, conn));

            conn.on('close', () => {

              mpState.clientConns.delete(conn.peer);

            });

          });


          peer.on('error', (err) => {

            if (err && err.type === 'unavailable-id') {

              // Room already exists on PeerJS broker -> join as client automatically!

              mpState.isHost = false;

              joinOrCreateMultiplayerRoom(mpState.roomCode, false);

            } else {

              setMpLog(`WebRTC Notice (${err.type || 'network'}): Same-PC BroadcastChannel sync active.`, '#ffd54f');

            }

          });

        } else {

          setMpLog(`Dialing Internet P2P Room [${mpState.roomCode}]...`, '#ffd54f');

          const peer = new window.Peer();

          mpState.peer = peer;


          peer.on('open', () => {

            const conn = peer.connect(hostPeerId, { reliable: true });

            mpState.hostConn = conn;


            conn.on('open', () => {

              setMpLog(`Connected to Host in Room [${mpState.roomCode}] via WebRTC P2P!`, '#74f7a5');

              sendNetPacket('JOIN_REQ', getLocalPlayerSummary());

            });


            conn.on('data', (data) => handleIncomingNetPacket(data, conn));

            conn.on('close', () => {

              setMpLog(`Host closed Room [${mpState.roomCode}].`, '#ff8a80');

            });

          });


          peer.on('error', (err) => {

            if (err && err.type === 'peer-unavailable' && mpState.roomCode === 'PUB01') {

              // Quick match room not hosted yet -> become Host of PUB01!

              setMpLog(`No active Quick Match host found — promoting you to Host of [PUB01]!`, '#74f7a5');

              joinOrCreateMultiplayerRoom('PUB01', true);

            } else {

              setMpLog(`Could not find Room [${mpState.roomCode}] (${err.type}). Check the code or host a room.`, '#ff8a80');

            }

          });

        }

        return;

      }


      // 3. LOCAL MULTI-TAB SYNC FALLBACK

      teardownPeerConnections();

      sendNetPacket('JOIN_REQ', localPlayer);

      setMpLog(`Room [${mpState.roomCode}] active on Local Multi-Tab Sync.`, '#74f7a5');

    }


    // Export standalone HTML file or Colyseus Server .TS

    function downloadStandaloneGameHtml() {

      audio.playUiConfirm();

      const htmlContent = '<!DOCTYPE html>\n' + document.documentElement.outerHTML;

      const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' });

      const url = URL.createObjectURL(blob);

      const a = document.createElement('a');

      a.href = url;

      a.download = 'sky_race_3d_multiplayer.html';

      a.click();

      URL.revokeObjectURL(url);

    }


    function exportColyseusServerCode() {

      const code = `// SkyRaceRoom.ts — Colyseus Server-Authoritative 3D Multiplayer Room

import { Room, Client } from "@colyseus/core";

import { Schema, MapSchema, type } from "@colyseus/schema";


export class RacerState extends Schema {

  @type("string") id: string = "";

  @type("string") name: string = "ACE";

  @type("string") planeId: string = "red-arrow";

  @type("boolean") ready: boolean = true;

  @type("number") s: number = 0;

  @type("number") xOffset: number = 0;

  @type("number") yOffset: number = 0;

  @type("number") lap: number = 1;

  @type("number") totalDistance: number = 0;

}


export class SkyRaceState extends Schema {

  @type("string") routeId: string = "miami";

  @type({ map: RacerState }) racers = new MapSchema<RacerState>();

}


export class SkyRaceRoom extends Room<SkyRaceState> {

  maxClients = 6;

  onCreate(options: any) {

    this.setState(new SkyRaceState());

    this.onMessage("TELEMETRY", (client, data) => {

      const r = this.state.racers.get(client.sessionId);

      if (r && typeof data.s === "number") {

        r.s = data.s;

        r.xOffset = Math.max(-18, Math.min(18, data.xOffset || 0));

        r.yOffset = Math.max(-6, Math.min(9, data.yOffset || 0));

        r.lap = data.lap || 1;

        r.totalDistance = data.totalDistance || 0;

        this.broadcast("TELEMETRY", { senderId: client.sessionId, payload: data }, { except: client });

      }

    });

  }

  onJoin(client: Client, options: any) {

    const r = new RacerState();

    r.id = client.sessionId;

    r.name = (options?.name || "PILOT").slice(0, 12);

    r.planeId = options?.planeId || "red-arrow";

    this.state.racers.set(client.sessionId, r);

  }

  onLeave(client: Client) {

    this.state.racers.delete(client.sessionId);

  }

}

`;

      const blob = new Blob([code], { type: 'text/plain' });

      const url = URL.createObjectURL(blob);

      const a = document.createElement('a');

      a.href = url;

      a.download = 'SkyRaceRoom.ts';

      a.click();

      URL.revokeObjectURL(url);

      setMpLog('Downloaded Colyseus SkyRaceRoom.ts server room file!', '#74f7a5');

    }


    // ============================================================================

    // 10. UI NAVIGATION, SCREEN TRANSITIONS & EVENT BINDING

    // ============================================================================

    const screenMap = {

      menu: 'screen-menu',

      planes: 'screen-planes',

      routes: 'screen-routes',

      multiplayer: 'screen-multiplayer',

      racing: 'hud-screen',

      pause: 'screen-pause',

      results: 'screen-results',

      options: 'screen-options'

    };


    function showScreen(targetState) {

      audio.playUiClick();

      Object.values(screenMap).forEach((id) => {

        document.getElementById(id).classList.remove('active');

      });

      document.getElementById(screenMap[targetState]).classList.add('active');

      currentAppState = targetState;


      if (targetState === 'menu') {

        audio.updateEnginePitch(0, false);

        audio.startLoop('menu');

        buildMenu3DDiorama();

      } else if (targetState === 'planes') {

        audio.updateEnginePitch(0, false);

        audio.startLoop('menu');

        buildHangar3DScene();

      } else if (targetState === 'routes' || targetState === 'multiplayer' || targetState === 'options') {

        audio.updateEnginePitch(0, false);

        audio.startLoop('menu');

        if (!menuAnimData) buildMenu3DDiorama();

      } else if (targetState === 'pause') {

        audio.updateEnginePitch(0, false);

      }

    }


    function start3DRaceSession(isMultiplayer = false, customRoster = null) {

      const route = ROUTES_DATA[appSettings.selectedRouteIndex];

      audio.playUiConfirm();

      audio.startLoop('race', route.musicRoot);

      build3DRaceWorld(route, isMultiplayer, customRoster);

      showScreen('racing');

    }


    function populatePlanesGridUI() {

      const container = document.getElementById('plane-cards-container');

      container.innerHTML = '';

      PLANES_DATA.forEach((plane, idx) => {

        const card = document.createElement('div');

        card.className = 'plane-card' + (idx === appSettings.selectedPlaneIndex ? ' selected' : '');

        const hexPrimary = '#' + plane.primary.toString(16).padStart(6, '0');

        const hexSec = '#' + plane.secondary.toString(16).padStart(6, '0');

        card.innerHTML = `

          <div class="plane-check">✓</div>

          <div class="plane-card-header">

            <span>#${plane.num}</span>

            <span class="plane-swatch" style="background:linear-gradient(135deg, ${hexPrimary} 55%, ${hexSec} 55%);"></span>

          </div>

          <div class="plane-card-name">${plane.num} ${plane.name}</div>

        `;

        card.addEventListener('click', () => {

          audio.playUiClick();

          updateHangarSelectedPlane(idx);

        });

        container.appendChild(card);

      });

    }


    function populateRoutesGridUI() {

      const container = document.getElementById('routes-grid-container');

      const mpSelect = document.getElementById('mp-route-select');

      container.innerHTML = '';

      mpSelect.innerHTML = '';


      ROUTES_DATA.forEach((route, idx) => {

        const card = document.createElement('div');

        card.className = 'route-card' + (idx === appSettings.selectedRouteIndex ? ' selected' : '');

        card.innerHTML = `

          <div class="route-title">${route.name}</div>

          <div class="route-sub">${route.subtitle}</div>

          <div class="route-badges">

            <span class="route-badge">DIFF: ${route.difficulty}</span>

            <span class="route-badge">WIND: ${Math.round(route.windForce * 100)}%</span>

            <span class="route-badge">3 LAPS • 3D CIRCUIT</span>

          </div>

        `;

        card.addEventListener('click', () => {

          audio.playUiClick();

          appSettings.selectedRouteIndex = idx;

          saveSettings(appSettings);

          document.querySelectorAll('.route-card').forEach((c, i) => c.classList.toggle('selected', i === idx));

        });

        card.addEventListener('dblclick', () => {

          appSettings.selectedRouteIndex = idx;

          start3DRaceSession(false);

        });

        container.appendChild(card);


        const opt = document.createElement('option');

        opt.value = route.id;

        opt.textContent = route.name;

        mpSelect.appendChild(opt);

      });

    }


    populatePlanesGridUI();

    populateRoutesGridUI();


    document.getElementById('mp-mode-select').addEventListener('change', (e) => {

      document.getElementById('colyseus-url-row').style.display =

        e.target.value === 'colyseus' ? 'block' : 'none';

    });


    // Global Action Dispatcher

    document.body.addEventListener('click', (e) => {

      const btn = e.target.closest('[data-action]');

      if (!btn) return;

      const action = btn.getAttribute('data-action');


      switch (action) {

        case 'nav-singleplayer':

          showScreen('routes');

          break;

        case 'nav-multiplayer':

          showScreen('multiplayer');

          if (!mpState.roomCode) joinOrCreateMultiplayerRoom('SKY3D', true);

          break;

        case 'nav-planes':

          planesReturnTarget = 'menu';

          showScreen('planes');

          break;

        case 'nav-options':

          showScreen('options');

          break;


        case 'planes-back':

        case 'planes-confirm':

          audio.playUiConfirm();

          showScreen(planesReturnTarget);

          break;


        case 'routes-back':

          showScreen('menu');

          break;

        case 'routes-change-plane':

          planesReturnTarget = 'routes';

          showScreen('planes');

          break;

        case 'routes-launch':

          start3DRaceSession(false);

          break;


        case 'mp-back':

          showScreen('menu');

          break;

        case 'mp-change-plane':

          planesReturnTarget = 'multiplayer';

          showScreen('planes');

          break;

        case 'mp-quick':

          joinOrCreateMultiplayerRoom('PUB01', false);

          break;

        case 'mp-create': {

          const randCode = 'SKY' + Math.floor(10 + Math.random() * 89);

          document.getElementById('mp-room-code-input').value = randCode;

          joinOrCreateMultiplayerRoom(randCode, true);

          break;

        }

        case 'mp-join': {

          const code = document.getElementById('mp-room-code-input').value.trim() || 'SKY3D';

          joinOrCreateMultiplayerRoom(code, false);

          break;

        }

        case 'mp-copy-invite': {

          const code = mpState.roomCode || 'SKY3D';

          const url = new URL(window.location.href);

          url.searchParams.set('room', code);

          navigator.clipboard?.writeText(url.toString()).then(() => {

            setMpLog(`Copied Invite Link for Room [${code}]! Send it to your friends to auto-join.`, '#ffd54f');

          }).catch(() => {

            setMpLog(`Share Room Code [${code}] with your friends!`, '#ffd54f');

          });

          break;

        }

        case 'mp-toggle-ready': {

          mpState.ready = !mpState.ready;

          document.getElementById('btn-mp-ready').textContent = mpState.ready ? '✓ READY UP' : '⏳ NOT READY';

          const me = getLocalPlayerSummary();

          mpState.players.set(mpState.clientId, me);

          refreshLobbyRosterUI();

          sendNetPacket('JOIN_REQ', me);

          break;

        }

        case 'mp-start-race': {

          const chosenRouteId = document.getElementById('mp-route-select').value;

          const rIdx = ROUTES_DATA.findIndex((r) => r.id === chosenRouteId);

          if (rIdx >= 0) appSettings.selectedRouteIndex = rIdx;

          const roster = Array.from(mpState.players.values());

          let aiNum = 1;

          while (roster.length < 6) {

            const p = PLANES_DATA[(appSettings.selectedPlaneIndex + aiNum * 2) % PLANES_DATA.length];

            roster.push({

              id: `ai-mp-${aiNum}`,

              name: `${p.name} [AI]`,

              planeId: p.id,

              ready: true,

              isAI: true

            });

            aiNum++;

          }

          sendNetPacket('START_RACE', { routeId: chosenRouteId, roster });

          start3DRaceSession(true, roster);

          break;

        }

        case 'mp-export-server':

          exportColyseusServerCode();

          break;


        case 'race-pause':

          showScreen('pause');

          break;

        case 'pause-resume':

          showScreen('racing');

          break;

        case 'pause-restart':

          start3DRaceSession(raceState ? raceState.isMultiplayer : false);

          break;

        case 'pause-quit':

          showScreen('menu');

          break;


        case 'results-rematch':

          start3DRaceSession(raceState ? raceState.isMultiplayer : false);

          break;

        case 'results-routes':

          showScreen('routes');

          break;

        case 'results-menu':

          showScreen('menu');

          break;


        case 'options-close':

          saveSettings(appSettings);

          showScreen('menu');

          break;

        case 'options-reset':

          Object.assign(appSettings, DEFAULT_SETTINGS);

          saveSettings(appSettings);

          syncOptionsUI();

          break;

        case 'options-mute':

          appSettings.muted = !appSettings.muted;

          saveSettings(appSettings);

          syncOptionsUI();

          break;

      }

    });


    // Options Sliders

    function syncOptionsUI() {

      document.getElementById('opt-music').value = appSettings.musicVolume;

      document.getElementById('lbl-opt-music').textContent = `${appSettings.musicVolume}%`;

      document.getElementById('opt-sfx').value = appSettings.sfxVolume;

      document.getElementById('lbl-opt-sfx').textContent = `${appSettings.sfxVolume}%`;

      document.getElementById('opt-sens').value = Math.round(appSettings.sensitivity * 100);

      document.getElementById('lbl-opt-sens').textContent = `${appSettings.sensitivity.toFixed(1)}x`;

      document.getElementById('opt-diff').value = String(appSettings.aiDifficulty);

      document.getElementById('btn-opt-mute').textContent = `MUTE ALL: ${appSettings.muted ? 'ON' : 'OFF'}`;

      document.getElementById('btn-mute-quick').textContent = appSettings.muted ? '🔇 AUDIO: OFF' : '🔊 AUDIO: ON';

      document.getElementById('mp-pilot-name').value = appSettings.pilotName;

    }

    syncOptionsUI();


    document.getElementById('opt-music').addEventListener('input', (e) => {

      appSettings.musicVolume = Number(e.target.value);

      syncOptionsUI();

    });

    document.getElementById('opt-sfx').addEventListener('input', (e) => {

      appSettings.sfxVolume = Number(e.target.value);

      syncOptionsUI();

    });

    document.getElementById('opt-sens').addEventListener('input', (e) => {

      appSettings.sensitivity = Number(e.target.value) / 100;

      syncOptionsUI();

    });

    document.getElementById('opt-diff').addEventListener('change', (e) => {

      appSettings.aiDifficulty = Number(e.target.value);

      saveSettings(appSettings);

    });


    document.getElementById('btn-mute-quick').addEventListener('click', () => {

      appSettings.muted = !appSettings.muted;

      saveSettings(appSettings);

      syncOptionsUI();

    });


    function cycleCameraMode() {

      cameraModeIndex = (cameraModeIndex + 1) % 3;

      const labels = ['🎥 CAM: CHASE', '🎥 CAM: FAR', '🎥 CAM: NOSE'];

      document.getElementById('btn-cam-mode').textContent = labels[cameraModeIndex];

    }

    document.getElementById('btn-cam-mode').addEventListener('click', cycleCameraMode);

    document.getElementById('btn-download-html').addEventListener('click', downloadStandaloneGameHtml);


    // Self-Verification Suite

    document.getElementById('btn-self-test').addEventListener('click', () => {

      const checks = [

        PLANES_DATA.length === 10,

        ROUTES_DATA.length === 7,

        renderer instanceof THREE.WebGLRenderer,

        typeof createLowPolyAirplaneMesh === 'function'

      ];

      const passed = checks.every(Boolean);

      const btn = document.getElementById('btn-self-test');

      btn.textContent = passed ? '✔ 3D + P2P OK (10P/7R)' : '✖ TEST ERR';

      btn.style.borderColor = passed ? '#35e86b' : '#ff3b2f';

      audio.playUiConfirm();

    });


    // ============================================================================

    // 11. MASTER 3D ANIMATION & RENDER LOOP + URL INVITE AUTO-JOIN

    // ============================================================================

    window.addEventListener('resize', () => {

      camera.aspect = window.innerWidth / window.innerHeight;

      camera.updateProjectionMatrix();

      renderer.setSize(window.innerWidth, window.innerHeight);

    });


    let lastFrameTime = performance.now();

    function animate(now) {

      requestAnimationFrame(animate);

      const dt = Math.min(0.05, (now - lastFrameTime) / 1000);

      lastFrameTime = now;


      if (currentAppState === 'menu' || currentAppState === 'routes' || currentAppState === 'multiplayer' || currentAppState === 'options') {

        if (menuAnimData) {

          menuAnimData.t += dt;

          const { heroPlane, rivalBlue, rivalYellow, clouds, t } = menuAnimData;

          heroPlane.position.y = -1.8 + Math.sin(t * 2.2) * 0.35;

          heroPlane.rotation.z = -0.42 + Math.cos(t * 1.8) * 0.06;

          if (heroPlane.userData.propeller) heroPlane.userData.propeller.rotation.z += dt * 38;


          rivalBlue.position.x = 11 - ((t * 4.5) % 28);

          rivalBlue.position.y = 5.2 + Math.sin(t * 2.5) * 0.4;

          if (rivalBlue.userData.propeller) rivalBlue.userData.propeller.rotation.z += dt * 36;


          rivalYellow.position.x = 14 - ((t * 3.8) % 30);

          if (rivalYellow.userData.propeller) rivalYellow.userData.propeller.rotation.z += dt * 34;


          clouds.forEach((c) => {

            c.position.x += dt * 1.6;

            if (c.position.x > 95) c.position.x = -95;

          });

        }

      } else if (currentAppState === 'planes') {

        if (hangarAnimData && hangarAnimData.pivot) {

          if (!hangarAnimData.isDragging) {

            hangarAnimData.rotY += dt * 0.55;

          }

          hangarAnimData.pivot.rotation.y = hangarAnimData.rotY;

          if (hangarAnimData.planeMesh && hangarAnimData.planeMesh.userData.propeller) {

            hangarAnimData.planeMesh.userData.propeller.rotation.z += dt * 18;

          }

        }

      } else if (currentAppState === 'racing') {

        updateRaceSimulation(dt);

      }


      renderer.render(scene, camera);

    }


    // Initialize 3D Main Menu Diorama

    buildMenu3DDiorama();


    // Check if URL contains ?room=CODE invite parameter for instant multiplayer auto-join

    const urlParams = new URLSearchParams(window.location.search);

    const invitedRoom = urlParams.get('room');

    if (invitedRoom) {

      const cleanCode = invitedRoom.trim().toUpperCase().slice(0, 6);

      document.getElementById('mp-room-code-input').value = cleanCode;

      showScreen('multiplayer');

      joinOrCreateMultiplayerRoom(cleanCode, false);

    }


    requestAnimationFrame(animate);

  </script>

</body>

</html>