Models 44

Mistral Large 4

by MistralReasoningClean
Open in new window

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

How it went

Total time
5 min 6 s
Cost
$0.051
Output tokens
24,320
Spent thinking
17,603 tokens
Speed
79.7 tok/s
Console errors
0
First token after
832 ms
First visible text after
4 min 11 s
Prompt tokens
438
Code size
19.1 KB
Finish reason
stop
Served by
Mistral
Model id
mistralai/mistral-large-4-0
Reasoning sent
{"effort":"high"}
Max output
262,144 tokens
Generated
Nov 12, 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 · 481 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>3D Aquarium — Scare the Fish</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html, body { width:100%; height:100%; overflow:hidden; background:#06202f; }
  body { font-family:'Segoe UI', system-ui, -apple-system, sans-serif; }
  canvas { display:block; touch-action:none; }
  #overlay {
    position:fixed; inset:0; display:flex; align-items:center; justify-content:center;
    pointer-events:none; z-index:10;
    background:radial-gradient(ellipse at center, rgba(4,26,40,.12), rgba(2,14,24,.42));
    transition:opacity .9s ease;
  }
  #overlay.hidden { opacity:0; }
  .card {
    text-align:center; padding:20px 32px; border-radius:18px; max-width:min(88vw,480px);
    background:rgba(6,32,47,.55); border:1px solid rgba(140,220,255,.28);
    backdrop-filter:blur(6px); color:#dff3ff; box-shadow:0 8px 40px rgba(0,0,0,.35);
    animation:float 3.5s ease-in-out infinite;
  }
  .card h1 { font-size:clamp(22px,4.5vw,34px); letter-spacing:.5px; margin-bottom:8px; text-shadow:0 2px 12px rgba(80,200,255,.55); }
  .card p { font-size:clamp(13px,2.6vw,16px); opacity:.92; }
  .card .dim { margin-top:7px; font-size:clamp(11px,2.2vw,13px); opacity:.65; animation:pulse 1.6s ease-in-out infinite; }
  @keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} }
  @keyframes pulse { 0%,100%{opacity:.35} 50%{opacity:.8} }
  #hint {
    position:fixed; left:50%; bottom:18px; transform:translateX(-50%);
    color:#cfeeff; background:rgba(6,32,47,.6); border:1px solid rgba(140,220,255,.25);
    padding:8px 16px; border-radius:999px; font-size:clamp(11px,2.4vw,14px);
    pointer-events:none; z-index:9; white-space:nowrap;
    animation:hintfade 1s ease 9s forwards;
  }
  @keyframes hintfade { to { opacity:0; visibility:hidden; } }
</style>
</head>
<body>
<div id="overlay">
  <div class="card">
    <h1>🐠 3D Aquarium</h1>
    <p>Move your mouse close to a fish and it will dart away!</p>
    <p class="dim">Click or press any key to start</p>
  </div>
</div>
<div id="hint">🖱️ Move near the fish to scare them</div>

<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>
<script type="module">
import * as THREE from 'three';

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

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x082a40);
scene.fog = new THREE.Fog(0x0a3a58, 12, 34);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth/window.innerHeight, 0.1, 200);
camera.position.set(0, 0.6, 15.5);

const TANK = { x:9.4, yMin:-4.4, yMax:4.3, z:3.4 };
let camZ = 15.5;

function fitCamera(){
  const aspect = window.innerWidth/window.innerHeight;
  const t = Math.tan(camera.fov*Math.PI/360);
  const distY = 5.8/t;
  const distX = 10.6/(t*aspect);
  camZ = Math.max(distY, distX) + 1.2;
  scene.fog.near = camZ*0.72;
  scene.fog.far  = camZ*2.3;
}
fitCamera();

/* ---------------- lights ---------------- */
scene.add(new THREE.HemisphereLight(0xcfeaff, 0x0a2f42, 1.0));
const sun = new THREE.DirectionalLight(0xffffff, 2.4);
sun.position.set(6, 14, 8);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
sun.shadow.camera.left = -13; sun.shadow.camera.right = 13;
sun.shadow.camera.top = 13;   sun.shadow.camera.bottom = -13;
sun.shadow.camera.far = 45;
sun.shadow.bias = -0.0006;
scene.add(sun);
const glow = new THREE.PointLight(0x7fd4ff, 60, 34, 2);
glow.position.set(0, 6.5, 0);
scene.add(glow);

/* ---------------- tank ---------------- */
const glassMat = new THREE.MeshPhysicalMaterial({
  color:0x9fd8ef, transparent:true, opacity:0.10, roughness:0.05,
  metalness:0, side:THREE.DoubleSide, depthWrite:false
});
const glass = new THREE.Mesh(new THREE.BoxGeometry(20, 10, 8), glassMat);
glass.renderOrder = 20;
scene.add(glass);
const edges = new THREE.LineSegments(
  new THREE.EdgesGeometry(glass.geometry),
  new THREE.LineBasicMaterial({ color:0xbfe9ff, transparent:true, opacity:0.55 })
);
scene.add(edges);

const surface = new THREE.Mesh(
  new THREE.PlaneGeometry(19.6, 7.6),
  new THREE.MeshPhongMaterial({ color:0x5fc4e8, transparent:true, opacity:0.4, shininess:120, specular:0xaaddff, side:THREE.DoubleSide })
);
surface.rotation.x = -Math.PI/2;
surface.position.y = 4.95;
scene.add(surface);

const sand = new THREE.Mesh(
  new THREE.PlaneGeometry(20, 8),
  new THREE.MeshStandardMaterial({ color:0xc9b083, roughness:0.95 })
);
sand.rotation.x = -Math.PI/2;
sand.position.y = -5;
sand.receiveShadow = true;
scene.add(sand);

const cabinet = new THREE.Mesh(new THREE.BoxGeometry(22, 1.2, 10), new THREE.MeshStandardMaterial({ color:0x3a2c22, roughness:0.8 }));
cabinet.position.y = -5.6;
scene.add(cabinet);
const rim = new THREE.Mesh(new THREE.BoxGeometry(21, 0.28, 9), new THREE.MeshStandardMaterial({ color:0x54402f, roughness:0.7 }));
rim.position.y = -4.95;
scene.add(rim);

/* ---------------- gravel (instanced) ---------------- */
const m4 = new THREE.Matrix4(), q = new THREE.Quaternion(), s = new THREE.Vector3(), p = new THREE.Vector3();
const col = new THREE.Color();
const GRAVEL_N = 260;
const gravel = new THREE.InstancedMesh(
  new THREE.IcosahedronGeometry(0.16, 0),
  new THREE.MeshStandardMaterial({ roughness:0.9, flatShading:true }),
  GRAVEL_N
);
gravel.receiveShadow = true;
for (let i=0;i<GRAVEL_N;i++){
  p.set((Math.random()-0.5)*19, -4.92+Math.random()*0.12, (Math.random()-0.5)*7.4);
  q.setFromEuler(new THREE.Euler(Math.random()*3, Math.random()*3, Math.random()*3));
  const sc = 0.5+Math.random()*1.4;
  s.set(sc, sc*(0.5+Math.random()*0.6), sc);
  m4.compose(p, q, s);
  gravel.setMatrixAt(i, m4);
  col.setHSL(0.08+Math.random()*0.07, 0.35+Math.random()*0.25, 0.42+Math.random()*0.22);
  gravel.setColorAt(i, col);
}
scene.add(gravel);

/* ---------------- plants ---------------- */
const plants = [];
function makePlant(x, z, h, hue){
  const g = new THREE.Group();
  const mat = new THREE.MeshStandardMaterial({ color:new THREE.Color().setHSL(hue, 0.65, 0.32), roughness:0.55 });
  const n = 5 + Math.floor(Math.random()*4);
  for (let i=0;i<n;i++){
    const bh = h*(0.55+Math.random()*0.55);
    const bend = (Math.random()-0.5)*1.6;
    const lean = (Math.random()-0.5)*0.5;
    const pts = [];
    for (let k=0;k<=6;k++){
      const t = k/6;
      pts.push(new THREE.Vector3(lean*t + Math.sin(t*2.2+bend)*0.35*bend*t, bh*t, Math.cos(t*bend)*0.3*bend*t));
    }
    const blade = new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 10, 0.045+Math.random()*0.05, 5), mat);
    blade.castShadow = true;
    g.add(blade);
  }
  g.position.set(x, -5, z);
  g.userData.phase = Math.random()*10;
  scene.add(g);
  plants.push(g);
}
makePlant(-7.5,-2.5,3.4,0.33); makePlant(-6.3,-3.0,2.4,0.36);
makePlant( 7.6,-2.6,3.8,0.30); makePlant( 6.4,-3.2,2.6,0.38);
makePlant(-8.2, 0.5,2.8,0.34); makePlant( 8.4, 1.2,3.0,0.31);
makePlant( 0.5,-3.4,2.0,0.37);

/* ---------------- rocks & chest ---------------- */
const rockMat = new THREE.MeshStandardMaterial({ color:0x6b7a86, roughness:0.9, flatShading:true });
[[-4.5,-3.1,1.1],[3.8,-3.3,0.8],[-1.5,-3.5,0.6],[5.9,-2.0,1.3]].forEach(([x,z,r])=>{
  const rk = new THREE.Mesh(new THREE.DodecahedronGeometry(r, 0), rockMat);
  rk.position.set(x, -5+r*0.55, z);
  rk.rotation.set(Math.random()*3, Math.random()*3, Math.random()*3);
  rk.castShadow = true; rk.receiveShadow = true;
  scene.add(rk);
});
const chest = new THREE.Group();
const wood = new THREE.MeshStandardMaterial({ color:0x7a4a26, roughness:0.7 });
const woodDark = new THREE.MeshStandardMaterial({ color:0x5d3618, roughness:0.75 });
const gold = new THREE.MeshStandardMaterial({ color:0xffd34d, metalness:0.8, roughness:0.3 });
const cb = new THREE.Mesh(new THREE.BoxGeometry(1.7, 0.85, 0.95), woodDark); cb.position.y = 0.42;
const lid = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.4, 1.05), wood); lid.position.y = 0.95;
const band1 = new THREE.Mesh(new THREE.BoxGeometry(0.18, 1.25, 1.02), gold); band1.position.set(-0.5, 0.6, 0);
const band2 = band1.clone(); band2.position.x = 0.5;
chest.add(cb, lid, band1, band2);
chest.position.set(4.6, -4.55, -1.8);
chest.rotation.y = -0.5;
chest.traverse(o => { if (o.isMesh) o.castShadow = true; });
scene.add(chest);

/* ---------------- god rays ---------------- */
const rayMats = [];
const rayGeo = new THREE.CylinderGeometry(0.5, 3.2, 18, 12, 1, true);
for (let i=0;i<5;i++){
  const mat = new THREE.MeshBasicMaterial({ color:0xaee2ff, transparent:true, opacity:0.05, blending:THREE.AdditiveBlending, side:THREE.DoubleSide, depthWrite:false });
  const ray = new THREE.Mesh(rayGeo, mat);
  ray.position.set(-7+i*3.5, 0, -1+(i%2));
  ray.rotation.z = 0.18;
  ray.rotation.x = (i-2)*0.05;
  ray.renderOrder = 5;
  scene.add(ray);
  rayMats.push(mat);
}

/* ---------------- bubbles & plankton ---------------- */
const BUB_N = 70;
const bubbles = new THREE.InstancedMesh(
  new THREE.SphereGeometry(0.09, 8, 6),
  new THREE.MeshStandardMaterial({ color:0xe6f7ff, transparent:true, opacity:0.5, roughness:0.15 }),
  BUB_N
);
bubbles.renderOrder = 6;
scene.add(bubbles);
const bub = [];
for (let i=0;i<BUB_N;i++) bub.push({ x:(Math.random()-0.5)*18, y:-5+Math.random()*10, z:(Math.random()-0.5)*7, v:0.5+Math.random()*0.9, ph:Math.random()*10, sc:0.4+Math.random()*1.1 });

const PN = 220;
const pGeo = new THREE.BufferGeometry();
const pPos = new Float32Array(PN*3);
for (let i=0;i<PN;i++){
  pPos[i*3]   = (Math.random()-0.5)*19;
  pPos[i*3+1] = -4.8+Math.random()*9.5;
  pPos[i*3+2] = (Math.random()-0.5)*7.4;
}
pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
const points = new THREE.Points(pGeo, new THREE.PointsMaterial({ color:0xbfeaff, size:0.06, transparent:true, opacity:0.5, blending:THREE.AdditiveBlending, depthWrite:false }));
scene.add(points);

/* ---------------- fish ---------------- */
const bodyGeo = new THREE.SphereGeometry(0.6, 24, 16);
const tailGeo = new THREE.ConeGeometry(0.45, 1.1, 16);
const finGeo  = new THREE.ConeGeometry(0.26, 0.7, 12);
const eyeGeo  = new THREE.SphereGeometry(0.115, 10, 8);
const pupilGeo= new THREE.SphereGeometry(0.055, 8, 6);
const eyeMat  = new THREE.MeshStandardMaterial({ color:0xffffff, roughness:0.2 });
const pupMat  = new THREE.MeshStandardMaterial({ color:0x111111, roughness:0.2 });

function makeFish(color, scale){
  const g = new THREE.Group();
  const bodyMat = new THREE.MeshStandardMaterial({ color, roughness:0.35, metalness:0.15 });
  const finMat = new THREE.MeshStandardMaterial({ color:new THREE.Color(color).offsetHSL(0, 0.05, 0.18), roughness:0.45, transparent:true, opacity:0.9 });

  const body = new THREE.Mesh(bodyGeo, bodyMat);
  body.scale.set(0.55, 0.78, 1.05);
  body.castShadow = true;
  g.add(body);

  const tailPivot = new THREE.Group();
  tailPivot.position.z = -0.55;
  const tail = new THREE.Mesh(tailGeo, finMat);
  tail.position.y = 0.55;
  tail.rotation.x = -Math.PI/2;
  tail.scale.set(0.16, 1, 1);
  tail.castShadow = true;
  tailPivot.add(tail);
  g.add(tailPivot);

  const dorsal = new THREE.Mesh(finGeo, finMat);
  dorsal.position.set(0, 0.42, -0.1);
  dorsal.rotation.x = -0.75;
  dorsal.scale.set(0.14, 1, 0.7);
  g.add(dorsal);

  const finL = new THREE.Mesh(finGeo, finMat);
  finL.position.set(0.3, -0.1, 0.05);
  finL.rotation.set(0.4, 0, -1.1);
  finL.scale.set(0.12, 1, 1);
  finL.userData.base = -1.1;
  g.add(finL);
  const finR = finL.clone();
  finR.position.x = -0.3;
  finR.rotation.z = 1.1;
  finR.userData.base = 1.1;
  g.add(finR);

  const eL = new THREE.Mesh(eyeGeo, eyeMat); eL.position.set( 0.21, 0.14, 0.42);
  const eR = new THREE.Mesh(eyeGeo, eyeMat); eR.position.set(-0.21, 0.14, 0.42);
  const pL = new THREE.Mesh(pupilGeo, pupMat); pL.position.set( 0.21, 0.14, 0.51);
  const pR = new THREE.Mesh(pupilGeo, pupMat); pR.position.set(-0.21, 0.14, 0.51);
  g.add(eL, eR, pL, pR);

  g.scale.setScalar(scale);
  return { group:g, tail:tailPivot, finL, finR };
}

const FISH_COLORS = [0xff8c42, 0xffd23f, 0x4dabf7, 0xb197fc, 0xff6b9d, 0x63e6a4, 0xff6b6b, 0x66d9e8, 0xf8a5c2, 0xffb347, 0x7bdff2];
const fishes = [];
for (let i=0;i<11;i++){
  const f = makeFish(FISH_COLORS[i % FISH_COLORS.length], 0.55+Math.random()*0.5);
  const pos = new THREE.Vector3((Math.random()-0.5)*16, -3.5+Math.random()*7, (Math.random()-0.5)*6);
  const vel = new THREE.Vector3(Math.random()-0.5, (Math.random()-0.5)*0.4, Math.random()-0.5).normalize().multiplyScalar(1+Math.random());
  f.group.position.copy(pos);
  scene.add(f.group);
  fishes.push({ ...f, pos, vel, maxSpeed:1.2+Math.random()*1.1, minSpeed:0.5, wander:1.6, panic:0 });
}

/* ---------------- mouse / raycast ---------------- */
const raycaster = new THREE.Raycaster();
const mouseNDC = new THREE.Vector2(0, 0);
const mouseWorld = new THREE.Vector3();
const fleePlane = new THREE.Plane(new THREE.Vector3(0,0,1), 0);
const mouse = { active:false };

function onPointer(e){
  const cx = (e.clientX !== undefined) ? e.clientX : (e.touches && e.touches[0] ? e.touches[0].clientX : undefined);
  const cy = (e.clientY !== undefined) ? e.clientY : (e.touches && e.touches[0] ? e.touches[0].clientY : undefined);
  if (cx === undefined) return;
  mouseNDC.set((cx/window.innerWidth)*2-1, -(cy/window.innerHeight)*2+1);
  raycaster.setFromCamera(mouseNDC, camera);
  const hit = new THREE.Vector3();
  if (raycaster.ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0,0,1), 0), hit)){
    mouseWorld.copy(hit);
    mouse.active = true;
  }
}

/* ---------------- ripple marker ---------------- */
const ripple = new THREE.Mesh(
  new THREE.RingGeometry(0.42, 0.55, 40),
  new THREE.MeshBasicMaterial({ color:0xcdeeff, transparent:true, opacity:0, side:THREE.DoubleSide, blending:THREE.AdditiveBlending, depthWrite:false })
);
ripple.renderOrder = 7;
ripple.visible = false;
scene.add(ripple);
let rippleT = 0;

/* ---------------- UI ---------------- */
const overlay = document.getElementById('overlay');
let started = false;
function start(){
  if (!started){ started = true; overlay.classList.add('hidden'); }
}
window.addEventListener('pointermove', e => { start(); onPointer(e); });
window.addEventListener('pointerdown', e => { start(); onPointer(e); });
window.addEventListener('touchmove',   e => { start(); onPointer(e); }, { passive:true });
window.addEventListener('keydown', start);
document.addEventListener('pointerleave', () => { mouse.active = false; });
window.addEventListener('blur', () => { mouse.active = false; });
setTimeout(start, 7000);
if ('ontouchstart' in window){
  document.querySelector('.card .dim').textContent = '👆 Tap the tank to start';
  document.getElementById('hint').textContent = '👆 Drag near the fish to scare them';
}

/* ---------------- resize ---------------- */
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth/window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  fitCamera();
});

/* ---------------- animation loop ---------------- */
const clock = new THREE.Clock();
const desired = new THREE.Vector3();
const tmpV = new THREE.Vector3();
const lookV = new THREE.Vector3();
const dummy = new THREE.Object3D();
const FLEE_R = 3.4;
const MAX_BOOST = 2.3;

function animate(){
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;

  for (const pl of plants){
    pl.rotation.z = Math.sin(t*0.9 + pl.userData.phase)*0.07;
    pl.rotation.x = Math.cos(t*0.7 + pl.userData.phase)*0.05;
  }
  rayMats.forEach((mat, i) => { mat.opacity = 0.035 + 0.03*(0.5+0.5*Math.sin(t*0.6 + i*1.7)); });
  surface.position.y = 4.95 + Math.sin(t*0.8)*0.06;
  surface.rotation.z = Math.sin(t*0.4)*0.02;
  points.rotation.y = Math.sin(t*0.05)*0.2;
  points.position.y = Math.sin(t*0.3)*0.2;

  for (let i=0;i<BUB_N;i++){
    const b = bub[i];
    b.y += b.v*dt;
    b.x += Math.sin(t*2 + b.ph)*0.25*dt;
    if (b.y > 4.7){ b.y = -4.9; b.x = (Math.random()-0.5)*18; b.z = (Math.random()-0.5)*7; }
    p.set(b.x, b.y, b.z);
    s.setScalar(b.sc*(0.7+0.3*Math.sin(t*3+b.ph)));
    m4.compose(p, q.identity(), s);
    bubbles.setMatrixAt(i, m4);
  }
  bubbles.instanceMatrix.needsUpdate = true;

  for (const f of fishes){
    let fleeing = false;
    if (mouse.active){
      fleePlane.constant = -f.pos.z;               // plane at the fish's own depth
      if (raycaster.ray.intersectPlane(fleePlane, tmpV)){
        const d = tmpV.distanceTo(f.pos);
        if (d < FLEE_R){
          fleeing = true;
          const urg = 1 - d/FLEE_R;
          desired.subVectors(f.pos, tmpV).normalize().multiplyScalar(f.maxSpeed*MAX_BOOST*(0.45+0.55*urg));
          desired.y += urg*1.4;                     // dart upward a little
          f.vel.lerp(desired, Math.min(1, urg*5*dt + 0.02));
          f.panic = Math.min(1, f.panic + urg*3*dt);
        }
      }
    }
    if (!fleeing){
      f.vel.x += (Math.random()-0.5)*f.wander*dt;
      f.vel.y += (Math.random()-0.5)*f.wander*0.5*dt - f.pos.y*0.15*dt;
      f.vel.z += (Math.random()-0.5)*f.wander*dt;
      f.panic = Math.max(0, f.panic - 2.5*dt);
      const steer = 14*dt;
      if (f.pos.x < -TANK.x) f.vel.x += (-TANK.x - f.pos.x)*steer*2;
      if (f.pos.x >  TANK.x) f.vel.x -= ( f.pos.x - TANK.x)*steer*2;
      if (f.pos.y <  TANK.yMin) f.vel.y += (TANK.yMin - f.pos.y)*steer*2;
      if (f.pos.y >  TANK.yMax) f.vel.y -= (f.pos.y - TANK.yMax)*steer*2;
      if (f.pos.z < -TANK.z) f.vel.z += (-TANK.z - f.pos.z)*steer*2;
      if (f.pos.z >  TANK.z) f.vel.z -= ( f.pos.z - TANK.z)*steer*2;
    }
    const sp = f.vel.length();
    if (sp > f.maxSpeed*MAX_BOOST) f.vel.setLength(f.maxSpeed*MAX_BOOST);
    else if (sp < f.minSpeed && f.panic <= 0.05) f.vel.setLength(f.minSpeed);

    f.pos.addScaledVector(f.vel, dt);
    f.pos.x = THREE.MathUtils.clamp(f.pos.x, -TANK.x, TANK.x);
    f.pos.y = THREE.MathUtils.clamp(f.pos.y, TANK.yMin, TANK.yMax);
    f.pos.z = THREE.MathUtils.clamp(f.pos.z, -TANK.z, TANK.z);
    f.group.position.copy(f.pos);

    if (f.vel.lengthSq() > 0.0001){
      dummy.position.copy(f.pos);
      lookV.copy(f.pos).add(f.vel);
      dummy.lookAt(lookV);
      f.group.quaternion.slerp(dummy.quaternion, 1 - Math.exp(-5*dt));
    }
    f.tail.rotation.y = Math.sin(t*(7 + f.panic*16) + sp*2)*(0.45 + f.panic*0.5);
    const flap = Math.sin(t*9)*0.22;
    f.finL.rotation.z = f.finL.userData.base + flap;
    f.finR.rotation.z = f.finR.userData.base - flap;
  }

  if (mouse.active){
    ripple.visible = true;
    ripple.position.copy(mouseWorld);
    ripple.lookAt(camera.position);
    rippleT += dt;
    const k = rippleT % 1.1;
    ripple.scale.setScalar(0.6 + k*3.2);
    ripple.material.opacity = Math.max(0, 0.55 - k*0.55);
  } else {
    ripple.visible = false;
  }

  camera.position.x += (mouseNDC.x*1.4 - camera.position.x)*(1 - Math.exp(-2*dt));
  camera.position.y += (0.6 - mouseNDC.y*0.9 - camera.position.y)*(1 - Math.exp(-2*dt));
  camera.position.z = camZ;
  camera.lookAt(0, 0, 0);

  renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
Reasoning trace (17,603 tokens)
The user wants a 3D aquarium with Three.js where fish swim away when mouse moves near them. Must use the import map specified. Self-contained HTML, single code block. Must look good at 1280×800 and adapt to 375px. Self-playing demo mode visible on first paint. Mouse interaction: fish flee from mouse raycast position.

Let me design:
- Import map with three 0.170.0 and addons.
- Scene: aquarium tank — glass box (transparent), water with fog/blue tint, gravel floor, plants (cones/cylinders or custom), rocks, bubbles, light rays (cones with additive blending), maybe a treasure chest.
- Fish: build fish from geometry — body (sphere scaled), tail (cone/plane), fins. Different colors. Each fish has velocity, wander behavior, and flee behavior when mouse ray intersects near them.
- Mouse: raycast onto a plane at some depth, get world point; fish within radius flee away from that point. Also on touch devices, touch move.
- Bubbles rising.
- OrbitControls? Could allow drag to rotate but mouse move should be for scaring fish. Maybe keep camera fixed with subtle sway, mouse only scares fish. Could add OrbitControls on right-drag... Simpler: fixed camera, mouse move scares fish. Maybe allow slight parallax camera based on mouse. That's fine.
- "Click or press a key to start" — the game isn't a game; but instructions say games must start in self-playing demo mode behind a message. This is not a game exactly, but I can add an overlay "Move your mouse to scare the fish — click or press any key" that fades. Actually the interaction is mouse move, which works without click in iframe? The iframe only receives keyboard after click, but mouse events should work. I'll show a hint overlay that fades on first mouse move. Also maybe a "feed" mechanic? Keep simple: hint text.

Fish design: create a group per fish:
- body: SphereGeometry scaled (1, 0.6, 0.35) or use a LatheGeometry. Simpler: sphere scaled.
- tail: ConeGeometry rotated, or a flattened cone at back, animated wagging via rotation.y oscillation.
- eyes: small spheres.
- fins: small flattened cones on sides.

Animation: each fish swims along a path with wander (steering), constrained within tank bounds (bounce/turn). When fleeing, add velocity away from mouse point, with speed boost, then decay.

Mouse world point: raycast from camera through NDC onto plane z = 0 (middle of tank) or use the tank's back wall. Better: intersect with a plane facing camera at the fish depth... Use raycaster.ray.intersectPlane with plane at z=0 normal (0,0,1). Tank dimensions: width 20, height 10, depth 8, centered at origin, floor at y=-5. Camera at (0, 1, 14) looking at (0,0,0).

Water: big box with MeshPhysicalMaterial transmission? Transmission is expensive but fine. Use MeshPhongMaterial transparent blue opacity 0.15 for glass walls, plus scene fog light blue, background gradient. Add a "water surface" plane at top with slight animation.

Lighting: ambient + directional + point lights. God rays: a few cones from top with additive transparent material, slowly rotating.

Plants: sway animation — use groups of curved blades: TorusGeometry segments or custom. Simple: cylinders bent? Use ConeGeometry curved via... simplest: create blade from a plane with sin curve, or use a few stretched cones leaning, sway rotation.z. Or use TubeGeometry along a curve — nice. I'll make a helper making a plant blade: CatmullRomCurve3 with slight bend, TubeGeometry radius tapering not possible; use multiple segments... simpler: use CylinderGeometry with small radius, bend by rotating segments in a group. Or just use cones leaning with sway. Good enough with nice colors.

Gravel: many small spheres/icosahedrons scattered on floor, random colors (sand tones). Use InstancedMesh for performance with ~300 pebbles.

Bubbles: instanced spheres rising with wobble, reset at top.

Fish count: ~10 fish, varied sizes/colors. Each fish: group with body mesh etc. Movement: velocity vector, max speed, wander via perlin-ish random steering. Bounds: if near wall, steer back to center. Flee: compute mouse world point; distance in 3D; if < fleeRadius (e.g., 3.5), set fleeTarget/velocity away with urgency proportional to closeness. Smooth: desired = (fish.pos - mousePoint).normalize * maxSpeed * urgency; velocity lerp toward desired. Also add slight vertical component. When not fleeing, lerp back to wander speed.

Also fish orientation: lookAt along velocity (with lerp). Tail wag: tail.rotation.y = sin(t*speed)*amp.

Mouse tracking: pointermove on window, convert to NDC, raycast plane z=0. Also show a subtle ripple/ring at mouse point in water? Could add a small expanding ring mesh at mouse point on the plane — nice feedback. Add a "mouse ripple" — a ring geometry that appears at intersection point, scales and fades.

Camera parallax: camera.position.x lerp to mouse.x*1.5 etc., lookAt origin. Subtle.

UI: title overlay "🐠 Aquarium — move your mouse near the fish to scare them away", fades after first move; also "Click or press any key" — since not a game, but the system prompt says games need that; this is interactive demo. I'll include an overlay that says "Move your cursor near the fish to scare them • click or press any key to begin" and fades out on interaction, then a small hint at bottom that reappears? Keep: overlay center fades after 2.5s or first interaction. On mobile touch: touchmove scares fish.

Resize handler. Pixel ratio clamp.

Performance: fine.

Let me write the fish geometry with shared geometries/materials.

Fish helper:
```js
function makeFish(color, scale) {
  const g = new THREE.Group();
  const bodyMat = new THREE.MeshStandardMaterial({color, roughness:0.35, metalness:0.1});
  const body = new THREE.Mesh(bodyGeo, bodyMat);
  body.scale.set(1, 0.62, 0.38);
  ...
}
```
Body sphere radius 1. Tail: cone radius 0.45 length 0.9, rotated so it points backward: cone points +y by default; rotate x by -90deg to point -z? Cone apex at +y top. Position at z=-1 (back), rotate x = -Math.PI/2 so apex points backward (-z)... rotateX(-PI/2): +y axis maps to -z? Rotation about x by -90°: y→ -z? Let's compute: R_x(θ): y' = y cosθ - z sinθ... vector (0,1,0) under R_x(θ) becomes (0, cosθ, sinθ). θ=-π/2 → (0,0,-1). Yes apex points -z (backward) if fish faces +z. Good: tail cone at z=-0.8, rotation.x=-π/2, scale flatten z? Cone is radially symmetric; flatten via scale.set(0.3,1,1)? After rotation, cone's local y is world -z; scale local: scale.set(0.25, 1, 1) makes it flat in x. Hmm want tail flat in x (thin sideways): yes scale.x=0.2. Wait tail fin should be vertical flat plane: thin in x, tall in y. Local cone: axis along y, circular in x-z. After rotation x=-90°, axis along -z, circular in x-y. Scale x small → thin in x. Good.

Tail pivot: put tail cone in a sub-group at z=-0.9, cone offset y=+0.45 in local (so it extends backward), then wag by rotating pivot.rotation.y.

Fins: two side fins: small flattened cones at sides angled, and a dorsal fin on top: cone flattened in z, pointing up-back.

Eyes: two white spheres with black pupils, at front (z=+0.6), y=0.15, x=±0.28.

Fish faces +z; movement orientation: group.lookAt(pos + vel) — lookAt makes -z? Object3D.lookAt orients +z? For non-camera objects, lookAt points the +z axis toward target? Actually Object3D.lookAt rotates the object so that its internal +z axis points at target for objects (cameras use -z). In three.js, Object3D.lookAt makes the object's positive z-axis point toward target (except cameras/lights which use -z). Yes: "rotates the object to face a point in world space" — for regular objects +z faces the point. So fish model facing +z works with lookAt(pos+vel).

Bounds: tank inner: x ∈ [-9,9], y ∈ [-4.2, 4.2], z ∈ [-3.5, 3.5]. If fish outside, steer toward center strongly.

Wander: each fish has heading velocity; add random small steering each frame: vel += random vec * wanderStrength * dt, clamp speed between min and max. Also gentle vertical bob.

Flee: mousePoint (Vector3) updated on pointermove; also store "mouseActive". Each frame: d = fish.pos.distanceTo(mousePoint); if d < FLEE_R (4): dir = (fish.pos - mousePoint).normalize(); urgency = 1 - d/FLEE_R; desiredVel = dir * (maxSpeed*1.8) * (0.4+0.6*urgency) plus slight up; fish.vel lerp to desired with k = urgency*6*dt... Use: fish.vel.lerp(desired, Math.min(1, urgency*5*dt)). Also scale tail wag faster when fleeing.

Also when mouse leaves window, mouseActive=false.

Ripple: ring mesh at mousePoint (clamped inside tank), animate scale up & opacity down, loop while mouse active; position ring on plane z = mousePoint.z? Just put at mousePoint with rotation facing camera (lookAt camera). Use RingGeometry, MeshBasicMaterial additive, transparent.

Bubbles: InstancedMesh sphere small, 60 bubbles; each has pos, speed; rise, wobble x; when y > top, reset to bottom near random x,z. Update via matrix compose each frame — 60 matrices fine.

Gravel: InstancedMesh of IcosahedronGeometry(0.18, 0), 250 instances, random positions on floor y=-5+small, random scale, colors via instanceColor sand palette.

Tank: glass walls: use BoxGeometry with MeshPhysicalMaterial({transparent, opacity:0.08, roughness:0.05, metalness:0, side:DoubleSide?}) Actually draw a box slightly bigger than water: edges via LineSegments with EdgesGeometry for nice frame. Glass: BoxGeometry(20,10,8) material transparent opacity 0.07, depthWrite false. Plus edge lines white-ish opacity 0.5. Base: a box/cylinder stand? Add a dark base box under tank (y=-5.6) as "cabinet" — nice look. And water surface plane at y=5 with animated opacity/waves? Simple plane with MeshPhongMaterial light blue, slight rotation animation... Use a plane with opacity 0.35 and animate position y slightly.

Background: scene.background = gradient? Use a large sphere with BackSide shader or just set clear color and fog. Use scene.fog = new THREE.Fog(0x0b3d5c, 18, 40). Background color 0x06283d. Add subtle "caustics" on floor: a plane with... skip; add a few spotlights.

God rays: 4 cones from top (CylinderGeometry radiusTop small, radiusBottom big, open-ended), MeshBasicMaterial additive, color 0x9fd8ff, opacity 0.05-0.08, depthWrite false, positioned tilted, animate rotation/opacity slowly.

Plants: make 5-7 plants: each is group of 5-9 blades; blade = TubeGeometry along curve bending, radius 0.06-0.12, color greens, MeshStandardMaterial. Sway: rotate blade group rotation.z = sin(t + phase)*0.08. Precompute curves once.

Rocks: 3-4 dodecahedron geometries gray, random scale/rotation.

Treasure chest maybe: skip or simple box+cylinder lid. Add small chest: box + half-cylinder, brown, with gold coins (small yellow cylinders)? Keep simple: a chest made of box and a slightly larger half-cylinder rotated. Optional — adds charm. I'll add it.

Lighting: HemisphereLight(0xbfdfff, 0x0a2a3a, 0.6), DirectionalLight white 1.2 from (5,10,8), PointLight inside tank top (0,6,0) cyan 0.8.

Fish colors: orange, yellow, blue, purple, pink, red, teal, white.

Fish list with per-fish params: pos random, vel random dir, speed 1-2, scale 0.5-0.9.

Also add small fish schooling? Keep 9-12 fish.

UI overlay: div absolute center: "🐟 3D Aquarium" title, subtitle "Move your mouse near the fish to scare them away" and "Click or press any key to start". On click/keydown/pointermove first time → fade out overlay (opacity 0, then display none). Also small persistent hint bottom-left that fades after 6s. Since screenshot at 3s should show interesting stuff — overlay semi-transparent so aquarium visible behind. Make overlay pointer-events none so mouse moves pass through. Good.

Touch: touchmove → update mouse point too; preventDefault? On window touchmove passive... just read touches.

Also handle pointer leaving: pointerleave on document → mouseActive=false.

Resize: update camera aspect, renderer size.

Write code carefully. Use import map exactly as given. Module script.

Edge: `three` import: `import * as THREE from 'three';` no addons needed (no OrbitControls). Good — fewer network deps.

Let me now write the full HTML.

Key snippets:

```js
import * as THREE from 'three';

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

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x07293e);
scene.fog = new THREE.Fog(0x0a3550, 16, 42);

const camera = new THREE.PerspectiveCamera(55, innerWidth/innerHeight, 0.1, 100);
camera.position.set(0, 0.5, 15);
```

Tank constants: W=20 (x -10..10), H=10 (y -5..5), D=8 (z -4..4).

Glass box: new THREE.Mesh(new THREE.BoxGeometry(20,10,8), glassMat) at origin. Edges: EdgesGeometry lines.

Water volume visual: same box with slightly smaller, material blue transparent 0.12, depthWrite false — maybe skip inner water box, use fog+background. Add subtle inner box opacity 0.05 for depth. I'll add water box (19.8, 9.8, 7.8) MeshPhongMaterial color 0x1a6fa8 transparent opacity 0.18, side: BackSide? If we render both glass and water, ordering issues. Use water box with side: THREE.BackSide so we see inside walls tinted... Actually simpler: glass box DoubleSide opacity 0.06 depthWrite false, renderOrder 10; water surface plane at top. Fog handles blue depth. Good.

Water surface: PlaneGeometry(20,8) rotated -90 x at y=4.98, MeshPhongMaterial color 0x66c7e8, transparent 0.45, shininess 80, specular white; animate via material... animate plane's rotation.z slight and position y bob. Also add second smaller plane? Fine.

Floor: PlaneGeometry(20,8) at y=-5 rotated +90 x, sand color 0xc2a878 with roughness; receiveShadow? Shadows: enable renderer.shadowMap, directional light castShadow — could be heavy but fine. I'll enable shadows lightly (only fish cast? gravel instanced receive). Keep shadows on: renderer.shadowMap.enabled = true; type PCFSoft. Directional light with shadow camera covering tank. Gravel receiveShadow. Fish castShadow. OK but instanced gravel receiving is fine.

Actually to keep perf safe and avoid shadow artifacts through glass, maybe skip shadows and fake with darker gradient. I'll enable shadows but glass doesn't cast (castShadow false). DirectionalLight position (6, 12, 6), target origin, shadow camera ortho ±12. Should be fine.

Plants along back and sides.

Now fish flee logic detail:

```js
const mouse = {x:0,y:0, active:false, world:new THREE.Vector3()};
const raycaster = new THREE.Raycaster();
const midPlane = new THREE.Plane(new THREE.Vector3(0,0,1), 0);
function updateMouse(e){
  const x = (e.clientX/innerWidth)*2-1;
  const y = -(e.clientY/innerHeight)*2+1;
  raycaster.setFromCamera({x,y}, camera);
  const p = new THREE.Vector3();
  if(raycaster.ray.intersectPlane(midPlane, p)){
    mouse.world.copy(p);
    mouse.world.x = clamp(p.x, -9.5, 9.5);
    mouse.world.y = clamp(p.y, -4.5, 4.5);
    // z keep as plane 0 — fish at various z; use distance in 3D with mouse.world.z=0. Maybe better: project onto nearest fish depth? Simple: set z=0 fine.
    mouse.active = true;
  }
}
window.addEventListener('pointermove', updateMouse);
```

Hmm, fish near front (z=3) vs mouse point z=0: distance includes z difference 3 — reduces flee. Better: for each fish, compute mouse point at fish's z: intersect ray with plane z = fish.z. Compute per fish: plane constant = -fish.z (plane normal (0,0,1), constant -z means z = fish.z). intersectPlane(plane with normal (0,0,1), constant = -fish.position.z). Plane: new THREE.Plane(new Vector3(0,0,1), -fish.z) gives z = fish.z. Then distance in that plane. That's nicer. Store ray in raycaster; per fish compute. 12 fish * plane intersect per frame = fine.

Flee radius ~ 3.2.

Fish update:

```js
const tmp = new THREE.Vector3();
for (const f of fishes){
  // flee
  plane.set(new THREE.Vector3(0,0,1), -f.pos.z); // or preallocate plane and set constant
  if(mouse.active && raycaster.ray.intersectPlane(fleePlane, tmp)){
    const d = tmp.distanceTo(f.pos);
    if(d < FLEE){
      const urg = 1 - d/FLEE;
      dir.subVectors(f.pos, tmp).normalize();
      desired.copy(dir).multiplyScalar(f.maxSpeed*2.2*(0.5+urg));
      desired.y += urg*1.2; // dart up a bit
      f.vel.lerp(desired, Math.min(1, urg*4*dt));
      f.panic = Math.max(f.panic, urg);
    }
  }
  // wander
  f.vel.x += (Math.random()-0.5)*f.wander*dt;
  ... 
  // bounds steering
  ...
  // clamp speed
  const sp = f.vel.length();
  if(sp > f.maxSpeed*(f.panic>0? 2.2:1)) f.vel.setLength(...);
  if(sp < f.minSpeed && f.panic<=0.01) f.vel.setLength(f.minSpeed);
  f.pos.addScaledVector(f.vel, dt);
  f.panic *= Math.pow(0.2, dt); // decay
  // orient
  look.copy(f.pos).add(f.vel);
  f.group.lookAt(look) — but smooth: use quaternion slerp.
  // tail wag
  f.tail.rotation.y = Math.sin(t*(6+f.panic*14))* (0.5+f.panic*0.6);
}
```

Bounds steering: if f.pos.x < -8.5 → f.vel.x += turn*dt etc. Or add force toward center when beyond margin:

```js
const m = 8.5; // x margin
if(f.pos.x < -m) f.vel.x += ( -m - f.pos.x ) * 2 * dt * 10? 
```
Simpler: f.vel.x += THREE.MathUtils.clamp((-m - f.pos.x), -1, 1) * 8 * dt when below; similarly others. Then also hard clamp position inside.

Smooth orientation: 
```js
const targetQ = new THREE.Quaternion();
const m4 = new THREE.Matrix4().lookAt(zero, f.vel.clone().normalize(), up); // Matrix4.lookAt(eye, target, up) creates rotation with -z toward target? Matrix4.lookAt makes z axis point from target to eye (i.e., -z faces target) — that's camera convention. For fish facing +z, use... 
```
Easier: use a dummy Object3D: dummy.position.copy(f.pos); dummy.lookAt(f.pos+vel); targetQ.setFromRotationMatrix(dummy.matrix)... dummy.lookAt updates rotation; then f.group.quaternion.slerp(dummy.quaternion, 1 - Math.pow(0.001, dt))? Use factor = 1 - Math.exp(-6*dt). Fine.

Fish model faces +z as designed, lookAt makes +z face target. Good.

Bubbles: 
```js
const bubbleGeo = new THREE.SphereGeometry(0.09, 8, 6);
const bubbleMat = new THREE.MeshStandardMaterial({color:0xdff6ff, transparent:true, opacity:0.55, roughness:0.1});
const bubbles = new THREE.InstancedMesh(bubbleGeo, bubbleMat, 70);
```
Each bubble: {x,y,z,speed,phase}. Update: y += speed*dt; x += sin(t*2+phase)*0.3*dt; if y>4.6 reset y=-4.8, random x,z. Compose matrix with scale varying 0.5-1.5.

Gravel instanced with instanceColor:
```js
const gravel = new THREE.InstancedMesh(geo, mat, count);
const c = new THREE.Color();
for(...){ matrix compose; gravel.setColorAt(i, c.setHSL(0.09+rand*0.05, 0.35+rand*0.2, 0.45+rand*0.2)); }
```

Plants: 
```js
function makePlant(x, z, height, hue){
  const g = new THREE.Group();
  const n = 6;
  for(let i=0;i<n;i++){
    const h = height*(0.6+Math.random()*0.5);
    const pts = [];
    const bend = (Math.random()-0.5)*1.2;
    for(let s=0;s<=5;s++){
      const t=s/5;
      pts.push(new THREE.Vector3(Math.sin(t*2+bend)*bend*0.4*t, h*t, Math.cos(t*bend)*bend*0.3*t));
    }
    const curve = new THREE.CatmullRomCurve3(pts);
    const blade = new THREE.Mesh(new THREE.TubeGeometry(curve, 8, 0.05+Math.random()*0.05, 5), plantMat(hue...));
    g.add(blade);
  }
  g.position.set(x, -5, z);
  return g;
}
```
Sway: store plants with blades; animate each group rotation.z = sin(t*0.8+phase)*0.06, rotation.x similar. Since blades curve, rotating group works.

Plant material: MeshStandardMaterial color green variations, roughness 0.6. Maybe use vertexColors... just multiple materials via hue per plant: create material per plant with HSL.

God rays: 
```js
const rayGeo = new THREE.CylinderGeometry(0.6, 2.6, 16, 12, 1, true);
const rayMat = new THREE.MeshBasicMaterial({color:0x9fdcff, transparent:true, opacity:0.06, blending:THREE.AdditiveBlending, side:THREE.DoubleSide, depthWrite:false});
```
4 rays at x=-6,-2,2,6, tilted rotation.z ~0.15, y=0, renderOrder 5. Animate opacity = 0.04+0.03*sin(t*0.5+i).

Chest: group at (5,-4.6,-2): base box 1.6x0.8x0.9 brown; lid: CylinderGeometry half? Use cylinder rotated: CylinderGeometry(0.45,0.45,1.6,12,1,false,0,Math.PI) rotated z 90 to lie along x, positioned y+0.4+... simpler: BoxGeometry lid slightly larger rotated 0 + gold trim: torus? Keep: base box, lid = box(1.7,0.35,1.0) at y=0.55, plus gold bands: two thin boxes yellow. Fine.

Rocks: DodecahedronGeometry(0.7) gray with random scale, positions.

Also add a small "anchor" or ship? skip.

Camera parallax: 
```js
camTarget.x = mouse.nx * 1.2; camTarget.y = 0.5 - mouse.ny*0.8;
camera.position.lerp(...) ; camera.lookAt(0,0,0);
```
Store normalized mouse nx, ny.

Overlay HTML:
```html
<div id="overlay">
  <div class="card">
    <h1>🐠 3D Aquarium</h1>
    <p>Move your mouse near the fish — they'll swim away!</p>
    <p class="dim">Click or press any key to start</p>
  </div>
</div>
```
Style: fixed inset 0, display flex center, background radial gradient subtle rgba(3,20,32,0.35), pointer-events none, transition opacity 0.8s. On first pointerdown/keydown/pointermove → add class hidden.

Also bottom hint: `<div id="hint">🖱️ move near fish to scare them</div>` bottom center, fades after 8s via CSS animation.

First paint meaningful: yes, scene renders immediately, fish swimming.

Touch support: pointermove covers touch (pointer events fire for touch drag). Also add touchmove fallback. And on touch devices overlay says "tap and drag". Detect 'ontouchstart' → change text. Fine, minor.

Now write final code. Careful with three r170 API: InstancedMesh setColorAt exists. Fog fine. `renderer.outputColorSpace` default SRGB in r170 — fine. Lights: useLegacyLights default false in r170? In r152+, renderer.useLegacyLights default false, so intensities need to be physical-ish: directional intensity ~2-3 works with standard materials. HemisphereLight intensity 0.6-1 fine. I'll set: hemi 0.9, dir 2.2, point 30 (point light physical units — with distance/decay 2 default, intensity needs to be high, like 40). PointLight(color, intensity, distance, decay): with decay 2, intensity in candela; for radius ~10, intensity 30-60. I'll use 40.

MeshStandardMaterial fine.

Shadow: dir light castShadow with camera bounds ±13, mapSize 1024. Fish castShadow=true (meshes). Gravel receive. Floor receive. Might have acne; set bias -0.0005. OK.

Let me also add subtle floating particles (plankton): Points with small white dots drifting — nice. 200 points in tank volume, PointsMaterial size 0.05, transparent 0.5, additive. Animate by moving whole points group slowly or per-point via position attribute update — just rotate group slowly and bob.

Write fish creation with shared geometries:

```js
const bodyGeo = new THREE.SphereGeometry(0.6, 20, 14);
const tailGeo = new THREE.ConeGeometry(0.42, 1.0, 4); // 4 radial segments for fin look? cone with 4 segments looks like flat diamond... use 16.
```
Tail as cone flattened: scale after rotation. I'll make tail pivot group at z=-0.55; cone position y=0.5 (local), rotation.x=-PI/2, scale.set(0.15,1,1)... wait after rotation.x=-π/2, local y→ world -z; local x stays x, local z → y? R_x(-90): (0,0,1)→(0,1,0)? Compute R_x(θ) on (0,0,1): (0, -sinθ, cosθ). θ=-π/2 → (0, 1, 0). So local z→world +y. Cone circular cross-section in local x-z becomes x-y plane → flat in... we scale local x by 0.15 → thin in world x. Good, tail fin vertical plane. 

Dorsal fin: cone at top: position y=0.35, z=-0.1, rotation.x = -PI/2 * 0.7? Just cone pointing up-back: rotation.x = -0.6 (tilt back), scale.set(0.12, 1, 0.6)? Eh: cone default points +y; to point up and slightly back, rotation.x = 0.5 (tilt toward -z? R_x positive rotates y toward z...). Keep simple: dorsal fin = flattened cone: rotation.x = Math.PI/2 * 0.85? Let me just: dorsal = cone at (0, 0.32, -0.15), rotation.x = -2.2 (pointing back-up), scale.set(0.15,1,0.5). Hmm risk of looking odd. Alternative dorsal: use a flattened triangle shape via cone with 3 segments? I'll do: dorsal fin cone, rotation.x = -Math.PI/2 + 0.5 → points mostly backward slightly up; position y=0.25. scale x 0.12 (thin sideways), z... after rotation local z maps toward y partially. It'll look fin-ish. Fine.

Side fins: two cones at (±0.32, -0.05, 0.1), rotation.z = ±1.2, rotation.x=-PI/2ish, scale thin. I'll make them: cone radius 0.25 length 0.6, rotation.x=-PI/2 (points back), rotation.z=±0.9, position (±0.3, -0.08, 0.05), scale.set(0.12,1,1). Flap animation: rotation.z base ±0.9 + sin small.

Eyes: white sphere r=0.11 at (±0.22, 0.12, 0.38); pupil black r=0.05 at (±0.22,0.12,0.47). Body radius 0.6 scaled z 0.38 → front z extent 0.6*? body scale: scale.set(0.55, 0.75, 1) meaning x radius 0.33, y 0.45, z 0.6. Eyes at z=0.42 x=±0.2 y=0.15 on surface. ok approximate; slight embed fine.

Mouth: skip.

Fish group scale overall per fish.

Colors: [0xff8c42 orange, 0xffd93d yellow, 0x4dabf7 blue, 0xb197fc purple, 0xff6b9d pink, 0x69db7c green, 0xff6b6b red, 0x66d9e8 cyan, 0xf8f9fa white-ish? maybe 0xffe066]. Also give some fish stripes? Could add a slightly darker top: skip; add tail/fins slightly different color (lighter/darker) via material per part: finMat color = color offset lighter. Use `new THREE.Color(color).offsetHSL(0, 0.1, 0.15)`.

Count 11 fish.

Now the animate loop with clock delta clamped to 0.05.

Ripple ring: 
```js
const ripple = new THREE.Mesh(new THREE.RingGeometry(0.4, 0.55, 32), new THREE.MeshBasicMaterial({color:0xbfeaff, transparent:true, opacity:0, side:DoubleSide, blending:Additive, depthWrite:false}));
```
Each frame if mouse.active: ripple.position.copy(mouse.world at z maybe 0.5 toward camera?) place at plane z=0.5? Place at mouse.world (z=0 plane) but fish flee uses per-fish plane; ripple at z=0 fine. Actually better visual: put ripple where ray hits, at z=0. Animate: rippleT += dt; scale = 0.5 + rippleT*3; opacity = max(0, 0.5 - rippleT*0.8); reset rippleT=0 when >0.8. rotation facing camera: ripple.lookAt(camera.position).

Also maybe a second ring delayed. Keep one.

Edge case: intersectPlane returns null if parallel — guard.

Now overlay dismissal: 
```js
let started=false;
function start(){ if(!started){started=true; overlay.classList.add('hidden'); } }
window.addEventListener('pointerdown', start);
window.addEventListener('keydown', start);
window.addEventListener('pointermove', ()=>{ start(); updateMouse... });
```
But pointermove fires constantly — start once is fine.

Hmm: "Click or press a key to start" required for keyboard games; here keyboard does nothing, but including message is harmless and matches instructions. Keep overlay visible until interaction OR auto-fade after 4s? Screenshot at 3s — overlay semi transparent so scene visible. I'll auto-fade overlay after 5s too (add hidden via setTimeout 6000) so it never blocks view. Actually keep it until first interaction, but make it subtle (small card, translucent). The requirement: "Something meaningful must be visible on first paint" — the aquarium behind translucent overlay counts. I'll make overlay background very light (rgba(2,18,30,0.25)) and card compact. Also auto-dismiss after 6 seconds regardless. Good.

Hint at bottom: "🖱️ Move your cursor near the fish to scare them" — fades out after 9s (CSS animation forwards). pointer-events none.

Mobile: media query for card font sizes. Viewport meta.

Prevent scroll on touch: body overflow hidden, touch-action none on canvas.

Let me now also handle WebGL fail? Assume fine.

Write the code now, complete.

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>3D Aquarium — Scare the Fish</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { width:100%; height:100%; overflow:hidden; background:#06202f; }
  body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; }
  canvas { display:block; touch-action:none; }
  #overlay { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; pointer-events:none; background:radial-gradient(ellipse at center, rgba(4,26,40,.15), rgba(2,14,24,.45)); transition:opacity .9s ease; z-index:10; }
  #overlay.hidden { opacity:0; }
  .card { text-align:center; padding:22px 34px; border-radius:18px; background:rgba(6,32,47,.55); border:1px solid rgba(140,220,255,.25); backdrop-filter: blur(6px); color:#dff3ff; box-shadow:0 8px 40px rgba(0,0,0,.35); animation: float 3.5s ease-in-out infinite; max-width: min(88vw, 480px); }
  .card h1 { font-size: clamp(22px, 4.5vw, 34px); letter-spacing:.5px; margin-bottom:8px; text-shadow:0 2px 12px rgba(80,200,255,.5); }
  .card p { font-size: clamp(13px, 2.6vw, 16px); opacity:.9; }
  .card .dim { margin-top:6px; font-size: clamp(11px, 2.2vw, 13px); opacity:.6; animation: pulse 1.6s ease-in-out infinite; }
  @keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} }
  @keyframes pulse { 0%,100%{opacity:.35} 50%{opacity:.75} }
  #hint { position:fixed; left:50%; bottom:18px; transform:translateX(-50%); color:#cfeeff; background:rgba(6,32,47,.6); border:1px solid rgba(140,220,255,.25); padding:8px 16px; border-radius:999px; font-size: clamp(11px, 2.4vw, 14px); pointer-events:none; z-index:9; white-space:nowrap; animation: hintfade 1s ease 9s forwards; }
  @keyframes hintfade { to { opacity:0; visibility:hidden; } }
</style>
</head>
<body>
<div id="overlay"><div class="card">
  <h1>🐠 3D Aquarium</h1>
  <p>Move your mouse close to a fish and it will dart away!</p>
  <p class="dim">Click or press any key to start</p>
</div></div>
<div id="hint">🖱️ Move near the fish to scare them</div>
<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>
<script type="module">
import * as THREE from 'three';
...
</script>
</body>
</html>
```

Now the module script in full.

```js
import * as THREE from 'three';

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

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x082a40);
scene.fog = new THREE.Fog(0x0a3a58, 15, 40);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 120);
camera.position.set(0, 0.6, 15.5);

// tank bounds
const TANK = { x: 9.4, yMin: -4.4, yMax: 4.3, z: 3.4 };

// ---------- lights ----------
scene.add(new THREE.HemisphereLight(0xcfeaff, 0x0a2f42, 1.0));
const sun = new THREE.DirectionalLight(0xffffff, 2.4);
sun.position.set(6, 14, 8);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
sun.shadow.camera.left = -13; sun.shadow.camera.right = 13;
sun.shadow.camera.top = 13; sun.shadow.camera.bottom = -13;
sun.shadow.camera.far = 40;
sun.shadow.bias = -0.0006;
scene.add(sun);
const glow = new THREE.PointLight(0x7fd4ff, 60, 30, 2);
glow.position.set(0, 6.5, 0);
scene.add(glow);
```

Hmm point light intensity 60 with decay 2 at distance ~8 → 60/64 ≈ 0.94. ok.

Tank:

```js
// glass
const glassMat = new THREE.MeshPhysicalMaterial({ color:0x9fd8ef, transparent:true, opacity:0.10, roughness:0.05, metalness:0, side:THREE.DoubleSide, depthWrite:false });
const glass = new THREE.Mesh(new THREE.BoxGeometry(20, 10, 8), glassMat);
glass.renderOrder = 20;
scene.add(glass);
const edges = new THREE.LineSegments(new THREE.EdgesGeometry(glass.geometry), new THREE.LineBasicMaterial({ color:0xbfe9ff, transparent:true, opacity:0.55 }));
scene.add(edges);

// water surface
const surface = new THREE.Mesh(new THREE.PlaneGeometry(19.6, 7.6), new THREE.MeshPhongMaterial({ color:0x5fc4e8, transparent:true, opacity:0.4, shininess:120, specular:0xaaddff, side:THREE.DoubleSide }));
surface.rotation.x = -Math.PI/2;
surface.position.y = 4.95;
scene.add(surface);

// floor sand
const sand = new THREE.Mesh(new THREE.PlaneGeometry(20, 8), new THREE.MeshStandardMaterial({ color:0xc9b083, roughness:0.95 }));
sand.rotation.x = -Math.PI/2; // wait plane default faces +z; rotate -90x to face up: rotation.x = -PI/2 gives normal +y. yes.
sand.position.y = -5;
sand.receiveShadow = true;
scene.add(sand);
```
Wait plane rotated -π/2 about x: normal (0,0,1)→(0,1,0)? R_x(-90): (0,0,1)→(0, -sin(-90), cos(-90)) = (0, 1, 0)? Using earlier formula (0,0,1)→(0,-sinθ,cosθ), θ=-π/2 → (0, 1, 0). Yes faces up. Good.

Cabinet base:
```js
const base = new THREE.Mesh(new THREE.BoxGeometry(22, 1.2, 10), new THREE.MeshStandardMaterial({ color:0x3a2c22, roughness:0.8 }));
base.position.y = -5.6;
scene.add(base);
const baseTop = new THREE.Mesh(new THREE.BoxGeometry(21, 0.25, 9), new THREE.MeshStandardMaterial({ color:0x54402f, roughness:0.7 }));
baseTop.position.y = -4.95; // sits on sand? sand at -5, top of tank bottom at -5. baseTop at -4.95 slightly above... fine visually as rim.
scene.add(baseTop);
```

Gravel:
```js
const gravelGeo = new THREE.IcosahedronGeometry(0.16, 0);
const gravelMat = new THREE.MeshStandardMaterial({ roughness:0.9, flatShading:true });
const GRAVEL_N = 260;
const gravel = new THREE.InstancedMesh(gravelGeo, gravelMat, GRAVEL_N);
gravel.receiveShadow = true;
const m4 = new THREE.Matrix4(), q = new THREE.Quaternion(), s = new THREE.Vector3(), p = new THREE.Vector3();
const col = new THREE.Color();
for (let i=0;i<GRAVEL_N;i++){
  p.set((Math.random()-0.5)*19, -4.92 + Math.random()*0.12, (Math.random()-0.5)*7.4);
  q.setFromEuler(new THREE.Euler(Math.random()*3, Math.random()*3, Math.random()*3));
  const sc = 0.5 + Math.random()*1.4;
  s.set(sc, sc*(0.5+Math.random()*0.6), sc);
  m4.compose(p, q, s);
  gravel.setMatrixAt(i, m4);
  col.setHSL(0.08 + Math.random()*0.07, 0.35 + Math.random()*0.25, 0.42 + Math.random()*0.22);
  gravel.setColorAt(i, col);
}
scene.add(gravel);
```

Plants:
```js
const plants = [];
function makePlant(x, z, h, hue){
  const g = new THREE.Group();
  const mat = new THREE.MeshStandardMaterial({ color: new THREE.Color().setHSL(hue, 0.65, 0.32), roughness:0.55 });
  const n = 5 + Math.floor(Math.random()*4);
  for (let i=0;i<n;i++){
    const bh = h * (0.55 + Math.random()*0.55);
    const bend = (Math.random()-0.5) * 1.6;
    const lean = (Math.random()-0.5)*0.5;
    const pts = [];
    for (let k=0;k<=6;k++){
      const t = k/6;
      pts.push(new THREE.Vector3(lean*t + Math.sin(t*2.2 + bend)*0.35*bend*t, bh*t, Math.cos(t*bend)*0.3*bend*t));
    }
    const curve = new THREE.CatmullRomCurve3(pts);
    const blade = new THREE.Mesh(new THREE.TubeGeometry(curve, 10, 0.045 + Math.random()*0.05, 5), mat);
    blade.castShadow = true;
    g.add(blade);
  }
  g.position.set(x, -5, z);
  g.userData.phase = Math.random()*10;
  scene.add(g);
  plants.push(g);
}
makePlant(-7.5, -2.5, 3.4, 0.33);
makePlant(-6.3, -3.0, 2.4, 0.36);
makePlant(7.6, -2.6, 3.8, 0.30);
makePlant(6.4, -3.2, 2.6, 0.38);
makePlant(-8.2, 0.5, 2.8, 0.34);
makePlant(8.4, 1.2, 3.0, 0.31);
makePlant(0.5, -3.4, 2.0, 0.37);
```

Rocks:
```js
const rockMat = new THREE.MeshStandardMaterial({ color:0x6b7a86, roughness:0.9, flatShading:true });
[[-4.5,-3.1,1.1],[3.8,-3.3,0.8],[-1.5,-3.5,0.6],[5.6,-2.2,1.4]].forEach(([x,z,r])=>{
  const rk = new THREE.Mesh(new THREE.DodecahedronGeometry(r, 0), rockMat);
  rk.position.set(x, -5 + r*0.55, z);
  rk.rotation.set(Math.random()*3, Math.random()*3, Math.random()*3);
  rk.castShadow = true; rk.receiveShadow = true;
  scene.add(rk);
});
```

Chest:
```js
const chest = new THREE.Group();
const wood = new THREE.MeshStandardMaterial({ color:0x7a4a26, roughness:0.7 });
const woodDark = new THREE.MeshStandardMaterial({ color:0x5d3618, roughness:0.75 });
const gold = new THREE.MeshStandardMaterial({ color:0xffd34d, metalness:0.8, roughness:0.3 });
const cb = new THREE.Mesh(new THREE.BoxGeometry(1.7, 0.85, 0.95), woodDark); cb.position.y = 0.42;
const lid = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.4, 1.05), wood); lid.position.y = 0.95;
const band1 = new THREE.Mesh(new THREE.BoxGeometry(0.18, 1.25, 1.0), gold); band1.position.set(-0.5, 0.6, 0);
const band2 = band1.clone(); band2.position.x = 0.5;
chest.add(cb, lid, band1, band2);
chest.position.set(4.6, -4.55, -1.8);
chest.rotation.y = -0.5;
chest.traverse(o=>{ if(o.isMesh){o.castShadow=true;} });
scene.add(chest);
```

God rays:
```js
const rays = [];
const rayGeo = new THREE.CylinderGeometry(0.5, 3.2, 18, 12, 1, true);
for (let i=0;i<5;i++){
  const mat = new THREE.MeshBasicMaterial({ color:0xaee2ff, transparent:true, opacity:0.05, blending:THREE.AdditiveBlending, side:THREE.DoubleSide, depthWrite:false });
  const ray = new THREE.Mesh(rayGeo, mat);
  ray.position.set(-7 + i*3.5, 0, -1 + (i%2));
  ray.rotation.z = 0.18;
  ray.rotation.x = (i-2)*0.05;
  ray.renderOrder = 5;
  scene.add(ray);
  rays.push(mat);
}
```

Bubbles:
```js
const BUB_N = 70;
const bubbleGeo = new THREE.SphereGeometry(0.09, 8, 6);
const bubbleMat = new THREE.MeshStandardMaterial({ color:0xe6f7ff, transparent:true, opacity:0.5, roughness:0.15 });
const bubbles = new THREE.InstancedMesh(bubbleGeo, bubbleMat, BUB_N);
bubbles.renderOrder = 6;
scene.add(bubbles);
const bub = [];
for (let i=0;i<BUB_N;i++) bub.push({ x:(Math.random()-0.5)*18, y:-5+Math.random()*10, z:(Math.random()-0.5)*7, v:0.5+Math.random()*0.9, ph:Math.random()*10, sc:0.4+Math.random()*1.1 });
```

Plankton points:
```js
const PN = 220;
const pGeo = new THREE.BufferGeometry();
const pPos = new Float32Array(PN*3);
for (let i=0;i<PN;i++){ pPos[i*3]=(Math.random()-0.5)*19; pPos[i*3+1]=-4.8+Math.random()*9.5; pPos[i*3+2]=(Math.random()-0.5)*7.4; }
pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
const points = new THREE.Points(pGeo, new THREE.PointsMaterial({ color:0xbfeaff, size:0.06, transparent:true, opacity:0.5, blending:THREE.AdditiveBlending, depthWrite:false, sizeAttenuation:true }));
scene.add(points);
```

Fish:

```js
const bodyGeo = new THREE.SphereGeometry(0.6, 24, 16);
const tailGeo = new THREE.ConeGeometry(0.45, 1.1, 16);
const finGeo = new THREE.ConeGeometry(0.26, 0.7, 12);
const eyeGeo = new THREE.SphereGeometry(0.115, 10, 8);
const pupilGeo = new THREE.SphereGeometry(0.055, 8, 6);

const FISH_COLORS = [0xff8c42, 0xffd23f, 0x4dabf7, 0xb197fc, 0xff6b9d, 0x63e6a4, 0xff6b6b, 0x66d9e8, 0xf8a5c2, 0xffb347, 0x7bdff2];

function makeFish(color, scale){
  const g = new THREE.Group();
  const bodyMat = new THREE.MeshStandardMaterial({ color, roughness:0.35, metalness:0.15 });
  const finColor = new THREE.Color(color).offsetHSL(0, 0.05, 0.18);
  const finMat = new THREE.MeshStandardMaterial({ color: finColor, roughness:0.45, metalness:0.1, transparent:true, opacity:0.9 });

  const body = new THREE.Mesh(bodyGeo, bodyMat);
  body.scale.set(0.55, 0.78, 1.05);
  body.castShadow = true;
  g.add(body);

  const tailPivot = new THREE.Group();
  tailPivot.position.z = -0.55;
  const tail = new THREE.Mesh(tailGeo, finMat);
  tail.position.y = 0.55;
  tail.rotation.x = -Math.PI/2;
  tail.scale.set(0.16, 1, 1);
  tail.castShadow = true;
  tailPivot.add(tail);
  g.add(tailPivot);

  const dorsal = new THREE.Mesh(finGeo, finMat);
  dorsal.position.set(0, 0.42, -0.1);
  dorsal.rotation.x = -2.4;
  dorsal.scale.set(0.14, 1, 0.7);
  g.add(dorsal);

  const finL = new THREE.Mesh(finGeo, finMat);
  finL.position.set(0.3, -0.1, 0.05);
  finL.rotation.set(-Math.PI/2, 0, 1.1);
  finL.scale.set(0.12, 1, 1);
  g.add(finL);
  const finR = finL.clone();
  finR.position.x = -0.3;
  finR.rotation.z = -1.1;
  g.add(finR);

  const eyeMat = new THREE.MeshStandardMaterial({ color:0xffffff, roughness:0.2 });
  const pupMat = new THREE.MeshStandardMaterial({ color:0x111111, roughness:0.2 });
  const eL = new THREE.Mesh(eyeGeo, eyeMat); eL.position.set(0.21, 0.14, 0.42);
  const eR = eL.clone(); eR.position.x = -0.21;
  const pL = new THREE.Mesh(pupilGeo, pupMat); pL.position.set(0.21, 0.14, 0.51);
  const pR = pL.clone(); pR.position.x = -0.21;
  g.add(eL, eR, pL, pR);

  g.scale.setScalar(scale);
  return { group:g, tail:tailPivot, finL, finR };
}
```

Wait body scale: sphere r 0.6, scale (0.55,0.78,1.05) → x extent 0.33, y 0.47, z 0.63. Eyes at z 0.42-0.51 — surface at that x,y? At (0.21,0.14,?) the ellipsoid: (0.21/0.33)^2+(0.14/0.47)^2 = 0.405+0.089=0.494 → z = 0.63*sqrt(0.506)=0.448. So eye at z=0.42 sits slightly inside, pupil at 0.51 slightly outside. Eye sphere r 0.115 centered 0.42 spans 0.305..0.535 — pokes out. Good enough visually.

Fish list:
```js
const fishes = [];
for (let i=0;i<11;i++){
  const f = makeFish(FISH_COLORS[i % FISH_COLORS.length], 0.55 + Math.random()*0.5);
  const pos = new THREE.Vector3((Math.random()-0.5)*16, -3.5+Math.random()*7, (Math.random()-0.5)*6);
  const vel = new THREE.Vector3(Math.random()-0.5, (Math.random()-0.5)*0.4, Math.random()-0.5).normalize().multiplyScalar(1+Math.random());
  f.group.position.copy(pos);
  scene.add(f.group);
  fishes.push({ ...f, pos, vel, maxSpeed: 1.2 + Math.random()*1.1, minSpeed: 0.5, wander: 1.6, panic: 0 });
}
```

Mouse:
```js
const raycaster = new THREE.Raycaster();
const mouseNDC = new THREE.Vector2(0,0);
const mouseWorld = new THREE.Vector3();
const mouse = { active:false };
function onPointer(e){
  const cx = e.clientX ?? (e.touches && e.touches[0].clientX);
  const cy = e.clientY ?? (e.touches && e.touches[0].clientY);
  if (cx === undefined) return;
  mouseNDC.set((cx/window.innerWidth)*2-1, -(cy/window.innerHeight)*2+1);
  raycaster.setFromCamera(mouseNDC, camera);
  const p = new THREE.Vector3();
  if (raycaster.ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0,0,1), 0), p)){
    mouseWorld.copy(p);
    mouse.active = true;
  }
}
```

Flee plane per fish: preallocate `const fleePlane = new THREE.Plane(new THREE.Vector3(0,0,1), 0);` and each frame set `fleePlane.constant = -f.pos.z;`.

Update loop:

```js
const clock = new THREE.Clock();
const desired = new THREE.Vector3();
const tmpV = new THREE.Vector3();
const lookV = new THREE.Vector3();
const dummy = new THREE.Object3D();
const FLEE_R = 3.4;

function animate(){
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;

  // plants sway
  for (const pl of plants){
    pl.rotation.z = Math.sin(t*0.9 + pl.userData.phase) * 0.07;
    pl.rotation.x = Math.cos(t*0.7 + pl.userData.phase) * 0.05;
  }

  // god rays shimmer
  rays.forEach((mat, i)=>{ mat.opacity = 0.035 + 0.03*(0.5+0.5*Math.sin(t*0.6 + i*1.7)); });

  // surface bob
  surface.position.y = 4.95 + Math.sin(t*0.8)*0.06;
  surface.rotation.z = Math.sin(t*0.4)*0.02;

  // bubbles
  for (let i=0;i<BUB_N;i++){
    const b = bub[i];
    b.y += b.v*dt;
    b.x += Math.sin(t*2 + b.ph)*0.25*dt;
    if (b.y > 4.7){ b.y = -4.9; b.x = (Math.random()-0.5)*18; b.z = (Math.random()-0.5)*7; }
    p.set(b.x, b.y, b.z);
    s.setScalar(b.sc * (0.7 + 0.3*Math.sin(t*3+b.ph)));
    m4.compose(p, q.identity(), s);
    bubbles.setMatrixAt(i, m4);
  }
  bubbles.instanceMatrix.needsUpdate = true;

  // plankton drift
  points.rotation.y = Math.sin(t*0.05)*0.2;
  points.position.y = Math.sin(t*0.3)*0.2;

  // fish
  const maxBoost = 2.3;
  for (const f of fishes){
    let fleeing = false;
    if (mouse.active){
      fleePlane.constant = -f.pos.z;
      if (raycaster.ray.intersectPlane(fleePlane, tmpV)){
        const d = tmpV.distanceTo(f.pos);
        if (d < FLEE_R){
          fleeing = true;
          const urg = 1 - d/FLEE_R;
          desired.subVectors(f.pos, tmpV).normalize().multiplyScalar(f.maxSpeed*maxBoost*(0.45+0.55*urg));
          desired.y += urg*1.4;
          f.vel.lerp(desired, Math.min(1, urg*5*dt + 0.02));
          f.panic = Math.min(1, f.panic + urg*3*dt);
        }
      }
    }
    if (!fleeing){
      // wander
      f.vel.x += (Math.random()-0.5)*f.wander*dt;
      f.vel.y += (Math.random()-0.5)*f.wander*0.5*dt;
      f.vel.z += (Math.random()-0.5)*f.wander*dt;
      // gentle pull to mid water
      f.vel.y += (-f.pos.y*0.15)*dt;
      f.panic = Math.max(0, f.panic - 2.5*dt);
      // bounds steering
      const steer = 14*dt;
      if (f.pos.x < -TANK.x) f.vel.x += (-TANK.x - f.pos.x)*steer*2;
      if (f.pos.x >  TANK.x) f.vel.x -= (f.pos.x - TANK.x)*steer*2;
      if (f.pos.y < TANK.yMin) f.vel.y += (TANK.yMin - f.pos.y)*steer*2;
      if (f.pos.y > TANK.yMax) f.vel.y -= (f.pos.y - TANK.yMax)*steer*2;
      if (f.pos.z < -TANK.z) f.vel.z += (-TANK.z - f.pos.z)*steer*2;
      if (f.pos.z >  TANK.z) f.vel.z -= (f.pos.z - TANK.z)*steer*2;
      // speed clamp
      const sp = f.vel.length();
      const cap = f.maxSpeed * (f.panic > 0.05 ? maxBoost : 1);
      if (sp > cap) f.vel.setLength(cap);
      else if (sp < f.minSpeed && f.panic <= 0.05) f.vel.setLength(f.minSpeed);
    }

    f.pos.addScaledVector(f.vel, dt);
    // hard clamp inside tank
    f.pos.x = THREE.MathUtils.clamp(f.pos.x, -TANK.x, TANK.x);
    f.pos.y = THREE.MathUtils.clamp(f.pos.y, TANK.yMin, TANK.yMax);
    f.pos.z = THREE.MathUtils.clamp(f.pos.z, -TANK.z, TANK.z);
    f.group.position.copy(f.pos);

    // smooth orientation along velocity
    if (f.vel.lengthSq() > 0.0001){
      dummy.position.copy(f.pos);
      lookV.copy(f.pos).add(f.vel);
      dummy.lookAt(lookV);
      f.group.quaternion.slerp(dummy.quaternion, 1 - Math.exp(-5*dt));
    }

    // tail wag (faster when panicked)
    const wag = Math.sin(t*(7 + f.panic*16) + f.vel.length()*2);
    f.tail.rotation.y = wag * (0.45 + f.panic*0.5);
    f.finL.rotation.z = 1.1 + Math.sin(t*9)*0.25;
    f.finR.rotation.z = -1.1 - Math.sin(t*9)*0.25;
  }

  // ripple at mouse point
  if (mouse.active){
    ripple.visible = true;
    ripple.position.copy(mouseWorld);
    ripple.lookAt(camera.position);
    rippleT += dt;
    const k = rippleT % 1.1;
    const sc = 0.6 + k*3.2;
    ripple.scale.setScalar(sc);
    ripple.material.opacity = Math.max(0, 0.55 - k*0.55);
  } else {
    ripple.visible = false;
  }

  // camera parallax
  const camTX = mouseNDC.x * 1.4;
  const camTY = 0.6 - mouseNDC.y * 0.9;
  camera.position.x += (camTX - camera.position.x) * (1 - Math.exp(-2*dt));
  camera.position.y += (camTY - camera.position.y) * (1 - Math.exp(-2*dt));
  camera.position.z = 15.5;
  camera.lookAt(0, 0, 0);

  renderer.render(scene, camera);
}
```

Wait: when fleeing, speed not clamped — vel lerps toward desired of magnitude up to maxSpeed*2.3; fine, but over multiple frames could exceed slightly; clamp anyway outside: after both branches clamp to maxSpeed*maxBoost always. I'll restructure: always clamp sp to cap where cap depends on panic; but during flee we want high speed. Set cap = f.maxSpeed*maxBoost always (wander won't reach it). And min clamp only when panic low. Simplify:

```js
const sp = f.vel.length();
if (sp > f.maxSpeed*maxBoost) f.vel.setLength(f.maxSpeed*maxBoost);
if (sp < f.minSpeed && f.panic <= 0.05) f.vel.setLength(f.minSpeed);
```
Do this always (remove inner clamps). Good.

Ripple init:
```js
const ripple = new THREE.Mesh(
  new THREE.RingGeometry(0.42, 0.55, 40),
  new THREE.MeshBasicMaterial({ color:0xcdeeff, transparent:true, opacity:0, side:THREE.DoubleSide, blending:THREE.AdditiveBlending, depthWrite:false })
);
ripple.renderOrder = 7;
let rippleT = 0;
scene.add(ripple);
```

Events:
```js
const overlay = document.getElementById('overlay');
let started = false;
function start(){
  if (!started){ started = true; overlay.classList.add('hidden'); }
}
window.addEventListener('pointerdown', () => { start(); onPointerMove... });
```
Combine: 
```js
window.addEventListener('pointermove', (e)=>{ start(); onPointer(e); });
window.addEventListener('pointerdown', (e)=>{ start(); onPointer(e); });
window.addEventListener('keydown', start);
window.addEventListener('touchmove', (e)=>{ start(); onPointer(e); }, { passive:true });
document.addEventListener('pointerleave', ()=>{ mouse.active = false; });
window.addEventListener('blur', ()=>{ mouse.active = false; });
setTimeout(start, 7000); // auto-dismiss
```

Hmm auto-dismiss at 7s; screenshot at 3s shows overlay — fine, translucent.

Also on touch devices change hint text: if ('ontouchstart' in window) hint.textContent = '👆 Touch & drag near the fish to scare them'; and card text maybe. Do it.

Resize:
```js
window.addEventListener('resize', ()=>{
  camera.aspect = window.innerWidth/window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
```

Start animate().

One concern: `raycaster.setFromCamera(mouseNDC, camera)` with Vector2 fine.

fleePlane: `const fleePlane = new THREE.Plane(new THREE.Vector3(0,0,1), 0);` constant semantics: plane: normal·p + constant = 0 → z + constant = 0 → z = -constant. To get z = f.pos.z, constant = -f.pos.z. ✓.

Also, initial mouse.active=false so fish just wander — good for first paint.

q.identity() each bubble compose — q is reused quaternion; identity fine.

Check fin rotation: finL rotation.set(-Math.PI/2, 0, 1.1): cone axis +y rotated... order XYZ. Eh, visual detail; acceptable.

Dorsal rotation.x = -2.4 rad: cone points +y; R_x(-2.4): (0,1,0)→(0,cos(-2.4), sin(-2.4)) = (0,-0.737,-0.675) → points down-back. Wrong! Want up-back: (0, cos, sin) with θ positive small... For up-back (y+, z-): need sinθ negative → θ negative gives z=-sin? R_x(θ) on (0,1,0) = (0, cosθ, sinθ). θ=-2.4: (0, -0.737, -0.675) down-back. θ = -0.7: (0, 0.765, -0.644) up-back. So dorsal.rotation.x = -0.7. Let me set dorsal rotation.x = -0.75, position (0, 0.4, -0.05), scale (0.14, 1, 0.6). Tail: rotation.x=-π/2 gives (0,1,0)→(0,0,-1) backward ✓ (computed earlier (0,cos(-90),sin(-90)) = (0,0,-1) ✓).

Side fins: want pointing out-down-back. finL at +x: cone +y rotated z by +1.1: R_z(1.1) on (0,1,0) = (-sin1.1, cos1.1, 0) = (-0.89, 0.45, 0) → points -x (inward for left fin at +x?). Hmm finL at x=+0.3 should point +x outward: rotation.z = -1.1 → (0.89, 0.45,0) outward-up; want outward-down: combine rotation.x = π/2? Let's do rotation.set(Math.PI/2 * 0.9, 0, -1.0)? Getting fiddly. Alternative: side fins as flattened cones pointing sideways-down: set rotation.z = -1.35 for right fin (x+): R_z(-1.35) on (0,1,0) = (sin1.35, cos1.35, 0)=(0.976, 0.219, 0) → mostly +x slightly up. Add rotation.x = 0.5 to tilt down? Rotation order XYZ applies... honestly, small fins — any orientation looks fine. I'll set finL.rotation.set(0.4, 0, -1.2), finR mirrored rotation.z=1.2, rotation.x=0.4. They'll look like swept fins. Fine.

Animation of fin rotation.z overrides base: I set f.finL.rotation.z = 1.1 + sin — but I set finL initial rotation.z=-1.1 (outward for +x?). Let me define: finL at x=+0.3, rotation.z = -1.1 (points +x-ish). Then animate f.finL.rotation.z = -1.1 + sin(t*9)*0.25; finR.rotation.z = 1.1 - sin... Let me just store base angles in userData. Simplify: 

```js
f.finL.userData.base = -1.1;
...
f.finL.rotation.z = f.finL.userData.base + Math.sin(t*9)*0.22;
f.finR.rotation.z = -f.finL.userData.base - Math.sin(t*9)*0.22; // mirrored: +1.1 - sin
```
Set finR.rotation.z = 1.1 - Math.sin(...)*0.22.

OK. Also tail pivot: tailPivot.rotation.y wag — tail extends -z, wag side to side ✓.

One more: fish group scale — tail etc all scaled ✓.

Shadow with 11 fish + plants fine.

Fog: camera at z=15.5, tank front at z=4 → distance ~11.5; fog starts 15 — glass front barely fogged; back wall at 19.5 fully fogged-ish (factor (19.5-15)/(40-15)=0.18). Hmm fog subtle. Set fog 12→34: back wall (19.5): (19.5-12)/22=0.34. Fine, gives depth. Use Fog(0x0a3a58, 12, 34). Background 0x082a40 close to fog color for blend.

Camera FOV 55 at z 15.5: visible height at z=0: 2*15.5*tan(27.5°)=16.15 → tank height 10 fits with margin ✓. Width at 16:9: 16.15*1.6=25.8 > 20 ✓. At 375px wide (aspect 0.46): visible width = 16.15*0.46=7.4 — tank won't fit horizontally; fish will be off-screen much of the time. Adapt: adjust camera distance based on aspect: 

```js
function fitCamera(){
  const aspect = innerWidth/innerHeight;
  const fov = camera.fov * Math.PI/180;
  const distY = (5.6) / Math.tan(fov/2); // half-height 5.6
  const distX = (10.8) / (Math.tan(fov/2)*aspect); // half-width 10.8
  camZ = Math.max(distY, distX) + 1.5;
}
```
Then camera.position.z = camZ each frame. Compute on resize. At 1280x800 aspect 1.6: distY = 5.6/tan27.5 = 5.6/0.5206=10.76; distX = 10.8/(0.5206*1.6)=12.96; camZ=14.46. Good. At 375x667 aspect 0.5625: distX = 10.8/(0.5206*0.5625)=36.9 → camZ 38.4 — very far, fog 12-34 would hide everything! Adjust fog based on camZ too: fog near = camZ*0.75, far = camZ*2.2. Set in fitCamera: scene.fog.near/far update. Background stays.

Also at narrow screens maybe reduce required half-width to 9 to not go too far: half-width 9.5: distX=9.5/(0.5206*0.5625)=32.4. Still far. It's fine — whole tank visible. Fish smaller but visible. Acceptable. Could also allow slight zoom... keep whole-tank view.

Parallax amplitude relative: camTX = mouseNDC.x * 1.4 fine.

Let me also ensure `renderer.domElement` style position fixed? Body overflow hidden, canvas block — fine.

Hint: on touch change text.

Also add `document.title` fine.

Double-check importmap must be exactly as given — yes, include verbatim before module script.

Potential issue: `backdrop-filter` fine in Chrome.

Now, "Click or press a key to start" — overlay includes it; dismissed on any interaction or 7s.

Let me also make sure no console errors: THREE.MathUtils exists. `q.identity()` returns quaternion ✓. `Plane.constant` settable ✓. RingGeometry ✓. `InstancedMesh.setColorAt` — must call before first render ideally; we do in loop before render ✓ (needs instanceColor buffer; setColorAt creates it). ✓

`renderer.useLegacyLights` — r170 default false; intensities: hemisphere 1.0, dir 2.4 ok. Point light 60 with decay 2 distance 30 — fine.

Shadow camera: directional at (6,14,8) targeting origin (default target 0,0,0 — need scene.add(sun.target)? Default target is at (0,0,0) and it's added? Light target must be added to scene for matrix updates if moved; default position (0,0,0) works without adding? The target's matrixWorld defaults to identity, so it works. OK.

Edge: EdgesGeometry on BoxGeometry ✓.

Water surface plane DoubleSide so visible from above/below ✓.

One more nicety: slight caustic light pattern on sand — skip.

Fish flee also when mouse near but fish behind glass front — fine.

Also add a few "small fish" far away? enough.

Now assemble final file. Also ensure the module script uses `window.innerWidth` etc.

Write out completely and carefully check syntax.

Also `onPointer` for touchmove: e.touches[0]. clientX. pointermove already covers touch in modern browsers; keep both, guard.

Also prevent context menu long-press? not needed.

Let me write final answer.

One more check: `surface.rotation.x = -Math.PI/2` then animating rotation.z — rotation order XYZ, fine.

In animate, variables p, q, s, m4 declared earlier in gravel section — they're in module scope ✓. col too.

dummy = new THREE.Object3D(); dummy.lookAt requires dummy not in scene — lookAt works on any object using its position and parent matrixWorld; parent null → uses own position ✓.

ripple.lookAt(camera.position) — ring default faces +z; lookAt orients +z toward camera ✓.

FLEE check uses raycaster.ray which is set only when onPointer ran; guard with mouse.active ✓.

Initial camera.position.z set by fitCamera before animate; call fitCamera() once at init and on resize.

camZ variable module scope.

Also on resize update fog.

Edge: `THREE.MathUtils.clamp` ✓.

Final HTML below. Also include `<meta name="viewport">` ✓.

Card text on touch: change second p? Just hint. Also overlay dim text "Click or press any key to start" — on touch, "Tap to start". Do:
```js
if ('ontouchstart' in window){
  document.querySelector('.card .dim').textContent = '👆 Tap the tank to start';
  document.getElementById('hint').textContent = '👆 Drag near the fish to scare them';
}
```

Count fish 11.

I think ready. Write it.