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
4 min 60 s
Cost
$0.069
Output tokens
32,886
Spent thinking
26,552 tokens
Speed
110.2 tok/s
Console errors
0
First token after
1.1 s
First visible text after
4 min 8 s
Prompt tokens
438
Code size
17.1 KB
Finish reason
stop
Served by
Mistral
Model id
mistralai/mistral-large-4-0
Reasoning sent
model default
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 · 428 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 — Scaredy Fish</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: #02101f; touch-action: none; }
  #c { display: block; width: 100%; height: 100%; }
  #vignette { position: fixed; inset: 0; pointer-events: none;
    background: radial-gradient(ellipse at 50% 42%, rgba(0,0,0,0) 52%, rgba(1,8,18,0.55) 100%); }
  #hud { position: fixed; top: 18px; left: 18px; pointer-events: none; color: #dff3ff; z-index: 10;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    background: rgba(6, 30, 52, 0.45); border: 1px solid rgba(140, 210, 255, 0.28);
    backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
    padding: 12px 16px; border-radius: 14px; max-width: 330px; }
  #hud h1 { font-size: 18px; font-weight: 700; letter-spacing: .4px; margin-bottom: 5px;
    background: linear-gradient(90deg, #8fd8ff, #c8f4ff 55%, #ffd9a0);
    -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent; }
  #hud .hint { font-size: 13px; line-height: 1.45; }
  #hud .sub { font-size: 11px; margin-top: 6px; opacity: .7; transition: opacity .6s; }
  @media (max-width: 480px) {
    #hud { top: 10px; left: 10px; right: 10px; max-width: none; padding: 10px 12px; }
    #hud h1 { font-size: 15px; } #hud .hint { font-size: 12px; } #hud .sub { font-size: 10.5px; }
  }
</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>
<canvas id="c"></canvas>
<div id="vignette"></div>
<div id="hud">
  <h1>🫧 Deep Blue Aquarium</h1>
  <p class="hint">Move your mouse (or drag a finger) near a fish — it will dart away!</p>
  <p class="sub" id="demoNote">✨ a demo cursor is swimming around until you move your pointer…</p>
  <p class="sub" id="count">😱 0 fish startled</p>
</div>

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

/* ---------- renderer / scene / camera ---------- */
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x0a2c4e, 20, 62);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 300);
camera.position.set(0, 1.4, 24);

/* ---------- gradient water backdrop ---------- */
const bg = new THREE.Mesh(
  new THREE.SphereGeometry(140, 32, 20),
  new THREE.ShaderMaterial({
    side: THREE.BackSide, depthWrite: false,
    uniforms: {
      top: { value: new THREE.Color(0x1e6ea6) },
      mid: { value: new THREE.Color(0x0a3560) },
      bot: { value: new THREE.Color(0x020d1c) }
    },
    vertexShader: 'varying vec3 vP; void main(){ vP = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }',
    fragmentShader: `varying vec3 vP; uniform vec3 top; uniform vec3 mid; uniform vec3 bot;
      void main(){ float h = normalize(vP).y;
        vec3 c = h > 0.0 ? mix(mid, top, pow(h, 0.65)) : mix(mid, bot, pow(-h, 0.85));
        gl_FragColor = vec4(c, 1.0); }`
  })
);
scene.add(bg);

/* ---------- lights ---------- */
scene.add(new THREE.AmbientLight(0x7fa8d0, 0.55));
const sun = new THREE.DirectionalLight(0xd6ecff, 1.8);
sun.position.set(8, 22, 10);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
sun.shadow.camera.left = -20; sun.shadow.camera.right = 20;
sun.shadow.camera.top = 20;   sun.shadow.camera.bottom = -20;
sun.shadow.camera.near = 2;    sun.shadow.camera.far = 60;
sun.shadow.camera.updateProjectionMatrix();
sun.shadow.bias = -0.002;
scene.add(sun);
const glow = new THREE.PointLight(0x3f7fbf, 80, 90, 2);
glow.position.set(-9, 3, -5);
scene.add(glow);

/* ---------- tank ---------- */
const LIM = { x: 13.4, yT: 5.9, yB: -6.1, z: 7.4 };   // fish swim limits
function sandH(x, z){ return Math.sin(x * 0.55) * Math.cos(z * 0.75) * 0.22 + Math.sin(x * 1.6 + z * 1.25) * 0.07; }

const glass = new THREE.Mesh(
  new THREE.BoxGeometry(30, 14, 18),
  new THREE.MeshPhongMaterial({ color: 0x9fd4ff, transparent: true, opacity: 0.07, shininess: 140, specular: 0xffffff, side: THREE.DoubleSide, depthWrite: false })
);
scene.add(glass);
scene.add(new THREE.LineSegments(
  new THREE.EdgesGeometry(glass.geometry),
  new THREE.LineBasicMaterial({ color: 0xbfe6ff, transparent: true, opacity: 0.45 })
));

const surface = new THREE.Mesh(
  new THREE.PlaneGeometry(30, 18),
  new THREE.MeshPhongMaterial({ color: 0x2f86c4, transparent: true, opacity: 0.42, shininess: 220, specular: 0xcfeaff, depthWrite: false })
);
surface.rotation.x = -Math.PI / 2;
surface.position.y = 6.95;
scene.add(surface);

/* sand floor */
const sandGeo = new THREE.PlaneGeometry(30, 18, 48, 28);
sandGeo.rotateX(-Math.PI / 2);
{
  const p = sandGeo.attributes.position;
  for (let i = 0; i < p.count; i++) p.setY(i, sandH(p.getX(i), p.getZ(i)));
  sandGeo.computeVertexNormals();
}
const sand = new THREE.Mesh(sandGeo, new THREE.MeshPhongMaterial({ color: 0xc9b083, shininess: 8 }));
sand.position.y = -7;
sand.receiveShadow = true;
scene.add(sand);

/* pebbles */
const pebbles = new THREE.InstancedMesh(
  new THREE.DodecahedronGeometry(0.16, 0),
  new THREE.MeshPhongMaterial({ flatShading: true }),
  140
);
{
  const m = new THREE.Matrix4(), q = new THREE.Quaternion(), v = new THREE.Vector3(), s = new THREE.Vector3(), c = new THREE.Color();
  for (let i = 0; i < 140; i++){
    const x = (Math.random() * 2 - 1) * 14, z = (Math.random() * 2 - 1) * 8;
    v.set(x, -7 + sandH(x, z) + 0.05, z);
    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.6 + Math.random() * 0.6), sc);
    m.compose(v, q, s);
    pebbles.setMatrixAt(i, m);
    c.setHSL(0.08 + Math.random() * 0.06, 0.25 + Math.random() * 0.2, 0.35 + Math.random() * 0.25);
    pebbles.setColorAt(i, c);
  }
  pebbles.instanceColor.needsUpdate = true;
  pebbles.receiveShadow = true;
}
scene.add(pebbles);

/* rocks */
for (let i = 0; i < 6; i++){
  const r = new THREE.Mesh(
    new THREE.DodecahedronGeometry(0.6 + Math.random() * 1.1, 0),
    new THREE.MeshPhongMaterial({ color: new THREE.Color().setHSL(0.58, 0.08 + Math.random() * 0.08, 0.32 + Math.random() * 0.15), flatShading: true, shininess: 20 })
  );
  const x = (Math.random() * 2 - 1) * 12, z = (Math.random() * 2 - 1) * 7;
  r.position.set(x, -7 + sandH(x, z) + 0.25, z);
  r.scale.y = 0.6 + Math.random() * 0.5;
  r.rotation.set(Math.random() * 3, Math.random() * 3, 0);
  r.castShadow = r.receiveShadow = true;
  scene.add(r);
}

/* swaying plants */
const plantMats = [0x2f8f5b, 0x3aa76d, 0x247a52].map(c => new THREE.MeshPhongMaterial({ color: c, shininess: 30 }));
const plants = [];
for (let i = 0; i < 9; i++){
  const g = new THREE.Group();
  const blades = 3 + Math.floor(Math.random() * 3);
  for (let b = 0; b < blades; b++){
    const h = 2 + Math.random() * 2.8;
    const geo = new THREE.ConeGeometry(0.10 + Math.random() * 0.08, h, 6, 4);
    geo.translate(0, h / 2, 0);
    const mesh = new THREE.Mesh(geo, plantMats[i % 3]);
    mesh.position.set((Math.random() * 2 - 1) * 0.5, 0, (Math.random() * 2 - 1) * 0.5);
    mesh.rotation.z = (Math.random() * 2 - 1) * 0.2;
    mesh.castShadow = true;
    g.add(mesh);
  }
  const x = (Math.random() * 2 - 1) * 12.5, z = (Math.random() * 2 - 1) * 7.5;
  g.position.set(x, -7 + sandH(x, z) + 0.05, z);
  g.userData.phase = Math.random() * 10;
  g.userData.amp = 0.08 + Math.random() * 0.08;
  scene.add(g);
  plants.push(g);
}

/* god-ray light shafts */
const shafts = [];
for (let i = 0; i < 4; i++){
  const h = 15;
  const geo = new THREE.ConeGeometry(1.6 + Math.random() * 1.8, h, 20, 1, true);
  geo.translate(0, -h / 2, 0);
  const mat = new THREE.MeshBasicMaterial({ color: 0xaadfff, transparent: true, opacity: 0.06, blending: THREE.AdditiveBlending, side: THREE.DoubleSide, depthWrite: false, fog: false });
  const m = new THREE.Mesh(geo, mat);
  m.position.set((Math.random() * 2 - 1) * 9, 6.9, (Math.random() * 2 - 1) * 5);
  m.rotation.z = (Math.random() * 2 - 1) * 0.12;
  m.rotation.x = (Math.random() * 2 - 1) * 0.12;
  m.userData.phase = Math.random() * 10;
  scene.add(m);
  shafts.push(m);
}

/* bubbles */
const bubbles = [];
const bubGeo = new THREE.SphereGeometry(0.09, 8, 6);
const bubMat = new THREE.MeshPhongMaterial({ color: 0xcfeaff, transparent: true, opacity: 0.55, shininess: 120, specular: 0xffffff });
for (let i = 0; i < 36; i++){
  const b = new THREE.Mesh(bubGeo, bubMat);
  b.position.set((Math.random() * 2 - 1) * 13, -6.5 + Math.random() * 13, (Math.random() * 2 - 1) * 7.5);
  b.scale.setScalar(0.4 + Math.random() * 0.9);
  b.userData.speed = 0.7 + Math.random() * 1.1;
  b.userData.wob = Math.random() * 10;
  scene.add(b);
  bubbles.push(b);
}

/* ---------- fish ---------- */
function makeFish(colorHex){
  const g = new THREE.Group();
  const color = new THREE.Color(colorHex);
  const bodyMat = new THREE.MeshPhongMaterial({ color, shininess: 90, specular: 0xaaddff });
  const body = new THREE.Mesh(new THREE.SphereGeometry(0.5, 20, 14), bodyMat);
  body.scale.set(0.55, 0.85, 1.8);            // fish faces +z
  body.castShadow = true;
  g.add(body);

  const finMat = new THREE.MeshPhongMaterial({ color: color.clone().multiplyScalar(0.8), shininess: 60, transparent: true, opacity: 0.92 });
  const tailPivot = new THREE.Group();
  tailPivot.position.set(0, 0, -0.62);
  const tail = new THREE.Mesh(new THREE.ConeGeometry(0.40, 0.75, 10), finMat);
  tail.rotation.x = -Math.PI / 2;
  tail.scale.set(0.28, 1, 1);
  tail.castShadow = true;
  tailPivot.add(tail);
  g.add(tailPivot);

  const dorsal = new THREE.Mesh(new THREE.ConeGeometry(0.22, 0.5, 8), finMat);
  dorsal.scale.z = 0.3;
  dorsal.position.set(0, 0.40, -0.05);
  dorsal.rotation.x = 0.5;
  g.add(dorsal);

  const eyeMat = new THREE.MeshPhongMaterial({ color: 0x101418, shininess: 120 });
  for (const sx of [-1, 1]){
    const eye = new THREE.Mesh(new THREE.SphereGeometry(0.075, 10, 8), eyeMat);
    eye.position.set(sx * 0.20, 0.16, 0.55);
    g.add(eye);
  }

  const finPivots = [];
  for (const sx of [-1, 1]){
    const p = new THREE.Group();
    p.position.set(sx * 0.26, -0.08, 0.15);
    const f = new THREE.Mesh(new THREE.ConeGeometry(0.16, 0.45, 6), finMat);
    f.rotation.z = -sx * Math.PI / 2;
    f.scale.set(0.3, 1, 0.6);
    p.add(f);
    g.add(p);
    finPivots.push({ p, sx });
  }
  return { g, body, bodyMat, tailPivot, finPivots };
}

const palette = [0xff9f43, 0xff6b6b, 0x4ecdc4, 0xa29bfe, 0xfdcb6e, 0x74b9ff, 0x55efc4, 0xff7675, 0xe17055, 0x00cec9, 0x6c5ce7, 0xf8a5c2];
const fishes = [];
for (let i = 0; i < palette.length; i++){
  const parts = makeFish(palette[i]);
  parts.g.scale.setScalar(0.75 + Math.random() * 0.65);
  scene.add(parts.g);
  fishes.push({
    ...parts,
    pos: new THREE.Vector3((Math.random() * 2 - 1) * 12, -3 + Math.random() * 6, (Math.random() * 2 - 1) * 6),
    vel: new THREE.Vector3(Math.random() - 0.5, 0, Math.random() - 0.5).normalize().multiplyScalar(2),
    target: new THREE.Vector3((Math.random() * 2 - 1) * 12, -4 + Math.random() * 8, (Math.random() * 2 - 1) * 6.5),
    speed: 1.7 + Math.random() * 1.5,
    phase: Math.random() * 20,
    retarget: Math.random() * 4,
    scared: false
  });
}

/* ---------- pointer (mouse / touch) ---------- */
const mouseNDC = new THREE.Vector2(0, 0.2);
const mouseWorld = new THREE.Vector3(0, 0, 0);
let hasPointer = false;
const demoNote = document.getElementById('demoNote');
const countEl = document.getElementById('count');
function onPointer(e){
  hasPointer = true;
  mouseNDC.set((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
  if (demoNote) demoNote.style.opacity = '0';
}
window.addEventListener('pointermove', onPointer, { passive: true });
window.addEventListener('pointerdown', onPointer, { passive: true });

const raycaster = new THREE.Raycaster();
const FLEE_R = 5.2;
const _desired = new THREE.Vector3();
const _look = new THREE.Vector3();

function updateFish(f, dt, t){
  // wander target
  f.retarget -= dt;
  if (f.retarget <= 0 || f.pos.distanceTo(f.target) < 1.2){
    f.target.set((Math.random() * 2 - 1) * 12.5, -4.5 + Math.random() * 8.5, (Math.random() * 2 - 1) * 6.8);
    f.retarget = 4 + Math.random() * 5;
  }
  _desired.copy(f.target).sub(f.pos).normalize().multiplyScalar(f.speed);

  // flee from the pointer's 3D position
  const dx = f.pos.x - mouseWorld.x;
  const dy = f.pos.y - mouseWorld.y;
  const dz = f.pos.z - mouseWorld.z;
  const d = Math.sqrt(dx * dx + dy * dy + dz * dz);
  f.scared = d < FLEE_R;
  if (f.scared && d > 1e-4){
    const s = (1 - d / FLEE_R) * 9.5 + 2.5;
    _desired.x += (dx / d) * s;
    _desired.y += (dy / d) * s;
    _desired.z += (dz / d) * s;
  }
  f.bodyMat.emissive.setScalar(f.scared ? (1 - Math.min(d, FLEE_R) / FLEE_R) * 0.35 : 0);

  // soft wall avoidance
  const m = 1.4;
  if (f.pos.x < -LIM.x + m) _desired.x += (-LIM.x + m - f.pos.x) * 3;
  if (f.pos.x >  LIM.x - m) _desired.x -= (f.pos.x - (LIM.x - m)) * 3;
  if (f.pos.y <  LIM.yB + m) _desired.y += (LIM.yB + m - f.pos.y) * 3;
  if (f.pos.y >  LIM.yT - m) _desired.y -= (f.pos.y - (LIM.yT - m)) * 3;
  if (f.pos.z < -LIM.z + m) _desired.z += (-LIM.z + m - f.pos.z) * 3;
  if (f.pos.z >  LIM.z - m) _desired.z -= (f.pos.z - (LIM.z - m)) * 3;

  f.vel.lerp(_desired, 1 - Math.exp(-2.8 * dt));
  if (f.vel.length() > 10) f.vel.setLength(10);
  const sp = f.vel.length();
  f.pos.addScaledVector(f.vel, dt);

  let clamped = false;
  if (f.pos.x < -LIM.x){ f.pos.x = -LIM.x; clamped = true; } else if (f.pos.x > LIM.x){ f.pos.x = LIM.x; clamped = true; }
  if (f.pos.y <  LIM.yB){ f.pos.y = LIM.yB; clamped = true; } else if (f.pos.y > LIM.yT){ f.pos.y = LIM.yT; clamped = true; }
  if (f.pos.z < -LIM.z){ f.pos.z = -LIM.z; clamped = true; } else if (f.pos.z > LIM.z){ f.pos.z = LIM.z; clamped = true; }
  if (clamped) f.target.set((Math.random() * 2 - 1) * 12, -4 + Math.random() * 8, (Math.random() * 2 - 1) * 6.5);

  f.g.position.copy(f.pos);
  if (sp > 0.15){
    _look.copy(f.pos).add(f.vel);
    f.g.lookAt(_look);
  }
  const wag = Math.sin(t * (5 + sp * 2.5) + f.phase) * (0.28 + Math.min(sp, 7) * 0.09);
  f.tailPivot.rotation.y = wag;
  f.body.rotation.y = Math.sin(t * 4 + f.phase) * 0.07;
  for (const fp of f.finPivots){
    fp.p.rotation.x = Math.sin(t * 7 + f.phase + fp.sx * 2) * 0.4;
  }
}

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

/* ---------- main loop ---------- */
const clock = new THREE.Clock();
let t = 0;
let scaredShown = -1;
function animate(){
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  t += dt;

  // self-playing demo cursor until the user moves their pointer
  if (!hasPointer) mouseNDC.set(Math.cos(t * 0.55) * 0.62, Math.sin(t * 0.8) * 0.42);

  // project pointer onto the tank's mid plane (z = 0)
  raycaster.setFromCamera(mouseNDC, camera);
  const ray = raycaster.ray;
  if (Math.abs(ray.direction.z) > 1e-6){
    const k = -ray.origin.z / ray.direction.z;
    mouseWorld.copy(ray.origin).addScaledVector(ray.direction, k);
  }
  mouseWorld.x = THREE.MathUtils.clamp(mouseWorld.x, -LIM.x, LIM.x);
  mouseWorld.y = THREE.MathUtils.clamp(mouseWorld.y, LIM.yB, LIM.yT);
  mouseWorld.z = THREE.MathUtils.clamp(mouseWorld.z, -LIM.z, LIM.z);

  for (const f of fishes) updateFish(f, dt, t);

  let sc = 0;
  for (const f of fishes) if (f.scared) sc++;
  if (sc !== scaredShown){ scaredShown = sc; countEl.textContent = '😱 ' + sc + ' fish startled'; }

  for (const b of bubbles){
    b.position.y += b.userData.speed * dt;
    b.position.x += Math.sin(t * 2.5 + b.userData.wob) * 0.006;
    b.position.z += Math.cos(t * 2.0 + b.userData.wob) * 0.006;
    if (b.position.y > 6.6){
      b.position.y = -6.6;
      b.position.x = (Math.random() * 2 - 1) * 13;
      b.position.z = (Math.random() * 2 - 1) * 7.5;
    }
  }
  for (const p of plants){
    p.rotation.z = Math.sin(t * 1.1 + p.userData.phase) * p.userData.amp;
    p.rotation.x = Math.cos(t * 0.85 + p.userData.phase) * p.userData.amp * 0.6;
  }
  for (const s of shafts){
    s.material.opacity = 0.045 + 0.03 * (0.5 + 0.5 * Math.sin(t * 0.4 + s.userData.phase));
    s.rotation.y = t * 0.05 + s.userData.phase;
  }
  surface.position.y = 6.95 + Math.sin(t * 0.7) * 0.06;
  surface.material.opacity = 0.38 + 0.07 * Math.sin(t * 1.1);

  // gentle camera parallax
  camera.position.x += (mouseNDC.x * 2.4 - camera.position.x) * 0.04;
  camera.position.y += (1.4 - mouseNDC.y * 1.6 - camera.position.y) * 0.04;
  camera.lookAt(0, -0.3, 0);

  renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
Reasoning trace (26,552 tokens)
The user wants a 3D aquarium with Three.js where fish swim away when the mouse moves near them. Requirements:

- Use the exact import map provided
- Self-contained HTML file, single code block
- 1280×800 viewport, adapt down to 375px
- Something visible on first paint (fish swimming immediately)
- Mouse interaction: fish flee from mouse. Since iframe only receives keyboard after click, but mouse move should work on pointermove. Actually mouse events on the iframe... pointermove should fire when hovering. To be safe, also map mouse to a 3D point via raycasting onto a plane, and fish flee from that point.
- Make it beautiful: water gradient background, fog, god rays (light shafts), bubbles, plants, sand, rocks, lighting.

Plan:
- Import three via import map.
- Scene with fog, gradient background (use a large sphere or scene background color + fog). Aquarium glass box: use a big box with transparent material showing edges.
- Sand floor: plane with sandy color, maybe displaced.
- Plants: cones/cylinders with sway animation (vertex displacement in shader or simple rotation sway).
- Rocks: dodecahedrons.
- Fish: build from simple geometry — body (sphere scaled), tail (cone/plane), fins. Use groups. Animate tail wag with rotation. Each fish has wander behavior: pick target points, swim toward, avoid glass walls, and flee from mouse projected point.
- Mouse: track normalized device coords, raycast to a plane at mid-depth (z=0 plane), get world point. Fish within radius get a flee vector away from mouse point.
- Bubbles: small spheres rising, respawn.
- Light shafts: a few transparent cones from top with additive blending.
- Camera slight parallax with mouse.
- "Move your mouse near the fish to scare them" hint overlay. Also works with touch.
- Performance: ~12 fish.

Fish flee behavior: compute mouse world position each frame. For each fish, vector from mouse point to fish; if distance < fleeRadius (e.g., 6), add strong acceleration away. Also clamp within tank bounds; when fleeing toward wall, steer back.

Fish model: 
- body: SphereGeometry(0.5, 16, 12) scaled (1.6, 0.8, 0.5)
- tail: ConeGeometry rotated, or a flattened cone at back, animate rotation.y wag.
- eyes: small black spheres.
- colors: vary orange, blue, yellow, purple.

Wander: each fish has a target; when close, pick new random target within bounds. Velocity-based steering: desired direction toward target, plus flee, plus wall avoidance; lerp velocity; orient fish along velocity (lookAt).

Tank: width 30, height 14, depth 18, centered at origin, floor at y=-7. Glass: BoxGeometry with MeshPhysicalMaterial transmission? Transmission is expensive; use MeshPhongMaterial transparent opacity 0.08 with edges lines. Add a subtle frame.

Background: scene.background = gradient via canvas texture? Simpler: large background sphere with shader gradient (deep blue at bottom to lighter at top) — actually aquarium viewed from outside: dark blue water. Use fog for depth: scene.fog = new THREE.Fog(0x0a2e4a, 20, 60). Background color 0x062033. Add a big gradient via CSS behind canvas? Canvas covers all. Use scene background color plus a large plane behind? Simplest: create a huge sphere with BackSide shader gradient. Or set renderer clear color and fog. I'll do a gradient using a canvas texture as scene.background — canvas texture works as background (equirect? no, it stretches). Better: CSS gradient on body and renderer alpha:true, with fog color matching. Fog with transparent background: fog blends toward fog color, which would mismatch CSS gradient. Alternative: use scene.background = new THREE.Color and add fog same color — fine, solid deep blue with fog looks okay but gradient nicer.

I'll make a background: big box/sphere with ShaderMaterial BackSide vertical gradient from #0b3d63 (top) to #02101f (bottom). Fog color mid blue #0a2c4d.

Light shafts: 3-4 cones, additive, opacity 0.05-0.12, from top, slightly rotated, animate opacity subtly.

Bubbles: ~40 small spheres, random x/z, rise from floor, wobble, reset at top.

Plants: create a few "seaweed" using cylinders bent — simpler: use a series of segments or a plane with sine wave via custom geometry. Easiest good-looking: ConeGeometry tall thin, then in animation rotate/skew slightly with sin(time + phase) — sway. Or use TubeGeometry along a curve that we update? Too heavy. I'll make seaweed from 3 stacked cones? Actually a common trick: create a plane geometry (width 0.6, height 4, segments 1x8) and displace vertices in onBeforeRender? Simpler: custom ShaderMaterial is overkill. I'll create seaweed as a cylinder with many height segments, and each frame update vertex positions with sine offsets (geometry.attributes.position). With ~8 plants × 9 segments, cheap. Store base positions.

Alternatively rotate whole plant slightly + scale pulse. Vertex sway looks much better. Let's do it: CylinderGeometry(0.08, 0.25, 4, 6, 12) — top radius small. Store base x,z per vertex, each frame y stays, x = baseX + sin(time*1.5 + y*0.8 + phase) * 0.3 * (y/4). Need recompute normals occasionally — skip normals or call computeVertexNormals each frame for few plants (fine, small).

Actually simpler and still nice: use MeshLambert/Phong and just sway via rotation.z = sin * 0.15 around base — but rotation pivots at center; translate geometry so base at origin, then rotate group. That gives decent sway without vertex updates. Combine: geometry translated up by height/2, group at floor, rotation.z sway, plus slight rotation.x. That's cheap and looks fine. I'll also add a second inner cone for fullness. Good enough.

Fish details: Group per fish:
- body mesh
- tail: use a flattened cone (ConeGeometry(0.35, 0.7, 8) scaled z 0.3) positioned at back (x negative if fish faces +x). Fish faces direction of velocity; set group.lookAt(pos + vel). Body along x-axis: scale sphere (1.8, 0.75, 0.45). Tail at x = -0.75, rotated so cone points backward: cone default points +y; rotate z by -90° so it points -x... ConeGeometry apex at +y/2? Cone has apex at top (+y). Rotate z by +PI/2 makes apex point +x. We want apex at back (-x) attached... Actually tail fin: wide part at back. Put cone with apex forward (toward body) and base at back: rotate z = PI/2 → apex +x? Rotating +90° about z maps +y to -x. Hmm: rotation about z by θ maps +y axis to (-sinθ, cosθ). θ=π/2 → (-1, 0) = -x. So apex points -x (back), base at +x near body. That works: tail cone apex out back, base attached to body. Position at x=-0.7. Then wag: tail.rotation.y = sin(t*speed)*0.5 (rotation about y since tail extends along x).
- dorsal fin: small flattened cone on top.
- eyes: two small spheres at front sides.
- pectoral fins: two small flattened cones sides, flap.

Colors per fish via material color; use MeshPhongMaterial with shininess.

Fish motion: velocity vector; each frame:
- desired = normalize(target - pos) * speed
- flee: d = pos - mouseWorld; dist; if dist < R: desired += (d/dist) * fleeStrength * (1 - dist/R)
- wall avoidance: if near bounds, add inward push.
- vel = lerp(vel, desired, 0.05); pos += vel * dt; clamp pos inside with margin; if clamped, pick new target.
- orient: group.position.copy(pos); group.lookAt(pos + vel) — but lookAt orients -z? Object3D.lookAt makes +z? For non-camera objects, lookAt orients the +z axis toward target? Actually Object3D.lookAt rotates so that its internal -z? Cameras look down -z; regular objects look down +z? In three.js, Object3D.lookAt makes the object's +z axis point toward target for non-camera objects... Let me recall: "Rotates the object to face a point in world space." For objects, it orients the positive z-axis toward the point (cameras use -z). Yes, for regular objects +z faces the target. So build fish facing +z instead: body scaled (0.45, 0.75, 1.8), tail at z=-0.7, eyes at z=+0.5. Then lookAt(pos+vel) works.

Mouse → world: raycaster from camera through NDC; intersect plane z = 0 (tank mid). mouseWorld = intersection point. If no intersection (mouse outside frustum), clamp. Also on touch, pointermove works.

Also add gentle camera sway and mouse parallax: camera.position.x lerp to mouse.x * 1.5, y to -mouse.y*1 + base.

Add gravel: small instanced pebbles? Could add InstancedMesh of small dodecahedrons on floor, ~150, sandy/gray colors. Nice touch, cheap.

Add a few rocks: 5-6 dodecahedron geometries random scale, gray.

Castle/ornament? Maybe skip, keep clean. Add a small treasure chest? Skip.

Glass: BoxGeometry(30,14,18) at y=0 center; material MeshPhongMaterial({color:0x9fd8ff, transparent:true, opacity:0.06, side:BackSide? }) Render both sides maybe. Plus EdgesGeometry LineSegments with opacity 0.35. Also a top rim frame. And water surface: plane at y=+7 with slight opacity and animated? Add plane with MeshPhongMaterial color 0x2e7fb8 transparent 0.35, animate position.y small bob or opacity. Nice.

Lights: ambient 0x4a6a8a, directional from top (0, 20, 5) intensity 1.2 color 0xbfe6ff, plus a point light bluish. Shadows? Skip shadows for perf, or enable with one directional and floor receive — shadows look great. Fish cast shadows on sand: enable renderer.shadowMap, dir light castShadow with tuned shadow camera bounds, floor receiveShadow, fish meshes castShadow. Could be okay perf with 12 fish. I'll enable PCFSoft, shadow map 1024. Should be fine.

Flee visualization: maybe when a fish is fleeing, it speeds up and tail wags faster — naturally happens if wag tied to speed.

UI overlay: title "AQUARIUM", hint "Move your mouse near the fish — they'll dart away". Styled glassmorphism top-left. Also a small counter? Maybe show "fish startled: X" — cute. Keep simple: hint text.

Also handle "click or press a key"? Not needed since no keyboard. Mouse move works via pointermove on window — but note: "The iframe only receives keyboard events after the user clicks it" — mouse events presumably also only after click/focus? It says keyboard specifically. Pointermove might work without click in iframe when mouse moves over it... In sandboxed iframes, mouse events do propagate when the cursor is over the iframe typically. But to be safe, also make the demo self-playing: fish wander autonomously (they do). Also add an "auto demo" mouse: a ghost cursor that moves around when the user hasn't moved the mouse, scaring fish periodically, so the screenshot at 3s shows action. Good idea: if no real pointer activity in last N seconds, animate a virtual pointer in a lissajous path. That guarantees visible fleeing in the auto screenshot. Label hint "Move your mouse to scare the fish — (demo cursor active until you move)".

Hmm, but the requirement says games need self-playing demo; this is not a game but interactive demo — still nice that fish swim visibly. Fish wander anyway. I'll add virtual pointer active for first ~6 seconds or until first real pointermove, moving in a circle so fish scatter — shows off the mechanic immediately.

Resize handling. Pixel ratio min(devicePixelRatio, 2).

Fish count: 12. Tank bounds: x ±13, y -6..6, z -7..7 (depth 18, so z ±8 minus margin).

Let me write fish creation:

```js
function makeFish(color){
  const g = new THREE.Group();
  const mat = new THREE.MeshPhongMaterial({color, shininess:80, specular:0x99ccff});
  const body = new THREE.Mesh(new THREE.SphereGeometry(0.5, 20, 14), mat);
  body.scale.set(0.5, 0.8, 1.7);
  body.castShadow = true;
  g.add(body);
  // tail
  const tailMat = new THREE.MeshPhongMaterial({color: new THREE.Color(color).multiplyScalar(0.85), shininess:60});
  const tail = new THREE.Mesh(new THREE.ConeGeometry(0.42, 0.8, 10), tailMat);
  tail.rotation.x = Math.PI/2; // apex along? cone +y; rotate x by 90°: +y -> +z? rotation about x: +y maps to +z. apex +z (front). We want apex back (-z): rotation.x = -PI/2.
  ...
}
```

Rotation about x by θ maps +y → (0, cosθ, sinθ). θ=-π/2 → (0,0,-1) = -z. Good: tail.rotation.x = -Math.PI/2 puts apex at -z (back), base near body. Position tail at z=-0.75. Wag: tail.rotation.y? After rotation.x applied... order matters. Set tail.rotation order 'YXZ' maybe. Simpler: wrap tail in a pivot group at z=-0.7; pivot.rotation.y = wag; inside, cone with rotation.x=-π/2. Pivot rotation.y wags around vertical axis — tail sweeps side to side. 

Dorsal fin: cone small on top: rotation.x = π/2? Fin pointing up: cone apex +y already up; flatten scale.z=0.3; position y=0.35, z=-0.1. Slight back tilt rotation.x = 0.4.

Eyes: SphereGeometry(0.07) black glossy at (±0.18, 0.12, 0.38).

Pectoral fins: two cones flattened, at (±0.28, -0.05, 0.1), rotation to point outward/back; flap with rotation.z. Wrap in pivots too, or just rotate mesh.rotation.x? Keep simple: fins as flattened cones angled, flap via rotation.z sin.

Fish scale variation: g.scale.setScalar(0.8 + rand*0.7).

Behavior per fish:
```js
fish = {group, vel: Vector3 random, target: Vector3, speed: 1.5+rand*1.5, phase: rand*10, size}
```

Update:
```js
const desired = tmp.copy(f.target).sub(f.pos).normalize().multiplyScalar(f.speed);
// flee
const dx = f.pos.x - mouseW.x, dy = f.pos.y - mouseW.y, dz = f.pos.z - mouseW.z;
const d = Math.sqrt(dx*dx+dy*dy+dz*dz);
const R = 5.5;
if (d < R && d > 0.001){
  const f = (1 - d/R) * 7;
  desired.x += dx/d*f; desired.y += dy/d*f; desired.z += dz/d*f;
}
// wall avoidance
const M = 1.2;
if (f.pos.x < -BX+M) desired.x += (-BX+M - f.pos.x)*2; ... etc for each wall
f.vel.lerp(desired, 1 - Math.exp(-3*dt));
const sp = f.vel.length(); if (sp>0.01){ f.pos.addScaledVector(f.vel, dt); }
clamp pos to bounds; if clamped hard, retarget.
if (f.pos.distanceTo(f.target) < 1.5 || retarget timer) pick new target.
group.position.copy(f.pos);
if (sp > 0.05) { lookTarget.copy(f.pos).add(f.vel); group.lookAt(lookTarget); }
// tail wag
const wag = Math.sin(t*10 + phase) * (0.25 + sp*0.12);
tailPivot.rotation.y = wag;
fin flap...
```

Note group.lookAt with +z forward as discussed. Fish geometry built facing +z. Good.

Bounds: BX=13, BY top 6.2 bottom -6.2, BZ=7.5.

Retarget: also add time-based: every 4-8s pick new target.

Bubbles: pool of 50 meshes (SphereGeometry(0.06..0.15) shared geo, MeshPhongMaterial transparent white opacity .5? Use MeshBasicMaterial? Phong with emissive slight). Each bubble: x,z random in tank, y from -6.5 rising speed 0.8-1.6, wobble x += sin. When y > 6.5 reset to bottom. Slight scale.

Light shafts: ConeGeometry(radius top? ConeGeometry(radius, height) base at bottom). For a shaft from surface down: cone with apex up: rotate x by π (apex up). Geometry ConeGeometry(2.2, 16, 24, 1, true) openEnded; material MeshBasicMaterial({color:0x9fdcff, transparent:true, opacity:0.06, blending:AdditiveBlending, side:DoubleSide, depthWrite:false}). Position y=1, x/z spread, rotate slight. Animate opacity 0.04+0.03*sin. 4 shafts.

Water surface: PlaneGeometry(30,18) rotated -π/2 at y=7, MeshPhongMaterial color 0x1c5f8f transparent opacity 0.5 shininess 200 specular white; animate slight y bob and maybe rotation? Just bob.

Sand: PlaneGeometry(30,18, 40,24) displaced with noise (simple pseudo-random sin combos), color 0xc2a878, receiveShadow. Compute vertex colors? Keep single color, add slight vertex color variation maybe. Displace y = noise. Rotate -π/2, position y=-7.

Pebbles: InstancedMesh DodecahedronGeometry(0.15,0), 120 instances, colors via setColorAt, random pos on sand (y = sandHeight(x,z)+0.05 approx — just y=-6.9 with random small). Fine.

Plants: 7 groups at random positions near edges/back. Each: 3-5 blades: cone geometry height 2-4.5, radius bottom 0.12-0.2; geometry translated so base at origin (translate(0, h/2, 0)); mesh in group; group.rotation.z sway = sin(t*1.2+phase)*0.12; also rotation.x slight. Colors green shades 0x2e8b57, 0x3fa66a, 0x27795f. MeshPhongMaterial.

Rocks: 5 dodecahedrons gray 0x6b7280 flat shading, random scale 0.8-2, positions.

Glass box edges: BoxGeometry(30,14,18) EdgesGeometry → LineSegments color 0xbfe6ff opacity 0.5. Also glass panes: Mesh with opacity 0.05. Position center y=0.

Frame: maybe top rim: 4 thin boxes? The edges lines suffice plus bottom frame. Keep lines.

Background gradient sphere: SphereGeometry(80, 32, 16), ShaderMaterial BackSide:
```
varying vec3 vP; void main(){vP=position; gl_Position=...}
frag: float h = normalize(vP).y*0.5+0.5; vec3 top=vec3(0.10,0.35,0.55); vec3 bot=vec3(0.01,0.05,0.12); gl_FragColor=vec4(mix(bot,top,h),1);
```
Fog: new THREE.Fog(0x0a2f52, 25, 70). Fog applies to materials; shader material unaffected by fog by default (fine). Glass with fog ok.

Camera: fov 55, pos (0, 1.5, 22), lookAt(0,0,0). Tank depth 18 → front glass at z=9; camera at 22 fine, fog 25..70 might fog the tank? Tank max distance from camera ~ 22+9=31 at back — fog would haze back of tank nicely. Start fog at 18, end 55 maybe. Let's fog(0x0a2f52, 20, 60).

Renderer: antialias, setPixelRatio min(dpr,2), shadowMap enabled PCFSoft, outputColorSpace default SRGB, toneMapping ACESFilmic exposure 1.1.

Mouse handling:
```js
let mouseNDC = new THREE.Vector2(0,0);
let mouseWorld = new THREE.Vector3(0,0,0);
let hasPointer = false;
window.addEventListener('pointermove', e => { hasPointer=true; mouseNDC.x = (e.clientX/innerWidth)*2-1; mouseNDC.y = -(e.clientY/innerHeight)*2+1; });
window.addEventListener('pointerdown', same);
```
Each frame compute ray from camera, intersect plane z=0: 
```js
raycaster.setFromCamera(mouseNDC, camera);
const t = -raycaster.ray.origin.z / raycaster.ray.direction.z;
mouseWorld = ray.origin + dir*t; clamp to bounds.
```
Guard direction.z ~ 0.

Virtual demo pointer: if (!hasPointer && t < 8) animate mouseNDC along circle: mouseNDC.set(cos(t*0.7)*0.5, sin(t*0.9)*0.35). This makes fish flee during first seconds. Also after hasPointer, real used. Maybe also if pointer idle > 10s, resume virtual? Could be fun: if now - lastPointerTime > 8s, blend virtual in. I'll do: virtual active until first real pointer move; after that real only. Screenshot at 3s shows virtual pointer scaring fish. 

Hint text: "🐟 Move your mouse near the fish to scare them away" plus note. Update: when hasPointer, change text? Keep static plus a small "demo cursor" indicator that hides after first move. Simple: element #hint, when hasPointer first time, fade the "(demo)" part. I'll have hint: "Move your mouse near the fish — they'll dart away!" and a sub note "auto-demo cursor is swimming until you move your mouse". On first pointermove, hide sub note.

Touch: pointermove covers touch drag. Add touchstart too.

Title overlay top center or top-left: "🫧 Deep Blue Aquarium". Styling: position absolute, pointer-events none, font system. Gradient text maybe.

Also FPS-independent dt clamp.

Fish colors: ['#ff9f43','#ff6b6b','#4ecdc4','#a29bfe','#fdcb6e','#74b9ff','#55efc4','#ff7675','#e17055','#00cec9','#6c5ce7','#dfe6e9'].

Eyes need to be on both sides: since body scaled z 1.7 (length along z), x is width 0.5*... body.scale.set(0.55, 0.85, 1.8) with sphere radius 0.5 → half extents x 0.275, y 0.425, z 0.9. Eyes at (±0.2, 0.15, 0.55). Tail pivot at z=-0.8, cone length 0.8 apex back: cone base radius 0.42 at z=0 of pivot... cone after rotation.x=-π/2: apex at -z end, base at +z. Cone height 0.8 centered at origin → apex at z=-0.4, base at z=+0.4. In pivot at z=-0.8: base at z=-0.4 (overlapping body rear at -0.9..) hmm body rear at z=-0.9. Tail pivot at z=-0.75, base at -0.35, apex at -1.15. Slight gap between body (-0.9) and tail base (-0.35)? Overlap region: tail base circle radius 0.42 centered z=-0.35 extends x ±0.42, body at z=-0.35 has radius... body is ellipsoid; at z=-0.35 cross-section small. Might look detached. Move pivot to z=-0.6: base at -0.2, apex -1.0. Better overlap. Also scale tail z? Cone flattened: tail.scale.z? After rotation, flatten along x (width) instead: we want tail fin tall in y and thin in x: cone radius in xy-plane; after rotation.x=-π/2, the cone's circular cross-section lies in x-y plane; to make fin thin in x, tall in y: scale.set(0.25, 1, 1) applied before rotation? Scale applies in local space before rotation (scale then rotate in matrix composition — object matrix = T*R*S, so scale applied first in local space, then rotated). Local cone axis +y; scale x by 0.25 → thin in x, then rotation.x=-π/2 maps +y to -z. Result: fin thin in x, tall in y, extending -z. 

Dorsal fin: cone radius 0.25 height 0.5, scale z 0.25 (thin), position (0, 0.42, -0.1), rotation.x = 0.5 tilt back.

Pectoral fins: cone radius 0.18 height 0.45, thin in y? Fin extends outward ±x and back: rotation.z = ±(π/2 + tilt) hmm. Place fin pivot at (±0.25, -0.1, 0.15); inside cone rotated so apex points outward-back-down. Simplify: fin mesh cone, rotation.z = ∓2.2 (pointing outward), scale.set(1,1,0.3) thin in local z → after rotation thin in ... let's not overthink; visually small fins with flap rotation.x = sin*0.4. I'll do: finGeo cone(0.16, 0.5, 6); mesh.scale.set(1,1,0.35); mesh.rotation.z = side * -1.9; positioned; flap by adjusting rotation.x? After rotation.z, changing rotation.x rotates about world-ish local x... Use pivot groups: finPivot at side, child cone with rotation.z = -1.9*side... ugh. Simpler: finPivot.rotation.z = base ± flap where base = side>0 ? -0.6 : 0.6? Let's define cone apex +y; to point fin outward (+x for right fin), rotate z by -π/2 (apex +y → +x? rotation z by θ maps +y→(-sinθ, cosθ); θ=-π/2 → (1,0) = +x. yes). So right fin pivot.rotation.z = -π/2 + flap; left = π/2 + flap... θ=π/2 → (-1,0) = -x. Good. Flap = sin(t*8+phase)*0.3*(speed factor). Child cone scale.set(0.3,1,0.3)? thin in x after rotation → thin in world y? Whatever, flatten local z (scale z 0.3) → after z-rotation, local z stays world z, thin in z. Fin plane in xy — fine, looks like fin.

OK. Also add slight body wiggle: body.rotation.y = sin(t*6+phase)*0.08.

Now sand displacement: 
```js
const sandGeo = new THREE.PlaneGeometry(30, 18, 48, 28);
sandGeo.rotateX(-Math.PI/2);
const pos = sandGeo.attributes.position;
for (i...){ const x=pos.getX(i), z=pos.getZ(i); const y = Math.sin(x*0.6)*Math.cos(z*0.8)*0.25 + Math.sin(x*1.7+z*1.3)*0.08; pos.setY(i, y); }
sandGeo.computeVertexNormals();
```
Sand at y=-7. Fish bottom bound -6.3.

Sand height function for pebbles: replicate formula approx.

Plants positioned on sand: y = -7 + sandH(x,z) + 0.1.

Rocks same.

Fish spawn: random pos in bounds, random target.

Shadow: dirLight.position.set(6, 18, 8); castShadow; shadow.camera left/right/top/bottom ±20, near 1 far 40; mapSize 1024. Sand receiveShadow. Fish castShadow (body, tail). Plants castShadow maybe. Bubbles no.

Check: glass material with transparent + fog fine.

Performance: 12 fish × ~6 meshes = 72 draw calls + rest ~ 120 total. Fine.

Now the code structure:

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>3D Aquarium — Scaredy Fish</title>
<style>...</style>
<script type="importmap">...</script>
</head>
<body>
<div id="ui">...</div>
<canvas id="c"></canvas> (or container)
<script type="module">
import * as THREE from 'three';
...
</script>
</body>
</html>
```

CSS: html,body margin 0, height 100%, overflow hidden, background #02101f. canvas display block. UI overlay absolute.

UI:
```html
<div id="hud">
  <h1>🫧 Deep Blue Aquarium</h1>
  <p class="hint">Move your mouse near the fish — they'll dart away!</p>
  <p class="sub" id="demoNote">…a demo cursor is swimming around until you take over</p>
</div>
```
Style: top-left, backdrop-filter blur, rgba background, border-radius, pointer-events none, color #dff3ff, font-family system-ui.

Media query for small screens: smaller font.

JS full:

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

const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({canvas, antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x0a2c4e, 20, 62);

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

// background gradient dome
const bg = new THREE.Mesh(new THREE.SphereGeometry(90, 32, 20), new THREE.ShaderMaterial({
  side: THREE.BackSide, depthWrite:false,
  uniforms:{ top:{value:new THREE.Color(0x1a5d8f)}, mid:{value:new THREE.Color(0x0a3560)}, bot:{value:new THREE.Color(0x020d1c)} },
  vertexShader:`varying vec3 vP; void main(){ vP = position; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
  fragmentShader:`varying vec3 vP; uniform vec3 top; uniform vec3 mid; uniform vec3 bot;
   void main(){ float h = normalize(vP).y; vec3 c = h>0.0 ? mix(mid, top, pow(h,0.7)) : mix(mid, bot, pow(-h,0.8)); gl_FragColor = vec4(c,1.0); }`
}));
scene.add(bg);
```

Lights:
```js
scene.add(new THREE.AmbientLight(0x6f9cc8, 0.7));
const sun = new THREE.DirectionalLight(0xcfeaff, 1.6);
sun.position.set(8, 20, 10);
sun.castShadow = true;
sun.shadow.mapSize.set(1024,1024);
sun.shadow.camera.left=-20; right=20; top=20; bottom=-20; near=2; far=50;
sun.shadow.bias = -0.002;
scene.add(sun);
const fill = new THREE.PointLight(0x3f7fbf, 30, 60); fill.position.set(-10, 4, -6); scene.add(fill);
```
Note: three r155+ uses physically correct lights by default; PointLight intensity in candela — 30 with distance 60 decay 2 fine. Directional 1.6 fine.

Tank constants: W=30 (x ±15), H=14 (y ±7), D=18 (z ±9). Glass box 30×14×18 centered origin.

Sand plane 30×18 at y=-7. Bounds for fish: x ±13.5, y -6..6, z ±7.5.

Glass:
```js
const glassMat = new THREE.MeshPhongMaterial({color:0x9fd4ff, transparent:true, opacity:0.07, shininess:120, specular:0xffffff, side:THREE.DoubleSide, depthWrite:false});
const glass = new THREE.Mesh(new THREE.BoxGeometry(30,14,18), glassMat);
scene.add(glass);
const edges = new THREE.LineSegments(new THREE.EdgesGeometry(glass.geometry), new THREE.LineBasicMaterial({color:0xbfe6ff, transparent:true, opacity:0.5}));
scene.add(edges);
```
depthWrite false for glass so fish visible — transparent objects render after opaque anyway.

Water surface plane at y=7:
```js
const surf = new THREE.Mesh(new THREE.PlaneGeometry(30,18), new THREE.MeshPhongMaterial({color:0x2f7fb8, transparent:true, opacity:0.45, shininess:200, specular:0xbbeaff}));
surf.rotation.x = -Math.PI/2; surf.position.y = 6.98; scene.add(surf);
```
Animate opacity subtly.

Sand:
```js
function sandH(x,z){ return Math.sin(x*0.6)*Math.cos(z*0.8)*0.22 + Math.sin(x*1.7+z*1.3)*0.07; }
const sandGeo = new THREE.PlaneGeometry(30,18,48,28);
sandGeo.rotateX(-Math.PI/2);
{ const p = sandGeo.attributes.position; for(let i=0;i<p.count;i++){ p.setY(i, sandH(p.getX(i), p.getZ(i))); } sandGeo.computeVertexNormals(); }
const sand = new THREE.Mesh(sandGeo, new THREE.MeshPhongMaterial({color:0xc9b083, shininess:10}));
sand.position.y = -7; sand.receiveShadow = true; scene.add(sand);
```

Pebbles:
```js
const pebGeo = new THREE.DodecahedronGeometry(0.16, 0);
const pebMat = new THREE.MeshPhongMaterial({flatShading:true});
const pebbles = new THREE.InstancedMesh(pebGeo, pebMat, 140);
const m = new THREE.Matrix4(), q = new THREE.Quaternion(), s = new THREE.Vector3(), v = new THREE.Vector3();
const c = new THREE.Color();
for (let i=0;i<140;i++){
  const x = (Math.random()*2-1)*14; const z = (Math.random()*2-1)*8;
  v.set(x, -7 + sandH(x,z) + 0.05, z);
  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.6+Math.random()*0.6), sc);
  m.compose(v,q,s); pebbles.setMatrixAt(i,m);
  c.setHSL(0.08+Math.random()*0.06, 0.25+Math.random()*0.2, 0.35+Math.random()*0.25);
  pebbles.setColorAt(i,c);
}
pebbles.receiveShadow = true;
scene.add(pebbles);
```

Rocks:
```js
const rockMat = new THREE.MeshPhongMaterial({color:0x5d6b7a, flatShading:true, shininess:20});
for (let i=0;i<6;i++){
  const r = new THREE.Mesh(new THREE.DodecahedronGeometry(0.6+Math.random()*1.1, 0), rockMat);
  const x=(Math.random()*2-1)*12, z=(Math.random()*2-1)*7;
  r.position.set(x, -7+sandH(x,z)+0.3, z);
  r.scale.y = 0.6+Math.random()*0.5;
  r.rotation.set(Math.random()*3, Math.random()*3, 0);
  r.castShadow = r.receiveShadow = true;
  scene.add(r);
}
```

Plants:
```js
const plantMat = [0x2f8f5b, 0x3aa76d, 0x247a52].map(c=>new THREE.MeshPhongMaterial({color:c, shininess:30}));
const plants = [];
for (let i=0;i<9;i++){
  const g = new THREE.Group();
  const blades = 3 + Math.floor(Math.random()*3);
  for (let b=0;b<blades;b++){
    const h = 2 + Math.random()*2.8;
    const geo = new THREE.ConeGeometry(0.10+Math.random()*0.08, h, 6, 4);
    geo.translate(0, h/2, 0);
    const mesh = new THREE.Mesh(geo, plantMat[i%3]);
    mesh.position.set((Math.random()*2-1)*0.5, 0, (Math.random()*2-1)*0.5);
    mesh.rotation.z = (Math.random()*2-1)*0.2;
    mesh.castShadow = true;
    g.add(mesh);
  }
  const x = (Math.random()*2-1)*12.5, z = (Math.random()*2-1)*7.5;
  g.position.set(x, -7+sandH(x,z)+0.05, z);
  g.userData.phase = Math.random()*10;
  g.userData.amp = 0.08+Math.random()*0.08;
  scene.add(g); plants.push(g);
}
```
Sway: g.rotation.z = sin(t*1.1+phase)*amp; g.rotation.x = cos(t*0.9+phase)*amp*0.6.

Light shafts:
```js
const shafts = [];
const shaftMat = new THREE.MeshBasicMaterial({color:0xaadfff, transparent:true, opacity:0.07, blending:THREE.AdditiveBlending, side:THREE.DoubleSide, depthWrite:false, fog:false});
for (let i=0;i<4;i++){
  const h = 15;
  const geo = new THREE.ConeGeometry(1.6+Math.random()*1.8, h, 20, 1, true);
  geo.translate(0, -h/2, 0); // apex at top y=0? cone apex +y; translate -h/2 puts apex at y=0... apex at +h/2 → 0, base at -h. good, hangs downward.
  const m = new THREE.Mesh(geo, shaftMat.clone());
  m.position.set((Math.random()*2-1)*9, 7, (Math.random()*2-1)*5);
  m.rotation.z = (Math.random()*2-1)*0.12;
  m.rotation.x = (Math.random()*2-1)*0.12;
  m.userData.phase = Math.random()*10;
  scene.add(m); shafts.push(m);
}
```
Wait cone apex is at +y? ConeGeometry: apex at top (+height/2), base at bottom. So after translate(0,-h/2,0): apex at y=0, base at y=-h. Position at y=7 → shaft from surface down to y=-8. Good. Additive blending with fog:false.

Bubbles:
```js
const bubbles = [];
const bubGeo = new THREE.SphereGeometry(0.09, 8, 6);
const bubMat = new THREE.MeshPhongMaterial({color:0xcfeaff, transparent:true, opacity:0.55, shininess:100, specular:0xffffff});
for (let i=0;i<36;i++){
  const b = new THREE.Mesh(bubGeo, bubMat);
  b.position.set((Math.random()*2-1)*13, -6.5+Math.random()*13, (Math.random()*2-1)*7.5);
  b.scale.setScalar(0.4+Math.random()*0.9);
  b.userData.speed = 0.7+Math.random()*1.1;
  b.userData.wob = Math.random()*10;
  scene.add(b); bubbles.push(b);
}
```
Update: y += speed*dt; x += sin(t*3+wob)*0.01; if y>6.6 → y=-6.6, new x/z.

Fish:

```js
const fishes = [];
const BOUNDS = {x:13.5, yTop:6.0, yBot:-6.2, z:7.5};
const FLEE_R = 5.5;

function makeFish(colorHex){
  const g = new THREE.Group();
  const color = new THREE.Color(colorHex);
  const bodyMat = new THREE.MeshPhongMaterial({color, shininess:90, specular:0xaaddff});
  const body = new THREE.Mesh(new THREE.SphereGeometry(0.5, 20, 14), bodyMat);
  body.scale.set(0.55, 0.85, 1.8);
  body.castShadow = true;
  g.add(body);

  const finMat = new THREE.MeshPhongMaterial({color: color.clone().multiplyScalar(0.8), shininess:60, transparent:true, opacity:0.9});
  // tail
  const tailPivot = new THREE.Group(); tailPivot.position.set(0,0,-0.62);
  const tail = new THREE.Mesh(new THREE.ConeGeometry(0.40, 0.75, 10), finMat);
  tail.rotation.x = -Math.PI/2; tail.scale.set(0.28,1,1);
  tail.castShadow = true;
  tailPivot.add(tail); g.add(tailPivot);
  // dorsal
  const dorsal = new THREE.Mesh(new THREE.ConeGeometry(0.22, 0.5, 8), finMat);
  dorsal.scale.z = 0.3; dorsal.position.set(0, 0.40, -0.05); dorsal.rotation.x = 0.5;
  g.add(dorsal);
  // eyes
  const eyeMat = new THREE.MeshPhongMaterial({color:0x111111, shininess:100});
  for (const sx of [-1,1]){
    const eye = new THREE.Mesh(new THREE.SphereGeometry(0.075, 10, 8), eyeMat);
    eye.position.set(sx*0.20, 0.16, 0.55);
    g.add(eye);
  }
  // pectoral fins
  const finPivots = [];
  for (const sx of [-1,1]){
    const p = new THREE.Group(); p.position.set(sx*0.26, -0.08, 0.15);
    const f = new THREE.Mesh(new THREE.ConeGeometry(0.16, 0.45, 6), finMat);
    f.rotation.z = -sx*Math.PI/2; f.scale.set(0.3,1,0.6);
    p.add(f); g.add(p); finPivots.push({p, sx});
  }
  return {g, tailPivot, finPivots, body};
}
```

Wait tail rotation: tail.rotation.x=-π/2 maps apex +y→-z. scale.set(0.28,1,1) applied before rotation in local space: thin in x. Good — fin thin horizontally (x), tall in y. 

Hmm actually tail fin should be wide in y? Fish tails are vertical fins (tall in y, thin in x). Yes correct.

Fish flap: finPivots p.rotation.z = base? I set f.rotation.z inside; to flap, rotate p.rotation.x maybe (fin sweeping forward/back): p.rotation.x = sin(...)*0.4. Or p.rotation.y. Use p.rotation.x = flap. But p at side; rotation.x tips fin up/down — okay.

Fish instances:
```js
const palette = [0xff9f43, 0xff6b6b, 0x4ecdc4, 0xa29bfe, 0xfdcb6e, 0x74b9ff, 0x55efc4, 0xff7675, 0xe17055, 0x00cec9, 0x6c5ce7, 0xf8a5c2];
for (let i=0;i<12;i++){
  const parts = makeFish(palette[i]);
  const g = parts.g;
  g.scale.setScalar(0.75 + Math.random()*0.65);
  scene.add(g);
  fishes.push({
    ...parts,
    pos: new THREE.Vector3((Math.random()*2-1)*12, -3+Math.random()*6, (Math.random()*2-1)*6),
    vel: new THREE.Vector3(Math.random()-0.5, 0, Math.random()-0.5).normalize().multiplyScalar(2),
    target: new THREE.Vector3(),
    speed: 1.6 + Math.random()*1.6,
    phase: Math.random()*20,
    retarget: 0
  });
  f.target.set((Math.random()*2-1)*12, -4+Math.random()*8, (Math.random()*2-1)*6.5);
}
```
Careful with spread of parts including g etc. Fine.

Update loop:

```js
const raycaster = new THREE.Raycaster();
const mouseNDC = new THREE.Vector2(0, 0);
const mouseWorld = new THREE.Vector3(0, 0, 0);
let hasPointer = false, lastPointer = -100;
addEventListener('pointermove', e => {
  hasPointer = true; lastPointer = performance.now()/1000;
  mouseNDC.set((e.clientX/innerWidth)*2-1, -(e.clientY/innerHeight)*2+1);
  document.getElementById('demoNote').style.opacity = 0;
});
addEventListener('pointerdown', same handler);
```

Demo cursor: 
```js
let tGlobal = 0;
// in loop:
if (!hasPointer) {
  mouseNDC.set(Math.cos(tGlobal*0.6)*0.55, Math.sin(tGlobal*0.85)*0.4);
}
raycaster.setFromCamera(mouseNDC, camera);
const ray = raycaster.ray;
if (Math.abs(ray.direction.z) > 1e-5){
  const t = -ray.origin.z / ray.direction.z;
  mouseWorld.copy(ray.origin).addScaledVector(ray.direction, t);
}
mouseWorld.x = clamp(mouseWorld.x, -13.5, 13.5); ... y clamp -6.2..6, z clamp -7.5..7.5;
```

Fish update as planned. Flee strength: base 8.

```js
const tmp = new THREE.Vector3(), look = new THREE.Vector3();
for (const f of fishes){
  tmp.copy(f.target).sub(f.pos);
  const distT = tmp.length();
  if (distT < 1.2){ f.target.set(...random); }
  tmp.normalize().multiplyScalar(f.speed);
  // flee
  const dx = f.pos.x-mouseWorld.x, dy=f.pos.y-mouseWorld.y, dz=f.pos.z-mouseWorld.z;
  const d = Math.sqrt(dx*dx+dy*dy+dz*dz);
  f.scared = d < FLEE_R;
  if (f.scared){
    const s = (1 - d/FLEE_R) * 9 + 2;
    tmp.x += dx/(d||1)*s; tmp.y += dy/(d||1)*s; tmp.z += dz/(d||1)*s;
  }
  // walls
  const m=1.0;
  if (f.pos.x < -BOUNDS.x+m) tmp.x += (-BOUNDS.x+m - f.pos.x)*3;
  if (f.pos.x >  BOUNDS.x-m) tmp.x -= (f.pos.x - (BOUNDS.x-m))*3;
  ... y/z similarly
  f.vel.lerp(tmp, 1-Math.exp(-2.5*dt));
  const sp = f.vel.length();
  f.pos.addScaledVector(f.vel, dt);
  // hard clamp
  clamp pos; if clamped any, choose new target;
  f.g.position.copy(f.pos);
  if (sp > 0.1){ look.copy(f.pos).add(f.vel); f.g.lookAt(look); }
  const wag = Math.sin(tGlobal*(6+sp*3)+f.phase) * (0.3 + Math.min(sp,6)*0.10);
  f.tailPivot.rotation.y = wag;
  f.body.rotation.y = Math.sin(tGlobal*5+f.phase)*0.06;
  for (const fp of f.finPivots){ fp.p.rotation.x = Math.sin(tGlobal*7+f.phase+fp.sx)*0.35; }
}
```

Note: lookAt on group whose parent is scene — fine.

One catch: group.lookAt makes +z point at target — but our fish faces +z (eyes at +z). Correct.

Camera parallax:
```js
camera.position.x += ((mouseNDC.x*2.2) - camera.position.x)*0.03;
camera.position.y += ((1.2 - mouseNDC.y*1.4) - camera.position.y)*0.03;
camera.lookAt(0, 0, 0);
```

Surface bob: surf.position.y = 6.98 + Math.sin(t*0.8)*0.05; surf.material.opacity = 0.4+Math.sin(t*1.3)*0.06.

Shafts: m.material.opacity = 0.05 + 0.03*Math.sin(t*0.5+phase); m.rotation.y slow spin? slight.

Plants sway as above.

Bubbles update.

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

Animation loop with clock:
```js
const clock = new THREE.Clock();
function animate(){
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  tGlobal += dt;
  ...updates...
  renderer.render(scene, camera);
}
animate();
```

Check three r170: `renderer.outputColorSpace` default SRGB — fine. Lights physical: PointLight intensity 30 with decay 2 — at distance ~10, 30/100 = 0.3, okay dim. Maybe 60. DirectionalLight 1.6 fine. Ambient 0.7.

Shadow config: sun.shadow.camera props need assignment then updateProjectionMatrix? Setting before first render is fine (three updates on first use? Actually shadow camera projection updated in render). Setting properties directly works since WebGLShadowMap calls updateMatrices which uses shadow.camera.projectionMatrix — need shadow.camera.updateProjectionMatrix() after changing? In three, DirectionalLightShadow camera is updated automatically? The light's shadow camera projection matrix is updated when? In WebGLShadowMap, it calls shadow.updateMatrices(light) which uses camera matrices but projectionMatrix must be updated manually after changing frustum. Many examples set left/right then it works because... examples often don't call updateProjectionMatrix and it works? Actually LightShadow.updateMatrices calls camera.updateMatrixWorld but not projection. Hmm, but default OrthographicCamera for directional shadow has default frustum -5..5; examples that set left/right etc. typically... Looking at three.js examples (e.g., webgl_shadowmap), they set shadow.camera.left etc. without updateProjectionMatrix? Actually they do: some call `light.shadow.camera.updateProjectionMatrix()`. To be safe, call sun.shadow.camera.updateProjectionMatrix().

Fog + ShaderMaterial bg: fog:false default for ShaderMaterial (fog only if material.fog true and shader includes fog chunks) — fine.

InstancedMesh color: need pebbles.instanceColor.needsUpdate = true after setColorAt — setting before first render is fine.

Check `flatShading` on PhongMaterial supported r170: yes.

ConeGeometry openEnded param: ConeGeometry(radius, height, radialSegments, heightSegments, openEnded). Good.

Edge case: pointermove on window — inside iframe, window receives events when hovering? The note says keyboard only after click; pointer events over iframe should fire normally when the mouse moves over the iframe area. Also add pointermove on document. Also 'touchmove' maps to pointermove in modern browsers. Fine.

Also add keyboard? Not needed.

The demo note element: hide on first pointer. Also change hint text? Keep.

Let me also consider: "Something meaningful must be visible on first paint" — yes, aquarium with swimming fish, and demo cursor scaring them within 3s.

Add slight vignette overlay via CSS radial-gradient for polish, pointer-events none.

Also mobile 375px: HUD scales via media query; canvas full. Fish flee from touch drag. Good.

Title maybe "🐠 Aquarium — scare the fish with your mouse". 

Let me now also double check tail wag pivot: tailPivot at z=-0.62, rotation.y wags — tail sweeps left/right. Good.

Body scale z 1.8 * 0.5 = 0.9 half-length; tail pivot at -0.62 inside body rear; apex at -0.62-0.375=-1.0. Good.

Dorsal at y=0.40 — body top at y=0.425. Good.

Eyes at z=0.55, x ±0.20, y 0.16 — body half width at that z: ellipsoid x half = 0.275*sqrt(1-(0.55/0.9)^2)=0.275*sqrt(1-0.373)=0.275*0.79=0.217. Eye at 0.20 slightly inside surface — eye radius 0.075 so sticks out. Good.

Now colors: finMat color multiplied 0.8 darker. ok.

One more: `g.lookAt(look)` — look is pos+vel in world; group at scene root; fine.

Flee radius 5.5 in world units; tank is 30 wide, fish speed ~2-3.5, flee adds up to ~11 → fast dart. dt-based lerp 2.5/s gives responsiveness. Maybe cap velocity magnitude to ~9: if (f.vel.length()>10) f.vel.setLength(10).

Retarget timer: also retarget every 5-9s: f.retarget -= dt; if <0 { pick target; f.retarget = 4+Math.random()*5 }.

Wall clamp: after adding vel, clamp pos into bounds; if hit wall, set target to random point away from wall (just random target) and zero the velocity component into wall.

Write clamp helper.

Also scared indicator? Could tint fish or show exclamation — skip; maybe scale pulse when scared: g.scale base * (1+0.15*sin) too fiddly. Instead, when scared, wag faster (already via sp). Good enough. Maybe add a small "!" sprite? Skip.

HUD extra: fish counter "12 fish". Fine.

Let me write the final HTML carefully.

Also `powerPreference: 'high-performance'` optional.

Potential console errors to avoid: none using blocked APIs. Import map exact as given.

One concern: `renderer.useLegacyLights` — in r170 default is false (physical). PointLight(0x3f7fbf, 60, 80) intensity 60 candela-ish; with decay 2 default. At distance 10 → 0.6. OK.

AmbientLight 0.7 might wash; use 0.55.

Exposure 1.1.

Fog color 0x0a2c4e vs bg mid 0x0a3560 — close enough.

Camera at z=24, tank front at z=9 → distance 15 to front glass; fog starts 20 so front clear, back of tank at 33 gets fogged moderately (fog factor (33-20)/(62-20)=0.31). Nice depth.

Background sphere radius 90 < camera far 200. ok.

Glass DoubleSide transparent opacity 0.07 depthWrite false — renders in transparent pass after fish (fish opaque). Fish visible through glass. Edges lines render... LineSegments transparent too. ok.

Shadows on sand from directional light through "glass" — glass is transparent mesh; shadow map will include glass? Glass material transparent — shadow depth material ignores transparency by default (uses depth material, glass would cast shadow). That would darken everything. Set glass.castShadow = false — but shadow rendering uses object.castShadow; glass default false since we never set true. Only set castShadow on fish/plants/rocks. Sand receiveShadow. Good. But wait — sun comes from above; glass top face at y=7 would cast if castShadow true; it's false. Good.

Water surface plane: transparent, no shadow.

Now write sandH used by plants/rocks/pebbles — define before use.

Also small detail: `renderer.setClearColor` not needed since bg dome covers.

Let me make sure the demo note hides: getElementById('demoNote') exists.

Edge: on very first frames before any pointer, demo cursor active — good.

Also add `touch-action: none` on body to prevent scroll on touch drag? Page doesn't scroll anyway (overflow hidden). Add for pointermove smoothness on touch.

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" />
<title>3D Aquarium — Scaredy Fish</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html, body { width:100%; height:100%; overflow:hidden; background:#02101f; touch-action:none; }
  #c { display:block; width:100%; height:100%; }
  #vignette { position:fixed; inset:0; pointer-events:none;
    background: radial-gradient(ellipse at 50% 42%, transparent 55%, rgba(1,8,18,0.55) 100%); }
  #hud { position:fixed; top:18px; left:18px; pointer-events:none; color:#dff3ff;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    background: rgba(6, 30, 52, 0.45); border:1px solid rgba(140, 210, 255, 0.25);
    backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
    padding:12px 16px; border-radius:14px; max-width: 320px; }
  #hud h1 { font-size:18px; font-weight:700; letter-spacing:0.4px; margin-bottom:4px;
    background: linear-gradient(90deg,#8fd8ff,#b7f0ff,#ffd9a0); -webkit-background-clip:text; background-clip:text; color:transparent; }
  #hud .hint { font-size:13px; line-height:1.45; opacity:0.95; }
  #hud .sub { font-size:11px; margin-top:6px; opacity:0.65; transition: opacity 0.6s; }
  @media (max-width: 480px){
    #hud { top:10px; left:10px; right:10px; max-width:none; padding:10px 12px; }
    #hud h1 { font-size:15px; } #hud .hint { font-size:12px; } #hud .sub { font-size:10.5px; }
  }
</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>
<canvas id="c"></canvas>
<div id="vignette"></div>
<div id="hud">
  <h1>🫧 Deep Blue Aquarium</h1>
  <p class="hint">Move your mouse (or finger) near a fish and it will dart away!</p>
  <p class="sub" id="demoNote">✨ A demo cursor is swimming around until you move your pointer…</p>
</div>
<script type="module">
import * as THREE from 'three';
...
</script>
</body>
</html>
```

Now full JS. Let me write it out completely and carefully.

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

// ---------- renderer / scene / camera ----------
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x0a2c4e, 20, 62);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 250);
camera.position.set(0, 1.4, 24);

// ---------- gradient backdrop ----------
const bg = new THREE.Mesh(
  new THREE.SphereGeometry(120, 32, 20),
  new THREE.ShaderMaterial({
    side: THREE.BackSide,
    depthWrite: false,
    uniforms: {
      top: { value: new THREE.Color(0x1e6ea6) },
      mid: { value: new THREE.Color(0x0a3560) },
      bot: { value: new THREE.Color(0x020d1c) }
    },
    vertexShader: `
      varying vec3 vP;
      void main(){ vP = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
    fragmentShader: `
      varying vec3 vP;
      uniform vec3 top; uniform vec3 mid; uniform vec3 bot;
      void main(){
        float h = normalize(vP).y;
        vec3 c = h > 0.0 ? mix(mid, top, pow(h, 0.65)) : mix(mid, bot, pow(-h, 0.85));
        gl_FragColor = vec4(c, 1.0);
      }`
  })
);
scene.add(bg);
```

Lights, tank, sand, etc. as planned.

Bounds:
```js
const TANK = { hx: 15, hy: 7, hz: 9 };      // glass half extents
const LIM  = { x: 13.4, yT: 5.9, yB: -6.1, z: 7.4 }; // fish swim limits
```

sandH:
```js
function sandH(x, z){
  return Math.sin(x * 0.55) * Math.cos(z * 0.75) * 0.22 + Math.sin(x * 1.6 + z * 1.25) * 0.07;
}
```

Sand geo, pebbles, rocks, plants, shafts, bubbles, glass, surface.

Fish making & update as planned.

Pointer:
```js
const mouseNDC = new THREE.Vector2(0, 0.2);
const mouseWorld = new THREE.Vector3(0, 0, 0);
let hasPointer = false;
const demoNote = document.getElementById('demoNote');
function onPointer(e){
  hasPointer = true;
  mouseNDC.set((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
  if (demoNote) demoNote.style.opacity = '0';
}
window.addEventListener('pointermove', onPointer, { passive: true });
window.addEventListener('pointerdown', onPointer, { passive: true });
```

In animate:
```js
if (!hasPointer){
  mouseNDC.set(Math.cos(t * 0.55) * 0.6, Math.sin(t * 0.8) * 0.42);
}
raycaster.setFromCamera(mouseNDC, camera);
const ray = raycaster.ray;
if (Math.abs(ray.direction.z) > 1e-6){
  const k = -ray.origin.z / ray.direction.z;
  mouseWorld.copy(ray.origin).addScaledVector(ray.direction, k);
}
mouseWorld.x = THREE.MathUtils.clamp(mouseWorld.x, -LIM.x, LIM.x);
mouseWorld.y = THREE.MathUtils.clamp(mouseWorld.y, LIM.yB, LIM.yT);
mouseWorld.z = THREE.MathUtils.clamp(mouseWorld.z, -LIM.z, LIM.z);
```

Fish update — write carefully:

```js
const _desired = new THREE.Vector3();
const _look = new THREE.Vector3();
const FLEE_R = 5.2;

function updateFish(f, dt, t){
  // retarget
  f.retarget -= dt;
  if (f.retarget <= 0 || f.pos.distanceTo(f.target) < 1.2){
    f.target.set((Math.random()*2-1)*12.5, -4.5 + Math.random()*8.5, (Math.random()*2-1)*6.8);
    f.retarget = 4 + Math.random()*5;
  }
  _desired.copy(f.target).sub(f.pos).normalize().multiplyScalar(f.speed);

  // flee from pointer
  const dx = f.pos.x - mouseWorld.x;
  const dy = f.pos.y - mouseWorld.y;
  const dz = f.pos.z - mouseWorld.z;
  const d = Math.sqrt(dx*dx + dy*dy + dz*dz);
  if (d < FLEE_R && d > 1e-4){
    const s = (1 - d / FLEE_R) * 9.5 + 2.5;
    _desired.x += (dx / d) * s;
    _desired.y += (dy / d) * s;
    _desired.z += (dz / d) * s;
  }

  // soft wall avoidance
  const m = 1.4;
  if (f.pos.x < -LIM.x + m) _desired.x += (-LIM.x + m - f.pos.x) * 3;
  if (f.pos.x >  LIM.x - m) _desired.x -= (f.pos.x - (LIM.x - m)) * 3;
  if (f.pos.y < LIM.yB + m) _desired.y += (LIM.yB + m - f.pos.y) * 3;
  if (f.pos.y > LIM.yT - m) _desired.y -= (f.pos.y - (LIM.yT - m)) * 3;
  if (f.pos.z < -LIM.z + m) _desired.z += (-LIM.z + m - f.pos.z) * 3;
  if (f.pos.z >  LIM.z - m) _desired.z -= (f.pos.z - (LIM.z - m)) * 3;

  f.vel.lerp(_desired, 1 - Math.exp(-2.8 * dt));
  if (f.vel.length() > 10) f.vel.setLength(10);
  const sp = f.vel.length();
  f.pos.addScaledVector(f.vel, dt);

  // hard clamp
  let clamped = false;
  if (f.pos.x < -LIM.x){ f.pos.x = -LIM.x; clamped = true; } else if (f.pos.x > LIM.x){ f.pos.x = LIM.x; clamped = true; }
  if (f.pos.y < LIM.yB){ f.pos.y = LIM.yB; clamped = true; } else if (f.pos.y > LIM.yT){ f.pos.y = LIM.yT; clamped = true; }
  if (f.pos.z < -LIM.z){ f.pos.z = -LIM.z; clamped = true; } else if (f.pos.z > LIM.z){ f.pos.z = LIM.z; clamped = true; }
  if (clamped){ f.target.set((Math.random()*2-1)*12, -4+Math.random()*8, (Math.random()*2-1)*6.5); }

  f.g.position.copy(f.pos);
  if (sp > 0.15){
    _look.copy(f.pos).add(f.vel);
    f.g.lookAt(_look);
  }
  const wag = Math.sin(t * (5 + sp * 2.5) + f.phase) * (0.28 + Math.min(sp, 7) * 0.09);
  f.tailPivot.rotation.y = wag;
  f.body.rotation.y = Math.sin(t * 4 + f.phase) * 0.07;
  for (const fp of f.finPivots){
    fp.p.rotation.x = Math.sin(t * 7 + f.phase + fp.sx * 2) * 0.4;
  }
}
```

Wait: fish built facing +z, and lookAt points +z toward velocity — but three.js Object3D.lookAt: "This method does not support objects having non-uniformly-scaled parent(s)." fine. And it orients the object's positive z-axis? Let me double-check: In three.js, Object3D.lookAt rotates the object so that its internal forward direction (-z for cameras) faces the target... Actually the docs: "Rotates the object to face a point in world space. This method does not support objects having non-uniformly-scaled parent(s). ... camera looks down its negative z-axis; other objects positive z?" The implementation uses Matrix4.lookAt(eye, target, up) which creates a rotation where the z axis points from target to eye (i.e., object's +z points AWAY from target) for cameras... Hmm. Let me recall precisely: Matrix4.lookAt constructs a rotation matrix whose Z axis = normalize(eye - target). Then Object3D.lookAt: if object is camera or light, m1.lookAt(position, target, up) else m1.lookAt(target, position, up). So for non-camera objects, Z axis = normalize(position - target)... wait m1.lookAt(target, position, up) → z = normalize(target - position)? Matrix4.lookAt(eye, target, up) sets z = eye - target normalized. For regular objects, lookAt calls m1.lookAt(target, position, up) meaning eye=target, target=position → z = normalize(target_arg - position) = normalize(originalTarget - objectPosition). So object's +z points TOWARD the target. Yes — regular objects face the target with +z. Confirmed: fish facing +z works with lookAt(pos+vel).

Bubbles, plants, shafts, surface updates in animate.

Camera parallax + lookAt(0,0,-0.5).

Resize handler.

Clock loop.

Double-check ConeGeometry for shafts open-ended: new THREE.ConeGeometry(r, h, 20, 1, true). Then geo.translate(0,-h/2,0): apex originally at +h/2 → 0; base at -h. Position mesh y=6.9. Good.

Shaft material fog:false — MeshBasicMaterial has fog property default true; set fog:false. With scene.fog set, basic material includes fog — shafts far would get fogged; set fog false. 

Glass material: MeshPhongMaterial transparent; fog affects — fine.

One more consideration: transparent glass renders after opaque fish; with depthWrite:false on glass, fish behind glass unaffected. Surface plane also transparent depthWrite default true — set depthWrite:false to avoid artifacts with shafts. ok.

Bubbles: MeshPhongMaterial transparent — they'll render in transparent pass sorted by distance; fine.

Now HUD: also maybe show a little legend of controls. Fine as is.

Also handle WebGL context creation failure? Not needed.

Potential issue: `renderer.shadowMap` with transparent glass — fine.

Check plant cone geometry: ConeGeometry(0.1..0.18, h, 6, 4) then translate(0,h/2,0) so base at y=0. Mesh inside group at y=0 → sits on sand. Group rotation sway pivots at base. 

Rock: DodecahedronGeometry(0.6+rand*1.1) — radius param; position y = sandH + 0.3*scaleY... approximate: y = -7 + sandH(x,z) + 0.25. With scale.y ~0.6-1.1, center slightly above sand; some may float/sink — acceptable. Use +0.2.

Pebbles y: -7 + sandH + 0.06, radius ~0.16*sc up to 0.38 — may sink partially, fine.

Fish y range -6.1..5.9, sand at -7±0.3 → clearance ok.

Now assemble everything. Also add a subtle "scared" visual: when scared, maybe emissive flash on fish: bodyMat.emissive set to white * (1-d/R)*0.4? Nice touch: f.bodyMat.emissive — store material ref in fish parts. In update: 
```js
const scare = d < FLEE_R ? (1 - d/FLEE_R) : 0;
f.bodyMat.emissive.setScalar(scare * 0.35);
```
MeshPhongMaterial has emissive. Store bodyMat in parts. 

Also HUD counter of startled fish? Could add: count scared fish, display "😱 N fish startled". Cute and shows interactivity in screenshot. Add element #count updated each frame (throttle to every ~200ms). Let's add: `<p class="sub" id="count">😱 0 fish startled</p>`. Update text when changed. With demo cursor active, some fish will be scared at 3s likely. Good for "meaningful visible".

Implement: in animate after fish updates, count scared, if changed update #count textContent.

Now finalize code. Also ensure no `alert` etc. None.

Write out:

Full JS assembled:

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

const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x0a2c4e, 20, 62);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 300);
camera.position.set(0, 1.4, 24);

... (bg, lights, tank, sand, pebbles, rocks, plants, shafts, surface, glass, bubbles, fish)

const LIM = { x: 13.4, yT: 5.9, yB: -6.1, z: 7.4 };
```

Order: define LIM and sandH before sand etc.

Lights:
```js
scene.add(new THREE.AmbientLight(0x7fa8d0, 0.55));
const sun = new THREE.DirectionalLight(0xd6ecff, 1.8);
sun.position.set(8, 22, 10);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
Object.assign(sun.shadow.camera, { left: -20, right: 20, top: 20, bottom: -20, near: 2, far: 60 });
sun.shadow.camera.updateProjectionMatrix();
sun.shadow.bias = -0.002;
scene.add(sun);
const glow = new THREE.PointLight(0x3f7fbf, 80, 90, 2);
glow.position.set(-9, 3, -5);
scene.add(glow);
```

Object.assign on camera props works (they're plain props). ok.

Tank glass:
```js
const glass = new THREE.Mesh(
  new THREE.BoxGeometry(30, 14, 18),
  new THREE.MeshPhongMaterial({ color: 0x9fd4ff, transparent: true, opacity: 0.07, shininess: 140, specular: 0xffffff, side: THREE.DoubleSide, depthWrite: false })
);
scene.add(glass);
const edge = new THREE.LineSegments(
  new THREE.EdgesGeometry(glass.geometry),
  new THREE.LineBasicMaterial({ color: 0xbfe6ff, transparent: true, opacity: 0.45 })
);
scene.add(edge);
```

Surface:
```js
const surface = new THREE.Mesh(
  new THREE.PlaneGeometry(30, 18),
  new THREE.MeshPhongMaterial({ color: 0x2f86c4, transparent: true, opacity: 0.42, shininess: 220, specular: 0xcfeaff, depthWrite: false })
);
surface.rotation.x = -Math.PI / 2;
surface.position.y = 6.95;
scene.add(surface);
```

Sand + sandH.

Pebbles instanced.

Rocks.

Plants.

Shafts.

Bubbles.

Fish factory + instances.

Pointer + raycast.

Animate.

Let me count fish palette 12 entries, 12 fish.

makeFish returns { g, body, bodyMat, tailPivot, finPivots }.

Fish instance creation:

```js
const fishes = [];
const palette = [0xff9f43, 0xff6b6b, 0x4ecdc4, 0xa29bfe, 0xfdcb6e, 0x74b9ff, 0x55efc4, 0xff7675, 0xe17055, 0x00cec9, 0x6c5ce7, 0xf8a5c2];
for (let i = 0; i < palette.length; i++){
  const parts = makeFish(palette[i]);
  const g = parts.g;
  const s = 0.75 + Math.random() * 0.65;
  g.scale.setScalar(s);
  scene.add(g);
  const f = {
    ...parts,
    pos: new THREE.Vector3((Math.random()*2-1)*12, -3 + Math.random()*6, (Math.random()*2-1)*6),
    vel: new THREE.Vector3(Math.random()-0.5, 0, Math.random()-0.5).normalize().multiplyScalar(2),
    target: new THREE.Vector3((Math.random()*2-1)*12, -4 + Math.random()*8, (Math.random()*2-1)*6.5),
    speed: 1.7 + Math.random() * 1.5,
    phase: Math.random() * 20,
    retarget: Math.random() * 4
  };
  fishes.push(f);
}
```

Note: g.scale set — lookAt with scaled object: "does not support non-uniformly scaled parents" — parent is scene, uniform scale on itself is fine (lookAt uses quaternion setFromRotationMatrix on its own matrix... uniform scale ok).

Emissive scare: f.bodyMat.emissive — MeshPhongMaterial emissive default black; setScalar each frame.

Bubbles update:
```js
for (const b of bubbles){
  b.position.y += b.userData.speed * dt;
  b.position.x += Math.sin(t * 2.5 + b.userData.wob) * 0.006;
  b.position.z += Math.cos(t * 2.0 + b.userData.wob) * 0.006;
  if (b.position.y > 6.6){
    b.position.y = -6.6;
    b.position.x = (Math.random()*2-1) * 13;
    b.position.z = (Math.random()*2-1) * 7.5;
  }
}
```

Plants:
```js
for (const p of plants){
  p.rotation.z = Math.sin(t * 1.1 + p.userData.phase) * p.userData.amp;
  p.rotation.x = Math.cos(t * 0.85 + p.userData.phase) * p.userData.amp * 0.6;
}
```

Shafts:
```js
for (const s of shafts){
  s.material.opacity = 0.045 + 0.03 * (0.5 + 0.5 * Math.sin(t * 0.4 + s.userData.phase));
  s.rotation.y = t * 0.05 + s.userData.phase;
}
```

Surface:
```js
surface.position.y = 6.95 + Math.sin(t * 0.7) * 0.06;
surface.material.opacity = 0.38 + 0.07 * Math.sin(t * 1.1);
```

Camera:
```js
camera.position.x += (mouseNDC.x * 2.4 - camera.position.x) * 0.04;
camera.position.y += (1.4 - mouseNDC.y * 1.6 - camera.position.y) * 0.04;
camera.lookAt(0, -0.3, 0);
```

Count scared:
```js
let scaredCount = -1;
...after fish loop:
let sc = 0; for (const f of fishes) if (f.scared) sc++;
if (sc !== scaredCount){ scaredCount = sc; countEl.textContent = `😱 ${sc} fish startled`; }
```
Set f.scared in update.

Set f.scared = d < FLEE_R.

Loop:
```js
const clock = new THREE.Clock();
let t = 0;
function animate(){
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  t += dt;
  // demo cursor
  if (!hasPointer) mouseNDC.set(Math.cos(t * 0.55) * 0.62, Math.sin(t * 0.8) * 0.42);
  raycaster.setFromCamera(mouseNDC, camera);
  const ray = raycaster.ray;
  if (Math.abs(ray.direction.z) > 1e-6){
    const k = -ray.origin.z / ray.direction.z;
    mouseWorld.copy(ray.origin).addScaledVector(ray.direction, k);
  }
  mouseWorld.x = THREE.MathUtils.clamp(mouseWorld.x, -LIM.x, LIM.x);
  mouseWorld.y = THREE.MathUtils.clamp(mouseWorld.y, LIM.yB, LIM.yT);
  mouseWorld.z = THREE.MathUtils.clamp(mouseWorld.z, -LIM.z, LIM.z);

  for (const f of fishes) updateFish(f, dt, t);
  ... bubbles, plants, shafts, surface, camera ...
  renderer.render(scene, camera);
}
animate();
```

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

updateFish needs mouseWorld, LIM, FLEE_R, _desired, _look in scope — define as module-level.

One subtlety: raycaster.setFromCamera with camera looking at origin from z=24 — ray.direction.z negative; plane z=0 intersection k = -origin.z/dir.z = -24/neg = positive. good.

Also initial mouseNDC (0, 0.2) fine.

Check that `f.g.lookAt` each frame after position set — order: set position then lookAt. Yes.

Tail wag rotation.y on tailPivot — pivot at z=-0.62 relative to group; rotating y swings tail side to side. Good.

finPivots: p.rotation.x flap — pivot at side of body; rotating x tips fin. ok.

Now, potential visual: fish eyes black spheres radius 0.075 at x ±0.20 — body half-width 0.275 at center; at z 0.55 half-width 0.217 → eye center 0.20 inside, radius .075 → protrudes to 0.275. visible. good.

Dorsal cone at y 0.40, body top 0.425 → base slightly inside, apex up. good.

Everything seems fine. Also add `scene.add(new THREE.HemisphereLight(0x9fc8ff, 0x1a2a3a, 0.4))` maybe instead of ambient — keep ambient simple.

Check colors: sand 0xc9b083 with fog... fine.

One more: pebbles InstancedMesh with MeshPhongMaterial and setColorAt — need `pebbles.instanceColor.needsUpdate = true` after loop (before first render it's ok to set needsUpdate true). Add it.

Also `pebMat` flatShading true.

Rocks: use flatShading, color variations: give each rock slightly different color via material per rock (6 materials fine).

Plants castShadow true — cones cast; ok.

Now, does the directional shadow cover whole tank? light at (8,22,10), target default (0,0,0). Shadow camera ortho ±20 covers tank (±15, ±9). far 60: distance from light to origin ~ sqrt(64+484+100)=~25.5; far 60 covers. near 2. ok.

Fish castShadow on body/tail — set body.castShadow = true, tail.castShadow = true. fins small skip.

Sand receiveShadow = true; pebbles receiveShadow true.

I think we're set. Also ensure `#count` element exists.

Also, hint about keyboard? Not needed.

Let me also double check import map usage: `<script type="importmap">` must appear before module script — yes. Exact JSON as given.

Potential pitfall: In some sandbox, `https://cdn.jsdelivr.net` allowed since task explicitly gives it. Good.

Now write final answer: single html code block, no extra text.

Let me write the complete file mentally once more, ensuring syntax correctness.

Also, small thing: `const demoNote = document.getElementById('demoNote');` before use in onPointer — define before listener registration.

countEl similar.

Full code:

```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 — Scaredy Fish</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: #02101f; touch-action: none; }
  #c { display: block; width: 100%; height: 100%; }
  #vignette { position: fixed; inset: 0; pointer-events: none;
    background: radial-gradient(ellipse at 50% 42%, rgba(0,0,0,0) 52%, rgba(1,8,18,0.55) 100%); }
  #hud { position: fixed; top: 18px; left: 18px; pointer-events: none; color: #dff3ff; z-index: 10;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    background: rgba(6, 30, 52, 0.45); border: 1px solid rgba(140, 210, 255, 0.28);
    backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
    padding: 12px 16px; border-radius: 14px; max-width: 330px; }
  #hud h1 { font-size: 18px; font-weight: 700; letter-spacing: .4px; margin-bottom: 5px;
    background: linear-gradient(90deg, #8fd8ff, #c8f4ff 55%, #ffd9a0);
    -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent; }
  #hud .hint { font-size: 13px; line-height: 1.45; }
  #hud .sub { font-size: 11px; margin-top: 6px; opacity: .7; transition: opacity .6s; }
  @media (max-width: 480px) {
    #hud { top: 10px; left: 10px; right: 10px; max-width: none; padding: 10px 12px; }
    #hud h1 { font-size: 15px; } #hud .hint { font-size: 12px; } #hud .sub { font-size: 10.5px; }
  }
</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>
<canvas id="c"></canvas>
<div id="vignette"></div>
<div id="hud">
  <h1>🫧 Deep Blue Aquarium</h1>
  <p class="hint">Move your mouse (or drag a finger) near a fish — it will dart away!</p>
  <p class="sub" id="demoNote">✨ a demo cursor is swimming around until you move your pointer…</p>
  <p class="sub" id="count">😱 0 fish startled</p>
</div>

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

/* renderer */
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x0a2c4e, 20, 62);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 300);
camera.position.set(0, 1.4, 24);

/* gradient backdrop */
const bg = new THREE.Mesh(
  new THREE.SphereGeometry(140, 32, 20),
  new THREE.ShaderMaterial({
    side: THREE.BackSide, depthWrite: false,
    uniforms: {
      top: { value: new THREE.Color(0x1e6ea6) },
      mid: { value: new THREE.Color(0x0a3560) },
      bot: { value: new THREE.Color(0x020d1c) }
    },
    vertexShader: 'varying vec3 vP; void main(){ vP = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }',
    fragmentShader: `varying vec3 vP; uniform vec3 top; uniform vec3 mid; uniform vec3 bot;
      void main(){ float h = normalize(vP).y;
        vec3 c = h > 0.0 ? mix(mid, top, pow(h, 0.65)) : mix(mid, bot, pow(-h, 0.85));
        gl_FragColor = vec4(c, 1.0); }`
  })
);
scene.add(bg);

/* lights */
scene.add(new THREE.AmbientLight(0x7fa8d0, 0.55));
const sun = new THREE.DirectionalLight(0xd6ecff, 1.8);
sun.position.set(8, 22, 10);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
sun.shadow.camera.left = -20; sun.shadow.camera.right = 20;
sun.shadow.camera.top = 20;   sun.shadow.camera.bottom = -20;
sun.shadow.camera.near = 2;    sun.shadow.camera.far = 60;
sun.shadow.camera.updateProjectionMatrix();
sun.shadow.bias = -0.002;
scene.add(sun);
const glow = new THREE.PointLight(0x3f7fbf, 80, 90, 2);
glow.position.set(-9, 3, -5);
scene.add(glow);

/* tank constants */
const LIM = { x: 13.4, yT: 5.9, yB: -6.1, z: 7.4 };
function sandH(x, z){ return Math.sin(x * 0.55) * Math.cos(z * 0.75) * 0.22 + Math.sin(x * 1.6 + z * 1.25) * 0.07; }

/* glass */
const glass = new THREE.Mesh(
  new THREE.BoxGeometry(30, 14, 18),
  new THREE.MeshPhongMaterial({ color: 0x9fd4ff, transparent: true, opacity: 0.07, shininess: 140, specular: 0xffffff, side: THREE.DoubleSide, depthWrite: false })
);
scene.add(glass);
scene.add(new THREE.LineSegments(
  new THREE.EdgesGeometry(glass.geometry),
  new THREE.LineBasicMaterial({ color: 0xbfe6ff, transparent: true, opacity: 0.45 })
));

/* water surface */
const surface = new THREE.Mesh(
  new THREE.PlaneGeometry(30, 18),
  new THREE.MeshPhongMaterial({ color: 0x2f86c4, transparent: true, opacity: 0.42, shininess: 220, specular: 0xcfeaff, depthWrite: false })
);
surface.rotation.x = -Math.PI / 2;
surface.position.y = 6.95;
scene.add(surface);

/* sand */
const sandGeo = new THREE.PlaneGeometry(30, 18, 48, 28);
sandGeo.rotateX(-Math.PI / 2);
{
  const p = sandGeo.attributes.position;
  for (let i = 0; i < p.count; i++) p.setY(i, sandH(p.getX(i), p.getZ(i)));
  sandGeo.computeVertexNormals();
}
const sand = new THREE.Mesh(sandGeo, new THREE.MeshPhongMaterial({ color: 0xc9b083, shininess: 8 }));
sand.position.y = -7;
sand.receiveShadow = true;
scene.add(sand);

/* pebbles */
const pebbles = new THREE.InstancedMesh(
  new THREE.DodecahedronGeometry(0.16, 0),
  new THREE.MeshPhongMaterial({ flatShading: true }),
  140
);
{
  const m = new THREE.Matrix4(), q = new THREE.Quaternion(), v = new THREE.Vector3(), s = new THREE.Vector3(), c = new THREE.Color();
  for (let i = 0; i < 140; i++){
    const x = (Math.random() * 2 - 1) * 14, z = (Math.random() * 2 - 1) * 8;
    v.set(x, -7 + sandH(x, z) + 0.05, z);
    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.6 + Math.random() * 0.6), sc);
    m.compose(v, q, s);
    pebbles.setMatrixAt(i, m);
    c.setHSL(0.08 + Math.random() * 0.06, 0.25 + Math.random() * 0.2, 0.35 + Math.random() * 0.25);
    pebbles.setColorAt(i, c);
  }
  pebbles.instanceColor.needsUpdate = true;
  pebbles.receiveShadow = true;
}
scene.add(pebbles);

/* rocks */
for (let i = 0; i < 6; i++){
  const r = new THREE.Mesh(
    new THREE.DodecahedronGeometry(0.6 + Math.random() * 1.1, 0),
    new THREE.MeshPhongMaterial({ color: new THREE.Color().setHSL(0.58, 0.08 + Math.random() * 0.08, 0.32 + Math.random() * 0.15), flatShading: true, shininess: 20 })
  );
  const x = (Math.random() * 2 - 1) * 12, z = (Math.random() * 2 - 1) * 7;
  r.position.set(x, -7 + sandH(x, z) + 0.25, z);
  r.scale.y = 0.6 + Math.random() * 0.5;
  r.rotation.set(Math.random() * 3, Math.random() * 3, 0);
  r.castShadow = r.receiveShadow = true;
  scene.add(r);
}

/* plants */
const plantMats = [0x2f8f5b, 0x3aa76d, 0x247a52].map(c => new THREE.MeshPhongMaterial({ color: c, shininess: 30 }));
const plants = [];
for (let i = 0; i < 9; i++){
  const g = new THREE.Group();
  const blades = 3 + Math.floor(Math.random() * 3);
  for (let b = 0; b < blades; b++){
    const h = 2 + Math.random() * 2.8;
    const geo = new THREE.ConeGeometry(0.10 + Math.random() * 0.08, h, 6, 4);
    geo.translate(0, h / 2, 0);
    const mesh = new THREE.Mesh(geo, plantMats[i % 3]);
    mesh.position.set((Math.random() * 2 - 1) * 0.5, 0, (Math.random() * 2 - 1) * 0.5);
    mesh.rotation.z = (Math.random() * 2 - 1) * 0.2;
    mesh.castShadow = true;
    g.add(mesh);
  }
  const x = (Math.random() * 2 - 1) * 12.5, z = (Math.random() * 2 - 1) * 7.5;
  g.position.set(x, -7 + sandH(x, z) + 0.05, z);
  g.userData.phase = Math.random() * 10;
  g.userData.amp = 0.08 + Math.random() * 0.08;
  scene.add(g);
  plants.push(g);
}

/* light shafts */
const shafts = [];
for (let i = 0; i < 4; i++){
  const h = 15;
  const geo = new THREE.ConeGeometry(1.6 + Math.random() * 1.8, h, 20, 1, true);
  geo.translate(0, -h / 2, 0);
  const mat = new THREE.MeshBasicMaterial({ color: 0xaadfff, transparent: true, opacity: 0.06, blending: THREE.AdditiveBlending, side: THREE.DoubleSide, depthWrite: false, fog: false });
  const m = new THREE.Mesh(geo, mat);
  m.position.set((Math.random() * 2 - 1) * 9, 6.9, (Math.random() * 2 - 1) * 5);
  m.rotation.z = (Math.random() * 2 - 1) * 0.12;
  m.rotation.x = (Math.random() * 2 - 1) * 0.12;
  m.userData.phase = Math.random() * 10;
  scene.add(m);
  shafts.push(m);
}

/* bubbles */
const bubbles = [];
const bubGeo = new THREE.SphereGeometry(0.09, 8, 6);
const bubMat = new THREE.MeshPhongMaterial({ color: 0xcfeaff, transparent: true, opacity: 0.55, shininess: 120, specular: 0xffffff });
for (let i = 0; i < 36; i++){
  const b = new THREE.Mesh(bubGeo, bubMat);
  b.position.set((Math.random() * 2 - 1) * 13, -6.5 + Math.random() * 13, (Math.random() * 2 - 1) * 7.5);
  b.scale.setScalar(0.4 + Math.random() * 0.9);
  b.userData.speed = 0.7 + Math.random() * 1.1;
  b.userData.wob = Math.random() * 10;
  scene.add(b);
  bubbles.push(b);
}

/* fish */
function makeFish(colorHex){
  const g = new THREE.Group();
  const color = new THREE.Color(colorHex);
  const bodyMat = new THREE.MeshPhongMaterial({ color, shininess: 90, specular: 0xaaddff });
  const body = new THREE.Mesh(new THREE.SphereGeometry(0.5, 20, 14), bodyMat);
  body.scale.set(0.55, 0.85, 1.8);
  body.castShadow = true;
  g.add(body);

  const finMat = new THREE.MeshPhongMaterial({ color: color.clone().multiplyScalar(0.8), shininess: 60, transparent: true, opacity: 0.92 });
  const tailPivot = new THREE.Group();
  tailPivot.position.set(0, 0, -0.62);
  const tail = new THREE.Mesh(new THREE.ConeGeometry(0.40, 0.75, 10), finMat);
  tail.rotation.x = -Math.PI / 2;
  tail.scale.set(0.28, 1, 1);
  tail.castShadow = true;
  tailPivot.add(tail);
  g.add(tailPivot);

  const dorsal = new THREE.Mesh(new THREE.ConeGeometry(0.22, 0.5, 8), finMat);
  dorsal.scale.z = 0.3;
  dorsal.position.set(0, 0.40, -0.05);
  dorsal.rotation.x = 0.5;
  g.add(dorsal);

  const eyeMat = new THREE.MeshPhongMaterial({ color: 0x101418, shininess: 120 });
  for (const sx of [-1, 1]){
    const eye = new THREE.Mesh(new THREE.SphereGeometry(0.075, 10, 8), eyeMat);
    eye.position.set(sx * 0.20, 0.16, 0.55);
    g.add(eye);
  }

  const finPivots = [];
  for (const sx of [-1, 1]){
    const p = new THREE.Group();
    p.position.set(sx * 0.26, -0.08, 0.15);
    const f = new THREE.Mesh(new THREE.ConeGeometry(0.16, 0.45, 6), finMat);
    f.rotation.z = -sx * Math.PI / 2;
    f.scale.set(0.3, 1, 0.6);
    p.add(f);
    g.add(p);
    finPivots.push({ p, sx });
  }
  return { g, body, bodyMat, tailPivot, finPivots };
}

const palette = [0xff9f43, 0xff6b6b, 0x4ecdc4, 0xa29bfe, 0xfdcb6e, 0x74b9ff, 0x55efc4, 0xff7675, 0xe17055, 0x00cec9, 0x6c5ce7, 0xf8a5c2];
const fishes = [];
for (let i = 0; i < palette.length; i++){
  const parts = makeFish(palette[i]);
  parts.g.scale.setScalar(0.75 + Math.random() * 0.65);
  scene.add(parts.g);
  fishes.push({
    ...parts,
    pos: new THREE.Vector3((Math.random() * 2 - 1) * 12, -3 + Math.random() * 6, (Math.random() * 2 - 1) * 6),
    vel: new THREE.Vector3(Math.random() - 0.5, 0, Math.random() - 0.5).normalize().multiplyScalar(2),
    target: new THREE.Vector3((Math.random() * 2 - 1) * 12, -4 + Math.random() * 8, (Math.random() * 2 - 1) * 6.5),
    speed: 1.7 + Math.random() * 1.5,
    phase: Math.random() * 20,
    retarget: Math.random() * 4,
    scared: false
  });
}

/* pointer */
const mouseNDC = new THREE.Vector2(0, 0.2);
const mouseWorld = new THREE.Vector3(0, 0, 0);
let hasPointer = false;
const demoNote = document.getElementById('demoNote');
const countEl = document.getElementById('count');
function onPointer(e){
  hasPointer = true;
  mouseNDC.set((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
  if (demoNote) demoNote.style.opacity = '0';
}
window.addEventListener('pointermove', onPointer, { passive: true });
window.addEventListener('pointerdown', onPointer, { passive: true });

const raycaster = new THREE.Raycaster();
const FLEE_R = 5.2;
const _desired = new THREE.Vector3();
const _look = new THREE.Vector3();

function updateFish(f, dt, t){
  f.retarget -= dt;
  if (f.retarget <= 0 || f.pos.distanceTo(f.target) < 1.2){
    f.target.set((Math.random() * 2 - 1) * 12.5, -4.5 + Math.random() * 8.5, (Math.random() * 2 - 1) * 6.8);
    f.retarget = 4 + Math.random() * 5;
  }
  _desired.copy(f.target).sub(f.pos).normalize().multiplyScalar(f.speed);

  const dx = f.pos.x - mouseWorld.x;
  const dy = f.pos.y - mouseWorld.y;
  const dz = f.pos.z - mouseWorld.z;
  const d = Math.sqrt(dx * dx + dy * dy + dz * dz);
  f.scared = d < FLEE_R;
  if (f.scared && d > 1e-4){
    const s = (1 - d / FLEE_R) * 9.5 + 2.5;
    _desired.x += (dx / d) * s;
    _desired.y += (dy / d) * s;
    _desired.z += (dz / d) * s;
  }
  f.bodyMat.emissive.setScalar(f.scared ? (1 - Math.min(d, FLEE_R) / FLEE_R) * 0.35 : 0);

  const m = 1.4;
  if (f.pos.x < -LIM.x + m) _desired.x += (-LIM.x + m - f.pos.x) * 3;
  if (f.pos.x >  LIM.x - m) _desired.x -= (f.pos.x - (LIM.x - m)) * 3;
  if (f.pos.y <  LIM.yB + m) _desired.y += (LIM.yB + m - f.pos.y) * 3;
  if (f.pos.y >  LIM.yT - m) _desired.y -= (f.pos.y - (LIM.yT - m)) * 3;
  if (f.pos.z < -LIM.z + m) _desired.z += (-LIM.z + m - f.pos.z) * 3;
  if (f.pos.z >  LIM.z - m) _desired.z -= (f.pos.z - (LIM.z - m)) * 3;

  f.vel.lerp(_desired, 1 - Math.exp(-2.8 * dt));
  if (f.vel.length() > 10) f.vel.setLength(10);
  const sp = f.vel.length();
  f.pos.addScaledVector(f.vel, dt);

  let clamped = false;
  if (f.pos.x < -LIM.x){ f.pos.x = -LIM.x; clamped = true; } else if (f.pos.x > LIM.x){ f.pos.x = LIM.x; clamped = true; }
  if (f.pos.y <  LIM.yB){ f.pos.y = LIM.yB; clamped = true; } else if (f.pos.y > LIM.yT){ f.pos.y = LIM.yT; clamped = true; }
  if (f.pos.z < -LIM.z){ f.pos.z = -LIM.z; clamped = true; } else if (f.pos.z > LIM.z){ f.pos.z = LIM.z; clamped = true; }
  if (clamped) f.target.set((Math.random() * 2 - 1) * 12, -4 + Math.random() * 8, (Math.random() * 2 - 1) * 6.5);

  f.g.position.copy(f.pos);
  if (sp > 0.15){
    _look.copy(f.pos).add(f.vel);
    f.g.lookAt(_look);
  }
  const wag = Math.sin(t * (5 + sp * 2.5) + f.phase) * (0.28 + Math.min(sp, 7) * 0.09);
  f.tailPivot.rotation.y = wag;
  f.body.rotation.y = Math.sin(t * 4 + f.phase) * 0.07;
  for (const fp of f.finPivots){
    fp.p.rotation.x = Math.sin(t * 7 + f.phase + fp.sx * 2) * 0.4;
  }
}

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

/* loop */
const clock = new THREE.Clock();
let t = 0;
let scaredShown = -1;
function animate(){
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  t += dt;

  if (!hasPointer) mouseNDC.set(Math.cos(t * 0.55) * 0.62, Math.sin(t * 0.8) * 0.42);

  raycaster.setFromCamera(mouseNDC, camera);
  const ray = raycaster.ray;
  if (Math.abs(ray.direction.z) > 1e-6){
    const k = -ray.origin.z / ray.direction.z;
    mouseWorld.copy(ray.origin).addScaledVector(ray.direction, k);
  }
  mouseWorld.x = THREE.MathUtils.clamp(mouseWorld.x, -LIM.x, LIM.x);
  mouseWorld.y = THREE.MathUtils.clamp(mouseWorld.y, LIM.yB, LIM.yT);
  mouseWorld.z = THREE.MathUtils.clamp(mouseWorld.z, -LIM.z, LIM.z);

  for (const f of fishes) updateFish(f, dt, t);

  let sc = 0;
  for (const f of fishes) if (f.scared) sc++;
  if (sc !== scaredShown){ scaredShown = sc; countEl.textContent = '😱 ' + sc + (sc === 1 ? ' fish' : ' fish') + ' startled'; }

  for (const b of bubbles){
    b.position.y += b.userData.speed * dt;
    b.position.x += Math.sin(t * 2.5 + b.userData.wob) * 0.006;
    b.position.z += Math.cos(t * 2.0 + b.userData.wob) * 0.006;
    if (b.position.y > 6.6){
      b.position.y = -6.6;
      b.position.x = (Math.random() * 2 - 1) * 13;
      b.position.z = (Math.random() * 2 - 1) * 7.5;
    }
  }
  for (const p of plants){
    p.rotation.z = Math.sin(t * 1.1 + p.userData.phase) * p.userData.amp;
    p.rotation.x = Math.cos(t * 0.85 + p.userData.phase) * p.userData.amp * 0.6;
  }
  for (const s of shafts){
    s.material.opacity = 0.045 + 0.03 * (0.5 + 0.5 * Math.sin(t * 0.4 + s.userData.phase));
    s.rotation.y = t * 0.05 + s.userData.phase;
  }
  surface.position.y = 6.95 + Math.sin(t * 0.7) * 0.06;
  surface.material.opacity = 0.38 + 0.07 * Math.sin(t * 1.1);

  camera.position.x += (mouseNDC.x * 2.4 - camera.position.x) * 0.04;
  camera.position.y += (1.4 - mouseNDC.y * 1.6 - camera.position.y) * 0.04;
  camera.lookAt(0, -0.3, 0);

  renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
```

Check: `countEl` could be null? It exists. ok.

One concern: `f.g.lookAt(_look)` when group has scale — fine.

Another: initial camera aspect uses window sizes — fine.

`THREE.MathUtils.clamp` exists.

Flee when d extremely small: guarded.

Emissive setScalar each frame — ok.

`raycaster.ray` — after setFromCamera, ray is set. ok.

Potential z-fighting of surface plane at y=6.95 vs glass top edge at y=7 — fine.

Glass edges lines: BoxGeometry edges — LineSegments with glass.geometry shared; fine.

The bg sphere radius 140, camera far 300 — ok. Fog doesn't affect ShaderMaterial (no fog chunks) — good.

One more check: MeshPhongMaterial with `transparent: true` for finMat opacity .92 — fins render in transparent pass; slight sorting issues with body? Body is opaque, fins transparent — fins render after, fine.

Bubbles share one material; opacity same for all — fine.

Shadow: sun.target default (0,0,0) added to scene? DirectionalLight target needs to be in scene for matrix updates — default target object is not added but three updates target matrix only if target.parent... Actually if light.target not in scene, its matrixWorld stays identity at origin — which is what we want (origin). three.js docs recommend adding target to scene, but default at origin works since matrixWorld identity. LightShadow.updateMatrices uses light.target.matrixWorld — identity → origin. Good.

Check plant sway rotation pivots at base since geometry translated — group origin at base. Yes.

Check shaft rotation.y spin — rotates around its own center axis (apex top) — slight wobble ok.

Edge: `powerPreference` fine.

Mobile: pointermove fires on touch drag in Chrome — yes with touch-action none.

I think the code is solid. One more consideration on the "3 seconds screenshot": demo cursor moves in circle radius ~0.62 NDC → world x up to ~±? At plane z=0 from camera z=24 fov 55: half-height at z=0 plane = tan(27.5°)*24 ≈ 12.5; half-width = 12.5*aspect(1.6) = 20. NDC 0.62 → x ≈ 12.4, y ≈ 5.2 — within tank, clamped. So demo pointer sweeps across the tank scaring fish near it. At t=3: cos(1.65)≈-0.08, sin(2.4)≈0.67 → NDC (-0.05, 0.28) → near center-ish. Fish near center will flee. Good — visible action.

Also initial fish random positions; fine.

Font: system fonts only. No external requests besides three CDN. Good.

Double-check no localStorage etc. None.

Write final answer with just the code block.