This paste is an HTML page. View it rendered — interactive parts (scripts) won’t run
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Bird vs Dr. J — Three.js Court</title>

  <style>
    :root {
      --ink: #f5e8cf;
      --red: #d84b31;
      --panel: rgba(12, 16, 23, .78);
    }

    * { box-sizing: border-box; }
    body {
      margin: 0;
      overflow: hidden;
      background: #080c12;
      color: var(--ink);
      font-family: Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif;
    }

    canvas { display: block; }

    #hud {
      position: fixed;
      z-index: 2;
      top: 18px;
      left: 18px;
      width: min(360px, calc(100vw - 36px));
      padding: 16px 18px;
      border: 1px solid rgba(245,232,207,.35);
      background: var(--panel);
      box-shadow: 0 12px 35px #0008;
      backdrop-filter: blur(8px);
    }

    h1 {
      margin: 0 0 5px;
      color: #fff3db;
      font-size: clamp(23px, 4vw, 37px);
      letter-spacing: 1px;
      line-height: .95;
    }

    #status {
      margin: 8px 0 12px;
      color: #e8d8be;
      font-family: system-ui, sans-serif;
      font-size: 13px;
      line-height: 1.4;
    }

    #scoreboard {
      display: flex;
      overflow: hidden;
      border: 2px solid #dbc49d;
      background: #0b0b0b;
    }

    .score {
      flex: 1;
      padding: 8px;
      text-align: center;
      color: #ff563c;
      font-size: 27px;
      line-height: .9;
      text-shadow: 0 0 10px #f33;
    }

    .score + .score { border-left: 2px solid #dbc49d; }

    .name {
      display: block;
      color: #e7dcc5;
      font-family: system-ui, sans-serif;
      font-size: 10px;
      font-weight: 800;
      letter-spacing: 1px;
      text-shadow: none;
    }

    button {
      margin-top: 12px;
      border: 1px solid #f8d9a7;
      padding: 10px 13px;
      color: #15100e;
      background: #f0c27a;
      cursor: pointer;
      font: 800 13px system-ui, sans-serif;
    }

    button:hover { background: #ffe0a9; }

    #caption {
      position: fixed;
      z-index: 2;
      bottom: 22px;
      left: 50%;
      width: min(650px, calc(100vw - 40px));
      transform: translateX(-50%);
      padding: 11px 16px;
      text-align: center;
      background: rgba(0,0,0,.54);
      border: 1px solid rgba(255,255,255,.18);
      color: #fff4dc;
      font-size: clamp(16px, 2.6vw, 26px);
      letter-spacing: .7px;
      text-shadow: 2px 2px #000;
      opacity: 0;
      transition: opacity .2s;
    }

    #hint {
      position: fixed;
      right: 18px;
      bottom: 18px;
      color: #e9dbc0;
      opacity: .72;
      font: 12px system-ui, sans-serif;
      text-align: right;
    }
  </style>
</head>
<body>
  <div id="hud">
    <h1>BIRD vs DR. J</h1>
    <div id="scoreboard">
      <div class="score"><span class="name">LARRY</span><span id="birdScore">00</span></div>
      <div class="score"><span class="name">DR. J</span><span id="jScore">00</span></div>
    </div>
    <p id="status">Click Start to enable court sounds. Spacebar: break the window.</p>
    <button id="start">START SCENE + SOUND</button>
  </div>

  <div id="caption"></div>
  <div id="hint">Drag to orbit • Wheel to zoom<br>Space = smash glass</div>

  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.174.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.174.0/examples/jsm/"
    }
  }
  </script>

  <script type="module">
    import * as THREE from "three";
    import { OrbitControls } from "three/addons/controls/OrbitControls.js";

    // ------------------------------------------------------------
    // Basic scene
    // ------------------------------------------------------------
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x101720);
    scene.fog = new THREE.Fog(0x101720, 25, 70);

    const camera = new THREE.PerspectiveCamera(
      48, innerWidth / innerHeight, 0.1, 150
    );
    camera.position.set(20, 13, 24);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(innerWidth, innerHeight);
    renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    document.body.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 3.2, -2);
    controls.enableDamping = true;
    controls.minDistance = 12;
    controls.maxDistance = 43;
    controls.maxPolarAngle = Math.PI * 0.48;

    scene.add(new THREE.HemisphereLight(0xb9d8ff, 0x423020, 2.3));

    const keyLight = new THREE.DirectionalLight(0xffe3b4, 3.6);
    keyLight.position.set(-10, 16, 9);
    keyLight.castShadow = true;
    keyLight.shadow.mapSize.set(2048, 2048);
    scene.add(keyLight);

    const rimLight = new THREE.PointLight(0xff6e48, 20, 32, 2);
    rimLight.position.set(4, 8, -10);
    scene.add(rimLight);

    // ------------------------------------------------------------
    // Materials and helpers
    // ------------------------------------------------------------
    const wood = new THREE.MeshStandardMaterial({
      color: 0xb87437, roughness: 0.7, metalness: 0.05
    });
    const paint = new THREE.MeshStandardMaterial({
      color: 0xd7b16e, roughness: 0.85
    });
    const black = new THREE.MeshStandardMaterial({
      color: 0x16191e, roughness: 0.65
    });
    const orange = new THREE.MeshStandardMaterial({
      color: 0xd76524, roughness: 0.75
    });
    const white = new THREE.MeshStandardMaterial({
      color: 0xf4ead8, roughness: 0.5
    });
    const red = new THREE.MeshStandardMaterial({
      color: 0xc9342f, roughness: 0.62
    });
    const blue = new THREE.MeshStandardMaterial({
      color: 0x28457e, roughness: 0.63
    });
    const skinLight = new THREE.MeshStandardMaterial({
      color: 0xd3976d, roughness: 0.8
    });
    const skinDark = new THREE.MeshStandardMaterial({
      color: 0x704432, roughness: 0.82
    });
    const glassMaterial = new THREE.MeshPhysicalMaterial({
      color: 0x94cff0,
      transparent: true,
      opacity: 0.28,
      roughness: 0.05,
      metalness: 0.15,
      side: THREE.DoubleSide
    });

    function box(w, h, d, material, x=0, y=0, z=0) {
      const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
      m.position.set(x, y, z);
      m.castShadow = true;
      m.receiveShadow = true;
      scene.add(m);
      return m;
    }

    function cylinder(r1, r2, h, material, x=0, y=0, z=0) {
      const m = new THREE.Mesh(new THREE.CylinderGeometry(r1, r2, h, 16), material);
      m.position.set(x, y, z);
      m.castShadow = true;
      m.receiveShadow = true;
      scene.add(m);
      return m;
    }

    function textSprite(text, color = "#ffffff") {
      const canvas = document.createElement("canvas");
      canvas.width = 512;
      canvas.height = 128;
      const c = canvas.getContext("2d");
      c.clearRect(0, 0, canvas.width, canvas.height);
      c.font = "bold 52px Arial";
      c.textAlign = "center";
      c.textBaseline = "middle";
      c.fillStyle = "#000";
      c.fillText(text, 258, 68);
      c.fillStyle = color;
      c.fillText(text, 256, 64);

      const tex = new THREE.CanvasTexture(canvas);
      const sprite = new THREE.Sprite(
        new THREE.SpriteMaterial({ map: tex, transparent: true })
      );
      sprite.scale.set(5.5, 1.38, 1);
      return sprite;
    }

    // ------------------------------------------------------------
    // Gym floor and walls
    // ------------------------------------------------------------
    const floor = box(34, 0.25, 26, wood, 0, -0.15, 0);
    floor.receiveShadow = true;

    // Wood floor stripes
    for (let z = -12; z <= 12; z += 2) {
      const stripe = box(33.6, 0.012, 0.035, paint, 0, 0.01, z);
      stripe.material = new THREE.MeshStandardMaterial({ color: 0x8b5128 });
    }

    const lineMat = new THREE.MeshBasicMaterial({ color: 0xf2e1bc });
    function courtLine(w, d, x, z) {
      const line = new THREE.Mesh(new THREE.BoxGeometry(w, 0.035, d), lineMat);
      line.position.set(x, 0.04, z);
      scene.add(line);
    }

    courtLine(0.11, 22, 0, 0);
    courtLine(30, 0.11, 0, 0);
    courtLine(0.11, 20, -14, 0);
    courtLine(0.11, 20, 14, 0);
    courtLine(28, 0.11, 0, -10);
    courtLine(28, 0.11, 0, 10);

    // Back wall
    box(34, 13, 0.45, black, 0, 6.4, -13.4);

    // Brick-style wall lines
    const wallLineMat = new THREE.MeshBasicMaterial({ color: 0x3c4149 });
    for (let y = 1; y < 12; y += 1.15) {
      const l = new THREE.Mesh(new THREE.BoxGeometry(33, 0.035, 0.025), wallLineMat);
      l.position.set(0, y, -13.14);
      scene.add(l);
    }

    // Ceiling lights
    for (const x of [-11, 0, 11]) {
      const fixture = box(4.2, 0.18, 1.3, white, x, 11.7, -2);
      const light = new THREE.PointLight(0xe9efff, 8, 20, 2);
      light.position.set(x, 10.8, -2);
      scene.add(light);
    }

    // ------------------------------------------------------------
    // Hoop
    // ------------------------------------------------------------
    const hoop = new THREE.Group();
    hoop.position.set(0, 0, -9.8);
    scene.add(hoop);

    const pole = new THREE.Mesh(
      new THREE.CylinderGeometry(.21, .28, 7, 12),
      black
    );
    pole.position.set(0, 3.5, -1.4);
    pole.castShadow = true;
    hoop.add(pole);

    const arm = new THREE.Mesh(new THREE.BoxGeometry(.25, .25, 2.2), black);
    arm.position.set(0, 6.55, -0.45);
    hoop.add(arm);

    const board = new THREE.Mesh(new THREE.BoxGeometry(4.2, 2.7, .16), white);
    board.position.set(0, 6.2, 0);
    board.castShadow = true;
    hoop.add(board);

    const boardSquare = new THREE.Mesh(
      new THREE.BoxGeometry(1.05, .82, .02),
      new THREE.MeshBasicMaterial({ color: 0x222222 })
    );
    boardSquare.position.set(0, 6.15, 0.095);
    hoop.add(boardSquare);

    const rim = new THREE.Mesh(
      new THREE.TorusGeometry(.72, .06, 10, 32),
      red
    );
    rim.rotation.x = Math.PI / 2;
    rim.position.set(0, 5.25, .48);
    hoop.add(rim);

    // Net
    const netMat = new THREE.MeshBasicMaterial({ color: 0xf5f0df });
    for (let i = 0; i < 10; i++) {
      const a = (i / 10) * Math.PI * 2;
      const top = new THREE.Vector3(Math.cos(a)*.7, 5.2, .48 + Math.sin(a)*.7);
      const bottom = new THREE.Vector3(Math.cos(a)*.43, 4.5, .48 + Math.sin(a)*.43);
      const mid = top.clone().lerp(bottom, .5);
      const len = top.distanceTo(bottom);
      const strand = new THREE.Mesh(
        new THREE.CylinderGeometry(.012, .012, len, 5), netMat
      );
      strand.position.copy(mid);
      strand.lookAt(bottom);
      strand.rotateX(Math.PI / 2);
      hoop.add(strand);
    }

    // ------------------------------------------------------------
    // Window + breakable shards
    // ------------------------------------------------------------
    const windowGroup = new THREE.Group();
    windowGroup.position.set(-10.5, 7.2, -13.08);
    scene.add(windowGroup);

    const frameMat = new THREE.MeshStandardMaterial({
      color: 0x30353c, metalness: 0.45, roughness: 0.45
    });

    box(7.1, .22, .22, frameMat, -10.5, 10.15, -13.0);
    box(7.1, .22, .22, frameMat, -10.5, 4.25, -13.0);
    box(.22, 6.1, .22, frameMat, -14.0, 7.2, -13.0);
    box(.22, 6.1, .22, frameMat, -7.0, 7.2, -13.0);

    const intactPane = new THREE.Mesh(
      new THREE.PlaneGeometry(6.65, 5.65), glassMaterial
    );
    intactPane.position.set(-10.5, 7.2, -12.94);
    scene.add(intactPane);

    // Window mullions
    for (let i = 1; i < 3; i++) {
      box(.08, 5.65, .1, frameMat, -14 + i * 2.33, 7.2, -12.9);
    }
    box(6.65, .08, .1, frameMat, -10.5, 7.2, -12.9);

    const shards = [];
    function makeShards() {
      for (let i = 0; i < 46; i++) {
        const shard = new THREE.Mesh(
          new THREE.TetrahedronGeometry(0.08 + Math.random() * 0.15),
          new THREE.MeshPhysicalMaterial({
            color: 0xa4e4ff,
            transparent: true,
            opacity: .7,
            roughness: .05,
            metalness: .2
          })
        );
        shard.visible = false;
        shard.userData.velocity = new THREE.Vector3();
        scene.add(shard);
        shards.push(shard);
      }
    }
    makeShards();

    let glassBroken = false;

    // ------------------------------------------------------------
    // Characters
    // ------------------------------------------------------------
    function makePlayer({ name, jersey, skin, hair, x, z }) {
      const g = new THREE.Group();
      g.position.set(x, 0, z);
      scene.add(g);

      const torso = new THREE.Mesh(
        new THREE.CylinderGeometry(.63, .78, 1.85, 12),
        jersey
      );
      torso.position.y = 3.15;
      torso.castShadow = true;
      g.add(torso);

      const shorts = new THREE.Mesh(
        new THREE.BoxGeometry(1.37, .64, .82),
        jersey
      );
      shorts.position.y = 2.14;
      shorts.castShadow = true;
      g.add(shorts);

      const neck = new THREE.Mesh(
        new THREE.CylinderGeometry(.19, .19, .3, 10), skin
      );
      neck.position.y = 4.18;
      g.add(neck);

      const head = new THREE.Mesh(
        new THREE.SphereGeometry(.49, 16, 12), skin
      );
      head.position.y = 4.67;
      head.castShadow = true;
      g.add(head);

      if (hair) {
        const hairMesh = new THREE.Mesh(
          new THREE.SphereGeometry(.51, 16, 12, 0, Math.PI * 2, 0, Math.PI * .48),
          hair
        );
        hairMesh.position.y = 4.83;
        g.add(hairMesh);
      }

      function limb(radius, length, material, px, py, pz) {
        const limb = new THREE.Mesh(
          new THREE.CylinderGeometry(radius, radius * .88, length, 10),
          material
        );
        limb.position.set(px, py, pz);
        limb.castShadow = true;
        g.add(limb);
        return limb;
      }

      const leftArm = limb(.16, 1.55, skin, -.78, 3.38, 0);
      const rightArm = limb(.16, 1.55, skin, .78, 3.38, 0);
      const leftLeg = limb(.23, 1.75, black, -.38, 1.12, 0);
      const rightLeg = limb(.23, 1.75, black, .38, 1.12, 0);

      const shoeL = new THREE.Mesh(new THREE.BoxGeometry(.52,.2,.95), white);
      shoeL.position.set(-.38, .18, .13);
      g.add(shoeL);
      const shoeR = shoeL.clone();
      shoeR.position.x = .38;
      g.add(shoeR);

      const label = textSprite(name, "#fff5d8");
      label.position.set(0, 6.1, 0);
      g.add(label);

      return { group: g, torso, leftArm, rightArm, leftLeg, rightLeg };
    }

    const bird = makePlayer({
      name: "LARRY BIRD", jersey: new THREE.MeshStandardMaterial({ color: 0x2b8b55 }),
      skin: skinLight,
      hair: new THREE.MeshStandardMaterial({ color: 0xd8b55d }),
      x: -3.7, z: 1.8
    });

    const drj = makePlayer({
      name: "DR. J", jersey: new THREE.MeshStandardMaterial({ color: 0xffffff }),
      skin: skinDark,
      hair: new THREE.MeshStandardMaterial({ color: 0x17110f }),
      x: 2.3, z: .4
    });

    // Janitor
    const janitor = new THREE.Group();
    janitor.position.set(-11.1, 0, -8.8);
    scene.add(janitor);

    const janitorBody = new THREE.Mesh(
      new THREE.CylinderGeometry(.52, .65, 1.75, 12),
      blue
    );
    janitorBody.position.y = 2.7;
    janitor.add(janitorBody);

    const janitorHead = new THREE.Mesh(
      new THREE.SphereGeometry(.42, 14, 12),
      skinDark
    );
    janitorHead.position.y = 4.05;
    janitor.add(janitorHead);

    const cap = new THREE.Mesh(
      new THREE.CylinderGeometry(.45,.45,.16,16),
      black
    );
    cap.position.y = 4.4;
    janitor.add(cap);

    const mop = new THREE.Group();
    mop.position.set(.9, 1.6, .2);
    janitor.add(mop);

    const mopHandle = new THREE.Mesh(
      new THREE.CylinderGeometry(.045,.045,3.6,8),
      new THREE.MeshStandardMaterial({ color: 0x9c6d3a })
    );
    mopHandle.rotation.z = -.36;
    mopHandle.position.y = 1.25;
    mop.add(mopHandle);

    const mopHead = new THREE.Mesh(
      new THREE.BoxGeometry(1.35,.13,.42),
      new THREE.MeshStandardMaterial({ color: 0xd2bd77 })
    );
    mopHead.position.set(-.62, -.38, 0);
    mop.add(mopHead);

    const janitorLabel = textSprite("JANITOR", "#ffe7a3");
    janitorLabel.position.y = 5.05;
    janitor.add(janitorLabel);

    // ------------------------------------------------------------
    // Basketball
    // ------------------------------------------------------------
    const ball = new THREE.Mesh(
      new THREE.SphereGeometry(.32, 20, 16),
      orange
    );
    ball.castShadow = true;
    ball.position.set(-3.3, 1.25, 1.7);
    scene.add(ball);

    const seamMat = new THREE.MeshBasicMaterial({ color: 0x47200c });
    const seam1 = new THREE.Mesh(new THREE.TorusGeometry(.326,.012,5,24), seamMat);
    ball.add(seam1);
    const seam2 = seam1.clone();
    seam2.rotation.x = Math.PI / 2;
    ball.add(seam2);

    // ------------------------------------------------------------
    // Sound effects: procedural, no external audio files
    // ------------------------------------------------------------
    let audioCtx = null;
    let soundEnabled = false;

    function initAudio() {
      if (!audioCtx) audioCtx = new AudioContext();
      audioCtx.resume();
      soundEnabled = true;
    }

    function tone(freq, duration, type = "sine", gain = .08, slide = 1) {
      if (!soundEnabled) return;
      const osc = audioCtx.createOscillator();
      const amp = audioCtx.createGain();
      osc.type = type;
      osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
      osc.frequency.exponentialRampToValueAtTime(
        Math.max(10, freq * slide), audioCtx.currentTime + duration
      );
      amp.gain.setValueAtTime(gain, audioCtx.currentTime);
      amp.gain.exponentialRampToValueAtTime(.001, audioCtx.currentTime + duration);
      osc.connect(amp).connect(audioCtx.destination);
      osc.start();
      osc.stop(audioCtx.currentTime + duration);
    }

    function noise(duration, gain = .12, highpass = 500) {
      if (!soundEnabled) return;
      const size = Math.floor(audioCtx.sampleRate * duration);
      const buffer = audioCtx.createBuffer(1, size, audioCtx.sampleRate);
      const data = buffer.getChannelData(0);
      for (let i = 0; i < size; i++) data[i] = Math.random() * 2 - 1;

      const source = audioCtx.createBufferSource();
      const filter = audioCtx.createBiquadFilter();
      const amp = audioCtx.createGain();
      filter.type = "highpass";
      filter.frequency.value = highpass;
      amp.gain.setValueAtTime(gain, audioCtx.currentTime);
      amp.gain.exponentialRampToValueAtTime(.001, audioCtx.currentTime + duration);
      source.buffer = buffer;
      source.connect(filter).connect(amp).connect(audioCtx.destination);
      source.start();
    }

    function bounceSound() { tone(105, .08, "sine", .13, .46); }
    function swishSound() { noise(.22, .075, 1800); tone(780, .12, "sine", .025, .55); }
    function glassSound() {
      noise(.7, .28, 1200);
      for (let i = 0; i < 8; i++) {
        setTimeout(() => tone(1000 + Math.random()*2300, .18, "triangle", .035, .55), i * 38);
      }
    }

    // ------------------------------------------------------------
    // Sequence / action
    // ------------------------------------------------------------
    const birdScoreEl = document.querySelector("#birdScore");
    const jScoreEl = document.querySelector("#jScore");
    const captionEl = document.querySelector("#caption");
    const statusEl = document.querySelector("#status");

    let birdScore = 0;
    let sceneStarted = false;
    let startTime = 0;
    let lastBounce = -1;
    let scoredThisRound = false;

    function caption(message, seconds = 1.8) {
      captionEl.textContent = message;
      captionEl.style.opacity = 1;
      clearTimeout(captionEl._timer);
      captionEl._timer = setTimeout(() => captionEl.style.opacity = 0, seconds * 1000);
    }

    function resetPlay() {
      bird.group.position.set(-3.7, 0, 1.8);
      drj.group.position.set(2.3, 0, .4);
      ball.position.set(-3.2, 1.15, 1.5);
      scoredThisRound = false;
    }

    function breakGlass() {
      if (glassBroken) return;
      glassBroken = true;
      intactPane.visible = false;
      glassSound();
      caption("THE JANITOR HAS SEEN ENOUGH.", 2.8);

      shards.forEach((shard, i) => {
        shard.visible = true;
        shard.position.set(
          -10.5 + (Math.random() - .5) * 5.6,
          7.3 + (Math.random() - .5) * 4.8,
          -12.75 + Math.random() * .22
        );
        shard.userData.velocity.set(
          (Math.random() - .5) * 7,
          Math.random() * 5 + 1,
          Math.random() * 3 + .5
        );
        shard.rotation.set(Math.random()*5, Math.random()*5, Math.random()*5);
      });

      janitor.rotation.z = -.22;
      mop.rotation.z = .65;
    }

    function animatePlay(elapsed, dt) {
      const cycle = elapsed % 10;

      // Reset / possession
      if (cycle < .15 && elapsed > .15) resetPlay();

      // Bird dribbles and drives first
      if (cycle < 3.2) {
        const p = cycle / 3.2;
        bird.group.position.x = -3.7 + p * 2.1;
        bird.group.position.z = 1.8 - p * 2.0;

        drj.group.position.x = 2.3 - p * 1.45;
        drj.group.position.z = .4 + Math.sin(p * Math.PI) * .6;

        const b = Math.abs(Math.sin(cycle * 7.5));
        ball.position.set(
          bird.group.position.x + .5,
          .33 + b * 1.1,
          bird.group.position.z + .2
        );
        bird.rightArm.rotation.z = -.55 + Math.sin(cycle*7.5)*.32;
        drj.leftArm.rotation.z = .55;

        const tick = Math.floor(cycle * 7.5 / Math.PI);
        if (tick !== lastBounce) {
          bounceSound();
          lastBounce = tick;
        }
      }

      // Shot trajectory to basket
      if (cycle >= 3.2 && cycle < 4.8) {
        const p = (cycle - 3.2) / 1.6;
        const start = new THREE.Vector3(-1.55, 1.6, -.2);
        const end = new THREE.Vector3(0, 5.18, -9.2);
        ball.position.lerpVectors(start, end, p);
        ball.position.y += Math.sin(p * Math.PI) * 3.1;
        ball.rotation.x += .32;
        bird.rightArm.rotation.z = -1.55 + p*.4;
        drj.leftArm.rotation.z = .8 + p*.7;
      }

      // Swish then window break
      if (cycle >= 4.8 && cycle < 5.3 && !scoredThisRound) {
        scoredThisRound = true;
        birdScore++;
        birdScoreEl.textContent = String(birdScore).padStart(2, "0");
        swishSound();
        caption("BIRD FOR THE WIN.", 1.8);
      }

      if (cycle > 5.7 && !glassBroken) breakGlass();

      // Dr J gets a brief response pose afterward
      if (cycle >= 6.4) {
        drj.group.position.x = 1.1 + Math.sin(cycle * 1.7) * .35;
        drj.rightArm.rotation.z = -.9;
        ball.position.set(0, 5.2, -9.1);
      }

      // Ambient character motion
      bird.torso.rotation.y = Math.sin(elapsed*2.0) * .08;
      drj.torso.rotation.y = Math.sin(elapsed*1.5+.7) * .08;
      janitor.position.x = -11.1 + Math.sin(elapsed*.55)*.22;
      janitor.rotation.y = .22 + Math.sin(elapsed*.55)*.12;

      // Shards fall after glass break
      if (glassBroken) {
        shards.forEach(shard => {
          if (!shard.visible) return;
          shard.userData.velocity.y -= 11.5 * dt;
          shard.position.addScaledVector(shard.userData.velocity, dt);
          shard.rotation.x += dt * 8;
          shard.rotation.y += dt * 11;

          if (shard.position.y < .15) {
            shard.position.y = .15;
            shard.userData.velocity.y *= -.22;
            shard.userData.velocity.x *= .72;
            shard.userData.velocity.z *= .72;
          }
        });
      }
    }

    // ------------------------------------------------------------
    // UI
    // ------------------------------------------------------------
    document.querySelector("#start").addEventListener("click", () => {
      initAudio();
      if (!sceneStarted) {
        sceneStarted = true;
        startTime = performance.now() / 1000;
        statusEl.textContent = "Bird drives left. Dr. J contests. The janitor watches the window.";
        caption("ONE-ON-ONE. NO EXCUSES.", 2.2);
        document.querySelector("#start").textContent = "SOUND ENABLED";
      }
    });

    addEventListener("keydown", (event) => {
      if (event.code === "Space") {
        event.preventDefault();
        initAudio();
        breakGlass();
      }
    });

    addEventListener("resize", () => {
      camera.aspect = innerWidth / innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(innerWidth, innerHeight);
    });

    // ------------------------------------------------------------
    // Render loop
    // ------------------------------------------------------------
    let previous = performance.now() / 1000;

    function render() {
      requestAnimationFrame(render);

      const now = performance.now() / 1000;
      const dt = Math.min(.04, now - previous);
      previous = now;

      if (sceneStarted) animatePlay(now - startTime, dt);

      controls.update();
      renderer.render(scene, camera);
    }

    render();
  </script>
</body>
</html>