Models 44

MiniMax M3.1 Flash

by MiniMaxReasoning32 console errors
Open in new window

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

How it went

Total time
1 min 59 s
Cost
$0.014
Output tokens
23,996
Spent thinking
11,224 tokens
Speed
202.9 tok/s
Console errors
32
First token after
684 ms
First visible text after
1 min 10 s
Prompt tokens
613
Code size
35.7 KB
Finish reason
stop
Served by
Nebius
Model id
minimax/minimax-m3.1-flash
Reasoning sent
{"effort":"high"}
Max output
64,000 tokens
Generated
Nov 11, 2026

Console errors

Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.

Cannot set properties of undefined (setting 'x')

Cannot set properties of undefined (setting 'x')

Cannot set properties of undefined (setting 'x')

Cannot set properties of undefined (setting 'x')

Cannot set properties of undefined (setting 'x')

Cannot set properties of undefined (setting 'x')

Cannot set properties of undefined (setting 'x')

Cannot set properties of undefined (setting 'x')

Cannot set properties of undefined (setting 'x')

Cannot set properties of undefined (setting 'x')

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 · 814 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Reef Nocturne — Interactive 3D Aquarium</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#03121c;
    font-family:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    color:#dff3ff;-webkit-font-smoothing:antialiased}
  #scene{position:fixed;inset:0;width:100%;height:100%;display:block;touch-action:none;cursor:crosshair}

  #ui{position:fixed;inset:0;pointer-events:none;z-index:5}
  .brand{position:absolute;top:22px;left:24px;display:flex;gap:12px;align-items:center}
  .pulse{width:12px;height:12px;border-radius:50%;background:#4fe3ff;box-shadow:0 0 0 0 rgba(79,227,255,.7);
    animation:pulse 2.2s infinite}
  @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(79,227,255,.6)}70%{box-shadow:0 0 0 16px rgba(79,227,255,0)}100%{box-shadow:0 0 0 0 rgba(79,227,255,0)}}
  .brand h1{margin:0;font-size:19px;letter-spacing:.22em;font-weight:600;
    background:linear-gradient(90deg,#b8f4ff,#3fb6ff);-webkit-background-clip:text;background-clip:text;color:transparent}
  .brand p{margin:3px 0 0;font-size:11.5px;letter-spacing:.09em;color:#79a8c4}

  .panel{position:absolute;left:24px;bottom:24px;pointer-events:auto;width:252px;padding:15px 16px 13px;
    border-radius:16px;background:rgba(8,28,42,.62);border:1px solid rgba(120,220,255,.18);
    backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 18px 44px rgba(0,0,0,.45)}
  .row{display:flex;align-items:center;gap:9px;margin-bottom:11px;font-size:11px;letter-spacing:.07em;color:#8fc3dd}
  .row label{flex:0 0 74px;text-transform:uppercase;font-size:9.5px;color:#6ea6c4}
  .row span{flex:0 0 40px;text-align:right;color:#cdf0ff;font-variant-numeric:tabular-nums}
  input[type=range]{flex:1;-webkit-appearance:none;appearance:none;height:4px;border-radius:4px;background:rgba(120,220,255,.22);outline:none}
  input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;border-radius:50%;
    background:#5fe1ff;box-shadow:0 0 10px rgba(95,225,255,.8);cursor:pointer}
  input[type=range]::-moz-range-thumb{width:13px;height:13px;border:0;border-radius:50%;background:#5fe1ff;cursor:pointer}
  .btns{display:flex;gap:8px;margin:13px 0 10px}
  button{flex:1;padding:8px 6px;border-radius:10px;border:1px solid rgba(120,220,255,.25);
    background:rgba(60,180,230,.14);color:#d8f4ff;font-size:10.5px;letter-spacing:.08em;cursor:pointer;
    text-transform:uppercase;transition:.18s;font-weight:600}
  button:hover{background:rgba(80,210,255,.3);border-color:rgba(140,235,255,.6);transform:translateY(-1px)}
  button:active{transform:translateY(0)}
  .hint{font-size:10px;color:#5f92ae;letter-spacing:.06em;border-top:1px solid rgba(120,220,255,.13);padding-top:9px}

  .stats{position:absolute;right:24px;top:22px;text-align:right;font-size:10.5px;letter-spacing:.12em;color:#79a8c4;line-height:1.9}
  .stats b{color:#cdf0ff;font-weight:600}
  .stats .tag{display:inline-block;padding:3px 9px;border-radius:20px;border:1px solid rgba(120,220,255,.22);
    background:rgba(8,28,42,.55);font-size:9px;letter-spacing:.16em;color:#8fe3ff}

  #overlay{position:fixed;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;
    background:radial-gradient(ellipse at center,rgba(3,18,28,.30),rgba(2,10,17,.74));
    transition:opacity .5s ease,visibility .5s}
  #overlay.gone{opacity:0;visibility:hidden;pointer-events:none}
  .card{text-align:center;padding:34px 46px;border-radius:22px;background:rgba(6,26,40,.66);
    border:1px solid rgba(120,220,255,.22);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
    box-shadow:0 30px 80px rgba(0,0,0,.55);max-width:min(90vw,460px)}
  .card .key{display:inline-block;font-size:10px;letter-spacing:.34em;color:#63d9ff;border:1px solid rgba(120,220,255,.3);
    border-radius:30px;padding:6px 16px;margin-bottom:18px}
  .card h2{margin:0 0 10px;font-size:27px;font-weight:600;letter-spacing:.01em;color:#eafaff}
  .card p{margin:0;font-size:12.5px;line-height:1.75;color:#8fb9d0}
  .card p b{color:#cdf0ff;font-weight:600}
  .blink{animation:bl 1.5s ease-in-out infinite}
  @keyframes bl{0%,100%{opacity:.45}50%{opacity:1}}

  @media (max-width:760px){
    .brand{top:14px;left:14px}.brand h1{font-size:15px}.brand p{font-size:10px;max-width:220px}
    .panel{left:12px;right:12px;bottom:12px;width:auto;padding:12px 13px}
    .stats{right:14px;top:14px;font-size:9.5px}
    .card{padding:26px 24px}.card h2{font-size:21px}
    .row label{flex-basis:66px}
  }
</style>
</head>
<body>
<canvas id="scene"></canvas>

<div id="ui">
  <div class="brand">
    <div class="pulse"></div>
    <div>
      <h1>REEF NOCTURNE</h1>
      <p>Interactive 3D aquarium — glide the cursor, the fish scatter</p>
    </div>
  </div>

  <div class="panel">
    <div class="row"><label>Population</label><input type="range" id="fishCount" min="4" max="26" value="15"><span id="fishCountVal">15</span></div>
    <div class="row"><label>Fear radius</label><input type="range" id="fear" min="0.6" max="3.6" step="0.1" value="1.9"><span id="fearVal">1.9m</span></div>
    <div class="btns">
      <button id="feed">Feed</button>
      <button id="bubbles">Bubbles on</button>
    </div>
    <div class="hint">Drag to orbit the tank · scroll to zoom · move the cursor near a fish to scare it</div>
  </div>

  <div class="stats">
    <div><span class="tag" id="modeTag">DEMO CURSOR</span></div>
    <div>Fish <b id="stFish">15</b></div>
    <div>Startled <b id="stScared">0</b></div>
    <div>Fed <b id="stFed">0</b></div>
  </div>
</div>

<div id="overlay">
  <div class="card">
    <div class="key">AQUARIUM READY</div>
    <h2 class="blink">Click or press a key to start</h2>
    <p>The fish are already swimming. <b>Move your cursor through the water</b> and they will dart away.<br>
       Drag to orbit, scroll to zoom, and drop food to lure them back.</p>
  </div>
</div>

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

<script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';

/* ------------------------------------------------------------------ utils */
const rand = (a,b)=>a+Math.random()*(b-a);
const clamp = (v,a,b)=>Math.min(b,Math.max(a,v));
const TAU = Math.PI*2;

/* --------------------------------------------------------------- renderer */
const canvas = document.getElementById('scene');
const renderer = new THREE.WebGLRenderer({canvas, antialias:true, powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(window.devicePixelRatio||1, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.18;

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x052335, 0.017);

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

/* background gradient */
(function bg(){
  const c=document.createElement('canvas'); c.width=8; c.height=256;
  const g=c.getContext('2d').createLinearGradient(0,0,0,256);
  g.addColorStop(0,'#11506f'); g.addColorStop(.42,'#0a2f4a');
  g.addColorStop(.78,'#061c2c'); g.addColorStop(1,'#030d16');
  const ctx=c.getContext('2d'); ctx.fillStyle=g; ctx.fillRect(0,0,8,256);
  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace;
  scene.background=t;
})();

/* ------------------------------------------------------------------ tank */
const T = {w:16, h:9, d:8};
const FLOOR = -T.h/2;
const BOUNDS = {x0:-T.w/2+0.9, x1:T.w/2-0.9, y0:FLOOR+0.7, y1:T.h/2-1.1, z0:-T.d/2+0.8, z1:T.d/2-0.8};

/* --- sand floor with dunes --- */
const sandGeo = new THREE.PlaneGeometry(T.w, T.d, 90, 50);
(function dunes(){
  const p = sandGeo.attributes.position;
  for(let i=0;i<p.count;i++){
    const x=p.getX(i), y=p.getY(i);
    let h = Math.sin(x*0.42)*0.20 + Math.cos(y*0.55+1.3)*0.17 + Math.sin((x+y)*0.22)*0.26;
    h += rand(-0.035,0.035);
    p.setZ(i,h);
  }
  sandGeo.computeVertexNormals();
})();
const sandTex = (function(){
  const c=document.createElement('canvas'); c.width=512; c.height=512;
  const x=c.getContext('2d');
  x.fillStyle='#cbb794'; x.fillRect(0,0,512,512);
  for(let i=0;i<9000;i++){
    x.fillStyle='rgba('+Math.floor(rand(140,225))+','+Math.floor(rand(120,200))+','+Math.floor(rand(90,165))+','+rand(.08,.32)+')';
    x.beginPath(); x.arc(rand(0,512),rand(0,512),rand(.5,2.1),0,TAU); x.fill();
  }
  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace;
  t.wrapS=t.wrapT=THREE.RepeatWrapping; t.repeat.set(4,2);
  return t;
})();
const sand = new THREE.Mesh(sandGeo, new THREE.MeshStandardMaterial({map:sandTex, color:0xd9c9a6, roughness:0.95, metalness:0}));
sand.rotation.x = -Math.PI/2;
sand.position.y = FLOOR;
scene.add(sand);

/* --- caustics overlay on the sand --- */
const causticUniforms = {uTime:{value:0}, uColor:{value:new THREE.Color(0x9ff3ff)}};
const caustic = new THREE.Mesh(
  new THREE.PlaneGeometry(T.w, T.d, 1, 1),
  new THREE.ShaderMaterial({
    uniforms: causticUniforms, transparent:true, depthWrite:false, blending:THREE.AdditiveBlending,
    vertexShader:`varying vec2 vUv; void main(){vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
    fragmentShader:`
      uniform float uTime; uniform vec3 uColor; varying vec2 vUv;
      float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}
      float noise(vec2 p){vec2 i=floor(p),f=fract(p);f=f*f*(3.0-2.0*f);
        float a=hash(i),b=hash(i+vec2(1,0)),c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1));
        return mix(mix(a,b,f.x),mix(c,d,f.x),f.y);}
      void main(){
        vec2 uv=vUv*vec2(9.0,5.0);
        float t=uTime*0.30;
        float n=noise(uv+vec2(t,-t*0.6));
        n+=noise(uv*1.8-vec2(t*0.7,t*0.35))*0.6;
        n+=noise(uv*3.3+vec2(t*1.2,t*0.8))*0.3;
        n/=1.9;
        float c=pow(smoothstep(0.46,0.95,n),2.2);
        float vign=smoothstep(0.0,0.35,vUv.x)*smoothstep(1.0,0.65,vUv.x)*smoothstep(0.0,0.3,vUv.y)*smoothstep(1.0,0.7,vUv.y);
        gl_FragColor=vec4(uColor,c*0.42*vign);
      }`
  })
);
caustic.rotation.x=-Math.PI/2; caustic.position.y=FLOOR+0.06; scene.add(caustic);

/* --- rocks --- */
function rockGeo(seed){
  const g=new THREE.IcosahedronGeometry(1,2);
  const p=g.attributes.position;
  for(let i=0;i<p.count;i++){
    const v=new THREE.Vector3().fromBufferAttribute(p,i);
    const n=1 + Math.sin(v.x*3.1+seed)*0.16 + Math.cos(v.y*2.6-seed)*0.14 + Math.sin(v.z*4.2)*0.09;
    v.multiplyScalar(n); v.y*=0.72;
    p.setXYZ(i,v.x,v.y,v.z);
  }
  g.computeVertexNormals(); return g;
}
const rockMat = new THREE.MeshStandardMaterial({color:0x54606b, roughness:0.95, metalness:0.05, flatShading:true});
for(let i=0;i<14;i++){
  const s=rand(0.35,1.5);
  const r=new THREE.Mesh(rockGeo(rand(0,10)), rockMat.clone());
  r.material.color.setHSL(rand(0.55,0.62), rand(0.08,0.2), rand(0.22,0.42));
  r.position.set(rand(BOUNDS.x0,BOUNDS.x1), FLOOR+s*0.28, rand(BOUNDS.z0,BOUNDS.z1));
  r.scale.set(s*rand(.9,1.5), s*rand(.7,1.1), s*rand(.9,1.5));
  r.rotation.set(rand(0,3),rand(0,3),rand(0,3));
  scene.add(r);
}

/* --- corals --- */
const coralGroup = new THREE.Group(); scene.add(coralGroup);
const coralColors=[0xff5f7e,0xffa63d,0x8f5cff,0x2ee6c0,0xff4fa0];
for(let i=0;i<16;i++){
  const col=coralColors[i%coralColors.length];
  const mat=new THREE.MeshStandardMaterial({color:col, roughness:0.55, metalness:0.05,
    emissive:new THREE.Color(col).multiplyScalar(0.25)});
  const type=i%3;
  let m;
  if(type===0){
    m=new THREE.Mesh(new THREE.IcosahedronGeometry(rand(0.35,0.75),2), mat);
    const p=m.geometry.attributes.position;
    for(let k=0;k<p.count;k++){
      const v=new THREE.Vector3().fromBufferAttribute(p,k);
      v.multiplyScalar(1+Math.sin(v.x*6+v.y*5)*0.14+Math.cos(v.z*7)*0.1);
      p.setXYZ(k,v.x,v.y,v.z);
    }
    m.geometry.computeVertexNormals();
    m.scale.y=0.8;
  } else if(type===1){
    m=new THREE.Group();
    const n=Math.floor(rand(3,7));
    for(let b=0;b<n;b++){
      const h=rand(0.4,1.4);
      const br=new THREE.Mesh(new THREE.CylinderGeometry(0.06,0.13,h,6), mat);
      br.position.set(rand(-0.2,0.2), h/2, rand(-0.2,0.2));
      br.rotation.set(rand(-0.4,0.4),rand(0,3),rand(-0.4,0.4));
      m.add(br);
      const tip=new THREE.Mesh(new THREE.SphereGeometry(0.09,8,6), mat);
      tip.position.copy(br.position); tip.position.y+=h/2; m.add(tip);
    }
  } else {
    m=new THREE.Mesh(new THREE.TorusKnotGeometry(0.32,0.10,80,10,2,3), mat);
    m.scale.setScalar(rand(0.7,1.2));
  }
  m.position.set(rand(BOUNDS.x0,BOUNDS.x1), FLOOR+rand(0.05,0.2), rand(BOUNDS.z0,BOUNDS.z1));
  m.rotation.y=rand(0,TAU);
  coralGroup.add(m);
}

/* --- seaweed (animated ribbons) --- */
const weeds=[];
function makeWeed(x,z,h,w,col){
  const g=new THREE.PlaneGeometry(w,h,1,10);
  g.translate(0,h/2,0);
  const mesh=new THREE.Mesh(g, new THREE.MeshStandardMaterial({color:col, roughness:0.75, metalness:0.0,
    side:THREE.DoubleSide, emissive:new THREE.Color(col).multiplyScalar(0.10)}));
  mesh.position.set(x, FLOOR, z);
  mesh.rotation.y=rand(0,TAU);
  scene.add(mesh);
  weeds.push({mesh, h, w, phase:rand(0,TAU), geo:g});
}
const weedCols=[0x1f7a4d,0x2c9c5c,0x1c6b57,0x39a86b,0x187a63];
for(let i=0;i<26;i++){
  makeWeed(rand(BOUNDS.x0-0.3,BOUNDS.x1+0.3), rand(BOUNDS.z0-0.3,BOUNDS.z1+0.3),
    rand(1.2,3.6), rand(0.16,0.42), weedCols[i%weedCols.length]);
}

/* --- glass walls + frame --- */
const glassMat = new THREE.MeshPhysicalMaterial({color:0x9fd9ff, transparent:true, opacity:0.07,
  roughness:0.06, metalness:0.0, transmission:0.0, side:THREE.DoubleSide, depthWrite:false});
const glassGroup=new THREE.Group();
function wall(w,h,x,y,z,ry){
  const m=new THREE.Mesh(new THREE.PlaneGeometry(w,h), glassMat);
  m.position.set(x,y,z); m.rotation.y=ry; glassGroup.add(m);
}
wall(T.w,T.h,0,0,-T.d/2,0);
wall(T.w,T.h,-T.w/2,0,0,Math.PI/2);
wall(T.w,T.h, T.w/2,0,0,Math.PI/2);
wall(T.w,T.h,0,0, T.d/2,0);
scene.add(glassGroup);
const frame=new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(T.w,T.h,T.d)),
  new THREE.LineBasicMaterial({color:0x8fd7ff, transparent:true, opacity:0.35}));
frame.position.y=0; scene.add(frame);

/* --- water surface --- */
const surfaceUniforms={uTime:{value:0}};
const surface=new THREE.Mesh(new THREE.PlaneGeometry(T.w,T.d,40,20),
  new THREE.ShaderMaterial({
    uniforms:surfaceUniforms, transparent:true, depthWrite:false, side:THREE.DoubleSide,
    vertexShader:`uniform float uTime; varying vec2 vUv; varying float vW;
      void main(){vUv=uv; vec3 p=position;
        float w=sin(p.x*1.6+uTime*1.5)*0.055+sin(p.y*2.1-uTime*1.1)*0.045+sin((p.x+p.y)*1.1+uTime*0.7)*0.03;
        p.z+=w; vW=w; gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.0);}`,
    fragmentShader:`uniform float uTime; varying vec2 vUv; varying float vW;
      void main(){
        float r=sin(vUv.x*46.0+uTime*1.4)*0.5+0.5;
        float r2=sin((vUv.x*23.0+vUv.y*17.0)-uTime*1.1)*0.5+0.5;
        vec3 col=mix(vec3(0.05,0.30,0.48),vec3(0.45,0.82,0.98),0.45+0.3*vW*8.0);
        float a=0.16+0.26*pow(r*r2,2.0);
        gl_FragColor=vec4(col,a);
      }`
  }));
surface.rotation.x=-Math.PI/2; surface.position.y=T.h/2-0.05; scene.add(surface);

/* --- light shafts --- */
const shaftTex=(function(){
  const c=document.createElement('canvas'); c.width=64;c.height=256;
  const x=c.getContext('2d');
  const g=x.createLinearGradient(0,0,0,256);
  g.addColorStop(0,'rgba(190,240,255,0.55)'); g.addColorStop(0.5,'rgba(150,220,255,0.18)');
  g.addColorStop(1,'rgba(120,200,255,0)');
  x.fillStyle=g; x.fillRect(0,0,64,256);
  // soften horizontal edges
  const gd=x.createLinearGradient(0,0,64,0);
  gd.addColorStop(0,'rgba(0,0,0,1)'); gd.addColorStop(0.5,'rgba(0,0,0,0)'); gd.addColorStop(1,'rgba(0,0,0,1)');
  x.globalCompositeOperation='destination-out'; x.fillStyle=gd; x.fillRect(0,0,64,256);
  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; return t;
})();
const shafts=[];
for(let i=0;i<6;i++){
  const m=new THREE.Mesh(new THREE.PlaneGeometry(rand(1.2,3.2), T.h),
    new THREE.MeshBasicMaterial({map:shaftTex, transparent:true, depthWrite:false, blending:THREE.AdditiveBlending,
      opacity:rand(0.25,0.55), side:THREE.DoubleSide}));
  m.position.set(rand(-T.w/2+1,T.w/2-1), 0.4, rand(-T.d/2+0.6,T.d/2-0.6));
  m.rotation.z=rand(-0.18,0.18);
  scene.add(m); shafts.push({m,phase:rand(0,TAU),base:m.material.opacity});
}

/* ----------------------------------------------------------------- lights */
scene.add(new THREE.HemisphereLight(0x9fd8ff, 0x14342c, 0.55));
const sun=new THREE.DirectionalLight(0xd6f2ff, 1.5);
sun.position.set(7,14,6); scene.add(sun);
const rim=new THREE.DirectionalLight(0x2a7fd0, 0.6); rim.position.set(-8,3,-6); scene.add(rim);
const lampA=new THREE.PointLight(0xff5fa8, 26, 14, 2); lampA.position.set(-5,0.4,-2); scene.add(lampA);
const lampB=new THREE.PointLight(0x35f0d0, 22, 14, 2); lampB.position.set(5.4,-0.6,1.5); scene.add(lampB);
const lampC=new THREE.PointLight(0x6aa8ff, 30, 20, 2); lampC.position.set(0,6,0); scene.add(lampC);

/* --------------------------------------------------------------- bubbles */
const bubbleGeo=new THREE.SphereGeometry(0.075,10,8);
const bubbleMat=new THREE.MeshPhysicalMaterial({color:0xdaf6ff, transparent:true, opacity:0.42,
  roughness:0.05, metalness:0.0, clearcoat:1.0, clearcoatRoughness:0.05, depthWrite:false});
const BUB=70;
const bubbles=new THREE.InstancedMesh(bubbleGeo,bubbleMat,BUB);
bubbles.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
const bState=[];
for(let i=0;i<BUB;i++) bState.push({x:rand(-T.w/2+0.5,T.w/2-0.5),y:rand(FLOOR,T.h/2-0.3),z:rand(-T.d/2+0.5,T.d/2-0.5),
  r:rand(0.4,1.6), sp:rand(0.5,1.5), ph:rand(0,TAU)});
scene.add(bubbles);
const dummy=new THREE.Object3D();

/* --------------------------------------------------------------- plankton */
const PLANK=420;
const pPos=new Float32Array(PLANK*3);
const pSeed=[];
for(let i=0;i<PLANK;i++){
  pPos[i*3]=rand(BOUNDS.x0,BOUNDS.x1); pPos[i*3+1]=rand(FLOOR,T.h/2-0.6); pPos[i*3+2]=rand(BOUNDS.z0,BOUNDS.z1);
  pSeed.push({ph:rand(0,TAU),sp:rand(.2,.8),dy:rand(.05,.3)});
}
const plankGeo=new THREE.BufferGeometry();
plankGeo.setAttribute('position', new THREE.BufferAttribute(pPos,3));
const plankton=new THREE.Points(plankGeo, new THREE.PointsMaterial({color:0xbfe9ff,size:0.05,
  transparent:true,opacity:0.55,depthWrite:false,blending:THREE.AdditiveBlending,sizeAttenuation:true}));
scene.add(plankton);

/* ------------------------------------------------------------------ fish */
const SPECIES=[
  {n:'Ember Tang',  body:0xff7a29, belly:0xffd9a0, accent:0xffffff, pattern:'bands', size:1.00},
  {n:'Sunburst',    body:0xffd23f, belly:0xfff4c8, accent:0x5a3d00, pattern:'grad',  size:0.85},
  {n:'Azure Chromis',body:0x35a7ff,belly:0xdaf1ff, accent:0x0a3a66, pattern:'grad',  size:0.80},
  {n:'Reef Clown',  body:0xff8c1a, belly:0xffe4bb, accent:0xffffff, pattern:'clown', size:0.95},
  {n:'Pearl Danio', body:0xe2ecf5, belly:0xffffff, accent:0x74a9c9, pattern:'stripe',size:0.72},
  {n:'Violet Bass', body:0xa06cff, belly:0xe8d8ff, accent:0x3d1f6b, pattern:'spots', size:1.05},
  {n:'Gold Scale',  body:0xffb703, belly:0xfff0b8, accent:0x6b4a00, pattern:'grad',  size:0.90},
  {n:'Mint Tang',   body:0x4fe0c0, belly:0xdcfff6, accent:0x0d5b4f, pattern:'bands', size:0.88},
];
const texCache={};
function fishTexture(sp){
  if(texCache[sp.n]) return texCache[sp.n];
  const c=document.createElement('canvas'); c.width=256;c.height=128;
  const x=c.getContext('2d');
  const base=new THREE.Color(sp.body), belly=new THREE.Color(sp.belly);
  const g=x.createLinearGradient(0,0,0,128);
  g.addColorStop(0,'#'+base.clone().multiplyScalar(1.12).getHexString());
  g.addColorStop(0.55,'#'+base.getHexString());
  g.addColorStop(1,'#'+belly.getHexString());
  x.fillStyle=g; x.fillRect(0,0,256,128);
  const acc='#'+new THREE.Color(sp.accent).getHexString();
  if(sp.pattern==='bands'){
    x.fillStyle=acc;
    for(let i=0;i<3;i++){ x.globalAlpha=0.75; x.fillRect(38+i*62,0,17,128); }
    x.globalAlpha=1;
  } else if(sp.pattern==='clown'){
    x.fillStyle=acc;
    for(let i=0;i<3;i++){ x.beginPath(); x.ellipse(62+i*58,64,17,58,0,0,TAU); x.fill(); }
    x.fillStyle='rgba(30,20,10,0.85)'; x.fillRect(0,0,256,10); x.fillRect(0,118,256,10);
  } else if(sp.pattern==='stripe'){
    x.fillStyle=acc;
    for(let i=0;i<5;i++){ x.globalAlpha=0.85; x.fillRect(20+i*48,52,8,30); }
    x.globalAlpha=1;
  } else if(sp.pattern==='spots'){
    x.fillStyle=acc;
    for(let i=0;i<34;i++){ x.globalAlpha=rand(.25,.6); x.beginPath();
      x.arc(rand(10,246),rand(18,110),rand(2.5,7),0,TAU); x.fill(); }
    x.globalAlpha=1;
  }
  // scales
  x.fillStyle='rgba(255,255,255,0.07)';
  for(let r=0;r<16;r++) for(let cc=0;cc<32;cc++){
    x.beginPath(); x.arc(cc*8+ (r%2?4:0), r*8+4, 4.2, 0, TAU); x.fill();
  }
  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace;
  texCache[sp.n]=t; return t;
}
function finGeo(pts){
  const s=new THREE.Shape();
  s.moveTo(pts[0][0],pts[0][1]);
  for(let i=1;i<pts.length;i++) s.lineTo(pts[i][0],pts[i][1]);
  s.closePath();
  return new THREE.ShapeGeometry(s);
}
const fishRoot=new THREE.Group(); scene.add(fishRoot);
const fishes=[];
function buildFish(sp){
  const g=new THREE.Group();
  const tex=fishTexture(sp);
  const bodyMat=new THREE.MeshStandardMaterial({map:tex, roughness:0.32, metalness:0.22,
    emissive:new THREE.Color(sp.body).multiplyScalar(0.14)});
  const body=new THREE.Mesh(new THREE.SphereGeometry(1,22,16), bodyMat);
  body.scale.set(0.26,0.34,0.60);
  g.add(body);
  const finMat=new THREE.MeshStandardMaterial({color:new THREE.Color(sp.accent).lerp(new THREE.Color(sp.body),0.35),
    transparent:true, opacity:0.85, side:THREE.DoubleSide, roughness:0.4, metalness:0.1,
    emissive:new THREE.Color(sp.body).multiplyScalar(0.10)});
  const tail=new THREE.Mesh(finGeo([[0,0],[-0.50,0.34],[-0.34,0],[-0.50,-0.34]]), finMat);
  tail.position.z=-0.56; g.add(tail);
  const dorsal=new THREE.Mesh(finGeo([[-0.30,0],[0.26,0],[0.02,0.34]]), finMat);
  dorsal.rotation.y=-Math.PI/2; dorsal.position.y=0.30; g.add(dorsal);
  const anal=new THREE.Mesh(finGeo([[-0.26,0],[0.10,0],[-0.06,-0.26]]), finMat);
  anal.rotation.y=-Math.PI/2; anal.rotation.z=Math.PI; anal.position.y=-0.30; g.add(anal);
  const pects=[];
  for(const s of [-1,1]){
    const p=new THREE.Mesh(finGeo([[0,0],[0.22,-0.09],[0.20,0.13]]), finMat);
    p.position.set(s*0.20,-0.07,0.20);
    p.rotation.y=s>0? -Math.PI/2+0.7 : Math.PI/2-0.7;
    g.add(p); pects.push(p);
  }
  const eyeMat=new THREE.MeshStandardMaterial({color:0x0a0a12, roughness:0.15, metalness:0.4});
  const eyeW=new THREE.MeshStandardMaterial({color:0xf2fbff, roughness:0.1});
  for(const s of [-1,1]){
    const e=new THREE.Mesh(new THREE.SphereGeometry(0.062,10,8), eyeMat);
    e.position.set(s*0.165,0.10,0.40); g.add(e);
    const gl=new THREE.Mesh(new THREE.SphereGeometry(0.022,6,5), eyeW);
    gl.position.set(s*0.195,0.145,0.435); g.add(gl);
  }
  g.scale.setScalar(sp.size);
  fishRoot.add(g);
  const f={g,body,tail,pect:dorsal?pects:pects,pect2:anal,sp,
    pos:new THREE.Vector3(rand(BOUNDS.x0,BOUNDS.x1),rand(BOUNDS.y0,BOUNDS.y1),rand(BOUNDS.z0,BOUNDS.z1)),
    vel:new THREE.Vector3(rand(-1,1),rand(-.3,.3),rand(-1,1)).normalize().multiplyScalar(1.1),
    target:new THREE.Vector3(), timer:0, phase:rand(0,TAU), fear:0, scale:sp.size};
  fishes.push(f);
  return f;
}
function removeFish(f){ fishRoot.remove(f.g); fishes.splice(fishes.indexOf(f),1); }
function setPopulation(n){
  while(fishes.length>n){ const f=fishes.pop(); fishRoot.remove(f.g); }
  while(fishes.length<n){ const sp=SPECIES[Math.floor(rand(0,SPECIES.length))]; buildFish(sp); }
  document.getElementById('fishCountVal').textContent=fishes.length;
  document.getElementById('stFish').textContent=fishes.length;
}
setPopulation(15);

/* ------------------------------------------------------------------ food */
const foods=[];
const foodGeo=new THREE.SphereGeometry(0.075,8,6);
const foodMat=new THREE.MeshStandardMaterial({color:0xff9a4d, emissive:0xff5a00, emissiveIntensity:0.5, roughness:0.7});
function dropFood(n, at){
  for(let i=0;i<n;i++){
    const m=new THREE.Mesh(foodGeo, foodMat);
    const p = at ? at.clone().add(new THREE.Vector3(rand(-1.2,1.2),rand(0.4,1.6),rand(-0.8,0.8)))
                 : new THREE.Vector3(rand(BOUNDS.x0,BOUNDS.x1), rand(1.0,3.2), rand(BOUNDS.z0,BOUNDS.z1));
    m.position.copy(p); scene.add(m);
    foods.push({m, vel:new THREE.Vector3(rand(-.15,.15),-rand(.25,.5),rand(-.15,.15)), life:14});
  }
}

/* --------------------------------------------------------- cursor marker */
const markerTex=(function(){
  const c=document.createElement('canvas'); c.width=128;c.height=128;
  const x=c.getContext('2d');
  const g=x.createRadialGradient(64,64,0,64,64,64);
  g.addColorStop(0,'rgba(190,245,255,0.95)'); g.addColorStop(0.25,'rgba(110,220,255,0.45)');
  g.addColorStop(0.6,'rgba(60,180,255,0.14)'); g.addColorStop(1,'rgba(40,140,255,0)');
  x.fillStyle=g; x.fillRect(0,0,128,128);
  x.strokeStyle='rgba(190,245,255,0.75)'; x.lineWidth=3;
  x.beginPath(); x.arc(64,64,42,0,TAU); x.stroke();
  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; return t;
})();
const marker=new THREE.Sprite(new THREE.SpriteMaterial({map:markerTex, transparent:true, depthWrite:false,
  blending:THREE.AdditiveBlending, opacity:0.85}));
marker.scale.set(1.6,1.6,1); scene.add(marker);

/* ------------------------------------------------------------- post fx */
const composer=new EffectComposer(renderer);
composer.addPass(new RenderPass(scene,camera));
const bloom=new UnrealBloomPass(new THREE.Vector2(innerWidth,innerHeight), 0.42, 0.75, 0.80);
composer.addPass(bloom);
composer.addPass(new OutputPass());

/* ---------------------------------------------------------- interaction */
const pointerNDC=new THREE.Vector2(0,0);
let lastUserMove=-1e9, manualPointer=false, started=false;
const raycaster=new THREE.Raycaster();
let fearRadius=1.9;
const camState={theta:0, phi:1.40, radius:17.4, tTheta:0, tPhi:1.40, tRadius:17.4};
const camTarget=new THREE.Vector3(0,-0.2,0);
let dragging=false, lastX=0, lastY=0;

function setPointerFromEvent(e){
  const r=canvas.getBoundingClientRect();
  pointerNDC.x=((e.clientX-r.left)/r.width)*2-1;
  pointerNDC.y=-((e.clientY-r.top)/r.height)*2+1;
  lastUserMove=performance.now();
  manualPointer=true;
}
canvas.addEventListener('pointermove',e=>{
  setPointerFromEvent(e);
  if(dragging){
    camState.tTheta -= (e.clientX-lastX)*0.005;
    camState.tPhi = clamp(camState.tPhi - (e.clientY-lastY)*0.004, 0.85, 1.62);
    lastX=e.clientX; lastY=e.clientY;
  }
});
canvas.addEventListener('pointerdown',e=>{
  dragging=true; lastX=e.clientX; lastY=e.clientY; canvas.setPointerCapture(e.pointerId);
  if(!started) startDemo();
});
window.addEventListener('pointerup',()=>{dragging=false;});
canvas.addEventListener('wheel',e=>{
  e.preventDefault();
  camState.tRadius=clamp(camState.tRadius+e.deltaY*0.012, 8, 32);
},{passive:false});

const overlay=document.getElementById('overlay');
function startDemo(){ started=true; overlay.classList.add('gone'); }
window.addEventListener('keydown',e=>{
  if(!started) startDemo();
  if(e.key===' '||e.key==='f'||e.key==='F'){ feedAtPointer(); }
});
canvas.addEventListener('click',()=>{ if(started) feedAtPointer(); });

/* ----------------------------------------------------------------- feed */
function feedAtPointer(){
  raycaster.setFromCamera(pointerNDC,camera);
  const plane=new THREE.Plane(new THREE.Vector3(0,0,1),0);
  const p=new THREE.Vector3();
  if(raycaster.ray.intersectPlane(plane,p)){
    p.x=clamp(p.x,BOUNDS.x0,BOUNDS.x1); p.z=clamp(p.z,BOUNDS.z0,BOUNDS.z1);
    p.y=clamp(p.y,1.0,3.4);
    dropFood(7,p);
  } else dropFood(7,null);
}
document.getElementById('feed').addEventListener('click',e=>{e.stopPropagation();feedAtPointer();});
document.getElementById('fishCount').addEventListener('input',e=>setPopulation(+e.target.value));
document.getElementById('fear').addEventListener('input',e=>{
  fearRadius=+e.target.value; document.getElementById('fearVal').textContent=fearRadius.toFixed(1)+'m';
});
let bubblesOn=true;
document.getElementById('bubbles').addEventListener('click',e=>{
  e.stopPropagation(); bubblesOn=!bubblesOn; bubbles.visible=bubblesOn;
  e.target.textContent=bubblesOn?'Bubbles on':'Bubbles off';
});

/* ------------------------------------------------------------ simulation */
const tmpA=new THREE.Vector3(), tmpB=new THREE.Vector3(), tmpC=new THREE.Vector3(), closest=new THREE.Vector3();
const dummyObj=new THREE.Object3D();
const qRoll=new THREE.Quaternion(); const zAxis=new THREE.Vector3(0,0,1);
let fed=0, scaredCount=0;

function updateFish(dt,t){
  scaredCount=0;
  raycaster.setFromCamera(pointerNDC,camera);
  const ray=raycaster.ray;
  for(let fi=0;fi<fishes.length;fi++){
    const f=fishes[fi];
    const acc=tmpA.set(0,0,0);

    /* wander */
    f.timer-=dt;
    if(f.timer<=0){ f.target.set(rand(BOUNDS.x0,BOUNDS.x1),rand(BOUNDS.y0,BOUNDS.y1),rand(BOUNDS.z0,BOUNDS.z1)); f.timer=rand(2.5,6); }
    tmpB.copy(f.target).sub(f.pos);
    const d2=tmpB.length();
    if(d2>0.01){ tmpB.divideScalar(d2); acc.addScaledVector(tmpB,1.45); }

    /* separation */
    tmpC.set(0,0,0);
    for(let oi=0;oi<fishes.length;oi++){
      if(oi===fi) continue;
      const o=fishes[oi];
      const dx=f.pos.x-o.pos.x, dy=f.pos.y-o.pos.y, dz=f.pos.z-o.pos.z;
      const dd=Math.sqrt(dx*dx+dy*dy+dz*dz);
      if(dd<1.15 && dd>0.0001){ const k=(1.15-dd)/1.15; tmpC.x+=dx/dd*k*2.6; tmpC.y+=dy/dd*k*2.6; tmpC.z+=dz/dd*k*2.6; }
    }
    acc.add(tmpC);

    /* --- flee from the cursor --- */
    f.fear=0;
    const toF=tmpB.copy(f.pos).sub(ray.origin);
    let tt=ray.direction.dot(toF); if(tt<0.2) tt=0.2;
    closest.copy(ray.direction).multiplyScalar(tt).add(ray.origin);
    const dist=f.pos.distanceTo(closest);
    if(dist<fearRadius){
      const w=(1-dist/fearRadius);
      f.fear=w; scaredCount++;
      const dir=tmpC.copy(f.pos).sub(closest);
      if(dir.lengthSq()<1e-8) dir.copy(ray.direction).negate();
      dir.normalize();
      acc.addScaledVector(dir, w*30);
      // slight upward panic
      acc.y += w*1.4;
    }

    /* food attraction */
    let best=-1, bestD=1e9;
    for(let k=0;k<foods.length;k++){
      const fd=f.pos.distanceTo(foods[k].m.position);
      if(fd<bestD){bestD=fd;best=k;}
    }
    if(best>=0 && bestD<7.5){
      tmpC.copy(foods[best].m.position).sub(f.pos);
      const l=tmpC.length();
      if(l>0.01){ tmpC.divideScalar(l); acc.addScaledVector(tmpC, 4.4); }
      if(bestD<0.32){
        const fm=foods[best];
        scene.remove(fm.m); foods.splice(best,1); fed++;
        document.getElementById('stFed').textContent=fed;
      }
    }

    /* containment */
    const B=BOUNDS;
    if(f.pos.x<B.x0) acc.x+=(B.x0-f.pos.x)*9;
    if(f.pos.x>B.x1) acc.x+=(B.x1-f.pos.x)*9;
    if(f.pos.y<B.y0) acc.y+=(B.y0-f.pos.y)*11;
    if(f.pos.y>B.y1) acc.y+=(B.y1-f.pos.y)*11;
    if(f.pos.z<B.z0) acc.z+=(B.z0-f.pos.z)*9;
    if(f.pos.z>B.z1) acc.z+=(B.z1-f.pos.z)*9;

    acc.clampLength(0,42);
    f.vel.addScaledVector(acc,dt);
    const maxSp=1.35+f.fear*5.2+f.sp.size*0.35;
    const minSp=0.55;
    const sp=f.vel.length();
    if(sp>maxSp) f.vel.multiplyScalar(maxSp/sp);
    if(sp<minSp && sp>0.001) f.vel.multiplyScalar(minSp/sp);
    f.pos.addScaledVector(f.vel,dt);

    /* orientation */
    dummyObj.position.copy(f.pos);
    dummyObj.lookAt(f.pos.x+f.vel.x, f.pos.y+f.vel.y, f.pos.z+f.vel.z);
    f.g.quaternion.slerp(dummyObj.quaternion, Math.min(1,dt*4.5));
    /* banking */
    const lateral=tmpB.set(0,1,0).cross(f.vel).dot(tmpC.set(f.vel.x,0,f.vel.z).normalize());
    qRoll.setFromAxisAngle(zAxis, clamp(-lateral*0.25,-0.35,0.35));
    f.g.quaternion.multiply(qRoll);

    /* fin animation */
    const spd=f.vel.length();
    f.tail.rotation.y=Math.sin(t*(5.0+spd*4.0)+f.phase)*0.5*(0.55+spd*0.22);
    f.body.rotation.y=Math.sin(t*(3.0+spd*2.2)+f.phase)*0.07;
    f.pect.rotation.x=Math.sin(t*7.0+f.phase)*0.35;
    f.pect2.rotation.x=Math.sin(t*7.0+f.phase+1.0)*0.3;
  }
}

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

  /* demo cursor when the user is idle */
  const idle=(now-lastUserMove)>2200;
  if(idle){
    manualPointer=false;
    pointerNDC.set(Math.sin(elapsed*0.52)*0.66, Math.sin(elapsed*0.33+1.2)*0.42);
  }
  document.getElementById('modeTag').textContent = manualPointer ? 'MANUAL CURSOR' : 'DEMO CURSOR';
  document.getElementById('stScared').textContent = scaredCount;

  /* camera */
  camState.theta+=(camState.tTheta-camState.theta)*Math.min(1,dt*6);
  camState.phi+=(camState.tPhi-camState.phi)*Math.min(1,dt*6);
  camState.radius+=(camState.tRadius-camState.radius)*Math.min(1,dt*6);
  camera.position.set(
    camTarget.x + camState.radius*Math.sin(camState.phi)*Math.sin(camState.theta),
    camTarget.y + camState.radius*Math.cos(camState.phi),
    camTarget.z + camState.radius*Math.sin(camState.phi)*Math.cos(camState.theta));
  camera.lookAt(camTarget);

  updateFish(dt, elapsed);

  /* seaweed sway */
  for(const w of weeds){
    const p=w.geo.attributes.position;
    for(let i=0;i<p.count;i++){
      const y=p.getY(i);
      const k=y/w.h;
      p.setX(i, p.getX(i)*0.0 + Math.sin(elapsed*1.3 + w.phase + k*2.4)*0.30*k*k);
      p.setZ(i, Math.cos(elapsed*1.0 + w.phase*1.4 + k*2.0)*0.22*k*k);
    }
    p.needsUpdate=true; w.geo.computeVertexNormals();
  }

  /* bubbles */
  if(bubblesOn){
    for(let i=0;i<BUB;i++){
      const b=bState[i];
      b.y+=b.sp*dt*1.2;
      b.x+=Math.sin(elapsed*1.6+b.ph)*0.004;
      b.z+=Math.cos(elapsed*1.3+b.ph)*0.004;
      if(b.y>T.h/2-0.25){ b.y=FLOOR+0.2; b.x=rand(-T.w/2+0.5,T.w/2-0.5); b.z=rand(-T.d/2+0.5,T.d/2-0.5); }
      dummy.position.set(b.x,b.y,b.z);
      const s=b.r*(0.7+0.3*Math.sin(elapsed*3+b.ph));
      dummy.scale.setScalar(s);
      dummy.updateMatrix();
      bubbles.setMatrixAt(i,dummy.matrix);
    }
    bubbles.instanceMatrix.needsUpdate=true;
  }

  /* plankton drift */
  for(let i=0;i<PLANK;i++){
    const s=pSeed[i];
    pPos[i*3]+=Math.sin(elapsed*0.5+s.ph)*0.0016;
    pPos[i*3+1]+=s.dy*dt*0.35;
    pPos[i*3+2]+=Math.cos(elapsed*0.4+s.ph)*0.0016;
    if(pPos[i*3+1]>T.h/2-0.5) pPos[i*3+1]=FLOOR+0.2;
  }
  plankGeo.attributes.position.needsUpdate=true;

  /* food */
  for(let i=foods.length-1;i>=0;i--){
    const fm=foods[i];
    fm.life-=dt;
    fm.vel.y-=0.16*dt;
    fm.m.position.addScaledVector(fm.vel,dt);
    fm.m.rotation.x+=dt*2; fm.m.rotation.z+=dt*1.4;
    if(fm.m.position.y<FLOOR+0.1){ fm.m.position.y=FLOOR+0.1; fm.vel.set(0,0,0); }
    if(fm.life<=0){ scene.remove(fm.m); foods.splice(i,1); }
  }

  /* light shafts */
  for(const s of shafts){
    s.m.material.opacity=s.base*(0.65+0.35*Math.sin(elapsed*0.5+s.phase));
    s.m.rotation.y=Math.sin(elapsed*0.15+s.phase)*0.25;
  }
  causticUniforms.uTime.value=elapsed;
  surfaceUniforms.uTime.value=elapsed;
  lampA.intensity=24+Math.sin(elapsed*1.7)*6;
  lampB.intensity=20+Math.cos(elapsed*1.3)*6;

  /* cursor marker in 3D */
  raycaster.setFromCamera(pointerNDC,camera);
  const plane=new THREE.Plane(new THREE.Vector3(0,0,1),0);
  const hit=new THREE.Vector3();
  if(raycaster.ray.intersectPlane(plane,hit)){
    hit.x=clamp(hit.x,-T.w/2,T.w/2); hit.z=clamp(hit.z,-T.d/2,T.d/2);
    hit.y=clamp(hit.y,FLOOR+0.4,T.h/2-0.2);
    marker.position.lerp(hit,Math.min(1,dt*12));
  }
  const pulse=1.5+Math.sin(elapsed*3.2)*0.14;
  marker.scale.set(pulse,pulse,1);

  composer.render();
}
animate();

window.addEventListener('resize',()=>{
  camera.aspect=innerWidth/innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth,innerHeight);
  composer.setSize(innerWidth,innerHeight);
});
</script>
</body>
</html>
Reasoning trace (11,224 tokens)
Let me build a nice 3D aquarium with Three.js. Features:
- Aquarium tank (glass box with water, sand, plants, rocks, bubbles)
- Fish that swim with boids-like behavior and flee from mouse when mouse is near (using raycast to 3D point)
- Mouse proximity detection: project fish position to screen, or better, raycast from mouse into the tank and compute distance to fish in 3D. Simpler: compute distance in screen space to mouse — flee if close on screen. But more 3D-correct: raycast against a plane at fish depth... I'll do both: compute a 3D "mouse point" by raycasting into the tank interior, and also screen-space distance. Let's do: for each fish, compute screen-space distance to mouse; if within radius, apply repulsion in 3D away from the camera-ray point.

Better approach: raycast mouse ray into tank; find closest point on ray to fish, compute perpendicular distance from ray; if within radius, flee vector = fish.position - closestPointOnRay. That gives true 3D flee. Combine with screen distance fallback.

Fish: procedurally generated geometry (body from lathe/sphere scaled, tail fin, dorsal fin). Use groups with simple sine tail animation.

Environment: sand floor with dunes, rocks, seaweed (swaying), corals, bubbles rising, caustics light, fog, god rays maybe. Keep performance fine.

Also add UI: title, hint text "Click or press a key to start", instructions, maybe a slider for fish count, toggle. Auto demo mode: mouse moves autonomously when idle (so fish flee on their own) — "the fish swim away when I move the mouse near them" plus a demo mode where an AI mouse moves around.

Let's write the code.

Structure:
- importmap
- renderer, scene, camera
- tank: glass walls via BoxGeometry with transparent material (MeshPhysicalMaterial transmission might be expensive but okay; maybe simpler: transparent standard material with low opacity + edges). Use transmission for glass? Might cause render cost. I'll use simple transparent material with opacity 0.08 and depthWrite false, plus wireframe edges with LineSegments for tank frame.
- water volume: large box with backside transparent blue material.
- sand: PlaneGeometry with vertex displacement (noise) - do simple random height via sin combos.
- rocks: IcosahedronGeometry deformed.
- plants: seaweed made of tube/plane strips swaying; corals: branching? Keep simple: use small sphere clusters and cone shapes.
- fish: build fish mesh function.
- bubbles: instanced spheres rising.
- particles for plankton: Points.
- lighting: hemisphere + directional (sun through water) + point lights.
- caustics: animated texture? Could use a canvas-generated caustic texture on the sand with additive blending and scrolling. Let's create a procedural caustic pattern via canvas drawing random blurred circles — or use a shader on sand with layered sine. Simpler: custom ShaderMaterial for sand with caustics computed in fragment shader using layered voronoi-ish noise functions. I'll do simple layered sin patterns.
- Post-processing bloom would be nice but requires addons EffectComposer — allowed via import map addons. Could add UnrealBloomPass for glow. That's fine; but keep perf. I'll include bloom with low strength.

Actually with transmission materials bloom can be weird. Let's skip bloom or use modest. I'll include it — nice look. Hmm, risk of errors. EffectComposer from three/addons/postprocessing/EffectComposer.js, RenderPass, UnrealBloomPass, OutputPass. These exist in 0.170. Let's do it carefully.

Mouse interaction: track pointer NDC. Raycast from camera. Compute flee.

Fish behavior: each fish has velocity, wander target, speed. Update: seek wander target, avoid walls (stay in box), separation from other fish (light), flee from mouse.

Also fish should orient along velocity: set quaternion via lookAt of position+velocity, with a slight bank.

Tail animation: rotate tail mesh around y based on phase, also body slight bend.

Let's build fish geometry: body = SphereGeometry scaled (0.35 length etc.) Actually fish shape: body ellipsoid (sphere scaled x=0.9? ), tail = cone/plane triangular. Use group:
- body: SphereGeometry(1, 16, 12) scaled (0.9, 0.55, 0.35)
- tail: a flat triangle ShapeGeometry attached at rear, rotating.
- dorsal fin: small triangle on top.
- eyes: two small spheres black + white.
Color variations: orange, yellow, blue, silver, striped clownfish-ish.

Add stripes by using a canvas texture? Could generate per-fish texture with stripes. Simpler: vary material color; maybe add a stripe mesh (torus ring around body) for clownfish.

Let's write a fish texture generator using canvas: gradient + stripes, used with MeshStandardMaterial map.

Number of fish ~ 14. Each ~ 200 triangles fine.

Bubbles: ~40 small spheres, transparent, rising with wobble, recycled.

Plankton particles: 300 points, drifting.

Camera: slight orbit auto drift; also allow drag to orbit? Mouse is used for flee, so keep camera mostly fixed with gentle drift. Provide optional drag to rotate camera via OrbitControls but OrbitControls conflicts with pointer; I'll implement custom: no camera controls, but subtle sway. Add a "shake" when fish flee? no.

UI overlay: title "Abyssal Reef Aquarium", subtitle, stats (fish count, cursor proximity indicator), control panel with slider for fish count? Keep simple but polished: a control panel with sliders (fish count, fear radius), buttons (feed food, toggle bubbles). "Feed" drops food pellets that fish chase — nice extra.

Also "Click or press a key to start" overlay that dismisses on interaction, and a demo AI mouse that moves automatically (wandering) until the user moves the real mouse.

Let me handle the start overlay: full-screen semi-transparent with big text; on click/keypress, hide overlay and start real interaction; before that, an auto "ghost cursor" moves around the tank so fish flee visibly in the demo.

Even after start, if user idle > 4s, resume auto cursor? Maybe keep a subtle auto drift pointer for demo, but the user's mouse overrides. I'll implement: pointer target = user mouse if user moved recently, else auto path.

Let's code.

Key details for no errors: use THREE from importmap 'three', addons from 'three/addons/'.

Caustics shader for sand: I'll write sand material as MeshStandardMaterial with onBeforeCompile adding caustics? Simpler to use custom ShaderMaterial with lighting approximated... Standard material with generated texture is simpler: create a canvas texture for sand (noise dots) and a second animated caustic overlay mesh just above the sand plane with additive blending and a scrolling procedural texture. I'll generate caustic texture on a canvas: draw many soft overlapping circles/strokes forming net pattern? A classic approach: generate a tileable texture using Worley-ish noise in JS. Let me do the shader approach for caustics overlay: ShaderMaterial with fragment computing layered voronoi caustic, transparent, additive. That's a nice look.

Let me write the caustic shader:

float hash(vec2 p){...}
vec2 hash2(vec2 p){...}
float voronoi(vec2 x){...}
caustic: use iterative pattern: c = pow(abs(sin(...)),...) Hmm.

Common caustic snippet:
```
float caustic(vec2 uv, float t){
  vec2 p = mod(uv*TAU, TAU) - 250.0;
  vec2 i = p; float c = 1.0; float inten = 0.005;
  for(int n=0;n<5;n++){
    float tt = t*(1.0-(3.5/float(n+1)));
    i = p + vec2(cos(tt - i.x)+sin(tt + i.y), sin(tt - i.y)+cos(tt + i.x));
    c += 1.0/length(vec2(p.x/(sin(i.x+tt)/inten), p.y/(cos(i.y+tt)/inten)));
  }
  c /= 5.0; c = 1.17-pow(c,1.4);
  return pow(abs(c), 8.0);
}
```
That's a known shader. Use with uv scaled and time. Apply to overlay plane with additive blending and opacity ~0.6, depthWrite false. Also apply similar on tank walls? Fine on sand only + maybe on rocks via standard material map (skip).

Also add "god rays" / light shafts: a few transparent cone/plane meshes with additive gradient going down from surface, gently animated. Nice touch: use a canvas gradient texture on planes with additive blending, always facing camera-ish. I'll add 3 light shaft planes.

Water surface: top of tank, a plane at y=height with a shader showing ripples and a brighter refracted look. Use a simple ShaderMaterial with wave normals computed and transparency. Keep it as a semi-transparent plane with animated specular via sin waves and fresnel-ish color.

OK, write code.

Tank dimensions: width 14, height 8, depth 7. Fish roam inside with margin.

Let me write the full HTML.

Fish mesh function:

```
function makeFish(colorScheme){
 const g = new THREE.Group();
 const body = new THREE.Mesh(sphereGeo, bodyMat);
 body.scale.set(0.62,0.34,0.26);
 ...
}
```
Body axis: let's have fish pointing +X? Then lookAt works with +Z forward by default for lookAt of Object3D (looks down -Z actually). Object3D.lookAt makes -Z? For non-camera objects, lookAt orients +Z toward target? Actually Object3D.lookAt rotates the object so its +Z axis points at the target (for cameras and lights it's -Z). Let me recall: `Object3D.lookAt` uses `m1.lookAt(eye, target, up)` where for cameras/lights it's position,target and for others target,position — so the object's +Z points toward the target. Yes: for regular objects, lookAt points +Z at the target.

So build fish facing +Z. Body: sphere scaled (0.26, 0.34, 0.62) => length along z. Tail at z=-0.62.

Tail fin: a flat triangle. Use ShapeGeometry in XY plane, positioned at z=-0.62, rotating around Y (swish). Use BufferGeometry with 3 vertices and DoubleSide material.

Dorsal fin: triangle in XY plane at top, slight tilt.
Pectoral fins: small triangles on sides, flapping.
Eyes: small spheres at z=+0.4, x=±0.18, y=0.08.

Use a canvas texture for body with stripes based on species.

Let me define species array: {name, color, accent, pattern}.

Texture generation:
```
function fishTexture(base, pattern){
  canvas 256x128; fill base gradient; if pattern==='stripe': draw vertical dark bands; if 'spot': random circles; if 'gradient': lighter belly.
}
```
Also add belly lighter gradient.

Materials: MeshStandardMaterial({map, roughness:0.35, metalness:0.15, emissive: base*0.1?}) Slight emissive helps them visible in dark water.

Fins material: transparent, same hue.

Fish update per frame:
- compute flee force from mouse.
- wander: target point changes occasionally; steer toward.
- separation from nearby fish (within 1.0) to avoid overlap.
- containment: clamp inside tank with soft push.
- speed limit; lerp velocity.
- position += velocity*dt; orient with quaternion slerp toward look direction (velocity direction) — use a dummy Object3D to compute quaternion.

Tail animation: tail.rotation.y = sin(time*speed + phase)*0.6; body slight yaw wobble.

Flee behavior: when the mouse is near, fish accelerate away, and maybe "startle" (speed burst).

Mouse near detection: use raycaster from camera with NDC pointer; ray into tank. For each fish, find closest point on ray: 
```
const ray = raycaster.ray;
// point on ray closest to fish: t = ray.direction.dot(fish.pos - ray.origin); clamp t>0; closest = origin + dir*t;
dist = closest.distanceTo(fish.pos)
```
If dist < fearRadius → flee vector = fish.pos - closest, normalized, weighted by (1 - dist/radius).

Also use screen-space distance as an alternative: project fish to NDC and compare with pointer NDC scaled by aspect — combined max. I'll compute both and use whichever gives larger repulsion (max weight). Screen distance: convert pointer NDC to pixels, project fish position, distance in pixels; radius in pixels ~ 220. Then flee direction should be in screen plane — using 3D ray approach handles it well. I'll combine: weight = max(rayWeight, screenWeight), direction from ray-based if rayWeight dominates else use screen-space direction in camera plane.

Simplify: Use both weights, direction = 3D ray-based direction (always valid). Screen weight adds when the pointer is close on screen even if ray misses the fish in depth. Good.

Also, a visual indicator: a glowing "cursor orb" in 3D at the ray intersection with the tank plane — showing where the "sonar" is. Plus ripples. Nice.

Feed food: click button or press F → drops pellets at the cursor's 3D point; fish seek food. Implement food as array of {pos, vel, life}; fish attraction to nearest food.

Extra: HUD panel with sliders. Keep it minimal and stylish.

Let me now write the code carefully.

Water fog: scene.fog = new THREE.FogExp2(0x0a2e4a, 0.035) — but that would fog the tank glass too. Fine, gives depth.

Background: gradient dark blue via scene.background = color; plus a large sphere with gradient shader? Use CSS behind transparent renderer? Renderer alpha true, body background radial gradient. That's simpler and looks good: canvas transparent, body has a deep-sea gradient. But bloom post-processing with alpha is messy. Let's set scene.background to a gradient texture generated on canvas (256x256 vertical gradient). Simple.

Bloom: I'll skip bloom to avoid complexity, but add emissive materials and additive glow sprites for bubbles. Actually bloom really helps underwater look. Let's include EffectComposer + RenderPass + UnrealBloomPass + OutputPass. With scene.background as texture, alpha not needed. Should work fine.

Note: OutputPass handles tone mapping + color space. Set renderer.toneMapping = ACESFilmic, outputColorSpace default SRGB. With composer, use OutputPass at end.

Careful: UnrealBloomPass with strength 0.35, radius 0.6, threshold 0.85.

Light shafts: additive planes, they will bloom nicely.

Performance: 1280x800, cap pixel ratio at 1.5-2.

Now write it.

Sand geometry: PlaneGeometry(W, D, 80, 40) rotated -PI/2, displaced with layered sin + random. Compute normals.

Rocks: icosahedron deformed with noise, random scales, gray-brown material.

Seaweed: build from a series of segments (ribbon geometry) using PlaneGeometry(0.25, h, 1, 8) with vertex sway in shader (custom onBeforeCompile) or CPU update. CPU update on ~8 plants with 9 verts each is cheap. I'll create ribbon with segments and update vertices each frame with a sway function. Let's make a "kelp" using a cylinder tube? Ribbon planes with double side look nice.

Simplify: create kelp as a group of tapered planes? I'll do: geometry = PlaneGeometry(width, height, 1, 10) with vertices shifted to curve; animate by updating position attribute each frame (bend by sin(time+ y*k)*amplitude*y/h). For 10 plants × 11 rows = 110 verts each → 1100 verts, fine.

Coral: branching using several cones/tubes — use small set of cylinders rotated. Or "brain coral" from sphere with noise + color. And fan coral: a shape geometry? Keep: sphere-based corals with emissive-ish colors and tube corals (cylinders with rounded tops).

Bubbles: InstancedMesh of sphere(0.06) with 60 instances; update each frame, wrap at top. Transparent material, transmission-ish: MeshPhysicalMaterial with transmission is expensive for instanced? Use MeshStandardMaterial with transparent opacity 0.35, roughness 0, metalness 0, envMap none. Fine. Or use simple sprite points. Instanced spheres ok.

Plankton: Points with small size, additive, slow drift.

Let's write.

Also glass: BoxGeometry(W,H,D) with MeshPhysicalMaterial({transmission:1, thickness:0.5, roughness:0.05, transparent:true, opacity:0.2?}) Transmission requires transmission render — cost. Use simple: MeshBasicMaterial? Instead: MeshPhysicalMaterial with transparent:true, opacity:0.12, roughness:0.1, metalness:0, side: DoubleSide? Use side: THREE.BackSide for inner walls tint. Let's create tank walls as separate planes with MeshStandardMaterial color 0x9fd8ff, transparent opacity 0.08, depthWrite false. Plus a frame of edges (BoxHelper-like) using EdgesGeometry LineSegments in light gray.

Water surface plane at y = H/2 - 0.1 with shader.

Also inner "water volume" fog: use scene.fog.

Camera position: (0, 2.2, 13) looking at (0,0,0). Tank W=16? Let's set W=16, H=9, D=8. Camera at z=16 to view whole thing. Let's compute: tank spans x -8..8, y -4.5..4.5. Camera at (0,1.5,15), fov 45 → visible height at distance 15 ≈ 2*15*tan(22.5)=12.4, width = 12.4*1.6=19.8. Good, tank fits.

Fish roam region: x ±6.5, y -3.2..3.2, z ±3.2.

Let me write the fish update with mouse "presence" 3D point: raycast to a plane z=0? The ray from camera hits various depths. I'll compute the closest point on ray per fish as described — good.

Additionally, "cursor orb" visual: place at intersection of ray with plane z=0 (or with the fish-depth average). Show a glowing ring sprite that pulses when fish flee.

Write HUD:
- top-left: title "REEF NOCTURNE — 3D Aquarium", subtitle "Move your cursor through the water — the fish flee."
- bottom-left: control card with sliders: "Fish population", "Fear radius"; buttons: "Feed", "Toggle bubbles", "Reset".
- bottom-right: hint "Drag to orbit"? We don't have orbit. Maybe add camera orbit with right-drag. I'll implement: pointer drag rotates camera azimuth slightly. But pointer move also drives flee... That's fine: camera azimuth controlled by dragging, and hover position drives flee. Could be confusing; but nice. Let's implement a gentle camera orbit: hold and drag anywhere rotates camera around the tank, and the flee uses the current camera raycast, so it still works. Add a note "Drag to orbit the tank".

Also scroll to zoom slightly.

Implement camera orbit manually with spherical coords (radius, theta, phi) and damping. Dragging updates target theta/phi; mouse wheel updates radius.

Pointerdown/move/up handling on the canvas.

Now demo auto-cursor: if no real pointer movement for 3 seconds after start, an auto cursor moves in a Lissajous path in NDC and drives the flee. Before start (overlay shown), auto cursor also active so fish are fleeing on first paint — good for the screenshot! Overlay message over the scene, semi transparent so the aquarium is visible behind. 

Let's make the overlay a card in the center with "Click or press any key to dive in" plus smaller text. But requirement: "Games must start in a self-playing demo mode behind a 'Click or press a key to start' message". So overlay present and demo running behind it. Good.

Let me write everything now.

Code for canvas textures.

fishTexture(species):
```
const c = document.createElement('canvas'); c.width=256;c.height=128;
const ctx=c.getContext('2d');
grad = linear gradient top->bottom: top = lighten(base, .25), bottom = darker
fill rect
if species.pattern==='bands': draw 3 vertical bands of accent color with soft edges
if 'spots': random circles
if 'stripe'...
Add subtle scale dots.
```
Use THREE.CanvasTexture, colorSpace = SRGBColorSpace.

Let me write helper mix colors using THREE.Color.

Now the main loop.

Fish class-ish object:
```
{
 group, body, tail, fins[], vel: Vector3, wander: Vector3, phase, speed, species, fear:0, scale
}
```

Update:
```
const acc = new THREE.Vector3();
// wander
if (f.wanderTimer<=0) { f.wander.set(rand(-1,1),rand(-0.4,0.4),rand(-1,1)).normalize().multiplyScalar(...) ; f.wanderTimer = 2+rand*3 }
// steer toward wander point offset from current position
target = f.pos + f.wanderDir * 3
```
Simplify: each fish has a wander target point inside the volume; re-pick when close or timer expires. Steering toward target with max force.

Separation: loop over all fish (14×14=196 per frame, fine).

Flee: compute.

Food attraction: if food list non-empty, find nearest food within 6 units → steer to it; when within 0.3, consume (remove) and fish "eats".

Integrate: vel += acc*dt; clamp speed between min and max (max increases when scared: fearSpeed). pos += vel*dt. Containment: if outside bounds, push back strongly.

Orientation: compute desired quaternion: dir = vel normalized; use dummy.position.copy(pos); dummy.lookAt(pos+dir) — but dummy.up = (0,1,0) default; ok. Then group.quaternion.slerp(dummy.quaternion, 0.15). Add roll/bank: after slerp, apply slight rotation around forward axis based on turning. Simpler: multiply by a small quaternion around Z axis (fish local forward is Z) using roll = clamp(-turnRate*0.3). Implement: qRoll.setFromAxisAngle(new Vector3(0,0,1), roll) and group.quaternion.multiply(qRoll).

Tail: tail.rotation.y = Math.sin(t*freq + phase) * 0.5 * (0.5+speed). Body wiggle: body.rotation.y? skip.

Fins flap: pectoral fins rotation.z = sin(...)*0.4.

Fear display: fish color slightly? Add scale pulse. Also emit small "startle" particles? Skip.

Let me handle the fear radius slider: 0.5–3.0 world units.

Screen-space fear: pointer NDC → pixels: px = (ndc.x*0.5+0.5)*width, py=(-ndc.y*0.5+0.5)*height. Projected fish pos → px. d = distance in px; radiusPx = fearRadius * (some scale) — using projected: a world distance at the fish's depth corresponds to px = worldDist * (height/(2*tan(fov/2)*distToCamera)). So radiusPx = fearRadius * projScale. Then screenWeight = 1 - d/radiusPx. Combine with ray weight = 1 - rayDist/fearRadius.

Compute projScale per fish: projScale = (H/2) / (tan(fov/2) * camDistToFish). 

So actually the screen-space radius equals the world radius at that depth, making both consistent. I'll just use the ray-based perpendicular distance which is the true 3D distance — simplest and correct. rayDist = distance from fish to the camera ray. If the fish is behind the camera... not possible.

But: if pointer is outside the tank, the ray still passes near fish → they flee. That's acceptable ("mouse near them"). But the ray is a line; a mouse far to the side but at screen edge still is near fish on screen → correct.

Use rayDist only. Good; plus also compute an "interest" if pointer over the canvas.

Also make the fear force only apply when the pointer has moved recently? No — if the pointer is stationary near fish, they'd be pushed away and stay away (since the force pushes them out of radius). That's fine and physically nice.

3D flee direction = (fishPos - closestPointOnRay).normalize(). But if the fish is nearly on the ray, direction is tiny → use camera-facing perpendicular fallback.

Also add: the fish should swim away and, after leaving the radius, resume normal behavior.

I'll compute closestPointOnRay via t = clamp(ray.direction.dot(fishPos - ray.origin), 0.1, 100).

Now the "sonar cursor" 3D marker: intersect ray with a plane at z = 0 (tank mid) → place a glowing ring there; plus a small point light? Add sprite with radial gradient texture, additive.

Also add ripple rings when fish flee? skip.

OK writing final code. I'll be thorough but careful.

Also need `renderer.setAnimationLoop`.

Let me write the HTML skeleton:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Reef Nocturne — 3D Aquarium</title>
<style> ... </style>
</head>
<body>
<div id="app"><canvas id="c"></canvas></div>
<div id="ui"> ... </div>
<div id="overlay"> ... </div>
<script type="importmap">...</script>
<script type="module"> ... </script>
</body>
</html>
```

UI styling: glassmorphism panels, cyan accents, font system stack.

Responsive: at width < 700, shrink panels, hide some.

Let's write the JS in detail.

```js
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
```

Setup renderer:
```js
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({canvas, antialias:true, powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
renderer.outputColorSpace = THREE.SRGBColorSpace;
```

Scene bg gradient texture:
```js
function bgTexture(){
 const c=document.createElement('canvas'); c.width=8;c.height=256;
 const g=c.getContext('2d').createLinearGradient(0,0,0,256);
 g.addColorStop(0,'#0d3a5c'); g.addColorStop(0.5,'#072538'); g.addColorStop(1,'#03121c');
 fill
 tex = new THREE.CanvasTexture(c); tex.colorSpace=SRGB; 
}
```
Use as scene.background with mapping? A plain texture background fills the screen stretched — that's fine (vertical gradient stretched horizontally).

Fog: THREE.FogExp2(0x06263c, 0.022). With camera at 15 units and tank at 10-ish depth, exp2 0.022 gives slight haze. ok.

Lights:
- HemisphereLight(0x9fd8ff, 0x1b3a2a, 0.55)
- DirectionalLight(0xcfeeff, 1.1) at (6,12,4) — sun from above
- PointLight(0x66ccff, 0.8) inside? Add a couple of colored point lights near corals for glow: magenta and cyan, intensity ~ 8 with distance 8 (physically correct lights in r155+ use candela; intensity in that units). Use PointLight(color, intensity=20, distance=12, decay=2).

Now let's write out the whole thing. I'll produce the final code.

Caustic overlay shader:

```glsl
uniform float uTime; uniform vec3 uColor; varying vec2 vUv;
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453123); }
vec2 hash2(vec2 p){ return fract(sin(vec2(dot(p,vec2(127.1,311.7)), dot(p,vec2(269.5,183.3))))*43758.5453); }
float noise(vec2 p){ vec2 i=floor(p), f=fract(p); f=f*f*(3.-2.*f);
 float a=hash(i),b=hash(i+vec2(1,0)),c=hash(i+vec2(0,1)),d=hash(i+vec2(1,1));
 return mix(mix(a,b,f.x),mix(c,d,f.x),f.y); }
void main(){
  vec2 uv = vUv*8.0;
  float t = uTime*0.35;
  float n = noise(uv + vec2(t, -t*0.6));
  n += noise(uv*1.7 - vec2(t*0.8, t*0.4))*0.6;
  n += noise(uv*3.1 + vec2(t*1.3, t))*0.3;
  n/=1.9;
  float c = pow(smoothstep(0.45,0.95,n), 2.0);
  gl_FragColor = vec4(uColor, c*0.55);
}
```
Good enough; use additive blending, depthWrite false.

Water surface shader:
```glsl
varying vec2 vUv; uniform float uTime;
void main(){
  vec2 p=vUv*vec2(30.,16.);
  float w = sin(p.x*0.8 + uTime*1.2)*0.5 + sin(p.y*1.3 - uTime*0.9)*0.5;
  vec3 col = mix(vec3(0.05,0.35,0.55), vec3(0.35,0.75,0.95), 0.5+0.5*w);
  float edge = smoothstep(0.0,0.08,vUv.x)*smoothstep(1.0,0.92,vUv.x)*smoothstep(0.0,0.08,vUv.y)*smoothstep(1.0,0.92,vUv.y);
  gl_FragColor = vec4(col, 0.35*edge + 0.1);
}
```
Also animated wave lines via caustic-ish.

Light shafts: PlaneGeometry with a canvas gradient texture (bright at top, fading down), additive, tilted, animated opacity.

Now write final code. Also the food pellets: small spheres falling; fish chase.

Feeding: click "Feed" button → spawn 8 pellets at random x near top, they sink with slight drift; fish seek them.

Actually more fun: pressing "Feed" drops pellets at the cursor location (3D point). If no cursor, random.

Let me now write out everything concretely.

I'll write the script with sections. Let me carefully produce the final file.

Extra: mobile - touch support: pointer events cover touch (pointerdown/move). The overlay says "Click or press a key to start" — on mobile tap works.

Also prevent default touch scrolling: body overflow hidden, touch-action:none on canvas.

Let me write.

```js
// ---------- utils
const rand=(a,b)=>a+Math.random()*(b-a);
const clamp=(v,a,b)=>Math.min(b,Math.max(a,v));
```

Species:
```js
const SPECIES=[
 {name:'Ember Tang', body:0xff7a29, belly:0xffd08a, pattern:'bands', accent:0xffffff, size:1.0},
 {name:'Sunburst', body:0xffd23f, belly:0xfff3c4, pattern:'grad', accent:0x3a2a00, size:0.85},
 {name:'Azure Chromis', body:0x35a7ff, belly:0xd9f2ff, pattern:'grad', accent:0x0b3c66, size:0.8},
 {name:'Reef Clown', body:0xff8c1a, belly:0xffe0b0, pattern:'bands3', accent:0xffffff, size:0.95},
 {name:'Pearl Danio', body:0xdfe9f2, belly:0xffffff, pattern:'stripe', accent:0x6ea8c9, size:0.7},
 {name:'Violet Bass', body:0xa06cff, belly:0xe6d6ff, pattern:'spots', accent:0x3d1f6b, size:1.05},
 {name:'Gold Scalar', body:0xffb703, belly:0xfff0b8, pattern:'grad', accent:0x6b4a00, size:0.9},
];
```

Texture gen per species (cache).

Fish build:
```js
function buildFish(sp, idx){
  const g=new THREE.Group();
  const tex=fishTexture(sp);
  const bodyMat=new THREE.MeshStandardMaterial({map:tex, roughness:0.35, metalness:0.2, emissive:new THREE.Color(sp.body).multiplyScalar(0.12)});
  const body=new THREE.Mesh(new THREE.SphereGeometry(1,20,14), bodyMat);
  body.scale.set(0.26,0.34,0.6);
  g.add(body);
  // tail
  const tailShape=new THREE.Shape();
  tailShape.moveTo(0,0); tailShape.lineTo(-0.02,0.3); ... 
```
Tail fin as a shape in XY plane: points (0,0), (-0.45,0.32), (-0.34,0), (-0.45,-0.32). Use THREE.Shape with lineTo, ShapeGeometry, material DoubleSide, color accent-ish.

Position tail at z=-0.58, so it swings behind.

Dorsal fin: shape triangle at top: (0.0,0) etc, positioned at y=0.3,z=0.05, rotated to lie along z? Make a fin shape in XZ-ish plane: use Shape in XY then rotate -PI/2 about X to lie in XZ plane, positioned at top of body.

Anal fin similar bottom.

Pectoral fins: two small shapes at x=±0.2, y=-0.05, z=0.2, rotate to be vertical-ish.

Eyes: sphere radius 0.055 black at (±0.16, 0.09, 0.42) plus tiny white highlight.

Maybe too many meshes per fish (7 meshes × 14 fish = 98 meshes + shadows off). Fine, no shadows.

Let me set group.scale to sp.size.

Fish object:
```js
{f: group, body, tail, pects:[], pos:new THREE.Vector3(), vel:new THREE.Vector3(), target:new THREE.Vector3(), phase:rand(0,6.28), fear:0, size:sp.size}
```

Update loop dt clamp.

Let me write the whole update:

```js
function updateFish(dt, t){
  const tmp=new THREE.Vector3(), sep=new THREE.Vector3(), acc=new THREE.Vector3();
  for(const f of fishes){
    acc.set(0,0,0);
    // wander
    f.timer-=dt;
    if(f.timer<=0){ f.target.set(rand(-6,6), rand(-3,3), rand(-3,3)); f.timer=rand(2,5); }
    tmp.copy(f.target).sub(f.pos);
    const dist=tmp.length();
    if(dist>0.001){ tmp.divideScalar(dist); acc.addScaledVector(tmp, 1.6); }
    // separation
    sep.set(0,0,0);
    for(const o of fishes){ if(o===f)continue; const d=f.pos.distanceTo(o.pos); if(d<1.1 && d>0.001){ sep.addScaledVector(tmp2.copy(f.pos).sub(o.pos).divideScalar(d), (1.1-d)/1.1*2.2);} }
    acc.add(sep);
    // mouse flee
    f.fear=0;
    if(pointerActive){
      raycaster.setFromCamera(pointerNDC, camera);
      const ray=raycaster.ray;
      const toF=tmp3.copy(f.pos).sub(ray.origin);
      let tt=ray.direction.dot(toF); tt=Math.max(tt,0.2);
      closest.copy(ray.direction).multiplyScalar(tt).add(ray.origin);
      const d=f.pos.distanceTo(closest);
      if(d<fearRadius){ const w=(1-d/fearRadius); f.fear=w; const dir=tmp3.copy(f.pos).sub(closest); if(dir.lengthSq()<1e-6) dir.copy(ray.direction).negate(); dir.normalize(); acc.addScaledVector(dir, w*26); }
    }
    // food
    ...
    // containment
    const b=BOUNDS;
    if(f.pos.x<b.x0) acc.x+=(b.x0-f.pos.x)*8; ...
    // integrate
    acc.clampLength(0,30);
    f.vel.addScaledVector(acc, dt);
    const maxSpeed = 1.6 + f.fear*4.5 + f.size*0.3;
    f.vel.clampLength(0.5, maxSpeed);
    f.pos.addScaledVector(f.vel, dt);
```
Wait, containment should be applied before integrate based on position; fine.

Orientation:
```js
    const speed=f.vel.length();
    dummy.position.copy(f.pos);
    dummy.lookAt(f.pos.x+f.vel.x, f.pos.y+f.vel.y, f.pos.z+f.vel.z);
    f.f.quaternion.slerp(dummy.quaternion, Math.min(1, dt*4));
```
Add roll: compute turn from cross of previous forward? Simplify: roll = clamp(-f.vel.x*0.06,-0.3,0.3) relative... skip; use lateral acceleration. I'll compute lateral = (vel dot camera right?) complicated. Skip banking.

Tail animation: f.tail.rotation.y = Math.sin(t*(6+speed*4)+f.phase)*0.45*(0.6+speed*0.25).

Also body bend: f.body.rotation.y = Math.sin(...)*0.06.

Pectoral: f.pect.rotation.x = Math.sin(t*8+phase)*0.4.

Ok.

Food: array of {mesh, pos, vel}. Update: pos.y -= 0.5*dt; wobble. Fish attraction: find nearest food within 8 → steer toward with weight 4; if distance<0.25 → remove food, fish "gulp" (scale pulse).

Now the "sonar cursor" marker: sprite with radial gradient texture, additive; position at ray∩plane z=0; scale pulses with proximity of nearest fish.

Implement: compute ray-plane intersection with z=0 plane; if no intersection (ray parallel), use distance 15.

Let me also add a subtle "shockwave" ring when a fish is very scared? Skip.

Now the demo auto-cursor: 
```js
let autoMode=true, lastUserMove=-Infinity;
pointermove handler sets pointerActive=true, lastUserMove=performance.now(), and sets pointerNDC.
In loop: if(now-lastUserMove>2500){ autoMode=true } else autoMode=false;
if(autoMode){ pointerNDC.set(Math.sin(t*0.6)*0.7, Math.sin(t*0.43)*0.45); }
```
Also pointerActive always true (auto generates pointer). So fish always flee from the auto cursor → demo action visible. But then when the user moves the mouse, real pointer takes over. Good. But maybe we want the auto cursor only when idle to not conflict... it's fine.

Wait: the requirement says fish flee when I move the mouse near them; the auto-cursor makes them flee in the demo too. Good.

But there's a subtlety: with auto cursor always active, fish never settle. That's fine, they also wander. Maybe reduce auto cursor speed and keep it wandering so they have moments of calm.

Let me set auto cursor to move slowly: pointerNDC = (sin(t*0.5)*0.65, sin(t*0.31+1)*0.4). Fish flee then recover.

Add a HUD indicator "CURSOR: DEMO / MANUAL".

Camera orbit: implement drag. pointerdown records; pointermove with drag → theta += dx*0.005, phi clamp. But pointermove also updates pointerNDC for flee — while dragging, that's fine.

Hmm: dragging moves the pointer and also rotates camera → ray changes → flee computed with new camera. Fine.

Also wheel → radius.

Camera spherical: radius default 16, theta 0, phi ~ PI/2 - 0.12 (slightly above). Target (0,0,0). Compute position.

Let's set camera fov 42.

Now write UI HTML:

```html
<div id="ui">
  <div class="brand">
    <div class="dot"></div>
    <div>
      <h1>REEF NOCTURNE</h1>
      <p>Interactive 3D Aquarium · sway the cursor, the fish scatter</p>
    </div>
  </div>
  <div class="panel" id="panel">
    <div class="row"><label>Population</label><input type="range" id="fishCount" min="4" max="24" value="14"><span id="fishCountVal">14</span></div>
    <div class="row"><label>Fear radius</label><input type="range" id="fear" min="0.6" max="3.5" step="0.1" value="1.8"><span id="fearVal">1.8m</span></div>
    <div class="btns"><button id="feed">🍤 Feed</button><button id="bubbles">Bubbles: on</button></div>
    <div class="hint">Drag to orbit · scroll to zoom</div>
  </div>
  <div class="stats" id="stats">...</div>
</div>
<div id="overlay"><div class="card"><h2>Click or press a key to start</h2><p>...</p></div></div>
```

The overlay should be dismissible and also the demo runs behind it.

Feed button: also give food at cursor.

Population slider: add/remove fish.

Let me write the code now in full. I'll be careful with syntax.

One risk: EffectComposer + UnrealBloomPass import paths in 0.170: 'three/addons/postprocessing/EffectComposer.js' — exists. OutputPass.js exists (added r152). Good.

Let me write out the final file.

For the bubbles InstancedMesh, update matrices each frame with dummy Object3D.

Bubbles spawn positions random within tank, rise to top, reset.

Plankton points: BufferGeometry with 400 positions, animate by updating positions each frame (400*3 floats, fine) or use a shader. I'll update in JS with slow drift.

Alright, final code.

Also tank glass panels: create 5 planes (back, left, right, front, top?) Use a BoxGeometry with MeshPhysicalMaterial transparent and side: BackSide to see interior tint? Let's do: 
- back wall: PlaneGeometry(W,H) at z=-D/2, material color 0x0a3b57 transparent opacity 0.9? Actually we want the scene visible through the front. Front glass: plane at z=+D/2, transparent opacity 0.06, plus a subtle specular streak. Use MeshPhysicalMaterial{transparent:true, opacity:0.08, roughness:0.05, metalness:0, side:DoubleSide, depthWrite:false}. Add edges frame.

I'll include: front glass (subtle), side walls subtle, back wall with a gradient blue (opaque-ish, since background behind), and top water surface.

Ok writing now. Let me produce the final answer.

Detailed code:

```js
import * as THREE from 'three';
import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
import {UnrealBloomPass} from 'three/addons/postprocessing/UnrealBloomPass.js';
import {OutputPass} from 'three/addons/postprocessing/OutputPass.js';

const canvas=document.getElementById('scene');
const renderer=new THREE.WebGLRenderer({canvas,antialias:true});
renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,2));
renderer.setSize(innerWidth,innerHeight);
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=1.15;
const scene=new THREE.Scene();
scene.background=bgTex();
scene.fog=new THREE.FogExp2(0x05202f,0.018);
const camera=new THREE.PerspectiveCamera(42,innerWidth/innerHeight,0.1,200);
```

TANK dims: W=16,H=9,D=8, floor y=-4.5.

Sand at y=-4.5.

Let me define const T={w:16,h:9,d:8}; floorY=-4.5.

Camera target (0,-0.2,0), radius 17, phi 1.42 (from +Y axis), theta 0 → position (0, r*cos(phi), r*sin(phi)) = (0, 17*0.15=2.5, 17*0.99=16.8). Good.

phi = 1.42 rad ≈ 81.4° from Y axis → slightly above horizontal. cos=0.1502.

Let me finalize.

Write the code fully now, then double-check for errors mentally.

Regarding `THREE.MathUtils.clamp` usage.

I'll write final answer.

For the stats: show fish count, "scared" count (fear>0.1), cursor mode.

Let me produce the file.

One more: "no horizontal scrollbar" — body overflow hidden, UI panels positioned with padding, on small screens stack.

Mobile: reduce fish count default? fine.

Final code below.

Careful with the fish shape geometry: THREE.ShapeGeometry needs shape in XY plane; material side DoubleSide.

Fin shape helper:
```js
function finGeo(points){ const s=new THREE.Shape(); s.moveTo(points[0][0],points[0][1]); for(let i=1;i<points.length;i++) s.lineTo(points[i][0],points[i][1]); s.closePath(); return new THREE.ShapeGeometry(s); }
```

Tail points: [[0,0],[-0.5,0.34],[-0.36,0],[-0.5,-0.34]] — a forked tail. Shape with lineTo sequence creates polygon; the "closePath" back to first. The polygon [[0,0],[-0.5,0.34],[-0.36,0],[-0.5,-0.34]] is a bowtie-ish; triangulation might be weird but ShapeGeometry uses earcut—concave shapes handled ok. Good.

Tail position z=-0.55, rotation.y animated (swish around vertical axis) — the tail plane is XY, so rotating around Y sweeps it side to side. Good.

Dorsal fin: shape [[-0.22,0],[0.22,0],[0,0.34]] in XY, then rotate about X by -PI/2 to lay it in XZ? If rotated -90° about X, the Y-extent becomes -Z extent (backwards). We want the fin to extend upward (+Y) from the body and be thin in X. So keep it in XY plane (thin in Z)? That would make the fin a vertical sheet along the body length (thin in Z) — actually the dorsal fin should be a sheet whose plane contains the body axis (Z) and up (Y), thin in X. So build shape in ZY plane: points (z,y): [[0.3,0],[-0.25,0],[-0.1,0.3]] then rotate about Y by PI/2 to map X→? Let's just build the shape in XY with x representing z-length, then rotate the mesh by rotateY(Math.PI/2) so local X maps to -Z... Let's define shape points as (z,y) pairs and set mesh.rotation.y = Math.PI/2, then local x-axis maps to world -z? Rotating by +90° about Y maps local +X → world -Z? Rotation about Y by angle a: x' = x cos a + z sin a; z' = -x sin a + z cos a. For a=PI/2: local (1,0,0) → world (0,0,-1). So local +X maps to world -Z. So shape x = -z_world. I'll just set rotation.y = -Math.PI/2: local (1,0,0) → world (0,0,1)=+Z. Check: a=-PI/2 → x'=x cos(-90)+z sin(-90)= -z ; z' = -x sin(-90)+z cos(-90) = x. So local +X → world +Z. Yes rotation.y=-PI/2.

So dorsal fin shape in local XY where x = forward (z world), y = up. Points: x from -0.3 (tailward) to +0.25 (noseward)? Dorsal fin is mid-back: shape [[-0.3,0],[0.25,0],[0.0,0.32]] with x=forward. Place at y = 0.30 (top of body), z=0. Use rotation.y=-PI/2 and position (0,0.30,0). The shape local x→world z, so the shape spans z from -0.3 to 0.25. Good.

Pectoral fin: small shape [[0,0],[0.18,-0.1],[0.16,0.12]] in local XY; place at (±0.19,-0.06,0.18), rotate to be vertical plane along z? Just orient roughly flat-ish angled; animate rotation.z.

Simplify: pectoral as shape in XY plane, positioned at x=±0.2, rotated y = ±PI/2 so it lies in the ZY plane (vertical, thin in X) — but real pectoral fins stick out sideways. Whatever, visually fine: rotate around z-axis for flapping.

I'll do: fin mesh rotation.y = ±(Math.PI/2 - 0.5), rotation.z animated for flap. Fine.

Let me write the final code.

Eye highlight: skip.

Now, write.

Also each fish should cast a subtle shadow on the sand? Skip shadows for perf, but a fake blob shadow: a dark ellipse sprite under each fish on the sand. Could be nice but adds complexity; skip. Actually contact shadows help ground the scene for the static corals/rocks, but those are static so skip.

Write final.

I need to make sure the module script runs after DOM. Put script at end of body.

Final answer below.