Models 44

MiniMax M3

by MiniMaxreasoning defaultClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
4 min 46 s
Cost
$0.049
Output tokens
40,510
Spent thinking
23,194 tokens
Speed
142 tok/s
Console errors
0
First token after
670 ms
First visible text after
3 min 46 s
Prompt tokens
611
Code size
54.3 KB
Finish reason
stop
Served by
Minimax
Model id
minimax/minimax-m3
Reasoning sent
model default
Max output
512,000 tokens
Generated
Nov 13, 2026

What it was asked

The prompt
“a 3D aquarium with Three.js where the fish swim away when I move the mouse near them (Load Three.js only through this import map: <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>)”
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 1635 lines

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tideline — Living Aquarium</title>
  <style>
    :root {
      --ink: #efffff;
      --muted: rgba(225, 250, 255, 0.62);
      --aqua: #75f4e4;
      --cyan: #61d8ff;
      --glass: rgba(5, 28, 40, 0.58);
      --line: rgba(164, 239, 255, 0.18);
    }

    * { box-sizing: border-box; }

    html,
    body {
      width: 100%;
      height: 100%;
      margin: 0;
      overflow: hidden;
      background:
        radial-gradient(circle at 50% 40%, #0a3445 0%, #031725 52%, #010a12 100%);
      color: var(--ink);
      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      -webkit-font-smoothing: antialiased;
      user-select: none;
    }

    #scene {
      position: fixed;
      inset: 0;
      background: #031725;
    }

    #scene canvas {
      width: 100%;
      height: 100%;
      display: block;
      outline: none;
      cursor: crosshair;
    }

    .vignette,
    .glass-glare,
    .grain {
      position: fixed;
      inset: 0;
      pointer-events: none;
      z-index: 2;
    }

    .vignette {
      background:
        radial-gradient(ellipse at 50% 47%, transparent 36%, rgba(0, 8, 15, 0.16) 67%, rgba(0, 6, 12, 0.72) 115%),
        linear-gradient(180deg, rgba(0, 7, 13, 0.36), transparent 20%, transparent 77%, rgba(0, 6, 12, 0.44));
    }

    .glass-glare {
      background:
        linear-gradient(112deg, transparent 8%, rgba(210, 250, 255, 0.035) 22%, transparent 34%),
        linear-gradient(295deg, transparent 66%, rgba(180, 244, 255, 0.025) 78%, transparent 89%);
      mix-blend-mode: screen;
    }

    .grain {
      opacity: 0.11;
      background-image:
        repeating-linear-gradient(0deg, rgba(255,255,255,0.012) 0, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 3px),
        repeating-linear-gradient(90deg, rgba(255,255,255,0.009) 0, rgba(255,255,255,0.009) 1px, transparent 1px, transparent 4px);
      mix-blend-mode: soft-light;
    }

    .topbar {
      position: fixed;
      z-index: 5;
      top: 25px;
      left: 30px;
      right: 30px;
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      pointer-events: none;
    }

    .brand {
      display: flex;
      align-items: center;
      gap: 14px;
      text-shadow: 0 2px 24px rgba(0, 13, 22, 0.58);
    }

    .brand-mark {
      position: relative;
      width: 48px;
      height: 48px;
      flex: 0 0 auto;
      border: 1px solid rgba(143, 244, 255, 0.33);
      border-radius: 16px;
      background:
        radial-gradient(circle at 32% 25%, rgba(185,255,255,0.27), transparent 36%),
        linear-gradient(145deg, rgba(60, 211, 230, 0.18), rgba(3, 38, 54, 0.38));
      box-shadow: inset 0 1px 0 rgba(255,255,255,0.12), 0 10px 35px rgba(0,0,0,0.18);
      overflow: hidden;
    }

    .brand-mark::before,
    .brand-mark::after {
      content: "";
      position: absolute;
      left: 9px;
      right: 9px;
      height: 13px;
      border-top: 1.5px solid rgba(157, 251, 242, 0.82);
      border-radius: 50%;
    }

    .brand-mark::before { top: 21px; transform: rotate(-4deg); }
    .brand-mark::after { top: 28px; opacity: 0.52; transform: rotate(5deg); }

    .bubble {
      position: absolute;
      border: 1px solid rgba(197,255,255,0.55);
      border-radius: 50%;
    }

    .bubble.one { width: 7px; height: 7px; right: 10px; top: 8px; }
    .bubble.two { width: 4px; height: 4px; right: 18px; top: 15px; opacity: 0.65; }

    .eyebrow {
      margin: 0 0 3px;
      color: var(--aqua);
      font-size: 9px;
      font-weight: 700;
      letter-spacing: 0.23em;
      text-transform: uppercase;
    }

    h1 {
      margin: 0;
      font-family: Georgia, "Times New Roman", serif;
      font-size: clamp(31px, 3.4vw, 46px);
      font-weight: 400;
      line-height: 0.92;
      letter-spacing: -0.045em;
    }

    .brand-subtitle {
      margin: 8px 0 0;
      color: var(--muted);
      font-size: 11px;
      letter-spacing: 0.08em;
    }

    .status-panel {
      display: flex;
      align-items: center;
      gap: 11px;
      min-width: 168px;
      padding: 11px 14px;
      border: 1px solid var(--line);
      border-radius: 15px;
      background: var(--glass);
      box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 14px 42px rgba(0,0,0,0.18);
      backdrop-filter: blur(16px) saturate(120%);
    }

    .live-dot {
      position: relative;
      width: 8px;
      height: 8px;
      flex: 0 0 auto;
      border-radius: 50%;
      background: var(--aqua);
      box-shadow: 0 0 0 4px rgba(117,244,228,0.1), 0 0 14px rgba(117,244,228,0.72);
      transition: 240ms ease;
    }

    .live-dot.alert {
      background: #ffbd69;
      box-shadow: 0 0 0 4px rgba(255,189,105,0.1), 0 0 16px rgba(255,189,105,0.78);
    }

    .status-panel > div:last-child {
      display: flex;
      flex-direction: column;
      gap: 2px;
    }

    .status-panel strong {
      font-size: 11px;
      font-weight: 600;
      letter-spacing: 0.02em;
    }

    .status-panel small {
      color: var(--muted);
      font-size: 9px;
      letter-spacing: 0.04em;
    }

    .status-panel b { color: rgba(237,255,255,0.9); font-weight: 600; }

    .legend {
      position: fixed;
      z-index: 5;
      right: 31px;
      top: 50%;
      transform: translateY(-42%);
      width: 154px;
      pointer-events: none;
    }

    .legend-title {
      margin: 0 0 12px;
      color: rgba(210,246,255,0.48);
      font-size: 8px;
      font-weight: 700;
      letter-spacing: 0.25em;
    }

    .legend-row {
      display: flex;
      align-items: center;
      gap: 9px;
      margin: 9px 0;
      color: rgba(233,252,255,0.72);
      font-size: 10px;
      letter-spacing: 0.015em;
    }

    .legend-dot {
      width: 6px;
      height: 6px;
      flex: 0 0 auto;
      border-radius: 50%;
      background: var(--dot);
      box-shadow: 0 0 10px color-mix(in srgb, var(--dot), transparent 35%);
    }

    .interaction-card {
      position: fixed;
      z-index: 5;
      left: 50%;
      bottom: 24px;
      transform: translateX(-50%);
      display: flex;
      align-items: center;
      gap: 13px;
      width: min(520px, calc(100vw - 32px));
      padding: 13px 16px;
      border: 1px solid var(--line);
      border-radius: 18px;
      background: linear-gradient(135deg, rgba(8,42,55,0.7), rgba(3,20,31,0.66));
      box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 18px 55px rgba(0,0,0,0.24);
      backdrop-filter: blur(18px) saturate(125%);
      pointer-events: none;
    }

    .gesture-icon {
      position: relative;
      width: 34px;
      height: 34px;
      flex: 0 0 auto;
      border: 1px solid rgba(130,239,239,0.27);
      border-radius: 12px;
      background: rgba(84,225,224,0.08);
    }

    .gesture-icon::before {
      content: "";
      position: absolute;
      width: 8px;
      height: 8px;
      left: 12px;
      top: 12px;
      border-radius: 50%;
      background: var(--aqua);
      box-shadow: 0 0 13px rgba(117,244,228,0.8);
      animation: wander 2.8s ease-in-out infinite;
    }

    .gesture-icon::after {
      content: "";
      position: absolute;
      inset: 6px;
      border: 1px dashed rgba(133,242,237,0.23);
      border-radius: 50%;
      animation: spin 12s linear infinite;
    }

    @keyframes wander {
      0%, 100% { transform: translate(-4px, -2px); }
      50% { transform: translate(5px, 3px); }
    }

    @keyframes spin { to { transform: rotate(360deg); } }

    .interaction-copy { display: flex; flex-direction: column; gap: 3px; }
    .interaction-copy strong { font-size: 11px; font-weight: 600; letter-spacing: 0.015em; }
    .interaction-copy small { color: var(--muted); font-size: 9px; letter-spacing: 0.025em; }

    #startPrompt {
      position: fixed;
      z-index: 8;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 11px 16px;
      border: 1px solid rgba(163,244,255,0.25);
      border-radius: 999px;
      background: rgba(3, 26, 38, 0.64);
      box-shadow: 0 14px 50px rgba(0,0,0,0.28), inset 0 1px 0 rgba(255,255,255,0.07);
      backdrop-filter: blur(15px);
      color: rgba(238,255,255,0.9);
      font-size: 10px;
      font-weight: 600;
      letter-spacing: 0.075em;
      text-transform: uppercase;
      white-space: nowrap;
      pointer-events: none;
      transition: opacity 450ms ease, transform 450ms ease;
    }

    #startPrompt::before {
      content: "";
      width: 7px;
      height: 7px;
      border-radius: 50%;
      background: var(--aqua);
      box-shadow: 0 0 0 4px rgba(117,244,228,0.1), 0 0 12px rgba(117,244,228,0.7);
      animation: pulse 1.8s ease-in-out infinite;
    }

    #startPrompt.is-hidden {
      opacity: 0;
      transform: translate(-50%, -45%) scale(0.94);
    }

    @keyframes pulse {
      50% { opacity: 0.45; transform: scale(0.76); }
    }

    #cursorAura {
      position: fixed;
      z-index: 4;
      width: 260px;
      height: 260px;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      border: 1px dashed rgba(120,241,235,0.2);
      border-radius: 50%;
      background: radial-gradient(circle, rgba(99,238,231,0.055), transparent 63%);
      opacity: 0;
      pointer-events: none;
      transition: opacity 220ms ease, width 220ms ease, height 220ms ease;
    }

    #cursorAura.active {
      opacity: 0.8;
    }

    #fishTag {
      position: fixed;
      z-index: 9;
      left: 0;
      top: 0;
      display: flex;
      align-items: center;
      gap: 7px;
      padding: 6px 9px;
      border: 1px solid rgba(155,239,255,0.18);
      border-radius: 9px;
      background: rgba(2, 20, 30, 0.75);
      box-shadow: 0 8px 30px rgba(0,0,0,0.25);
      backdrop-filter: blur(12px);
      color: rgba(238,255,255,0.88);
      font-size: 8px;
      font-weight: 650;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      opacity: 0;
      transform: translate(15px, 7px);
      pointer-events: none;
      transition: opacity 160ms ease;
      white-space: nowrap;
    }

    #fishTag.visible { opacity: 1; }

    #fishTag i {
      width: 5px;
      height: 5px;
      border-radius: 50%;
      background: var(--fish-color, var(--aqua));
      box-shadow: 0 0 8px var(--fish-color, var(--aqua));
    }

    @media (max-width: 860px) {
      .legend { display: none; }
    }

    @media (max-width: 640px) {
      .topbar { top: 18px; left: 17px; right: 17px; }
      .brand { gap: 10px; }
      .brand-mark { width: 41px; height: 41px; border-radius: 14px; }
      .brand-mark::before { top: 17px; }
      .brand-mark::after { top: 24px; }
      .brand-subtitle { display: none; }
      .eyebrow { font-size: 7px; }
      h1 { font-size: 31px; }
      .status-panel { min-width: 0; padding: 9px 10px; gap: 8px; border-radius: 13px; }
      .status-panel strong { font-size: 9px; }
      .status-panel small { font-size: 7px; }
      .interaction-card { bottom: 15px; padding: 11px 12px; border-radius: 16px; }
      .interaction-copy strong { font-size: 10px; }
      .interaction-copy small { font-size: 8px; }
      .gesture-icon { width: 31px; height: 31px; }
      .gesture-icon::before { left: 11px; top: 11px; }
      #startPrompt { font-size: 8px; padding: 10px 13px; }
      #cursorAura { display: none; }
    }

    @media (max-height: 560px) {
      .legend { display: none; }
      .topbar { top: 14px; }
      .interaction-card { bottom: 12px; }
    }
  </style>

  <script type="importmap">
    {"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}
  </script>
</head>
<body>
  <div id="scene" aria-label="An interactive three-dimensional aquarium"></div>
  <div class="vignette"></div>
  <div class="glass-glare"></div>
  <div class="grain"></div>

  <header class="topbar">
    <div class="brand">
      <div class="brand-mark" aria-hidden="true">
        <span class="bubble one"></span>
        <span class="bubble two"></span>
      </div>
      <div>
        <p class="eyebrow">Living habitat / 01</p>
        <h1>Tideline</h1>
        <p class="brand-subtitle">A small world, gently disturbed.</p>
      </div>
    </div>

    <div class="status-panel">
      <span class="live-dot" id="liveDot"></span>
      <div>
        <strong id="statusText">Calm water</strong>
        <small>24.0°C · <b id="residentCount">14</b> residents</small>
      </div>
    </div>
  </header>

  <aside class="legend" aria-label="Fish species">
    <p class="legend-title">INHABITANTS</p>
    <div class="legend-row"><span class="legend-dot" style="--dot:#ffd34f"></span>Golden tang</div>
    <div class="legend-row"><span class="legend-dot" style="--dot:#4c85ff"></span>Blue tang</div>
    <div class="legend-row"><span class="legend-dot" style="--dot:#ff8246"></span>Clown anemonefish</div>
    <div class="legend-row"><span class="legend-dot" style="--dot:#72f6ed"></span>Neon tetra</div>
    <div class="legend-row"><span class="legend-dot" style="--dot:#f4d69a"></span>Moon angelfish</div>
  </aside>

  <div class="interaction-card">
    <div class="gesture-icon" aria-hidden="true"></div>
    <div class="interaction-copy">
      <strong>Move closer. Watch them retreat.</strong>
      <small>Fish react to your cursor · click or tap to send a ripple</small>
    </div>
  </div>

  <div id="cursorAura"></div>
  <div id="fishTag"><i></i><span>Golden tang</span></div>
  <div id="startPrompt">Click or press a key to start</div>

  <script type="module">
    import * as THREE from 'three';

    const sceneHost = document.querySelector('#scene');
    const prompt = document.querySelector('#startPrompt');
    const aura = document.querySelector('#cursorAura');
    const fishTag = document.querySelector('#fishTag');
    const fishTagText = fishTag.querySelector('span');
    const statusText = document.querySelector('#statusText');
    const liveDot = document.querySelector('#liveDot');
    const residentCount = document.querySelector('#residentCount');

    const renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: false,
      powerPreference: 'high-performance'
    });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.8));
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.18;
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.setClearColor(0x031725, 1);
    renderer.domElement.setAttribute('aria-label', 'Three-dimensional aquarium with interactive fish');
    sceneHost.appendChild(renderer.domElement);

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x031725);
    scene.fog = new THREE.FogExp2(0x062a3d, 0.022);

    const camera = new THREE.PerspectiveCamera(
      42,
      window.innerWidth / window.innerHeight,
      0.1,
      80
    );

    let cameraBaseZ = 10.9;
    camera.position.set(0, 0.2, cameraBaseZ);
    camera.lookAt(0, 0.05, 0);

    const hemisphere = new THREE.HemisphereLight(0xbaf7ff, 0x031019, 2.15);
    scene.add(hemisphere);

    const keyLight = new THREE.DirectionalLight(0xe5ffff, 2.65);
    keyLight.position.set(-4.5, 8, 6);
    keyLight.castShadow = true;
    keyLight.shadow.mapSize.set(1024, 1024);
    keyLight.shadow.camera.left = -7;
    keyLight.shadow.camera.right = 7;
    keyLight.shadow.camera.top = 6;
    keyLight.shadow.camera.bottom = -6;
    keyLight.shadow.camera.near = 1;
    keyLight.shadow.camera.far = 24;
    keyLight.shadow.bias = -0.0004;
    scene.add(keyLight);

    const cyanLight = new THREE.PointLight(0x55ddff, 26, 18, 2);
    cyanLight.position.set(-2.5, 2.7, 4.2);
    scene.add(cyanLight);

    const warmLight = new THREE.PointLight(0xff9e79, 16, 15, 2);
    warmLight.position.set(4, -0.8, 3);
    scene.add(warmLight);

    const tank = new THREE.Group();
    scene.add(tank);

    const backUniforms = { uTime: { value: 0 } };
    const backMaterial = new THREE.ShaderMaterial({
      uniforms: backUniforms,
      vertexShader: `
        varying vec2 vUv;
        void main() {
          vUv = uv;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        uniform float uTime;
        varying vec2 vUv;
        void main() {
          vec3 deep = vec3(0.012, 0.105, 0.155);
          vec3 upper = vec3(0.035, 0.30, 0.37);
          float gradient = smoothstep(0.0, 1.0, vUv.y);
          vec3 color = mix(deep, upper, gradient);
          float wave = sin((vUv.x * 18.0 + sin(vUv.y * 10.0 + uTime * 0.55)) * 1.7 + uTime * 0.35);
          float caustic = pow(max(0.0, wave * 0.5 + 0.5), 8.0);
          float beam = pow(max(0.0, sin((vUv.x * 6.0 - 0.35) * 3.14159)), 15.0);
          beam *= smoothstep(0.18, 1.0, vUv.y) * 0.10;
          float edge = smoothstep(0.0, 0.10, vUv.x) * smoothstep(0.0, 0.10, 1.0 - vUv.x);
          color += vec3(0.12, 0.52, 0.54) * caustic * 0.16;
          color += vec3(0.10, 0.34, 0.38) * beam;
          color *= 0.88 + 0.12 * edge;
          gl_FragColor = vec4(color, 1.0);
        }
      `
    });

    const backWall = new THREE.Mesh(new THREE.PlaneGeometry(11.55, 6.45), backMaterial);
    backWall.position.set(0, 0.52, -2.34);
    tank.add(backWall);

    let randomSeed = 72841;
    function random() {
      randomSeed |= 0;
      randomSeed = randomSeed + 0x6D2B79F5 | 0;
      let value = Math.imul(randomSeed ^ randomSeed >>> 15, 1 | randomSeed);
      value = value + Math.imul(value ^ value >>> 7, 61 | value) ^ value;
      return ((value ^ value >>> 14) >>> 0) / 4294967296;
    }

    const floorGeometry = new THREE.PlaneGeometry(11.55, 4.65, 56, 28);
    const floorPositions = floorGeometry.attributes.position;
    const floorColors = [];
    const sandColor = new THREE.Color();

    for (let i = 0; i < floorPositions.count; i++) {
      const x = floorPositions.getX(i);
      const y = floorPositions.getY(i);
      const h = Math.sin(x * 1.4) * 0.035 + Math.cos(y * 2.1) * 0.022 + (random() - 0.5) * 0.018;
      floorPositions.setZ(i, h);
      sandColor.setHSL(0.105 + random() * 0.018, 0.27, 0.54 + h * 0.6 + random() * 0.04);
      floorColors.push(sandColor.r, sandColor.g, sandColor.b);
    }

    floorGeometry.setAttribute('color', new THREE.Float32BufferAttribute(floorColors, 3));
    floorGeometry.rotateX(-Math.PI / 2);
    floorGeometry.computeVertexNormals();

    const sand = new THREE.Mesh(
      floorGeometry,
      new THREE.MeshStandardMaterial({
        color: 0xffffff,
        vertexColors: true,
        roughness: 0.93,
        metalness: 0
      })
    );
    sand.position.y = -2.53;
    sand.receiveShadow = true;
    tank.add(sand);

    const causticUniforms = { uTime: { value: 0 } };
    const floorCaustics = new THREE.Mesh(
      new THREE.PlaneGeometry(11.45, 4.58),
      new THREE.ShaderMaterial({
        uniforms: causticUniforms,
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        vertexShader: `
          varying vec2 vUv;
          void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
          }
        `,
        fragmentShader: `
          uniform float uTime;
          varying vec2 vUv;
          void main() {
            vec2 p = vUv * vec2(17.0, 8.0);
            float a = sin(p.x + sin(p.y + uTime * 0.58));
            float b = sin(p.y * 1.27 + sin(p.x * 0.72 - uTime * 0.43));
            float light = pow(max(0.0, 1.0 - abs((a + b) * 0.5)), 7.0);
            float edge = smoothstep(0.0, 0.07, vUv.x) * smoothstep(0.0, 0.07, 1.0 - vUv.x)
                       * smoothstep(0.0, 0.07, vUv.y) * smoothstep(0.0, 0.07, 1.0 - vUv.y);
            gl_FragColor = vec4(0.35, 1.0, 0.88, light * edge * 0.19);
          }
        `
      })
    );
    floorCaustics.rotation.x = -Math.PI / 2;
    floorCaustics.position.set(0, -2.445, 0.02);
    tank.add(floorCaustics);

    const waterUniforms = { uTime: { value: 0 } };
    const waterSurface = new THREE.Mesh(
      new THREE.PlaneGeometry(11.4, 4.55, 48, 24),
      new THREE.ShaderMaterial({
        uniforms: waterUniforms,
        transparent: true,
        depthWrite: false,
        side: THREE.DoubleSide,
        vertexShader: `
          uniform float uTime;
          varying vec2 vUv;
          void main() {
            vUv = uv;
            vec3 p = position;
            p.z += sin(p.x * 1.9 + uTime * 0.72) * 0.035 + sin(p.y * 2.4 - uTime * 0.91) * 0.025;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
          }
        `,
        fragmentShader: `
          uniform float uTime;
          varying vec2 vUv;
          void main() {
            float shimmer = pow(max(0.0, sin(vUv.x * 31.0 + uTime) * sin(vUv.y * 19.0 - uTime * 0.8)), 5.0);
            vec3 color = mix(vec3(0.05, 0.35, 0.42), vec3(0.45, 0.95, 0.94), shimmer);
            gl_FragColor = vec4(color, 0.075 + shimmer * 0.08);
          }
        `
      })
    );
    waterSurface.rotation.x = -Math.PI / 2;
    waterSurface.position.y = 3.43;
    tank.add(waterSurface);

    const glassMaterial = new THREE.MeshPhysicalMaterial({
      color: 0xa6f4ff,
      transparent: true,
      opacity: 0.045,
      roughness: 0.08,
      metalness: 0,
      depthWrite: false,
      side: THREE.DoubleSide
    });

    const waterVolume = new THREE.Mesh(
      new THREE.BoxGeometry(11.48, 6.2, 4.6),
      new THREE.MeshPhysicalMaterial({
        color: 0x52dceb,
        transparent: true,
        opacity: 0.025,
        roughness: 0.1,
        depthWrite: false,
        side: THREE.BackSide
      })
    );
    waterVolume.position.y = 0.48;
    tank.add(waterVolume);

    const frontGlass = new THREE.Mesh(new THREE.PlaneGeometry(11.5, 6.3), glassMaterial);
    frontGlass.position.set(0, 0.48, 2.31);
    frontGlass.renderOrder = 20;
    tank.add(frontGlass);

    const frameMaterial = new THREE.MeshStandardMaterial({
      color: 0x0b3948,
      roughness: 0.24,
      metalness: 0.55,
      emissive: 0x062d38,
      emissiveIntensity: 0.25
    });

    function addBox(size, position, material = frameMaterial) {
      const mesh = new THREE.Mesh(new THREE.BoxGeometry(...size), material);
      mesh.position.set(...position);
      mesh.castShadow = true;
      mesh.receiveShadow = true;
      tank.add(mesh);
      return mesh;
    }

    addBox([11.72, 0.13, 4.82], [0, -2.64, 0]);
    addBox([11.72, 0.055, 4.82], [0, 3.57, 0], new THREE.MeshStandardMaterial({
      color: 0x8bddea, transparent: true, opacity: 0.2, roughness: 0.2, depthWrite: false
    }));

    for (const x of [-5.74, 5.74]) {
      for (const z of [-2.28, 2.28]) {
        addBox([0.045, 6.22, 0.045], [x, 0.46, z]);
      }
    }

    const tankEdges = new THREE.LineSegments(
      new THREE.EdgesGeometry(new THREE.BoxGeometry(11.55, 6.25, 4.64)),
      new THREE.LineBasicMaterial({ color: 0x9aeafa, transparent: true, opacity: 0.25 })
    );
    tankEdges.position.y = 0.46;
    tank.add(tankEdges);

    const pebbleGeometry = new THREE.IcosahedronGeometry(1, 0);
    const pebbleMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.88 });
    const pebbles = new THREE.InstancedMesh(pebbleGeometry, pebbleMaterial, 115);
    const dummy = new THREE.Object3D();

    for (let i = 0; i < 115; i++) {
      dummy.position.set(
        (random() - 0.5) * 11.15,
        -2.47 + random() * 0.05,
        (random() - 0.5) * 4.22
      );
      const size = 0.035 + random() * 0.095;
      dummy.scale.set(size * (0.8 + random() * 0.6), size * 0.52, size);
      dummy.rotation.set(random() * Math.PI, random() * Math.PI, random() * Math.PI);
      dummy.updateMatrix();
      pebbles.setMatrixAt(i, dummy.matrix);
      const color = new THREE.Color().setHSL(0.08 + random() * 0.08, 0.12 + random() * 0.18, 0.37 + random() * 0.2);
      pebbles.setColorAt(i, color);
    }
    pebbles.instanceMatrix.needsUpdate = true;
    pebbles.instanceColor.needsUpdate = true;
    pebbles.receiveShadow = true;
    tank.add(pebbles);

    const rockGeometry = new THREE.DodecahedronGeometry(1, 1);
    function addRock(x, z, sx, sy, sz, color, rotation = 0) {
      const material = new THREE.MeshStandardMaterial({
        color,
        roughness: 0.84,
        metalness: 0.02,
        flatShading: true
      });
      const rock = new THREE.Mesh(rockGeometry, material);
      rock.scale.set(sx, sy, sz);
      rock.position.set(x, -2.48 + sy * 0.42, z);
      rock.rotation.set(random() * 0.35, rotation, random() * 0.25);
      rock.castShadow = true;
      rock.receiveShadow = true;
      tank.add(rock);
      return rock;
    }

    addRock(-4.45, -1.38, 1.05, 0.78, 0.86, 0x365b61, 0.4);
    addRock(-4.72, 0.46, 0.68, 0.48, 0.55, 0x41686b, 1.1);
    addRock(-3.72, 1.22, 0.48, 0.34, 0.43, 0x527477, 0.2);
    addRock(4.30, -1.42, 1.00, 0.73, 0.78, 0x3c6265, 1.7);
    addRock(4.54, 0.62, 0.66, 0.44, 0.58, 0x537578, 0.8);
    addRock(3.63, 1.33, 0.43, 0.30, 0.39, 0x486d70, 2.1);
    addRock(-1.90, -1.96, 0.54, 0.38, 0.42, 0x49686a, 0.9);
    addRock(1.72, -2.02, 0.66, 0.43, 0.48, 0x47696b, 2.4);

    const arch = new THREE.Mesh(
      new THREE.TorusGeometry(1.72, 0.34, 10, 64, Math.PI),
      new THREE.MeshStandardMaterial({
        color: 0x38595c,
        roughness: 0.9,
        flatShading: true
      })
    );
    arch.position.set(0, -2.17, -1.92);
    arch.scale.y = 1.35;
    arch.castShadow = true;
    arch.receiveShadow = true;
    tank.add(arch);

    const upVector = new THREE.Vector3(0, 1, 0);
    function createCoral(x, z, color, scale = 1) {
      const root = new THREE.Group();
      root.position.set(x, -2.48, z);
      root.scale.setScalar(scale);
      const material = new THREE.MeshStandardMaterial({
        color,
        roughness: 0.62,
        flatShading: true,
        emissive: color,
        emissiveIntensity: 0.035
      });

      function branch(start, direction, length, radius, depth) {
        const end = start.clone().addScaledVector(direction, length);
        const geometry = new THREE.CylinderGeometry(radius * 0.66, radius, length, 7);
        const segment = new THREE.Mesh(geometry, material);
        segment.position.copy(start).lerp(end, 0.5);
        segment.quaternion.setFromUnitVectors(upVector, direction.clone().normalize());
        segment.castShadow = true;
        root.add(segment);

        if (depth === 0) {
          const tip = new THREE.Mesh(new THREE.IcosahedronGeometry(radius * 0.9, 1), material);
          tip.position.copy(end);
          tip.castShadow = true;
          root.add(tip);
          return;
        }

        const branches = depth > 1 ? 3 : 2;
        for (let i = 0; i < branches; i++) {
          const next = direction.clone();
          next.x += (random() - 0.5) * 0.85;
          next.z += (random() - 0.5) * 0.7;
          next.y = Math.max(0.35, next.y * 0.8 + 0.25);
          next.normalize();
          branch(end, next, length * (0.64 + random() * 0.08), radius * 0.63, depth - 1);
        }
      }

      branch(new THREE.Vector3(0, 0, 0), new THREE.Vector3((random() - 0.5) * 0.12, 1, 0).normalize(), 0.75, 0.11, 3);
      tank.add(root);
      return root;
    }

    createCoral(-4.75, -1.84, 0xf06f74, 1.08);
    createCoral(4.32, -1.95, 0xf39a52, 1.18);
    createCoral(-2.62, -2.04, 0xa982d7, 0.88);
    createCoral(2.68, -2.08, 0xe7bb58, 0.9);

    const leafShape = new THREE.Shape();
    leafShape.moveTo(0, 0);
    leafShape.bezierCurveTo(0.18, 0.20, 0.17, 0.66, 0, 1);
    leafShape.bezierCurveTo(-0.17, 0.67, -0.17, 0.21, 0, 0);
    const leafGeometry = new THREE.ShapeGeometry(leafShape);
    const plants = [];

    function addPlant(x, z, height, color, phase) {
      const group = new THREE.Group();
      group.position.set(x, -2.49, z);
      const material = new THREE.MeshStandardMaterial({
        color,
        roughness: 0.74,
        side: THREE.DoubleSide,
        emissive: color,
        emissiveIntensity: 0.025
      });

      for (let i = 0; i < 5; i++) {
        const leaf = new THREE.Mesh(leafGeometry, material);
        const h = height * (0.62 + random() * 0.52);
        leaf.scale.set(0.16 + random() * 0.08, h, 1);
        leaf.position.set((random() - 0.5) * 0.28, 0, (random() - 0.5) * 0.3);
        leaf.rotation.y = random() * Math.PI;
        leaf.rotation.z = (random() - 0.5) * 0.32;
        leaf.castShadow = true;
        group.add(leaf);
      }

      tank.add(group);
      plants.push({ group, phase, base: group.rotation.z });
    }

    addPlant(-4.95, -1.98, 1.75, 0x287f72, 0.2);
    addPlant(-3.76, -2.10, 1.38, 0x3a9a79, 1.4);
    addPlant(-1.94, -2.12, 1.18, 0x267a70, 2.7);
    addPlant(-0.86, -2.18, 1.58, 0x368d76, 4.1);
    addPlant(0.98, -2.18, 1.47, 0x2a8373, 1.1);
    addPlant(2.06, -2.12, 1.72, 0x399979, 3.3);
    addPlant(3.72, -2.08, 1.39, 0x26776e, 5.0);
    addPlant(4.95, -1.95, 1.83, 0x348a70, 2.2);

    const bubbleCount = 34;
    const bubblePositions = new Float32Array(bubbleCount * 3);
    const bubbleSizes = new Float32Array(bubbleCount);
    const bubbleData = [];

    for (let i = 0; i < bubbleCount; i++) {
      const x = (random() - 0.5) * 10.7;
      const y = -2.3 + random() * 5.7;
      const z = (random() - 0.5) * 3.8;
      bubblePositions[i * 3] = x;
      bubblePositions[i * 3 + 1] = y;
      bubblePositions[i * 3 + 2] = z;
      bubbleSizes[i] = 0.26 + random() * 0.34;
      bubbleData.push({ speed: 0.17 + random() * 0.3, phase: random() * 8, drift: 0.02 + random() * 0.04 });
    }

    const bubbleGeometry = new THREE.BufferGeometry();
    bubbleGeometry.setAttribute('position', new THREE.BufferAttribute(bubblePositions, 3));
    bubbleGeometry.setAttribute('aSize', new THREE.BufferAttribute(bubbleSizes, 1));
    const bubbleMaterial = new THREE.ShaderMaterial({
      uniforms: { uPixelRatio: { value: renderer.getPixelRatio() } },
      transparent: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      vertexShader: `
        uniform float uPixelRatio;
        attribute float aSize;
        varying float vFade;
        void main() {
          vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
          gl_Position = projectionMatrix * mvPosition;
          gl_PointSize = aSize * uPixelRatio * (82.0 / max(1.0, -mvPosition.z));
          vFade = smoothstep(0.0, 1.0, position.y + 2.5);
        }
      `,
      fragmentShader: `
        varying float vFade;
        void main() {
          vec2 uv = gl_PointCoord - 0.5;
          float dist = length(uv);
          if (dist > 0.5) discard;
          float edge = smoothstep(0.48, 0.36, dist) - smoothstep(0.31, 0.20, dist);
          float center = smoothstep(0.26, 0.04, dist) * 0.12;
          vec3 color = vec3(0.58, 0.95, 1.0) * edge + vec3(0.25, 0.65, 0.8) * center;
          gl_FragColor = vec4(color, (edge * 0.52 + center) * vFade);
        }
      `
    });
    const bubbles = new THREE.Points(bubbleGeometry, bubbleMaterial);
    tank.add(bubbles);

    const jellyfish = [];
    function createJellyfish(x, y, z, color, scale) {
      const group = new THREE.Group();
      group.position.set(x, y, z);
      group.scale.setScalar(scale);

      const material = new THREE.MeshStandardMaterial({
        color,
        emissive: color,
        emissiveIntensity: 0.34,
        transparent: true,
        opacity: 0.45,
        roughness: 0.18,
        side: THREE.DoubleSide,
        depthWrite: false
      });
      const bell = new THREE.Mesh(
        new THREE.SphereGeometry(0.34, 24, 14, 0, Math.PI * 2, 0, Math.PI / 2),
        material
      );
      bell.position.y = 0.2;
      bell.scale.y = 0.72;
      group.add(bell);

      const ring = new THREE.Mesh(
        new THREE.TorusGeometry(0.34, 0.018, 6, 30),
        new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.48, depthWrite: false })
      );
      ring.rotation.x = Math.PI / 2;
      ring.position.y = 0.2;
      group.add(ring);

      for (let i = 0; i < 5; i++) {
        const angle = i / 5 * Math.PI * 2;
        const points = [];
        for (let j = 0; j < 5; j++) {
          points.push(new THREE.Vector3(
            Math.cos(angle) * (0.10 + j * 0.015),
            0.16 - j * 0.19,
            Math.sin(angle) * (0.10 + j * 0.015) + Math.sin(j * 1.6 + i) * 0.025
          ));
        }
        const curve = new THREE.CatmullRomCurve3(points);
        const tentacle = new THREE.Mesh(
          new THREE.TubeGeometry(curve, 18, 0.011, 5, false),
          new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.35, depthWrite: false })
        );
        group.add(tentacle);
      }

      tank.add(group);
      jellyfish.push({ group, baseY: y, phase: random() * 8 });
    }

    createJellyfish(-3.72, 1.62, -1.62, 0x8bdcff, 0.88);
    createJellyfish(3.47, 2.12, -1.68, 0xc1a0ff, 0.72);

    const species = {
      golden: {
        label: 'Golden tang',
        body: 0xffcf3e,
        accent: 0x173c5a,
        dims: [0.57, 0.33, 0.235],
        speed: 0.92,
        scale: 1,
        home: new THREE.Vector3(-2.15, 0.15, 0.2)
      },
      blue: {
        label: 'Blue tang',
        body: 0x3278f2,
        accent: 0xffd344,
        dims: [0.55, 0.34, 0.225],
        speed: 0.98,
        scale: 0.98,
        home: new THREE.Vector3(2.0, 0.28, 0.05)
      },
      clown: {
        label: 'Clown anemonefish',
        body: 0xff7835,
        accent: 0xffa047,
        dims: [0.49, 0.30, 0.215],
        speed: 0.82,
        scale: 0.9,
        home: new THREE.Vector3(0.0, -0.78, 0.42)
      },
      neon: {
        label: 'Neon tetra',
        body: 0x75e8e7,
        accent: 0xff4d69,
        dims: [0.37, 0.155, 0.13],
        speed: 1.22,
        scale: 0.88,
        home: new THREE.Vector3(-0.5, 1.33, -0.2)
      },
      angel: {
        label: 'Moon angelfish',
        body: 0xf1d294,
        accent: 0x29475b,
        dims: [0.38, 0.49, 0.14],
        speed: 0.72,
        scale: 0.96,
        home: new THREE.Vector3(0.25, 0.92, -0.82)
      }
    };

    const bodyGeometry = new THREE.SphereGeometry(1, 26, 18);
    const eyeGeometry = new THREE.SphereGeometry(1, 12, 8);
    const whiteMaterial = new THREE.MeshStandardMaterial({ color: 0xf4ffff, roughness: 0.3 });
    const darkMaterial = new THREE.MeshStandardMaterial({ color: 0x071117, roughness: 0.2 });
    const fish = [];

    function createTailGeometry() {
      const shape = new THREE.Shape();
      shape.moveTo(0, 0);
      shape.bezierCurveTo(-0.13, 0.17, -0.38, 0.25, -0.56, 0.35);
      shape.bezierCurveTo(-0.49, 0.10, -0.49, -0.10, -0.56, -0.35);
      shape.bezierCurveTo(-0.34, -0.25, -0.12, -0.12, 0, 0);
      return new THREE.ShapeGeometry(shape);
    }

    function createDorsalGeometry() {
      const shape = new THREE.Shape();
      shape.moveTo(-0.24, 0.04);
      shape.bezierCurveTo(-0.16, 0.30, 0.07, 0.43, 0.25, 0.06);
      shape.bezierCurveTo(0.08, 0.14, -0.10, 0.12, -0.24, 0.04);
      return new THREE.ShapeGeometry(shape);
    }

    function createPectoralGeometry() {
      const shape = new THREE.Shape();
      shape.moveTo(0, 0);
      shape.quadraticCurveTo(-0.16, -0.14, -0.28, -0.02);
      shape.quadraticCurveTo(-0.14, 0.06, 0, 0);
      return new THREE.ShapeGeometry(shape);
    }

    const tailGeometry = createTailGeometry();
    const dorsalGeometry = createDorsalGeometry();
    const pectoralGeometry = createPectoralGeometry();

    function createFish(type, position, scale, phase) {
      const config = species[type];
      const root = new THREE.Group();
      const visual = new THREE.Group();
      visual.scale.setScalar(scale * config.scale);
      root.add(visual);
      root.position.copy(position);

      const bodyMaterial = new THREE.MeshStandardMaterial({
        color: config.body,
        roughness: 0.31,
        metalness: 0.055,
        emissive: config.body,
        emissiveIntensity: 0.055
      });
      const accentMaterial = new THREE.MeshStandardMaterial({
        color: config.accent,
        roughness: 0.38,
        metalness: 0.035,
        side: THREE.DoubleSide,
        emissive: config.accent,
        emissiveIntensity: 0.04
      });

      const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
      body.scale.set(...config.dims);
      body.castShadow = true;
      visual.add(body);

      const tail = new THREE.Mesh(tailGeometry, accentMaterial);
      tail.position.x = -config.dims[0] * 0.82;
      tail.scale.set(1, type === 'angel' ? 1.28 : type === 'neon' ? 0.72 : 1, 1);
      tail.castShadow = true;
      visual.add(tail);

      const dorsal = new THREE.Mesh(dorsalGeometry, bodyMaterial);
      dorsal.position.set(-0.05, config.dims[1] * 0.62, 0);
      dorsal.scale.set(config.dims[0] * 1.05, config.dims[1] * 1.1, 1);
      dorsal.castShadow = true;
      visual.add(dorsal);

      const pectorals = [];
      for (const side of [-1, 1]) {
        const pectoral = new THREE.Mesh(pectoralGeometry, accentMaterial);
        pectoral.position.set(0.08, -config.dims[1] * 0.08, side * config.dims[2] * 0.88);
        pectoral.rotation.z = side * 0.22;
        pectoral.scale.setScalar(config.dims[0] * 1.2);
        visual.add(pectoral);
        pectorals.push(pectoral);
      }

      const eyeRadius = Math.min(0.074, config.dims[0] * 0.115);
      const eyeX = config.dims[0] * 0.66;
      const eyeY = config.dims[1] * 0.23;

      for (const side of [-1, 1]) {
        const eye = new THREE.Mesh(eyeGeometry, whiteMaterial);
        eye.position.set(eyeX, eyeY, side * config.dims[2] * 0.88);
        eye.scale.setScalar(eyeRadius);
        visual.add(eye);

        const pupil = new THREE.Mesh(eyeGeometry, darkMaterial);
        pupil.position.set(eyeX + eyeRadius * 0.32, eyeY, side * (config.dims[2] * 0.88 + eyeRadius * 0.5));
        pupil.scale.setScalar(eyeRadius * 0.48);
        visual.add(pupil);
      }

      const mouth = new THREE.Mesh(eyeGeometry, darkMaterial);
      mouth.position.set(config.dims[0] * 0.94, -config.dims[1] * 0.06, 0);
      mouth.scale.set(config.dims[0] * 0.032, config.dims[1] * 0.045, config.dims[2] * 0.04);
      visual.add(mouth);

      if (type === 'clown') {
        for (const x of [-0.27, 0, 0.27]) {
          const band = new THREE.Mesh(new THREE.TorusGeometry(1, 0.105, 8, 32), whiteMaterial);
          band.rotation.y = Math.PI / 2;
          band.position.x = x;
          band.scale.set(config.dims[2] * 0.93, config.dims[1] * 0.94, 0.67);
          band.castShadow = true;
          visual.add(band);
        }
      }

      if (type === 'angel') {
        for (const x of [-0.12, 0.16]) {
          const band = new THREE.Mesh(new THREE.TorusGeometry(1, 0.085, 8, 28), accentMaterial);
          band.rotation.y = Math.PI / 2;
          band.position.x = x;
          band.scale.set(config.dims[2] * 0.94, config.dims[1] * 0.95, 0.72);
          visual.add(band);
        }
      }

      if (type === 'golden') {
        for (const side of [-1, 1]) {
          const spot = new THREE.Mesh(new THREE.CircleGeometry(1, 22), accentMaterial);
          spot.position.set(0.19, -0.015, side * config.dims[2] * 0.96);
          spot.scale.set(0.10, 0.135, 1);
          visual.add(spot);
        }
      }

      if (type === 'neon') {
        const stripeMaterial = new THREE.MeshStandardMaterial({
          color: 0xd8ffff,
          emissive: 0x74ffff,
          emissiveIntensity: 1.15,
          roughness: 0.3
        });
        for (const side of [-1, 1]) {
          const stripe = new THREE.Mesh(bodyGeometry, stripeMaterial);
          stripe.position.set(0, 0.015, side * config.dims[2] * 0.94);
          stripe.scale.set(config.dims[0] * 0.64, 0.026, 0.018);
          visual.add(stripe);
        }
      }

      const initialVelocity = new THREE.Vector3(
        random() - 0.5,
        (random() - 0.5) * 0.45,
        (random() - 0.5) * 0.5
      );
      if (initialVelocity.lengthSq() < 0.1) initialVelocity.set(1, 0, 0);
      initialVelocity.normalize().multiplyScalar(config.speed);

      const target = config.home.clone().add(new THREE.Vector3(
        (random() - 0.5) * 2.5,
        (random() - 0.5) * 1.8,
        (random() - 0.5) * 1.4
      ));

      const state = {
        root,
        visual,
        body,
        bodyBaseScale: body.scale.clone(),
        tail,
        pectorals,
        type,
        config,
        velocity: initialVelocity,
        target,
        home: config.home.clone(),
        phase,
        flee: 0,
        decisionTime: 1 + random() * 3,
        bank: 0
      };

      tank.add(root);
      fish.push(state);
    }

    const roster = [
      ['golden', 3],
      ['blue', 3],
      ['clown', 3],
      ['neon', 3],
      ['angel', 2]
    ];

    for (const [type, count] of roster) {
      const config = species[type];
      for (let i = 0; i < count; i++) {
        const position = config.home.clone().add(new THREE.Vector3(
          (random() - 0.5) * 3.5,
          (random() - 0.5) * 2.0,
          (random() - 0.5) * 1.7
        ));
        position.x = THREE.MathUtils.clamp(position.x, -4.9, 4.9);
        position.y = THREE.MathUtils.clamp(position.y, -1.95, 2.62);
        position.z = THREE.MathUtils.clamp(position.z, -1.35, 1.4);
        createFish(type, position, 0.88 + random() * 0.2, random() * Math.PI * 2);
      }
    }

    residentCount.textContent = String(fish.length);

    const pointer = {
      x: window.innerWidth * 0.5,
      y: window.innerHeight * 0.5,
      ndc: new THREE.Vector2(),
      active: false,
      type: 'mouse',
      radius: window.innerWidth < 640 ? 112 : 150
    };

    let started = false;
    let fleeingCount = 0;

    function begin() {
      if (started) return;
      started = true;
      prompt.classList.add('is-hidden');
    }

    window.addEventListener('pointermove', event => {
      pointer.x = event.clientX;
      pointer.y = event.clientY;
      pointer.ndc.set(
        event.clientX / window.innerWidth * 2 - 1,
        -(event.clientY / window.innerHeight) * 2 + 1
      );
      pointer.active = true;
      pointer.type = event.pointerType;
      aura.style.left = `${pointer.x}px`;
      aura.style.top = `${pointer.y}px`;
      aura.classList.add('active');
      begin();
    }, { passive: true });

    document.addEventListener('mouseleave', () => {
      pointer.active = false;
      aura.classList.remove('active');
      fishTag.classList.remove('visible');
    });

    const rippleRaycaster = new THREE.Raycaster();
    const ripplePlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), -0.25);
    const ripples = [];

    function createRipple(clientX, clientY) {
      const ndc = new THREE.Vector2(
        clientX / window.innerWidth * 2 - 1,
        -(clientY / window.innerHeight) * 2 + 1
      );
      rippleRaycaster.setFromCamera(ndc, camera);
      const hit = rippleRaycaster.ray.intersectPlane(ripplePlane, new THREE.Vector3());
      if (!hit) return;

      const material = new THREE.MeshBasicMaterial({
        color: 0x9dfff0,
        transparent: true,
        opacity: 0.7,
        side: THREE.DoubleSide,
        depthWrite: false
      });
      const mesh = new THREE.Mesh(new THREE.RingGeometry(0.1, 0.135, 48), material);
      mesh.position.copy(hit);
      mesh.position.z = 0.28;
      scene.add(mesh);
      ripples.push({ mesh, age: 0 });
    }

    window.addEventListener('pointerdown', event => {
      pointer.x = event.clientX;
      pointer.y = event.clientY;
      pointer.ndc.set(
        event.clientX / window.innerWidth * 2 - 1,
        -(event.clientY / window.innerHeight) * 2 + 1
      );
      pointer.active = true;
      pointer.type = event.pointerType;
      begin();
      createRipple(event.clientX, event.clientY);
    }, { passive: true });

    window.addEventListener('keydown', event => {
      if (event.code === 'Space' || event.key.length === 1) event.preventDefault();
      begin();
    });

    const projection = new THREE.Vector3();
    const worldPosition = new THREE.Vector3();
    const axisX = new THREE.Vector3(1, 0, 0);
    const orientation = new THREE.Quaternion();
    const desiredVelocity = new THREE.Vector3();
    const steering = new THREE.Vector3();
    const toTarget = new THREE.Vector3();
    const awayFromPointer = new THREE.Vector3();
    const averageVelocity = new THREE.Vector3();
    const averagePosition = new THREE.Vector3();
    const separation = new THREE.Vector3();
    const difference = new THREE.Vector3();

    function updateFish(delta, time) {
      fleeingCount = 0;
      let nearestDistance = Infinity;
      let nearestFish = null;

      for (const current of fish) {
        const position = current.root.position;
        steering.set(0, 0, 0);
        current.decisionTime -= delta;

        if (current.decisionTime <= 0) {
          current.decisionTime = 1.7 + random() * 3.1;
          current.target.set(
            current.home.x + (random() - 0.5) * 3.6,
            current.home.y + (random() - 0.5) * 2.3,
            current.home.z + (random() - 0.5) * 1.8
          );
          current.target.x = THREE.MathUtils.clamp(current.target.x, -4.7, 4.7);
          current.target.y = THREE.MathUtils.clamp(current.target.y, -1.9, 2.65);
          current.target.z = THREE.MathUtils.clamp(current.target.z, -1.25, 1.35);
        }

        toTarget.copy(current.target).sub(position);
        if (toTarget.lengthSq() > 0.04) {
          steering.addScaledVector(toTarget.normalize(), 0.88);
        }

        averageVelocity.set(0, 0, 0);
        averagePosition.set(0, 0, 0);
        separation.set(0, 0, 0);
        let neighbors = 0;

        for (const other of fish) {
          if (other === current || other.type !== current.type) continue;
          difference.copy(position).sub(other.root.position);
          const distance = difference.length();
          if (distance < 2.55 && distance > 0.001) {
            averageVelocity.add(other.velocity);
            averagePosition.add(other.root.position);
            neighbors++;
          }
          if (distance < 0.76 && distance > 0.001) {
            separation.addScaledVector(difference.normalize(), (0.76 - distance) * 2.0);
          }
        }

        if (neighbors > 0) {
          averageVelocity.divideScalar(neighbors).sub(current.velocity).multiplyScalar(0.26);
          averagePosition.divideScalar(neighbors).sub(position).normalize().multiplyScalar(0.16);
          steering.add(averageVelocity).add(averagePosition);
        }
        steering.add(separation);

        if (position.x > 4.75) steering.x -= (position.x - 4.75) * 4.4;
        if (position.x < -4.75) steering.x += (-4.75 - position.x) * 4.4;
        if (position.y > 2.62) steering.y -= (position.y - 2.62) * 5.2;
        if (position.y < -2.02) steering.y += (-2.02 - position.y) * 5.2;
        if (position.z > 1.42) steering.z -= (position.z - 1.42) * 4.0;
        if (position.z < -1.38) steering.z += (-1.38 - position.z) * 4.0;

        if (pointer.active) {
          current.root.getWorldPosition(worldPosition);
          projection.copy(worldPosition).project(camera);
          const screenX = (projection.x * 0.5 + 0.5) * window.innerWidth;
          const screenY = (-projection.y * 0.5 + 0.5) * window.innerHeight;
          const dx = screenX - pointer.x;
          const dy = screenY - pointer.y;
          const distance = Math.hypot(dx, dy);

          if (distance < pointer.radius) {
            const strength = Math.pow(1 - distance / pointer.radius, 2);
            const viewHeight = 2 * Math.tan(THREE.MathUtils.degToRad(camera.fov * 0.5)) * camera.position.z;
            const viewWidth = viewHeight * camera.aspect;
            awayFromPointer.set(
              dx * viewWidth / window.innerWidth,
              -dy * viewHeight / window.innerHeight,
              0
            );
            if (awayFromPointer.lengthSq() < 0.0001) {
              awayFromPointer.set(Math.cos(current.phase), Math.sin(current.phase), 0);
            }
            awayFromPointer.normalize();
            steering.addScaledVector(awayFromPointer, 6.2 * strength);
            steering.z += strength * 1.2;
            current.flee = Math.max(current.flee, strength);
          }

          if (distance < nearestDistance) {
            nearestDistance = distance;
            nearestFish = current;
          }
        }

        if (current.flee > 0.08) {
          fleeingCount++;
          current.flee *= Math.exp(-delta * 1.35);
        } else {
          current.flee = 0;
        }

        let targetSpeed = current.config.speed * (1 + current.flee * 1.35);
        if (steering.lengthSq() > 0.00001) {
          desiredVelocity.copy(steering).normalize().multiplyScalar(targetSpeed);
        } else {
          desiredVelocity.copy(current.velocity).normalize().multiplyScalar(targetSpeed);
        }

        const response = 1 - Math.exp(-delta * (current.flee > 0.08 ? 8.5 : 2.7));
        current.velocity.lerp(desiredVelocity, response);
        const maxSpeed = current.config.speed * 2.25;
        if (current.velocity.length() > maxSpeed) current.velocity.setLength(maxSpeed);
        if (current.velocity.length() < current.config.speed * 0.32) {
          current.velocity.setLength(current.config.speed * 0.32);
        }

        position.addScaledVector(current.velocity, delta);

        if (position.x > 5.12) { position.x = 5.12; current.velocity.x = -Math.abs(current.velocity.x); }
        if (position.x < -5.12) { position.x = -5.12; current.velocity.x = Math.abs(current.velocity.x); }
        if (position.y > 2.78) { position.y = 2.78; current.velocity.y = -Math.abs(current.velocity.y); }
        if (position.y < -2.16) { position.y = -2.16; current.velocity.y = Math.abs(current.velocity.y); }
        if (position.z > 1.58) { position.z = 1.58; current.velocity.z = -Math.abs(current.velocity.z); }
        if (position.z < -1.52) { position.z = -1.52; current.velocity.z = Math.abs(current.velocity.z); }

        if (current.velocity.lengthSq() > 0.00001) {
          orientation.setFromUnitVectors(axisX, current.velocity.clone().normalize());
          current.root.quaternion.slerp(orientation, 1 - Math.exp(-delta * 5.2));
        }

        const desiredBank = THREE.MathUtils.clamp(current.velocity.y * 0.18 - current.flee * 0.13, -0.28, 0.28);
        current.bank = THREE.MathUtils.lerp(current.bank, desiredBank, 1 - Math.exp(-delta * 5));
        current.visual.rotation.z = current.bank;
        current.visual.position.y = Math.sin(time * 2.1 + current.phase) * 0.022;
        current.tail.rotation.y = Math.sin(time * (8.5 + current.flee * 5) + current.phase) * (0.32 + current.flee * 0.36);
        current.tail.rotation.z = Math.sin(time * 4.2 + current.phase) * 0.07;
        current.body.scale.y = current.bodyBaseScale.y * (1 + Math.sin(time * 2.4 + current.phase) * 0.014);
        for (let i = 0; i < current.pectorals.length; i++) {
          current.pectorals[i].rotation.x = Math.sin(time * 7.2 + current.phase + i * Math.PI) * 0.34;
        }
      }

      if (pointer.active && nearestFish && nearestDistance < pointer.radius * 0.92) {
        fishTag.classList.add('visible');
        fishTagText.textContent = nearestFish.config.label;
        fishTag.style.setProperty('--fish-color', '#' + new THREE.Color(nearestFish.config.body).getHexString());
        fishTag.style.left = `${Math.min(pointer.x, window.innerWidth - 190)}px`;
        fishTag.style.top = `${Math.max(12, pointer.y - 36)}px`;
      } else {
        fishTag.classList.remove('visible');
      }

      if (statusText.textContent !== (fleeingCount > 0 ? `${fleeingCount} fish darting away` : 'Calm water')) {
        statusText.textContent = fleeingCount > 0 ? `${fleeingCount} fish darting away` : 'Calm water';
        liveDot.classList.toggle('alert', fleeingCount > 0);
      }
    }

    function updateBubbles(delta, time) {
      const positions = bubbleGeometry.attributes.position;
      for (let i = 0; i < bubbleCount; i++) {
        let y = positions.getY(i) + bubbleData[i].speed * delta;
        if (y > 3.33) y = -2.35;
        const x = positions.getX(i) + Math.sin(time * 0.8 + bubbleData[i].phase) * bubbleData[i].drift * delta;
        positions.setY(i, y);
        positions.setX(i, x);
      }
      positions.needsUpdate = true;
    }

    function resize() {
      const width = window.innerWidth;
      const height = window.innerHeight;
      const mobile = width < 640;

      camera.aspect = width / height;
      camera.fov = mobile ? 37 : 42;
      cameraBaseZ = mobile ? 15.0 : 10.9;
      camera.updateProjectionMatrix();

      renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.8));
      renderer.setSize(width, height, false);
      bubbleMaterial.uniforms.uPixelRatio.value = renderer.getPixelRatio();
      pointer.radius = mobile ? 112 : 150;
      aura.style.width = `${pointer.radius * 2}px`;
      aura.style.height = `${pointer.radius * 2}px`;
    }

    window.addEventListener('resize', resize);
    resize();

    const clock = new THREE.Clock();

    function animate() {
      const delta = Math.min(clock.getDelta(), 0.04);
      const time = clock.elapsedTime;

      const desiredCameraX = pointer.active && pointer.type !== 'touch' ? pointer.ndc.x * 0.23 : 0;
      const desiredCameraY = pointer.active && pointer.type !== 'touch' ? 0.18 + pointer.ndc.y * 0.13 : 0.18;
      camera.position.x = THREE.MathUtils.lerp(camera.position.x, desiredCameraX, 1 - Math.exp(-delta * 2.2));
      camera.position.y = THREE.MathUtils.lerp(camera.position.y, desiredCameraY, 1 - Math.exp(-delta * 2.2));
      camera.position.z = cameraBaseZ;
      camera.lookAt(0, 0.05, 0);
      camera.updateMatrixWorld();
      scene.updateMatrixWorld();

      backUniforms.uTime.value = time;
      causticUniforms.uTime.value = time;
      waterUniforms.uTime.value = time;

      updateFish(delta, time);
      updateBubbles(delta, time);

      for (const plant of plants) {
        plant.group.rotation.z = plant.base + Math.sin(time * 0.58 + plant.phase) * 0.045;
        plant.group.rotation.x = Math.sin(time * 0.42 + plant.phase * 1.3) * 0.022;
      }

      for (const jelly of jellyfish) {
        jelly.group.position.y = jelly.baseY + Math.sin(time * 0.75 + jelly.phase) * 0.16;
        jelly.group.rotation.z = Math.sin(time * 0.43 + jelly.phase) * 0.08;
        jelly.group.scale.y = jelly.group.scale.x * (1 + Math.sin(time * 1.7 + jelly.phase) * 0.055);
      }

      for (let i = ripples.length - 1; i >= 0; i--) {
        const ripple = ripples[i];
        ripple.age += delta;
        const progress = Math.min(1, ripple.age / 0.85);
        const scale = 1 + progress * 5.4;
        ripple.mesh.scale.setScalar(scale);
        ripple.mesh.material.opacity = 0.68 * (1 - progress) * (1 - progress);
        if (progress >= 1) {
          scene.remove(ripple.mesh);
          ripple.mesh.geometry.dispose();
          ripple.mesh.material.dispose();
          ripples.splice(i, 1);
        }
      }

      renderer.render(scene, camera);
    }

    renderer.setAnimationLoop(animate);
  </script>
</body>
</html>
Reasoning trace (23,194 tokens)
We need produce exactly one fenced code block complete HTML. Need make 3D aquarium Three.js import map specified only. Need no external files/images/fonts network except CDN. Need inline CSS JS. Need visual rich, 1280x800 responsive 375. Fish swim away when mouse near them. Need self-playing demo maybe click/press key message requirement only if keyboard; user asks mouse interaction, no need key perhaps click to start? "Games must start in self-playing demo mode (simple AI plays) behind a 'Click or press a key to start' message" This is not necessarily game, but interactive experience. Could still have overlay "Click or press a key to start" and aquarium animates? Requirement says behind message. Better message clearly and fades on click/mousemove, but fish visible and moving perhaps before start. Yet "games must start ... behind a message". Aquarium is toy, could auto-animate behind overlay, with prompt "Click or press a key to start" then any pointer movement maybe dismiss. But user specifically wants moving mouse near fish, if first interaction movement dismisses and proximity starts. Fine.

Need use Three.js. Need build detailed aquarium: glass tank perhaps transparent walls, blue gradient background, sandy floor, rocks, coral, bubbles, seaweed, fish of multiple species with geometry, fins, eyes. Mouse raycasting/proximity in screen coordinates or 3D. Fish swim away when cursor near them. Need make compelling.

Need carefully code zero errors. Three r170. Import map exact perhaps script type importmap JSON. Then module import THREE and maybe EffectComposer? Could avoid addons to minimize. Use WebGLRenderer, fog, lights. Need shape fish perhaps Group with body (scaled SphereGeometry), tail, fins, eyes, stripes. Use MeshStandardMaterial. Add caustic light? Could animate point lights.

Need responsive and polished UI. Main scene full viewport. Overlay title top left "TIDELINE / LIVING AQUARIUM", badges, bottom control panel. Need first paint meaningful. Need no overflow mobile. Could have UI layer pointer-events none except buttons/control. Interaction prompt centered maybe.

Need mouse behavior. We can create raycaster from pointer, intersect fish hit meshes and also screen-space nearest. "move mouse near them": pointer ray in camera direction, calculate distance to fish world positions. Better project each fish to NDC each frame, find fish whose projected pixel distance < proximity radius (e.g. 140 px) and in front. Then avoidance vector away from pointer in screen coordinates, mapped into world x/y, plus z slight. Fish schools have target steering. Need 3D scene tank coordinate. Fish movement constrained x [-5.3,5.3], y [0.8,4.2], z [-1.7,1.5]. Tank maybe x width 11.4, height 6.2, depth 4.3, floor y=-2.8? Let's set scene coordinates:
floor y=-2.7, top y=3.5, x +/-5.5, z +/-2.1. Camera at (0,0.4,11.5), looking (0,0,0). Water back wall maybe z=-2.2.
Fish around y -1.5 to 2.5.
Screen dimensions responsive; camera adapt aspect. At mobile, perhaps camera z 13 and look x? width narrow means tank clips. We can camera distance based aspect to fit x width: vertical FOV 42, at aspect 0.47 need z ~ 12.8 to fit width 5.5 (tan(21)*z*aspect = width); 5.5/(.384*.469)=30.5! Can't fit 11 width at narrow without cropping. Graceful adaptation might show central tank with some crop, not horizontal scroll. We can scale tank scene? Or camera responsive and position. Better camera z computed max(11.2, 6.6/(tan fov/2*aspect)) ~31 on mobile, tiny. Could instead rotate tank or use CSS? Three canvas full. Mobile central crop with tank extending beyond width okay, no horizontal scroll. But user sees fish. At 375x800 aspect .47, camera z 12.5, horizontal visible ~4.4, enough several fish. We can shift groups into central 3.5 width on mobile? We can update fish x bounds based viewport? But geometry fixed. Could use camera x 0 and visible central. Fine. Title adapt.

Need glass tank with perspective and maybe water volume. Could create transparent rounded? Three geometry no RoundedBox unless addon; could use BoxGeometry with MeshPhysicalMaterial transparent. Back panel at z=-2.25, floor, side glass edges. We want realistic aquarium not box in room maybe underwater environment. We can make huge blue gradient via CSS body behind transparent renderer, scene.background perhaps Color dark navy. Fog. Add back wall plane with gradient shader? Inline ShaderMaterial perhaps. The actual tank can have glass edges.

Maybe scene:
- canvas full screen
- renderer alpha true, antialias true, toneMapping ACES, output colorSpace SRGB, shadows.
- camera fov 43.
- scene background Color #031a2b; fog exp2 #06233a density .035.
- HemisphereLight sky #b8f5ff ground #06121f intensity 2.
- directional light top-left #dffcff intensity 2.5, castShadow.
- PointLight cyan at (-3,2,4) intensity 80? physicallyCorrect? r170 lights units. Use 45 maybe.
- ambient.
- caustic texture via CanvasTexture generated in JS allowed (no external image), mapped onto floor/walls perhaps. But canvas creation in inline JS no external. Could add PlaneGeometry floor with ShaderMaterial to animate caustics. We can use custom shader on floor and back wall? More visual complexity.
- tank floor PlaneGeometry 12,6 rotated; material MeshStandard with sand color, roughness. Add procedural dunes via PlaneGeometry vertices manipulated in JS? Set plane segments and heights. But floor perspective.
- Gravel pebbles instanced random shapes.
- rocks dodecahedron geometries and coral branches.
- seaweed via curved TubeGeometry? Could create flat leaves/planes or use curves. Use CatmullRomCurve3 and TubeGeometry, animate slight sway? group rotate.
- coral using cylinders/spheres.
- bubbles spheres with transparent material, rise.
- fish.

Fish:
Create different species and colors. Group root. body sphere scaled (0.55,0.3,0.24), oriented along local +X maybe. Group.lookAt movement direction. Geometry body mesh userData fish. Tail cone? Use ConeGeometry radius .3, height .55 rotated z? Cone points along y by default, need tail plane, local x negative. Could use BufferGeometry triangle shape for tail in XY plane, double side. Since body elongated x. Tail triangle vertices (-.45,0,0), (-.9,.32,0), (-.9,-.32,0), maybe using ShapeGeometry then positioned, but shape coordinates x/y and z=0; fine. Better curved tail using Shape with bezier. create THREE.Shape; ShapeGeometry. Add slight z. Fins dorsal/ventral as ShapeGeometry triangle. Pectoral fins.
Eyes on both z sides at x .33, y .08, z ±.21: white sphere + black pupil.
Stripes: for clownfish/angelfish. Could add several torus? easier narrow ellipsoid surface overlay? Dark stripe meshes as torus segments? Use TorusGeometry and scale? A torus around body oriented around x axis: default torus in XY plane normal Z. Body long X, cross-section YZ, so torus normal X; rotate y = PI/2. But striped band around x positions: TorusGeometry(radius .29, tube .055), rotate y PI/2, scale maybe y/z? Torus radial, cross body. Fish body ellipse x .55 y .3 z .24. Torus radius .3, scale z .75? Since after rotation? Need orientation. Torus local XY -> after rotateY 90 maps local x to z, local y stays y, so ellipse z? scale local x .8, local y 1, okay. Tube protrudes. Position x. For stripe fish use dark material. But if opaque overlay ring perhaps looks like belt around fish. Fine.
- yellow tang: body bright yellow, dark pattern via maybe torus-like patches.
- blue tang: royal blue body, yellow tail.
- clownfish orange with white bands.
- neon tetra small slender cyan with red tail.
- angelfish tall body scaled.
Could make 13 fish. Need all schools naturally swim. Each fish has current velocity and target vector, phase, speed, group. Autonomous wandering using target updates; boundary steering; maybe boids alignment/cohesion/separation. Implement each fish with school ID. For each frame, calculate neighbor forces in same school and flock. Also pointer avoidance projected screen proximity:
  pointer active if pointermove and not over UI? all.
  project fish.getWorldPosition(temp) to NDC. convert px. Distance normalized maybe px. If distance < radius (desktop 160; mobile 100), force strength (1-d/r)^2 * 7. Map screen dx/dy to world direction: world x = dx * 2? screen up corresponds world y = -dy normalized. Determine vector from pointer to fish: (fishPx - pointerPx); world x = dx / viewport width * visibleWidth, world y = -dy / height * visibleHeight. Normalize and add avoidance. Also depth push toward camera z + 0.7 perhaps. Need fish swim away fast, body turns.

Could maybe use pointer ray plane at fish z and calculate world distance in x/y. But project approach robust.

Fish movement:
state = position group.position, velocity Vector3, target, timer, school. Each update:
- autonomous target randomly every 2-5 seconds within bounds.
- schooling:
  alignment average normalized velocity * 0.2; cohesion toward neighbors * .08; separation inverse dist.
- pointer force.
- boundary force.
- maintain depth around z.
- velocity lerp desired; speed clamp species speed [1.0,2.2].
- position add velocity dt.
- orient group along +X. group.rotation.y? group.lookAt(position + velocity) makes local -Z point target by default? Object3D.lookAt aligns its +Z toward target for objects? For camera -Z, object +Z. Our body +X, so need quaternion setFromUnitVectors(new Vector3(1,0,0), direction). Use root.quaternion.slerp(q, factor). But roll not needed.
- tail wiggle child rotation y? Tail plane XY, wag around y (z movement), yes tail.rotation.y = sin*0.5.
- pectoral fins rotate x.
- body bob.
Need avoid fish group includes hit shapes but projection root position okay.

Autonomous fish perhaps if pointer inactive, wander. On pointermove, pointer active and prompt hidden. The prompt says click or key to start but pointer movement itself should hide. Since mouse moves likely. We can include overlay at initial center maybe visually covers. We need ensure "meaningful visible". Under overlay, aquarium visible.

Could implement click to enter "explore mode" with pointer lock? no. Prompt "MOVE CURSOR • CLICK OR PRESS A KEY TO START" and after any click/key, disappears, but then fish movement might already react even before. Better overlay doesn't block pointer? pointer-events:none so pointer moves; on first pointermove hide it. But wording says behind message. Good.

Maybe a "FLEE ZONE" visual ring follows cursor? Could show subtle radial gradient/circle at pointer, indicating fish flee. Use custom cursor glow overlay div fixed width 220, pointer-events none, opacity; follows pointer. But if hover fish, ring. Could make using CSS radial gradient. Helps communicate. On touch, tap. Add label near bottom. On mobile, no cursor; tap.

UI:
Top left:
small pill "INTERACTIVE HABITAT"
h1 "Tidepool"
subtitle "A living 3D aquarium"
Maybe top right "14 residents / water 24°C"
Bottom centered glass card: "Move your cursor through the water" with icon and "Fish keep their distance". and "click / tap to calm the current" perhaps.
At bottom left species tags with colored dots. But avoid clutter.
Maybe status top-right: LIVE, water temperature.
Need user asked only aquarium, UI can enhance.

Need aquarium tank framing:
Could create tank frame edges using cylinders/boxes:
- bottom rim at y=-2.8 x length 12 z depth 4.8 dark navy maybe.
- top rim perhaps not visible? Full glass tank with thin bright cyan edges, transparent panels.
- back glass plane maybe blue transparent.
- water volume: BoxGeometry(11.2,6,4.2) MeshPhysicalMaterial color cyan transparent opacity .05 side BackSide? But can obscure. Use edges lines via EdgesGeometry LineSegments with transparent.
- Floor inside.
- front glass at z=2.2 MeshPhysical transmission? transmission requires env maybe; transparent opacity .04 roughness. Use MeshPhysicalMaterial.
- water surface at y=3.45 plane horizontal, transparent cyan, maybe animated shader with ripples and reflected highlight.
- bubbles in tank.

Caustics:
Create ShaderMaterial floor fragment with animated sin patterns? Use plane with standard plus overlay transparent plane custom shader at y floor + .03. But MeshBasic shader with uniforms time. fragment computes voronoi-ish waves using sine. Need no texture.
Back wall maybe large PlaneGeometry 12x7 at z=-2.35, MeshStandard color #06354b roughness. Add large gradient maybe vertex colors. Could make ShaderMaterial with radial/linear color, caustic sin. But light interactions. Use ShaderMaterial for back:
vertex pass uv, frag:
vec3 top = ...; vec3 bottom; float gradient; caustics = pow(abs(sin(...)+sin(...)), maybe smooth; add.
Need color correct. Transparent? no.
Floor Plane 12x5 at y=-2.7 rotate -pi/2. Could use custom shader as well to appear sand with caustics. But standard and separate caustic overlay.
Need perhaps front plane with slight blue tint could wash.

Geometry:
Rocks: DodecahedronGeometry scaled, at floor. Material flatShading.
Coral:
- branching coral built from cylinders between points (function addBranch). Could create each segment CylinderGeometry, orient along vector using quaternion. Branching structure bright coral orange/pink.
- fan coral maybe TorusGeometry partial? use Tube curve.
Seaweed: leaves as tapered planes (ShapeGeometry) or tubes. Use green/teal tubes with multiple points. animate group rotation z.
Anemone tubes.
Maybe central arch rock to add visual hierarchy. A rock arch could be created using torus geometry partial (TorusGeometry radius 1.2 tube .5 arc PI) rotated x? But fish tank. Torus lies XY by default, positioned y -1.8, z -1.4, arc from 0 to PI giving upper half? Torus parameter starts +x, arc upper, good. Color stone. Could fill floor under. It might look nice but can distract. Instead rocks and corals at sides leaving center swimming lane.

Background bubbles and tiny particles.

Water surface:
PlaneGeometry 11.2,4.2, 64,24, rotate x -PI/2, at y=3.35, ShaderMaterial transparent opacity with vertex displacement sin. Fragment varying cyan shimmer. But camera front at y 0.3, surface near top, visible.
Glass walls edges.

Caustic gobo perhaps projected via SpotLight map unavailable. Custom shader overlay on floor/back.

Need maybe use THREE MathUtils. No external assets.

Camera:
desktop pos (0,0.3,11.8), lookAt(0,0,0). On mobile pos z 13.8 perhaps, fov 47, visible width at z ~13: 2*tan23.5*13*.469=5.36. central enough. Title top may overlap but okay.
At 1280x800 aspect 1.6, visible width 2*tan21.5*11.8*1.6=14.9, tank 12 fits. Great.
Tank top 3.5 and bottom -2.8 vertical total 6.3; vertical visible at z11.8 and fov43 =9.3, lots space for UI, tank centered perhaps camera y 0.2. Fine. At desktop tank occupies ~68% height. Could camera z 10.4 gives vertical 8.2, tank 77%. x visible 13.1, tank 12 fits. Good. Camera y=0.1, look at y=0.1. On mobile z=14.2 visible width 5.36 and vertical 11.4, aquarium central vertical small. We could set camera fov 38? z 15 maybe vertical 10.3. Tank 61%. okay. Could adjust scene group scale on mobile to .8? Then tank width 9.6 still bigger than 5.5. Crop. Fish central.
Could perhaps use perspective camera and set view offset? no.
Responsive CSS title.

Need include import map exactly as specified. They said Load Three.js only through this import map. So use importmap. Do not import other libs. Fine.

Need code robust:
- import * as THREE from 'three';
- no unsupported methods.
- WebGLRenderer may fail in sandbox? Usually supported. Need first paint perhaps if fails. But expected.
- handle renderer setAnimationLoop.
- window resize.
- pointer event listeners on renderer.domElement.
- pointer coordinates from client.
- event passive.
- Keyboard event hide prompt.
- buttons maybe sound toggle? Avoid unnecessary.
- maybe UI toggle "CALM WATER" resets pointer. Button clickable. But pointer-events.
- no local storage.
- No alert etc.
- no external fonts.

Can include CSS gradient fallback and loading panel. The overlay "Click or press a key to start" visible. Should fish start moving behind. "self-playing demo mode" means no user needed. yes.

Need think whether "press a key to start" in message exact. Include.

Could animate fish even before interaction. Pointer force only after started? Since prompt hides on movement; if movement hides, then pointer is active and avoidance. Good.
But if pointer move to UI? renderer canvas underlying. canvas event pointermove only when on canvas; UI elements pointer-events maybe none, button pointer-events auto. Entire app. Better listen window pointermove. Cursor glow follows. On first move hide prompt and set started. But before first move, pointer initialized at center? We can set pointer active false, no avoidance.
Click event sets started and maybe creates ripple at click position. Could add ripple meshes in world? Nice: on click/tap create expanding ring at fish plane z=0.5. But user asks moving mouse. Could have click ripple. Yet click/press start, maybe pointer location. Use RingGeometry, MeshBasic transparent, animate scale/fade. This creates action.

Fish flee:
Projection each frame after matrices update. Ensure root world matrices updated? scene.updateMatrixWorld before projecting. We can call scene.updateMatrixWorld() each update before projection; renderer also later. getWorldPosition updates matrixWorld maybe. Vector3.project(camera) uses camera matrices updated. call camera.updateMatrixWorld().
Compute pointer px from event relative viewport. If pointer starts center, avoid.
For each fish:
fish world pos. project. Need account behind camera: z between -1 and1. Since all in scene.
distance.
Avoidance mapping. Let screenDelta = fishPx - pointerPx. visible world width at fish depth can be approximated from camera; we can simply world x = screenDelta.x * 0.012, world y = -screenDelta.y*0.012 (viewport 800 =&gt; .012 gives 160px -&gt;1.92). Normalize. strength 7. We want away. If fish very close pointer (distance &lt; 30), strong. boundary.
Could add erratic dart: desired speed increases 2x; smoothing faster.
Flee state decays.
Maybe fish body lean banking. Quaternion orientation. Could add root.rotation? Since quaternion handles orientation, tail etc. Banking by adjusting local z rotation perhaps but root quaternion to direction; could use group.rotateZ? Not after each. Could create inner visual group to bank while root orientation. Root contains visual; visual.rotation.z bank. Good.
Structure: fish.root Group position; fish.visual Group; visual contains body etc. root quaternion orient; visual.rotation.z = bank (rotation around local? visual local z).
Tail child.
Eyes.

Fish geometry:
Let's design functions:

const fishPalette etc.
function makeFish({species,color,accent,scale=1,speed,school,z,y}):
root = new THREE.Group(); visual = new THREE.Group();
root.add(visual); visual.scale.setScalar(scale)
body geometry SphereGeometry(1,32,20), material MeshStandardMaterial color roughness .35 metalness .08
body.scale based species:
 tang bodyScale (.62,.36,.26)
 tetra (.38,.16,.14)
 angel (.38,.52,.12)
 clown (.52,.32,.23)
Maybe use body mesh with castShadow.
For angelfish, body sphere scale .4,.5,.16.
Tail:
Shape with points 0? create shape:
moveTo(0,0); bezierCurveTo(-.2,.2,-.42,.28,-.55,.34); bezierCurveTo(-.48,.05,-.48,-.05,-.55,-.34); bezierCurveTo(-.25,-.25,-.1,-.12,0,0)
Need tail anchored at x=-bodyLength*.85. geometry in XY. root visual. tail.position.x = -length*.8. Material color accent or body.
For tetra tail accent red etc.
Fins: top fin shape from (-.1, bodyHeight*.8) to etc. Shape geometry; position maybe. Or BufferGeometry triangles. Use Shape.
Pectoral fins perhaps ConeGeometry flattened? Use ShapeGeometry triangle, positioned x=.05,y=-.02,z=side*.22, rotate x? Since plane XY, needs side perhaps mirror. can be same.
Eyes: at front x=bodyLength*.62, y=.06, z=side*bodyDepth*.9. white Sphere .07 and pupil .035 position x +.025, z side*.05. Eye black.
Stripes:
clown: 3 bands positions x=-.25,0,.25; TorusGeometry radius 1 tube .11 radial 8 tubular 32; default plane XY, rotateY PI/2, scale.set(bodyDepth? Let's derive). Torus major radius 1. after rotation Y, points in local XY map x to z, y stays y. Need ellipse cross section radius y .31 and z .24. Scale x? Mesh scale applied local before rotation: local x corresponds eventual z, so scale.x=bodyDepth/bodyLength? body dims absolute. body scale y=.32,z=.24. ring major radius 1 then scale local x=.24, local y=.32, local z? tube thickness local z becomes after rotation world x thickness; scale.z maybe .1? However mesh scale x=.24,y=.32,z=.7. position x stripe. It would be thin along x if z .7. This makes belt.
But Torus tube radius .08 scaled causes different cross. Could use CylinderGeometry ring? Torus okay.
Yellow tang: add blue-ish spots maybe dark ellipse patches on body side. Use CircleGeometry scaled .12 etc at z side*.235 and positions. Blue tang body blue accent yellow tail.
Neon tetra: glowing lateral stripe using thin Capsule? Use BoxGeometry or sphere scaled .28,.025,.01 positioned side. create a Mesh sphere scale etc at z side*.13.
Angelfish stripes vertical: Torus same.
Glow for neon: MeshStandard emissive cyan intensity .5.

Material clones per fish? Fine 14* ~10 meshes could be 140 draw calls, okay. Can share geometries but shapes vary. We can reuse sphere geometry, eye geometries, materials by palette. Need code simpler and performance okay. 15 fish, each 10 =150 draw calls plus environment ~100 =250, fine desktop. Shadows maybe heavy. renderer pixel ratio min 1.7. Shadow map 1024.
Could use InstancedMesh for bubbles/pebbles. Bubbles ~28 transparent spheres each draw call; could use Points with custom circles. Better use InstancedMesh with MeshBasicMaterial transparent and animate matrix. One draw. But per-bubble opacity impossible unless color alpha not per instance. Can use Points ShaderMaterial with per size; nice. Create BufferGeometry positions and PointsMaterial size .08 transparent opacity .55; update y. But points circular squares? PointsMaterial gives round? default map absent squares perhaps. Custom shader with gl_PointCoord circle. We'll create ShaderMaterial:
vertex attribute float aSize; varying; gl_PointSize = aSize * (180.0 / -mvPosition.z); fragment discard outside circle, alpha smooth.
Need bubble rings? enough.
Caustic motes maybe Points too.

Environment:
Back wall plane at z=-2.4, position y=.3, geometry 12x7. Maybe tank floor extends z from -2.3 to2.3. camera sees.

Tank water surface y=3.3.
Fish bound y [-1.8,2.6], x [-4.8,4.8], z [-1.5,1.5].

Coral and rocks:
floor y=-2.55 (water bottom). Use PlaneGeometry(11.6,4.6,60,30), rotate -PI/2, manipulate z before rotate? Geometry's local coordinates x,y, after rotate, local z is world y. We can modify position.z? PlaneGeometry vertices have x,y,z=0; setZ(height). Then rotateX(-PI/2): local z becomes world y? Rotation x -90 maps local y to world z=-local y, local z to world y=local z yes. Good. heights subtle .05 sin.
Sand material vertexColors maybe assign colors based height. Could use color attr. MeshStandardMaterial vertexColors true, color white.
Place rocks y floor + radii.

Add pebbles instanced:
IcosahedronGeometry(.12,0), material; 90 random positions, random scales, rotation. InstancedMesh with colors maybe setColorAt. Matrix. Shadows maybe receive.
Rocks 8 dodecahedron scaled.
Coral branches:
function cylinderBetween(a,b,r1? CylinderGeometry radiusTop radiusBottom height, 10). Geometry dynamic segment many. Maybe each coral creates 20 mesh calls. 3 corals. Fine.
Can use TubeGeometry for branches but same.
createCoral(base, color, scale):
root position; recursively branches 3 levels:
segment from p to p + direction*len with CylinderGeometry(.07,.11,len,8), orient.
At each level 2-3 branches. ~15 each.
Maybe stylized.
Seaweed:
createLeafMesh using shape and MeshStandard double side. stem tubes. group. animate rotations.
Could create 14 ribbon leaves: Shape from (0,0) to width, etc; geometry vertices, set local position; bend via rotation. Perhaps TubeGeometry with curve yields rounded green tubes; not leaf-like. Use ShapeGeometry elongated oval-ish:
shape.moveTo(0,0); bezierCurveTo(.18,.25,.12,.7,0,1); bezierCurveTo(-.15,.7,-.18,.25,0,0). geometry, rotate around? A leaf in XY, position base; rotation y random to create depth; material double side. Scale. Each plant group has leaves; animate group rotation z and x. Height 1-2.
Place along back and sides.

Background giant kelp silhouettes at back maybe.

Fish school targets. Need collision with decor? Not necessary but fish may swim through coral; constrain z center maybe. Put coral mostly back z=-1.7 and sides. Fish z [-1,1.2], so some overlap but okay.

UI species count maybe actual 14. Let's count:
3 yellow tangs (school 0)
4 blue tangs (school1)
3 clownfish (school2)
4 neon tetras (school3)
2 angelfish (school4) =16. Could show "16 residents". Tank width 11. Fine. Some small.
Maybe use 14: 3 yellow, 3 blue, 3 clown, 3 neon, 2 angel =14.
Initialize positions spread:
tang at -3,0 etc; blue right; clown center; tetras upper; angels side.
Each school has home center. Autonomous target around home ± 2.
Flocking uses neighbor within 2.3.

Need hover label for nearest fish? Could add DOM tooltip "YELLOW TANG • FLEEING" when pointer near. Nice. Update every 100ms, a floating label near cursor. This reinforces interaction. Could show species and "darting away". But UI pointer-follow ring and label may be too much. We can implement #fishTag absolutely fixed, initially opacity 0, with dot and text. On nearest fish within radius, show species and "noticed you". It follows pointer maybe at pointer+20. CSS. This is meaningful.

Could include sound? No audio to avoid autoplay. No.

Need ensure 3 sec screenshot: fish moving, prompt visible. Good.

Need visual layout not generic. Use modern deep teal, aqua, coral. CSS:
:root colors.
body margin0 overflow hidden background radial gradients; font-family system.
#scene position fixed inset0.
#vignette pointer-events none; background radial-gradient transparent center, rgba dark edges; z2.
grain overlay maybe CSS repeating? Can use pseudo with SVG? no external. Better subtle noise via CSS? Use multiple radial gradients. No need.
Header z5 pointer-events none.
brand row: logo icon circle with waves using CSS.
h1 letter spacing -.04em 38px.
top right glass status.
bottom panel maybe fixed bottom 22 left 50 transform. background rgba; border.
species legend perhaps right bottom vertical.
At mobile hide legend and header right; bottom panel full width.
Prompt center at z8: small capsule backdrop blur; pointer-events none. Initially. "CLICK OR PRESS A KEY TO START" exact. Since fish visible behind. Could maybe instruction text. Prompt should not dominate. #startPrompt opacity 1 transition.
Add corner brackets maybe.

Use SVG inline for icons? allowed. CSS enough.

HTML:
&lt;div id="app"&gt;
 &lt;div id="scene" aria-label...&gt;&lt;/div&gt;
 &lt;div class atmosphere...
 &lt;header&gt;...
 &lt;aside class=readout&gt; ...
 &lt;section class=legend&gt; ...
 &lt;div id=cursorAura&gt;&lt;/div&gt;
 &lt;div id=fishTag&gt;...
 &lt;div id=startPrompt&gt;...
 &lt;div class=bottomHint&gt;...
&lt;/div&gt;
Need buttons perhaps "CALM WATER" bottom right? Could include "Click / tap to send a ripple". UI pointer event.
Maybe a small "paused when hidden"? no.

At 375, title and bottom. The prompt center might overlay fish but semi-transparent and small.
Responsive:
@media max-width 640:
 header top 18 left 18; h1 28; subtitle hidden; status top 18 right 14 but maybe only temp.
 legend display none.
 bottomHint left 14 right14 bottom16, width auto.
 fishTag smaller.
 canvas perhaps no touch action.
 cursor aura hidden on touch.
@media max-height 600 etc.

Accessibility: aria labels.

JS details.

Let's formulate code.

HTML doctype. Import map in head exactly.
CSS all inline.
Body:
&lt;div id="scene"></div>
<div class="vignette"></div><div class="surface-glare"></div> maybe.
Header:
<div class=brand>
 <div class=brandMark><span></span> etc
 <div><p class=eyebrow>INTERACTIVE HABITAT · 01</p><h1>Tideline</h1></div>
</div>
<div class=statusCard><span class=live-dot></span><div><b>Water is calm</b><small>24.0°C · 16 residents</small></div></div>
Maybe status updates "3 fish darting" dynamically.
Legend:
p "RESIDENTS"; row etc. Use data.

bottomHint:
icon cross arrows, text "Sweep your cursor through the water" small "Nearby fish will dart away · click for a ripple"
Maybe top says exact prompt.

Start prompt:
<div class=promptDot></div><span>Click or press a key to start</span>
Under maybe "The aquarium is already alive". But user says exact maybe capitalization. Keep.
Maybe the pointer prompt hides on any pointer move though then you never see "click". Good.

Canvas container.

JS setup:
const container = document.querySelector('#scene');
const renderer = new THREE.WebGLRenderer({antialias:true,alpha:true,powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.75));
renderer.setSize(innerWidth,innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.setClearColor(0x021824,1);
container.appendChild(renderer.domElement);
canvas style.

scene background maybe Color 0x031b2a; fog = FogExp2(0x062a3e,.025).
Camera = PerspectiveCamera(43,aspect,.1,80);
set based.
Could background CSS gradient be hidden by scene background. Use scene.background = new Color 0x031721. Better create large background plane? The tank back wall fills. Outside area dark.
Lights:
HemisphereLight(0xaeefff,0x02080f,2.2)
DirectionalLight(0xd9ffff,3.2) pos(-4,8,6), castShadow, shadow camera left etc.
Directional light maybe r170 intensity.
PointLight(0x39d9ff,45,20,2) pos(0,2,4)
PointLight(0xff9a7a,20,15,2) pos(4,-1,2)
AmbientLight(0x0c2e40,.7)
Need no physically correct property removed? Three r170 uses physically correct by default maybe intensity values reasonable. Directional 3.

Tank:
const tank = new Group; scene.add.
floor geometry with colors:
const floorGeo = new THREE.PlaneGeometry(11.5,4.6,48,24);
positions. We need local z height:
for i:
 const x = pos.getX(i), y=pos.getY(i)
 const h=0.035*Math.sin(x*1.5)+... plus near edges maybe.
 pos.setZ(i,h)
 colors based h/random? deterministic pseudo random. use rng.
floorGeo.computeVertexNormals();
floorGeo.rotateX(-Math.PI/2);
But after rotate, local z height becomes world y, and local y becomes world z negative. Great.
floorMesh position y=-2.55.
Sand standard color 0xb9aa82 roughness .92, vertexColors true. If vertex colors multiply material; set colors around beige.
Need avoid color over-dark. material color white.

Back panel plane 11.5x6.6 at (0,.55,-2.35) (bottom -2.75 top3.85). ShaderMaterial:
vertex basic.
fragment:
float grad=smoothstep(0.,1.,vUv.y); vec3 c=mix(bottom,top,grad); float rays=pow(max(0.,sin(vUv.x*... + time*...)*sin(...)),?); c += caustic * .05; vignette edges.
Could just use ShaderMaterial no lights. Need fog? ShaderMaterial fog false. Fine.
Add side glass planes maybe BoxGeometry transparent. Maybe tank edge lines.
Water volume box:
new BoxGeometry(11.5,6.3,4.6), MeshPhysicalMaterial color 0x66e7ff transparent opacity .035 roughness .05 metalness0, side BackSide, depthWrite false. Since inside view, BackSide visible. Could overlay.
Front glass plane at z=2.3, geometry 11.5x6.3 MeshPhysicalMaterial opacity .055 color 0xb5f6ff transmission? Transmission .8 might need render target and maybe expensive. Use transparent Standard.
Edges: BoxGeometry edges at center y=.5, z0; LineBasicMaterial color 0x8defff opacity .28. But top/bottom. Add thick rim boxes:
bottom box 11.8,.16,4.9 at y=-2.7 dark teal with emissive slight.
top maybe thin line box 11.7,.06,4.8 at y=3.68, transparent blue. side vertical bars x±5.75 etc.
Back bottom line.
Could obscure. okay.

Water surface shader plane:
PlaneGeometry(11.4,4.5,48,24), rotateX(-PI/2), at y=3.42. Custom vertex modifies z? Plane after rotation? Better create before rotate then modify z; vertex shader gets position local and adds wave to z, after rotate local z -> y. But normal. Use ShaderMaterial with vertex:
vec3 p=position; p.z += sin(p.x*1.8+time*.7)*.035 + sin(p.y*2.3-time*.9)*.025; vUv; gl_Position...
fragment alpha .08 + shimmer. double side transparent depthWrite false.
Surface at y 3.35.

Caustics on floor: ShaderMaterial overlay PlaneGeometry(11.4,4.5) rotated; local position. frag:
float c = abs(sin((vUv.x*14 + sin(vUv.y*8+time))*2) + sin(...)); c=pow(...); gl_FragColor vec4(0.4,1,1,c*.12); additive blending, transparent.
At y=-2.49. But floor undulates; overlay flat maybe okay.

Back shader caustics.

Decor:
deterministic rng function mulberry32.
Add pebbles:
const pebbleGeo = new THREE.IcosahedronGeometry(1,0); InstancedMesh count 110, mat standard vertexColors? setColorAt. For each random x [-5.4,5.4], z[-2.2,2.1], y floor approx -2.48, scale .03-.12; random rotation. Use dummy.
Colors mix.
Instanced cast/receive.
Rocks:
function addRock(x,z,sx,sy,sz,color,rot)
DodecahedronGeometry(1,1), scale, pos y=-2.5+sy*.55, maybe sunk.
Place:
(-4.3,-1.2) big 1.3,.9,.9
(-3.9,.9) .7...
(3.8,-1.4)
(4.3,.8)
(-1.8,-2.0 small)
(1.5,-2)
central open.

Coral:
positions near back:
coral group at (-4.4,-2.45,-1.7) scale1.2 color coral pink.
(3.9,-2.45,-1.8) orange
(-2.5,-2.45,-2.05) violet
(2.7,-2.45,-2.05) yellow.
Create branching with cylinders. But if branches start from base local y0 and height. For each branch:
function branch(start, dir, len, radius, depth)
 end = start + dir*len
 mesh cylinder between
 if depth >0:
 child dirs rotated random.
Use deterministic random passed.
CylinderGeometry(radius*.65,radius,len,7), standard flat.
Need orientation:
mid; mesh.quaternion.setFromUnitVectors(new Vector3(0,1,0), dir.clone().normalize()).
Add small sphere at branch tips perhaps. Many draw calls.
Could create all branch geometry merged but no BufferGeometryUtils import. Keep.

Seaweed:
function addPlant(x,z,height,color):
group; 5 leaves. shape geometry maybe.
Create leaf geometry once per color? ShapeGeometry shape from 0 to 1, then mesh scale x width, y height, position at base; rotate y random. But vertical plane XY. Color double side. Leaf anchored bottom at shape origin. Use Bezier. set leaf.position base plus slight. It will stand upright, z orientations. Animate group.rotation.z = sin(t*...)*.04 and child leaf rotation.z.
Leaves cast shadow.
Add back plants x [-4.8..4.8] around z=-2.1; 9.

Maybe rocks in front.

Bubbles:
Create 34 points positions x random, y random, z random. Buffer attributes position, size. Shader. Need update positions each frame; maintain bubbleData array with base x,z,speed,phase,y. In animate, update attr array. Shader uses aSize maybe 6-15.
Point size formula in pixels: size world? aSize 8? gl_PointSize=aSize*(120/-mv.z), if aSize=.08? We'll set aSize .09? Better: aSize 7.0 * (50 / -mv.z) at z camera 11 ->31 px too big. Use aSize .12*100 / 11=1.1. Set .18 =>1.6. bubbles should 3-8 px. Set aSize .3 =>2.7. We'll set random .18-.45.
Fragment:
vec2 c=gl_PointCoord-.5; float d=length(c); alpha=smoothstep(.5,.3,d)*.4; ring = smoothstep(.38,.48,d)-smoothstep(.24,.38,d); color mix vec3 .7,1,1 ring. gl_FragColor alpha...
Transparent, depthWrite false, blending Additive maybe.
positions update y from -2.3 to3.2; reset.
Motes another Points maybe.

Fish materials:
Need ensure standard material colors bright under lights. emissive subtle.
const bodyMat = new MeshStandardMaterial({color, roughness:.28, metalness:.08, emissive: color.clone().multiplyScalar(.05)}) But emissive expects color, can use color and intensity .08.
For yellow #ffd23f etc.
accent.
Create species:
const speciesConfigs:
yellowTang {label:'Golden tang', school:'tang', bodyScale:[.58,.34,.24], tail:'#16395b', pattern:'blue' maybe body yellow with dark face spot}
blueTang {body #2c70ff, tail #ffd23f}
clown {body #ff7b32, tail #ff9b3d, white bands}
neon {body #75f6ef, tail #ff4e68, stripe}
angel {body #f3c667, pattern:'stripe'}
Could vary each fish material shades.

Patterns:
clown white bands: TorusGeometry(1,.11,8,24), material cream. mesh rotation.y=PI/2; scale.set(bodyDepth*.95, bodyHeight*.95, .65?) Wait Torus local dimensions after scaling: local XY; local x radial horizontal becomes world z after rotation. scale.x = depth/body major radius; local y=height. local z=tube depth becomes world x; scale.z maybe .7. Mesh position x. body length .52. body surface at x band; okay.
But body sphere radius 1 scaled. A torus of major radius1 around x; body cross section at x has radius less than full, so band floats/ intersects. At x=0 body radius 1 scaled; band radius1 exact. At x=.25 body radius sqrt(1-.25²/.52²)=.88, so band radius1 too large. We could scale band overall based x: radial scale .9. Fine.
Use geometry TorusGeometry(1,.1,8,32); ring.scale.set(depth*0.98,height*.98,.7). Since after rotation and scaling local z .7 affects thickness along x? scaling local z scales tube thickness not centerline; axis tube in local z transforms world x, so band width .1*.7? okay.
Set ring rotation.y = PI/2.
Pattern positions.
For angel vertical stripes same.
Yellow tang spots: CircleGeometry maybe MeshBasic dark navy at z±.245, scale .11,.16; positions. Since side only. Add both side.
Blue tang maybe yellow tail and a dark shape.

Tail shape geometry: Need orient maybe ShapeGeometry has UV; fine.
Add tail at -length*.78. For angelfish tail larger. Could scale tail by species.
Dorsal fin shape:
shape moveTo(-.22,.14) etc. Since body y height. geometry at body.
Maybe create fin shape coordinates relative body and position.
For tang: points (-.25,.17),(.05,.44),(.28,.18)
But fin should above body at y=.22. Build.
Pectoral fin shape triangle and position.
Eyes:
body front x=length*.72? Sphere body surface at x .4 for tang; eye position x=.37, z=.21. White radius .07.
Pupil position x=.405,z=side*.265.
Need orient eyes toward side. Spheres no issue.
Mouth perhaps tiny line/torus arc not needed.

Fish root metadata.
Initial root.position.
Initial velocity from angle.
School home.
Species fish count.
Make functions returning references.

Schools:
const schoolHomes:
tang new Vector3(-2,0,0.3)
blue (2.1,.2,0)
clown (0,-.6,.5)
neon (-.4,1.3,-.3)
angel (0,1.1,-1)
Need each fish target own random.

Add fishes:
for count config positions random around home x +/-2, y +/-1.1, z +/-1.
Need make them visible in 3 sec.
Fish species array definitions with scale:
yellow tang count3 scale .9
blue 3 scale1
clown 3 scale .82
neon 3 scale .72
angel2 scale .95
14.
Each body ~1.1 long, tank 11.5, good.

Fish update details:
Use shared vectors to avoid allocation maybe but 14 no issue. But code clarity.

state:
{
 root, visual, body, tail, pectorals,
 velocity: new Vector3(rand-...,rand,...),
 target: new Vector3(...),
 phase, speed, minSpeed, maxSpeed, flee:0, school, species, home, nextDecision:0, bank
}
Need orient plusX:
const desiredQuat = new Quaternion().setFromUnitVectors(axisX, velocity.clone().normalize());
root.quaternion.slerp(desiredQuat, 1 - Math.exp(-dt*4.5));
But this Quaternion aligns +X to vector, keeps roll minimal. Fine.
Visual rotation.z bank: visual.rotation.z = THREE.MathUtils.lerp(...). Need bank sign from turn? Compute cross between old velocity and new? Could derive velocity.y change. Simpler bank = clamp(velocity.y*.12,-.25,.25).
Visual.position.y = sin(time*2+phase)*.025.
tail.rotation.y = sin(time*9+phase)*(.32 + flee*.4); because plane XY rotates around y making tail depth, okay. Maybe tail.rotation.z wiggle also.
Fins rotate x maybe.
Pectoral orientation plane XY and to flap around x? Rotating x moves y into z, yes.
Body slight scale pulse.
School forces:
for each fish, iterate j:
if same school and d&lt;2.2:
 alignment.add(other.velocity) etc
 cohesion.add(other.pos)
 separation if d&lt;.75 add diff*(.75-d)
Need careful modifications:
desired = target - pos normalize * speed.
alignment = avg vel normalized * speed * .3
cohesion direction to avg pos * speed*.12
separation vector magnitude maybe speed*.6
Add.
Pointer:
projected distance. Need maybe pointer force based screen radius responsive 145. If pointer over canvas.
if dist &lt; radius:
 awayScreen = (fishPos - pointerPx) normalized; if zero use random
 world direction = new Vector3(awayScreen.x * (camera visibleWidth?), awayScreen.y negative)
 Instead mapping and normalize.
 strength = (1-dist/radius)^2
 desired.addScaledVector(awayWorld, speed*4.2*strength)
 state.flee = max...
Also push z forward (toward camera) `desired.z += 1.5*strength` but camera facing direction z positive. Fish position z maybe; add.
Desired speed increased by 1.8.
Maybe cursor not directly near fish but line between? "near them" radius.

Boundary:
if x >4.9 add negative etc. y >2.8 down, y<-2.1 up, z >1.6 back, z<-1.65 front.
Target update timer.

Autonomous:
state.target updates at random intervals 1.5-4.5, target = home + random.
At start targets.
Use easing velocity:
const desiredSpeed = config speed; velocity.lerp(desired.normalize()*desiredSpeed, responsiveness dt). If pointer strong responsiveness 10.
But alignment etc summing desired. Let's structure:
steer = Vector3:
toTarget .8
alignment .25
cohesion .18
separation .7
boundary 2
pointer 5
Then if length >0 normalize * target speed.
velocity.lerp(desiredVel, clamp(dt*2.5,0,1)); plus slight sinusoidal.
state.position.addScaledVector(v,dt).
Clamp hard and bounce target.

For pointer responsiveness, velocity lerp factor 1-exp(-dt*(flee?8:2.2)).
Speed clamp maybe max base*2.
Use root.position.addScaledVector(v,dt).
if out bounds clamp and invert.

Need tag nearest:
After update projections, determine nearest under radius. If started. tag text. species color via CSS var perhaps style border. Show if dist < radius*0.8? Could show "GOLDEN TANG · keeping distance". If pointer near, fish flees.
DOM update at 10fps maybe every frame okay.
cursor aura position and scale based nearest.
CSS `#cursorAura` width 170 height170 border etc transform translate(-50,-50) scale; pointer-events none; opacity 0. On mousemove update left/top. Could set opacity .65. But ring should indicate radius 150px. width 220 maybe.
Use aura background radial-gradient(circle, rgba(112,255,235,.08), transparent 60%), border 1px dashed rgba.
At mobile display none.

Start prompt:
let started=false;
function begin() { if(started)return; started=true; prompt.classList.add('hidden'); document.body.classList.add('started');}
window pointermove e -> pointer coordinates; if !started begin(); pointer.active true.
window pointerdown -> begin, ripple at screen coords; pointer active.
window keydown -> begin.
But if first pointermove hides exact prompt before user can click; okay. It says click or press key to start but move also starts. The requirement only says behind message, not strict.
Maybe keep prompt until click/key? But then user can't see while moving; fish could still avoid as pointer moves even prompt visible. The prompt remains, satisfying. After click/key hides. Yet task specifically mouse move should cause fish flee; yes even without click. Let pointermove active and fish flee while prompt remains. Prompt pointer-events none. On click/key, hide. This is better. The message remains first seconds though screenshot sees. But phrase "start" perhaps. Game auto behind. Do that: pointer move updates active but does not hide. `begin()` only click/key. It could be frustrating to have overlay, but small.
Could hide prompt on pointerdown. The user can test fish immediately even prompt visible.
"Click or press a key to start" exact. It can also have "or simply move your cursor — fish already react." But message exact.
Could on pointer move after 1.2s fade prompt automatically? Hmm. Requirement says start behind message. It should disappear upon interaction. Movement is interaction. So hide on first meaningful movement maybe best. We'll use first pointermove to hide too, plus click/key. The prompt is visible at initial screenshot, then moving user gets clean aquarium. Good.

Need `pointer.active` maybe set to true at pointermove. Initial false.
Ripple:
function createRipple(clientX,clientY):
Need map screen to world at z=0.5. Use Raycaster setFromCamera NDC, ray intersect plane z=0.5. In Three Plane(new Vector3(0,0,1), -0.5) equation z-0.5=0. ray.intersectPlane. RingGeometry(.12,.16,48), MeshBasicMaterial color cyan transparent opacity .7 side DoubleSide depthWrite false; position world x,y,z=0.4; default Ring lies XY facing camera, good. Add to scene. anim scale from1 to4, opacity. Since screen click maybe maps.
Store ripples array with life. update.

We should not use pointer events on UI. bottom hint pointer-events none. header none. Maybe no button. Fine.
Canvas cursor none? Could hide default and use aura. On mobile none. But user may not know. use body cursor crosshair? CSS canvas cursor: none and aura. Better use default `cursor: crosshair`; aura plus default can look weird. set cursor:none desktop, pointer. But accessibility maybe. Let's set canvas cursor: crosshair, aura subtle. no custom hide.

Status dynamic:
top status small label "CALM WATER" then when flee count >0 "CURRENT DISTURBED". Every 0.2 sec update. Resident count 14.
DOM `statusText`, `residentCount`.
Legend labels actual.

Need maybe "water is calm" but fish flee. dynamic.

Need make camera subtle parallax based pointer for depth, but if camera moves, projection distances change. We can add slight camera x/y offset based pointer, smooth; nice. But pointer movement causes camera shift, perhaps fine. Could create cameraRig? Camera position x=pointer.x*.25, y=.15+pointer.y*.12; lookAt center. This may make fish less precisely screen-aligned but still. On mobile no.
Camera update each frame; projection should use updated camera. We need update camera before fish avoidance. In animate:
time.
updateCamera(dt) based pointer.
camera.lookAt(0,0.1,0);
camera.updateMatrixWorld();
then fish update/project.
Renderer.
At desktop initial camera (0,.2,10.8). On mobile adjust z14.2 and fov42 perhaps.
Responsive function:
const mobile = innerWidth&lt;700;
camera.fov = mobile ? 46 : 42;
const distance = mobile ? 13.4 : 10.8;
At mobile aspect .469, visible width=2*tan23*13.4*.469=5.45. central.
At desktop aspect1.6, width=2*tan21*10.8*1.6=13.3. tank width11.5 fits. vertical=2*tan21*10.8=8.3. tank6.3 =76%. Good.
Camera z 11.2 maybe.
Desktop y=.25, target y=.15.
On mobile y=.1 and z14? vertical 11.9? Wait vertical fov46 gives 2*tan23*13.4=11.37, tank 6.3 only 55%. Tiny. Could camera fov38 on mobile: vertical=9.2, tank 68%, width=4.1. Crop more but okay. Use fov39, distance13.8 gives vertical9.75, width4.58. central tank crop but fish visible. Better.
At width375 x800, central visible width 4.6, only center region. Some side fish offscreen. Could dynamically constrain fish x to [-2,2] on mobile? But environment corals cropped too, tank central still. We can set camera distance 16 maybe visible width5.4 and vertical10.9, aquarium 58%. Still okay on tall phone; UI top/bottom. Fish visible.
Maybe instead adjust camera FOV and tank scaling group? We can scale tank down on mobile to 0.65, but tank x width7.5 still &gt;4.6. scale .45 width5.2; fish size small. Yet mobile screenshot central tank fitting entire might be expected. Could set camera distance20, fov39 visible width6.6, tank11.5 still cropped. scale tank .55 =&gt;6.3 fits. Fish scale .55 too small. Could have separate UI? User wants adapt gracefully, cropping aquarium can still be graceful, but full tank desirable. We can set camera z 21, tank width 11.5, horizontal visible at aspect .469 fov39 =6.9, crop 40%. That's okay. Vertical visible=14.9, tank 42% height too tiny. Could use CSS canvas view? impossible with perspective.
Alternative use orthographic camera on mobile with width11.5, height=11.5/aspect=24.5, tank tiny vertically. Same.
Could reorient tank diagonally on mobile? no.
Could rearrange fish into central. Since environment side crops is common immersive mobile design. Keep z13.8, central detail fills. graceful.

Need perhaps adjust `tank.scale`? no.

Scene background outside tank dark. On mobile sides not visible because camera view central tank.

Animation time and dt clamp .05.

Potential issue transparency sorting for glass and water. Set depthWrite false.

Add fish shadows. Directional shadow map. Transparent water doesn't.

Caustic shader code:
Back material:
const backUniforms={uTime:{value:0}};
vertex:
varying vec2 vUv; void main(){vUv=uv; gl_Position=...}
fragment:
float waves = sin((vUv.x*18.+sin(vUv.y*11.+uTime*.45))*1.4+uTime*.2);
float c=pow(max(0., waves*.5+.5), 7.);
float edge=smoothstep(0.,.15,vUv.x)*...
vec3 deep=vec3(0.015,0.12,0.18), up=vec3(.03,.32,.40);
vec3 col=mix(deep,up,vUv.y);
col += vec3(.12,.55,.58)*c*.16;
gl_FragColor=vec4(col,1);
Maybe too bright. tone mapping shader colors not automatically? ShaderMaterial toneMapped true by default? yes maybe. Fine.
Add back panel vertical gradients from top bright.

Floor caustic shader fragment:
vec2 p=vUv*vec2(16,8); float a=sin(p.x+sin(p.y+uTime*.6)); float b=sin(p.y*1.3+sin(p.x*.8-uTime*.45)); float c=pow(max(0.,1.-abs(a+b)*.55),6.); gl_FragColor=vec4(.3,1.,.9,c*.18); Additive.
Actually `1-abs` max maybe.

Surface shader:
vertex waves local z. frag alpha.
Need plane local dimensions after rotation. Create PlaneGeometry width depth, rotate? If shader vertex uses position.z and rotation is on mesh -PI/2, local z maps y. Put plane rotateX(-pi/2). The wave local z becomes world y. good.
position.y=3.45.
fragment ripple with vUv.

Sand floor geometry color:
Loop positions and set color `new Color().setHSL(.11, .25, .55 + h...)` maybe. Standard material color 0xffffff.
Need use deterministic RNG across. Good.

Seaweed leaf ShapeGeometry local y, but group base at floor and leaves start y0. place each leaf x random .1, z etc.
Animate arrays plants with baseRot, phase.

Coral branch code maybe use `THREE.CylinderGeometry` per segment. orient.

Create rocks and coral.

Potential z-fighting floor caustic y = -2.48 while floor around -2.55+h. Some heights up .08, overlay at -2.48 above all. okay.

Bubbles shader gl_PointSize scaling with DPR? Uniform pixel ratio maybe. Set renderer pixel ratio; gl points scale by physical buffer? gl_PointSize in physical pixels, okay. `aSize * (80.0 / -mv.z)` with aSize .35 gives2.3 physical px. need bigger maybe .55 =>4.0. Great.

Add tiny particles with PointsMaterial size .025 maybe.

Fish fins double side and shadows.

Maybe add bioluminescent jellyfish? Interesting but complexity. Could add 2 small jellyfish in background: translucent bell sphere, tentacles lines. But aquarium focus fish. Might add visual interest top corners. Could create jellyfish group with hemisphere geometry (SphereGeometry .3 24 12, phi 0..2pi theta0..pi/2) and tentacles curves. Animate bell. However user asked fish; fish enough. But polished aquarium could include jellyfish. Yet 14 fish already.

Could add a couple jellyfish in back upper area. But screenshots may make it rich. Need not.

Maybe central "sunken arch" with fish swimming. Let's create torus arch at back? Rock arch helps scene. TorusGeometry(1.35,.42,8,48,Math.PI), rotate z? Default torus arc starts at angle 0 right and goes counterclockwise to pi left through y positive, making upside U. Position y=-2.0 (bottom endpoints around -2, top -.65), too low. Scale y 1.5 => top .0, fish can swim through opening from -2 to0. We want larger arch around center, radius1.4, scale y1.5, pos y=-2.3, top -0.2. Not tall. Scale y2 => top .5. Place z=-1.8, color stone. But top part overlaps fish central. Fine. However Torus tube creates rounded arch. Position at (0,-2.45,-1.75), radius1.4, tube .35, scale y1.4 => top -0.49? Formula torus center y=-2.45, arc y0..1.4*1.4=1.96 => top-.49. Need center -1.9 -> top.06. endpoints y -1.9 not floor -2.5; add legs rocks. Could position -2.1, top -.14. Fine.
Could set radius1.8 scale y1.4 top .42, endpoints -2.1. Big. x width3.6, central. Fish swim in front z > -1.4. Nice.
Torus is actual tube and cast shadow. Add rock bases at endpoints.

But tank might look like a decorative arch, nice.

Water caustics.

Need maybe use `renderer.compile`? no.

Need accessibility no user requested controls. Fine.

Let's write final code exactly one fence.

Before final, let's deeply ensure JS no errors:
- Import map must be before module.
- `THREE.MathUtils.clamp` works.
- `THREE.Color` etc.
- `Shape.bezierCurveTo`.
- `InstancedMesh.setColorAt` requires material vertexColors? For InstancedMesh, instanceColor automatically used by standard material if setColorAt? In recent Three yes material vertexColors need not? I think setColorAt creates instanceColor and shader uses it regardless of vertexColors maybe defines USE_INSTANCING_COLOR. Standard supports. Fine.
- `DirectionalLight.shadow.camera` properties.
- `renderer.setAnimationLoop(animate)`.
- CSS backdrop-filter okay.
- `window.matchMedia('(hover: none)')` no issue.
- `e.pointerType`.
- `ResizeObserver` maybe not needed.
- `document.visibilityState`.
- `THREE.Plane` etc.
- `new THREE.Raycaster`.
- `ripple.material.opacity`.
- Remove material/geometry when done to prevent memory but few. `ripple.mesh.material.dispose(); ripple.mesh.geometry.dispose();`
- group look.
- fish root orientation quaternion; `root.quaternion.slerp(q, alpha)`. q setFromUnitVectors +X to normalized vel.
- state target.
- Neighbor calculations use position clone many but okay.

Need ensure `fish.visual.rotation.z` and root quaternion don't conflict weird.
- eye z orientation.

Flocking algorithm:
function updateFish(dt,time) {
 const projected = [];
 // camera update first.
 for fish:
  const pos = fish.root.position; world = fish.root.getWorldPosition(temp)
projected.
 for fish:
  let steer= new Vector3();
  to target ...
  neighbor...
  pointer...
  boundary...
  if steer.lengthSq>0:
    let desiredSpeed=f.speed*(1+f.flee*1.2);
    desired = steer.normalize()*desiredSpeed;
  else desired=velocity clone speed.
  alpha=1-exp(-dt*(f.flee>.1?8:2.4));
  velocity.lerp(desired,alpha);
  speed clamp .35 to max.
  position add...
  enforce bounds...
  orientation q.
  f.flee*=exp(-dt*1.5)
 ...
}
Potential issue pointer force units vs steering:
toTarget .9, align .25, cohesion .18, sep .4, pointer 5*strength. pointer dominates.
World away direction mapping:
const dx = (p.x-pointer.x)/radius; dy=(p.y-pointer.y)/radius; away = new Vector3(dx, -dy, 0).normalize();
This directly screen axes; world x/y ratio differs due aspect. x force should maybe divide aspect. But visual direction in screen needs compensate: world x per pixel less than world y because wide screen; if dx world 1 and dy world1, screen movement worldx maps more pixels. To move away in screen, need world x = dx * visibleWidth/height relation. At 1280x800, world width13/height8, pixels per world x97, y100 similar, so okay. Mobile also aspect differs: visibleWidth4.6/375=.0123, visibleHeight9.7/800=.0121, similar! because camera visible height bigger. Actually visible width pixel per world=81, y=82. similar due aspect; fine.
distance uses pixel.
if pointer over fish, dx=0 dy=0 choose away from nearest neighbor or random.

Boundary y: floor fish center -2.1, top2.75. x±5.1, z [-1.45,1.6].
Fish could swim through back arch but okay.

Tag nearest:
we can store screen positions each fish. At end, find minDist. `if (started && nearestDist<pointerRadius*0.9)` show. Since start on first movement, yes.
`tag.style.setProperty('--fish-color',...)`.
Position fixed left pointer.x+18 top pointer.y-14. But if near right edge, clamp. Use transform? CSS left top. style.
Status
update.

Pointer radius responsive: innerWidth&lt;700 ? 110 : 150.
Cursor aura size radius*2? CSS fixed 260 width; can transform scale based. Set width/height via JS `style.width = `${radius*2}px`;` but CSS. Use transform translate(-50%,-50%) scale(.9). left top.
No need.

Prompt initial visible. On first pointermove:
if (!started) {started=true; prompt.classList.add('is-hidden');}
pointer.active=true.
But user might just move mouse from somewhere into iframe; pointermove event fires and hides immediately, good. Screenshot at 3 seconds no pointer maybe prompt visible. If automated screenshot pointer static maybe no movement. Fine.

Maybe keydown starts but pointer.active remains false; after key, no avoidance until move. okay.
Click creates ripple and sets pointer active/position.

Camera parallax:
let cameraBaseZ.
function onResize:
camera.aspect; camera.fov = width&lt;650?40:42;
cameraBaseZ = width&lt;650 ? 14.8 : 10.9;
camera.updateProjectionMatrix;
Maybe at 375, vertical visible=2*tan20*14.8=10.77, tank58%; central visible width5.05. It's okay.
At desktop vertical8.37 tank75%.
Set camera.position z base.
In animate:
if pointer.active and pointerType !==touch:
 camera.position.x += ((pointer.ndc.x*.22)-camera.position.x)*...
 camera.position.y += ((.15+pointer.ndc.y*.12)-...)*...
else x lerp0.
camera.position.z cameraBaseZ.
camera.lookAt(0,.05,0)
Potential camera x shift changes projection each frame but stable.
Need NDC pointer updated:
pointer.ndc.x = e.clientX/innerWidth*2-1; ndc.y=-(e.clientY/innerHeight*2-1).
Touch pointermove could work but pointermove on touch only while touch. Set.

Lighting maybe tank dark. We need ensure screenshot bright. Use scene background #031a29, back shader brighter. Renderer exposure1.2. Floor visible. Coral bright.

Decor material.
Rock color.
Add subtle SpotLight? directional enough. Point light.

Tank frame. For vertical side bars x ±5.75, y .45, z0 maybe dark transparent. They may be offscreen mobile.
Top rim at y3.55.
Water surface.

Add background bubbles maybe.

Jellyfish: Let's perhaps implement 2 to add depth. Create group with bell Mesh SphereGeometry(.35,24,12,0,2pi,0,pi/2) (hemisphere dome). Material MeshPhysical transparent .38 emissive blue. Tentacles using CatmullRomCurve3 + TubeGeometry .015. Group at (-3.7,1.8,-1.4) and (3.2,2.2,-1.5). But fish count and visuals. Jellyfish bell moves up/down, tentacles oscillate. Could be nice but aquarium requested fish. Yet more meaningful. We can include. Need code moderate.
Create jelly:
const bellMat shared? each color.
sphere hemisphere upper half has open bottom. Torus ring bottom.
tentacles curves created each frame? static curves and group rotation. Add 5 Line curves using BufferGeometry line not tube, transparent. Use LineBasicMaterial.
animate group y = baseY + sin(t)*.15; rotation.z...
At mobile they may be off center. okay.
Maybe avoid complexity.

Aquarium water glass needs front plane can tint entire scene and UI unaffected. opacity .025.

Add caustic overlay.

CSS details:
body background #03131f; color.
canvas display block.
header pointer-events none.
brandMark 42x42 border radius 14 background linear gradient; pseudo bubble.
title `font-family: Georgia?` Use system to avoid external. A stylish serif title maybe `font-family: "Iowan Old Style", "Palatino Linotype", Georgia, serif;` allowed system font no external. Use bold? Could combine.
"THALASSA" maybe. User expects aquarium. Name "Blue Hour Aquarium". Let's call "THE BLUE HOUR" or "TIDELINE". The title should be clear: "Blue Room" with subtitle "A living 3D aquarium". Perhaps "TIDEPOOL" but actual ocean fish. "TIDELINE / LIVING AQUARIUM". Good.
Use h1 42 with serif italic? `font-family: Georgia; font-weight:400; letter-spacing:-.04em`.
Eyebrow aqua.

Status card top right. Legend right middle/bottom.
Bottom hint glass card with animated cursor icon.
Prompt center.

Maybe show frame counter "HABITAT 01" etc.

CSS no horizontal scroll.

Need code block only. Before final, let's consider any constraints: "Everything must be inline CSS in &lt;style&gt;, JavaScript in &lt;script&gt;. No external files, images, fonts or network requests, except CDN URLs explicitly allowed." We use import map CDN. No external images. Good. "Load Three.js only through this import map" yes.
No localStorage etc. no alert.
"Everything interesting visible first seconds" yes.
"Games start self-playing behind prompt" fish animate.
"if demo uses keyboard display clear prompt" we do.

Need maybe no `console.log`; zero errors.

Could include fallback if WebGL unavailable? If renderer creation throws, then page blank but console error. Sandbox supports WebGL. Could wrap try/catch but then need no errors. `try { renderer=... } catch { fallback... }`. But if catch, no animation. We can create CSS background and message. Yet not needed. But robust:
let renderer;
try { renderer = new THREE.WebGLRenderer(...);} catch(error) { document.getElementById('startPrompt').innerHTML = ...; throw? no. }
But then code references. We can not throw. However no need.
No console errors from unsupported `powerPreference`. fine.

Use Canvas 2D? no.

Let's flesh code.

HTML header:
```
<header class="topbar">
 <div class=brand>
  <div class=brand-mark aria-hidden=true><span class=bubble b1>...
  <div><div class=eyebrow><span>LIVE HABITAT</span> / 01</div><h1>Tideline</h1><p>Living aquarium</p></div>
 </div>
 <div class=status-panel>
  <div class=live-dot></div>
  <div><span id=statusText>Calm water</span><small>24.0°C · <b id=residentCount>14</b> residents</small></div>
 </div>
</header>
```
Legend at right:
```
<aside class=legend>
 <p>INHABITANTS</p>
 row...
</aside>
```
Bottom card:
```
<div class=interaction-card>
 <div class=gesture-icon><span></span>...
 <div><strong>Move closer. Watch them retreat.</strong><small>Fish react to your cursor · click or tap to send a ripple</small></div>
</div>
```
Prompt:
```
<div id=startPrompt><span class=prompt-icon>...</span><span>Click or press a key to start</span></div>
```
Could initial prompt center conflict with screenshot but okay.

CSS:
```
#scene {position:fixed; inset:0; background: radial-gradient(...);}
#scene canvas{width:100%;height:100%;display:block}
.vignette {position:fixed; inset:0; pointer-events:none; z-index:2; background: radial-gradient(ellipse at 50% 48%, transparent 38%, rgba(...).35 85%,...); }
.grain { ... background-image:url? no; use repeating-linear-gradient}
```
No data URI images? Could use CSS gradients. Fine.
`mix-blend-mode: screen`.

Responsive legend hidden max900. bottom card max620.

JS environment details:
Back wall might cover frame and floor. position z=-2.32. Camera sees.
Directional shadows with floor receives. back plane receives? Shader no.
Transparent water volume BoxGeometry with BackSide. It may render after opaque due transparent and depthWrite false; okay.
Water surface.

Sand geometry color:
```
const pos=floorGeo.attributes.position;
const colors=[];
for (...) {
 const h =...
 pos.setZ(i,h)
 const c=new Color().setHSL(.105,.28,.53+...);
 colors.push(c.r,c.g,c.b)
}
floorGeo.setAttribute('color',new Float32BufferAttribute(colors,3));
```
When plane rotated after setting z, then compute normals after rotation? Must rotate then compute normals. Set colors unaffected.
`floorGeo.rotateX(-Math.PI/2); floorGeo.computeVertexNormals();`
The original PlaneGeometry has indexed vertices; setZ works.
position y=-2.52.

Back panel plane geometry 11.5,6.5 at y=.55 bottom -2.7 top3.8.
water volume center y=.5 height6.3.

Box frame geometry.

Coral branch orientation and material:
`const coralMat = new MeshStandardMaterial({color, roughness:.62, metalness:0, flatShading:true});`
Each branch shares.
Branch recursion:
```
function growBranch(start, direction, length, radius, depth) {
 const end = start.clone().addScaledVector(direction,length);
 const mesh=...
 mesh.position.copy(start).lerp(end,.5)
 mesh.quaternion.setFromUnitVectors(Y, direction)
 root.add(mesh)
 if depth===0 { tip sphere...}
 else {
  const count=depth>1?3:2;
  for... {
   let nd=direction.clone();
   nd.x += rand...
   nd.z...
   nd.y=Math.abs(nd.y)*.7+.35; normalize
   growBranch(end,nd,length*.68,radius*.65,depth-1)
  }
 }
}
```
Need start direction points upward.
Plant root position.
Create 4 coral groups.
Could branch through floor? no.

Rocks:
`DodecahedronGeometry(1,1)` flat. mesh.scale; position y=-2.48+sy*.35 maybe sink half.
Colors.

Torus arch:
TorusGeometry(1.65,.36,10,64,Math.PI). Default arc from x positive to negative with y positive. Position y=-2.18, z=-1.86. scale.set(1,1.35,1), so top y .05. endpoints at -2.18. Great. x radius1.65. Tube .36. Add. Maybe top behind fish.
Set rotation z? none. Material gray teal.
At endpoints, rocks.

Seaweed positions:
plants array each group. leaf shape:
```
shape.moveTo(0,0); bezierCurveTo(.16,.18,.20,.62,0,1); bezierCurveTo(-.18,.66,-.14,.22,0,0)
```
Geom ShapeGeometry; mesh.position.set(offset,0,offsetz), rotation.y=angle, scale.set(width,height,1); but geometry y dimension 1. scale y height. Material side double. To bend, mesh.rotation.z random.
Plant group position floor. animate.
Place mostly back and corners.
Because ShapeGeometry plane lies XY, rotate y changes orientation.
Color teal/green.

Back kelp silhouettes maybe same.

Fish materials unique:
Need `makeFish` body color material. Use new each fish to allow emissive.
Tail shape coordinates perhaps use `tailScale`:
```
const tailShape = new Shape();
tailShape.moveTo(0,0);
tailShape.bezierCurveTo(-.12,.18,-.38,.24,-.55,.34);
tailShape.bezierCurveTo(-.48,.1,-.48,-.1,-.55,-.34);
tailShape.bezierCurveTo(-.33,-.25,-.12,-.12,0,0);
const tailGeo = new ShapeGeometry(tailShape);
tail.position.x=-len*.78;
tail.scale.set(species.tailScale??1,...)
```
Tail starts at 0 then negative x. Good.
For angelfish, tail scale maybe1.15 and body.
Add caudal peduncle? body overlaps at tail.
Dorsal fin shape:
```
moveTo(-.2,.1); quadratic...
```
position y=bodyY*.7 maybe.
Pectoral:
triangle from 0 to -.25,.12 etc, position x=.12,z=side*depth*.8, maybe rotate x side? Because ShapeGeometry plane XY; to appear side visible from camera along z, actually front camera sees XY plane, so pectoral fins in XY visible. We want fins extend downward and backward. Put on visible side z=±depth*.78, but XY positions. Rotation z? Fine.
At z side, material double side.
Eye positions.

Patterns Torus after rotation might use material double. Fine.
Need set `ring.scale.set(depth, height, .6);` Here Torus major radius1; after scale local x=depth, y=height; local z scales tube thickness. tube radius .1; world x width after rotation is local z*.6=.06, good. ring cross-section z depth scale? local x .24, yes. Good.
Position stripes x:
clown [-.27,0,.27] relative body length. body length .52.
angel [-.12,.16]? Body length .38.
But torus orientation normal after rotation along x, correct.

Neon stripe:
mesh SphereGeometry maybe scale(.26,.025,.015), pos x=0,y=.01,z=side*depth*.95. Because sphere elongated x. emissive mat.
Also red eye? no.

Tang spot:
CircleGeometry radius1 20, scale .09,.14, position x=.18,y=-.03,z side*depth*.96; plane XY. Add.
Fish shadow cast from fins? okay.

Root visual body etc.

Fish speed:
yellow .9, blue1.0, clown .8, neon1.25, angel .72.
Base scale values.

Need initial velocity orientation. `new Vector3(rand-.5, rand-.3, rand*.4).normalize().multiplyScalar(speed)`. If near zero.
Position target random.
`nextDecision = random*3`.
school home.
Fish type meta.

Use color for tag.

Create fish order.

Maybe `body.scale` set on mesh, but pulse visual scale would alter whole. We can pulse `body.scale.y = original* (1+sin*.015)`.
Store bodyBaseScale clone.

Neighbor
force:
```
const neighborVelocity = new Vector3(), center = ...
for each other:
 if same school:
  d=...
  if d&lt;2.6:
   neighborVelocity.add(other.velocity)
   center.add(other.position)
...
if n:
 align.copy(neighborVelocity).divideScalar(n).sub(velocity).multiplyScalar(.22)
 cohesion.copy(center/n).sub(pos).normalize().multiplyScalar(.15)
```
Separation separately.
For separation if d&lt;.85:
 diff=pos-other.pos normalized *(.85-d)*1.4.
No collision huge.
Target to:
`toTarget.copy(target).sub(pos); if len&gt;.2 steer.add(normalized*0.85)`
Boundary:
```
if pos.x&gt;4.8 steer.x -= (pos.x-4.8)*3;
...
```
Need avoid target outside.

Pointer projected:
project fish root position. `proj.z` within -1,1.
distance.
World away from pointer:
`const away = new Vector3((proj.x-pointer.ndc.x)*0.5?` Better use pixel differences and scale:
dx=(projPx.x-pointer.x)/pointerRadius;
dy=(projPx.y-pointer.y)/pointerRadius;
away.set(dx, -dy, 0).normalize();
Since proj NDC y not screen, use screen coords from projection: px=(proj.x*.5+.5)*innerWidth; py=(-proj.y*.5+.5)*innerHeight.
Map to world with factor maybe visibleWorldWidth/innerWidth and visibleHeight/innerHeight. camera at z fish, but roughly:
const viewH=2*Math.tan(THREE.MathUtils.degToRad(camera.fov/2))*camera.position.z;
const viewW=viewH*camera.aspect;
away.set(dx*viewW/innerWidth, -dy*viewH/innerHeight,0).normalize();
This precisely.
Push z `steer.z += strength*1.2`.
Distance radius. strength `(1-dist/r)^2`.
Also flee if dist&lt;radius.
Set nearest.

Desired vector normalization after steering. If pointer force and steering huge, normalized direction loses magnitude but speed multiplier. okay.
Flee speed = speed*(1+1.4*strength). state.flee = max(flee,strength). `flee` decays.
Maybe fish near cursor dart then veer.
Orientation smooth.

Bound hard clamp:
```
if x &gt;5.15 {x=5.15; vel.x=-abs...}
```
Tag nearest after update; pointer radius and maybe projected positions stale after movement, recalc at end? We can recompute projections after moving for tag. Or use stored pre-move adequate. Better update positions then for each project and distance. We'll separate:
First update all movement without pointer avoidance? Need pointer avoidance uses current positions. Then after update recalc. Fine.
Nearest computed in avoidance loop pre movement.

Camera matrix and renderer.

Animate:
```
const clock=new THREE.Clock();
function animate(){
 const dt=Math.min(clock.getDelta(),.04);
 const t=clock.elapsedTime;
 update camera
 back uniforms...
 fish
 plants
 bubbles
 jelly...
 ripples
 renderer.render
}
```
Clock elapsedTime updates with getDelta yes.
Could use performance now.

Bubble update:
positions array, each bubble y += speed*dt; if >3.35 reset -2.35; x += sin(t*...+phase)*dt*.03; maybe.
Need attribute needsUpdate.
Motes can static.

Rings.

Jellyfish optional. Let's add 2 simple to make aquarium rich.
CreateJelly(color, x,y,z,scale):
Group. bell SphereGeometry(.32,24,12,0,2pi,0,Math.PI/2), material Physical transparent .42 roughness .1 transmission .25 opacity .42 depthWrite false emissive.
Bell position y .2. ring TorusGeometry(.32,.025...). rotate x PI/2? Torus default XY normal Z, ring horizontal should normal Y -> rotateX PI/2.
Tentacles: use `THREE.CatmullRomCurve3` and `TubeGeometry(curve,20,.012,5,false)` 5, around. Material Basic transparent .45.
Group.
Animate group y base + sin; group.rotation.z sin*.08; scale.
But hemisphere sphere orientation: Sphere phi theta0 to PI/2 yields top half y&gt;=0, open bottom, good.
Tentacles start y=.18 down to-.8.
Place (-3.6,1.4,-1.55) and (3.4,2.0,-1.6). They may overlap fish but background.
Add to jelly arrays.

Depth and fish tags.

Need maybe soft particles no.

Caustics on jelly? no.

No external network beyond module.

Potential issue `THREE.WebGLRenderer` alpha true and scene.background; fine.

Use `renderer.domElement.addEventListener('webglcontextlost', e=>e.preventDefault())` optional.

Responsive camera:
```
function resize() {
 const w=innerWidth,h=innerHeight;
 camera.aspect=w/h;
 camera.fov=w&lt;640?39:42;
 cameraBaseZ=w&lt;640?15.2:10.9;
 camera.updateProjectionMatrix();
 renderer.setSize(w,h,false);
 renderer.setPixelRatio...
 pointer.radius...
}
```
At desktop 1280x800 tank width visible: viewH=2tan21*10.9=8.36, viewW13.38, tank11.5 fits.
Vertical tank6.3 =75%. Great.
At mobile viewH=2tan19.5*15.2=10.78, viewW5.05, crop. tank 58%. Fine.
Maybe set fov34 mobile, z15.2 viewH9.34, viewW4.38 crop more, tank67%. use 37, viewH10.2. good.
`cameraBaseZ = w&lt;640 ? 15.0 : 10.9; fov = w&lt;640?37:42`.

UI title at top may overlay tank top. fine.

Initial camera maybe base z set after resize.

Need use `scene.updateMatrixWorld()` before project. camera update.

Prompt overlay:
CSS position fixed left50 top50 transform etc, z7. Since aquarium behind. Add `backdrop-filter blur(12px)` but only small.
Start prompt hidden class opacity0 transform scale .96; pointer-events none.
Maybe it says exact and additional "The water is already moving.".

Interaction card bottom width.

Status dynamic every frame but only update if change.

Legend names:
Golden tang (yellow)
Blue tang (blue)
Clown anemonefish (orange)
Neon tetra (cyan)
Moon angelfish (gold).
Moon angelfish body maybe cream/yellow stripes.
Resident count 14.

Species configs:
```
const species = {
 golden:{label:'Golden tang', color:0xffd447, accent:0x174b73, scale:1, dims... speed:.9, school:0, home...},
 blue...
 clown...
 neon...
 angel...
}
```
Create lists:
`[{type:'golden',count:3,scale:1.0},...]`.
Position random:
```
const home = cfg.home;
pos x home.x+(rng-.5)*3.4, y home.y+(rng-.5)*2, z home.z+(rng-.5)*1.6
```
clamp y.
Need ensure different z visible. Fine.
Initial targets:
home + random2.
Root added.

Could show fish shadows on floor only for lower fish.

Tag `tagDot` style.

Pointer event:
```
window.addEventListener('pointermove', e=>{
 pointer.x=e.clientX...
 pointer.ndc...
 pointer.active=true;
 pointer.type=e.pointerType;
 if (!started){started=true; prompt.classList.add('is-hidden');}
}, {passive:true});
```
This means any mouse movement enters. `begin` message no longer visible as soon as mouse moves, good.
window pointerdown sets same and create ripple. But pointerdown may occur before pointermove? browser usually move first.
Keyboard: prevent? only if no inputs. no forms. `if(e.code==='Space'||e.key.length===1) e.preventDefault(); begin()`.
Potential page scrolling no due overflow, but prevent space.
No local storage.

Click ripple mapping:
raycaster set from NDC at click, plane z=.25. If camera parallax, okay.
Ring position.
Ring material. Add.
Could add ripple scale based. Update.
The ring appears in tank plane and fish may react because pointer active at click.

Cursor aura:
`aura.style.left=pointer.x+'px'; top...`; opacity .8.
At touch pointerType touch hide after up? use.
`pointer.active` stays true; touch after tap camera parallax maybe avoid. Set pointer type.
Window pointerout? no.

Use `pointerleave` to set active false? On iframe pointer leaves, window maybe pointerout. Could listen document `mouseleave`, set active false. But fish should calm. `document.addEventListener('mouseleave',()=>{pointer.active=false; aura opacity0;})`. On iframe moving out maybe.
`pointerout` relatedTarget null.

Status calm when no fleeing.

Need maybe display cursor aura over UI though pointer-events none. fine.

CSS radial aura.

Let's verify colors and shapes.

Fish pattern for clown bands:
Torus ring scale might be weird due TorusGeometry tube radius .11 and mesh.scale z .65. White band circumference. Great.
Eye sphere white body? For fish with white band, eyes black with tiny white.
`eyeWhite` maybe color 0xf7ffff.
Pupil x offset .025.
Mouth small sphere dark at x=bodyLen*.96? body sphere surface front x .52, eye x .36. mouth x=.49, y=-.02, z=0 maybe dark sphere .025, visible at front but camera sees x side? Camera front z, fish sideways. Mouth on front x, seen side. okay.
Add gill line? Torus half impossible.

Fish body shape can be more fish-like using Sphere. Tail/fins enough.
Neon tetra scale dims .36,.16,.13 and body shape.

Angelfish body dims .36,.52,.14. Tail.

Dorsal fin geometry per species maybe. Add.

Pattern spot circles might z-fight. z=side*depth*.94 and body surface at that x. Circle plane, fine.
Clown bands are 3D.

Jelly material physical `transmission` can have issues no env but okay. Use standard transparent to avoid.
Bell SphereGeometry upper hemisphere, MeshStandard transparent opacity .5 emissive. depthWrite false.

Floor pebbles instance colors:
Use dummy object, set matrix. `dummy.position.set(...)`, `dummy.scale.set(...)`, `dummy.rotation.set(...)`; update.
SetColorAt and instanceColor.needsUpdate true.
Need material color 0xffffff.

Water volume might cover fish with blue tint; opacity .025.

Point lights:
`new THREE.PointLight(0x52e9ff, 24, 18, 2)`; in Three physically correct intensity 24 candela maybe subtle. directional main.
Could use `RectAreaLight` but no UniformsLib? RectAreaLight needs RectAreaLightUniformsLib init for Standard? Avoid.
Directional 2.6, hemi2.0.

Shadows:
direction shadow camera near1 far25 left-7 right7 top6 bottom-6; map 1024. Fish cast.
Floor receives.
Coral etc.

Back wall no shadow receive.

Tank outside background. Camera at z10.9; front glass z2.25. good.

Water surface at y3.45.

Tank top maybe header overlay. okay.

Maybe add tank floor front lip.

Add a soft light circle behind tank? scene background color plus CSS. Could create large plane outside? Not needed.
Could use scene.background = new Color(0x020d17) and a large sprite? no.

Add volumetric light rays as transparent planes in back:
create 4 PlaneGeometry with gradient shader? Could enhance. But tank already rich. Maybe shader back includes beams.
Back fragment:
```
float beam = pow(max(0., sin((vUv.x*7.-.5)*3.14)),12.) * smoothstep(.2,1.,vUv.y);
col += vec3(.05,.3,.35)*beam*.12;
```

Caustic floor overlay.

Ambient motes.

UI hide when loading? prompt.

Need maybe first screenshot with prompt centered, but bottom card and header too. Great.

Let's draft code carefully.

CSS:
Use `font-synthesis:none`.
No external font.

HTML `script type=importmap` exactly JSON with spacing optional. They explicitly ask only through this import map. Use exactly:
```
<script type="importmap">
{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}
</script>
```
Then module.

JS:
const prefersReducedMotion maybe no; aquarium should move.

Let's write all functions.

Pseudo code for environment:

```
const tank = new THREE.Group();
scene.add(tank);
```
All objects except lights and maybe jelly in tank. Back panel etc.

Back ShaderMaterial uniforms.
Floor.
Tank edges.
Decor.

Frame bars:
```
const frameMat = new MeshStandardMaterial({color:0x0a3342, roughness:.25, metalness:.55, emissive:0x073240, emissiveIntensity:.2});
function box(w,h,d,x,y,z,mat){...}
box(11.7,.13,4.8,0,-2.64,0)
box(11.7,.045,4.8,0,3.56,0,glassEdgeMat)
box(.07,6.25,.07,±5.72,.47,±2.3?) corners maybe 4 vertical bars. front bars could obstruct. Use only back corners? Full tank frame nice.
```
Could add 4 corner bars x±5.7,z±2.28, y.45, dims .045,6.2,.045.
Front top etc.
Glass front plane.

Edges line from BoxGeometry maybe.
```
const edges = new THREE.LineSegments(new THREE.EdgesGeometry(new BoxGeometry(...)), lineMat)
position y=.45
```
This draws all 12 edges. Great. line transparent.

Water volume uses BoxGeometry 11.4,6.15,4.45.

Back wall. floor.

Floor uses `tank.add`.

Plants in tank.

Corals.

Jellyfish.

Fish.

Camera.

Order transparent: Back wall opaque. Water volume transparent. Fish opaque. Front glass transparent.

Maybe front glass MeshPhysical can hide via opacity .035; renderOrder 10.

Shadows with transparent plane? okay.

`MeshStandardMaterial` `emissiveIntensity` accepted.

Fish body material:
```
const mat = new THREE.MeshStandardMaterial({color, roughness:.32, metalness:.05, emissive:new THREE.Color(color).multiplyScalar(.12)});
```
`new THREE.Color(color).multiplyScalar(.12)` gives dark emissive; emissiveIntensity maybe1.2. Better `emissive: color, emissiveIntensity:.06`.
`emissive` parameter can hex.
Accent material same.
Pattern white.

Fin material `side:THREE.DoubleSide`, transparent? no.

Fish mesh geometry sphere segments 28,18. 14* body ~ moderate. Eyes 4 each, 56. Fins 4 each. 200 calls.
Could share base geometries but dimensions via scale. Use global `sphereGeo = new SphereGeometry(1,24,16)` for all bodies/eyes but eyes can same scaled. Good. `eyeGeo = new SphereGeometry(1,12,8)`.
Tail geometry per species reused across same fish? Create global per config. We can make fish function uses new ShapeGeometry each call. 14 fine.
Torus per pattern fish each ring, 9. fine.
Circle spots each.
Total ~160 calls plus coral ~80 and pebbles1. Fine.

Need clone material? each fish 3 mats.

Create fish:
```
const bodyMat ...
const accentMat ...
const whiteMat maybe global.
```
Global white material and dark material. But clown bands all white. Eyes all white. use globals.
Black eye material roughness .2.
Pattern dark global? Blue tang tail uses own.
Tang spot uses accent? accent dark.

Global geometries:
sphereGeo, eyeGeo.
Fin shape geometry can generate per type but each fin.

Pectoral fin for each side with same geometry; add mesh. Store array.
Dorsal.
Tail.

Species dims object.

`fish.visual.scale.setScalar(scale)`.

School home vectors.

Add root to tank. root.position.

Pattern:
- clown bands: create 3 rings.
- angel bands: 2 rings dark maybe accent; body cream, accent 0x233d52.
- golden spots: body yellow, accent navy tail maybe; 3 spots both sides.
- blue tang: body blue, tail yellow; perhaps a yellow crescent tail.
- neon stripe: 2 sides emissive.

Create tail accent.

For golden tang maybe tail accent navy? Real yellow tang tail yellow. Better accent same body or slightly. But user sees species. Golden body with dark spot near gill. tail body. accent used spots.
Blue tang accent yellow tail.
Clown accent orange tail, white pattern.
Neon accent red tail.
Angel accent dark stripe/tail.

Pattern ring orientation.
Dorsal fin color accent maybe body.

Position eye relative dims:
```
const eyeX = dims[0]*.66
const eyeY = dims[1]*.25
const eyeZ = side*dims[2]*.86
```
Eye white scale .065* maybe dimensions? Neon smaller eye .045. Use `eyeR = Math.min(.075,dims[0]*.11)`.
Pupil at eyeX+eyeR*.25, eyeY, eyeZ+side*(eyeR*.55)
```
white mesh scale eyeR
pupil scale eyeR*.48
```
At z side, pupil more outward. x forward.
Mouth dark sphere scale dims[0]*.025 at x dims[0]*.94 y=-dims[1]*.05 z=0; might not visible from front but okay.
Maybe gill ring? no.

Tail position `x=-dims[0]*.74`, scale? geometry extends to -.55; body extends -.55, overlap. Position x=-dims[0]*.82 gives tail start inside body at -.43 and end -.98; good.
For angelfish tail shape longer? scale1.1.
Tail shape vertical span .34; for angel body height .52, scale y1.25; for tetra scale .7.
Dorsal geometry:
```
shape.moveTo(-.22,.05); bezierCurveTo(-.12,.38,.12,.44,.28,.08)
...
```
Position y=dims[1]*.55 maybe.
Fins on side:
shape moveTo(0,0), lineTo(-.22,-.18), lineTo(-.28,.02), close. Position x=.1,y=-.05,z side*dims[2]*.82. But same XY; may be hidden inside body. rotate y? Plane in XY, z position side, okay. It projects as fin overlapping side. Set z side*1.02.
Maybe pectoral on front z=side; camera at z+, only front side positive visible. Both.

Tail orientation group local XY facing camera when fish direction along x; if fish turns in z, still. fine.

Orientation velocity z could point camera causing body end-on, natural.

School update.

Tag species.

Now `residentCount` set actual.

Status:
```
if fleeCount>0 statusText.textContent = `${fleeCount} fish darting away`; live-dot class alert maybe
else 'Calm water'
```
top card small. dynamic.

Prompt first paint and no JS if module delayed maybe.

Potential issue import map JSON script indentation is okay.

Let's inspect syntax for shader strings. Use backticks.

Back fragment:
```
float c = pow(max(0.0, sin((vUv.x * 18.0 + sin(vUv.y * 10.0 + uTime * 0.55)) * 1.7 + uTime * 0.35) * 0.5 + 0.5), 8.0);
```
sin ranges -1..1 then *.5+.5.
edge vignette.
`gl_FragColor=vec4(color,1.0);`

Floor caustic:
```
float a = sin(p.x + sin(p.y + uTime*.6));
float b=...
float c=pow(max(0.0,1.0-abs((a+b)*.5)),8.0);
alpha c*.24
```
Add blending.

Water surface vertex modifies `transformed.z`. Since plane mesh rotated, local z becomes world y. yes.
frag shimmer.

Maybe shader uniform time in each.

Jelly.

Bubble shader uses `attribute float aSize;` in ShaderMaterial vertex and `uniform float uPixelRatio` maybe update on resize. Set gl_PointSize = aSize*uPixelRatio*(80.0/-mv.z); aSize random .4 =>2.9 * DPR. good.
At far z=9 etc.

Particle points:
`new THREE.PointsMaterial({color:0xc8ffff,size:.025,transparent:true,opacity:.45,depthWrite:false})`; PointsMaterial square, tiny not noticeable.
Could use same bubble shader for motes but no.

Bubble geometry positions dynamic. count30.

Jelly tentacles tubes static with transparent; create curves:
```
for i:
 points [new V3(cos angle*.12,.15,sin*.12), ... y -.8 with waves]
 const curve = new CatmullRomCurve3(points)
 const geo = new TubeGeometry(curve,18,.012,5,false)
```
No animation shape, group rotates.
Bell pulse scale y 1+sin*.08.

Fish `state.target` within bounds.
Potential fish targets and school homes.

`makeFish` position uses home but configs each. Could pass initial position.

Call:
```
const roster = [...]
for each:
 for i:
  const angle=...
  const pos = home.clone().add(new Vector3((rng-.5)*3, ...))
  clamp...
  const fish = createFish...
  fish.velocity...
  fishes.push
```
Need `createFish` root initially maybe position set later.

Color config.
School same type.

Neon tetra home perhaps x=-.6,y1.2.
Tang yellow home (-2.2,.2,0)
Blue (2,.3,0)
Clown (0,-.8,.45)
Angel (0,1,-1)
At mobile central.
Maybe 14 distributed.

Tang home etc.

Update each:
`f.targetTimer -= dt; if <=0 set target`.
Home target: f.home + random [-2,2], y[-1.2,1.2], z[-1,1].
Clamp.

School neighbor align.

Pointer nearest:
Need pointer active even before started? pointermove triggers started. yes.
`pointer.ndc` used camera.

For projection root world position; root local pos same as tank no transform. getWorldPosition.

Compute distance in pixels; if pointer over fish body center.

Camera parallax may move and pointer screen mapping, but fine.

`pointer.radius` responsive.

Potential pointer from touch and `clientX` etc. pointerdown.

Ripple mapping:
```
const ndc = new Vector2((x/width)*2-1, -(y/height)*2+1);
raycaster.setFromCamera(ndc,camera);
const hit=raycaster.ray.intersectPlane(plane,new Vector3());
if(hit){...}
```
Plane z=.1 maybe front; set new Plane(new V3(0,0,1), -.2) equation z - .2=0. Constructor constant -.2. Use.
Ring geometry normal +Z, camera from +z, visible.
Store.

Animate ripples:
life +=dt; t=life/.8; scale=1+t*5; opacity=.65*(1-t); remove >1.
No errors.

Maybe prompt start message fades on pointermove immediately; but prompt class `.is-hidden`. Good.

Need code no `await` top-level. module.

Let's consider use of `performance` etc.

Let's create final code. But desired oververbosity doesn't matter due exact one block.

We should perhaps test mentally Three API r170:
- `renderer.outputColorSpace = THREE.SRGBColorSpace` yes.
- `renderer.toneMapping = THREE.ACESFilmicToneMapping`.
- `THREE.PCFSoftShadowMap`.
- `THREE.ShapeGeometry`.
- `THREE.MeshPhysicalMaterial` `transmission` supported but don't use.
- `THREE.MathUtils.degToRad`.
- `THREE.Quaternion.setFromUnitVectors`.
- `Vector3.project`.
- `BufferAttribute.setY`.
- `InstancedMesh.instanceColor` created after setColorAt, needsUpdate.
- `PointLight` constructor intensity distance decay.
- `THREE.AdditiveBlending`.
- `ShaderMaterial` gl_FragColor works WebGL2 compatibility? Three compiles GLSL1 by default and converts. yes.
- `material.fog` default true for Standard; shader false default.
- `MeshStandardMaterial.flatShading`.
- `Color.multiplyScalar` could exceed but used.
- `ShapeGeometry` normals point +Z, side DoubleSide.

Potential issue adding `scene.background=Color` and renderer alpha true means CSS no matter.
- `camera.lookAt` each frame.
- `renderer.setSize(width,height,false)` canvas CSS set width height via CSS, but canvas backing updates. set false doesn't set style; CSS already 100%.
- `renderer.setPixelRatio` each resize maybe resets.
- `window.devicePixelRatio`.
- `canvas aria` set.

Let's create robust fallback for renderer:
```
let renderer;
try {...} catch(err) {
 const p=...; p.innerHTML='<span>WebGL is required...</span>';
}
```
But module code then can return? Top-level return illegal. Could wrap all in `init()` function and catch:
```
try { init(); } catch (error) { ... }
function init(){...}
```
This catches runtime initial errors but no console. However async errors in animation not caught. We trust.
Could use `init();` no catch. Fine.

Need zero console error from CDN maybe network okay.

Let's maybe avoid `Math.random` deterministic no issue.

Decorative rocks:
Arch TorusGeometry arc parameter accepted: `new THREE.TorusGeometry(radius, tube, radialSegments, tubularSegments, arc)`. yes.

Coral `root` group add, use `mesh.quaternion.setFromUnitVectors(up, dir)`.
Cylinder height len and radius top/bottom; mesh centered.
Tips sphere geometry.

Seaweed leaf material side double.

Maybe backdrop shadows.

Let's flesh actual final code now. Ensure onecode fence.